dex-python-sdk 1.11.0__tar.gz → 1.14.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.
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/PKG-INFO +35 -2
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/README.md +34 -1
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/__init__.py +8 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_pull_worker.py +78 -6
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/async_worker.py +71 -1
- dex_python_sdk-1.14.0/dex/dexpb/dex_pb2.py +545 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/dexpb/dex_pb2.pyi +28 -8
- dex_python_sdk-1.14.0/dex/pull_registration.py +181 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/worker.py +71 -2
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/worker_options.py +6 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/pyproject.toml +1 -1
- dex_python_sdk-1.11.0/dex/dexpb/dex_pb2.py +0 -545
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/LICENSE +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_async_worker_dispatcher.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_credential_transport.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_invocation_context.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_native.pyi +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_server_protocol.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_utils.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_worker_dispatcher.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/_worker_service.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/async_client.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/attribute.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/attribute_match.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/blob_cache.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/channel.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/client.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/client_options.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/codec.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/condition.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/context.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/credential.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/dexpb/dex_pb2_grpc.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/flow.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/flow_config.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/flow_info.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/flow_options.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/flow_result.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/py.typed +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/rpc_invoke_options.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/runtime_errors.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/step.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/step_execution.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/stream.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/subflow.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/timer.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/wait.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/dex/wait_options.py +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-1.11.0 → dex_python_sdk-1.14.0}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dex-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.14.0
|
|
4
4
|
Requires-Dist: grpcio>=1.83.0
|
|
5
5
|
Requires-Dist: grpcio-status>=1.83.0
|
|
6
6
|
Requires-Dist: protobuf>=7.35.1
|
|
@@ -100,6 +100,14 @@ max_concurrent_tasks)` streams because busy streams are replaced with idle
|
|
|
100
100
|
streams and retained after tasks finish. Size these options for the server's
|
|
101
101
|
per-Worker stream cap (256 by default).
|
|
102
102
|
|
|
103
|
+
The Worker rejects an unregistered Step or RPC by name. Dex retries that task
|
|
104
|
+
on another pull Worker without consuming a Step attempt, including when the
|
|
105
|
+
Flow was sticky to the rejecting Worker. If no capable Worker accepts it within
|
|
106
|
+
the server's dispatch wait (10 seconds by default, or the local Step attempt),
|
|
107
|
+
the Step follows its failure and retry policy; an RPC caller receives the named
|
|
108
|
+
error. This reassignment requires a Dex Server that supports it. Against older
|
|
109
|
+
Servers, the Worker returns the not-registered error as before.
|
|
110
|
+
|
|
103
111
|
The generated `app_worker_id` uses the first nonempty `DEX_WORKER_INSTANCE`,
|
|
104
112
|
`POD_NAME`, `HOSTNAME`, or system hostname, plus eight random base32 characters.
|
|
105
113
|
An override must uniquely identify one Worker process and change after restart.
|
|
@@ -114,7 +122,7 @@ an active task to finish, then waits up to `stop_drain_timeout` for the server
|
|
|
114
122
|
to close the stream after the final frame. An idle stream reconnects
|
|
115
123
|
immediately. An older server without pull support produces one explicit error
|
|
116
124
|
log and is retried after one minute. `start()` reports startup
|
|
117
|
-
synchronization errors, while later stream errors are logged and retried.
|
|
125
|
+
synchronization errors, while later stream errors are logged and retried by default.
|
|
118
126
|
`stop()` drains for at most
|
|
119
127
|
`stop_drain_timeout`, which defaults to 30 seconds. Afterward it requests
|
|
120
128
|
cancellation and waits for handlers to return. Synchronous handlers should
|
|
@@ -122,6 +130,31 @@ check `Context.is_cancellation_requested()` during long operations. Streams
|
|
|
122
130
|
that sent a final frame wait for server EOF until the remaining Stop drain
|
|
123
131
|
deadline before the channel closes.
|
|
124
132
|
|
|
133
|
+
Call `worker.wait_pull_registered(timeout=30)` after starting a synchronous
|
|
134
|
+
Worker on another thread. For AsyncWorker, start it in another task and use
|
|
135
|
+
`await worker.wait_pull_registered(timeout=30)`. The timeout is in seconds;
|
|
136
|
+
`None` waits indefinitely. Each waits for every Flow type queue and the app
|
|
137
|
+
Worker ID queue to register at least once. Timeout raises `TimeoutError`;
|
|
138
|
+
async cancellation cancels only that wait. Startup failures reach waiters.
|
|
139
|
+
A permanent rejection before readiness raises `PullRegistrationError`, whose
|
|
140
|
+
`queue` and `error` identify the rejected queue and original transport error.
|
|
141
|
+
Listening mode and stop before readiness raise `RuntimeError`.
|
|
142
|
+
|
|
143
|
+
Use `worker.pull_registration_state()` for subsequent readiness probes. Its
|
|
144
|
+
immutable `queues` expose `queue`, `has_registered_stream`, and
|
|
145
|
+
`last_permanent_error`. After initial registration, `is_ready` stays true across
|
|
146
|
+
transient failures, reconnects, and stop. It becomes false only when a queue
|
|
147
|
+
has a permanent error and no registered stream. Re-registration clears the
|
|
148
|
+
error. The startup wait keeps succeeding after initial registration, including
|
|
149
|
+
after stop. Multiple callers may wait independently before or after start.
|
|
150
|
+
|
|
151
|
+
`PullWorkerOptions(stop_on_permanent_stream_rejection=True)` makes the first
|
|
152
|
+
permanent rejection leaving a queue without a registered stream drain within
|
|
153
|
+
`stop_drain_timeout`, then raise `PullRegistrationError` from `start()`. This
|
|
154
|
+
applies before and after readiness. Another registered stream for that queue
|
|
155
|
+
prevents shutdown. The default is False and retries indefinitely. Always call
|
|
156
|
+
`stop()` or `close()` afterward; both remain safe after `start()` returns.
|
|
157
|
+
|
|
125
158
|
## Flow state I/O
|
|
126
159
|
|
|
127
160
|
Applications read and write Flow state through typed RPCs. Client and AsyncClient
|
|
@@ -84,6 +84,14 @@ max_concurrent_tasks)` streams because busy streams are replaced with idle
|
|
|
84
84
|
streams and retained after tasks finish. Size these options for the server's
|
|
85
85
|
per-Worker stream cap (256 by default).
|
|
86
86
|
|
|
87
|
+
The Worker rejects an unregistered Step or RPC by name. Dex retries that task
|
|
88
|
+
on another pull Worker without consuming a Step attempt, including when the
|
|
89
|
+
Flow was sticky to the rejecting Worker. If no capable Worker accepts it within
|
|
90
|
+
the server's dispatch wait (10 seconds by default, or the local Step attempt),
|
|
91
|
+
the Step follows its failure and retry policy; an RPC caller receives the named
|
|
92
|
+
error. This reassignment requires a Dex Server that supports it. Against older
|
|
93
|
+
Servers, the Worker returns the not-registered error as before.
|
|
94
|
+
|
|
87
95
|
The generated `app_worker_id` uses the first nonempty `DEX_WORKER_INSTANCE`,
|
|
88
96
|
`POD_NAME`, `HOSTNAME`, or system hostname, plus eight random base32 characters.
|
|
89
97
|
An override must uniquely identify one Worker process and change after restart.
|
|
@@ -98,7 +106,7 @@ an active task to finish, then waits up to `stop_drain_timeout` for the server
|
|
|
98
106
|
to close the stream after the final frame. An idle stream reconnects
|
|
99
107
|
immediately. An older server without pull support produces one explicit error
|
|
100
108
|
log and is retried after one minute. `start()` reports startup
|
|
101
|
-
synchronization errors, while later stream errors are logged and retried.
|
|
109
|
+
synchronization errors, while later stream errors are logged and retried by default.
|
|
102
110
|
`stop()` drains for at most
|
|
103
111
|
`stop_drain_timeout`, which defaults to 30 seconds. Afterward it requests
|
|
104
112
|
cancellation and waits for handlers to return. Synchronous handlers should
|
|
@@ -106,6 +114,31 @@ check `Context.is_cancellation_requested()` during long operations. Streams
|
|
|
106
114
|
that sent a final frame wait for server EOF until the remaining Stop drain
|
|
107
115
|
deadline before the channel closes.
|
|
108
116
|
|
|
117
|
+
Call `worker.wait_pull_registered(timeout=30)` after starting a synchronous
|
|
118
|
+
Worker on another thread. For AsyncWorker, start it in another task and use
|
|
119
|
+
`await worker.wait_pull_registered(timeout=30)`. The timeout is in seconds;
|
|
120
|
+
`None` waits indefinitely. Each waits for every Flow type queue and the app
|
|
121
|
+
Worker ID queue to register at least once. Timeout raises `TimeoutError`;
|
|
122
|
+
async cancellation cancels only that wait. Startup failures reach waiters.
|
|
123
|
+
A permanent rejection before readiness raises `PullRegistrationError`, whose
|
|
124
|
+
`queue` and `error` identify the rejected queue and original transport error.
|
|
125
|
+
Listening mode and stop before readiness raise `RuntimeError`.
|
|
126
|
+
|
|
127
|
+
Use `worker.pull_registration_state()` for subsequent readiness probes. Its
|
|
128
|
+
immutable `queues` expose `queue`, `has_registered_stream`, and
|
|
129
|
+
`last_permanent_error`. After initial registration, `is_ready` stays true across
|
|
130
|
+
transient failures, reconnects, and stop. It becomes false only when a queue
|
|
131
|
+
has a permanent error and no registered stream. Re-registration clears the
|
|
132
|
+
error. The startup wait keeps succeeding after initial registration, including
|
|
133
|
+
after stop. Multiple callers may wait independently before or after start.
|
|
134
|
+
|
|
135
|
+
`PullWorkerOptions(stop_on_permanent_stream_rejection=True)` makes the first
|
|
136
|
+
permanent rejection leaving a queue without a registered stream drain within
|
|
137
|
+
`stop_drain_timeout`, then raise `PullRegistrationError` from `start()`. This
|
|
138
|
+
applies before and after readiness. Another registered stream for that queue
|
|
139
|
+
prevents shutdown. The default is False and retries indefinitely. Always call
|
|
140
|
+
`stop()` or `close()` afterward; both remain safe after `start()` returns.
|
|
141
|
+
|
|
109
142
|
## Flow state I/O
|
|
110
143
|
|
|
111
144
|
Applications read and write Flow state through typed RPCs. Client and AsyncClient
|
|
@@ -113,6 +113,11 @@ from dex.timer import Timer
|
|
|
113
113
|
from dex.wait import Wait
|
|
114
114
|
from dex.wait_options import WaitForAttributeOptions, WaitForStepCompletionOptions
|
|
115
115
|
from dex.worker import Worker
|
|
116
|
+
from dex.pull_registration import (
|
|
117
|
+
PullRegistrationError,
|
|
118
|
+
PullRegistrationState,
|
|
119
|
+
PullQueueRegistrationState,
|
|
120
|
+
)
|
|
116
121
|
from dex.worker_options import PullWorkerOptions, WorkerOptions, WorkerTarget
|
|
117
122
|
|
|
118
123
|
__all__ = [
|
|
@@ -214,6 +219,9 @@ __all__ = [
|
|
|
214
219
|
"WorkerOptions",
|
|
215
220
|
"WorkerTarget",
|
|
216
221
|
"PullWorkerOptions",
|
|
222
|
+
"PullRegistrationError",
|
|
223
|
+
"PullRegistrationState",
|
|
224
|
+
"PullQueueRegistrationState",
|
|
217
225
|
"WaitForAttributeOptions",
|
|
218
226
|
"WaitForStepCompletionOptions",
|
|
219
227
|
"dead_end",
|
|
@@ -28,6 +28,7 @@ from dex._server_protocol import sdk_version
|
|
|
28
28
|
from dex.dexpb import dex_pb2 as pb
|
|
29
29
|
from dex.dexpb import dex_pb2_grpc
|
|
30
30
|
from dex.flow import Registry
|
|
31
|
+
from dex.pull_registration import PullRegistrationError, _PullRegistration
|
|
31
32
|
from dex.worker_options import PullWorkerOptions
|
|
32
33
|
|
|
33
34
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -82,9 +83,16 @@ class PullWorkerRuntime:
|
|
|
82
83
|
app_worker_id: str | None = None,
|
|
83
84
|
credential_runner: AsyncCredentialRunner | None = None,
|
|
84
85
|
) -> None:
|
|
86
|
+
self._registry = registry
|
|
85
87
|
self.app_worker_id = app_worker_id or resolve_app_worker_id(registry, options)
|
|
86
88
|
self.flow_types = sorted(registry._registered_flows)
|
|
87
89
|
self.queues = (self.app_worker_id, *self.flow_types)
|
|
90
|
+
self.registration = _PullRegistration(self.queues)
|
|
91
|
+
self._stop_on_permanent_stream_rejection = (
|
|
92
|
+
options.stop_on_permanent_stream_rejection
|
|
93
|
+
)
|
|
94
|
+
self._permanent_error: PullRegistrationError | None = None
|
|
95
|
+
self._rejection_stop_task: asyncio.Task[None] | None = None
|
|
88
96
|
self.max_concurrent_tasks = options.max_concurrent_tasks or os.cpu_count() or 1
|
|
89
97
|
self.idle_streams_per_queue = options.idle_streams_per_queue or 1
|
|
90
98
|
self.stop_drain_timeout = options.stop_drain_timeout.total_seconds() or 30
|
|
@@ -103,6 +111,26 @@ class PullWorkerRuntime:
|
|
|
103
111
|
self._busy_tasks = 0
|
|
104
112
|
self._unsupported_logged = False
|
|
105
113
|
|
|
114
|
+
def unknown_task_reason(
|
|
115
|
+
self, task: pb.PollWorkerTask
|
|
116
|
+
) -> pb.PollWorkerTaskRejectReason | None:
|
|
117
|
+
kind = task.WhichOneof("request")
|
|
118
|
+
if kind == "rpc":
|
|
119
|
+
flow = self._registry._registered_flows.get(task.rpc.flow_type)
|
|
120
|
+
if flow is None or task.rpc.rpc_name not in flow.rpcs:
|
|
121
|
+
return pb.POLL_WORKER_TASK_REJECT_REASON_UNKNOWN_RPC
|
|
122
|
+
elif kind in ("wait_for", "execute"):
|
|
123
|
+
request = task.wait_for if kind == "wait_for" else task.execute
|
|
124
|
+
flow = self._registry._registered_flows.get(request.flow_type)
|
|
125
|
+
if flow is None:
|
|
126
|
+
return pb.POLL_WORKER_TASK_REJECT_REASON_UNKNOWN_STEP
|
|
127
|
+
if kind == "execute" and request.step_type == "sys:timeout_handler":
|
|
128
|
+
if not flow.has_timeout_handler:
|
|
129
|
+
return pb.POLL_WORKER_TASK_REJECT_REASON_UNKNOWN_STEP
|
|
130
|
+
elif request.step_type not in flow.steps:
|
|
131
|
+
return pb.POLL_WORKER_TASK_REJECT_REASON_UNKNOWN_STEP
|
|
132
|
+
return None
|
|
133
|
+
|
|
106
134
|
async def run(self) -> None:
|
|
107
135
|
self._loop = asyncio.get_running_loop()
|
|
108
136
|
self._loop_thread_id = threading.get_ident()
|
|
@@ -110,9 +138,10 @@ class PullWorkerRuntime:
|
|
|
110
138
|
self._stopping = asyncio.Event()
|
|
111
139
|
self._stop_completed = asyncio.Event()
|
|
112
140
|
self._started.set()
|
|
113
|
-
channel =
|
|
114
|
-
stub = dex_pb2_grpc.WorkerTaskServiceStub(channel) # type: ignore[no-untyped-call]
|
|
141
|
+
channel: grpc.aio.Channel | None = None
|
|
115
142
|
try:
|
|
143
|
+
channel = outbound_worker_task_channel(self._server_address)
|
|
144
|
+
stub = dex_pb2_grpc.WorkerTaskServiceStub(channel) # type: ignore[no-untyped-call]
|
|
116
145
|
self._wake.set()
|
|
117
146
|
while not self._stopping.is_set():
|
|
118
147
|
await self._wake.wait()
|
|
@@ -123,9 +152,14 @@ class PullWorkerRuntime:
|
|
|
123
152
|
*(stream.run_task for stream in tuple(self._streams)),
|
|
124
153
|
return_exceptions=True,
|
|
125
154
|
)
|
|
155
|
+
if self._rejection_stop_task is not None:
|
|
156
|
+
await self._rejection_stop_task
|
|
157
|
+
if self._permanent_error is not None:
|
|
158
|
+
raise self._permanent_error
|
|
126
159
|
finally:
|
|
127
160
|
try:
|
|
128
|
-
|
|
161
|
+
if channel is not None:
|
|
162
|
+
await channel.close(grace=None)
|
|
129
163
|
finally:
|
|
130
164
|
self._loop_thread_id = None
|
|
131
165
|
|
|
@@ -171,6 +205,7 @@ class PullWorkerRuntime:
|
|
|
171
205
|
self.signal()
|
|
172
206
|
|
|
173
207
|
async def stop(self) -> None:
|
|
208
|
+
self.registration.record_stopping()
|
|
174
209
|
if self._stopping is None:
|
|
175
210
|
return
|
|
176
211
|
if self._stopping.is_set():
|
|
@@ -242,6 +277,7 @@ class PullStream:
|
|
|
242
277
|
self.handler_task: asyncio.Task[None] | None = None
|
|
243
278
|
self.task_id = ""
|
|
244
279
|
self.is_busy = False
|
|
280
|
+
self._is_registered = False
|
|
245
281
|
self.is_draining = False
|
|
246
282
|
self.is_half_closing = False
|
|
247
283
|
self._active = threading.Event()
|
|
@@ -249,6 +285,7 @@ class PullStream:
|
|
|
249
285
|
self._send_lock = asyncio.Lock()
|
|
250
286
|
self._is_reconnecting = False
|
|
251
287
|
self._deadline_handle: asyncio.TimerHandle | None = None
|
|
288
|
+
self._supports_unknown_name_rejection = False
|
|
252
289
|
|
|
253
290
|
def on_done(self, completed: asyncio.Task[None]) -> None:
|
|
254
291
|
self.runtime._stream_done(self, completed)
|
|
@@ -267,7 +304,24 @@ class PullStream:
|
|
|
267
304
|
_LOGGER.exception(
|
|
268
305
|
"Worker task stream failed", extra={"queue": self.queue}
|
|
269
306
|
)
|
|
270
|
-
|
|
307
|
+
finally:
|
|
308
|
+
code = error.code() if error is not None else None
|
|
309
|
+
rejection = self.runtime.registration.record_stream_result(
|
|
310
|
+
self.queue,
|
|
311
|
+
self._is_registered,
|
|
312
|
+
error if code in _PERMANENT_CODES else None,
|
|
313
|
+
)
|
|
314
|
+
self._is_registered = False
|
|
315
|
+
if (
|
|
316
|
+
rejection is not None
|
|
317
|
+
and self.runtime._stop_on_permanent_stream_rejection
|
|
318
|
+
and self.runtime._permanent_error is None
|
|
319
|
+
):
|
|
320
|
+
self.runtime._permanent_error = rejection
|
|
321
|
+
self.runtime._rejection_stop_task = asyncio.create_task(
|
|
322
|
+
self.runtime.stop()
|
|
323
|
+
)
|
|
324
|
+
if self.runtime.is_stopping or self.runtime._permanent_error is not None:
|
|
271
325
|
break
|
|
272
326
|
if self._is_reconnecting:
|
|
273
327
|
self._is_reconnecting = False
|
|
@@ -301,6 +355,7 @@ class PullStream:
|
|
|
301
355
|
async def _poll(self) -> bool:
|
|
302
356
|
if self.runtime.is_stopping:
|
|
303
357
|
return False
|
|
358
|
+
self._supports_unknown_name_rejection = False
|
|
304
359
|
if self.runtime._credential_runner is None:
|
|
305
360
|
self._call = self.stub.PollWorkerTask()
|
|
306
361
|
else:
|
|
@@ -364,7 +419,12 @@ class PullStream:
|
|
|
364
419
|
return False
|
|
365
420
|
if kind != "registered":
|
|
366
421
|
raise RuntimeError("Worker task stream did not register")
|
|
422
|
+
self._supports_unknown_name_rejection = (
|
|
423
|
+
frame.registered.supports_unknown_name_rejection
|
|
424
|
+
)
|
|
367
425
|
was_registered = True
|
|
426
|
+
self._is_registered = True
|
|
427
|
+
self.runtime.registration.record_registered_stream(self.queue)
|
|
368
428
|
continue
|
|
369
429
|
if kind == "task":
|
|
370
430
|
await self._start_task(frame.task)
|
|
@@ -443,6 +503,14 @@ class PullStream:
|
|
|
443
503
|
pass
|
|
444
504
|
|
|
445
505
|
async def _start_task(self, task: pb.PollWorkerTask) -> None:
|
|
506
|
+
unknown_reason = (
|
|
507
|
+
self.runtime.unknown_task_reason(task)
|
|
508
|
+
if self._supports_unknown_name_rejection
|
|
509
|
+
else None
|
|
510
|
+
)
|
|
511
|
+
if unknown_reason is not None:
|
|
512
|
+
await self._reject_task(task, unknown_reason)
|
|
513
|
+
return
|
|
446
514
|
if self.is_busy or self.is_draining or not self.runtime.accept_task():
|
|
447
515
|
await self._reject_task(task)
|
|
448
516
|
return
|
|
@@ -458,12 +526,16 @@ class PullStream:
|
|
|
458
526
|
)
|
|
459
527
|
self.runtime.signal()
|
|
460
528
|
|
|
461
|
-
async def _reject_task(
|
|
529
|
+
async def _reject_task(
|
|
530
|
+
self,
|
|
531
|
+
task: pb.PollWorkerTask,
|
|
532
|
+
reason: pb.PollWorkerTaskRejectReason = pb.POLL_WORKER_TASK_REJECT_REASON_NO_CAPACITY,
|
|
533
|
+
) -> None:
|
|
462
534
|
await self._send(
|
|
463
535
|
pb.PollWorkerTaskRequest(
|
|
464
536
|
reject=pb.PollWorkerTaskReject(
|
|
465
537
|
task_id=task.task_id,
|
|
466
|
-
reason=
|
|
538
|
+
reason=reason,
|
|
467
539
|
)
|
|
468
540
|
)
|
|
469
541
|
)
|
|
@@ -30,6 +30,7 @@ from dex.blob_cache import BlobCache
|
|
|
30
30
|
from dex.dexpb import dex_pb2 as pb
|
|
31
31
|
from dex.dexpb import dex_pb2_grpc
|
|
32
32
|
from dex.flow import Registry
|
|
33
|
+
from dex.pull_registration import PullRegistrationState
|
|
33
34
|
from dex.worker_options import WorkerOptions, WorkerTarget
|
|
34
35
|
|
|
35
36
|
|
|
@@ -100,6 +101,7 @@ class AsyncWorker:
|
|
|
100
101
|
self._server_protocol,
|
|
101
102
|
)
|
|
102
103
|
self._server = None
|
|
104
|
+
self._has_start_called = False
|
|
103
105
|
self._pull = None
|
|
104
106
|
if self.options.pull is not None:
|
|
105
107
|
self._pull = PullWorkerRuntime(
|
|
@@ -167,11 +169,77 @@ class AsyncWorker:
|
|
|
167
169
|
|
|
168
170
|
``start`` may be awaited exactly once. It contacts FlowService before binding
|
|
169
171
|
and completes only after ``stop`` terminates the listener or pull streams.
|
|
170
|
-
|
|
172
|
+
Permanent pull stream rejections raise PullRegistrationError when
|
|
173
|
+
stop_on_permanent_stream_rejection is enabled; otherwise streams retry.
|
|
171
174
|
|
|
172
175
|
Raises:
|
|
176
|
+
PullRegistrationError: A permanent stream rejection stops this Worker.
|
|
173
177
|
RuntimeError: If lifecycle state, index synchronization, or binding fails.
|
|
174
178
|
"""
|
|
179
|
+
if self._has_start_called:
|
|
180
|
+
raise RuntimeError("Worker can only start once")
|
|
181
|
+
self._has_start_called = True
|
|
182
|
+
start_error: BaseException | None = None
|
|
183
|
+
try:
|
|
184
|
+
await self._start()
|
|
185
|
+
except BaseException as failure:
|
|
186
|
+
start_error = failure
|
|
187
|
+
raise
|
|
188
|
+
finally:
|
|
189
|
+
if self._pull is not None:
|
|
190
|
+
self._pull.registration.finish_start(start_error)
|
|
191
|
+
if self._state == "running":
|
|
192
|
+
await self.stop()
|
|
193
|
+
|
|
194
|
+
async def wait_pull_registered(self, timeout: float | None = None) -> None:
|
|
195
|
+
"""Wait until every pull queue has registered at least once.
|
|
196
|
+
|
|
197
|
+
Start the Worker separately before waiting, or start it concurrently.
|
|
198
|
+
Successful registration latches, including after stop. Use
|
|
199
|
+
pull_registration_state for subsequent readiness probes. A permanent
|
|
200
|
+
rejection before readiness raises PullRegistrationError only when no
|
|
201
|
+
other stream is registered for that queue. Startup failures wake waiters.
|
|
202
|
+
Multiple callers may wait independently, including before start.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
timeout: Maximum seconds to wait; None waits indefinitely.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
PullRegistrationError: A queue is permanently rejected before readiness.
|
|
209
|
+
TimeoutError: The timeout expires.
|
|
210
|
+
asyncio.CancelledError: The calling task is canceled; the Worker keeps running.
|
|
211
|
+
RuntimeError: This is a listening Worker, or it stops before readiness.
|
|
212
|
+
BaseException: The original startup failure, if start fails.
|
|
213
|
+
|
|
214
|
+
Examples:
|
|
215
|
+
>>> start_task = asyncio.create_task(worker.start())
|
|
216
|
+
|
|
217
|
+
>>> await worker.wait_pull_registered(timeout=30)
|
|
218
|
+
>>> is_ready = worker.pull_registration_state().is_ready
|
|
219
|
+
"""
|
|
220
|
+
if self._pull is None:
|
|
221
|
+
raise RuntimeError("wait_pull_registered requires a pull Worker")
|
|
222
|
+
await self._pull.registration.wait_async(timeout)
|
|
223
|
+
|
|
224
|
+
def pull_registration_state(self) -> PullRegistrationState:
|
|
225
|
+
"""Return an immutable snapshot of pull queue registrations.
|
|
226
|
+
|
|
227
|
+
Transient failures, reconnects, and stop preserve initial readiness.
|
|
228
|
+
A permanent rejection makes readiness false only while that queue has no
|
|
229
|
+
registered stream. Re-registration clears its error. Calls are safe
|
|
230
|
+
concurrently with start, stop, and other waiters. See wait_pull_registered
|
|
231
|
+
for an example.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Current per-queue state, or an empty nonready state in listening mode.
|
|
235
|
+
"""
|
|
236
|
+
return (
|
|
237
|
+
PullRegistrationState()
|
|
238
|
+
if self._pull is None
|
|
239
|
+
else self._pull.registration.snapshot()
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def _start(self) -> None:
|
|
175
243
|
if self._state != "created":
|
|
176
244
|
raise RuntimeError(f"AsyncWorker cannot start from state {self._state}")
|
|
177
245
|
try:
|
|
@@ -247,6 +315,8 @@ class AsyncWorker:
|
|
|
247
315
|
Half-closed pull streams wait for server EOF until the remaining drain
|
|
248
316
|
deadline before the channel closes.
|
|
249
317
|
"""
|
|
318
|
+
if self._pull is not None:
|
|
319
|
+
self._pull.registration.record_stopping()
|
|
250
320
|
if self._state in ("stopped", "closed"):
|
|
251
321
|
return
|
|
252
322
|
self._state = "stopping"
|