durabletask 1.4.0.dev30__py3-none-any.whl → 1.4.0.dev32__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.
- durabletask/__init__.py +3 -0
- durabletask/client.py +127 -19
- durabletask/grpc_options.py +102 -0
- durabletask/history.py +535 -0
- durabletask/internal/history_helpers.py +68 -0
- durabletask/internal/shared.py +44 -10
- durabletask/testing/in_memory_backend.py +71 -2
- durabletask/worker.py +23 -7
- {durabletask-1.4.0.dev30.dist-info → durabletask-1.4.0.dev32.dist-info}/METADATA +1 -1
- {durabletask-1.4.0.dev30.dist-info → durabletask-1.4.0.dev32.dist-info}/RECORD +13 -10
- {durabletask-1.4.0.dev30.dist-info → durabletask-1.4.0.dev32.dist-info}/WHEEL +0 -0
- {durabletask-1.4.0.dev30.dist-info → durabletask-1.4.0.dev32.dist-info}/licenses/LICENSE +0 -0
- {durabletask-1.4.0.dev30.dist-info → durabletask-1.4.0.dev32.dist-info}/top_level.txt +0 -0
durabletask/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
"""Durable Task SDK for Python"""
|
|
5
5
|
|
|
6
|
+
from durabletask.grpc_options import GrpcChannelOptions, GrpcRetryPolicyOptions
|
|
6
7
|
from durabletask.payload.store import LargePayloadStorageOptions, PayloadStore
|
|
7
8
|
from durabletask.worker import (
|
|
8
9
|
ActivityWorkItemFilter,
|
|
@@ -17,6 +18,8 @@ __all__ = [
|
|
|
17
18
|
"ActivityWorkItemFilter",
|
|
18
19
|
"ConcurrencyOptions",
|
|
19
20
|
"EntityWorkItemFilter",
|
|
21
|
+
"GrpcChannelOptions",
|
|
22
|
+
"GrpcRetryPolicyOptions",
|
|
20
23
|
"LargePayloadStorageOptions",
|
|
21
24
|
"OrchestrationWorkItemFilter",
|
|
22
25
|
"PayloadStore",
|
durabletask/client.py
CHANGED
|
@@ -6,14 +6,17 @@ 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
|
|
17
|
+
from durabletask.grpc_options import GrpcChannelOptions
|
|
16
18
|
import durabletask.internal.helpers as helpers
|
|
19
|
+
import durabletask.internal.history_helpers as history_helpers
|
|
17
20
|
import durabletask.internal.orchestrator_service_pb2 as pb
|
|
18
21
|
import durabletask.internal.orchestrator_service_pb2_grpc as stubs
|
|
19
22
|
import durabletask.internal.shared as shared
|
|
@@ -37,6 +40,7 @@ from durabletask.payload.store import PayloadStore
|
|
|
37
40
|
|
|
38
41
|
TInput = TypeVar('TInput')
|
|
39
42
|
TOutput = TypeVar('TOutput')
|
|
43
|
+
TItem = TypeVar('TItem')
|
|
40
44
|
|
|
41
45
|
|
|
42
46
|
class OrchestrationStatus(Enum):
|
|
@@ -99,6 +103,12 @@ class PurgeInstancesResult:
|
|
|
99
103
|
is_complete: bool
|
|
100
104
|
|
|
101
105
|
|
|
106
|
+
@dataclass
|
|
107
|
+
class Page(Generic[TItem]):
|
|
108
|
+
items: List[TItem]
|
|
109
|
+
continuation_token: Optional[str]
|
|
110
|
+
|
|
111
|
+
|
|
102
112
|
@dataclass
|
|
103
113
|
class CleanEntityStorageResult:
|
|
104
114
|
empty_entities_removed: int
|
|
@@ -152,18 +162,22 @@ class TaskHubGrpcClient:
|
|
|
152
162
|
metadata: Optional[list[tuple[str, str]]] = None,
|
|
153
163
|
log_handler: Optional[logging.Handler] = None,
|
|
154
164
|
log_formatter: Optional[logging.Formatter] = None,
|
|
165
|
+
channel: Optional[grpc.Channel] = None,
|
|
155
166
|
secure_channel: bool = False,
|
|
156
167
|
interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
|
|
168
|
+
channel_options: Optional[GrpcChannelOptions] = None,
|
|
157
169
|
default_version: Optional[str] = None,
|
|
158
170
|
payload_store: Optional[PayloadStore] = None):
|
|
159
171
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
self._owns_channel = channel is None
|
|
173
|
+
if channel is None:
|
|
174
|
+
interceptors = prepare_sync_interceptors(metadata, interceptors)
|
|
175
|
+
channel = shared.get_grpc_channel(
|
|
176
|
+
host_address=host_address,
|
|
177
|
+
secure_channel=secure_channel,
|
|
178
|
+
interceptors=interceptors,
|
|
179
|
+
channel_options=channel_options,
|
|
180
|
+
)
|
|
167
181
|
self._channel = channel
|
|
168
182
|
self._stub = stubs.TaskHubSidecarServiceStub(channel)
|
|
169
183
|
self._logger = shared.get_logger("client", log_handler, log_formatter)
|
|
@@ -171,8 +185,15 @@ class TaskHubGrpcClient:
|
|
|
171
185
|
self._payload_store = payload_store
|
|
172
186
|
|
|
173
187
|
def close(self) -> None:
|
|
174
|
-
"""Closes the underlying gRPC channel.
|
|
175
|
-
|
|
188
|
+
"""Closes the underlying gRPC channel.
|
|
189
|
+
|
|
190
|
+
Only closes channels created internally. If a pre-configured channel
|
|
191
|
+
was passed via the ``channel`` constructor parameter, this method is
|
|
192
|
+
a no-op — the caller retains ownership and is responsible for closing
|
|
193
|
+
it.
|
|
194
|
+
"""
|
|
195
|
+
if self._owns_channel:
|
|
196
|
+
self._channel.close()
|
|
176
197
|
|
|
177
198
|
def schedule_new_orchestration(self, orchestrator: Union[task.Orchestrator[TInput, TOutput], str], *,
|
|
178
199
|
input: Optional[TInput] = None,
|
|
@@ -218,6 +239,44 @@ class TaskHubGrpcClient:
|
|
|
218
239
|
payload_helpers.deexternalize_payloads(res, self._payload_store)
|
|
219
240
|
return new_orchestration_state(req.instanceId, res)
|
|
220
241
|
|
|
242
|
+
def get_orchestration_history(self,
|
|
243
|
+
instance_id: str, *,
|
|
244
|
+
execution_id: Optional[str] = None,
|
|
245
|
+
for_work_item_processing: bool = False) -> List[history.HistoryEvent]:
|
|
246
|
+
req = pb.StreamInstanceHistoryRequest(
|
|
247
|
+
instanceId=instance_id,
|
|
248
|
+
executionId=helpers.get_string_value(execution_id),
|
|
249
|
+
forWorkItemProcessing=for_work_item_processing,
|
|
250
|
+
)
|
|
251
|
+
self._logger.info(f"Retrieving history for instance '{instance_id}'.")
|
|
252
|
+
stream = self._stub.StreamInstanceHistory(req)
|
|
253
|
+
return history_helpers.collect_history_events(stream, self._payload_store)
|
|
254
|
+
|
|
255
|
+
def list_instance_ids(self,
|
|
256
|
+
runtime_status: Optional[List[OrchestrationStatus]] = None,
|
|
257
|
+
completed_time_from: Optional[datetime] = None,
|
|
258
|
+
completed_time_to: Optional[datetime] = None,
|
|
259
|
+
page_size: Optional[int] = None,
|
|
260
|
+
continuation_token: Optional[str] = None) -> Page[str]:
|
|
261
|
+
req = pb.ListInstanceIdsRequest(
|
|
262
|
+
runtimeStatus=[status.value for status in runtime_status] if runtime_status else [],
|
|
263
|
+
completedTimeFrom=helpers.new_timestamp(completed_time_from) if completed_time_from else None,
|
|
264
|
+
completedTimeTo=helpers.new_timestamp(completed_time_to) if completed_time_to else None,
|
|
265
|
+
pageSize=page_size or 0,
|
|
266
|
+
lastInstanceKey=helpers.get_string_value(continuation_token),
|
|
267
|
+
)
|
|
268
|
+
self._logger.info(
|
|
269
|
+
"Listing terminal instance IDs with filters: "
|
|
270
|
+
f"runtime_status={[str(status) for status in runtime_status] if runtime_status else None}, "
|
|
271
|
+
f"completed_time_from={completed_time_from}, "
|
|
272
|
+
f"completed_time_to={completed_time_to}, "
|
|
273
|
+
f"page_size={page_size}, "
|
|
274
|
+
f"continuation_token={continuation_token}"
|
|
275
|
+
)
|
|
276
|
+
resp: pb.ListInstanceIdsResponse = self._stub.ListInstanceIds(req)
|
|
277
|
+
next_token = resp.lastInstanceKey.value if resp.HasField("lastInstanceKey") else None
|
|
278
|
+
return Page(items=list(resp.instanceIds), continuation_token=next_token)
|
|
279
|
+
|
|
221
280
|
def get_all_orchestration_states(self,
|
|
222
281
|
orchestration_query: Optional[OrchestrationQuery] = None
|
|
223
282
|
) -> List[OrchestrationState]:
|
|
@@ -433,18 +492,22 @@ class AsyncTaskHubGrpcClient:
|
|
|
433
492
|
metadata: Optional[list[tuple[str, str]]] = None,
|
|
434
493
|
log_handler: Optional[logging.Handler] = None,
|
|
435
494
|
log_formatter: Optional[logging.Formatter] = None,
|
|
495
|
+
channel: Optional[grpc.aio.Channel] = None,
|
|
436
496
|
secure_channel: bool = False,
|
|
437
497
|
interceptors: Optional[Sequence[shared.AsyncClientInterceptor]] = None,
|
|
498
|
+
channel_options: Optional[GrpcChannelOptions] = None,
|
|
438
499
|
default_version: Optional[str] = None,
|
|
439
500
|
payload_store: Optional[PayloadStore] = None):
|
|
440
501
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
502
|
+
self._owns_channel = channel is None
|
|
503
|
+
if channel is None:
|
|
504
|
+
interceptors = prepare_async_interceptors(metadata, interceptors)
|
|
505
|
+
channel = shared.get_async_grpc_channel(
|
|
506
|
+
host_address=host_address,
|
|
507
|
+
secure_channel=secure_channel,
|
|
508
|
+
interceptors=interceptors,
|
|
509
|
+
channel_options=channel_options,
|
|
510
|
+
)
|
|
448
511
|
self._channel = channel
|
|
449
512
|
self._stub = stubs.TaskHubSidecarServiceStub(channel)
|
|
450
513
|
self._logger = shared.get_logger("async_client", log_handler, log_formatter)
|
|
@@ -452,8 +515,15 @@ class AsyncTaskHubGrpcClient:
|
|
|
452
515
|
self._payload_store = payload_store
|
|
453
516
|
|
|
454
517
|
async def close(self) -> None:
|
|
455
|
-
"""Closes the underlying gRPC channel.
|
|
456
|
-
|
|
518
|
+
"""Closes the underlying gRPC channel.
|
|
519
|
+
|
|
520
|
+
Only closes channels created internally. If a pre-configured channel
|
|
521
|
+
was passed via the ``channel`` constructor parameter, this method is
|
|
522
|
+
a no-op — the caller retains ownership and is responsible for closing
|
|
523
|
+
it.
|
|
524
|
+
"""
|
|
525
|
+
if self._owns_channel:
|
|
526
|
+
await self._channel.close()
|
|
457
527
|
|
|
458
528
|
async def __aenter__(self):
|
|
459
529
|
return self
|
|
@@ -502,6 +572,44 @@ class AsyncTaskHubGrpcClient:
|
|
|
502
572
|
await payload_helpers.deexternalize_payloads_async(res, self._payload_store)
|
|
503
573
|
return new_orchestration_state(req.instanceId, res)
|
|
504
574
|
|
|
575
|
+
async def get_orchestration_history(self,
|
|
576
|
+
instance_id: str, *,
|
|
577
|
+
execution_id: Optional[str] = None,
|
|
578
|
+
for_work_item_processing: bool = False) -> List[history.HistoryEvent]:
|
|
579
|
+
req = pb.StreamInstanceHistoryRequest(
|
|
580
|
+
instanceId=instance_id,
|
|
581
|
+
executionId=helpers.get_string_value(execution_id),
|
|
582
|
+
forWorkItemProcessing=for_work_item_processing,
|
|
583
|
+
)
|
|
584
|
+
self._logger.info(f"Retrieving history for instance '{instance_id}'.")
|
|
585
|
+
stream = self._stub.StreamInstanceHistory(req)
|
|
586
|
+
return await history_helpers.collect_history_events_async(stream, self._payload_store)
|
|
587
|
+
|
|
588
|
+
async def list_instance_ids(self,
|
|
589
|
+
runtime_status: Optional[List[OrchestrationStatus]] = None,
|
|
590
|
+
completed_time_from: Optional[datetime] = None,
|
|
591
|
+
completed_time_to: Optional[datetime] = None,
|
|
592
|
+
page_size: Optional[int] = None,
|
|
593
|
+
continuation_token: Optional[str] = None) -> Page[str]:
|
|
594
|
+
req = pb.ListInstanceIdsRequest(
|
|
595
|
+
runtimeStatus=[status.value for status in runtime_status] if runtime_status else [],
|
|
596
|
+
completedTimeFrom=helpers.new_timestamp(completed_time_from) if completed_time_from else None,
|
|
597
|
+
completedTimeTo=helpers.new_timestamp(completed_time_to) if completed_time_to else None,
|
|
598
|
+
pageSize=page_size or 0,
|
|
599
|
+
lastInstanceKey=helpers.get_string_value(continuation_token),
|
|
600
|
+
)
|
|
601
|
+
self._logger.info(
|
|
602
|
+
"Listing terminal instance IDs with filters: "
|
|
603
|
+
f"runtime_status={[str(status) for status in runtime_status] if runtime_status else None}, "
|
|
604
|
+
f"completed_time_from={completed_time_from}, "
|
|
605
|
+
f"completed_time_to={completed_time_to}, "
|
|
606
|
+
f"page_size={page_size}, "
|
|
607
|
+
f"continuation_token={continuation_token}"
|
|
608
|
+
)
|
|
609
|
+
resp: pb.ListInstanceIdsResponse = await self._stub.ListInstanceIds(req)
|
|
610
|
+
next_token = resp.lastInstanceKey.value if resp.HasField("lastInstanceKey") else None
|
|
611
|
+
return Page(items=list(resp.instanceIds), continuation_token=next_token)
|
|
612
|
+
|
|
505
613
|
async def get_all_orchestration_states(self,
|
|
506
614
|
orchestration_query: Optional[OrchestrationQuery] = None
|
|
507
615
|
) -> List[OrchestrationState]:
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class GrpcRetryPolicyOptions:
|
|
13
|
+
"""Configuration for transport-level gRPC retries."""
|
|
14
|
+
|
|
15
|
+
max_attempts: int = 4
|
|
16
|
+
initial_backoff_seconds: float = 0.05
|
|
17
|
+
max_backoff_seconds: float = 0.25
|
|
18
|
+
backoff_multiplier: float = 2.0
|
|
19
|
+
retryable_status_codes: list[str] = field(default_factory=lambda: ["UNAVAILABLE"])
|
|
20
|
+
|
|
21
|
+
def __post_init__(self) -> None:
|
|
22
|
+
if self.max_attempts < 2:
|
|
23
|
+
raise ValueError("max_attempts must be >= 2")
|
|
24
|
+
if self.initial_backoff_seconds <= 0:
|
|
25
|
+
raise ValueError("initial_backoff_seconds must be > 0")
|
|
26
|
+
if self.max_backoff_seconds <= 0:
|
|
27
|
+
raise ValueError("max_backoff_seconds must be > 0")
|
|
28
|
+
if self.backoff_multiplier <= 0:
|
|
29
|
+
raise ValueError("backoff_multiplier must be > 0")
|
|
30
|
+
if self.max_backoff_seconds < self.initial_backoff_seconds:
|
|
31
|
+
raise ValueError("max_backoff_seconds must be >= initial_backoff_seconds")
|
|
32
|
+
if len(self.retryable_status_codes) == 0:
|
|
33
|
+
raise ValueError("retryable_status_codes cannot be empty")
|
|
34
|
+
# Validate that backoff values are representable as non-zero gRPC duration strings.
|
|
35
|
+
self._format_duration(self.initial_backoff_seconds)
|
|
36
|
+
self._format_duration(self.max_backoff_seconds)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _format_duration(seconds: float) -> str:
|
|
40
|
+
formatted = f"{seconds:.9f}".rstrip('0')
|
|
41
|
+
if formatted.endswith('.'):
|
|
42
|
+
formatted += '0'
|
|
43
|
+
if float(formatted) == 0:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Duration {seconds!r} rounds to zero; use a value large enough to "
|
|
46
|
+
"produce a non-zero gRPC duration string."
|
|
47
|
+
)
|
|
48
|
+
return f"{formatted}s"
|
|
49
|
+
|
|
50
|
+
def to_service_config(self) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"methodConfig": [
|
|
53
|
+
{
|
|
54
|
+
"name": [{}],
|
|
55
|
+
"retryPolicy": {
|
|
56
|
+
"maxAttempts": self.max_attempts,
|
|
57
|
+
"initialBackoff": self._format_duration(self.initial_backoff_seconds),
|
|
58
|
+
"maxBackoff": self._format_duration(self.max_backoff_seconds),
|
|
59
|
+
"backoffMultiplier": self.backoff_multiplier,
|
|
60
|
+
"retryableStatusCodes": self.retryable_status_codes,
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class GrpcChannelOptions:
|
|
69
|
+
"""Configuration for transport-level gRPC channel behavior."""
|
|
70
|
+
|
|
71
|
+
max_receive_message_length: Optional[int] = None
|
|
72
|
+
max_send_message_length: Optional[int] = None
|
|
73
|
+
keepalive_time_ms: Optional[int] = None
|
|
74
|
+
keepalive_timeout_ms: Optional[int] = None
|
|
75
|
+
keepalive_permit_without_calls: Optional[bool] = None
|
|
76
|
+
retry_policy: Optional[GrpcRetryPolicyOptions] = None
|
|
77
|
+
raw_options: list[tuple[str, Any]] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
def to_grpc_options(self) -> list[tuple[str, Any]]:
|
|
80
|
+
options = list(self.raw_options)
|
|
81
|
+
|
|
82
|
+
if self.max_receive_message_length is not None:
|
|
83
|
+
options.append(("grpc.max_receive_message_length", self.max_receive_message_length))
|
|
84
|
+
if self.max_send_message_length is not None:
|
|
85
|
+
options.append(("grpc.max_send_message_length", self.max_send_message_length))
|
|
86
|
+
if self.keepalive_time_ms is not None:
|
|
87
|
+
options.append(("grpc.keepalive_time_ms", self.keepalive_time_ms))
|
|
88
|
+
if self.keepalive_timeout_ms is not None:
|
|
89
|
+
options.append(("grpc.keepalive_timeout_ms", self.keepalive_timeout_ms))
|
|
90
|
+
if self.keepalive_permit_without_calls is not None:
|
|
91
|
+
options.append(
|
|
92
|
+
(
|
|
93
|
+
"grpc.keepalive_permit_without_calls",
|
|
94
|
+
1 if self.keepalive_permit_without_calls else 0,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if self.retry_policy is not None:
|
|
99
|
+
options.append(("grpc.enable_retries", 1))
|
|
100
|
+
options.append(("grpc.service_config", json.dumps(self.retry_policy.to_service_config())))
|
|
101
|
+
|
|
102
|
+
return options
|
durabletask/history.py
ADDED
|
@@ -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
|
durabletask/internal/shared.py
CHANGED
|
@@ -9,6 +9,7 @@ from typing import Any, Optional, Sequence, Union
|
|
|
9
9
|
|
|
10
10
|
import grpc
|
|
11
11
|
import grpc.aio
|
|
12
|
+
from durabletask.grpc_options import GrpcChannelOptions
|
|
12
13
|
|
|
13
14
|
ClientInterceptor = Union[
|
|
14
15
|
grpc.UnaryUnaryClientInterceptor,
|
|
@@ -39,7 +40,8 @@ def get_default_host_address() -> str:
|
|
|
39
40
|
def get_grpc_channel(
|
|
40
41
|
host_address: Optional[str],
|
|
41
42
|
secure_channel: bool = False,
|
|
42
|
-
interceptors: Optional[Sequence[ClientInterceptor]] = None
|
|
43
|
+
interceptors: Optional[Sequence[ClientInterceptor]] = None,
|
|
44
|
+
channel_options: Optional[GrpcChannelOptions] = None) -> grpc.Channel:
|
|
43
45
|
|
|
44
46
|
if host_address is None:
|
|
45
47
|
host_address = get_default_host_address()
|
|
@@ -59,10 +61,21 @@ def get_grpc_channel(
|
|
|
59
61
|
break
|
|
60
62
|
|
|
61
63
|
# Create the base channel
|
|
64
|
+
options = channel_options.to_grpc_options() if channel_options is not None else None
|
|
62
65
|
if secure_channel:
|
|
63
|
-
|
|
66
|
+
if options is None:
|
|
67
|
+
channel = grpc.secure_channel(host_address, grpc.ssl_channel_credentials())
|
|
68
|
+
else:
|
|
69
|
+
channel = grpc.secure_channel(
|
|
70
|
+
host_address,
|
|
71
|
+
grpc.ssl_channel_credentials(),
|
|
72
|
+
options=options,
|
|
73
|
+
)
|
|
64
74
|
else:
|
|
65
|
-
|
|
75
|
+
if options is None:
|
|
76
|
+
channel = grpc.insecure_channel(host_address)
|
|
77
|
+
else:
|
|
78
|
+
channel = grpc.insecure_channel(host_address, options=options)
|
|
66
79
|
|
|
67
80
|
# Apply interceptors ONLY if they exist
|
|
68
81
|
if interceptors:
|
|
@@ -73,7 +86,8 @@ def get_grpc_channel(
|
|
|
73
86
|
def get_async_grpc_channel(
|
|
74
87
|
host_address: Optional[str],
|
|
75
88
|
secure_channel: bool = False,
|
|
76
|
-
interceptors: Optional[Sequence[AsyncClientInterceptor]] = None
|
|
89
|
+
interceptors: Optional[Sequence[AsyncClientInterceptor]] = None,
|
|
90
|
+
channel_options: Optional[GrpcChannelOptions] = None) -> grpc.aio.Channel:
|
|
77
91
|
|
|
78
92
|
if host_address is None:
|
|
79
93
|
host_address = get_default_host_address()
|
|
@@ -90,14 +104,34 @@ def get_async_grpc_channel(
|
|
|
90
104
|
host_address = host_address[len(protocol):]
|
|
91
105
|
break
|
|
92
106
|
|
|
107
|
+
options = channel_options.to_grpc_options() if channel_options is not None else None
|
|
108
|
+
|
|
93
109
|
if secure_channel:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
if options is None:
|
|
111
|
+
channel = grpc.aio.secure_channel(
|
|
112
|
+
host_address,
|
|
113
|
+
grpc.ssl_channel_credentials(),
|
|
114
|
+
interceptors=interceptors,
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
channel = grpc.aio.secure_channel(
|
|
118
|
+
host_address,
|
|
119
|
+
grpc.ssl_channel_credentials(),
|
|
120
|
+
interceptors=interceptors,
|
|
121
|
+
options=options,
|
|
122
|
+
)
|
|
97
123
|
else:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
if options is None:
|
|
125
|
+
channel = grpc.aio.insecure_channel(
|
|
126
|
+
host_address,
|
|
127
|
+
interceptors=interceptors,
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
channel = grpc.aio.insecure_channel(
|
|
131
|
+
host_address,
|
|
132
|
+
interceptors=interceptors,
|
|
133
|
+
options=options,
|
|
134
|
+
)
|
|
101
135
|
|
|
102
136
|
return channel
|
|
103
137
|
|
|
@@ -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."""
|
durabletask/worker.py
CHANGED
|
@@ -21,6 +21,7 @@ from packaging.version import InvalidVersion, parse
|
|
|
21
21
|
import grpc
|
|
22
22
|
from google.protobuf import empty_pb2
|
|
23
23
|
|
|
24
|
+
from durabletask.grpc_options import GrpcChannelOptions
|
|
24
25
|
from durabletask.entities.entity_operation_failed_exception import EntityOperationFailedException
|
|
25
26
|
from durabletask.internal import helpers
|
|
26
27
|
from durabletask.internal.entity_state_shim import StateShim
|
|
@@ -361,8 +362,13 @@ class TaskHubGrpcWorker:
|
|
|
361
362
|
Defaults to None.
|
|
362
363
|
secure_channel (bool, optional): Whether to use a secure gRPC channel (TLS).
|
|
363
364
|
Defaults to False.
|
|
365
|
+
channel (Optional[grpc.Channel], optional): Pre-configured gRPC channel to use.
|
|
366
|
+
If set, host address, secure_channel, interceptors, and channel_options
|
|
367
|
+
are ignored when creating connections.
|
|
364
368
|
interceptors (Optional[Sequence[shared.ClientInterceptor]], optional): Custom gRPC
|
|
365
369
|
interceptors to apply to the channel. Defaults to None.
|
|
370
|
+
channel_options (Optional[GrpcChannelOptions], optional): Extra low-level gRPC
|
|
371
|
+
channel configuration including retry/service config options.
|
|
366
372
|
concurrency_options (Optional[ConcurrencyOptions], optional): Configuration for
|
|
367
373
|
controlling worker concurrency limits. If None, default settings are used.
|
|
368
374
|
|
|
@@ -426,8 +432,10 @@ class TaskHubGrpcWorker:
|
|
|
426
432
|
metadata: Optional[list[tuple[str, str]]] = None,
|
|
427
433
|
log_handler: Optional[logging.Handler] = None,
|
|
428
434
|
log_formatter: Optional[logging.Formatter] = None,
|
|
435
|
+
channel: Optional[grpc.Channel] = None,
|
|
429
436
|
secure_channel: bool = False,
|
|
430
437
|
interceptors: Optional[Sequence[shared.ClientInterceptor]] = None,
|
|
438
|
+
channel_options: Optional[GrpcChannelOptions] = None,
|
|
431
439
|
concurrency_options: Optional[ConcurrencyOptions] = None,
|
|
432
440
|
maximum_timer_interval: Optional[timedelta] = DEFAULT_MAXIMUM_TIMER_INTERVAL,
|
|
433
441
|
payload_store: Optional[PayloadStore] = None,
|
|
@@ -439,8 +447,10 @@ class TaskHubGrpcWorker:
|
|
|
439
447
|
self._logger = shared.get_logger("worker", log_handler, log_formatter)
|
|
440
448
|
self._shutdown = Event()
|
|
441
449
|
self._is_running = False
|
|
450
|
+
self._channel = channel
|
|
442
451
|
self._secure_channel = secure_channel
|
|
443
452
|
self._payload_store = payload_store
|
|
453
|
+
self._channel_options = channel_options
|
|
444
454
|
|
|
445
455
|
# Use provided concurrency options or create default ones
|
|
446
456
|
self._concurrency_options = (
|
|
@@ -590,7 +600,7 @@ class TaskHubGrpcWorker:
|
|
|
590
600
|
|
|
591
601
|
def create_fresh_connection():
|
|
592
602
|
nonlocal current_channel, current_stub, conn_retry_count
|
|
593
|
-
if current_channel:
|
|
603
|
+
if current_channel and self._channel is None:
|
|
594
604
|
try:
|
|
595
605
|
current_channel.close()
|
|
596
606
|
except Exception:
|
|
@@ -598,16 +608,22 @@ class TaskHubGrpcWorker:
|
|
|
598
608
|
current_channel = None
|
|
599
609
|
current_stub = None
|
|
600
610
|
try:
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
611
|
+
if self._channel is not None:
|
|
612
|
+
current_channel = self._channel
|
|
613
|
+
else:
|
|
614
|
+
current_channel = shared.get_grpc_channel(
|
|
615
|
+
self._host_address,
|
|
616
|
+
self._secure_channel,
|
|
617
|
+
self._interceptors,
|
|
618
|
+
channel_options=self._channel_options,
|
|
619
|
+
)
|
|
604
620
|
current_stub = stubs.TaskHubSidecarServiceStub(current_channel)
|
|
605
621
|
current_stub.Hello(empty_pb2.Empty())
|
|
606
622
|
conn_retry_count = 0
|
|
607
623
|
self._logger.info(f"Created fresh connection to {self._host_address}")
|
|
608
624
|
except Exception as e:
|
|
609
625
|
self._logger.warning(f"Failed to create connection: {e}")
|
|
610
|
-
current_channel = None
|
|
626
|
+
current_channel = self._channel if self._channel is not None else None
|
|
611
627
|
current_stub = None
|
|
612
628
|
raise
|
|
613
629
|
|
|
@@ -632,12 +648,12 @@ class TaskHubGrpcWorker:
|
|
|
632
648
|
current_reader_thread = None
|
|
633
649
|
|
|
634
650
|
# Close the channel
|
|
635
|
-
if current_channel:
|
|
651
|
+
if current_channel and self._channel is None:
|
|
636
652
|
try:
|
|
637
653
|
current_channel.close()
|
|
638
654
|
except Exception:
|
|
639
655
|
pass
|
|
640
|
-
current_channel = None
|
|
656
|
+
current_channel = self._channel if self._channel is not None else None
|
|
641
657
|
current_stub = None
|
|
642
658
|
|
|
643
659
|
def should_invalidate_connection(rpc_error):
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
durabletask/__init__.py,sha256=
|
|
2
|
-
durabletask/client.py,sha256=
|
|
1
|
+
durabletask/__init__.py,sha256=GhLZFOSOVlP7Jomi6f-nNYiUxDf5a2wNylkBAcshEBA,780
|
|
2
|
+
durabletask/client.py,sha256=PUOTYi0Kw7JoREbz-W8Gw5tjN_VCZxBLMqLq0T97unQ,40086
|
|
3
|
+
durabletask/grpc_options.py,sha256=k1xXnKzK5b_86wibuh7Ype396A-Gs6P4nbHBaUSpE9U,4174
|
|
4
|
+
durabletask/history.py,sha256=VhrWb5eFr9TZSaazpotdMzKt2quwLuJVRk9j39WbTPs,18680
|
|
3
5
|
durabletask/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
6
|
durabletask/task.py,sha256=QS7RPUaWhb4Xq5hi7nnueooJqQSDO6ipEoiAiSVkAIs,24009
|
|
5
|
-
durabletask/worker.py,sha256=
|
|
7
|
+
durabletask/worker.py,sha256=ZNx_85fGjgUkjJK_dEI2AZRB4qM3XMV6hgPlc3PPdFk,134691
|
|
6
8
|
durabletask/entities/__init__.py,sha256=DbNd5riqWZaj3tG6gN82O8Q6wTmFpe6QaH0pQgDSPHs,721
|
|
7
9
|
durabletask/entities/durable_entity.py,sha256=LQPWnUlRsHiFVRoTdpeSK--eXtjf2UGbVQwEEKf7QwI,3318
|
|
8
10
|
durabletask/entities/entity_context.py,sha256=U-B3i9QP34N-6Fikx_tMp8zo0YLdmwhwpdxwjHd7z-M,5346
|
|
@@ -19,21 +21,22 @@ durabletask/internal/entity_state_shim.py,sha256=Z_LUy7wYKELg3vBvti6hKRthRpfXqwL
|
|
|
19
21
|
durabletask/internal/exceptions.py,sha256=G1qnH5KReJk04_eewMIr2TTXvdeWDriq-SiAwSahMMs,298
|
|
20
22
|
durabletask/internal/grpc_interceptor.py,sha256=ZWNwIBCy-TzVr4KjuPqzgKsWDHvGPUbwWlw4NF0T4A0,5820
|
|
21
23
|
durabletask/internal/helpers.py,sha256=FVDJypvUmOKKcosRoh8N7rs-UL-ai15KN_IBC-XkU50,12772
|
|
24
|
+
durabletask/internal/history_helpers.py,sha256=myZQoN--CNaJqEr7jGyLJRBd0QvpVtZbxILArASgYow,2463
|
|
22
25
|
durabletask/internal/json_encode_output_exception.py,sha256=y13uVALM5pGQKMHEKy1npKHJgKGAjhAKoUcU-wZGYSg,426
|
|
23
26
|
durabletask/internal/orchestration_entity_context.py,sha256=zggVZ1L1YMm2eSzQy7m6jlag9XkP1DGgZVrZzJmr5yE,5410
|
|
24
27
|
durabletask/internal/orchestrator_service_pb2.py,sha256=K5iG8oguyGhTGt0YmkDsK9ZQz5tt7fFxgaFJKRru0w4,53149
|
|
25
28
|
durabletask/internal/orchestrator_service_pb2.pyi,sha256=TFa752MEoXPdK1fP2fDIIaqpGQJ0sbyXRoNg51hlegE,78590
|
|
26
29
|
durabletask/internal/orchestrator_service_pb2_grpc.py,sha256=Rk0QpGOc8fTeIGULB6xww9CtX6GiLGlZ3JMoZyW9Wuo,60845
|
|
27
30
|
durabletask/internal/proto_task_hub_sidecar_service_stub.py,sha256=pFlOCj1LhuhU5OKU4CZIuQCCsf9jilC9GXDqlEQYQ0U,1503
|
|
28
|
-
durabletask/internal/shared.py,sha256=
|
|
31
|
+
durabletask/internal/shared.py,sha256=00NrfM7dmdBQznLt0S9rfWoYWBTOXHWnmg3nemhyWco,6819
|
|
29
32
|
durabletask/internal/tracing.py,sha256=3cSanl0UcmGekItHcQ6tSl7wqSkS7_wOKzsDvJyS_ag,28821
|
|
30
33
|
durabletask/payload/__init__.py,sha256=1h68pQvgk8JUp5LBJuBq9W4GUPYkdlhqmCCQEg6YBYI,784
|
|
31
34
|
durabletask/payload/helpers.py,sha256=RYG5MEVAqHjm4zfFHs3Td91FVQHUoCcb5hbEJ4sYj5s,12350
|
|
32
35
|
durabletask/payload/store.py,sha256=3qJMvKxRUkr6ScWUzxpKAVgzuhFLywRW8a2_5OOmNk4,3000
|
|
33
36
|
durabletask/testing/__init__.py,sha256=rXbcSFtzuaRAbDNX-HmdgbxLTegvKJ1FRjZfSOIAMgA,323
|
|
34
|
-
durabletask/testing/in_memory_backend.py,sha256=
|
|
35
|
-
durabletask-1.4.0.
|
|
36
|
-
durabletask-1.4.0.
|
|
37
|
-
durabletask-1.4.0.
|
|
38
|
-
durabletask-1.4.0.
|
|
39
|
-
durabletask-1.4.0.
|
|
37
|
+
durabletask/testing/in_memory_backend.py,sha256=mF0zCA3GMLUd0vqa7CvkgBhYXH4VCsqHReIWuw72wvA,82530
|
|
38
|
+
durabletask-1.4.0.dev32.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
|
|
39
|
+
durabletask-1.4.0.dev32.dist-info/METADATA,sha256=6jrrQkmgSTCTRRdoeoFUUKpP0OUVNjB4inn5V4Ua9-M,4404
|
|
40
|
+
durabletask-1.4.0.dev32.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
41
|
+
durabletask-1.4.0.dev32.dist-info/top_level.txt,sha256=EBVyuKWnjOwq8bJI1Uvb9U3c4fzQxACWj9p83he6fik,12
|
|
42
|
+
durabletask-1.4.0.dev32.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|