chalk-remote-call-python 1.8.7__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.
Files changed (66) hide show
  1. {chalk_remote_call_python-1.8.7/chalk_remote_call_python.egg-info → chalk_remote_call_python-1.9.0}/PKG-INFO +2 -1
  2. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/README.md +1 -0
  3. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/Cargo.lock +7 -7
  4. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml +3 -3
  5. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-server/src/self_consumer.rs +222 -61
  6. chalk_remote_call_python-1.9.0/chalk_remote_call/_version.py +1 -0
  7. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0/chalk_remote_call_python.egg-info}/PKG-INFO +2 -1
  8. chalk_remote_call_python-1.8.7/chalk_remote_call/_version.py +0 -1
  9. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/MANIFEST.in +0 -0
  10. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/Cargo.toml +0 -0
  11. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-proto/Cargo.toml +0 -0
  12. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.auth.v1.rs +0 -0
  13. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.common.v1.rs +0 -0
  14. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.rs +0 -0
  15. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.tonic.rs +0 -0
  16. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.utils.v1.rs +0 -0
  17. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/descriptor.bin +0 -0
  18. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-proto/src/lib.rs +0 -0
  19. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-server/src/async_service.rs +0 -0
  20. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-server/src/coalesce.rs +0 -0
  21. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-server/src/lib.rs +0 -0
  22. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-server/src/metrics.rs +0 -0
  23. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-server/src/python_bridge.rs +0 -0
  24. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-server/src/server.rs +0 -0
  25. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/chalk-remote-call-server/src/service.rs +0 -0
  26. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk-remote-call-rs/rust-toolchain.toml +0 -0
  27. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/__init__.py +0 -0
  28. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/__main__.py +0 -0
  29. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/__init__.py +0 -0
  30. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/__init__.py +0 -0
  31. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
  32. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
  33. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +0 -0
  34. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +0 -0
  35. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
  36. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
  37. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +0 -0
  38. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +0 -0
  39. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
  40. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
  41. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +0 -0
  42. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +0 -0
  43. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
  44. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
  45. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +0 -0
  46. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +0 -0
  47. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +0 -0
  48. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +0 -0
  49. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +0 -0
  50. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +0 -0
  51. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/_native.pyi +0 -0
  52. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/arrow_utils.py +0 -0
  53. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/cli.py +0 -0
  54. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/handler_loader.py +0 -0
  55. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/input_transform.py +0 -0
  56. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/server.py +0 -0
  57. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/servicer.py +0 -0
  58. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call/tracing.py +0 -0
  59. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call_python.egg-info/SOURCES.txt +0 -0
  60. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call_python.egg-info/dependency_links.txt +0 -0
  61. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call_python.egg-info/entry_points.txt +0 -0
  62. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call_python.egg-info/requires.txt +0 -0
  63. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/chalk_remote_call_python.egg-info/top_level.txt +0 -0
  64. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/pyproject.toml +0 -0
  65. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/setup.cfg +0 -0
  66. {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.8.7
3
+ Version: 1.9.0
4
4
  Summary: Chalk remote call Python runtime interface client
5
5
  Author: Chalk AI, Inc.
6
6
  Project-URL: Homepage, https://chalk.ai
@@ -113,6 +113,7 @@ python -m chalk_remote_call --handler my_handler.handler
113
113
  | Variable | Description |
114
114
  |----------|-------------|
115
115
  | `CHALK_INPUT_ARGS` | Comma-separated list of column names (e.g. `x,y,z`). Renames incoming RecordBatch columns by index. If unset, the original column names are used. Reserved columns (below) are held out of this mapping and keep their own name. |
116
+ | `CHALK_FNQ_QUEUE_PROTOCOL` | Self-consumer work transport: `list_v1` (default/backward compatible) or `stream_v1`. Deployment tooling sets this from immutable scaling-group revision metadata. |
116
117
 
117
118
  #### Reserved columns
118
119
 
@@ -75,6 +75,7 @@ python -m chalk_remote_call --handler my_handler.handler
75
75
  | Variable | Description |
76
76
  |----------|-------------|
77
77
  | `CHALK_INPUT_ARGS` | Comma-separated list of column names (e.g. `x,y,z`). Renames incoming RecordBatch columns by index. If unset, the original column names are used. Reserved columns (below) are held out of this mapping and keep their own name. |
78
+ | `CHALK_FNQ_QUEUE_PROTOCOL` | Self-consumer work transport: `list_v1` (default/backward compatible) or `stream_v1`. Deployment tooling sets this from immutable scaling-group revision metadata. |
78
79
 
79
80
  #### Reserved columns
80
81
 
@@ -484,7 +484,7 @@ dependencies = [
484
484
  [[package]]
485
485
  name = "chalk-core"
486
486
  version = "0.0.0"
487
- source = "git+https://github.com/chalk-ai/chalk-private.git?rev=ae8e792f746f77253f682ee0c56b246cfdd88f51#ae8e792f746f77253f682ee0c56b246cfdd88f51"
487
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=783e9ce6b2c#783e9ce6b2c17563ab3302e5d11f8a664ed00232"
488
488
  dependencies = [
489
489
  "anyhow",
490
490
  "envconfig",
@@ -499,7 +499,7 @@ dependencies = [
499
499
  [[package]]
500
500
  name = "chalk-protos"
501
501
  version = "0.0.0"
502
- source = "git+https://github.com/chalk-ai/chalk-private.git?rev=ae8e792f746f77253f682ee0c56b246cfdd88f51#ae8e792f746f77253f682ee0c56b246cfdd88f51"
502
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=783e9ce6b2c#783e9ce6b2c17563ab3302e5d11f8a664ed00232"
503
503
  dependencies = [
504
504
  "prost 0.11.9",
505
505
  "prost-build",
@@ -511,7 +511,7 @@ dependencies = [
511
511
  [[package]]
512
512
  name = "chalk-queue"
513
513
  version = "0.0.0"
514
- source = "git+https://github.com/chalk-ai/chalk-private.git?rev=ae8e792f746f77253f682ee0c56b246cfdd88f51#ae8e792f746f77253f682ee0c56b246cfdd88f51"
514
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=783e9ce6b2c#783e9ce6b2c17563ab3302e5d11f8a664ed00232"
515
515
  dependencies = [
516
516
  "base64 0.22.1",
517
517
  "redis",
@@ -560,7 +560,7 @@ dependencies = [
560
560
  [[package]]
561
561
  name = "chalk_metric_definitions"
562
562
  version = "0.0.0"
563
- source = "git+https://github.com/chalk-ai/chalk-private.git?rev=ae8e792f746f77253f682ee0c56b246cfdd88f51#ae8e792f746f77253f682ee0c56b246cfdd88f51"
563
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=783e9ce6b2c#783e9ce6b2c17563ab3302e5d11f8a664ed00232"
564
564
  dependencies = [
565
565
  "chalk-protos",
566
566
  "metrics-generator",
@@ -570,7 +570,7 @@ dependencies = [
570
570
  [[package]]
571
571
  name = "chalk_metrics"
572
572
  version = "0.0.0"
573
- source = "git+https://github.com/chalk-ai/chalk-private.git?rev=ae8e792f746f77253f682ee0c56b246cfdd88f51#ae8e792f746f77253f682ee0c56b246cfdd88f51"
573
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=783e9ce6b2c#783e9ce6b2c17563ab3302e5d11f8a664ed00232"
574
574
  dependencies = [
575
575
  "async-trait",
576
576
  "chalk-core",
@@ -604,7 +604,7 @@ dependencies = [
604
604
  [[package]]
605
605
  name = "chalk_statsd"
606
606
  version = "0.0.0"
607
- source = "git+https://github.com/chalk-ai/chalk-private.git?rev=ae8e792f746f77253f682ee0c56b246cfdd88f51#ae8e792f746f77253f682ee0c56b246cfdd88f51"
607
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=783e9ce6b2c#783e9ce6b2c17563ab3302e5d11f8a664ed00232"
608
608
  dependencies = [
609
609
  "chalk-core",
610
610
  "chrono",
@@ -1867,7 +1867,7 @@ dependencies = [
1867
1867
  [[package]]
1868
1868
  name = "metrics-generator"
1869
1869
  version = "0.1.0"
1870
- source = "git+https://github.com/chalk-ai/chalk-private.git?rev=ae8e792f746f77253f682ee0c56b246cfdd88f51#ae8e792f746f77253f682ee0c56b246cfdd88f51"
1870
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=783e9ce6b2c#783e9ce6b2c17563ab3302e5d11f8a664ed00232"
1871
1871
  dependencies = [
1872
1872
  "codegen",
1873
1873
  "serde",
@@ -36,6 +36,6 @@ futures-util = "0.3"
36
36
  # `[patch.crates-io]` here it builds against vanilla crates.io redis (it only
37
37
  # uses standard APIs). Same rev across all three so cargo reuses one checkout.
38
38
  [target.'cfg(target_os = "linux")'.dependencies]
39
- chalk-queue = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "ae8e792f746f77253f682ee0c56b246cfdd88f51" }
40
- chalk_metrics = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "ae8e792f746f77253f682ee0c56b246cfdd88f51" }
41
- chalk_statsd = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "ae8e792f746f77253f682ee0c56b246cfdd88f51" }
39
+ chalk-queue = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "783e9ce6b2c" }
40
+ chalk_metrics = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "783e9ce6b2c" }
41
+ chalk_statsd = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "783e9ce6b2c" }
@@ -3,7 +3,10 @@
3
3
  //! This runs *alongside* the gRPC server, which is untouched: the synchronous
4
4
  //! / direct `CallFunction` path keeps serving inbound calls.
5
5
  //!
6
- //! Concurrency is capped by a Redis-backed distributed semaphore (per function,
6
+ //! Work is read from the revision's advertised Redis List or Stream transport.
7
+ //! Stream deliveries are acknowledged only after result writes complete and
8
+ //! reclaimed after a crashed owner stops heartbeating. Concurrency is capped
9
+ //! by a Redis-backed distributed semaphore (per function,
7
10
  //! across every pod in the scaling group). The dispatch loop dispatches as fast
8
11
  //! as it can acquire a slot and pop work; in-flight worker tasks are therefore
9
12
  //! bounded by `max_concurrent`, not by traffic. A single per-pod renewer task
@@ -30,14 +33,17 @@
30
33
 
31
34
  use std::collections::HashMap;
32
35
  use std::panic::AssertUnwindSafe;
36
+ use std::str::FromStr;
33
37
  use std::sync::{Arc, Mutex};
34
38
  use std::time::{Duration, Instant};
35
39
 
36
40
  use chalk_queue::semaphore::renew_many;
37
41
  use chalk_queue::stream_writer;
38
42
  use chalk_queue::types::QueuedCall;
39
- use chalk_queue::{FunctionQueue, SemaphoreLease};
40
- use dashmap::DashSet;
43
+ use chalk_queue::{
44
+ BoundFunctionQueue, DequeuedCall, FunctionQueue, QueueProtocol, QueueReceipt, SemaphoreLease,
45
+ };
46
+ use dashmap::DashMap;
41
47
  use futures_util::FutureExt;
42
48
  use redis::aio::ConnectionManager;
43
49
  use redis::Client;
@@ -59,6 +65,7 @@ const ENV_SEM_TTL_SECS: &str = "CHALK_FNQ_SEM_TTL_SECS";
59
65
  const ENV_POLL_BACKOFF_INITIAL_MS: &str = "CHALK_FNQ_POLL_BACKOFF_INITIAL_MS";
60
66
  const ENV_POLL_BACKOFF_MAX_SECS: &str = "CHALK_FNQ_POLL_BACKOFF_MAX_SECS";
61
67
  const ENV_FLUSH_TIMEOUT_MS: &str = "CHALK_FNQ_FLUSH_TIMEOUT_MS";
68
+ const ENV_QUEUE_PROTOCOL: &str = "CHALK_FNQ_QUEUE_PROTOCOL";
62
69
 
63
70
  /// Lease lifetime (default). Intentionally short relative to how long a call may run: the
64
71
  /// per-pod renewer keeps a live call's lease alive (see module docs), so the
@@ -101,6 +108,7 @@ pub struct SelfConsumerConfig {
101
108
  pub poll_backoff_initial: Duration,
102
109
  pub poll_backoff_max: Duration,
103
110
  pub flush_timeout: Duration,
111
+ pub queue_protocol: QueueProtocol,
104
112
  }
105
113
 
106
114
  impl SelfConsumerConfig {
@@ -162,6 +170,16 @@ impl SelfConsumerConfig {
162
170
  .filter(|&n| n > 0)
163
171
  .unwrap_or(DEFAULT_FLUSH_TIMEOUT_MS);
164
172
  let flush_timeout = Duration::from_millis(flush_timeout_ms);
173
+ let queue_protocol = std::env::var(ENV_QUEUE_PROTOCOL)
174
+ .ok()
175
+ .filter(|value| !value.is_empty())
176
+ .map(|value| {
177
+ QueueProtocol::from_str(&value).unwrap_or_else(|error| {
178
+ warn!(%error, "invalid queue protocol; using legacy List transport");
179
+ QueueProtocol::ListV1
180
+ })
181
+ })
182
+ .unwrap_or_default();
165
183
 
166
184
  Some(Self {
167
185
  redis_url,
@@ -172,6 +190,7 @@ impl SelfConsumerConfig {
172
190
  poll_backoff_initial,
173
191
  poll_backoff_max,
174
192
  flush_timeout,
193
+ queue_protocol,
175
194
  })
176
195
  }
177
196
  }
@@ -179,7 +198,7 @@ impl SelfConsumerConfig {
179
198
  /// Set of lease tokens this pod currently holds. The dispatch loop inserts a
180
199
  /// token when it admits a call; the worker removes it on completion. The
181
200
  /// renewer heartbeats whatever is in the set on each tick.
182
- type LiveTokens = Arc<DashSet<String>>;
201
+ type LiveCalls = Arc<DashMap<String, QueueReceipt>>;
183
202
 
184
203
  /// Connect to Redis and run the consume loop until the process exits. Returns
185
204
  /// `Err` only on a fatal setup failure (bad URL / unreachable Redis at startup);
@@ -194,16 +213,32 @@ pub async fn run(
194
213
  let client = Client::open(config.redis_url.clone())?;
195
214
  let conn = ConnectionManager::new(client).await?;
196
215
  let queue = FunctionQueue::new(conn.clone(), config.tenant.clone());
216
+ let input_queue = queue.bind(&config.function_name, config.queue_protocol);
217
+ let legacy_queue = (config.queue_protocol == QueueProtocol::StreamV1)
218
+ .then(|| queue.bind(&config.function_name, QueueProtocol::ListV1));
197
219
  let sem = queue.semaphore(&config.function_name, config.max_concurrent, config.sem_ttl);
198
220
  let sem_key = queue.sem_key_for(&config.function_name);
199
- let live_tokens: LiveTokens = Arc::new(DashSet::new());
221
+ let live_calls: LiveCalls = Arc::new(DashMap::new());
222
+ let consumer_name = format!(
223
+ "{}:{}",
224
+ std::env::var("HOSTNAME").unwrap_or_else(|_| "fnq-shim".to_string()),
225
+ Uuid::new_v4()
226
+ );
200
227
 
201
- spawn_renewer(conn.clone(), sem_key, live_tokens.clone(), config.sem_ttl);
228
+ spawn_renewer(
229
+ input_queue.clone(),
230
+ conn.clone(),
231
+ sem_key,
232
+ live_calls.clone(),
233
+ config.sem_ttl,
234
+ );
202
235
 
203
236
  info!(
204
237
  function = %config.function_name,
205
238
  tenant = %config.tenant,
206
239
  max_concurrent = config.max_concurrent,
240
+ queue_protocol = config.queue_protocol.as_str(),
241
+ consumer = %consumer_name,
207
242
  "fnq self-consumer started"
208
243
  );
209
244
 
@@ -211,8 +246,9 @@ pub async fn run(
211
246
  loop {
212
247
  // Idle-gate: if there is no work, don't take (then immediately release)
213
248
  // a semaphore slot — that churn would race genuine acquirers on other
214
- // pods. LLEN is as cheap as the RPOP we'd otherwise do.
215
- match queue.current_depth(&config.function_name).await {
249
+ // pods. During a Stream rollout, also observe the legacy List so a new
250
+ // shim can drain work queued before its revision metadata changed.
251
+ match pending_depth(&input_queue, legacy_queue.as_ref()).await {
216
252
  Ok(0) => {
217
253
  tokio::time::sleep(backoff).await;
218
254
  backoff = (backoff * 2).min(config.poll_backoff_max);
@@ -243,8 +279,15 @@ pub async fn run(
243
279
  }
244
280
  };
245
281
 
246
- let work = match queue.pop(&config.function_name).await {
247
- Ok(Some(work)) => work,
282
+ let delivery = match dequeue_next(
283
+ &input_queue,
284
+ legacy_queue.as_ref(),
285
+ &consumer_name,
286
+ config.sem_ttl,
287
+ )
288
+ .await
289
+ {
290
+ Ok(Some(delivery)) => delivery,
248
291
  // Raced another consumer (or our own idle-gate) to an empty queue:
249
292
  // give the slot back and retry.
250
293
  Ok(None) => {
@@ -253,7 +296,7 @@ pub async fn run(
253
296
  continue;
254
297
  }
255
298
  Err(e) => {
256
- warn!(error = %e, "pop failed; releasing slot");
299
+ warn!(error = %e, "dequeue failed; releasing slot");
257
300
  let _ = lease.release().await;
258
301
  backoff = (backoff * 2).min(config.poll_backoff_max);
259
302
  continue;
@@ -264,7 +307,7 @@ pub async fn run(
264
307
 
265
308
  // Register the token so the pod-level renewer heartbeats this lease
266
309
  // while the call runs.
267
- live_tokens.insert(lease.token().to_string());
310
+ live_calls.insert(lease.token().to_string(), delivery.receipt.clone());
268
311
 
269
312
  match &coalescing_queue {
270
313
  // Batching on: route through the shared coalescing queue so this
@@ -273,11 +316,12 @@ pub async fn run(
273
316
  Some(cq) => {
274
317
  tokio::spawn(run_one_coalesced_call(
275
318
  queue.clone(),
319
+ input_queue.clone(),
276
320
  cq.clone(),
277
321
  conn.clone(),
278
- work,
322
+ delivery,
279
323
  lease,
280
- live_tokens.clone(),
324
+ live_calls.clone(),
281
325
  metrics_pipeline.clone(),
282
326
  ));
283
327
  }
@@ -286,10 +330,11 @@ pub async fn run(
286
330
  tokio::spawn(run_one_call(
287
331
  handler.clone(),
288
332
  queue.clone(),
333
+ input_queue.clone(),
289
334
  conn.clone(),
290
- work,
335
+ delivery,
291
336
  lease,
292
- live_tokens.clone(),
337
+ live_calls.clone(),
293
338
  config.flush_timeout,
294
339
  metrics_pipeline.clone(),
295
340
  ));
@@ -300,6 +345,43 @@ pub async fn run(
300
345
  }
301
346
  }
302
347
 
348
+ async fn pending_depth(
349
+ queue: &BoundFunctionQueue,
350
+ legacy_queue: Option<&BoundFunctionQueue>,
351
+ ) -> Result<u64, redis::RedisError> {
352
+ let selected = queue.current_depth().await?;
353
+ let legacy = match legacy_queue {
354
+ Some(queue) => queue.current_depth().await?,
355
+ None => 0,
356
+ };
357
+ Ok(selected + legacy)
358
+ }
359
+
360
+ async fn dequeue_next(
361
+ queue: &BoundFunctionQueue,
362
+ legacy_queue: Option<&BoundFunctionQueue>,
363
+ consumer_name: &str,
364
+ reclaim_idle: Duration,
365
+ ) -> Result<Option<DequeuedCall>, Box<dyn std::error::Error + Send + Sync>> {
366
+ // Recovered work wins first. The legacy List is then drained before new
367
+ // Stream traffic; producers never route new work to both transports.
368
+ if let Some(reclaimed) = queue
369
+ .reclaim_stale(consumer_name, reclaim_idle, 1)
370
+ .await?
371
+ .into_iter()
372
+ .next()
373
+ {
374
+ return Ok(Some(reclaimed));
375
+ }
376
+ if let Some(legacy_queue) = legacy_queue {
377
+ if let Some(legacy) = legacy_queue.dequeue(consumer_name).await? {
378
+ info!(function = %queue.function_name(), "draining legacy List item during Stream migration");
379
+ return Ok(Some(legacy));
380
+ }
381
+ }
382
+ queue.dequeue(consumer_name).await
383
+ }
384
+
303
385
  /// Run a single queued call to completion, write its results to the call's
304
386
  /// stream, then deregister + release its lease. Always releases the lease,
305
387
  /// even if execution or stream writes fail or panic.
@@ -307,15 +389,20 @@ pub async fn run(
307
389
  async fn run_one_call(
308
390
  handler: Arc<PythonHandler>,
309
391
  queue: FunctionQueue,
392
+ input_queue: BoundFunctionQueue,
310
393
  mut conn: ConnectionManager,
311
- work: QueuedCall,
394
+ delivery: DequeuedCall,
312
395
  lease: SemaphoreLease,
313
- live_tokens: LiveTokens,
396
+ live_calls: LiveCalls,
314
397
  flush_timeout: Duration,
315
398
  metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
316
399
  ) {
400
+ let DequeuedCall {
401
+ call: work,
402
+ receipt,
403
+ } = delivery;
317
404
  let call_id = work.call_id.clone();
318
- match AssertUnwindSafe(execute_and_write(
405
+ let completed = match AssertUnwindSafe(execute_and_write(
319
406
  &handler,
320
407
  &queue,
321
408
  &mut conn,
@@ -326,11 +413,12 @@ async fn run_one_call(
326
413
  .catch_unwind()
327
414
  .await
328
415
  {
329
- Ok(Ok(())) => {}
416
+ Ok(Ok(())) => true,
330
417
  Ok(Err(e)) => {
331
418
  // The call's result stream may be incomplete; the call's 24h stream TTL
332
419
  // and the meta status are the backstop for a stuck poller.
333
420
  error!(call_id = %call_id, error = %e, "failed to execute/write queued call");
421
+ false
334
422
  }
335
423
  Err(panic_payload) => {
336
424
  let panic_message = panic_payload
@@ -343,12 +431,41 @@ async fn run_one_call(
343
431
  panic = %panic_message,
344
432
  "panic while executing/writing queued call"
345
433
  );
434
+ false
346
435
  }
347
- }
436
+ };
437
+
438
+ finish_delivery(
439
+ &input_queue,
440
+ &receipt,
441
+ completed,
442
+ &call_id,
443
+ lease,
444
+ &live_calls,
445
+ )
446
+ .await;
447
+ }
348
448
 
349
- // Deregister before releasing so the renewer stops heartbeating this token
350
- // immediately; the subsequent release ZREMs it from the semaphore ZSET.
351
- live_tokens.remove(lease.token());
449
+ async fn finish_delivery(
450
+ queue: &BoundFunctionQueue,
451
+ receipt: &QueueReceipt,
452
+ completed: bool,
453
+ call_id: &str,
454
+ lease: SemaphoreLease,
455
+ live_calls: &LiveCalls,
456
+ ) {
457
+ // Stop renewing before ack. If completion or ack failed, the Stream item
458
+ // becomes reclaimable after the idle timeout; List behavior is unchanged.
459
+ live_calls.remove(lease.token());
460
+ if completed {
461
+ match queue.acknowledge(receipt).await {
462
+ Ok(true) => {}
463
+ Ok(false) => warn!(%call_id, "delivery ownership was lost before acknowledgement"),
464
+ Err(error) => {
465
+ warn!(%call_id, %error, "delivery acknowledgement failed; item will be reclaimed")
466
+ }
467
+ }
468
+ }
352
469
  if let Err(e) = lease.release().await {
353
470
  warn!(call_id = %call_id, error = %e, "failed to release semaphore slot (will auto-recover after ttl)");
354
471
  }
@@ -357,17 +474,23 @@ async fn run_one_call(
357
474
  /// Coalesced counterpart to [`run_one_call`]: submit the call into the shared
358
475
  /// [`CoalescingQueue`], write its single result blob to the call's stream, then
359
476
  /// deregister + release its lease (always, even on failure).
477
+ #[allow(clippy::too_many_arguments)]
360
478
  async fn run_one_coalesced_call(
361
479
  queue: FunctionQueue,
480
+ input_queue: BoundFunctionQueue,
362
481
  coalescing_queue: Arc<CoalescingQueue>,
363
482
  mut conn: ConnectionManager,
364
- work: QueuedCall,
483
+ delivery: DequeuedCall,
365
484
  lease: SemaphoreLease,
366
- live_tokens: LiveTokens,
485
+ live_calls: LiveCalls,
367
486
  metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
368
487
  ) {
488
+ let DequeuedCall {
489
+ call: work,
490
+ receipt,
491
+ } = delivery;
369
492
  let call_id = work.call_id.clone();
370
- if let Err(e) = coalesced_execute_and_write(
493
+ let completed = match coalesced_execute_and_write(
371
494
  &coalescing_queue,
372
495
  &queue,
373
496
  &mut conn,
@@ -376,17 +499,24 @@ async fn run_one_coalesced_call(
376
499
  )
377
500
  .await
378
501
  {
379
- // The call's result stream may be incomplete; the 24h stream TTL and
380
- // the meta status are the backstop for a stuck poller.
381
- error!(call_id = %call_id, error = %e, "failed to execute/write coalesced queued call");
382
- }
502
+ Ok(()) => true,
503
+ Err(e) => {
504
+ // The call's result stream may be incomplete; the 24h stream TTL and
505
+ // the meta status are the backstop for a stuck poller.
506
+ error!(call_id = %call_id, error = %e, "failed to execute/write coalesced queued call");
507
+ false
508
+ }
509
+ };
383
510
 
384
- // Deregister before releasing so the renewer stops heartbeating this token
385
- // immediately; the subsequent release ZREMs it from the semaphore ZSET.
386
- live_tokens.remove(lease.token());
387
- if let Err(e) = lease.release().await {
388
- warn!(call_id = %call_id, error = %e, "failed to release semaphore slot (will auto-recover after ttl)");
389
- }
511
+ finish_delivery(
512
+ &input_queue,
513
+ &receipt,
514
+ completed,
515
+ &call_id,
516
+ lease,
517
+ &live_calls,
518
+ )
519
+ .await;
390
520
  }
391
521
 
392
522
  /// Submit one call into the shared coalescing queue and relay its single
@@ -666,14 +796,15 @@ async fn flush_new(
666
796
 
667
797
  /// Spawn the single per-pod renewer: every `SEM_RENEW_INTERVAL` it heartbeats
668
798
  /// all currently-held lease tokens in one `renew_many` round-trip. A reclaimed
669
- /// token means this pod stalled past `sem_ttl` without renewing (the slot was
670
- /// then handed to another consumer); the work was already popped so it won't be
671
- /// re-run from the queue, and a GIL-blocked handler can't be aborted, so we only
672
- /// surface it.
799
+ /// token means this pod stalled past `sem_ttl` without renewing. Stream
800
+ /// receipts are renewed in grouped round trips too; ownership loss is surfaced
801
+ /// because a GIL-blocked handler cannot be aborted, while the reclaimed call may
802
+ /// already be running on another pod.
673
803
  fn spawn_renewer(
804
+ queue: BoundFunctionQueue,
674
805
  mut conn: ConnectionManager,
675
806
  sem_key: String,
676
- live_tokens: LiveTokens,
807
+ live_calls: LiveCalls,
677
808
  sem_ttl: Duration,
678
809
  ) {
679
810
  tokio::spawn(async move {
@@ -681,7 +812,11 @@ fn spawn_renewer(
681
812
  ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
682
813
  loop {
683
814
  ticker.tick().await;
684
- let tokens: Vec<String> = live_tokens.iter().map(|r| r.key().clone()).collect();
815
+ let active: Vec<(String, QueueReceipt)> = live_calls
816
+ .iter()
817
+ .map(|entry| (entry.key().clone(), entry.value().clone()))
818
+ .collect();
819
+ let tokens: Vec<String> = active.iter().map(|(token, _)| token.clone()).collect();
685
820
  if tokens.is_empty() {
686
821
  continue;
687
822
  }
@@ -695,6 +830,16 @@ fn spawn_renewer(
695
830
  Ok(_) => {}
696
831
  Err(e) => warn!(error = %e, "batch lease renew failed"),
697
832
  }
833
+ let receipts: Vec<QueueReceipt> =
834
+ active.into_iter().map(|(_, receipt)| receipt).collect();
835
+ match queue.heartbeat_many(&receipts).await {
836
+ Ok(lost) if !lost.is_empty() => warn!(
837
+ count = lost.len(),
838
+ "Stream deliveries reclaimed while calls were still running"
839
+ ),
840
+ Ok(_) => {}
841
+ Err(error) => warn!(%error, "batch Stream delivery renewal failed"),
842
+ }
698
843
  }
699
844
  });
700
845
  }
@@ -736,6 +881,7 @@ mod tests {
736
881
  std::env::remove_var(ENV_POLL_BACKOFF_INITIAL_MS);
737
882
  std::env::remove_var(ENV_POLL_BACKOFF_MAX_SECS);
738
883
  std::env::remove_var(ENV_FLUSH_TIMEOUT_MS);
884
+ std::env::remove_var(ENV_QUEUE_PROTOCOL);
739
885
 
740
886
  let config = SelfConsumerConfig::from_env().expect("config should parse");
741
887
  assert_eq!(config.sem_ttl, Duration::from_secs(DEFAULT_SEM_TTL_SECS));
@@ -751,6 +897,7 @@ mod tests {
751
897
  config.flush_timeout,
752
898
  Duration::from_millis(DEFAULT_FLUSH_TIMEOUT_MS)
753
899
  );
900
+ assert_eq!(config.queue_protocol, QueueProtocol::ListV1);
754
901
  }
755
902
 
756
903
  #[test]
@@ -785,6 +932,20 @@ mod tests {
785
932
  assert_eq!(config.max_concurrent, 1);
786
933
  }
787
934
 
935
+ #[test]
936
+ fn test_stream_queue_protocol_from_env() {
937
+ let _env = lock_env();
938
+
939
+ std::env::set_var(ENV_REDIS_URL, "redis://localhost");
940
+ std::env::set_var(ENV_TENANT, "test_tenant");
941
+ std::env::set_var(ENV_FUNCTION_NAME, "test_fn");
942
+ std::env::set_var(ENV_QUEUE_PROTOCOL, "stream_v1");
943
+
944
+ let config = SelfConsumerConfig::from_env().expect("config should parse");
945
+ assert_eq!(config.queue_protocol, QueueProtocol::StreamV1);
946
+ std::env::remove_var(ENV_QUEUE_PROTOCOL);
947
+ }
948
+
788
949
  #[test]
789
950
  fn test_invalid_timeout_values_use_defaults() {
790
951
  let _env = lock_env();
@@ -894,7 +1055,7 @@ mod tests {
894
1055
 
895
1056
  // Verify notify_handle can be cloned (Arc behavior)
896
1057
  let _notify_handle_clone = notify_handle.clone();
897
- assert_eq!(std::ptr::eq(&notify_handle, &_notify_handle_clone), false);
1058
+ assert!(!std::ptr::eq(&notify_handle, &_notify_handle_clone));
898
1059
  // But they should point to the same underlying Notify
899
1060
  }
900
1061
 
@@ -977,42 +1138,42 @@ mod tests {
977
1138
  // Integration tests for live token tracking and core dispatch logic
978
1139
  #[tokio::test]
979
1140
  async fn test_live_tokens_insert_and_remove() {
980
- let live_tokens: LiveTokens = Arc::new(DashSet::new());
1141
+ let live_calls: LiveCalls = Arc::new(DashMap::new());
981
1142
  let token1 = "lease-token-1";
982
1143
  let token2 = "lease-token-2";
983
1144
 
984
1145
  // Simulate dispatch: register tokens as calls are admitted
985
- live_tokens.insert(token1.to_string());
986
- live_tokens.insert(token2.to_string());
1146
+ live_calls.insert(token1.to_string(), QueueReceipt::ListV1);
1147
+ live_calls.insert(token2.to_string(), QueueReceipt::ListV1);
987
1148
 
988
- assert!(live_tokens.contains(token1));
989
- assert!(live_tokens.contains(token2));
990
- assert_eq!(live_tokens.iter().count(), 2, "should track both tokens");
1149
+ assert!(live_calls.contains_key(token1));
1150
+ assert!(live_calls.contains_key(token2));
1151
+ assert_eq!(live_calls.iter().count(), 2, "should track both tokens");
991
1152
 
992
1153
  // Simulate cleanup: remove as calls complete
993
- live_tokens.remove(token1);
994
- assert!(!live_tokens.contains(token1));
995
- assert!(live_tokens.contains(token2));
996
- assert_eq!(live_tokens.iter().count(), 1);
1154
+ live_calls.remove(token1);
1155
+ assert!(!live_calls.contains_key(token1));
1156
+ assert!(live_calls.contains_key(token2));
1157
+ assert_eq!(live_calls.iter().count(), 1);
997
1158
 
998
1159
  // Final cleanup
999
- live_tokens.remove(token2);
1000
- assert_eq!(live_tokens.iter().count(), 0, "all tokens cleaned up");
1160
+ live_calls.remove(token2);
1161
+ assert_eq!(live_calls.iter().count(), 0, "all tokens cleaned up");
1001
1162
  }
1002
1163
 
1003
1164
  #[tokio::test]
1004
1165
  async fn test_live_tokens_concurrent_access() {
1005
- let live_tokens: LiveTokens = Arc::new(DashSet::new());
1166
+ let live_calls: LiveCalls = Arc::new(DashMap::new());
1006
1167
  let mut handles = vec![];
1007
1168
 
1008
1169
  // Spawn 10 tasks that each insert and remove a token
1009
1170
  for i in 0..10 {
1010
- let tokens = live_tokens.clone();
1171
+ let calls = live_calls.clone();
1011
1172
  let handle = tokio::spawn(async move {
1012
1173
  let token = format!("token-{}", i);
1013
- tokens.insert(token.clone());
1174
+ calls.insert(token.clone(), QueueReceipt::ListV1);
1014
1175
  tokio::time::sleep(Duration::from_millis(1)).await;
1015
- tokens.remove(&token);
1176
+ calls.remove(&token);
1016
1177
  });
1017
1178
  handles.push(handle);
1018
1179
  }
@@ -1024,7 +1185,7 @@ mod tests {
1024
1185
 
1025
1186
  // All tokens should be cleaned up
1026
1187
  assert_eq!(
1027
- live_tokens.iter().count(),
1188
+ live_calls.iter().count(),
1028
1189
  0,
1029
1190
  "concurrent inserts/removes should leave set empty"
1030
1191
  );
@@ -0,0 +1 @@
1
+ __version__ = "1.9.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.8.7
3
+ Version: 1.9.0
4
4
  Summary: Chalk remote call Python runtime interface client
5
5
  Author: Chalk AI, Inc.
6
6
  Project-URL: Homepage, https://chalk.ai
@@ -113,6 +113,7 @@ python -m chalk_remote_call --handler my_handler.handler
113
113
  | Variable | Description |
114
114
  |----------|-------------|
115
115
  | `CHALK_INPUT_ARGS` | Comma-separated list of column names (e.g. `x,y,z`). Renames incoming RecordBatch columns by index. If unset, the original column names are used. Reserved columns (below) are held out of this mapping and keep their own name. |
116
+ | `CHALK_FNQ_QUEUE_PROTOCOL` | Self-consumer work transport: `list_v1` (default/backward compatible) or `stream_v1`. Deployment tooling sets this from immutable scaling-group revision metadata. |
116
117
 
117
118
  #### Reserved columns
118
119
 
@@ -1 +0,0 @@
1
- __version__ = "1.8.7"