durabletask 1.1.0.dev4__tar.gz → 1.1.0.dev6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/PKG-INFO +1 -1
  2. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/internal/helpers.py +17 -0
  3. durabletask-1.1.0.dev6/durabletask/internal/proto_task_hub_sidecar_service_stub.py +33 -0
  4. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/worker.py +94 -32
  5. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask.egg-info/PKG-INFO +1 -1
  6. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask.egg-info/SOURCES.txt +1 -0
  7. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/pyproject.toml +1 -1
  8. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/LICENSE +0 -0
  9. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/README.md +0 -0
  10. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/__init__.py +0 -0
  11. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/client.py +0 -0
  12. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/entities/__init__.py +0 -0
  13. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/entities/durable_entity.py +0 -0
  14. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/entities/entity_context.py +0 -0
  15. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/entities/entity_instance_id.py +0 -0
  16. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/entities/entity_lock.py +0 -0
  17. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/entities/entity_metadata.py +0 -0
  18. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/internal/entity_state_shim.py +0 -0
  19. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/internal/exceptions.py +0 -0
  20. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/internal/grpc_interceptor.py +0 -0
  21. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/internal/orchestration_entity_context.py +0 -0
  22. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/internal/orchestrator_service_pb2.py +0 -0
  23. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/internal/orchestrator_service_pb2.pyi +0 -0
  24. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/internal/orchestrator_service_pb2_grpc.py +0 -0
  25. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/internal/shared.py +0 -0
  26. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/py.typed +0 -0
  27. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask/task.py +0 -0
  28. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask.egg-info/dependency_links.txt +0 -0
  29. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask.egg-info/requires.txt +0 -0
  30. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/durabletask.egg-info/top_level.txt +0 -0
  31. {durabletask-1.1.0.dev4 → durabletask-1.1.0.dev6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durabletask
3
- Version: 1.1.0.dev4
3
+ Version: 1.1.0.dev6
4
4
  Summary: A Durable Task Client SDK for Python
5
5
  License: MIT License
6
6
 
@@ -20,6 +20,11 @@ def new_orchestrator_started_event(timestamp: Optional[datetime] = None) -> pb.H
20
20
  return pb.HistoryEvent(eventId=-1, timestamp=ts, orchestratorStarted=pb.OrchestratorStartedEvent())
21
21
 
22
22
 
23
+ def new_orchestrator_completed_event() -> pb.HistoryEvent:
24
+ return pb.HistoryEvent(eventId=-1, timestamp=timestamp_pb2.Timestamp(),
25
+ orchestratorCompleted=pb.OrchestratorCompletedEvent())
26
+
27
+
23
28
  def new_execution_started_event(name: str, instance_id: str, encoded_input: Optional[str] = None,
24
29
  tags: Optional[dict[str, str]] = None) -> pb.HistoryEvent:
25
30
  return pb.HistoryEvent(
@@ -119,6 +124,18 @@ def new_failure_details(ex: Exception) -> pb.TaskFailureDetails:
119
124
  )
120
125
 
121
126
 
127
+ def new_event_sent_event(event_id: int, instance_id: str, input: str):
128
+ return pb.HistoryEvent(
129
+ eventId=event_id,
130
+ timestamp=timestamp_pb2.Timestamp(),
131
+ eventSent=pb.EventSentEvent(
132
+ name="",
133
+ input=get_string_value(input),
134
+ instanceId=instance_id
135
+ )
136
+ )
137
+
138
+
122
139
  def new_event_raised_event(name: str, encoded_input: Optional[str] = None) -> pb.HistoryEvent:
123
140
  return pb.HistoryEvent(
124
141
  eventId=-1,
@@ -0,0 +1,33 @@
1
+ from typing import Any, Callable, Protocol
2
+
3
+
4
+ class ProtoTaskHubSidecarServiceStub(Protocol):
5
+ """A stub class matching the TaskHubSidecarServiceStub generated from the .proto file.
6
+ Allows the use of TaskHubGrpcWorker methods when a real sidecar stub is not available.
7
+ """
8
+ Hello: Callable[..., Any]
9
+ StartInstance: Callable[..., Any]
10
+ GetInstance: Callable[..., Any]
11
+ RewindInstance: Callable[..., Any]
12
+ WaitForInstanceStart: Callable[..., Any]
13
+ WaitForInstanceCompletion: Callable[..., Any]
14
+ RaiseEvent: Callable[..., Any]
15
+ TerminateInstance: Callable[..., Any]
16
+ SuspendInstance: Callable[..., Any]
17
+ ResumeInstance: Callable[..., Any]
18
+ QueryInstances: Callable[..., Any]
19
+ PurgeInstances: Callable[..., Any]
20
+ GetWorkItems: Callable[..., Any]
21
+ CompleteActivityTask: Callable[..., Any]
22
+ CompleteOrchestratorTask: Callable[..., Any]
23
+ CompleteEntityTask: Callable[..., Any]
24
+ StreamInstanceHistory: Callable[..., Any]
25
+ CreateTaskHub: Callable[..., Any]
26
+ DeleteTaskHub: Callable[..., Any]
27
+ SignalEntity: Callable[..., Any]
28
+ GetEntity: Callable[..., Any]
29
+ QueryEntities: Callable[..., Any]
30
+ CleanEntityStorage: Callable[..., Any]
31
+ AbandonTaskActivityWorkItem: Callable[..., Any]
32
+ AbandonTaskOrchestratorWorkItem: Callable[..., Any]
33
+ AbandonTaskEntityWorkItem: Callable[..., Any]
@@ -12,7 +12,7 @@ from datetime import datetime, timedelta, timezone
12
12
  from threading import Event, Thread
13
13
  from types import GeneratorType
14
14
  from enum import Enum
15
- from typing import Any, Generator, Optional, Sequence, TypeVar, Union
15
+ from typing import Any, Generator, Optional, Sequence, Tuple, TypeVar, Union
16
16
  import uuid
17
17
  from packaging.version import InvalidVersion, parse
18
18
 
@@ -24,6 +24,7 @@ from durabletask.internal.entity_state_shim import StateShim
24
24
  from durabletask.internal.helpers import new_timestamp
25
25
  from durabletask.entities import DurableEntity, EntityLock, EntityInstanceId, EntityContext
26
26
  from durabletask.internal.orchestration_entity_context import OrchestrationEntityContext
27
+ from durabletask.internal.proto_task_hub_sidecar_service_stub import ProtoTaskHubSidecarServiceStub
27
28
  import durabletask.internal.helpers as ph
28
29
  import durabletask.internal.exceptions as pe
29
30
  import durabletask.internal.orchestrator_service_pb2 as pb
@@ -631,7 +632,7 @@ class TaskHubGrpcWorker:
631
632
  def _execute_orchestrator(
632
633
  self,
633
634
  req: pb.OrchestratorRequest,
634
- stub: stubs.TaskHubSidecarServiceStub,
635
+ stub: Union[stubs.TaskHubSidecarServiceStub, ProtoTaskHubSidecarServiceStub],
635
636
  completionToken,
636
637
  ):
637
638
  try:
@@ -679,7 +680,7 @@ class TaskHubGrpcWorker:
679
680
  def _cancel_orchestrator(
680
681
  self,
681
682
  req: pb.OrchestratorRequest,
682
- stub: stubs.TaskHubSidecarServiceStub,
683
+ stub: Union[stubs.TaskHubSidecarServiceStub, ProtoTaskHubSidecarServiceStub],
683
684
  completionToken,
684
685
  ):
685
686
  stub.AbandonTaskOrchestratorWorkItem(
@@ -692,7 +693,7 @@ class TaskHubGrpcWorker:
692
693
  def _execute_activity(
693
694
  self,
694
695
  req: pb.ActivityRequest,
695
- stub: stubs.TaskHubSidecarServiceStub,
696
+ stub: Union[stubs.TaskHubSidecarServiceStub, ProtoTaskHubSidecarServiceStub],
696
697
  completionToken,
697
698
  ):
698
699
  instance_id = req.orchestrationInstance.instanceId
@@ -725,7 +726,7 @@ class TaskHubGrpcWorker:
725
726
  def _cancel_activity(
726
727
  self,
727
728
  req: pb.ActivityRequest,
728
- stub: stubs.TaskHubSidecarServiceStub,
729
+ stub: Union[stubs.TaskHubSidecarServiceStub, ProtoTaskHubSidecarServiceStub],
729
730
  completionToken,
730
731
  ):
731
732
  stub.AbandonTaskActivityWorkItem(
@@ -738,7 +739,7 @@ class TaskHubGrpcWorker:
738
739
  def _execute_entity_batch(
739
740
  self,
740
741
  req: Union[pb.EntityBatchRequest, pb.EntityRequest],
741
- stub: stubs.TaskHubSidecarServiceStub,
742
+ stub: Union[stubs.TaskHubSidecarServiceStub, ProtoTaskHubSidecarServiceStub],
742
743
  completionToken,
743
744
  ):
744
745
  if isinstance(req, pb.EntityRequest):
@@ -807,7 +808,7 @@ class TaskHubGrpcWorker:
807
808
  def _cancel_entity_batch(
808
809
  self,
809
810
  req: Union[pb.EntityBatchRequest, pb.EntityRequest],
810
- stub: stubs.TaskHubSidecarServiceStub,
811
+ stub: Union[stubs.TaskHubSidecarServiceStub, ProtoTaskHubSidecarServiceStub],
811
812
  completionToken,
812
813
  ):
813
814
  stub.AbandonTaskEntityWorkItem(
@@ -831,6 +832,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
831
832
  self._pending_tasks: dict[int, task.CompletableTask] = {}
832
833
  # Maps entity ID to task ID
833
834
  self._entity_task_id_map: dict[str, tuple[EntityInstanceId, int]] = {}
835
+ self._entity_lock_task_id_map: dict[str, tuple[EntityInstanceId, int]] = {}
834
836
  # Maps criticalSectionId to task ID
835
837
  self._entity_lock_id_map: dict[str, int] = {}
836
838
  self._sequence_number = 0
@@ -1605,33 +1607,40 @@ class _OrchestrationExecutor:
1605
1607
  else:
1606
1608
  raise TypeError("Unexpected sub-orchestration task type")
1607
1609
  elif event.HasField("eventRaised"):
1608
- # event names are case-insensitive
1609
- event_name = event.eventRaised.name.casefold()
1610
- if not ctx.is_replaying:
1611
- self._logger.info(f"{ctx.instance_id} Event raised: {event_name}")
1612
- task_list = ctx._pending_events.get(event_name, None)
1613
- decoded_result: Optional[Any] = None
1614
- if task_list:
1615
- event_task = task_list.pop(0)
1616
- if not ph.is_empty(event.eventRaised.input):
1617
- decoded_result = shared.from_json(event.eventRaised.input.value)
1618
- event_task.complete(decoded_result)
1619
- if not task_list:
1620
- del ctx._pending_events[event_name]
1621
- ctx.resume()
1610
+ if event.eventRaised.name in ctx._entity_task_id_map:
1611
+ entity_id, task_id = ctx._entity_task_id_map.get(event.eventRaised.name, (None, None))
1612
+ self._handle_entity_event_raised(ctx, event, entity_id, task_id, False)
1613
+ elif event.eventRaised.name in ctx._entity_lock_task_id_map:
1614
+ entity_id, task_id = ctx._entity_lock_task_id_map.get(event.eventRaised.name, (None, None))
1615
+ self._handle_entity_event_raised(ctx, event, entity_id, task_id, True)
1622
1616
  else:
1623
- # buffer the event
1624
- event_list = ctx._received_events.get(event_name, None)
1625
- if not event_list:
1626
- event_list = []
1627
- ctx._received_events[event_name] = event_list
1628
- if not ph.is_empty(event.eventRaised.input):
1629
- decoded_result = shared.from_json(event.eventRaised.input.value)
1630
- event_list.append(decoded_result)
1617
+ # event names are case-insensitive
1618
+ event_name = event.eventRaised.name.casefold()
1631
1619
  if not ctx.is_replaying:
1632
- self._logger.info(
1633
- f"{ctx.instance_id}: Event '{event_name}' has been buffered as there are no tasks waiting for it."
1634
- )
1620
+ self._logger.info(f"{ctx.instance_id} Event raised: {event_name}")
1621
+ task_list = ctx._pending_events.get(event_name, None)
1622
+ decoded_result: Optional[Any] = None
1623
+ if task_list:
1624
+ event_task = task_list.pop(0)
1625
+ if not ph.is_empty(event.eventRaised.input):
1626
+ decoded_result = shared.from_json(event.eventRaised.input.value)
1627
+ event_task.complete(decoded_result)
1628
+ if not task_list:
1629
+ del ctx._pending_events[event_name]
1630
+ ctx.resume()
1631
+ else:
1632
+ # buffer the event
1633
+ event_list = ctx._received_events.get(event_name, None)
1634
+ if not event_list:
1635
+ event_list = []
1636
+ ctx._received_events[event_name] = event_list
1637
+ if not ph.is_empty(event.eventRaised.input):
1638
+ decoded_result = shared.from_json(event.eventRaised.input.value)
1639
+ event_list.append(decoded_result)
1640
+ if not ctx.is_replaying:
1641
+ self._logger.info(
1642
+ f"{ctx.instance_id}: Event '{event_name}' has been buffered as there are no tasks waiting for it."
1643
+ )
1635
1644
  elif event.HasField("executionSuspended"):
1636
1645
  if not self._is_suspended and not ctx.is_replaying:
1637
1646
  self._logger.info(f"{ctx.instance_id}: Execution suspended.")
@@ -1759,6 +1768,21 @@ class _OrchestrationExecutor:
1759
1768
  self._logger.info(f"{ctx.instance_id}: Entity operation failed.")
1760
1769
  self._logger.info(f"Data: {json.dumps(event.entityOperationFailed)}")
1761
1770
  pass
1771
+ elif event.HasField("orchestratorCompleted"):
1772
+ # Added in Functions only (for some reason) and does not affect orchestrator flow
1773
+ pass
1774
+ elif event.HasField("eventSent"):
1775
+ # Check if this eventSent corresponds to an entity operation call after being translated to the old
1776
+ # entity protocol by the Durable WebJobs extension. If so, treat this message similarly to
1777
+ # entityOperationCalled and remove the pending action. Also store the entity id and event id for later
1778
+ action = ctx._pending_actions.pop(event.eventId, None)
1779
+ if action and action.HasField("sendEntityMessage"):
1780
+ if action.sendEntityMessage.HasField("entityOperationCalled"):
1781
+ entity_id, event_id = self._parse_entity_event_sent_input(event)
1782
+ ctx._entity_task_id_map[event_id] = (entity_id, event.eventId)
1783
+ elif action.sendEntityMessage.HasField("entityLockRequested"):
1784
+ entity_id, event_id = self._parse_entity_event_sent_input(event)
1785
+ ctx._entity_lock_task_id_map[event_id] = (entity_id, event.eventId)
1762
1786
  else:
1763
1787
  eventType = event.WhichOneof("eventType")
1764
1788
  raise task.OrchestrationStateError(
@@ -1768,6 +1792,44 @@ class _OrchestrationExecutor:
1768
1792
  # The orchestrator generator function completed
1769
1793
  ctx.set_complete(generatorStopped.value, pb.ORCHESTRATION_STATUS_COMPLETED)
1770
1794
 
1795
+ def _parse_entity_event_sent_input(self, event: pb.HistoryEvent) -> Tuple[EntityInstanceId, str]:
1796
+ try:
1797
+ entity_id = EntityInstanceId.parse(event.eventSent.instanceId)
1798
+ except ValueError:
1799
+ raise RuntimeError(f"Could not parse entity ID from instanceId '{event.eventSent.instanceId}'")
1800
+ try:
1801
+ event_id = json.loads(event.eventSent.input.value)["id"]
1802
+ except (json.JSONDecodeError, KeyError, TypeError) as ex:
1803
+ raise RuntimeError(f"Could not parse event ID from eventSent input '{event.eventSent.input.value}'") from ex
1804
+ return entity_id, event_id
1805
+
1806
+ def _handle_entity_event_raised(self,
1807
+ ctx: _RuntimeOrchestrationContext,
1808
+ event: pb.HistoryEvent,
1809
+ entity_id: Optional[EntityInstanceId],
1810
+ task_id: Optional[int],
1811
+ is_lock_event: bool):
1812
+ # This eventRaised represents the result of an entity operation after being translated to the old
1813
+ # entity protocol by the Durable WebJobs extension
1814
+ if entity_id is None:
1815
+ raise RuntimeError(f"Could not retrieve entity ID for entity-related eventRaised with ID '{event.eventId}'")
1816
+ if task_id is None:
1817
+ raise RuntimeError(f"Could not retrieve task ID for entity-related eventRaised with ID '{event.eventId}'")
1818
+ entity_task = ctx._pending_tasks.pop(task_id, None)
1819
+ if not entity_task:
1820
+ raise RuntimeError(f"Could not retrieve entity task for entity-related eventRaised with ID '{event.eventId}'")
1821
+ result = None
1822
+ if not ph.is_empty(event.eventRaised.input):
1823
+ # TODO: Investigate why the event result is wrapped in a dict with "result" key
1824
+ result = shared.from_json(event.eventRaised.input.value)["result"]
1825
+ if is_lock_event:
1826
+ ctx._entity_context.complete_acquire(event.eventRaised.name)
1827
+ entity_task.complete(EntityLock(ctx))
1828
+ else:
1829
+ ctx._entity_context.recover_lock_after_call(entity_id)
1830
+ entity_task.complete(result)
1831
+ ctx.resume()
1832
+
1771
1833
  def evaluate_orchestration_versioning(self, versioning: Optional[VersioningOptions], orchestration_version: Optional[str]) -> Optional[pb.TaskFailureDetails]:
1772
1834
  if versioning is None:
1773
1835
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durabletask
3
- Version: 1.1.0.dev4
3
+ Version: 1.1.0.dev6
4
4
  Summary: A Durable Task Client SDK for Python
5
5
  License: MIT License
6
6
 
@@ -25,4 +25,5 @@ durabletask/internal/orchestration_entity_context.py
25
25
  durabletask/internal/orchestrator_service_pb2.py
26
26
  durabletask/internal/orchestrator_service_pb2.pyi
27
27
  durabletask/internal/orchestrator_service_pb2_grpc.py
28
+ durabletask/internal/proto_task_hub_sidecar_service_stub.py
28
29
  durabletask/internal/shared.py
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
9
9
 
10
10
  [project]
11
11
  name = "durabletask"
12
- version = "1.1.0.dev4"
12
+ version = "1.1.0.dev6"
13
13
  description = "A Durable Task Client SDK for Python"
14
14
  keywords = [
15
15
  "durable",