durabletask 0.0.0.dev1__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.
@@ -0,0 +1,34 @@
1
+ from typing import Any, Callable, Protocol
2
+
3
+
4
+ class ProtoTaskHubSidecarServiceStub(Protocol):
5
+ """A stub class roughly matching the TaskHubSidecarServiceStub generated from the .proto file.
6
+ Used by Azure Functions during orchestration and entity executions to inject custom behavior,
7
+ as no real sidecar stub is available.
8
+ """
9
+ Hello: Callable[..., Any]
10
+ StartInstance: Callable[..., Any]
11
+ GetInstance: Callable[..., Any]
12
+ RewindInstance: Callable[..., Any]
13
+ WaitForInstanceStart: Callable[..., Any]
14
+ WaitForInstanceCompletion: Callable[..., Any]
15
+ RaiseEvent: Callable[..., Any]
16
+ TerminateInstance: Callable[..., Any]
17
+ SuspendInstance: Callable[..., Any]
18
+ ResumeInstance: Callable[..., Any]
19
+ QueryInstances: Callable[..., Any]
20
+ PurgeInstances: Callable[..., Any]
21
+ GetWorkItems: Callable[..., Any]
22
+ CompleteActivityTask: Callable[..., Any]
23
+ CompleteOrchestratorTask: Callable[..., Any]
24
+ CompleteEntityTask: Callable[..., Any]
25
+ StreamInstanceHistory: Callable[..., Any]
26
+ CreateTaskHub: Callable[..., Any]
27
+ DeleteTaskHub: Callable[..., Any]
28
+ SignalEntity: Callable[..., Any]
29
+ GetEntity: Callable[..., Any]
30
+ QueryEntities: Callable[..., Any]
31
+ CleanEntityStorage: Callable[..., Any]
32
+ AbandonTaskActivityWorkItem: Callable[..., Any]
33
+ AbandonTaskOrchestratorWorkItem: Callable[..., Any]
34
+ AbandonTaskEntityWorkItem: Callable[..., Any]
@@ -0,0 +1,66 @@
1
+ from typing import Any, TypeVar, Union
2
+ from typing import Optional, Type, overload
3
+
4
+ import durabletask.internal.orchestrator_service_pb2 as pb
5
+
6
+ TState = TypeVar("TState")
7
+
8
+
9
+ class StateShim:
10
+ def __init__(self, start_state):
11
+ self._current_state: Any = start_state
12
+ self._checkpoint_state: Any = start_state
13
+ self._operation_actions: list[pb.OperationAction] = []
14
+ self._actions_checkpoint_state: int = 0
15
+
16
+ @overload
17
+ def get_state(self, intended_type: Type[TState], default: TState) -> TState:
18
+ ...
19
+
20
+ @overload
21
+ def get_state(self, intended_type: Type[TState]) -> Optional[TState]:
22
+ ...
23
+
24
+ @overload
25
+ def get_state(self, intended_type: None = None, default: Any = None) -> Any:
26
+ ...
27
+
28
+ def get_state(self, intended_type: Optional[Type[TState]] = None, default: Optional[TState] = None) -> Union[None, TState, Any]:
29
+ if self._current_state is None and default is not None:
30
+ return default
31
+
32
+ if intended_type is None:
33
+ return self._current_state
34
+
35
+ if isinstance(self._current_state, intended_type):
36
+ return self._current_state
37
+
38
+ try:
39
+ return intended_type(self._current_state) # type: ignore[call-arg]
40
+ except Exception as ex:
41
+ raise TypeError(
42
+ f"Could not convert state of type '{type(self._current_state).__name__}' to '{intended_type.__name__}'"
43
+ ) from ex
44
+
45
+ def set_state(self, state):
46
+ self._current_state = state
47
+
48
+ def add_operation_action(self, action: pb.OperationAction):
49
+ self._operation_actions.append(action)
50
+
51
+ def get_operation_actions(self) -> list[pb.OperationAction]:
52
+ return self._operation_actions[:self._actions_checkpoint_state]
53
+
54
+ def commit(self):
55
+ self._checkpoint_state = self._current_state
56
+ self._actions_checkpoint_state = len(self._operation_actions)
57
+
58
+ def rollback(self):
59
+ self._current_state = self._checkpoint_state
60
+ self._operation_actions = self._operation_actions[:self._actions_checkpoint_state]
61
+
62
+ def reset(self):
63
+ self._current_state = None
64
+ self._checkpoint_state = None
65
+ self._operation_actions = []
66
+ self._actions_checkpoint_state = 0
@@ -0,0 +1,11 @@
1
+ import durabletask.internal.orchestrator_service_pb2 as pb
2
+
3
+
4
+ class VersionFailureException(Exception):
5
+ def __init__(self, error_details: pb.TaskFailureDetails) -> None:
6
+ super().__init__()
7
+ self.error_details = error_details
8
+
9
+
10
+ class AbandonOrchestrationError(Exception):
11
+ pass
@@ -0,0 +1,65 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ from collections import namedtuple
5
+
6
+ import grpc
7
+
8
+
9
+ class _ClientCallDetails(
10
+ namedtuple(
11
+ '_ClientCallDetails',
12
+ ['method', 'timeout', 'metadata', 'credentials', 'wait_for_ready', 'compression']),
13
+ grpc.ClientCallDetails):
14
+ """This is an implementation of the ClientCallDetails interface needed for interceptors.
15
+ This class takes six named values and inherits the ClientCallDetails from grpc package.
16
+ This class encloses the values that describe a RPC to be invoked.
17
+ """
18
+ pass
19
+
20
+
21
+ class DefaultClientInterceptorImpl (
22
+ grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor,
23
+ grpc.StreamUnaryClientInterceptor, grpc.StreamStreamClientInterceptor):
24
+ """The class implements a UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor,
25
+ StreamUnaryClientInterceptor and StreamStreamClientInterceptor from grpc to add an
26
+ interceptor to add additional headers to all calls as needed."""
27
+
28
+ def __init__(self, metadata: list[tuple[str, str]]):
29
+ super().__init__()
30
+ self._metadata = metadata
31
+
32
+ def _intercept_call(
33
+ self, client_call_details: _ClientCallDetails) -> grpc.ClientCallDetails:
34
+ """Internal intercept_call implementation which adds metadata to grpc metadata in the RPC
35
+ call details."""
36
+ if self._metadata is None:
37
+ return client_call_details
38
+
39
+ if client_call_details.metadata is not None:
40
+ metadata = list(client_call_details.metadata)
41
+ else:
42
+ metadata = []
43
+
44
+ metadata.extend(self._metadata)
45
+ client_call_details = _ClientCallDetails(
46
+ client_call_details.method, client_call_details.timeout, metadata,
47
+ client_call_details.credentials, client_call_details.wait_for_ready, client_call_details.compression)
48
+
49
+ return client_call_details
50
+
51
+ def intercept_unary_unary(self, continuation, client_call_details, request):
52
+ new_client_call_details = self._intercept_call(client_call_details)
53
+ return continuation(new_client_call_details, request)
54
+
55
+ def intercept_unary_stream(self, continuation, client_call_details, request):
56
+ new_client_call_details = self._intercept_call(client_call_details)
57
+ return continuation(new_client_call_details, request)
58
+
59
+ def intercept_stream_unary(self, continuation, client_call_details, request):
60
+ new_client_call_details = self._intercept_call(client_call_details)
61
+ return continuation(new_client_call_details, request)
62
+
63
+ def intercept_stream_stream(self, continuation, client_call_details, request):
64
+ new_client_call_details = self._intercept_call(client_call_details)
65
+ return continuation(new_client_call_details, request)
@@ -0,0 +1,288 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT License.
3
+
4
+ import traceback
5
+ from datetime import datetime
6
+ from typing import Optional
7
+
8
+ from google.protobuf import timestamp_pb2, wrappers_pb2
9
+
10
+ from durabletask.entities import EntityInstanceId
11
+ import durabletask.internal.orchestrator_service_pb2 as pb
12
+
13
+ # TODO: The new_xxx_event methods are only used by test code and should be moved elsewhere
14
+
15
+
16
+ def new_orchestrator_started_event(timestamp: Optional[datetime] = None) -> pb.HistoryEvent:
17
+ ts = timestamp_pb2.Timestamp()
18
+ if timestamp is not None:
19
+ ts.FromDatetime(timestamp)
20
+ return pb.HistoryEvent(eventId=-1, timestamp=ts, orchestratorStarted=pb.OrchestratorStartedEvent())
21
+
22
+
23
+ def new_execution_started_event(name: str, instance_id: str, encoded_input: Optional[str] = None,
24
+ tags: Optional[dict[str, str]] = None) -> pb.HistoryEvent:
25
+ return pb.HistoryEvent(
26
+ eventId=-1,
27
+ timestamp=timestamp_pb2.Timestamp(),
28
+ executionStarted=pb.ExecutionStartedEvent(
29
+ name=name,
30
+ input=get_string_value(encoded_input),
31
+ orchestrationInstance=pb.OrchestrationInstance(instanceId=instance_id),
32
+ tags=tags))
33
+
34
+
35
+ def new_timer_created_event(timer_id: int, fire_at: datetime) -> pb.HistoryEvent:
36
+ ts = timestamp_pb2.Timestamp()
37
+ ts.FromDatetime(fire_at)
38
+ return pb.HistoryEvent(
39
+ eventId=timer_id,
40
+ timestamp=timestamp_pb2.Timestamp(),
41
+ timerCreated=pb.TimerCreatedEvent(fireAt=ts)
42
+ )
43
+
44
+
45
+ def new_timer_fired_event(timer_id: int, fire_at: datetime) -> pb.HistoryEvent:
46
+ ts = timestamp_pb2.Timestamp()
47
+ ts.FromDatetime(fire_at)
48
+ return pb.HistoryEvent(
49
+ eventId=-1,
50
+ timestamp=timestamp_pb2.Timestamp(),
51
+ timerFired=pb.TimerFiredEvent(fireAt=ts, timerId=timer_id)
52
+ )
53
+
54
+
55
+ def new_task_scheduled_event(event_id: int, name: str, encoded_input: Optional[str] = None) -> pb.HistoryEvent:
56
+ return pb.HistoryEvent(
57
+ eventId=event_id,
58
+ timestamp=timestamp_pb2.Timestamp(),
59
+ taskScheduled=pb.TaskScheduledEvent(name=name, input=get_string_value(encoded_input))
60
+ )
61
+
62
+
63
+ def new_task_completed_event(event_id: int, encoded_output: Optional[str] = None) -> pb.HistoryEvent:
64
+ return pb.HistoryEvent(
65
+ eventId=-1,
66
+ timestamp=timestamp_pb2.Timestamp(),
67
+ taskCompleted=pb.TaskCompletedEvent(taskScheduledId=event_id, result=get_string_value(encoded_output))
68
+ )
69
+
70
+
71
+ def new_task_failed_event(event_id: int, ex: Exception) -> pb.HistoryEvent:
72
+ return pb.HistoryEvent(
73
+ eventId=-1,
74
+ timestamp=timestamp_pb2.Timestamp(),
75
+ taskFailed=pb.TaskFailedEvent(taskScheduledId=event_id, failureDetails=new_failure_details(ex))
76
+ )
77
+
78
+
79
+ def new_sub_orchestration_created_event(
80
+ event_id: int,
81
+ name: str,
82
+ instance_id: str,
83
+ encoded_input: Optional[str] = None) -> pb.HistoryEvent:
84
+ return pb.HistoryEvent(
85
+ eventId=event_id,
86
+ timestamp=timestamp_pb2.Timestamp(),
87
+ subOrchestrationInstanceCreated=pb.SubOrchestrationInstanceCreatedEvent(
88
+ name=name,
89
+ input=get_string_value(encoded_input),
90
+ instanceId=instance_id)
91
+ )
92
+
93
+
94
+ def new_sub_orchestration_completed_event(event_id: int, encoded_output: Optional[str] = None) -> pb.HistoryEvent:
95
+ return pb.HistoryEvent(
96
+ eventId=-1,
97
+ timestamp=timestamp_pb2.Timestamp(),
98
+ subOrchestrationInstanceCompleted=pb.SubOrchestrationInstanceCompletedEvent(
99
+ result=get_string_value(encoded_output),
100
+ taskScheduledId=event_id)
101
+ )
102
+
103
+
104
+ def new_sub_orchestration_failed_event(event_id: int, ex: Exception) -> pb.HistoryEvent:
105
+ return pb.HistoryEvent(
106
+ eventId=-1,
107
+ timestamp=timestamp_pb2.Timestamp(),
108
+ subOrchestrationInstanceFailed=pb.SubOrchestrationInstanceFailedEvent(
109
+ failureDetails=new_failure_details(ex),
110
+ taskScheduledId=event_id)
111
+ )
112
+
113
+
114
+ def new_failure_details(ex: Exception) -> pb.TaskFailureDetails:
115
+ return pb.TaskFailureDetails(
116
+ errorType=type(ex).__name__,
117
+ errorMessage=str(ex),
118
+ stackTrace=wrappers_pb2.StringValue(value=''.join(traceback.format_tb(ex.__traceback__)))
119
+ )
120
+
121
+
122
+ def new_event_raised_event(name: str, encoded_input: Optional[str] = None) -> pb.HistoryEvent:
123
+ return pb.HistoryEvent(
124
+ eventId=-1,
125
+ timestamp=timestamp_pb2.Timestamp(),
126
+ eventRaised=pb.EventRaisedEvent(name=name, input=get_string_value(encoded_input))
127
+ )
128
+
129
+
130
+ def new_suspend_event() -> pb.HistoryEvent:
131
+ return pb.HistoryEvent(
132
+ eventId=-1,
133
+ timestamp=timestamp_pb2.Timestamp(),
134
+ executionSuspended=pb.ExecutionSuspendedEvent()
135
+ )
136
+
137
+
138
+ def new_resume_event() -> pb.HistoryEvent:
139
+ return pb.HistoryEvent(
140
+ eventId=-1,
141
+ timestamp=timestamp_pb2.Timestamp(),
142
+ executionResumed=pb.ExecutionResumedEvent()
143
+ )
144
+
145
+
146
+ def new_terminated_event(*, encoded_output: Optional[str] = None) -> pb.HistoryEvent:
147
+ return pb.HistoryEvent(
148
+ eventId=-1,
149
+ timestamp=timestamp_pb2.Timestamp(),
150
+ executionTerminated=pb.ExecutionTerminatedEvent(
151
+ input=get_string_value(encoded_output)
152
+ )
153
+ )
154
+
155
+
156
+ def get_string_value(val: Optional[str]) -> Optional[wrappers_pb2.StringValue]:
157
+ if val is None:
158
+ return None
159
+ else:
160
+ return wrappers_pb2.StringValue(value=val)
161
+
162
+
163
+ def get_string_value_or_empty(val: Optional[str]) -> wrappers_pb2.StringValue:
164
+ if val is None:
165
+ return wrappers_pb2.StringValue(value="")
166
+ return wrappers_pb2.StringValue(value=val)
167
+
168
+
169
+ def new_complete_orchestration_action(
170
+ id: int,
171
+ status: pb.OrchestrationStatus,
172
+ result: Optional[str] = None,
173
+ failure_details: Optional[pb.TaskFailureDetails] = None,
174
+ carryover_events: Optional[list[pb.HistoryEvent]] = None) -> pb.OrchestratorAction:
175
+ completeOrchestrationAction = pb.CompleteOrchestrationAction(
176
+ orchestrationStatus=status,
177
+ result=get_string_value(result),
178
+ failureDetails=failure_details,
179
+ carryoverEvents=carryover_events)
180
+
181
+ return pb.OrchestratorAction(id=id, completeOrchestration=completeOrchestrationAction)
182
+
183
+
184
+ def new_create_timer_action(id: int, fire_at: datetime) -> pb.OrchestratorAction:
185
+ timestamp = timestamp_pb2.Timestamp()
186
+ timestamp.FromDatetime(fire_at)
187
+ return pb.OrchestratorAction(id=id, createTimer=pb.CreateTimerAction(fireAt=timestamp))
188
+
189
+
190
+ def new_schedule_task_action(id: int, name: str, encoded_input: Optional[str],
191
+ tags: Optional[dict[str, str]]) -> pb.OrchestratorAction:
192
+ return pb.OrchestratorAction(id=id, scheduleTask=pb.ScheduleTaskAction(
193
+ name=name,
194
+ input=get_string_value(encoded_input),
195
+ tags=tags
196
+ ))
197
+
198
+
199
+ def new_call_entity_action(id: int,
200
+ parent_instance_id: str,
201
+ entity_id: EntityInstanceId,
202
+ operation: str, encoded_input: Optional[str],
203
+ request_id: str):
204
+ return pb.OrchestratorAction(id=id, sendEntityMessage=pb.SendEntityMessageAction(entityOperationCalled=pb.EntityOperationCalledEvent(
205
+ requestId=request_id,
206
+ operation=operation,
207
+ scheduledTime=None,
208
+ input=get_string_value(encoded_input),
209
+ parentInstanceId=get_string_value(parent_instance_id),
210
+ parentExecutionId=None,
211
+ targetInstanceId=get_string_value(str(entity_id)),
212
+ )))
213
+
214
+
215
+ def new_signal_entity_action(id: int,
216
+ entity_id: EntityInstanceId,
217
+ operation: str,
218
+ encoded_input: Optional[str],
219
+ request_id: str):
220
+ return pb.OrchestratorAction(id=id, sendEntityMessage=pb.SendEntityMessageAction(entityOperationSignaled=pb.EntityOperationSignaledEvent(
221
+ requestId=request_id,
222
+ operation=operation,
223
+ scheduledTime=None,
224
+ input=get_string_value(encoded_input),
225
+ targetInstanceId=get_string_value(str(entity_id)),
226
+ )))
227
+
228
+
229
+ def new_lock_entities_action(id: int, entity_message: pb.SendEntityMessageAction):
230
+ return pb.OrchestratorAction(id=id, sendEntityMessage=entity_message)
231
+
232
+
233
+ def convert_to_entity_batch_request(req: pb.EntityRequest) -> tuple[pb.EntityBatchRequest, list[pb.OperationInfo]]:
234
+ batch_request = pb.EntityBatchRequest(entityState=req.entityState, instanceId=req.instanceId, operations=[])
235
+
236
+ operation_infos: list[pb.OperationInfo] = []
237
+
238
+ for op in req.operationRequests:
239
+ if op.HasField("entityOperationSignaled"):
240
+ batch_request.operations.append(pb.OperationRequest(requestId=op.entityOperationSignaled.requestId,
241
+ operation=op.entityOperationSignaled.operation,
242
+ input=op.entityOperationSignaled.input))
243
+ operation_infos.append(pb.OperationInfo(requestId=op.entityOperationSignaled.requestId,
244
+ responseDestination=None))
245
+ elif op.HasField("entityOperationCalled"):
246
+ batch_request.operations.append(pb.OperationRequest(requestId=op.entityOperationCalled.requestId,
247
+ operation=op.entityOperationCalled.operation,
248
+ input=op.entityOperationCalled.input))
249
+ operation_infos.append(pb.OperationInfo(requestId=op.entityOperationCalled.requestId,
250
+ responseDestination=pb.OrchestrationInstance(
251
+ instanceId=op.entityOperationCalled.parentInstanceId.value,
252
+ executionId=op.entityOperationCalled.parentExecutionId
253
+ )))
254
+
255
+ return batch_request, operation_infos
256
+
257
+
258
+ def new_timestamp(dt: datetime) -> timestamp_pb2.Timestamp:
259
+ ts = timestamp_pb2.Timestamp()
260
+ ts.FromDatetime(dt)
261
+ return ts
262
+
263
+
264
+ def new_create_sub_orchestration_action(
265
+ id: int,
266
+ name: str,
267
+ instance_id: Optional[str],
268
+ encoded_input: Optional[str],
269
+ version: Optional[str]) -> pb.OrchestratorAction:
270
+ return pb.OrchestratorAction(id=id, createSubOrchestration=pb.CreateSubOrchestrationAction(
271
+ name=name,
272
+ instanceId=instance_id,
273
+ input=get_string_value(encoded_input),
274
+ version=get_string_value(version)
275
+ ))
276
+
277
+
278
+ def is_empty(v: wrappers_pb2.StringValue):
279
+ return v is None or v.value == ''
280
+
281
+
282
+ def get_orchestration_status_str(status: pb.OrchestrationStatus):
283
+ try:
284
+ const_name = pb.OrchestrationStatus.Name(status)
285
+ if const_name.startswith('ORCHESTRATION_STATUS_'):
286
+ return const_name[len('ORCHESTRATION_STATUS_'):]
287
+ except Exception:
288
+ return "UNKNOWN"
@@ -0,0 +1,115 @@
1
+ from datetime import datetime
2
+ from typing import Generator, List, Optional, Tuple, Union
3
+
4
+ from durabletask.internal.helpers import get_string_value
5
+ import durabletask.internal.orchestrator_service_pb2 as pb
6
+ from durabletask.entities import EntityInstanceId
7
+
8
+
9
+ class OrchestrationEntityContext:
10
+ def __init__(self, instance_id: str):
11
+ self.instance_id = instance_id
12
+
13
+ self.lock_acquisition_pending = False
14
+
15
+ self.critical_section_id = None
16
+ self.critical_section_locks: list[EntityInstanceId] = []
17
+ self.available_locks: list[EntityInstanceId] = []
18
+
19
+ @property
20
+ def is_inside_critical_section(self) -> bool:
21
+ return self.critical_section_id is not None
22
+
23
+ def get_available_entities(self) -> Generator[EntityInstanceId, None, None]:
24
+ if self.is_inside_critical_section:
25
+ for available_lock in self.available_locks:
26
+ yield available_lock
27
+
28
+ def validate_suborchestration_transition(self) -> Tuple[bool, str]:
29
+ if self.is_inside_critical_section:
30
+ return False, "While holding locks, cannot call suborchestrators."
31
+ return True, ""
32
+
33
+ def validate_operation_transition(self, target_instance_id: EntityInstanceId, one_way: bool) -> Tuple[bool, str]:
34
+ if self.is_inside_critical_section:
35
+ lock_to_use = target_instance_id
36
+ if one_way:
37
+ if target_instance_id in self.critical_section_locks:
38
+ return False, "Must not signal a locked entity from a critical section."
39
+ else:
40
+ try:
41
+ self.available_locks.remove(lock_to_use)
42
+ except ValueError:
43
+ if self.lock_acquisition_pending:
44
+ return False, "Must await the completion of the lock request prior to calling any entity."
45
+ if lock_to_use in self.critical_section_locks:
46
+ return False, "Must not call an entity from a critical section while a prior call to the same entity is still pending."
47
+ else:
48
+ return False, "Must not call an entity from a critical section if it is not one of the locked entities."
49
+ return True, ""
50
+
51
+ def validate_acquire_transition(self) -> Tuple[bool, str]:
52
+ if self.is_inside_critical_section:
53
+ return False, "Must not enter another critical section from within a critical section."
54
+ return True, ""
55
+
56
+ def recover_lock_after_call(self, target_instance_id: EntityInstanceId):
57
+ if self.is_inside_critical_section:
58
+ self.available_locks.append(target_instance_id)
59
+
60
+ def emit_lock_release_messages(self):
61
+ if self.is_inside_critical_section:
62
+ for entity_id in self.critical_section_locks:
63
+ unlock_event = pb.SendEntityMessageAction(entityUnlockSent=pb.EntityUnlockSentEvent(
64
+ criticalSectionId=self.critical_section_id,
65
+ targetInstanceId=get_string_value(str(entity_id)),
66
+ parentInstanceId=get_string_value(self.instance_id)
67
+ ))
68
+ yield unlock_event
69
+
70
+ self.critical_section_locks = []
71
+ self.available_locks = []
72
+ self.critical_section_id = None
73
+
74
+ def emit_request_message(self, target, operation_name: str, one_way: bool, operation_id: str,
75
+ scheduled_time_utc: datetime, input: Optional[str],
76
+ request_time: Optional[datetime] = None, create_trace: bool = False):
77
+ raise NotImplementedError()
78
+
79
+ def emit_acquire_message(self, critical_section_id: str, entities: List[EntityInstanceId]) -> Union[Tuple[None, None], Tuple[pb.SendEntityMessageAction, pb.OrchestrationInstance]]:
80
+ if not entities:
81
+ return None, None
82
+
83
+ # Acquire the locks in a globally fixed order to avoid deadlocks
84
+ # Also remove duplicates - this can be optimized for perf if necessary
85
+ entity_ids = sorted(entities)
86
+ entity_ids_dedup = []
87
+ for i, entity_id in enumerate(entity_ids):
88
+ if entity_id != entity_ids[i - 1] if i > 0 else True:
89
+ entity_ids_dedup.append(entity_id)
90
+
91
+ target = pb.OrchestrationInstance(instanceId=str(entity_ids_dedup[0]))
92
+ request = pb.SendEntityMessageAction(entityLockRequested=pb.EntityLockRequestedEvent(
93
+ criticalSectionId=critical_section_id,
94
+ parentInstanceId=get_string_value(self.instance_id),
95
+ lockSet=[str(eid) for eid in entity_ids_dedup],
96
+ position=0,
97
+ ))
98
+
99
+ self.critical_section_id = critical_section_id
100
+ self.critical_section_locks = entity_ids_dedup
101
+ self.lock_acquisition_pending = True
102
+
103
+ return request, target
104
+
105
+ def complete_acquire(self, critical_section_id):
106
+ if self.critical_section_id != critical_section_id:
107
+ raise RuntimeError(f"Unexpected lock acquire for critical section ID '{critical_section_id}' (expected '{self.critical_section_id}')")
108
+ self.available_locks = self.critical_section_locks
109
+ self.lock_acquisition_pending = False
110
+
111
+ def adjust_outgoing_message(self, instance_id: str, request_message, capped_time: datetime) -> str:
112
+ raise NotImplementedError()
113
+
114
+ def deserialize_entity_response_event(self, event_content: str):
115
+ raise NotImplementedError()