durabletask 1.4.0__tar.gz → 1.4.0.dev31__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.
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/PKG-INFO +1 -1
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/client.py +86 -1
- durabletask-1.4.0.dev31/durabletask/history.py +535 -0
- durabletask-1.4.0.dev31/durabletask/internal/history_helpers.py +68 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/testing/in_memory_backend.py +71 -2
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask.egg-info/PKG-INFO +1 -1
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask.egg-info/SOURCES.txt +2 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/pyproject.toml +1 -1
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/LICENSE +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/README.md +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/__init__.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/entities/__init__.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/entities/durable_entity.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/entities/entity_context.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/entities/entity_instance_id.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/entities/entity_lock.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/entities/entity_metadata.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/entities/entity_operation_failed_exception.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/extensions/__init__.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/extensions/azure_blob_payloads/__init__.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/extensions/azure_blob_payloads/blob_payload_store.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/extensions/azure_blob_payloads/options.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/client_helpers.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/entity_state_shim.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/exceptions.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/grpc_interceptor.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/helpers.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/json_encode_output_exception.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/orchestration_entity_context.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/orchestrator_service_pb2.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/orchestrator_service_pb2.pyi +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/orchestrator_service_pb2_grpc.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/proto_task_hub_sidecar_service_stub.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/shared.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/tracing.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/payload/__init__.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/payload/helpers.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/payload/store.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/py.typed +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/task.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/testing/__init__.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/worker.py +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask.egg-info/dependency_links.txt +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask.egg-info/requires.txt +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask.egg-info/top_level.txt +0 -0
- {durabletask-1.4.0 → durabletask-1.4.0.dev31}/setup.cfg +0 -0
|
@@ -6,14 +6,16 @@ import uuid
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from enum import Enum
|
|
9
|
-
from typing import Any, List, Optional, Sequence, TypeVar, Union
|
|
9
|
+
from typing import Any, Generic, List, Optional, Sequence, TypeVar, Union
|
|
10
10
|
|
|
11
11
|
import grpc
|
|
12
12
|
import grpc.aio
|
|
13
13
|
|
|
14
|
+
import durabletask.history as history
|
|
14
15
|
from durabletask.entities import EntityInstanceId
|
|
15
16
|
from durabletask.entities.entity_metadata import EntityMetadata
|
|
16
17
|
import durabletask.internal.helpers as helpers
|
|
18
|
+
import durabletask.internal.history_helpers as history_helpers
|
|
17
19
|
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
18
20
|
import durabletask.internal.orchestrator_service_pb2_grpc as stubs
|
|
19
21
|
import durabletask.internal.shared as shared
|
|
@@ -37,6 +39,7 @@ from durabletask.payload.store import PayloadStore
|
|
|
37
39
|
|
|
38
40
|
TInput = TypeVar('TInput')
|
|
39
41
|
TOutput = TypeVar('TOutput')
|
|
42
|
+
TItem = TypeVar('TItem')
|
|
40
43
|
|
|
41
44
|
|
|
42
45
|
class OrchestrationStatus(Enum):
|
|
@@ -99,6 +102,12 @@ class PurgeInstancesResult:
|
|
|
99
102
|
is_complete: bool
|
|
100
103
|
|
|
101
104
|
|
|
105
|
+
@dataclass
|
|
106
|
+
class Page(Generic[TItem]):
|
|
107
|
+
items: List[TItem]
|
|
108
|
+
continuation_token: Optional[str]
|
|
109
|
+
|
|
110
|
+
|
|
102
111
|
@dataclass
|
|
103
112
|
class CleanEntityStorageResult:
|
|
104
113
|
empty_entities_removed: int
|
|
@@ -218,6 +227,44 @@ class TaskHubGrpcClient:
|
|
|
218
227
|
payload_helpers.deexternalize_payloads(res, self._payload_store)
|
|
219
228
|
return new_orchestration_state(req.instanceId, res)
|
|
220
229
|
|
|
230
|
+
def get_orchestration_history(self,
|
|
231
|
+
instance_id: str, *,
|
|
232
|
+
execution_id: Optional[str] = None,
|
|
233
|
+
for_work_item_processing: bool = False) -> List[history.HistoryEvent]:
|
|
234
|
+
req = pb.StreamInstanceHistoryRequest(
|
|
235
|
+
instanceId=instance_id,
|
|
236
|
+
executionId=helpers.get_string_value(execution_id),
|
|
237
|
+
forWorkItemProcessing=for_work_item_processing,
|
|
238
|
+
)
|
|
239
|
+
self._logger.info(f"Retrieving history for instance '{instance_id}'.")
|
|
240
|
+
stream = self._stub.StreamInstanceHistory(req)
|
|
241
|
+
return history_helpers.collect_history_events(stream, self._payload_store)
|
|
242
|
+
|
|
243
|
+
def list_instance_ids(self,
|
|
244
|
+
runtime_status: Optional[List[OrchestrationStatus]] = None,
|
|
245
|
+
completed_time_from: Optional[datetime] = None,
|
|
246
|
+
completed_time_to: Optional[datetime] = None,
|
|
247
|
+
page_size: Optional[int] = None,
|
|
248
|
+
continuation_token: Optional[str] = None) -> Page[str]:
|
|
249
|
+
req = pb.ListInstanceIdsRequest(
|
|
250
|
+
runtimeStatus=[status.value for status in runtime_status] if runtime_status else [],
|
|
251
|
+
completedTimeFrom=helpers.new_timestamp(completed_time_from) if completed_time_from else None,
|
|
252
|
+
completedTimeTo=helpers.new_timestamp(completed_time_to) if completed_time_to else None,
|
|
253
|
+
pageSize=page_size or 0,
|
|
254
|
+
lastInstanceKey=helpers.get_string_value(continuation_token),
|
|
255
|
+
)
|
|
256
|
+
self._logger.info(
|
|
257
|
+
"Listing terminal instance IDs with filters: "
|
|
258
|
+
f"runtime_status={[str(status) for status in runtime_status] if runtime_status else None}, "
|
|
259
|
+
f"completed_time_from={completed_time_from}, "
|
|
260
|
+
f"completed_time_to={completed_time_to}, "
|
|
261
|
+
f"page_size={page_size}, "
|
|
262
|
+
f"continuation_token={continuation_token}"
|
|
263
|
+
)
|
|
264
|
+
resp: pb.ListInstanceIdsResponse = self._stub.ListInstanceIds(req)
|
|
265
|
+
next_token = resp.lastInstanceKey.value if resp.HasField("lastInstanceKey") else None
|
|
266
|
+
return Page(items=list(resp.instanceIds), continuation_token=next_token)
|
|
267
|
+
|
|
221
268
|
def get_all_orchestration_states(self,
|
|
222
269
|
orchestration_query: Optional[OrchestrationQuery] = None
|
|
223
270
|
) -> List[OrchestrationState]:
|
|
@@ -502,6 +549,44 @@ class AsyncTaskHubGrpcClient:
|
|
|
502
549
|
await payload_helpers.deexternalize_payloads_async(res, self._payload_store)
|
|
503
550
|
return new_orchestration_state(req.instanceId, res)
|
|
504
551
|
|
|
552
|
+
async def get_orchestration_history(self,
|
|
553
|
+
instance_id: str, *,
|
|
554
|
+
execution_id: Optional[str] = None,
|
|
555
|
+
for_work_item_processing: bool = False) -> List[history.HistoryEvent]:
|
|
556
|
+
req = pb.StreamInstanceHistoryRequest(
|
|
557
|
+
instanceId=instance_id,
|
|
558
|
+
executionId=helpers.get_string_value(execution_id),
|
|
559
|
+
forWorkItemProcessing=for_work_item_processing,
|
|
560
|
+
)
|
|
561
|
+
self._logger.info(f"Retrieving history for instance '{instance_id}'.")
|
|
562
|
+
stream = self._stub.StreamInstanceHistory(req)
|
|
563
|
+
return await history_helpers.collect_history_events_async(stream, self._payload_store)
|
|
564
|
+
|
|
565
|
+
async def list_instance_ids(self,
|
|
566
|
+
runtime_status: Optional[List[OrchestrationStatus]] = None,
|
|
567
|
+
completed_time_from: Optional[datetime] = None,
|
|
568
|
+
completed_time_to: Optional[datetime] = None,
|
|
569
|
+
page_size: Optional[int] = None,
|
|
570
|
+
continuation_token: Optional[str] = None) -> Page[str]:
|
|
571
|
+
req = pb.ListInstanceIdsRequest(
|
|
572
|
+
runtimeStatus=[status.value for status in runtime_status] if runtime_status else [],
|
|
573
|
+
completedTimeFrom=helpers.new_timestamp(completed_time_from) if completed_time_from else None,
|
|
574
|
+
completedTimeTo=helpers.new_timestamp(completed_time_to) if completed_time_to else None,
|
|
575
|
+
pageSize=page_size or 0,
|
|
576
|
+
lastInstanceKey=helpers.get_string_value(continuation_token),
|
|
577
|
+
)
|
|
578
|
+
self._logger.info(
|
|
579
|
+
"Listing terminal instance IDs with filters: "
|
|
580
|
+
f"runtime_status={[str(status) for status in runtime_status] if runtime_status else None}, "
|
|
581
|
+
f"completed_time_from={completed_time_from}, "
|
|
582
|
+
f"completed_time_to={completed_time_to}, "
|
|
583
|
+
f"page_size={page_size}, "
|
|
584
|
+
f"continuation_token={continuation_token}"
|
|
585
|
+
)
|
|
586
|
+
resp: pb.ListInstanceIdsResponse = await self._stub.ListInstanceIds(req)
|
|
587
|
+
next_token = resp.lastInstanceKey.value if resp.HasField("lastInstanceKey") else None
|
|
588
|
+
return Page(items=list(resp.instanceIds), continuation_token=next_token)
|
|
589
|
+
|
|
505
590
|
async def get_all_orchestration_states(self,
|
|
506
591
|
orchestration_query: Optional[OrchestrationQuery] = None
|
|
507
592
|
) -> List[OrchestrationState]:
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from google.protobuf import json_format
|
|
11
|
+
from google.protobuf.message import Message
|
|
12
|
+
|
|
13
|
+
from durabletask import task
|
|
14
|
+
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class OrchestrationInstance:
|
|
19
|
+
instance_id: str
|
|
20
|
+
execution_id: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class ParentInstanceInfo:
|
|
25
|
+
task_scheduled_id: int
|
|
26
|
+
name: Optional[str] = None
|
|
27
|
+
version: Optional[str] = None
|
|
28
|
+
orchestration_instance: Optional[OrchestrationInstance] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class TraceContext:
|
|
33
|
+
trace_parent: str
|
|
34
|
+
span_id: str
|
|
35
|
+
trace_state: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class HistoryEvent:
|
|
40
|
+
event_id: int
|
|
41
|
+
timestamp: datetime
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
return _to_serializable(asdict(self))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class ExecutionStartedEvent(HistoryEvent):
|
|
49
|
+
name: str
|
|
50
|
+
version: Optional[str] = None
|
|
51
|
+
input: Optional[str] = None
|
|
52
|
+
orchestration_instance: Optional[OrchestrationInstance] = None
|
|
53
|
+
parent_instance: Optional[ParentInstanceInfo] = None
|
|
54
|
+
scheduled_start_timestamp: Optional[datetime] = None
|
|
55
|
+
parent_trace_context: Optional[TraceContext] = None
|
|
56
|
+
orchestration_span_id: Optional[str] = None
|
|
57
|
+
tags: Optional[dict[str, str]] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(slots=True)
|
|
61
|
+
class ExecutionCompletedEvent(HistoryEvent):
|
|
62
|
+
orchestration_status: int
|
|
63
|
+
result: Optional[str] = None
|
|
64
|
+
failure_details: Optional[task.FailureDetails] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(slots=True)
|
|
68
|
+
class ExecutionTerminatedEvent(HistoryEvent):
|
|
69
|
+
input: Optional[str] = None
|
|
70
|
+
recurse: bool = False
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(slots=True)
|
|
74
|
+
class TaskScheduledEvent(HistoryEvent):
|
|
75
|
+
name: str
|
|
76
|
+
version: Optional[str] = None
|
|
77
|
+
input: Optional[str] = None
|
|
78
|
+
parent_trace_context: Optional[TraceContext] = None
|
|
79
|
+
tags: Optional[dict[str, str]] = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(slots=True)
|
|
83
|
+
class TaskCompletedEvent(HistoryEvent):
|
|
84
|
+
task_scheduled_id: int
|
|
85
|
+
result: Optional[str] = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(slots=True)
|
|
89
|
+
class TaskFailedEvent(HistoryEvent):
|
|
90
|
+
task_scheduled_id: int
|
|
91
|
+
failure_details: Optional[task.FailureDetails] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(slots=True)
|
|
95
|
+
class SubOrchestrationInstanceCreatedEvent(HistoryEvent):
|
|
96
|
+
instance_id: str
|
|
97
|
+
name: str
|
|
98
|
+
version: Optional[str] = None
|
|
99
|
+
input: Optional[str] = None
|
|
100
|
+
parent_trace_context: Optional[TraceContext] = None
|
|
101
|
+
tags: Optional[dict[str, str]] = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(slots=True)
|
|
105
|
+
class SubOrchestrationInstanceCompletedEvent(HistoryEvent):
|
|
106
|
+
task_scheduled_id: int
|
|
107
|
+
result: Optional[str] = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(slots=True)
|
|
111
|
+
class SubOrchestrationInstanceFailedEvent(HistoryEvent):
|
|
112
|
+
task_scheduled_id: int
|
|
113
|
+
failure_details: Optional[task.FailureDetails] = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(slots=True)
|
|
117
|
+
class TimerCreatedEvent(HistoryEvent):
|
|
118
|
+
fire_at: datetime
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(slots=True)
|
|
122
|
+
class TimerFiredEvent(HistoryEvent):
|
|
123
|
+
fire_at: datetime
|
|
124
|
+
timer_id: int
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(slots=True)
|
|
128
|
+
class OrchestratorStartedEvent(HistoryEvent):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(slots=True)
|
|
133
|
+
class OrchestratorCompletedEvent(HistoryEvent):
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(slots=True)
|
|
138
|
+
class EventSentEvent(HistoryEvent):
|
|
139
|
+
instance_id: str
|
|
140
|
+
name: str
|
|
141
|
+
input: Optional[str] = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(slots=True)
|
|
145
|
+
class EventRaisedEvent(HistoryEvent):
|
|
146
|
+
name: str
|
|
147
|
+
input: Optional[str] = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(slots=True)
|
|
151
|
+
class GenericEvent(HistoryEvent):
|
|
152
|
+
data: Optional[str] = None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(slots=True)
|
|
156
|
+
class HistoryStateEvent(HistoryEvent):
|
|
157
|
+
orchestration_state: dict[str, Any]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass(slots=True)
|
|
161
|
+
class ContinueAsNewEvent(HistoryEvent):
|
|
162
|
+
input: Optional[str] = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(slots=True)
|
|
166
|
+
class ExecutionSuspendedEvent(HistoryEvent):
|
|
167
|
+
input: Optional[str] = None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(slots=True)
|
|
171
|
+
class ExecutionResumedEvent(HistoryEvent):
|
|
172
|
+
input: Optional[str] = None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass(slots=True)
|
|
176
|
+
class EntityOperationSignaledEvent(HistoryEvent):
|
|
177
|
+
request_id: str
|
|
178
|
+
operation: str
|
|
179
|
+
scheduled_time: Optional[datetime] = None
|
|
180
|
+
input: Optional[str] = None
|
|
181
|
+
target_instance_id: Optional[str] = None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(slots=True)
|
|
185
|
+
class EntityOperationCalledEvent(HistoryEvent):
|
|
186
|
+
request_id: str
|
|
187
|
+
operation: str
|
|
188
|
+
scheduled_time: Optional[datetime] = None
|
|
189
|
+
input: Optional[str] = None
|
|
190
|
+
parent_instance_id: Optional[str] = None
|
|
191
|
+
parent_execution_id: Optional[str] = None
|
|
192
|
+
target_instance_id: Optional[str] = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(slots=True)
|
|
196
|
+
class EntityOperationCompletedEvent(HistoryEvent):
|
|
197
|
+
request_id: str
|
|
198
|
+
output: Optional[str] = None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(slots=True)
|
|
202
|
+
class EntityOperationFailedEvent(HistoryEvent):
|
|
203
|
+
request_id: str
|
|
204
|
+
failure_details: Optional[task.FailureDetails] = None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(slots=True)
|
|
208
|
+
class EntityLockRequestedEvent(HistoryEvent):
|
|
209
|
+
critical_section_id: str
|
|
210
|
+
lock_set: list[str]
|
|
211
|
+
position: int
|
|
212
|
+
parent_instance_id: Optional[str] = None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass(slots=True)
|
|
216
|
+
class EntityLockGrantedEvent(HistoryEvent):
|
|
217
|
+
critical_section_id: str
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@dataclass(slots=True)
|
|
221
|
+
class EntityUnlockSentEvent(HistoryEvent):
|
|
222
|
+
critical_section_id: str
|
|
223
|
+
parent_instance_id: Optional[str] = None
|
|
224
|
+
target_instance_id: Optional[str] = None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(slots=True)
|
|
228
|
+
class ExecutionRewoundEvent(HistoryEvent):
|
|
229
|
+
reason: Optional[str] = None
|
|
230
|
+
parent_execution_id: Optional[str] = None
|
|
231
|
+
instance_id: Optional[str] = None
|
|
232
|
+
parent_trace_context: Optional[TraceContext] = None
|
|
233
|
+
name: Optional[str] = None
|
|
234
|
+
version: Optional[str] = None
|
|
235
|
+
input: Optional[str] = None
|
|
236
|
+
parent_instance: Optional[ParentInstanceInfo] = None
|
|
237
|
+
tags: Optional[dict[str, str]] = None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _from_protobuf(event: pb.HistoryEvent) -> HistoryEvent:
|
|
241
|
+
event_type = event.WhichOneof('eventType')
|
|
242
|
+
if event_type is None:
|
|
243
|
+
raise ValueError('History event does not have an eventType set')
|
|
244
|
+
converter = _EVENT_CONVERTERS.get(event_type)
|
|
245
|
+
if converter is None:
|
|
246
|
+
raise ValueError(f'Unsupported history event type: {event_type}')
|
|
247
|
+
return converter(event)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def to_dict(event: HistoryEvent) -> dict[str, Any]:
|
|
251
|
+
return event.to_dict()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _base_kwargs(event: pb.HistoryEvent) -> dict[str, Any]:
|
|
255
|
+
return {
|
|
256
|
+
'event_id': event.eventId,
|
|
257
|
+
'timestamp': event.timestamp.ToDatetime(timezone.utc),
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _string_value(msg: Message, field_name: str) -> Optional[str]:
|
|
262
|
+
if msg.HasField(field_name):
|
|
263
|
+
return getattr(msg, field_name).value
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _timestamp_value(msg: Message, field_name: str) -> Optional[datetime]:
|
|
268
|
+
if msg.HasField(field_name):
|
|
269
|
+
return getattr(msg, field_name).ToDatetime(timezone.utc)
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _failure_details(msg: Message, field_name: str) -> Optional[task.FailureDetails]:
|
|
274
|
+
if not msg.HasField(field_name):
|
|
275
|
+
return None
|
|
276
|
+
details = getattr(msg, field_name)
|
|
277
|
+
return task.FailureDetails(
|
|
278
|
+
details.errorMessage,
|
|
279
|
+
details.errorType,
|
|
280
|
+
details.stackTrace.value if details.HasField('stackTrace') else None,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _trace_context(msg: Message, field_name: str) -> Optional[TraceContext]:
|
|
285
|
+
if not msg.HasField(field_name):
|
|
286
|
+
return None
|
|
287
|
+
value = getattr(msg, field_name)
|
|
288
|
+
return TraceContext(
|
|
289
|
+
trace_parent=value.traceParent,
|
|
290
|
+
span_id=value.spanID,
|
|
291
|
+
trace_state=value.traceState.value if value.HasField('traceState') else None,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _orchestration_instance(msg: Message, field_name: str) -> Optional[OrchestrationInstance]:
|
|
296
|
+
if not msg.HasField(field_name):
|
|
297
|
+
return None
|
|
298
|
+
value = getattr(msg, field_name)
|
|
299
|
+
return OrchestrationInstance(
|
|
300
|
+
instance_id=value.instanceId,
|
|
301
|
+
execution_id=value.executionId.value if value.HasField('executionId') else None,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _parent_instance(msg: Message, field_name: str) -> Optional[ParentInstanceInfo]:
|
|
306
|
+
if not msg.HasField(field_name):
|
|
307
|
+
return None
|
|
308
|
+
value = getattr(msg, field_name)
|
|
309
|
+
orchestration_instance = None
|
|
310
|
+
if value.HasField('orchestrationInstance'):
|
|
311
|
+
orchestration_instance = OrchestrationInstance(
|
|
312
|
+
instance_id=value.orchestrationInstance.instanceId,
|
|
313
|
+
execution_id=value.orchestrationInstance.executionId.value
|
|
314
|
+
if value.orchestrationInstance.HasField('executionId') else None,
|
|
315
|
+
)
|
|
316
|
+
return ParentInstanceInfo(
|
|
317
|
+
task_scheduled_id=value.taskScheduledId,
|
|
318
|
+
name=value.name.value if value.HasField('name') else None,
|
|
319
|
+
version=value.version.value if value.HasField('version') else None,
|
|
320
|
+
orchestration_instance=orchestration_instance,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _message_to_dict(msg: Message) -> dict[str, Any]:
|
|
325
|
+
return json_format.MessageToDict(msg, preserving_proto_field_name=True)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _to_serializable(value: Any) -> Any:
|
|
329
|
+
if isinstance(value, datetime):
|
|
330
|
+
return value.isoformat()
|
|
331
|
+
if isinstance(value, list):
|
|
332
|
+
return [_to_serializable(item) for item in value]
|
|
333
|
+
if isinstance(value, dict):
|
|
334
|
+
return {key: _to_serializable(item) for key, item in value.items()}
|
|
335
|
+
return value
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
_EVENT_CONVERTERS: dict[str, Any] = {
|
|
339
|
+
'executionStarted': lambda event: ExecutionStartedEvent(
|
|
340
|
+
**_base_kwargs(event),
|
|
341
|
+
name=event.executionStarted.name,
|
|
342
|
+
version=_string_value(event.executionStarted, 'version'),
|
|
343
|
+
input=_string_value(event.executionStarted, 'input'),
|
|
344
|
+
orchestration_instance=_orchestration_instance(event.executionStarted, 'orchestrationInstance'),
|
|
345
|
+
parent_instance=_parent_instance(event.executionStarted, 'parentInstance'),
|
|
346
|
+
scheduled_start_timestamp=_timestamp_value(event.executionStarted, 'scheduledStartTimestamp'),
|
|
347
|
+
parent_trace_context=_trace_context(event.executionStarted, 'parentTraceContext'),
|
|
348
|
+
orchestration_span_id=_string_value(event.executionStarted, 'orchestrationSpanID'),
|
|
349
|
+
tags=dict(event.executionStarted.tags) if event.executionStarted.tags else None,
|
|
350
|
+
),
|
|
351
|
+
'executionCompleted': lambda event: ExecutionCompletedEvent(
|
|
352
|
+
**_base_kwargs(event),
|
|
353
|
+
orchestration_status=event.executionCompleted.orchestrationStatus,
|
|
354
|
+
result=_string_value(event.executionCompleted, 'result'),
|
|
355
|
+
failure_details=_failure_details(event.executionCompleted, 'failureDetails'),
|
|
356
|
+
),
|
|
357
|
+
'executionTerminated': lambda event: ExecutionTerminatedEvent(
|
|
358
|
+
**_base_kwargs(event),
|
|
359
|
+
input=_string_value(event.executionTerminated, 'input'),
|
|
360
|
+
recurse=event.executionTerminated.recurse,
|
|
361
|
+
),
|
|
362
|
+
'taskScheduled': lambda event: TaskScheduledEvent(
|
|
363
|
+
**_base_kwargs(event),
|
|
364
|
+
name=event.taskScheduled.name,
|
|
365
|
+
version=_string_value(event.taskScheduled, 'version'),
|
|
366
|
+
input=_string_value(event.taskScheduled, 'input'),
|
|
367
|
+
parent_trace_context=_trace_context(event.taskScheduled, 'parentTraceContext'),
|
|
368
|
+
tags=dict(event.taskScheduled.tags) if event.taskScheduled.tags else None,
|
|
369
|
+
),
|
|
370
|
+
'taskCompleted': lambda event: TaskCompletedEvent(
|
|
371
|
+
**_base_kwargs(event),
|
|
372
|
+
task_scheduled_id=event.taskCompleted.taskScheduledId,
|
|
373
|
+
result=_string_value(event.taskCompleted, 'result'),
|
|
374
|
+
),
|
|
375
|
+
'taskFailed': lambda event: TaskFailedEvent(
|
|
376
|
+
**_base_kwargs(event),
|
|
377
|
+
task_scheduled_id=event.taskFailed.taskScheduledId,
|
|
378
|
+
failure_details=_failure_details(event.taskFailed, 'failureDetails'),
|
|
379
|
+
),
|
|
380
|
+
'subOrchestrationInstanceCreated': lambda event: SubOrchestrationInstanceCreatedEvent(
|
|
381
|
+
**_base_kwargs(event),
|
|
382
|
+
instance_id=event.subOrchestrationInstanceCreated.instanceId,
|
|
383
|
+
name=event.subOrchestrationInstanceCreated.name,
|
|
384
|
+
version=_string_value(event.subOrchestrationInstanceCreated, 'version'),
|
|
385
|
+
input=_string_value(event.subOrchestrationInstanceCreated, 'input'),
|
|
386
|
+
parent_trace_context=_trace_context(event.subOrchestrationInstanceCreated, 'parentTraceContext'),
|
|
387
|
+
tags=dict(event.subOrchestrationInstanceCreated.tags) if event.subOrchestrationInstanceCreated.tags else None,
|
|
388
|
+
),
|
|
389
|
+
'subOrchestrationInstanceCompleted': lambda event: SubOrchestrationInstanceCompletedEvent(
|
|
390
|
+
**_base_kwargs(event),
|
|
391
|
+
task_scheduled_id=event.subOrchestrationInstanceCompleted.taskScheduledId,
|
|
392
|
+
result=_string_value(event.subOrchestrationInstanceCompleted, 'result'),
|
|
393
|
+
),
|
|
394
|
+
'subOrchestrationInstanceFailed': lambda event: SubOrchestrationInstanceFailedEvent(
|
|
395
|
+
**_base_kwargs(event),
|
|
396
|
+
task_scheduled_id=event.subOrchestrationInstanceFailed.taskScheduledId,
|
|
397
|
+
failure_details=_failure_details(event.subOrchestrationInstanceFailed, 'failureDetails'),
|
|
398
|
+
),
|
|
399
|
+
'timerCreated': lambda event: TimerCreatedEvent(
|
|
400
|
+
**_base_kwargs(event),
|
|
401
|
+
fire_at=event.timerCreated.fireAt.ToDatetime(timezone.utc),
|
|
402
|
+
),
|
|
403
|
+
'timerFired': lambda event: TimerFiredEvent(
|
|
404
|
+
**_base_kwargs(event),
|
|
405
|
+
fire_at=event.timerFired.fireAt.ToDatetime(timezone.utc),
|
|
406
|
+
timer_id=event.timerFired.timerId,
|
|
407
|
+
),
|
|
408
|
+
'orchestratorStarted': lambda event: OrchestratorStartedEvent(**_base_kwargs(event)),
|
|
409
|
+
'orchestratorCompleted': lambda event: OrchestratorCompletedEvent(**_base_kwargs(event)),
|
|
410
|
+
'eventSent': lambda event: EventSentEvent(
|
|
411
|
+
**_base_kwargs(event),
|
|
412
|
+
instance_id=event.eventSent.instanceId,
|
|
413
|
+
name=event.eventSent.name,
|
|
414
|
+
input=_string_value(event.eventSent, 'input'),
|
|
415
|
+
),
|
|
416
|
+
'eventRaised': lambda event: EventRaisedEvent(
|
|
417
|
+
**_base_kwargs(event),
|
|
418
|
+
name=event.eventRaised.name,
|
|
419
|
+
input=_string_value(event.eventRaised, 'input'),
|
|
420
|
+
),
|
|
421
|
+
'genericEvent': lambda event: GenericEvent(
|
|
422
|
+
**_base_kwargs(event),
|
|
423
|
+
data=_string_value(event.genericEvent, 'data'),
|
|
424
|
+
),
|
|
425
|
+
'historyState': lambda event: HistoryStateEvent(
|
|
426
|
+
**_base_kwargs(event),
|
|
427
|
+
orchestration_state=_message_to_dict(event.historyState.orchestrationState),
|
|
428
|
+
),
|
|
429
|
+
'continueAsNew': lambda event: ContinueAsNewEvent(
|
|
430
|
+
**_base_kwargs(event),
|
|
431
|
+
input=_string_value(event.continueAsNew, 'input'),
|
|
432
|
+
),
|
|
433
|
+
'executionSuspended': lambda event: ExecutionSuspendedEvent(
|
|
434
|
+
**_base_kwargs(event),
|
|
435
|
+
input=_string_value(event.executionSuspended, 'input'),
|
|
436
|
+
),
|
|
437
|
+
'executionResumed': lambda event: ExecutionResumedEvent(
|
|
438
|
+
**_base_kwargs(event),
|
|
439
|
+
input=_string_value(event.executionResumed, 'input'),
|
|
440
|
+
),
|
|
441
|
+
'entityOperationSignaled': lambda event: EntityOperationSignaledEvent(
|
|
442
|
+
**_base_kwargs(event),
|
|
443
|
+
request_id=event.entityOperationSignaled.requestId,
|
|
444
|
+
operation=event.entityOperationSignaled.operation,
|
|
445
|
+
scheduled_time=_timestamp_value(event.entityOperationSignaled, 'scheduledTime'),
|
|
446
|
+
input=_string_value(event.entityOperationSignaled, 'input'),
|
|
447
|
+
target_instance_id=_string_value(event.entityOperationSignaled, 'targetInstanceId'),
|
|
448
|
+
),
|
|
449
|
+
'entityOperationCalled': lambda event: EntityOperationCalledEvent(
|
|
450
|
+
**_base_kwargs(event),
|
|
451
|
+
request_id=event.entityOperationCalled.requestId,
|
|
452
|
+
operation=event.entityOperationCalled.operation,
|
|
453
|
+
scheduled_time=_timestamp_value(event.entityOperationCalled, 'scheduledTime'),
|
|
454
|
+
input=_string_value(event.entityOperationCalled, 'input'),
|
|
455
|
+
parent_instance_id=_string_value(event.entityOperationCalled, 'parentInstanceId'),
|
|
456
|
+
parent_execution_id=_string_value(event.entityOperationCalled, 'parentExecutionId'),
|
|
457
|
+
target_instance_id=_string_value(event.entityOperationCalled, 'targetInstanceId'),
|
|
458
|
+
),
|
|
459
|
+
'entityOperationCompleted': lambda event: EntityOperationCompletedEvent(
|
|
460
|
+
**_base_kwargs(event),
|
|
461
|
+
request_id=event.entityOperationCompleted.requestId,
|
|
462
|
+
output=_string_value(event.entityOperationCompleted, 'output'),
|
|
463
|
+
),
|
|
464
|
+
'entityOperationFailed': lambda event: EntityOperationFailedEvent(
|
|
465
|
+
**_base_kwargs(event),
|
|
466
|
+
request_id=event.entityOperationFailed.requestId,
|
|
467
|
+
failure_details=_failure_details(event.entityOperationFailed, 'failureDetails'),
|
|
468
|
+
),
|
|
469
|
+
'entityLockRequested': lambda event: EntityLockRequestedEvent(
|
|
470
|
+
**_base_kwargs(event),
|
|
471
|
+
critical_section_id=event.entityLockRequested.criticalSectionId,
|
|
472
|
+
lock_set=list(event.entityLockRequested.lockSet),
|
|
473
|
+
position=event.entityLockRequested.position,
|
|
474
|
+
parent_instance_id=_string_value(event.entityLockRequested, 'parentInstanceId'),
|
|
475
|
+
),
|
|
476
|
+
'entityLockGranted': lambda event: EntityLockGrantedEvent(
|
|
477
|
+
**_base_kwargs(event),
|
|
478
|
+
critical_section_id=event.entityLockGranted.criticalSectionId,
|
|
479
|
+
),
|
|
480
|
+
'entityUnlockSent': lambda event: EntityUnlockSentEvent(
|
|
481
|
+
**_base_kwargs(event),
|
|
482
|
+
critical_section_id=event.entityUnlockSent.criticalSectionId,
|
|
483
|
+
parent_instance_id=_string_value(event.entityUnlockSent, 'parentInstanceId'),
|
|
484
|
+
target_instance_id=_string_value(event.entityUnlockSent, 'targetInstanceId'),
|
|
485
|
+
),
|
|
486
|
+
'executionRewound': lambda event: ExecutionRewoundEvent(
|
|
487
|
+
**_base_kwargs(event),
|
|
488
|
+
reason=_string_value(event.executionRewound, 'reason'),
|
|
489
|
+
parent_execution_id=_string_value(event.executionRewound, 'parentExecutionId'),
|
|
490
|
+
instance_id=_string_value(event.executionRewound, 'instanceId'),
|
|
491
|
+
parent_trace_context=_trace_context(event.executionRewound, 'parentTraceContext'),
|
|
492
|
+
name=_string_value(event.executionRewound, 'name'),
|
|
493
|
+
version=_string_value(event.executionRewound, 'version'),
|
|
494
|
+
input=_string_value(event.executionRewound, 'input'),
|
|
495
|
+
parent_instance=_parent_instance(event.executionRewound, 'parentInstance'),
|
|
496
|
+
tags=dict(event.executionRewound.tags) if event.executionRewound.tags else None,
|
|
497
|
+
),
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
__all__ = [
|
|
502
|
+
'ContinueAsNewEvent',
|
|
503
|
+
'EntityLockGrantedEvent',
|
|
504
|
+
'EntityLockRequestedEvent',
|
|
505
|
+
'EntityOperationCalledEvent',
|
|
506
|
+
'EntityOperationCompletedEvent',
|
|
507
|
+
'EntityOperationFailedEvent',
|
|
508
|
+
'EntityOperationSignaledEvent',
|
|
509
|
+
'EntityUnlockSentEvent',
|
|
510
|
+
'EventRaisedEvent',
|
|
511
|
+
'EventSentEvent',
|
|
512
|
+
'ExecutionCompletedEvent',
|
|
513
|
+
'ExecutionResumedEvent',
|
|
514
|
+
'ExecutionRewoundEvent',
|
|
515
|
+
'ExecutionStartedEvent',
|
|
516
|
+
'ExecutionSuspendedEvent',
|
|
517
|
+
'ExecutionTerminatedEvent',
|
|
518
|
+
'GenericEvent',
|
|
519
|
+
'HistoryEvent',
|
|
520
|
+
'HistoryStateEvent',
|
|
521
|
+
'OrchestrationInstance',
|
|
522
|
+
'OrchestratorCompletedEvent',
|
|
523
|
+
'OrchestratorStartedEvent',
|
|
524
|
+
'ParentInstanceInfo',
|
|
525
|
+
'SubOrchestrationInstanceCompletedEvent',
|
|
526
|
+
'SubOrchestrationInstanceCreatedEvent',
|
|
527
|
+
'SubOrchestrationInstanceFailedEvent',
|
|
528
|
+
'TaskCompletedEvent',
|
|
529
|
+
'TaskFailedEvent',
|
|
530
|
+
'TaskScheduledEvent',
|
|
531
|
+
'TimerCreatedEvent',
|
|
532
|
+
'TimerFiredEvent',
|
|
533
|
+
'TraceContext',
|
|
534
|
+
'to_dict',
|
|
535
|
+
]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import AsyncIterable, Iterable, Optional
|
|
7
|
+
|
|
8
|
+
import durabletask.history as history
|
|
9
|
+
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
10
|
+
from durabletask.payload import helpers as payload_helpers
|
|
11
|
+
from durabletask.payload.store import PayloadStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def collect_history_events(
|
|
15
|
+
chunks: Iterable[pb.HistoryChunk],
|
|
16
|
+
payload_store: Optional[PayloadStore] = None,
|
|
17
|
+
) -> list[history.HistoryEvent]:
|
|
18
|
+
events: list[history.HistoryEvent] = []
|
|
19
|
+
for chunk in chunks:
|
|
20
|
+
events.extend(_clone_and_convert_events(chunk.events, payload_store))
|
|
21
|
+
return events
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def collect_history_events_async(
|
|
25
|
+
chunks: AsyncIterable[pb.HistoryChunk],
|
|
26
|
+
payload_store: Optional[PayloadStore] = None,
|
|
27
|
+
) -> list[history.HistoryEvent]:
|
|
28
|
+
events: list[history.HistoryEvent] = []
|
|
29
|
+
async for chunk in chunks:
|
|
30
|
+
events.extend(await _clone_and_convert_events_async(chunk.events, payload_store))
|
|
31
|
+
return events
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def history_event_to_dict(event: history.HistoryEvent) -> dict:
|
|
35
|
+
return history.to_dict(event)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _clone_and_convert_events(
|
|
39
|
+
source_events: Iterable[pb.HistoryEvent],
|
|
40
|
+
payload_store: Optional[PayloadStore],
|
|
41
|
+
) -> list[history.HistoryEvent]:
|
|
42
|
+
events: list[history.HistoryEvent] = []
|
|
43
|
+
for source_event in source_events:
|
|
44
|
+
event = source_event
|
|
45
|
+
if payload_store is not None:
|
|
46
|
+
# deexternalize_payloads mutates messages in-place, so clone to avoid
|
|
47
|
+
# mutating protobuf instances owned by gRPC/deserializer internals.
|
|
48
|
+
event = pb.HistoryEvent()
|
|
49
|
+
event.CopyFrom(source_event)
|
|
50
|
+
payload_helpers.deexternalize_payloads(event, payload_store)
|
|
51
|
+
events.append(history._from_protobuf(event))
|
|
52
|
+
return events
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def _clone_and_convert_events_async(
|
|
56
|
+
source_events: Iterable[pb.HistoryEvent],
|
|
57
|
+
payload_store: Optional[PayloadStore],
|
|
58
|
+
) -> list[history.HistoryEvent]:
|
|
59
|
+
events: list[history.HistoryEvent] = []
|
|
60
|
+
for source_event in source_events:
|
|
61
|
+
event = source_event
|
|
62
|
+
if payload_store is not None:
|
|
63
|
+
# Async deexternalization mutates messages in-place, so clone first.
|
|
64
|
+
event = pb.HistoryEvent()
|
|
65
|
+
event.CopyFrom(source_event)
|
|
66
|
+
await payload_helpers.deexternalize_payloads_async(event, payload_store)
|
|
67
|
+
events.append(history._from_protobuf(event))
|
|
68
|
+
return events
|
|
@@ -10,6 +10,7 @@ unit testing and integration testing scenarios where a sidecar process
|
|
|
10
10
|
or external storage is not desired.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import bisect
|
|
13
14
|
import logging
|
|
14
15
|
import threading
|
|
15
16
|
import time
|
|
@@ -41,6 +42,7 @@ class OrchestrationInstance:
|
|
|
41
42
|
custom_status: Optional[str] = None
|
|
42
43
|
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
43
44
|
last_updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
45
|
+
completed_at: Optional[datetime] = None
|
|
44
46
|
failure_details: Optional[pb.TaskFailureDetails] = None
|
|
45
47
|
history: list[pb.HistoryEvent] = field(default_factory=list)
|
|
46
48
|
pending_events: list[pb.HistoryEvent] = field(default_factory=list)
|
|
@@ -97,6 +99,10 @@ class StateWaiter:
|
|
|
97
99
|
result: Optional[OrchestrationInstance] = None
|
|
98
100
|
|
|
99
101
|
|
|
102
|
+
_DEFAULT_PAGE_SIZE = 100
|
|
103
|
+
_TOKEN_SEP = '|'
|
|
104
|
+
|
|
105
|
+
|
|
100
106
|
class InMemoryOrchestrationBackend(stubs.TaskHubSidecarServiceServicer):
|
|
101
107
|
"""
|
|
102
108
|
In-memory backend for durable orchestrations suitable for testing.
|
|
@@ -238,6 +244,7 @@ class InMemoryOrchestrationBackend(stubs.TaskHubSidecarServiceServicer):
|
|
|
238
244
|
input=request.input.value if request.input else None,
|
|
239
245
|
created_at=now,
|
|
240
246
|
last_updated_at=now,
|
|
247
|
+
completed_at=None,
|
|
241
248
|
completion_token=self._next_completion_token,
|
|
242
249
|
tags=dict(request.tags) if request.tags else None,
|
|
243
250
|
)
|
|
@@ -453,6 +460,48 @@ class InMemoryOrchestrationBackend(stubs.TaskHubSidecarServiceServicer):
|
|
|
453
460
|
f"Restarted instance '{request.instanceId}' as '{new_instance_id}'")
|
|
454
461
|
return pb.RestartInstanceResponse(instanceId=new_instance_id)
|
|
455
462
|
|
|
463
|
+
def ListInstanceIds(self, request: pb.ListInstanceIdsRequest, context):
|
|
464
|
+
"""Lists terminal orchestration instance IDs with completion-time pagination."""
|
|
465
|
+
with self._lock:
|
|
466
|
+
matching = []
|
|
467
|
+
for instance in self._instances.values():
|
|
468
|
+
if not self._is_terminal_status(instance.status):
|
|
469
|
+
continue
|
|
470
|
+
if request.runtimeStatus and instance.status not in request.runtimeStatus:
|
|
471
|
+
continue
|
|
472
|
+
if instance.completed_at is None:
|
|
473
|
+
continue
|
|
474
|
+
if request.HasField("completedTimeFrom") and instance.completed_at < request.completedTimeFrom.ToDatetime(timezone.utc):
|
|
475
|
+
continue
|
|
476
|
+
if request.HasField("completedTimeTo") and instance.completed_at >= request.completedTimeTo.ToDatetime(timezone.utc):
|
|
477
|
+
continue
|
|
478
|
+
matching.append(instance)
|
|
479
|
+
|
|
480
|
+
matching.sort(key=lambda i: (i.completed_at, i.instance_id))
|
|
481
|
+
sort_keys = [(i.completed_at, i.instance_id) for i in matching]
|
|
482
|
+
|
|
483
|
+
start_index = 0
|
|
484
|
+
if request.HasField("lastInstanceKey") and request.lastInstanceKey.value:
|
|
485
|
+
token = request.lastInstanceKey.value
|
|
486
|
+
sep_idx = token.index(_TOKEN_SEP)
|
|
487
|
+
token_ts = datetime.fromisoformat(token[:sep_idx]).replace(tzinfo=timezone.utc)
|
|
488
|
+
token_id = token[sep_idx + 1:]
|
|
489
|
+
# bisect_right positions us just after the cursor entry
|
|
490
|
+
start_index = bisect.bisect_right(sort_keys, (token_ts, token_id))
|
|
491
|
+
|
|
492
|
+
page_size = request.pageSize if request.pageSize > 0 else _DEFAULT_PAGE_SIZE
|
|
493
|
+
page = matching[start_index:start_index + page_size]
|
|
494
|
+
next_token = None
|
|
495
|
+
if start_index + page_size < len(matching) and page:
|
|
496
|
+
last = page[-1]
|
|
497
|
+
encoded = f"{last.completed_at.isoformat()}{_TOKEN_SEP}{last.instance_id}"
|
|
498
|
+
next_token = wrappers_pb2.StringValue(value=encoded)
|
|
499
|
+
|
|
500
|
+
return pb.ListInstanceIdsResponse(
|
|
501
|
+
instanceIds=[instance.instance_id for instance in page],
|
|
502
|
+
lastInstanceKey=next_token,
|
|
503
|
+
)
|
|
504
|
+
|
|
456
505
|
@staticmethod
|
|
457
506
|
def _parse_work_item_filters(request: pb.GetWorkItemsRequest):
|
|
458
507
|
"""Extract filters from the request.
|
|
@@ -1084,8 +1133,18 @@ class InMemoryOrchestrationBackend(stubs.TaskHubSidecarServiceServicer):
|
|
|
1084
1133
|
)
|
|
1085
1134
|
|
|
1086
1135
|
def StreamInstanceHistory(self, request: pb.StreamInstanceHistoryRequest, context):
|
|
1087
|
-
"""Streams
|
|
1088
|
-
|
|
1136
|
+
"""Streams orchestration history for an instance."""
|
|
1137
|
+
with self._lock:
|
|
1138
|
+
instance = self._instances.get(request.instanceId)
|
|
1139
|
+
if instance is None:
|
|
1140
|
+
context.abort(grpc.StatusCode.NOT_FOUND,
|
|
1141
|
+
f"Orchestration instance '{request.instanceId}' not found")
|
|
1142
|
+
return
|
|
1143
|
+
history = [self._clone_history_event(event) for event in instance.history]
|
|
1144
|
+
|
|
1145
|
+
chunk_size = 100
|
|
1146
|
+
for offset in range(0, len(history), chunk_size):
|
|
1147
|
+
yield pb.HistoryChunk(events=history[offset:offset + chunk_size])
|
|
1089
1148
|
|
|
1090
1149
|
def CreateTaskHub(self, request: pb.CreateTaskHubRequest, context):
|
|
1091
1150
|
"""Creates task hub resources (no-op for in-memory)."""
|
|
@@ -1178,6 +1237,7 @@ class InMemoryOrchestrationBackend(stubs.TaskHubSidecarServiceServicer):
|
|
|
1178
1237
|
input=encoded_input,
|
|
1179
1238
|
created_at=now,
|
|
1180
1239
|
last_updated_at=now,
|
|
1240
|
+
completed_at=None,
|
|
1181
1241
|
completion_token=self._next_completion_token,
|
|
1182
1242
|
)
|
|
1183
1243
|
self._next_completion_token += 1
|
|
@@ -1239,6 +1299,7 @@ class InMemoryOrchestrationBackend(stubs.TaskHubSidecarServiceServicer):
|
|
|
1239
1299
|
orchestrationStatus=instance.status,
|
|
1240
1300
|
createdTimestamp=created_ts,
|
|
1241
1301
|
lastUpdatedTimestamp=updated_ts,
|
|
1302
|
+
completedTimestamp=helpers.new_timestamp(instance.completed_at) if instance.completed_at else None,
|
|
1242
1303
|
input=wrappers_pb2.StringValue(value=instance.input) if include_payloads and instance.input else None,
|
|
1243
1304
|
output=wrappers_pb2.StringValue(value=instance.output) if include_payloads and instance.output else None,
|
|
1244
1305
|
customStatus=wrappers_pb2.StringValue(value=instance.custom_status) if instance.custom_status else None,
|
|
@@ -1319,6 +1380,7 @@ class InMemoryOrchestrationBackend(stubs.TaskHubSidecarServiceServicer):
|
|
|
1319
1380
|
instance.status = status
|
|
1320
1381
|
instance.output = complete_action.result.value if complete_action.result else None
|
|
1321
1382
|
instance.failure_details = complete_action.failureDetails if complete_action.failureDetails else None
|
|
1383
|
+
instance.completed_at = datetime.now(timezone.utc) if self._is_terminal_status(status) else None
|
|
1322
1384
|
|
|
1323
1385
|
if status == pb.ORCHESTRATION_STATUS_CONTINUED_AS_NEW:
|
|
1324
1386
|
# Handle continue-as-new
|
|
@@ -1336,6 +1398,7 @@ class InMemoryOrchestrationBackend(stubs.TaskHubSidecarServiceServicer):
|
|
|
1336
1398
|
instance.output = None
|
|
1337
1399
|
instance.failure_details = None
|
|
1338
1400
|
instance.status = pb.ORCHESTRATION_STATUS_PENDING
|
|
1401
|
+
instance.completed_at = None
|
|
1339
1402
|
|
|
1340
1403
|
# Save any events that arrived during the in-flight dispatch so
|
|
1341
1404
|
# they can be appended AFTER the new execution started events.
|
|
@@ -1357,6 +1420,12 @@ class InMemoryOrchestrationBackend(stubs.TaskHubSidecarServiceServicer):
|
|
|
1357
1420
|
|
|
1358
1421
|
self._enqueue_orchestration(instance.instance_id)
|
|
1359
1422
|
|
|
1423
|
+
@staticmethod
|
|
1424
|
+
def _clone_history_event(event: pb.HistoryEvent) -> pb.HistoryEvent:
|
|
1425
|
+
cloned_event = pb.HistoryEvent()
|
|
1426
|
+
cloned_event.CopyFrom(event)
|
|
1427
|
+
return cloned_event
|
|
1428
|
+
|
|
1360
1429
|
def _process_schedule_task_action(self, instance: OrchestrationInstance,
|
|
1361
1430
|
action: pb.OrchestratorAction):
|
|
1362
1431
|
"""Processes a schedule task action."""
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
durabletask/__init__.py
|
|
5
5
|
durabletask/client.py
|
|
6
|
+
durabletask/history.py
|
|
6
7
|
durabletask/py.typed
|
|
7
8
|
durabletask/task.py
|
|
8
9
|
durabletask/worker.py
|
|
@@ -27,6 +28,7 @@ durabletask/internal/entity_state_shim.py
|
|
|
27
28
|
durabletask/internal/exceptions.py
|
|
28
29
|
durabletask/internal/grpc_interceptor.py
|
|
29
30
|
durabletask/internal/helpers.py
|
|
31
|
+
durabletask/internal/history_helpers.py
|
|
30
32
|
durabletask/internal/json_encode_output_exception.py
|
|
31
33
|
durabletask/internal/orchestration_entity_context.py
|
|
32
34
|
durabletask/internal/orchestrator_service_pb2.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/extensions/azure_blob_payloads/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/extensions/azure_blob_payloads/options.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/json_encode_output_exception.py
RENAMED
|
File without changes
|
{durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/orchestration_entity_context.py
RENAMED
|
File without changes
|
{durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/orchestrator_service_pb2.py
RENAMED
|
File without changes
|
{durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/orchestrator_service_pb2.pyi
RENAMED
|
File without changes
|
{durabletask-1.4.0 → durabletask-1.4.0.dev31}/durabletask/internal/orchestrator_service_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|