durabletask 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

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.

Potentially problematic release.


This version of durabletask might be problematic. Click here for more details.

durabletask/task.py CHANGED
@@ -35,6 +35,21 @@ class OrchestrationContext(ABC):
35
35
  """
36
36
  pass
37
37
 
38
+ @property
39
+ @abstractmethod
40
+ def version(self) -> Optional[str]:
41
+ """Get the version of the orchestration instance.
42
+
43
+ This version is set when the orchestration is scheduled and can be used
44
+ to determine which version of the orchestrator function is being executed.
45
+
46
+ Returns
47
+ -------
48
+ Optional[str]
49
+ The version of the orchestration instance, or None if not set.
50
+ """
51
+ pass
52
+
38
53
  @property
39
54
  @abstractmethod
40
55
  def current_utc_datetime(self) -> datetime:
@@ -100,7 +115,8 @@ class OrchestrationContext(ABC):
100
115
  @abstractmethod
101
116
  def call_activity(self, activity: Union[Activity[TInput, TOutput], str], *,
102
117
  input: Optional[TInput] = None,
103
- retry_policy: Optional[RetryPolicy] = None) -> Task[TOutput]:
118
+ retry_policy: Optional[RetryPolicy] = None,
119
+ tags: Optional[dict[str, str]] = None) -> Task[TOutput]:
104
120
  """Schedule an activity for execution.
105
121
 
106
122
  Parameters
@@ -111,6 +127,8 @@ class OrchestrationContext(ABC):
111
127
  The JSON-serializable input (or None) to pass to the activity.
112
128
  retry_policy: Optional[RetryPolicy]
113
129
  The retry policy to use for this activity call.
130
+ tags: Optional[dict[str, str]]
131
+ Optional tags to associate with the activity invocation.
114
132
 
115
133
  Returns
116
134
  -------
@@ -123,7 +141,8 @@ class OrchestrationContext(ABC):
123
141
  def call_sub_orchestrator(self, orchestrator: Orchestrator[TInput, TOutput], *,
124
142
  input: Optional[TInput] = None,
125
143
  instance_id: Optional[str] = None,
126
- retry_policy: Optional[RetryPolicy] = None) -> Task[TOutput]:
144
+ retry_policy: Optional[RetryPolicy] = None,
145
+ version: Optional[str] = None) -> Task[TOutput]:
127
146
  """Schedule sub-orchestrator function for execution.
128
147
 
129
148
  Parameters
durabletask/worker.py CHANGED
@@ -10,12 +10,15 @@ from concurrent.futures import ThreadPoolExecutor
10
10
  from datetime import datetime, timedelta
11
11
  from threading import Event, Thread
12
12
  from types import GeneratorType
13
+ from enum import Enum
13
14
  from typing import Any, Generator, Optional, Sequence, TypeVar, Union
15
+ from packaging.version import InvalidVersion, parse
14
16
 
15
17
  import grpc
16
18
  from google.protobuf import empty_pb2
17
19
 
18
20
  import durabletask.internal.helpers as ph
21
+ import durabletask.internal.exceptions as pe
19
22
  import durabletask.internal.orchestrator_service_pb2 as pb
20
23
  import durabletask.internal.orchestrator_service_pb2_grpc as stubs
21
24
  import durabletask.internal.shared as shared
@@ -72,9 +75,56 @@ class ConcurrencyOptions:
72
75
  )
73
76
 
74
77
 
78
+ class VersionMatchStrategy(Enum):
79
+ """Enumeration for version matching strategies."""
80
+
81
+ NONE = 1
82
+ STRICT = 2
83
+ CURRENT_OR_OLDER = 3
84
+
85
+
86
+ class VersionFailureStrategy(Enum):
87
+ """Enumeration for version failure strategies."""
88
+
89
+ REJECT = 1
90
+ FAIL = 2
91
+
92
+
93
+ class VersioningOptions:
94
+ """Configuration options for orchestrator and activity versioning.
95
+
96
+ This class provides options to control how versioning is handled for orchestrators
97
+ and activities, including whether to use the default version and how to compare versions.
98
+ """
99
+
100
+ version: Optional[str] = None
101
+ default_version: Optional[str] = None
102
+ match_strategy: Optional[VersionMatchStrategy] = None
103
+ failure_strategy: Optional[VersionFailureStrategy] = None
104
+
105
+ def __init__(self, version: Optional[str] = None,
106
+ default_version: Optional[str] = None,
107
+ match_strategy: Optional[VersionMatchStrategy] = None,
108
+ failure_strategy: Optional[VersionFailureStrategy] = None
109
+ ):
110
+ """Initialize versioning options.
111
+
112
+ Args:
113
+ version: The version of orchestrations that the worker can work on.
114
+ default_version: The default version that will be used for starting new orchestrations.
115
+ match_strategy: The versioning strategy for the Durable Task worker.
116
+ failure_strategy: The versioning failure strategy for the Durable Task worker.
117
+ """
118
+ self.version = version
119
+ self.default_version = default_version
120
+ self.match_strategy = match_strategy
121
+ self.failure_strategy = failure_strategy
122
+
123
+
75
124
  class _Registry:
76
125
  orchestrators: dict[str, task.Orchestrator]
77
126
  activities: dict[str, task.Activity]
127
+ versioning: Optional[VersioningOptions] = None
78
128
 
79
129
  def __init__(self):
80
130
  self.orchestrators = {}
@@ -279,6 +329,12 @@ class TaskHubGrpcWorker:
279
329
  )
280
330
  return self._registry.add_activity(fn)
281
331
 
332
+ def use_versioning(self, version: VersioningOptions) -> None:
333
+ """Initializes versioning options for sub-orchestrators and activities."""
334
+ if self._is_running:
335
+ raise RuntimeError("Cannot set default version while the worker is running.")
336
+ self._registry.versioning = version
337
+
282
338
  def start(self):
283
339
  """Starts the worker on a background thread and begins listening for work items."""
284
340
  if self._is_running:
@@ -513,6 +569,16 @@ class TaskHubGrpcWorker:
513
569
  customStatus=ph.get_string_value(result.encoded_custom_status),
514
570
  completionToken=completionToken,
515
571
  )
572
+ except pe.AbandonOrchestrationError:
573
+ self._logger.info(
574
+ f"Abandoning orchestration. InstanceId = '{req.instanceId}'. Completion token = '{completionToken}'"
575
+ )
576
+ stub.AbandonTaskOrchestratorWorkItem(
577
+ pb.AbandonOrchestrationTaskRequest(
578
+ completionToken=completionToken
579
+ )
580
+ )
581
+ return
516
582
  except Exception as ex:
517
583
  self._logger.exception(
518
584
  f"An error occurred while trying to execute instance '{req.instanceId}': {ex}"
@@ -574,7 +640,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
574
640
  _generator: Optional[Generator[task.Task, Any, Any]]
575
641
  _previous_task: Optional[task.Task]
576
642
 
577
- def __init__(self, instance_id: str):
643
+ def __init__(self, instance_id: str, registry: _Registry):
578
644
  self._generator = None
579
645
  self._is_replaying = True
580
646
  self._is_complete = False
@@ -584,6 +650,8 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
584
650
  self._sequence_number = 0
585
651
  self._current_utc_datetime = datetime(1000, 1, 1)
586
652
  self._instance_id = instance_id
653
+ self._registry = registry
654
+ self._version: Optional[str] = None
587
655
  self._completion_status: Optional[pb.OrchestrationStatus] = None
588
656
  self._received_events: dict[str, list[Any]] = {}
589
657
  self._pending_events: dict[str, list[task.CompletableTask]] = {}
@@ -646,7 +714,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
646
714
  )
647
715
  self._pending_actions[action.id] = action
648
716
 
649
- def set_failed(self, ex: Exception):
717
+ def set_failed(self, ex: Union[Exception, pb.TaskFailureDetails]):
650
718
  if self._is_complete:
651
719
  return
652
720
 
@@ -658,7 +726,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
658
726
  self.next_sequence_number(),
659
727
  pb.ORCHESTRATION_STATUS_FAILED,
660
728
  None,
661
- ph.new_failure_details(ex),
729
+ ph.new_failure_details(ex) if isinstance(ex, Exception) else ex,
662
730
  )
663
731
  self._pending_actions[action.id] = action
664
732
 
@@ -709,6 +777,10 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
709
777
  def instance_id(self) -> str:
710
778
  return self._instance_id
711
779
 
780
+ @property
781
+ def version(self) -> Optional[str]:
782
+ return self._version
783
+
712
784
  @property
713
785
  def current_utc_datetime(self) -> datetime:
714
786
  return self._current_utc_datetime
@@ -752,11 +824,12 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
752
824
  *,
753
825
  input: Optional[TInput] = None,
754
826
  retry_policy: Optional[task.RetryPolicy] = None,
827
+ tags: Optional[dict[str, str]] = None,
755
828
  ) -> task.Task[TOutput]:
756
829
  id = self.next_sequence_number()
757
830
 
758
831
  self.call_activity_function_helper(
759
- id, activity, input=input, retry_policy=retry_policy, is_sub_orch=False
832
+ id, activity, input=input, retry_policy=retry_policy, is_sub_orch=False, tags=tags
760
833
  )
761
834
  return self._pending_tasks.get(id, task.CompletableTask())
762
835
 
@@ -767,9 +840,12 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
767
840
  input: Optional[TInput] = None,
768
841
  instance_id: Optional[str] = None,
769
842
  retry_policy: Optional[task.RetryPolicy] = None,
843
+ version: Optional[str] = None,
770
844
  ) -> task.Task[TOutput]:
771
845
  id = self.next_sequence_number()
772
846
  orchestrator_name = task.get_name(orchestrator)
847
+ default_version = self._registry.versioning.default_version if self._registry.versioning else None
848
+ orchestrator_version = version if version else default_version
773
849
  self.call_activity_function_helper(
774
850
  id,
775
851
  orchestrator_name,
@@ -777,6 +853,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
777
853
  retry_policy=retry_policy,
778
854
  is_sub_orch=True,
779
855
  instance_id=instance_id,
856
+ version=orchestrator_version
780
857
  )
781
858
  return self._pending_tasks.get(id, task.CompletableTask())
782
859
 
@@ -787,9 +864,11 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
787
864
  *,
788
865
  input: Optional[TInput] = None,
789
866
  retry_policy: Optional[task.RetryPolicy] = None,
867
+ tags: Optional[dict[str, str]] = None,
790
868
  is_sub_orch: bool = False,
791
869
  instance_id: Optional[str] = None,
792
870
  fn_task: Optional[task.CompletableTask[TOutput]] = None,
871
+ version: Optional[str] = None,
793
872
  ):
794
873
  if id is None:
795
874
  id = self.next_sequence_number()
@@ -806,7 +885,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
806
885
  if isinstance(activity_function, str)
807
886
  else task.get_name(activity_function)
808
887
  )
809
- action = ph.new_schedule_task_action(id, name, encoded_input)
888
+ action = ph.new_schedule_task_action(id, name, encoded_input, tags)
810
889
  else:
811
890
  if instance_id is None:
812
891
  # Create a deteministic instance ID based on the parent instance ID
@@ -814,7 +893,7 @@ class _RuntimeOrchestrationContext(task.OrchestrationContext):
814
893
  if not isinstance(activity_function, str):
815
894
  raise ValueError("Orchestrator function name must be a string")
816
895
  action = ph.new_create_sub_orchestration_action(
817
- id, activity_function, instance_id, encoded_input
896
+ id, activity_function, instance_id, encoded_input, version
818
897
  )
819
898
  self._pending_actions[id] = action
820
899
 
@@ -890,7 +969,8 @@ class _OrchestrationExecutor:
890
969
  "The new history event list must have at least one event in it."
891
970
  )
892
971
 
893
- ctx = _RuntimeOrchestrationContext(instance_id)
972
+ ctx = _RuntimeOrchestrationContext(instance_id, self._registry)
973
+ version_failure = None
894
974
  try:
895
975
  # Rebuild local state by replaying old history into the orchestrator function
896
976
  self._logger.debug(
@@ -900,6 +980,23 @@ class _OrchestrationExecutor:
900
980
  for old_event in old_events:
901
981
  self.process_event(ctx, old_event)
902
982
 
983
+ # Process versioning if applicable
984
+ execution_started_events = [e.executionStarted for e in old_events if e.HasField("executionStarted")]
985
+ # We only check versioning if there are executionStarted events - otherwise, on the first replay when
986
+ # ctx.version will be Null, we may invalidate orchestrations early depending on the versioning strategy.
987
+ if self._registry.versioning and len(execution_started_events) > 0:
988
+ version_failure = self.evaluate_orchestration_versioning(
989
+ self._registry.versioning,
990
+ ctx.version
991
+ )
992
+ if version_failure:
993
+ self._logger.warning(
994
+ f"Orchestration version did not meet worker versioning requirements. "
995
+ f"Error action = '{self._registry.versioning.failure_strategy}'. "
996
+ f"Version error = '{version_failure}'"
997
+ )
998
+ raise pe.VersionFailureException
999
+
903
1000
  # Get new actions by executing newly received events into the orchestrator function
904
1001
  if self._logger.level <= logging.DEBUG:
905
1002
  summary = _get_new_event_summary(new_events)
@@ -910,6 +1007,15 @@ class _OrchestrationExecutor:
910
1007
  for new_event in new_events:
911
1008
  self.process_event(ctx, new_event)
912
1009
 
1010
+ except pe.VersionFailureException as ex:
1011
+ if self._registry.versioning and self._registry.versioning.failure_strategy == VersionFailureStrategy.FAIL:
1012
+ if version_failure:
1013
+ ctx.set_failed(version_failure)
1014
+ else:
1015
+ ctx.set_failed(ex)
1016
+ elif self._registry.versioning and self._registry.versioning.failure_strategy == VersionFailureStrategy.REJECT:
1017
+ raise pe.AbandonOrchestrationError
1018
+
913
1019
  except Exception as ex:
914
1020
  # Unhandled exceptions fail the orchestration
915
1021
  ctx.set_failed(ex)
@@ -959,6 +1065,9 @@ class _OrchestrationExecutor:
959
1065
  f"A '{event.executionStarted.name}' orchestrator was not registered."
960
1066
  )
961
1067
 
1068
+ if event.executionStarted.version:
1069
+ ctx._version = event.executionStarted.version.value
1070
+
962
1071
  # deserialize the input, if any
963
1072
  input = None
964
1073
  if (
@@ -1221,6 +1330,48 @@ class _OrchestrationExecutor:
1221
1330
  # The orchestrator generator function completed
1222
1331
  ctx.set_complete(generatorStopped.value, pb.ORCHESTRATION_STATUS_COMPLETED)
1223
1332
 
1333
+ def evaluate_orchestration_versioning(self, versioning: Optional[VersioningOptions], orchestration_version: Optional[str]) -> Optional[pb.TaskFailureDetails]:
1334
+ if versioning is None:
1335
+ return None
1336
+ version_comparison = self.compare_versions(orchestration_version, versioning.version)
1337
+ if versioning.match_strategy == VersionMatchStrategy.NONE:
1338
+ return None
1339
+ elif versioning.match_strategy == VersionMatchStrategy.STRICT:
1340
+ if version_comparison != 0:
1341
+ return pb.TaskFailureDetails(
1342
+ errorType="VersionMismatch",
1343
+ errorMessage=f"The orchestration version '{orchestration_version}' does not match the worker version '{versioning.version}'.",
1344
+ isNonRetriable=True,
1345
+ )
1346
+ elif versioning.match_strategy == VersionMatchStrategy.CURRENT_OR_OLDER:
1347
+ if version_comparison > 0:
1348
+ return pb.TaskFailureDetails(
1349
+ errorType="VersionMismatch",
1350
+ errorMessage=f"The orchestration version '{orchestration_version}' is greater than the worker version '{versioning.version}'.",
1351
+ isNonRetriable=True,
1352
+ )
1353
+ else:
1354
+ # If there is a type of versioning we don't understand, it is better to treat it as a versioning failure.
1355
+ return pb.TaskFailureDetails(
1356
+ errorType="VersionMismatch",
1357
+ errorMessage=f"The version match strategy '{versioning.match_strategy}' is unknown.",
1358
+ isNonRetriable=True,
1359
+ )
1360
+
1361
+ def compare_versions(self, source_version: Optional[str], default_version: Optional[str]) -> int:
1362
+ if not source_version and not default_version:
1363
+ return 0
1364
+ if not source_version:
1365
+ return -1
1366
+ if not default_version:
1367
+ return 1
1368
+ try:
1369
+ source_version_parsed = parse(source_version)
1370
+ default_version_parsed = parse(default_version)
1371
+ return (source_version_parsed > default_version_parsed) - (source_version_parsed < default_version_parsed)
1372
+ except InvalidVersion:
1373
+ return (source_version > default_version) - (source_version < default_version)
1374
+
1224
1375
 
1225
1376
  class _ActivityExecutor:
1226
1377
  def __init__(self, registry: _Registry, logger: logging.Logger):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durabletask
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: A Durable Task Client SDK for Python
5
5
  License: MIT License
6
6
 
@@ -36,6 +36,7 @@ License-File: LICENSE
36
36
  Requires-Dist: grpcio
37
37
  Requires-Dist: protobuf
38
38
  Requires-Dist: asyncio
39
+ Requires-Dist: packaging
39
40
  Dynamic: license-file
40
41
 
41
42
  # Durable Task SDK for Python
@@ -217,10 +218,11 @@ make test-unit
217
218
 
218
219
  ### Running E2E tests
219
220
 
220
- The E2E (end-to-end) tests require a sidecar process to be running. You can use the Dapr sidecar for this or run a Durable Task test sidecar using the following `docker` command:
221
+ The E2E (end-to-end) tests require a sidecar process to be running. You can use the Dapr sidecar for this or run a Durable Task test sidecar using the following command:
221
222
 
222
223
  ```sh
223
- docker run --name durabletask-sidecar -p 4001:4001 --env 'DURABLETASK_SIDECAR_LOGLEVEL=Debug' --rm cgillum/durabletask-sidecar:latest start --backend Emulator
224
+ go install github.com/microsoft/durabletask-go@main
225
+ durabletask-go --port 4001
224
226
  ```
225
227
 
226
228
  To run the E2E tests, run the following command from the project root:
@@ -0,0 +1,16 @@
1
+ durabletask/__init__.py,sha256=1PTnFPvigLCs2apa7ITASeqqFOS09Zn-rrllyWeoDJE,263
2
+ durabletask/client.py,sha256=VrHXntWfSk4xYAv7JkOOXgNoFzefYWcD7UPGgn2zuWM,10312
3
+ durabletask/task.py,sha256=LCtKrDh_Yb94Zjxx8fUtXt3z5GwSaV-EieIFXKeZNOI,18404
4
+ durabletask/worker.py,sha256=fmKqjrBdU3eTq9sdVhFyjbMPgg_b60ASHggdsKvcaJQ,73669
5
+ durabletask/internal/exceptions.py,sha256=GVtYAhyCtoPFcbddW2rClskBIc1FkcqTOCz_EZiBd9o,176
6
+ durabletask/internal/grpc_interceptor.py,sha256=KGl8GGIbNdiEnWVLwQwkOemWvIlcEO0dh-_Tg20h5XA,2834
7
+ durabletask/internal/helpers.py,sha256=7A1Bb-KNuAovQTOh9mxeTFahCPtl45C5j5tUJ43BXuo,7538
8
+ durabletask/internal/orchestrator_service_pb2.py,sha256=q4elBQnofrZ4eYVCVnmed4vsa-FLNFLeBRtFOZYhTv8,44631
9
+ durabletask/internal/orchestrator_service_pb2.pyi,sha256=FFxZhxdV7SX0pKxwKPHTGaAdXKowXnib4WvjhGFw2eo,66480
10
+ durabletask/internal/orchestrator_service_pb2_grpc.py,sha256=5xhDLJ73Ipsp1tjwWhUqVqEm30MjWHlj71zfPNBehWc,54366
11
+ durabletask/internal/shared.py,sha256=dKRGU8z1EQM4_YA6zkKeKfiaWbiZ6-B8lP-wHy7Q_jI,4379
12
+ durabletask-0.4.0.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
13
+ durabletask-0.4.0.dist-info/METADATA,sha256=7ua3w7kWAdszCcUvchLxY_wWFdPd2r6Bb_Ca6N57_sk,12894
14
+ durabletask-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ durabletask-0.4.0.dist-info/top_level.txt,sha256=EBVyuKWnjOwq8bJI1Uvb9U3c4fzQxACWj9p83he6fik,12
16
+ durabletask-0.4.0.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- durabletask/__init__.py,sha256=5YY3OLjqPt2tL8c9MLMn5eDfkMQgNIzYP7ir8MJtD-M,223
2
- durabletask/client.py,sha256=vAm7BtVHeeWFVoiwvOGcrhrkand43oBCCVNnzbNfH6I,10011
3
- durabletask/task.py,sha256=Brxt-cFqFaIjU07UFLCQoRv7ioycOLcJQbRh9Je_UW4,17722
4
- durabletask/worker.py,sha256=D9TTl6QZ9f5uYvFNZnEAndgkqWRKkV66UNGLgp59sDY,66491
5
- durabletask/internal/grpc_interceptor.py,sha256=KGl8GGIbNdiEnWVLwQwkOemWvIlcEO0dh-_Tg20h5XA,2834
6
- durabletask/internal/helpers.py,sha256=G4nEhLnRUE1VbFHkOMX277_6LSsMH9lTh9sXUD0GdHM,7289
7
- durabletask/internal/orchestrator_service_pb2.py,sha256=nkADgSglhimtNjAuISJdBz1bwA8xYm1cEQdL9ZifsmU,33993
8
- durabletask/internal/orchestrator_service_pb2.pyi,sha256=99AIPzz4AdXrkQrN2MHkHkW9zKqmH4puSwvg9ze5IjA,50517
9
- durabletask/internal/orchestrator_service_pb2_grpc.py,sha256=mZXK0QtvaRr6cjm8gi9y-DjMNR2Xg2Adu79WsR22pQc,41146
10
- durabletask/internal/shared.py,sha256=dKRGU8z1EQM4_YA6zkKeKfiaWbiZ6-B8lP-wHy7Q_jI,4379
11
- durabletask-0.3.0.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
12
- durabletask-0.3.0.dist-info/METADATA,sha256=yy_ChhOJe2RiOkp_QvGzLEUvMaluwBmVd8H0Rfi58kg,12958
13
- durabletask-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- durabletask-0.3.0.dist-info/top_level.txt,sha256=EBVyuKWnjOwq8bJI1Uvb9U3c4fzQxACWj9p83he6fik,12
15
- durabletask-0.3.0.dist-info/RECORD,,