chalk-remote-call-python 1.9.0__tar.gz → 1.9.1__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.9.0/chalk_remote_call_python.egg-info → chalk_remote_call_python-1.9.1}/PKG-INFO +1 -1
  2. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/Cargo.lock +1 -1
  3. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml +1 -1
  4. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/self_consumer.rs +219 -0
  5. chalk_remote_call_python-1.9.1/chalk_remote_call/_version.py +1 -0
  6. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/tracing.py +34 -7
  7. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1/chalk_remote_call_python.egg-info}/PKG-INFO +1 -1
  8. chalk_remote_call_python-1.9.0/chalk_remote_call/_version.py +0 -1
  9. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/MANIFEST.in +0 -0
  10. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/README.md +0 -0
  11. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/Cargo.toml +0 -0
  12. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/Cargo.toml +0 -0
  13. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.auth.v1.rs +0 -0
  14. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.common.v1.rs +0 -0
  15. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.rs +0 -0
  16. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.tonic.rs +0 -0
  17. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.utils.v1.rs +0 -0
  18. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/descriptor.bin +0 -0
  19. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/lib.rs +0 -0
  20. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/async_service.rs +0 -0
  21. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/coalesce.rs +0 -0
  22. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/lib.rs +0 -0
  23. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/metrics.rs +0 -0
  24. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/python_bridge.rs +0 -0
  25. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/server.rs +0 -0
  26. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/service.rs +0 -0
  27. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/rust-toolchain.toml +0 -0
  28. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/__init__.py +0 -0
  29. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/__main__.py +0 -0
  30. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/__init__.py +0 -0
  31. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/__init__.py +0 -0
  32. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
  33. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
  34. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +0 -0
  35. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +0 -0
  36. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
  37. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
  38. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +0 -0
  39. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +0 -0
  40. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
  41. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
  42. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +0 -0
  43. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +0 -0
  44. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
  45. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
  46. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +0 -0
  47. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +0 -0
  48. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +0 -0
  49. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +0 -0
  50. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +0 -0
  51. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +0 -0
  52. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_native.pyi +0 -0
  53. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/arrow_utils.py +0 -0
  54. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/cli.py +0 -0
  55. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/handler_loader.py +0 -0
  56. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/input_transform.py +0 -0
  57. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/server.py +0 -0
  58. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call/servicer.py +0 -0
  59. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/SOURCES.txt +0 -0
  60. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/dependency_links.txt +0 -0
  61. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/entry_points.txt +0 -0
  62. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/requires.txt +0 -0
  63. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/top_level.txt +0 -0
  64. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/pyproject.toml +0 -0
  65. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/setup.cfg +0 -0
  66. {chalk_remote_call_python-1.9.0 → chalk_remote_call_python-1.9.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.9.0
3
+ Version: 1.9.1
4
4
  Summary: Chalk remote call Python runtime interface client
5
5
  Author: Chalk AI, Inc.
6
6
  Project-URL: Homepage, https://chalk.ai
@@ -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=783e9ce6b2c#783e9ce6b2c17563ab3302e5d11f8a664ed00232"
514
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=9c61f6a938d8f885f138217ac64149e29d7a691e#9c61f6a938d8f885f138217ac64149e29d7a691e"
515
515
  dependencies = [
516
516
  "base64 0.22.1",
517
517
  "redis",
@@ -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 = "783e9ce6b2c" }
39
+ chalk-queue = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "9c61f6a938d8f885f138217ac64149e29d7a691e" }
40
40
  chalk_metrics = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "783e9ce6b2c" }
41
41
  chalk_statsd = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "783e9ce6b2c" }
@@ -66,6 +66,7 @@ const ENV_POLL_BACKOFF_INITIAL_MS: &str = "CHALK_FNQ_POLL_BACKOFF_INITIAL_MS";
66
66
  const ENV_POLL_BACKOFF_MAX_SECS: &str = "CHALK_FNQ_POLL_BACKOFF_MAX_SECS";
67
67
  const ENV_FLUSH_TIMEOUT_MS: &str = "CHALK_FNQ_FLUSH_TIMEOUT_MS";
68
68
  const ENV_QUEUE_PROTOCOL: &str = "CHALK_FNQ_QUEUE_PROTOCOL";
69
+ const ENV_STREAM_READ_COUNT: &str = "CHALK_FNQ_STREAM_READ_COUNT";
69
70
 
70
71
  /// Lease lifetime (default). Intentionally short relative to how long a call may run: the
71
72
  /// per-pod renewer keeps a live call's lease alive (see module docs), so the
@@ -88,6 +89,14 @@ const DEFAULT_POLL_BACKOFF_MAX_SECS: u64 = 2;
88
89
  /// chunk append races with the wait setup.
89
90
  /// Configurable via `CHALK_FNQ_FLUSH_TIMEOUT_MS`.
90
91
  const DEFAULT_FLUSH_TIMEOUT_MS: u64 = 100;
92
+ /// A bounded prefetch window for Redis Streams. This is deliberately much
93
+ /// smaller than a logical handler batch: several reads can feed the shared
94
+ /// coalescer, while one read remains cheap to reserve, decode and recover.
95
+ const DEFAULT_STREAM_READ_COUNT: usize = 64;
96
+ /// Recovery does not belong on the normal dequeue path. A dead consumer is
97
+ /// still recovered promptly, while healthy traffic avoids an XAUTOCLAIM RPC
98
+ /// for every read window.
99
+ const STREAM_RECLAIM_INTERVAL: Duration = Duration::from_secs(10);
91
100
 
92
101
  /// `peer` recorded in the handler context for queue-consumed calls; there is no
93
102
  /// inbound network peer on this path.
@@ -109,6 +118,7 @@ pub struct SelfConsumerConfig {
109
118
  pub poll_backoff_max: Duration,
110
119
  pub flush_timeout: Duration,
111
120
  pub queue_protocol: QueueProtocol,
121
+ pub stream_read_count: usize,
112
122
  }
113
123
 
114
124
  impl SelfConsumerConfig {
@@ -180,6 +190,12 @@ impl SelfConsumerConfig {
180
190
  })
181
191
  })
182
192
  .unwrap_or_default();
193
+ let stream_read_count = std::env::var(ENV_STREAM_READ_COUNT)
194
+ .ok()
195
+ .and_then(|s| s.parse::<usize>().ok())
196
+ .filter(|&n| n > 0)
197
+ .unwrap_or(DEFAULT_STREAM_READ_COUNT)
198
+ .min(max_concurrent as usize);
183
199
 
184
200
  Some(Self {
185
201
  redis_url,
@@ -191,6 +207,7 @@ impl SelfConsumerConfig {
191
207
  poll_backoff_max,
192
208
  flush_timeout,
193
209
  queue_protocol,
210
+ stream_read_count,
194
211
  })
195
212
  }
196
213
  }
@@ -225,6 +242,10 @@ pub async fn run(
225
242
  Uuid::new_v4()
226
243
  );
227
244
 
245
+ // XGROUP CREATE is a startup operation. Keeping it out of every XREAD and
246
+ // XAUTOCLAIM avoids two avoidable Redis round trips per dispatch window.
247
+ input_queue.initialize_consumer_group().await?;
248
+
228
249
  spawn_renewer(
229
250
  input_queue.clone(),
230
251
  conn.clone(),
@@ -238,10 +259,28 @@ pub async fn run(
238
259
  tenant = %config.tenant,
239
260
  max_concurrent = config.max_concurrent,
240
261
  queue_protocol = config.queue_protocol.as_str(),
262
+ stream_read_count = config.stream_read_count,
241
263
  consumer = %consumer_name,
242
264
  "fnq self-consumer started"
243
265
  );
244
266
 
267
+ if config.queue_protocol == QueueProtocol::StreamV1 {
268
+ return run_stream_dispatch_loop(
269
+ &config,
270
+ queue,
271
+ input_queue,
272
+ legacy_queue,
273
+ sem,
274
+ conn,
275
+ consumer_name,
276
+ live_calls,
277
+ handler,
278
+ metrics_pipeline,
279
+ coalescing_queue,
280
+ )
281
+ .await;
282
+ }
283
+
245
284
  let mut backoff = config.poll_backoff_initial;
246
285
  loop {
247
286
  // Idle-gate: if there is no work, don't take (then immediately release)
@@ -345,6 +384,165 @@ pub async fn run(
345
384
  }
346
385
  }
347
386
 
387
+ #[allow(clippy::too_many_arguments)]
388
+ async fn run_stream_dispatch_loop(
389
+ config: &SelfConsumerConfig,
390
+ queue: FunctionQueue,
391
+ input_queue: BoundFunctionQueue,
392
+ legacy_queue: Option<BoundFunctionQueue>,
393
+ sem: chalk_queue::DistributedSemaphore,
394
+ conn: ConnectionManager,
395
+ consumer_name: String,
396
+ live_calls: LiveCalls,
397
+ handler: Arc<PythonHandler>,
398
+ metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
399
+ coalescing_queue: Option<Arc<CoalescingQueue>>,
400
+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
401
+ let mut backoff = config.poll_backoff_initial;
402
+ // Reclaim once immediately to recover work from a prior crashed pod, then
403
+ // periodically. Healthy messages are kept alive by `spawn_renewer`.
404
+ let mut last_reclaim = Instant::now() - STREAM_RECLAIM_INTERVAL;
405
+
406
+ loop {
407
+ // Capacity must be reserved before XREADGROUP. Reading first would put
408
+ // a potentially huge number of messages into this consumer's PEL while
409
+ // they wait for semaphore slots.
410
+ let mut leases = match sem.try_acquire_many(config.stream_read_count).await {
411
+ Ok(leases) if !leases.is_empty() => leases,
412
+ Ok(_) => {
413
+ tokio::time::sleep(backoff).await;
414
+ backoff = (backoff * 2).min(config.poll_backoff_max);
415
+ continue;
416
+ }
417
+ Err(error) => {
418
+ warn!(%error, "batch semaphore acquire failed; backing off");
419
+ tokio::time::sleep(backoff).await;
420
+ backoff = (backoff * 2).min(config.poll_backoff_max);
421
+ continue;
422
+ }
423
+ };
424
+
425
+ let mut deliveries = Vec::with_capacity(leases.len());
426
+ if last_reclaim.elapsed() >= STREAM_RECLAIM_INTERVAL {
427
+ last_reclaim = Instant::now();
428
+ match input_queue
429
+ .reclaim_stale(&consumer_name, config.sem_ttl, leases.len())
430
+ .await
431
+ {
432
+ Ok(reclaimed) => deliveries.extend(reclaimed),
433
+ Err(error) => {
434
+ warn!(%error, "Stream recovery scan failed; continuing with new work")
435
+ }
436
+ }
437
+ }
438
+
439
+ // During the one-way List → Stream migration, drain old List entries
440
+ // before reading new Stream traffic. This is one bounded operation per
441
+ // read window, not an LLEN on every Stream message.
442
+ if deliveries.len() < leases.len() {
443
+ if let Some(legacy_queue) = &legacy_queue {
444
+ match legacy_queue
445
+ .dequeue_many(&consumer_name, leases.len() - deliveries.len())
446
+ .await
447
+ {
448
+ Ok(legacy) if !legacy.is_empty() => {
449
+ info!(function = %input_queue.function_name(), count = legacy.len(), "draining legacy List items during Stream migration");
450
+ deliveries.extend(legacy);
451
+ }
452
+ Ok(_) => {}
453
+ Err(error) => {
454
+ warn!(%error, "legacy List dequeue failed; continuing with Stream work")
455
+ }
456
+ }
457
+ }
458
+ }
459
+ if deliveries.len() < leases.len() {
460
+ match input_queue
461
+ .dequeue_many(&consumer_name, leases.len() - deliveries.len())
462
+ .await
463
+ {
464
+ Ok(new_deliveries) => deliveries.extend(new_deliveries),
465
+ Err(error) => warn!(%error, "batched Stream dequeue failed"),
466
+ }
467
+ }
468
+
469
+ if deliveries.is_empty() {
470
+ for lease in leases {
471
+ let _ = lease.release().await;
472
+ }
473
+ tokio::time::sleep(backoff).await;
474
+ backoff = (backoff * 2).min(config.poll_backoff_max);
475
+ continue;
476
+ }
477
+
478
+ backoff = config.poll_backoff_initial;
479
+ // `dequeue_many` can return fewer records than requested. Only retain
480
+ // one lease per record and immediately return the unneeded capacity.
481
+ let used = deliveries.len().min(leases.len());
482
+ let unused = leases.split_off(used);
483
+ for lease in unused {
484
+ let _ = lease.release().await;
485
+ }
486
+ for (delivery, lease) in deliveries.into_iter().zip(leases) {
487
+ dispatch_delivery(
488
+ queue.clone(),
489
+ input_queue.clone(),
490
+ coalescing_queue.clone(),
491
+ handler.clone(),
492
+ conn.clone(),
493
+ delivery,
494
+ lease,
495
+ live_calls.clone(),
496
+ config.flush_timeout,
497
+ metrics_pipeline.clone(),
498
+ );
499
+ }
500
+ }
501
+ }
502
+
503
+ #[allow(clippy::too_many_arguments)]
504
+ fn dispatch_delivery(
505
+ queue: FunctionQueue,
506
+ input_queue: BoundFunctionQueue,
507
+ coalescing_queue: Option<Arc<CoalescingQueue>>,
508
+ handler: Arc<PythonHandler>,
509
+ conn: ConnectionManager,
510
+ delivery: DequeuedCall,
511
+ lease: SemaphoreLease,
512
+ live_calls: LiveCalls,
513
+ flush_timeout: Duration,
514
+ metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
515
+ ) {
516
+ live_calls.insert(lease.token().to_string(), delivery.receipt.clone());
517
+ match coalescing_queue {
518
+ Some(cq) => {
519
+ tokio::spawn(run_one_coalesced_call(
520
+ queue,
521
+ input_queue,
522
+ cq,
523
+ conn,
524
+ delivery,
525
+ lease,
526
+ live_calls,
527
+ metrics_pipeline,
528
+ ));
529
+ }
530
+ None => {
531
+ tokio::spawn(run_one_call(
532
+ handler,
533
+ queue,
534
+ input_queue,
535
+ conn,
536
+ delivery,
537
+ lease,
538
+ live_calls,
539
+ flush_timeout,
540
+ metrics_pipeline,
541
+ ));
542
+ }
543
+ }
544
+ }
545
+
348
546
  async fn pending_depth(
349
547
  queue: &BoundFunctionQueue,
350
548
  legacy_queue: Option<&BoundFunctionQueue>,
@@ -882,6 +1080,7 @@ mod tests {
882
1080
  std::env::remove_var(ENV_POLL_BACKOFF_MAX_SECS);
883
1081
  std::env::remove_var(ENV_FLUSH_TIMEOUT_MS);
884
1082
  std::env::remove_var(ENV_QUEUE_PROTOCOL);
1083
+ std::env::remove_var(ENV_STREAM_READ_COUNT);
885
1084
 
886
1085
  let config = SelfConsumerConfig::from_env().expect("config should parse");
887
1086
  assert_eq!(config.sem_ttl, Duration::from_secs(DEFAULT_SEM_TTL_SECS));
@@ -898,6 +1097,7 @@ mod tests {
898
1097
  Duration::from_millis(DEFAULT_FLUSH_TIMEOUT_MS)
899
1098
  );
900
1099
  assert_eq!(config.queue_protocol, QueueProtocol::ListV1);
1100
+ assert_eq!(config.stream_read_count, 1);
901
1101
  }
902
1102
 
903
1103
  #[test]
@@ -940,10 +1140,29 @@ mod tests {
940
1140
  std::env::set_var(ENV_TENANT, "test_tenant");
941
1141
  std::env::set_var(ENV_FUNCTION_NAME, "test_fn");
942
1142
  std::env::set_var(ENV_QUEUE_PROTOCOL, "stream_v1");
1143
+ std::env::set_var(ENV_MAX_CONCURRENT, "128");
1144
+ std::env::remove_var(ENV_STREAM_READ_COUNT);
943
1145
 
944
1146
  let config = SelfConsumerConfig::from_env().expect("config should parse");
945
1147
  assert_eq!(config.queue_protocol, QueueProtocol::StreamV1);
1148
+ assert_eq!(config.stream_read_count, DEFAULT_STREAM_READ_COUNT);
946
1149
  std::env::remove_var(ENV_QUEUE_PROTOCOL);
1150
+ std::env::remove_var(ENV_STREAM_READ_COUNT);
1151
+ }
1152
+
1153
+ #[test]
1154
+ fn test_stream_read_count_is_configurable_and_capped_by_concurrency() {
1155
+ let _env = lock_env();
1156
+
1157
+ std::env::set_var(ENV_REDIS_URL, "redis://localhost");
1158
+ std::env::set_var(ENV_TENANT, "test_tenant");
1159
+ std::env::set_var(ENV_FUNCTION_NAME, "test_fn");
1160
+ std::env::set_var(ENV_MAX_CONCURRENT, "32");
1161
+ std::env::set_var(ENV_STREAM_READ_COUNT, "128");
1162
+
1163
+ let config = SelfConsumerConfig::from_env().expect("config should parse");
1164
+ assert_eq!(config.stream_read_count, 32);
1165
+ std::env::remove_var(ENV_STREAM_READ_COUNT);
947
1166
  }
948
1167
 
949
1168
  #[test]
@@ -0,0 +1 @@
1
+ __version__ = "1.9.1"
@@ -11,6 +11,9 @@ _TRACER_NAME = "chalk_remote_call"
11
11
  _TRACE_CONTEXT_METADATA_KEYS = ("traceparent", "tracestate", "baggage")
12
12
  _REMOTE_FUNCTION_TRACE_POLICY_ENV_VAR = "CHALK_REMOTE_FUNCTION_TRACE_POLICY"
13
13
  _REMOTE_FUNCTION_TRACE_SAMPLE_RATE_ENV_VAR = "CHALK_REMOTE_FUNCTION_TRACE_SAMPLE_RATE"
14
+ _REMOTE_FUNCTION_TRACE_OVERRIDE_KEY = "x-chalk-remote-function-tracing"
15
+ _TRACE_OVERRIDE_ON = "on"
16
+ _TRACE_OVERRIDE_OFF = "off"
14
17
  _TRACE_POLICY_PARENT_BASED_ALWAYS_OFF = "parentbased_always_off"
15
18
  _TRACE_POLICY_PARENT_BASED_TRACE_ID_RATIO = "parentbased_traceidratio"
16
19
  _TRACE_POLICY_ALWAYS_OFF = "always_off"
@@ -74,7 +77,9 @@ def _trace_policy() -> str | None:
74
77
  return None
75
78
 
76
79
 
77
- def _trace_sample_rate() -> float | None:
80
+ def _trace_sample_rate(force_trace: bool) -> float | None:
81
+ if force_trace:
82
+ return 1.0
78
83
  raw_rate = os.environ.get(_REMOTE_FUNCTION_TRACE_SAMPLE_RATE_ENV_VAR)
79
84
  if raw_rate is None:
80
85
  return None
@@ -136,11 +141,11 @@ def _configure_runtime_tracing(sample_rate: float) -> None:
136
141
  _runtime_tracing_configured = True
137
142
 
138
143
 
139
- def _sample_rate_for_inherited_parent(policy: str) -> float | None:
144
+ def _sample_rate_for_inherited_parent(policy: str, force_trace: bool) -> float | None:
140
145
  if policy == _TRACE_POLICY_PARENT_BASED_ALWAYS_OFF:
141
146
  return 0.0
142
147
  if policy == _TRACE_POLICY_PARENT_BASED_TRACE_ID_RATIO:
143
- return _trace_sample_rate()
148
+ return _trace_sample_rate(force_trace)
144
149
  return None
145
150
 
146
151
 
@@ -148,6 +153,7 @@ def _span_metadata(
148
153
  first_metadata: Mapping[str, Any] | None,
149
154
  traceparent_metadata: Mapping[str, Any] | None,
150
155
  policy: str,
156
+ force_trace: bool,
151
157
  ) -> tuple[Mapping[str, Any] | None, Any | None, Any]:
152
158
  if traceparent_metadata is not None:
153
159
  carrier = _trace_carrier(traceparent_metadata)
@@ -164,7 +170,7 @@ def _span_metadata(
164
170
  if not parent_span_context.is_valid:
165
171
  return None, None, None
166
172
 
167
- sample_rate = _sample_rate_for_inherited_parent(policy)
173
+ sample_rate = _sample_rate_for_inherited_parent(policy, force_trace)
168
174
  if sample_rate is None:
169
175
  return None, None, None
170
176
  _configure_runtime_tracing(sample_rate)
@@ -173,7 +179,7 @@ def _span_metadata(
173
179
  if policy != _TRACE_POLICY_PARENT_BASED_TRACE_ID_RATIO:
174
180
  return None, None, None
175
181
 
176
- sample_rate = _trace_sample_rate()
182
+ sample_rate = _trace_sample_rate(force_trace)
177
183
  if sample_rate is None:
178
184
  return None, None, None
179
185
 
@@ -184,6 +190,17 @@ def _span_metadata(
184
190
  return first_metadata, None, otel_modules
185
191
 
186
192
 
193
+ def _force_sampled_traceparent(metadata: Mapping[str, Any]) -> Mapping[str, Any]:
194
+ parts = str(metadata.get("traceparent") or "").split("-")
195
+ if len(parts) != 4 or len(parts[3]) != 2:
196
+ return metadata
197
+ try:
198
+ parts[3] = f"{int(parts[3], 16) | 1:02x}"
199
+ except ValueError:
200
+ return metadata
201
+ return {**metadata, "traceparent": "-".join(parts)}
202
+
203
+
187
204
  def _always_off_span_metadata(
188
205
  first_metadata: Mapping[str, Any] | None,
189
206
  traceparent_metadata: Mapping[str, Any] | None,
@@ -275,12 +292,21 @@ def remote_function_invocation_span(
275
292
  context_metadata: Any,
276
293
  coalesced_count: int | None = None,
277
294
  ) -> Iterator[None]:
278
- policy = _trace_policy()
295
+ first_metadata, traceparent_metadata = _collect_trace_metadata(context_metadata)
296
+ override = str((first_metadata or {}).get(_REMOTE_FUNCTION_TRACE_OVERRIDE_KEY) or "").strip().lower()
297
+ if override == _TRACE_OVERRIDE_ON:
298
+ policy = _TRACE_POLICY_PARENT_BASED_TRACE_ID_RATIO
299
+ elif override == _TRACE_OVERRIDE_OFF:
300
+ policy = _TRACE_POLICY_ALWAYS_OFF
301
+ else:
302
+ policy = _trace_policy()
279
303
  if policy is None:
280
304
  yield
281
305
  return
282
306
 
283
- first_metadata, traceparent_metadata = _collect_trace_metadata(context_metadata)
307
+ force_trace = override == _TRACE_OVERRIDE_ON
308
+ if force_trace and traceparent_metadata is not None:
309
+ traceparent_metadata = _force_sampled_traceparent(traceparent_metadata)
284
310
  if policy == _TRACE_POLICY_PARENT_BASED_ALWAYS_OFF and traceparent_metadata is None:
285
311
  yield
286
312
  return
@@ -295,6 +321,7 @@ def remote_function_invocation_span(
295
321
  first_metadata,
296
322
  traceparent_metadata,
297
323
  policy,
324
+ force_trace,
298
325
  )
299
326
  tracer = _get_tracer(otel_modules[2]) if otel_modules is not None else None
300
327
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.9.0
3
+ Version: 1.9.1
4
4
  Summary: Chalk remote call Python runtime interface client
5
5
  Author: Chalk AI, Inc.
6
6
  Project-URL: Homepage, https://chalk.ai
@@ -1 +0,0 @@
1
- __version__ = "1.9.0"