durabletask.azuremanaged 1.7.2__tar.gz → 1.9.0__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_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/PKG-INFO +3 -3
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/internal/access_token_manager.py +37 -7
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/internal/durabletask_grpc_interceptor.py +22 -17
- durabletask_azuremanaged-1.9.0/durabletask/azuremanaged/preview/sandboxes/__init__.py +80 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/preview/sandboxes/profile_builder.py +92 -6
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/preview/sandboxes/worker.py +61 -21
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask.azuremanaged.egg-info/PKG-INFO +3 -3
- durabletask_azuremanaged-1.9.0/durabletask.azuremanaged.egg-info/requires.txt +5 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/pyproject.toml +3 -3
- durabletask_azuremanaged-1.7.2/durabletask/azuremanaged/preview/sandboxes/__init__.py +0 -32
- durabletask_azuremanaged-1.7.2/durabletask.azuremanaged.egg-info/requires.txt +0 -5
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/__init__.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/client.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/internal/py.typed +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/internal/sandbox_service_pb2.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/internal/sandbox_service_pb2.pyi +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/internal/sandbox_service_pb2_grpc.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/preview/__init__.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/preview/sandboxes/client.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/preview/sandboxes/helpers.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/preview/sandboxes/transport.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/preview/sandboxes/worker_messages.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/preview/sandboxes/worker_profiles.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/py.typed +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/worker.py +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask.azuremanaged.egg-info/SOURCES.txt +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask.azuremanaged.egg-info/dependency_links.txt +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask.azuremanaged.egg-info/top_level.txt +0 -0
- {durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durabletask.azuremanaged
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler
|
|
5
5
|
Project-URL: repository, https://github.com/microsoft/durabletask-python
|
|
6
6
|
Project-URL: changelog, https://github.com/microsoft/durabletask-python/blob/main/CHANGELOG.md
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: durabletask>=1.
|
|
13
|
+
Requires-Dist: durabletask>=1.9.0
|
|
14
14
|
Requires-Dist: azure-identity>=1.19.0
|
|
15
15
|
Provides-Extra: azure-blob-payloads
|
|
16
|
-
Requires-Dist: durabletask[azure-blob-payloads]>=1.
|
|
16
|
+
Requires-Dist: durabletask[azure-blob-payloads]>=1.9.0; extra == "azure-blob-payloads"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Copyright (c) Microsoft Corporation.
|
|
2
2
|
# Licensed under the MIT License.
|
|
3
|
+
import asyncio
|
|
3
4
|
from datetime import datetime, timedelta, timezone
|
|
4
5
|
from threading import Lock
|
|
5
6
|
|
|
@@ -13,6 +14,7 @@ import durabletask.internal.shared as shared
|
|
|
13
14
|
class AccessTokenManager:
|
|
14
15
|
|
|
15
16
|
_token: AccessToken | None
|
|
17
|
+
expiry_time: datetime | None
|
|
16
18
|
|
|
17
19
|
def __init__(self, token_credential: TokenCredential | None, refresh_interval_seconds: int = 600):
|
|
18
20
|
self._scope = "https://durabletask.io/.default"
|
|
@@ -22,12 +24,12 @@ class AccessTokenManager:
|
|
|
22
24
|
self._credential = token_credential
|
|
23
25
|
self._refresh_lock = Lock()
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
# Token acquisition is deferred to the first get_access_token() call so that
|
|
28
|
+
# constructing a client or worker does not perform a blocking credential round
|
|
29
|
+
# trip. The deferred first acquisition still goes through the double-checked
|
|
30
|
+
# refresh lock below, so it remains single-flight across threads.
|
|
31
|
+
self._token = None
|
|
32
|
+
self.expiry_time = None
|
|
31
33
|
|
|
32
34
|
def get_access_token(self) -> AccessToken | None:
|
|
33
35
|
if self._token is None or self.is_token_expired():
|
|
@@ -70,9 +72,37 @@ class AsyncAccessTokenManager:
|
|
|
70
72
|
self._token = None
|
|
71
73
|
self.expiry_time = None
|
|
72
74
|
|
|
75
|
+
# An asyncio.Lock binds itself to the event loop it is first used on, and this
|
|
76
|
+
# manager may outlive a loop or be shared across loops. Locks are therefore
|
|
77
|
+
# created lazily per running loop, guarded by a plain threading lock because
|
|
78
|
+
# different loops may run on different threads.
|
|
79
|
+
self._refresh_locks: dict[asyncio.AbstractEventLoop, asyncio.Lock] = {}
|
|
80
|
+
self._refresh_locks_guard = Lock()
|
|
81
|
+
|
|
82
|
+
def _get_refresh_lock(self) -> asyncio.Lock:
|
|
83
|
+
loop = asyncio.get_running_loop()
|
|
84
|
+
with self._refresh_locks_guard:
|
|
85
|
+
lock = self._refresh_locks.get(loop)
|
|
86
|
+
if lock is None:
|
|
87
|
+
# Discard locks belonging to loops that are no longer usable so the
|
|
88
|
+
# mapping does not grow without bound.
|
|
89
|
+
stale_loops = [
|
|
90
|
+
existing for existing in self._refresh_locks if existing.is_closed()
|
|
91
|
+
]
|
|
92
|
+
for stale_loop in stale_loops:
|
|
93
|
+
del self._refresh_locks[stale_loop]
|
|
94
|
+
lock = asyncio.Lock()
|
|
95
|
+
self._refresh_locks[loop] = lock
|
|
96
|
+
return lock
|
|
97
|
+
|
|
73
98
|
async def get_access_token(self) -> AccessToken | None:
|
|
74
99
|
if self._token is None or self.is_token_expired():
|
|
75
|
-
|
|
100
|
+
async with self._get_refresh_lock():
|
|
101
|
+
# Re-check under the lock: a concurrent caller may have already
|
|
102
|
+
# refreshed the token while this one was waiting, so only a single
|
|
103
|
+
# credential request is made per refresh window.
|
|
104
|
+
if self._token is None or self.is_token_expired():
|
|
105
|
+
await self.refresh_token()
|
|
76
106
|
return self._token
|
|
77
107
|
|
|
78
108
|
def is_token_expired(self) -> bool:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright (c) Microsoft Corporation.
|
|
2
2
|
# Licensed under the MIT License.
|
|
3
3
|
|
|
4
|
+
from functools import lru_cache
|
|
4
5
|
from importlib.metadata import version
|
|
5
6
|
|
|
6
7
|
import grpc
|
|
@@ -17,6 +18,22 @@ from durabletask.internal.grpc_interceptor import (
|
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
@lru_cache(maxsize=1)
|
|
22
|
+
def _get_sdk_version() -> str:
|
|
23
|
+
"""Return the installed version of the azuremanaged package.
|
|
24
|
+
|
|
25
|
+
Resolving the version walks distribution metadata on disk, so the result is
|
|
26
|
+
cached and shared by every interceptor instance instead of being recomputed
|
|
27
|
+
on each client or worker construction. Falls back to ``"unknown"`` when the
|
|
28
|
+
version cannot be determined.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
return version('durabletask-azuremanaged')
|
|
32
|
+
except Exception:
|
|
33
|
+
# Fallback if version cannot be determined
|
|
34
|
+
return "unknown"
|
|
35
|
+
|
|
36
|
+
|
|
20
37
|
class DTSDefaultClientInterceptorImpl (DefaultClientInterceptorImpl):
|
|
21
38
|
"""The class implements a UnaryUnaryClientInterceptor, UnaryStreamClientInterceptor,
|
|
22
39
|
StreamUnaryClientInterceptor and StreamStreamClientInterceptor from grpc to add an
|
|
@@ -27,13 +44,7 @@ class DTSDefaultClientInterceptorImpl (DefaultClientInterceptorImpl):
|
|
|
27
44
|
token_credential: TokenCredential | None,
|
|
28
45
|
taskhub_name: str,
|
|
29
46
|
worker_id: str | None = None):
|
|
30
|
-
|
|
31
|
-
# Get the version of the azuremanaged package
|
|
32
|
-
sdk_version = version('durabletask-azuremanaged')
|
|
33
|
-
except Exception:
|
|
34
|
-
# Fallback if version cannot be determined
|
|
35
|
-
sdk_version = "unknown"
|
|
36
|
-
user_agent = f"durabletask-python/{sdk_version}"
|
|
47
|
+
user_agent = f"durabletask-python/{_get_sdk_version()}"
|
|
37
48
|
self._metadata = [
|
|
38
49
|
("taskhub", taskhub_name),
|
|
39
50
|
("x-user-agent", user_agent)] # 'user-agent' is a reserved header; use 'x-user-agent'
|
|
@@ -41,13 +52,13 @@ class DTSDefaultClientInterceptorImpl (DefaultClientInterceptorImpl):
|
|
|
41
52
|
self._metadata.append(("workerid", worker_id))
|
|
42
53
|
super().__init__(self._metadata)
|
|
43
54
|
|
|
55
|
+
# Token acquisition is deferred to the first _intercept_call invocation rather
|
|
56
|
+
# than happening in __init__, so that constructing a client or worker does not
|
|
57
|
+
# block on a credential round trip before any RPC is made.
|
|
44
58
|
self._token_manager = None
|
|
45
59
|
if token_credential is not None:
|
|
46
60
|
self._token_credential = token_credential
|
|
47
61
|
self._token_manager = AccessTokenManager(token_credential=self._token_credential)
|
|
48
|
-
access_token = self._token_manager.get_access_token()
|
|
49
|
-
if access_token is not None:
|
|
50
|
-
self._upsert_authorization_header(access_token.token)
|
|
51
62
|
|
|
52
63
|
def _upsert_authorization_header(self, token: str) -> None:
|
|
53
64
|
found = False
|
|
@@ -81,13 +92,7 @@ class DTSAsyncDefaultClientInterceptorImpl(DefaultAsyncClientInterceptorImpl):
|
|
|
81
92
|
(task hub name, user agent, and authentication token) to all async calls."""
|
|
82
93
|
|
|
83
94
|
def __init__(self, token_credential: AsyncTokenCredential | None, taskhub_name: str):
|
|
84
|
-
|
|
85
|
-
# Get the version of the azuremanaged package
|
|
86
|
-
sdk_version = version('durabletask-azuremanaged')
|
|
87
|
-
except Exception:
|
|
88
|
-
# Fallback if version cannot be determined
|
|
89
|
-
sdk_version = "unknown"
|
|
90
|
-
user_agent = f"durabletask-python/{sdk_version}"
|
|
95
|
+
user_agent = f"durabletask-python/{_get_sdk_version()}"
|
|
91
96
|
self._metadata = [
|
|
92
97
|
("taskhub", taskhub_name),
|
|
93
98
|
("x-user-agent", user_agent)]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation.
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Sandbox preview APIs for Durable Task Scheduler.
|
|
5
|
+
|
|
6
|
+
This extension provides preview APIs for declaring sandbox activity
|
|
7
|
+
worker images and running a Python activity worker inside a Durable Task
|
|
8
|
+
Scheduler-launched sandbox.
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
from durabletask.azuremanaged.preview.sandboxes import (
|
|
13
|
+
SandboxWorker,
|
|
14
|
+
SandboxActivitiesClient,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
The exports below are resolved lazily on first attribute access, so importing
|
|
18
|
+
this package does not load the sandbox worker runtime (and its Azure Identity
|
|
19
|
+
and gRPC dependencies) unless those APIs are actually used.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from importlib import import_module
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from durabletask.azuremanaged.preview.sandboxes.client import SandboxActivitiesClient
|
|
27
|
+
from durabletask.azuremanaged.preview.sandboxes.helpers import SandboxActivity
|
|
28
|
+
from durabletask.azuremanaged.preview.sandboxes.worker_profiles import SandboxWorkerProfile
|
|
29
|
+
from durabletask.azuremanaged.preview.sandboxes.worker_profiles import SandboxWorkerProfileOptions
|
|
30
|
+
from durabletask.azuremanaged.preview.sandboxes.worker_profiles import sandbox_worker_profile
|
|
31
|
+
from durabletask.azuremanaged.preview.sandboxes.worker import SandboxWorker
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"SandboxWorker",
|
|
35
|
+
"SandboxActivity",
|
|
36
|
+
"SandboxWorkerProfile",
|
|
37
|
+
"SandboxWorkerProfileOptions",
|
|
38
|
+
"SandboxActivitiesClient",
|
|
39
|
+
"sandbox_worker_profile",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Public export name -> submodule of this package that defines it.
|
|
43
|
+
_LAZY_EXPORTS: dict[str, str] = {
|
|
44
|
+
"SandboxWorker": "worker",
|
|
45
|
+
"SandboxActivity": "helpers",
|
|
46
|
+
"SandboxWorkerProfile": "worker_profiles",
|
|
47
|
+
"SandboxWorkerProfileOptions": "worker_profiles",
|
|
48
|
+
"SandboxActivitiesClient": "client",
|
|
49
|
+
"sandbox_worker_profile": "worker_profiles",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Submodules that eager imports previously bound as attributes of this package.
|
|
53
|
+
# They remain reachable through attribute access without an explicit import.
|
|
54
|
+
_LAZY_SUBMODULES: frozenset[str] = frozenset({
|
|
55
|
+
"client",
|
|
56
|
+
"helpers",
|
|
57
|
+
"profile_builder",
|
|
58
|
+
"transport",
|
|
59
|
+
"worker",
|
|
60
|
+
"worker_messages",
|
|
61
|
+
"worker_profiles",
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def __getattr__(name: str) -> Any:
|
|
66
|
+
"""Import public sandbox exports on first access (PEP 562)."""
|
|
67
|
+
submodule = _LAZY_EXPORTS.get(name)
|
|
68
|
+
if submodule is not None:
|
|
69
|
+
value = getattr(import_module(f".{submodule}", __name__), name)
|
|
70
|
+
elif name in _LAZY_SUBMODULES:
|
|
71
|
+
value = import_module(f".{name}", __name__)
|
|
72
|
+
else:
|
|
73
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
74
|
+
|
|
75
|
+
globals()[name] = value
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def __dir__() -> list[str]:
|
|
80
|
+
return sorted(set(globals()) | set(__all__) | _LAZY_SUBMODULES)
|
|
@@ -7,7 +7,6 @@ from typing import Iterable, Optional
|
|
|
7
7
|
from durabletask.azuremanaged.internal import sandbox_service_pb2 as pb
|
|
8
8
|
from durabletask.azuremanaged.preview.sandboxes.helpers import (
|
|
9
9
|
SandboxActivity,
|
|
10
|
-
activities_overlap,
|
|
11
10
|
format_activity,
|
|
12
11
|
normalize_required,
|
|
13
12
|
resolve_activities,
|
|
@@ -91,22 +90,109 @@ def _build_sandbox_worker_profile(
|
|
|
91
90
|
return worker_profile
|
|
92
91
|
|
|
93
92
|
|
|
93
|
+
class _ActivityOwnerSlot:
|
|
94
|
+
"""Earliest worker profiles recorded for a single activity overlap bucket.
|
|
95
|
+
|
|
96
|
+
Only two owners ever need to be retained to answer "which worker profile
|
|
97
|
+
first claimed an activity in this bucket, ignoring `worker_profile_id`":
|
|
98
|
+
the very first owner, plus the first owner that differs from it.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
__slots__ = ("_first", "_first_other")
|
|
102
|
+
|
|
103
|
+
def __init__(self) -> None:
|
|
104
|
+
self._first: Optional[tuple[int, str]] = None
|
|
105
|
+
self._first_other: Optional[tuple[int, str]] = None
|
|
106
|
+
|
|
107
|
+
def add(self, order: int, worker_profile_id: str) -> None:
|
|
108
|
+
if self._first is None:
|
|
109
|
+
self._first = (order, worker_profile_id)
|
|
110
|
+
elif self._first_other is None and worker_profile_id != self._first[1]:
|
|
111
|
+
self._first_other = (order, worker_profile_id)
|
|
112
|
+
|
|
113
|
+
def first_owner_other_than(self, worker_profile_id: str) -> Optional[tuple[int, str]]:
|
|
114
|
+
if self._first is not None and self._first[1] != worker_profile_id:
|
|
115
|
+
return self._first
|
|
116
|
+
return self._first_other
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class _ActivityOwnerIndex:
|
|
120
|
+
"""Indexes activity ownership so overlap checks cost O(1) per activity.
|
|
121
|
+
|
|
122
|
+
Reproduces the semantics of
|
|
123
|
+
:func:`durabletask.azuremanaged.preview.sandboxes.helpers.activities_overlap`
|
|
124
|
+
exactly: activity names are compared case insensitively, an unversioned
|
|
125
|
+
activity overlaps every version of the same name, and two activities with
|
|
126
|
+
the same name overlap when their explicit versions are equal. Registration
|
|
127
|
+
order is tracked so the reported conflict matches the first overlapping
|
|
128
|
+
activity, exactly as a linear scan would report it.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(self) -> None:
|
|
132
|
+
self._registration_count = 0
|
|
133
|
+
self._by_name: dict[str, _ActivityOwnerSlot] = {}
|
|
134
|
+
self._by_name_and_version: dict[tuple[str, Optional[str]], _ActivityOwnerSlot] = {}
|
|
135
|
+
|
|
136
|
+
def find_conflicting_profile(
|
|
137
|
+
self,
|
|
138
|
+
activity: SandboxActivity,
|
|
139
|
+
worker_profile_id: str) -> Optional[str]:
|
|
140
|
+
"""Return the profile that first claimed an overlapping activity, if any."""
|
|
141
|
+
name_key = activity.name.casefold()
|
|
142
|
+
if activity.version is None:
|
|
143
|
+
# An unversioned activity overlaps every version of the same name.
|
|
144
|
+
owner = _first_owner_other_than(self._by_name.get(name_key), worker_profile_id)
|
|
145
|
+
else:
|
|
146
|
+
# A versioned activity overlaps the same version plus any
|
|
147
|
+
# unversioned registration of the same name.
|
|
148
|
+
owner = _earlier_owner(
|
|
149
|
+
_first_owner_other_than(
|
|
150
|
+
self._by_name_and_version.get((name_key, None)), worker_profile_id),
|
|
151
|
+
_first_owner_other_than(
|
|
152
|
+
self._by_name_and_version.get((name_key, activity.version)), worker_profile_id))
|
|
153
|
+
return None if owner is None else owner[1]
|
|
154
|
+
|
|
155
|
+
def add(self, activity: SandboxActivity, worker_profile_id: str) -> None:
|
|
156
|
+
"""Record `worker_profile_id` as an owner of `activity`."""
|
|
157
|
+
name_key = activity.name.casefold()
|
|
158
|
+
order = self._registration_count
|
|
159
|
+
self._registration_count += 1
|
|
160
|
+
self._by_name.setdefault(name_key, _ActivityOwnerSlot()).add(order, worker_profile_id)
|
|
161
|
+
self._by_name_and_version.setdefault(
|
|
162
|
+
(name_key, activity.version), _ActivityOwnerSlot()).add(order, worker_profile_id)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _first_owner_other_than(
|
|
166
|
+
slot: Optional[_ActivityOwnerSlot],
|
|
167
|
+
worker_profile_id: str) -> Optional[tuple[int, str]]:
|
|
168
|
+
return None if slot is None else slot.first_owner_other_than(worker_profile_id)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _earlier_owner(
|
|
172
|
+
left: Optional[tuple[int, str]],
|
|
173
|
+
right: Optional[tuple[int, str]]) -> Optional[tuple[int, str]]:
|
|
174
|
+
if left is None:
|
|
175
|
+
return right
|
|
176
|
+
if right is None:
|
|
177
|
+
return left
|
|
178
|
+
return left if left[0] <= right[0] else right
|
|
179
|
+
|
|
180
|
+
|
|
94
181
|
def build_sandbox_worker_profiles() -> list[pb.SandboxWorkerProfile]:
|
|
95
182
|
"""Build sandbox worker_profiles from worker profile configuration."""
|
|
96
183
|
worker_profiles: list[pb.SandboxWorkerProfile] = []
|
|
97
|
-
activity_owners
|
|
184
|
+
activity_owners = _ActivityOwnerIndex()
|
|
98
185
|
for profile in registered_sandbox_worker_profiles():
|
|
99
186
|
activities = resolve_activities(profile.activities)
|
|
100
187
|
|
|
101
188
|
for activity in activities:
|
|
102
|
-
existing_profile =
|
|
103
|
-
|
|
104
|
-
and owner_profile != profile.worker_profile_id), None)
|
|
189
|
+
existing_profile = activity_owners.find_conflicting_profile(
|
|
190
|
+
activity, profile.worker_profile_id)
|
|
105
191
|
if existing_profile:
|
|
106
192
|
raise ValueError(
|
|
107
193
|
f"Sandbox activity '{format_activity(activity)}' is assigned to both worker profile "
|
|
108
194
|
f"'{existing_profile}' and '{profile.worker_profile_id}'.")
|
|
109
|
-
activity_owners.
|
|
195
|
+
activity_owners.add(activity, profile.worker_profile_id)
|
|
110
196
|
|
|
111
197
|
worker_profiles.append(_build_sandbox_worker_profile(
|
|
112
198
|
activities=activities,
|
|
@@ -130,30 +130,50 @@ class SandboxWorker(DurableTaskSchedulerWorker):
|
|
|
130
130
|
|
|
131
131
|
def _run_sandbox_registration_loop(self) -> None:
|
|
132
132
|
retry_delay = 1.0
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
host_address=self._sandbox_host_address,
|
|
137
|
-
taskhub=self._sandbox_taskhub,
|
|
138
|
-
token_credential=self._sandbox_token_credential,
|
|
139
|
-
secure_channel=self._sandbox_secure_channel)
|
|
133
|
+
client: Optional[SandboxActivitiesGrpcTransport] = None
|
|
134
|
+
try:
|
|
135
|
+
while not self._sandbox_registration_stop.is_set():
|
|
140
136
|
try:
|
|
137
|
+
if client is None:
|
|
138
|
+
client = SandboxActivitiesGrpcTransport(
|
|
139
|
+
host_address=self._sandbox_host_address,
|
|
140
|
+
taskhub=self._sandbox_taskhub,
|
|
141
|
+
token_credential=self._sandbox_token_credential,
|
|
142
|
+
secure_channel=self._sandbox_secure_channel)
|
|
141
143
|
client.connect_sandbox_activity_worker(self._registration_messages())
|
|
142
144
|
retry_delay = 1.0
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
except Exception as ex:
|
|
146
|
+
# gRPC channels reconnect on their own after transient stream
|
|
147
|
+
# failures, so the transport (and its channel, stub, and cached
|
|
148
|
+
# access token) is reused instead of being rebuilt per attempt.
|
|
149
|
+
if _requires_new_registration_transport(ex):
|
|
150
|
+
self._close_sandbox_registration_transport(client)
|
|
151
|
+
client = None
|
|
152
|
+
if self._sandbox_registration_stop.is_set():
|
|
153
|
+
break
|
|
154
|
+
if not _is_retriable_registration_failure(ex):
|
|
155
|
+
self._sandbox_logger.error(
|
|
156
|
+
"Sandbox activity worker registration failed permanently: %s", ex)
|
|
157
|
+
self._sandbox_registration_stop.set()
|
|
158
|
+
break
|
|
159
|
+
self._sandbox_logger.warning(
|
|
160
|
+
"Sandbox activity worker registration failed: %s", ex)
|
|
161
|
+
delay = random.uniform(0, retry_delay)
|
|
162
|
+
self._sandbox_registration_stop.wait(delay)
|
|
163
|
+
retry_delay = min(retry_delay * 2, 30.0)
|
|
164
|
+
finally:
|
|
165
|
+
self._close_sandbox_registration_transport(client)
|
|
166
|
+
|
|
167
|
+
def _close_sandbox_registration_transport(
|
|
168
|
+
self,
|
|
169
|
+
client: Optional[SandboxActivitiesGrpcTransport]) -> None:
|
|
170
|
+
if client is None:
|
|
171
|
+
return
|
|
172
|
+
try:
|
|
173
|
+
client.close()
|
|
174
|
+
except Exception as ex:
|
|
175
|
+
self._sandbox_logger.debug(
|
|
176
|
+
"Sandbox activity worker registration transport failed to close: %s", ex)
|
|
157
177
|
|
|
158
178
|
def _registration_messages(self) -> Iterator[pb.SandboxActivityWorkerMessage]:
|
|
159
179
|
yield build_sandbox_worker_start(
|
|
@@ -264,6 +284,26 @@ def _is_retriable_registration_failure(ex: Exception) -> bool:
|
|
|
264
284
|
return isinstance(ex, OSError)
|
|
265
285
|
|
|
266
286
|
|
|
287
|
+
def _requires_new_registration_transport(ex: Exception) -> bool:
|
|
288
|
+
"""Returns whether a failure leaves the registration transport unusable.
|
|
289
|
+
|
|
290
|
+
A gRPC channel transparently reconnects after a stream fails, so the same
|
|
291
|
+
transport can serve the next registration attempt. A channel that has been
|
|
292
|
+
shut down rejects every later RPC, and a failure raised outside of gRPC (for
|
|
293
|
+
example while the channel itself is being created) can leave the transport
|
|
294
|
+
only partially initialized, so both cases need a replacement transport.
|
|
295
|
+
"""
|
|
296
|
+
if not isinstance(ex, grpc.RpcError):
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
if ex.code() != grpc.StatusCode.CANCELLED:
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
details = getattr(ex, "details", None)
|
|
303
|
+
resolved_details = details() if callable(details) else None
|
|
304
|
+
return isinstance(resolved_details, str) and "channel closed" in resolved_details.lower()
|
|
305
|
+
|
|
306
|
+
|
|
267
307
|
def _resolve_max_concurrent_activities() -> int:
|
|
268
308
|
value = os.getenv("DTS_SANDBOX_MAX_ACTIVITIES")
|
|
269
309
|
if value is None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durabletask.azuremanaged
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler
|
|
5
5
|
Project-URL: repository, https://github.com/microsoft/durabletask-python
|
|
6
6
|
Project-URL: changelog, https://github.com/microsoft/durabletask-python/blob/main/CHANGELOG.md
|
|
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
|
-
Requires-Dist: durabletask>=1.
|
|
13
|
+
Requires-Dist: durabletask>=1.9.0
|
|
14
14
|
Requires-Dist: azure-identity>=1.19.0
|
|
15
15
|
Provides-Extra: azure-blob-payloads
|
|
16
|
-
Requires-Dist: durabletask[azure-blob-payloads]>=1.
|
|
16
|
+
Requires-Dist: durabletask[azure-blob-payloads]>=1.9.0; extra == "azure-blob-payloads"
|
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "durabletask.azuremanaged"
|
|
12
|
-
version = "1.
|
|
12
|
+
version = "1.9.0"
|
|
13
13
|
description = "Durable Task Python SDK provider implementation for the Azure Durable Task Scheduler"
|
|
14
14
|
keywords = [
|
|
15
15
|
"durable",
|
|
@@ -26,13 +26,13 @@ requires-python = ">=3.10"
|
|
|
26
26
|
license = {file = "LICENSE"}
|
|
27
27
|
readme = "README.md"
|
|
28
28
|
dependencies = [
|
|
29
|
-
"durabletask>=1.
|
|
29
|
+
"durabletask>=1.9.0",
|
|
30
30
|
"azure-identity>=1.19.0"
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.optional-dependencies]
|
|
34
34
|
azure-blob-payloads = [
|
|
35
|
-
"durabletask[azure-blob-payloads]>=1.
|
|
35
|
+
"durabletask[azure-blob-payloads]>=1.9.0"
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[project.urls]
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# Copyright (c) Microsoft Corporation.
|
|
2
|
-
# Licensed under the MIT License.
|
|
3
|
-
|
|
4
|
-
"""Sandbox preview APIs for Durable Task Scheduler.
|
|
5
|
-
|
|
6
|
-
This extension provides preview APIs for declaring sandbox activity
|
|
7
|
-
worker images and running a Python activity worker inside a Durable Task
|
|
8
|
-
Scheduler-launched sandbox.
|
|
9
|
-
|
|
10
|
-
Usage::
|
|
11
|
-
|
|
12
|
-
from durabletask.azuremanaged.preview.sandboxes import (
|
|
13
|
-
SandboxWorker,
|
|
14
|
-
SandboxActivitiesClient,
|
|
15
|
-
)
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from durabletask.azuremanaged.preview.sandboxes.client import SandboxActivitiesClient
|
|
19
|
-
from durabletask.azuremanaged.preview.sandboxes.helpers import SandboxActivity
|
|
20
|
-
from durabletask.azuremanaged.preview.sandboxes.worker_profiles import SandboxWorkerProfile
|
|
21
|
-
from durabletask.azuremanaged.preview.sandboxes.worker_profiles import SandboxWorkerProfileOptions
|
|
22
|
-
from durabletask.azuremanaged.preview.sandboxes.worker_profiles import sandbox_worker_profile
|
|
23
|
-
from durabletask.azuremanaged.preview.sandboxes.worker import SandboxWorker
|
|
24
|
-
|
|
25
|
-
__all__ = [
|
|
26
|
-
"SandboxWorker",
|
|
27
|
-
"SandboxActivity",
|
|
28
|
-
"SandboxWorkerProfile",
|
|
29
|
-
"SandboxWorkerProfileOptions",
|
|
30
|
-
"SandboxActivitiesClient",
|
|
31
|
-
"sandbox_worker_profile",
|
|
32
|
-
]
|
|
File without changes
|
{durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/client.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
|
{durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/py.typed
RENAMED
|
File without changes
|
{durabletask_azuremanaged-1.7.2 → durabletask_azuremanaged-1.9.0}/durabletask/azuremanaged/worker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|