dex-python-sdk 1.20.0__tar.gz → 1.25.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.20.0 → dex_python_sdk-1.25.0}/PKG-INFO +47 -37
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/README.md +46 -36
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/__init__.py +1 -2
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_grpc_errors.py +1 -18
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_pull_worker.py +126 -7
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_worker_dispatcher.py +4 -8
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/async_client.py +12 -22
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/async_worker.py +35 -106
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/client.py +10 -23
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/client_options.py +1 -5
- dex_python_sdk-1.25.0/dex/dexpb/dex_pb2.py +551 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/dexpb/dex_pb2.pyi +12 -6
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/flow_config.py +2 -5
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/pull_registration.py +1 -1
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/runtime_errors.py +1 -1
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/worker.py +37 -124
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/worker_options.py +20 -73
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/pyproject.toml +1 -1
- dex_python_sdk-1.20.0/dex/_async_worker_service.py +0 -122
- dex_python_sdk-1.20.0/dex/_worker_service.py +0 -94
- dex_python_sdk-1.20.0/dex/dexpb/dex_pb2.py +0 -551
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/LICENSE +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_async_worker_dispatcher.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_credential_transport.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_invocation_context.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_native.pyi +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_server_protocol.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_utils.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/attribute.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/attribute_match.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/blob_cache.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/channel.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/codec.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/condition.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/context.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/credential.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/dexpb/dex_pb2_grpc.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/flow.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/flow_info.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/flow_options.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/flow_result.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/py.typed +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/rpc_invoke_options.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/step.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/step_execution.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/stream.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/subflow.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/timer.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/wait.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/dex/wait_options.py +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/README.md +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.0}/sdk-rust/crates/dex-blob-cache-python/README.md +0 -0
- {dex_python_sdk-1.20.0 → dex_python_sdk-1.25.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.25.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
|
|
@@ -95,14 +95,17 @@ retain up to four idle credential threads. Sync token lookup must return
|
|
|
95
95
|
promptly. By default, no authorization metadata is sent. Dex uses plaintext
|
|
96
96
|
gRPC, so protect the network path that carries the token.
|
|
97
97
|
|
|
98
|
-
##
|
|
98
|
+
## Workers
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`
|
|
104
|
-
|
|
105
|
-
SubFlow
|
|
100
|
+
Workers receive tasks through outbound pull streams. `Worker` and `AsyncWorker`
|
|
101
|
+
need only an outbound connection to the Dex Server at `server_address`. Configure
|
|
102
|
+
capacity and shutdown with `WorkerOptions.pull`; its default is
|
|
103
|
+
`PullWorkerOptions()`. Clients and Flows use pull routing automatically.
|
|
104
|
+
|
|
105
|
+
StartFlow and SubFlow always select pull Workers. Updating Flow configuration
|
|
106
|
+
preserves its existing Worker target; an empty `FlowConfig()` changes nothing.
|
|
107
|
+
Before upgrading an application with active push Flows, use an SDK version that
|
|
108
|
+
supports Worker targets to migrate those Flows to pull routing.
|
|
106
109
|
|
|
107
110
|
```python
|
|
108
111
|
import threading
|
|
@@ -118,10 +121,11 @@ worker = Worker(
|
|
|
118
121
|
)
|
|
119
122
|
worker_thread = threading.Thread(target=worker.start)
|
|
120
123
|
worker_thread.start()
|
|
124
|
+
worker.wait_pull_registered(timeout=30)
|
|
121
125
|
client = Client(
|
|
122
126
|
registry,
|
|
123
127
|
cache,
|
|
124
|
-
ClientOptions(server_address="dex.example:8801"
|
|
128
|
+
ClientOptions(server_address="dex.example:8801"),
|
|
125
129
|
)
|
|
126
130
|
try:
|
|
127
131
|
client.start_flow(flow, flow_id, input_value)
|
|
@@ -135,13 +139,18 @@ The Worker polls its sticky queue and every registered Flow type queue. Flow
|
|
|
135
139
|
type names must be unique within a Dex server. `max_concurrent_tasks` defaults
|
|
136
140
|
to the CPU count, and `idle_streams_per_queue` defaults to one. At capacity,
|
|
137
141
|
the Worker rejects an assignment so Dex can reassign it. Each stream runs one
|
|
138
|
-
task at a time; Step heartbeat and stream writes use the same stream. After
|
|
139
|
-
gracefully closes surplus idle streams while preserving
|
|
142
|
+
task at a time; Step heartbeat and stream writes use the same stream. After
|
|
143
|
+
tasks finish, the Worker gracefully closes surplus idle streams while preserving
|
|
144
|
+
readiness. The SDK sends an upstream Pong every 20 seconds while a task runs and
|
|
145
|
+
stops before its final frame. This interval is internal and needs no Worker
|
|
146
|
+
option. After load drops,
|
|
140
147
|
the steady-state bound is `max_concurrent_tasks + (1 + number of Flow types) *
|
|
141
|
-
idle_streams_per_queue` streams.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
idle_streams_per_queue` streams.
|
|
149
|
+
Allow up to one more stream per queue for 30 seconds after reserve declines,
|
|
150
|
+
or up to `1 + number of Flow types` additional streams. The transient peak
|
|
151
|
+
during a burst remains bounded by the matching caps. Size these options for
|
|
152
|
+
`matching.maxConcurrentTasksCap` per app Worker ID per matching owner (256 by
|
|
153
|
+
default) and `matching.maxStreams` per Matching instance (100,000 by default).
|
|
145
154
|
|
|
146
155
|
The Worker rejects an unregistered Step or RPC by name. Dex retries that task
|
|
147
156
|
on another pull Worker without consuming a Step attempt, including when the
|
|
@@ -155,10 +164,7 @@ The generated `app_worker_id` uses the first nonempty `DEX_WORKER_INSTANCE`,
|
|
|
155
164
|
`POD_NAME`, `HOSTNAME`, or system hostname, plus eight random base32 characters.
|
|
156
165
|
An override must uniquely identify one Worker process and change after restart.
|
|
157
166
|
It must contain 1-63 ASCII letters, digits, dots, underscores, or hyphens and
|
|
158
|
-
must not equal a registered Flow type.
|
|
159
|
-
or an advertised address or headless target. Use `WorkerTarget(pull=True)` for
|
|
160
|
-
Client defaults or Flow overrides when the Worker target is not directly in
|
|
161
|
-
scope.
|
|
167
|
+
must not equal a registered Flow type.
|
|
162
168
|
|
|
163
169
|
Pull streams reconnect after failures. A server-directed reconnect waits for
|
|
164
170
|
an active task to finish, then waits up to `stop_drain_timeout` for the server
|
|
@@ -181,7 +187,7 @@ Worker ID queue to register at least once. Timeout raises `TimeoutError`;
|
|
|
181
187
|
async cancellation cancels only that wait. Startup failures reach waiters.
|
|
182
188
|
A permanent rejection before readiness raises `PullRegistrationError`, whose
|
|
183
189
|
`queue` and `error` identify the rejected queue and original transport error.
|
|
184
|
-
|
|
190
|
+
Stopping before readiness raises `RuntimeError`.
|
|
185
191
|
|
|
186
192
|
Use `worker.pull_registration_state()` for subsequent readiness probes. Its
|
|
187
193
|
immutable `queues` expose `queue`, `has_registered_stream`, and
|
|
@@ -206,11 +212,11 @@ limit is at least two, otherwise zero. With the CPU-count default, Steps get one
|
|
|
206
212
|
fewer slot than before. Set it explicitly to zero to opt out. With one slot and
|
|
207
213
|
no explicit reserve, Worker start logs one warning: RPCs may wait behind
|
|
208
214
|
long-running Steps; use a limit of at least two or set the reserve explicitly.
|
|
209
|
-
Flows must stay responsive
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
Flows must stay responsive because RPCs share Worker task capacity with Steps.
|
|
216
|
+
The value must be nonnegative and less than `max_concurrent_tasks` after its
|
|
217
|
+
CPU-count default is resolved; invalid values fail construction. RPC and Step
|
|
218
|
+
handlers count together. WaitFor and Execute tasks, including local attempts,
|
|
219
|
+
are declined with `NO_CAPACITY` when the busy
|
|
214
220
|
count reaches the task limit minus the reserve. API-direct and transactional
|
|
215
221
|
InvokeWorkerRPC tasks can use the full limit.
|
|
216
222
|
|
|
@@ -222,15 +228,19 @@ A decline lets Dex try another Worker without consuming an attempt. If all
|
|
|
222
228
|
Workers remain at their Step limit, a Step can wait until the dispatch wait
|
|
223
229
|
expires, then receive `NO_WORKER_AVAILABLE`, consume an attempt, and follow its
|
|
224
230
|
retry policy, as when all Workers are full. Local attempts wait the local
|
|
225
|
-
dispatch wait, then fall back according to their policy.
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
dispatch wait, then fall back according to their policy. A queue that declines
|
|
232
|
+
a Step for reserved RPC capacity keeps one additional idle stream until 30
|
|
233
|
+
seconds after its last decline. Other queues retain their configured count.
|
|
234
|
+
The Worker does not open the extra stream while all task slots are busy. Set
|
|
235
|
+
`idle_streams_per_queue` to at least two for heavy Step backlogs. A reserve decline
|
|
236
|
+
does not close a stream or change registration readiness.
|
|
228
237
|
|
|
229
238
|
Each reserve decline records the Flow type through the dex._pull_worker logger
|
|
230
239
|
at DEBUG level. Export the cumulative `pull_reserved_rpc_declines` Worker value
|
|
231
|
-
to distinguish reserve pressure from full-capacity rejections. The count
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
to distinguish reserve pressure from full-capacity rejections. The count excludes
|
|
241
|
+
full-capacity rejections and unknown names. It counts decline events, including
|
|
242
|
+
repeated declines of the same Step dispatch while Dex retries it. Read its rate
|
|
243
|
+
to assess reserve pressure. It never resets and remains available after shutdown.
|
|
234
244
|
|
|
235
245
|
## Flow state I/O
|
|
236
246
|
|
|
@@ -388,7 +398,7 @@ explicit codec only for a custom encoding or a type Registry cannot derive.
|
|
|
388
398
|
partitions them by definition type.
|
|
389
399
|
|
|
390
400
|
`Worker` and `AsyncWorker` synchronize all registered Indexed Attributes with
|
|
391
|
-
Dex Server before opening
|
|
401
|
+
Dex Server before opening outbound task streams. Existing indexes return immediately;
|
|
392
402
|
failure or the default two-minute deadline aborts startup. An indexed
|
|
393
403
|
`AttributeMap` must provide one fixed `index_key`.
|
|
394
404
|
|
|
@@ -784,8 +794,8 @@ environment:
|
|
|
784
794
|
./run-integration-tests.sh
|
|
785
795
|
```
|
|
786
796
|
|
|
787
|
-
|
|
788
|
-
|
|
797
|
+
The suite always uses pull Workers. Set `DEX_INTEG_ENGINE=zd` to test against
|
|
798
|
+
server-zd, or leave it unset to use the Temporal-backed server.
|
|
789
799
|
|
|
790
800
|
### Measure integration coverage
|
|
791
801
|
|
|
@@ -858,9 +868,9 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#releases-monorepo-tags) for monorepo ta
|
|
|
858
868
|
## Server protocol compatibility
|
|
859
869
|
|
|
860
870
|
Both `Worker` and `AsyncWorker` call `GetServerInfo`, negotiate the highest
|
|
861
|
-
common protocol, synchronize Attribute indexes, and only then
|
|
862
|
-
|
|
863
|
-
invalid or disjoint intervals, and RPC failures stop startup before
|
|
871
|
+
common protocol, synchronize Attribute indexes, and only then open outbound
|
|
872
|
+
task streams. The initial Python SDK interval is `[1,1]`. Missing information,
|
|
873
|
+
invalid or disjoint intervals, and RPC failures stop startup before polling.
|
|
864
874
|
The current Python SDK interval is `[1,3]`. Protocol 3 adds hard-delete
|
|
865
875
|
Attributes. Workers record the negotiated version at startup and reject each
|
|
866
876
|
handler result that contains a hard-delete Attribute write when it is below 3.
|
|
@@ -79,14 +79,17 @@ retain up to four idle credential threads. Sync token lookup must return
|
|
|
79
79
|
promptly. By default, no authorization metadata is sent. Dex uses plaintext
|
|
80
80
|
gRPC, so protect the network path that carries the token.
|
|
81
81
|
|
|
82
|
-
##
|
|
82
|
+
## Workers
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`
|
|
88
|
-
|
|
89
|
-
SubFlow
|
|
84
|
+
Workers receive tasks through outbound pull streams. `Worker` and `AsyncWorker`
|
|
85
|
+
need only an outbound connection to the Dex Server at `server_address`. Configure
|
|
86
|
+
capacity and shutdown with `WorkerOptions.pull`; its default is
|
|
87
|
+
`PullWorkerOptions()`. Clients and Flows use pull routing automatically.
|
|
88
|
+
|
|
89
|
+
StartFlow and SubFlow always select pull Workers. Updating Flow configuration
|
|
90
|
+
preserves its existing Worker target; an empty `FlowConfig()` changes nothing.
|
|
91
|
+
Before upgrading an application with active push Flows, use an SDK version that
|
|
92
|
+
supports Worker targets to migrate those Flows to pull routing.
|
|
90
93
|
|
|
91
94
|
```python
|
|
92
95
|
import threading
|
|
@@ -102,10 +105,11 @@ worker = Worker(
|
|
|
102
105
|
)
|
|
103
106
|
worker_thread = threading.Thread(target=worker.start)
|
|
104
107
|
worker_thread.start()
|
|
108
|
+
worker.wait_pull_registered(timeout=30)
|
|
105
109
|
client = Client(
|
|
106
110
|
registry,
|
|
107
111
|
cache,
|
|
108
|
-
ClientOptions(server_address="dex.example:8801"
|
|
112
|
+
ClientOptions(server_address="dex.example:8801"),
|
|
109
113
|
)
|
|
110
114
|
try:
|
|
111
115
|
client.start_flow(flow, flow_id, input_value)
|
|
@@ -119,13 +123,18 @@ The Worker polls its sticky queue and every registered Flow type queue. Flow
|
|
|
119
123
|
type names must be unique within a Dex server. `max_concurrent_tasks` defaults
|
|
120
124
|
to the CPU count, and `idle_streams_per_queue` defaults to one. At capacity,
|
|
121
125
|
the Worker rejects an assignment so Dex can reassign it. Each stream runs one
|
|
122
|
-
task at a time; Step heartbeat and stream writes use the same stream. After
|
|
123
|
-
gracefully closes surplus idle streams while preserving
|
|
126
|
+
task at a time; Step heartbeat and stream writes use the same stream. After
|
|
127
|
+
tasks finish, the Worker gracefully closes surplus idle streams while preserving
|
|
128
|
+
readiness. The SDK sends an upstream Pong every 20 seconds while a task runs and
|
|
129
|
+
stops before its final frame. This interval is internal and needs no Worker
|
|
130
|
+
option. After load drops,
|
|
124
131
|
the steady-state bound is `max_concurrent_tasks + (1 + number of Flow types) *
|
|
125
|
-
idle_streams_per_queue` streams.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
idle_streams_per_queue` streams.
|
|
133
|
+
Allow up to one more stream per queue for 30 seconds after reserve declines,
|
|
134
|
+
or up to `1 + number of Flow types` additional streams. The transient peak
|
|
135
|
+
during a burst remains bounded by the matching caps. Size these options for
|
|
136
|
+
`matching.maxConcurrentTasksCap` per app Worker ID per matching owner (256 by
|
|
137
|
+
default) and `matching.maxStreams` per Matching instance (100,000 by default).
|
|
129
138
|
|
|
130
139
|
The Worker rejects an unregistered Step or RPC by name. Dex retries that task
|
|
131
140
|
on another pull Worker without consuming a Step attempt, including when the
|
|
@@ -139,10 +148,7 @@ The generated `app_worker_id` uses the first nonempty `DEX_WORKER_INSTANCE`,
|
|
|
139
148
|
`POD_NAME`, `HOSTNAME`, or system hostname, plus eight random base32 characters.
|
|
140
149
|
An override must uniquely identify one Worker process and change after restart.
|
|
141
150
|
It must contain 1-63 ASCII letters, digits, dots, underscores, or hyphens and
|
|
142
|
-
must not equal a registered Flow type.
|
|
143
|
-
or an advertised address or headless target. Use `WorkerTarget(pull=True)` for
|
|
144
|
-
Client defaults or Flow overrides when the Worker target is not directly in
|
|
145
|
-
scope.
|
|
151
|
+
must not equal a registered Flow type.
|
|
146
152
|
|
|
147
153
|
Pull streams reconnect after failures. A server-directed reconnect waits for
|
|
148
154
|
an active task to finish, then waits up to `stop_drain_timeout` for the server
|
|
@@ -165,7 +171,7 @@ Worker ID queue to register at least once. Timeout raises `TimeoutError`;
|
|
|
165
171
|
async cancellation cancels only that wait. Startup failures reach waiters.
|
|
166
172
|
A permanent rejection before readiness raises `PullRegistrationError`, whose
|
|
167
173
|
`queue` and `error` identify the rejected queue and original transport error.
|
|
168
|
-
|
|
174
|
+
Stopping before readiness raises `RuntimeError`.
|
|
169
175
|
|
|
170
176
|
Use `worker.pull_registration_state()` for subsequent readiness probes. Its
|
|
171
177
|
immutable `queues` expose `queue`, `has_registered_stream`, and
|
|
@@ -190,11 +196,11 @@ limit is at least two, otherwise zero. With the CPU-count default, Steps get one
|
|
|
190
196
|
fewer slot than before. Set it explicitly to zero to opt out. With one slot and
|
|
191
197
|
no explicit reserve, Worker start logs one warning: RPCs may wait behind
|
|
192
198
|
long-running Steps; use a limit of at least two or set the reserve explicitly.
|
|
193
|
-
Flows must stay responsive
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
199
|
+
Flows must stay responsive because RPCs share Worker task capacity with Steps.
|
|
200
|
+
The value must be nonnegative and less than `max_concurrent_tasks` after its
|
|
201
|
+
CPU-count default is resolved; invalid values fail construction. RPC and Step
|
|
202
|
+
handlers count together. WaitFor and Execute tasks, including local attempts,
|
|
203
|
+
are declined with `NO_CAPACITY` when the busy
|
|
198
204
|
count reaches the task limit minus the reserve. API-direct and transactional
|
|
199
205
|
InvokeWorkerRPC tasks can use the full limit.
|
|
200
206
|
|
|
@@ -206,15 +212,19 @@ A decline lets Dex try another Worker without consuming an attempt. If all
|
|
|
206
212
|
Workers remain at their Step limit, a Step can wait until the dispatch wait
|
|
207
213
|
expires, then receive `NO_WORKER_AVAILABLE`, consume an attempt, and follow its
|
|
208
214
|
retry policy, as when all Workers are full. Local attempts wait the local
|
|
209
|
-
dispatch wait, then fall back according to their policy.
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
dispatch wait, then fall back according to their policy. A queue that declines
|
|
216
|
+
a Step for reserved RPC capacity keeps one additional idle stream until 30
|
|
217
|
+
seconds after its last decline. Other queues retain their configured count.
|
|
218
|
+
The Worker does not open the extra stream while all task slots are busy. Set
|
|
219
|
+
`idle_streams_per_queue` to at least two for heavy Step backlogs. A reserve decline
|
|
220
|
+
does not close a stream or change registration readiness.
|
|
212
221
|
|
|
213
222
|
Each reserve decline records the Flow type through the dex._pull_worker logger
|
|
214
223
|
at DEBUG level. Export the cumulative `pull_reserved_rpc_declines` Worker value
|
|
215
|
-
to distinguish reserve pressure from full-capacity rejections. The count
|
|
216
|
-
|
|
217
|
-
|
|
224
|
+
to distinguish reserve pressure from full-capacity rejections. The count excludes
|
|
225
|
+
full-capacity rejections and unknown names. It counts decline events, including
|
|
226
|
+
repeated declines of the same Step dispatch while Dex retries it. Read its rate
|
|
227
|
+
to assess reserve pressure. It never resets and remains available after shutdown.
|
|
218
228
|
|
|
219
229
|
## Flow state I/O
|
|
220
230
|
|
|
@@ -372,7 +382,7 @@ explicit codec only for a custom encoding or a type Registry cannot derive.
|
|
|
372
382
|
partitions them by definition type.
|
|
373
383
|
|
|
374
384
|
`Worker` and `AsyncWorker` synchronize all registered Indexed Attributes with
|
|
375
|
-
Dex Server before opening
|
|
385
|
+
Dex Server before opening outbound task streams. Existing indexes return immediately;
|
|
376
386
|
failure or the default two-minute deadline aborts startup. An indexed
|
|
377
387
|
`AttributeMap` must provide one fixed `index_key`.
|
|
378
388
|
|
|
@@ -768,8 +778,8 @@ environment:
|
|
|
768
778
|
./run-integration-tests.sh
|
|
769
779
|
```
|
|
770
780
|
|
|
771
|
-
|
|
772
|
-
|
|
781
|
+
The suite always uses pull Workers. Set `DEX_INTEG_ENGINE=zd` to test against
|
|
782
|
+
server-zd, or leave it unset to use the Temporal-backed server.
|
|
773
783
|
|
|
774
784
|
### Measure integration coverage
|
|
775
785
|
|
|
@@ -842,9 +852,9 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#releases-monorepo-tags) for monorepo ta
|
|
|
842
852
|
## Server protocol compatibility
|
|
843
853
|
|
|
844
854
|
Both `Worker` and `AsyncWorker` call `GetServerInfo`, negotiate the highest
|
|
845
|
-
common protocol, synchronize Attribute indexes, and only then
|
|
846
|
-
|
|
847
|
-
invalid or disjoint intervals, and RPC failures stop startup before
|
|
855
|
+
common protocol, synchronize Attribute indexes, and only then open outbound
|
|
856
|
+
task streams. The initial Python SDK interval is `[1,1]`. Missing information,
|
|
857
|
+
invalid or disjoint intervals, and RPC failures stop startup before polling.
|
|
848
858
|
The current Python SDK interval is `[1,3]`. Protocol 3 adds hard-delete
|
|
849
859
|
Attributes. Workers record the negotiated version at startup and reject each
|
|
850
860
|
handler result that contains a hard-delete Attribute write when it is below 3.
|
|
@@ -124,7 +124,7 @@ from dex.timer import Timer
|
|
|
124
124
|
from dex.wait import Wait
|
|
125
125
|
from dex.wait_options import WaitForAttributeOptions, WaitForStepCompletionOptions
|
|
126
126
|
from dex.worker import Worker
|
|
127
|
-
from dex.worker_options import PullWorkerOptions, WorkerOptions
|
|
127
|
+
from dex.worker_options import PullWorkerOptions, WorkerOptions
|
|
128
128
|
|
|
129
129
|
__all__ = [
|
|
130
130
|
"BOOL",
|
|
@@ -226,7 +226,6 @@ __all__ = [
|
|
|
226
226
|
"Worker",
|
|
227
227
|
"WorkerInvocationError",
|
|
228
228
|
"WorkerOptions",
|
|
229
|
-
"WorkerTarget",
|
|
230
229
|
"PullWorkerOptions",
|
|
231
230
|
"PullRegistrationError",
|
|
232
231
|
"PullRegistrationState",
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import traceback
|
|
12
12
|
from dataclasses import dataclass
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import Literal, cast
|
|
14
14
|
|
|
15
15
|
import grpc
|
|
16
16
|
from google.protobuf import any_pb2
|
|
@@ -165,23 +165,6 @@ def _worker_code(details: pb.ServiceErrorResponse | None) -> grpc.StatusCode | N
|
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
def abort_worker_error(
|
|
169
|
-
context: grpc.ServicerContext,
|
|
170
|
-
error: BaseException,
|
|
171
|
-
) -> None:
|
|
172
|
-
context.abort_with_status(rpc_status.to_status(_worker_error_status(error)))
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
async def async_abort_worker_error(
|
|
176
|
-
context: grpc.aio.ServicerContext[Any, Any],
|
|
177
|
-
error: BaseException,
|
|
178
|
-
) -> None:
|
|
179
|
-
# types-grpcio on 3.11 omits aio abort_with_status; runtime provides it.
|
|
180
|
-
await cast(Any, context).abort_with_status(
|
|
181
|
-
rpc_status.to_status(_worker_error_status(error))
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
168
|
def _worker_error_status(error: BaseException) -> status_pb2.Status:
|
|
186
169
|
retry_after_error: RetryAfterError | None = None
|
|
187
170
|
reported = error
|
|
@@ -16,6 +16,7 @@ import random
|
|
|
16
16
|
import re
|
|
17
17
|
import socket
|
|
18
18
|
import threading
|
|
19
|
+
import time
|
|
19
20
|
from concurrent.futures import ThreadPoolExecutor
|
|
20
21
|
from datetime import datetime, timezone
|
|
21
22
|
from typing import Any
|
|
@@ -37,6 +38,7 @@ from dex.worker_options import PullWorkerOptions
|
|
|
37
38
|
|
|
38
39
|
_LOGGER = logging.getLogger(__name__)
|
|
39
40
|
_GRPC_AIO_EOF = getattr(grpc.aio, "EOF")
|
|
41
|
+
_BUSY_PULL_STREAM_KEEPALIVE_SECONDS = 20
|
|
40
42
|
_PERMANENT_CODES = {
|
|
41
43
|
grpc.StatusCode.UNAUTHENTICATED,
|
|
42
44
|
grpc.StatusCode.PERMISSION_DENIED,
|
|
@@ -86,6 +88,7 @@ class PullWorkerRuntime:
|
|
|
86
88
|
executor: ThreadPoolExecutor | None,
|
|
87
89
|
app_worker_id: str | None = None,
|
|
88
90
|
credential_runner: AsyncCredentialRunner | None = None,
|
|
91
|
+
reserve_pressure_linger: float = 30.0,
|
|
89
92
|
) -> None:
|
|
90
93
|
self._registry = registry
|
|
91
94
|
self.app_worker_id = app_worker_id or resolve_app_worker_id(registry, options)
|
|
@@ -107,6 +110,9 @@ class PullWorkerRuntime:
|
|
|
107
110
|
options.reserved_rpc_tasks is None and self.max_concurrent_tasks == 1
|
|
108
111
|
)
|
|
109
112
|
self.reserved_rpc_declines = 0
|
|
113
|
+
self._last_reserve_decline: dict[str, float] = {}
|
|
114
|
+
self._reserve_pressure_timers: dict[str, asyncio.TimerHandle] = {}
|
|
115
|
+
self._reserve_pressure_linger = reserve_pressure_linger
|
|
110
116
|
self.idle_streams_per_queue = options.idle_streams_per_queue or 1
|
|
111
117
|
self.stop_drain_timeout = options.stop_drain_timeout.total_seconds() or 30
|
|
112
118
|
self._dispatcher = dispatcher
|
|
@@ -196,12 +202,26 @@ class PullWorkerRuntime:
|
|
|
196
202
|
and not stream.is_half_closing
|
|
197
203
|
for stream in self._streams
|
|
198
204
|
)
|
|
199
|
-
|
|
205
|
+
desired_idle = self.desired_idle_streams_per_queue(queue)
|
|
206
|
+
creation_target = (
|
|
207
|
+
self.idle_streams_per_queue
|
|
208
|
+
if self._busy_tasks == self.max_concurrent_tasks
|
|
209
|
+
else desired_idle
|
|
210
|
+
)
|
|
211
|
+
for _ in range(creation_target - idle):
|
|
200
212
|
stream = PullStream(self, stub, queue)
|
|
201
213
|
self._streams.add(stream)
|
|
202
214
|
stream.run_task = asyncio.create_task(stream.run())
|
|
203
215
|
stream.run_task.add_done_callback(stream.on_done)
|
|
204
216
|
|
|
217
|
+
def desired_idle_streams_per_queue(self, queue: str) -> int:
|
|
218
|
+
last_decline = self._last_reserve_decline.get(queue)
|
|
219
|
+
return self.idle_streams_per_queue + int(
|
|
220
|
+
self.reserved_rpc_tasks > 0
|
|
221
|
+
and last_decline is not None
|
|
222
|
+
and time.monotonic() - last_decline < self._reserve_pressure_linger
|
|
223
|
+
)
|
|
224
|
+
|
|
205
225
|
def _stream_done(self, stream: PullStream, completed: asyncio.Task[None]) -> None:
|
|
206
226
|
self._streams.discard(stream)
|
|
207
227
|
if not completed.cancelled():
|
|
@@ -210,7 +230,9 @@ class PullWorkerRuntime:
|
|
|
210
230
|
_LOGGER.error("Worker task stream failed", exc_info=failure)
|
|
211
231
|
self.signal()
|
|
212
232
|
|
|
213
|
-
def accept_task(
|
|
233
|
+
def accept_task(
|
|
234
|
+
self, task: pb.PollWorkerTask | None = None, queue: str | None = None
|
|
235
|
+
) -> bool:
|
|
214
236
|
if self._stopping is None or self._stopping.is_set():
|
|
215
237
|
return False
|
|
216
238
|
if self._busy_tasks >= self.max_concurrent_tasks:
|
|
@@ -223,6 +245,18 @@ class PullWorkerRuntime:
|
|
|
223
245
|
and self.unknown_task_reason(task) is None
|
|
224
246
|
):
|
|
225
247
|
self.reserved_rpc_declines += 1
|
|
248
|
+
queue_name = queue or self.app_worker_id
|
|
249
|
+
self._last_reserve_decline[queue_name] = time.monotonic()
|
|
250
|
+
existing_timer = self._reserve_pressure_timers.pop(queue_name, None)
|
|
251
|
+
if existing_timer is not None:
|
|
252
|
+
existing_timer.cancel()
|
|
253
|
+
if self._loop is not None:
|
|
254
|
+
self._reserve_pressure_timers[queue_name] = self._loop.call_later(
|
|
255
|
+
self._reserve_pressure_linger,
|
|
256
|
+
self._expire_reserve_pressure,
|
|
257
|
+
queue_name,
|
|
258
|
+
)
|
|
259
|
+
self.signal()
|
|
226
260
|
request = task.wait_for if task.HasField("wait_for") else task.execute
|
|
227
261
|
_LOGGER.debug(
|
|
228
262
|
"decline Step task for reserved RPC capacity: %s", request.flow_type
|
|
@@ -232,6 +266,21 @@ class PullWorkerRuntime:
|
|
|
232
266
|
self.signal()
|
|
233
267
|
return True
|
|
234
268
|
|
|
269
|
+
def _expire_reserve_pressure(self, queue: str) -> None:
|
|
270
|
+
last_decline = self._last_reserve_decline.get(queue)
|
|
271
|
+
if last_decline is None:
|
|
272
|
+
return
|
|
273
|
+
remaining = self._reserve_pressure_linger - (time.monotonic() - last_decline)
|
|
274
|
+
if remaining > 0:
|
|
275
|
+
assert self._loop is not None
|
|
276
|
+
self._reserve_pressure_timers[queue] = self._loop.call_later(
|
|
277
|
+
remaining, self._expire_reserve_pressure, queue
|
|
278
|
+
)
|
|
279
|
+
return
|
|
280
|
+
self._last_reserve_decline.pop(queue, None)
|
|
281
|
+
self._reserve_pressure_timers.pop(queue, None)
|
|
282
|
+
self.signal()
|
|
283
|
+
|
|
235
284
|
def finish_task(self) -> None:
|
|
236
285
|
self._busy_tasks -= 1
|
|
237
286
|
self.signal()
|
|
@@ -250,6 +299,9 @@ class PullWorkerRuntime:
|
|
|
250
299
|
asyncio.get_running_loop().time() + self.stop_drain_timeout
|
|
251
300
|
)
|
|
252
301
|
self._stopping.set()
|
|
302
|
+
for timer in self._reserve_pressure_timers.values():
|
|
303
|
+
timer.cancel()
|
|
304
|
+
self._reserve_pressure_timers.clear()
|
|
253
305
|
try:
|
|
254
306
|
for stream in tuple(self._streams):
|
|
255
307
|
stream.drain()
|
|
@@ -317,6 +369,8 @@ class PullStream:
|
|
|
317
369
|
self._active = threading.Event()
|
|
318
370
|
self._call: Any = None
|
|
319
371
|
self._send_lock = asyncio.Lock()
|
|
372
|
+
self._keepalive_task: asyncio.Task[None] | None = None
|
|
373
|
+
self._is_final_frame_sent = False
|
|
320
374
|
self._is_reconnecting = False
|
|
321
375
|
self._stream_metadata: Any = None
|
|
322
376
|
self._deadline_handle: asyncio.TimerHandle | None = None
|
|
@@ -424,6 +478,7 @@ class PullStream:
|
|
|
424
478
|
if self.runtime.is_stopping:
|
|
425
479
|
return False
|
|
426
480
|
self._supports_unknown_name_rejection = False
|
|
481
|
+
self._is_final_frame_sent = False
|
|
427
482
|
self._stream_metadata = None
|
|
428
483
|
if self.runtime._credential_runner is None:
|
|
429
484
|
self._call = self.stub.PollWorkerTask()
|
|
@@ -471,9 +526,11 @@ class PullStream:
|
|
|
471
526
|
and frame.WhichOneof("frame") == "task"
|
|
472
527
|
):
|
|
473
528
|
await self._reject_task(frame.task)
|
|
474
|
-
self.
|
|
475
|
-
|
|
476
|
-
|
|
529
|
+
async with self._send_lock:
|
|
530
|
+
self._stop_keepalive()
|
|
531
|
+
self.is_half_closing = True
|
|
532
|
+
self.runtime.signal()
|
|
533
|
+
await self._call.done_writing()
|
|
477
534
|
await self._await_stream_eof(reader)
|
|
478
535
|
reader = None
|
|
479
536
|
self._is_reconnecting = not self._is_retiring
|
|
@@ -515,6 +572,8 @@ class PullStream:
|
|
|
515
572
|
self._is_reconnecting = True
|
|
516
573
|
return was_registered
|
|
517
574
|
finally:
|
|
575
|
+
async with self._send_lock:
|
|
576
|
+
self._stop_keepalive()
|
|
518
577
|
retirement.cancel()
|
|
519
578
|
await asyncio.gather(retirement, return_exceptions=True)
|
|
520
579
|
self.is_half_closing = False
|
|
@@ -586,12 +645,18 @@ class PullStream:
|
|
|
586
645
|
if unknown_reason is not None:
|
|
587
646
|
await self._reject_task(task, unknown_reason)
|
|
588
647
|
return
|
|
589
|
-
if
|
|
648
|
+
if (
|
|
649
|
+
self.is_busy
|
|
650
|
+
or self.is_draining
|
|
651
|
+
or not self.runtime.accept_task(task, self.queue)
|
|
652
|
+
):
|
|
590
653
|
await self._reject_task(task)
|
|
591
654
|
return
|
|
592
655
|
self.is_busy = True
|
|
593
656
|
self.task_id = task.task_id
|
|
594
657
|
self._active.set()
|
|
658
|
+
self._is_final_frame_sent = False
|
|
659
|
+
self._keepalive_task = asyncio.create_task(self._send_busy_keepalives())
|
|
595
660
|
self.handler_task = asyncio.create_task(self._execute(task))
|
|
596
661
|
if task.HasField("deadline"):
|
|
597
662
|
deadline = task.deadline.ToDatetime(tzinfo=timezone.utc)
|
|
@@ -716,14 +781,68 @@ class PullStream:
|
|
|
716
781
|
async def _send_frame(self, frame: pb.PollWorkerTaskFrame) -> None:
|
|
717
782
|
await self._send(pb.PollWorkerTaskRequest(task_frame=frame))
|
|
718
783
|
|
|
784
|
+
async def _send_busy_keepalives(self) -> None:
|
|
785
|
+
try:
|
|
786
|
+
while True:
|
|
787
|
+
await asyncio.sleep(_BUSY_PULL_STREAM_KEEPALIVE_SECONDS)
|
|
788
|
+
async with self._send_lock:
|
|
789
|
+
if self._is_final_frame_sent or self.is_half_closing:
|
|
790
|
+
return
|
|
791
|
+
await self._call.write(
|
|
792
|
+
pb.PollWorkerTaskRequest(pong=pb.PollWorkerTaskPong())
|
|
793
|
+
)
|
|
794
|
+
except asyncio.CancelledError:
|
|
795
|
+
return
|
|
796
|
+
except Exception:
|
|
797
|
+
_LOGGER.exception("send Worker keepalive Pong failed")
|
|
798
|
+
|
|
799
|
+
def _stop_keepalive(self) -> None:
|
|
800
|
+
if self._keepalive_task is not None:
|
|
801
|
+
self._keepalive_task.cancel()
|
|
802
|
+
self._keepalive_task = None
|
|
803
|
+
|
|
719
804
|
async def _send(self, request: pb.PollWorkerTaskRequest) -> None:
|
|
720
805
|
async with self._send_lock:
|
|
806
|
+
if request.HasField("task_frame"):
|
|
807
|
+
frame = request.task_frame
|
|
808
|
+
is_progress_frame = (
|
|
809
|
+
frame.HasField("wait_for")
|
|
810
|
+
and frame.wait_for.WhichOneof("output")
|
|
811
|
+
in ("heartbeat", "stream_write")
|
|
812
|
+
or frame.HasField("execute")
|
|
813
|
+
and frame.execute.WhichOneof("output")
|
|
814
|
+
in ("heartbeat", "stream_write")
|
|
815
|
+
)
|
|
816
|
+
if (
|
|
817
|
+
frame.task_id != self.task_id
|
|
818
|
+
or self._is_final_frame_sent
|
|
819
|
+
or not self._active.is_set()
|
|
820
|
+
and is_progress_frame
|
|
821
|
+
):
|
|
822
|
+
return
|
|
823
|
+
if (
|
|
824
|
+
frame.HasField("rpc")
|
|
825
|
+
or frame.HasField("worker_error")
|
|
826
|
+
or frame.HasField("wait_for")
|
|
827
|
+
and frame.wait_for.WhichOneof("output") == "result"
|
|
828
|
+
or frame.HasField("execute")
|
|
829
|
+
and frame.execute.WhichOneof("output") == "result"
|
|
830
|
+
):
|
|
831
|
+
self._stop_keepalive()
|
|
832
|
+
self._is_final_frame_sent = True
|
|
833
|
+
if request.HasField("pong") and (
|
|
834
|
+
self._is_final_frame_sent or self.is_half_closing
|
|
835
|
+
):
|
|
836
|
+
return
|
|
721
837
|
await self._call.write(request)
|
|
722
838
|
|
|
723
839
|
async def _finish_handler(self) -> None:
|
|
724
840
|
if self.handler_task is None:
|
|
725
841
|
return
|
|
726
842
|
await asyncio.gather(self.handler_task, return_exceptions=True)
|
|
843
|
+
async with self._send_lock:
|
|
844
|
+
self._stop_keepalive()
|
|
845
|
+
self._is_final_frame_sent = False
|
|
727
846
|
if self._deadline_handle is not None:
|
|
728
847
|
self._deadline_handle.cancel()
|
|
729
848
|
self._deadline_handle = None
|
|
@@ -744,7 +863,7 @@ class PullStream:
|
|
|
744
863
|
and not candidate.is_draining
|
|
745
864
|
for candidate in self.runtime._streams
|
|
746
865
|
)
|
|
747
|
-
if idle_replacements >= self.runtime.
|
|
866
|
+
if idle_replacements >= self.runtime.desired_idle_streams_per_queue(self.queue):
|
|
748
867
|
self.is_draining = True
|
|
749
868
|
self._is_retiring = True
|
|
750
869
|
self._retirement_requested.set()
|