chalk-remote-call-python 1.8.7__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.
- {chalk_remote_call_python-1.8.7/chalk_remote_call_python.egg-info → chalk_remote_call_python-1.9.1}/PKG-INFO +2 -1
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/README.md +1 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/Cargo.lock +7 -7
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml +3 -3
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/self_consumer.rs +441 -61
- chalk_remote_call_python-1.9.1/chalk_remote_call/_version.py +1 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/tracing.py +34 -7
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1/chalk_remote_call_python.egg-info}/PKG-INFO +2 -1
- chalk_remote_call_python-1.8.7/chalk_remote_call/_version.py +0 -1
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/MANIFEST.in +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/Cargo.toml +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/Cargo.toml +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.auth.v1.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.common.v1.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.tonic.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.utils.v1.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/descriptor.bin +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/lib.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/async_service.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/coalesce.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/lib.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/metrics.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/python_bridge.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/server.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/chalk-remote-call-server/src/service.rs +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/rust-toolchain.toml +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/__main__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_native.pyi +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/arrow_utils.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/cli.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/handler_loader.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/input_transform.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/server.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/servicer.py +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/SOURCES.txt +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/dependency_links.txt +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/entry_points.txt +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/requires.txt +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call_python.egg-info/top_level.txt +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/pyproject.toml +0 -0
- {chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/setup.cfg +0 -0
- {chalk_remote_call_python-1.8.7 → 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.
|
|
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
|
|
@@ -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
|
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/Cargo.lock
RENAMED
|
@@ -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=
|
|
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=
|
|
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=
|
|
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",
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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 = "
|
|
40
|
-
chalk_metrics = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "
|
|
41
|
-
chalk_statsd = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "
|
|
39
|
+
chalk-queue = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "9c61f6a938d8f885f138217ac64149e29d7a691e" }
|
|
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
|
-
//!
|
|
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::{
|
|
40
|
-
|
|
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,8 @@ 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";
|
|
69
|
+
const ENV_STREAM_READ_COUNT: &str = "CHALK_FNQ_STREAM_READ_COUNT";
|
|
62
70
|
|
|
63
71
|
/// Lease lifetime (default). Intentionally short relative to how long a call may run: the
|
|
64
72
|
/// per-pod renewer keeps a live call's lease alive (see module docs), so the
|
|
@@ -81,6 +89,14 @@ const DEFAULT_POLL_BACKOFF_MAX_SECS: u64 = 2;
|
|
|
81
89
|
/// chunk append races with the wait setup.
|
|
82
90
|
/// Configurable via `CHALK_FNQ_FLUSH_TIMEOUT_MS`.
|
|
83
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);
|
|
84
100
|
|
|
85
101
|
/// `peer` recorded in the handler context for queue-consumed calls; there is no
|
|
86
102
|
/// inbound network peer on this path.
|
|
@@ -101,6 +117,8 @@ pub struct SelfConsumerConfig {
|
|
|
101
117
|
pub poll_backoff_initial: Duration,
|
|
102
118
|
pub poll_backoff_max: Duration,
|
|
103
119
|
pub flush_timeout: Duration,
|
|
120
|
+
pub queue_protocol: QueueProtocol,
|
|
121
|
+
pub stream_read_count: usize,
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
impl SelfConsumerConfig {
|
|
@@ -162,6 +180,22 @@ impl SelfConsumerConfig {
|
|
|
162
180
|
.filter(|&n| n > 0)
|
|
163
181
|
.unwrap_or(DEFAULT_FLUSH_TIMEOUT_MS);
|
|
164
182
|
let flush_timeout = Duration::from_millis(flush_timeout_ms);
|
|
183
|
+
let queue_protocol = std::env::var(ENV_QUEUE_PROTOCOL)
|
|
184
|
+
.ok()
|
|
185
|
+
.filter(|value| !value.is_empty())
|
|
186
|
+
.map(|value| {
|
|
187
|
+
QueueProtocol::from_str(&value).unwrap_or_else(|error| {
|
|
188
|
+
warn!(%error, "invalid queue protocol; using legacy List transport");
|
|
189
|
+
QueueProtocol::ListV1
|
|
190
|
+
})
|
|
191
|
+
})
|
|
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);
|
|
165
199
|
|
|
166
200
|
Some(Self {
|
|
167
201
|
redis_url,
|
|
@@ -172,6 +206,8 @@ impl SelfConsumerConfig {
|
|
|
172
206
|
poll_backoff_initial,
|
|
173
207
|
poll_backoff_max,
|
|
174
208
|
flush_timeout,
|
|
209
|
+
queue_protocol,
|
|
210
|
+
stream_read_count,
|
|
175
211
|
})
|
|
176
212
|
}
|
|
177
213
|
}
|
|
@@ -179,7 +215,7 @@ impl SelfConsumerConfig {
|
|
|
179
215
|
/// Set of lease tokens this pod currently holds. The dispatch loop inserts a
|
|
180
216
|
/// token when it admits a call; the worker removes it on completion. The
|
|
181
217
|
/// renewer heartbeats whatever is in the set on each tick.
|
|
182
|
-
type
|
|
218
|
+
type LiveCalls = Arc<DashMap<String, QueueReceipt>>;
|
|
183
219
|
|
|
184
220
|
/// Connect to Redis and run the consume loop until the process exits. Returns
|
|
185
221
|
/// `Err` only on a fatal setup failure (bad URL / unreachable Redis at startup);
|
|
@@ -194,25 +230,64 @@ pub async fn run(
|
|
|
194
230
|
let client = Client::open(config.redis_url.clone())?;
|
|
195
231
|
let conn = ConnectionManager::new(client).await?;
|
|
196
232
|
let queue = FunctionQueue::new(conn.clone(), config.tenant.clone());
|
|
233
|
+
let input_queue = queue.bind(&config.function_name, config.queue_protocol);
|
|
234
|
+
let legacy_queue = (config.queue_protocol == QueueProtocol::StreamV1)
|
|
235
|
+
.then(|| queue.bind(&config.function_name, QueueProtocol::ListV1));
|
|
197
236
|
let sem = queue.semaphore(&config.function_name, config.max_concurrent, config.sem_ttl);
|
|
198
237
|
let sem_key = queue.sem_key_for(&config.function_name);
|
|
199
|
-
let
|
|
238
|
+
let live_calls: LiveCalls = Arc::new(DashMap::new());
|
|
239
|
+
let consumer_name = format!(
|
|
240
|
+
"{}:{}",
|
|
241
|
+
std::env::var("HOSTNAME").unwrap_or_else(|_| "fnq-shim".to_string()),
|
|
242
|
+
Uuid::new_v4()
|
|
243
|
+
);
|
|
200
244
|
|
|
201
|
-
|
|
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
|
+
|
|
249
|
+
spawn_renewer(
|
|
250
|
+
input_queue.clone(),
|
|
251
|
+
conn.clone(),
|
|
252
|
+
sem_key,
|
|
253
|
+
live_calls.clone(),
|
|
254
|
+
config.sem_ttl,
|
|
255
|
+
);
|
|
202
256
|
|
|
203
257
|
info!(
|
|
204
258
|
function = %config.function_name,
|
|
205
259
|
tenant = %config.tenant,
|
|
206
260
|
max_concurrent = config.max_concurrent,
|
|
261
|
+
queue_protocol = config.queue_protocol.as_str(),
|
|
262
|
+
stream_read_count = config.stream_read_count,
|
|
263
|
+
consumer = %consumer_name,
|
|
207
264
|
"fnq self-consumer started"
|
|
208
265
|
);
|
|
209
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
|
+
|
|
210
284
|
let mut backoff = config.poll_backoff_initial;
|
|
211
285
|
loop {
|
|
212
286
|
// Idle-gate: if there is no work, don't take (then immediately release)
|
|
213
287
|
// a semaphore slot — that churn would race genuine acquirers on other
|
|
214
|
-
// pods.
|
|
215
|
-
|
|
288
|
+
// pods. During a Stream rollout, also observe the legacy List so a new
|
|
289
|
+
// shim can drain work queued before its revision metadata changed.
|
|
290
|
+
match pending_depth(&input_queue, legacy_queue.as_ref()).await {
|
|
216
291
|
Ok(0) => {
|
|
217
292
|
tokio::time::sleep(backoff).await;
|
|
218
293
|
backoff = (backoff * 2).min(config.poll_backoff_max);
|
|
@@ -243,8 +318,15 @@ pub async fn run(
|
|
|
243
318
|
}
|
|
244
319
|
};
|
|
245
320
|
|
|
246
|
-
let
|
|
247
|
-
|
|
321
|
+
let delivery = match dequeue_next(
|
|
322
|
+
&input_queue,
|
|
323
|
+
legacy_queue.as_ref(),
|
|
324
|
+
&consumer_name,
|
|
325
|
+
config.sem_ttl,
|
|
326
|
+
)
|
|
327
|
+
.await
|
|
328
|
+
{
|
|
329
|
+
Ok(Some(delivery)) => delivery,
|
|
248
330
|
// Raced another consumer (or our own idle-gate) to an empty queue:
|
|
249
331
|
// give the slot back and retry.
|
|
250
332
|
Ok(None) => {
|
|
@@ -253,7 +335,7 @@ pub async fn run(
|
|
|
253
335
|
continue;
|
|
254
336
|
}
|
|
255
337
|
Err(e) => {
|
|
256
|
-
warn!(error = %e, "
|
|
338
|
+
warn!(error = %e, "dequeue failed; releasing slot");
|
|
257
339
|
let _ = lease.release().await;
|
|
258
340
|
backoff = (backoff * 2).min(config.poll_backoff_max);
|
|
259
341
|
continue;
|
|
@@ -264,7 +346,7 @@ pub async fn run(
|
|
|
264
346
|
|
|
265
347
|
// Register the token so the pod-level renewer heartbeats this lease
|
|
266
348
|
// while the call runs.
|
|
267
|
-
|
|
349
|
+
live_calls.insert(lease.token().to_string(), delivery.receipt.clone());
|
|
268
350
|
|
|
269
351
|
match &coalescing_queue {
|
|
270
352
|
// Batching on: route through the shared coalescing queue so this
|
|
@@ -273,11 +355,12 @@ pub async fn run(
|
|
|
273
355
|
Some(cq) => {
|
|
274
356
|
tokio::spawn(run_one_coalesced_call(
|
|
275
357
|
queue.clone(),
|
|
358
|
+
input_queue.clone(),
|
|
276
359
|
cq.clone(),
|
|
277
360
|
conn.clone(),
|
|
278
|
-
|
|
361
|
+
delivery,
|
|
279
362
|
lease,
|
|
280
|
-
|
|
363
|
+
live_calls.clone(),
|
|
281
364
|
metrics_pipeline.clone(),
|
|
282
365
|
));
|
|
283
366
|
}
|
|
@@ -286,10 +369,11 @@ pub async fn run(
|
|
|
286
369
|
tokio::spawn(run_one_call(
|
|
287
370
|
handler.clone(),
|
|
288
371
|
queue.clone(),
|
|
372
|
+
input_queue.clone(),
|
|
289
373
|
conn.clone(),
|
|
290
|
-
|
|
374
|
+
delivery,
|
|
291
375
|
lease,
|
|
292
|
-
|
|
376
|
+
live_calls.clone(),
|
|
293
377
|
config.flush_timeout,
|
|
294
378
|
metrics_pipeline.clone(),
|
|
295
379
|
));
|
|
@@ -300,6 +384,202 @@ pub async fn run(
|
|
|
300
384
|
}
|
|
301
385
|
}
|
|
302
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
|
+
|
|
546
|
+
async fn pending_depth(
|
|
547
|
+
queue: &BoundFunctionQueue,
|
|
548
|
+
legacy_queue: Option<&BoundFunctionQueue>,
|
|
549
|
+
) -> Result<u64, redis::RedisError> {
|
|
550
|
+
let selected = queue.current_depth().await?;
|
|
551
|
+
let legacy = match legacy_queue {
|
|
552
|
+
Some(queue) => queue.current_depth().await?,
|
|
553
|
+
None => 0,
|
|
554
|
+
};
|
|
555
|
+
Ok(selected + legacy)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async fn dequeue_next(
|
|
559
|
+
queue: &BoundFunctionQueue,
|
|
560
|
+
legacy_queue: Option<&BoundFunctionQueue>,
|
|
561
|
+
consumer_name: &str,
|
|
562
|
+
reclaim_idle: Duration,
|
|
563
|
+
) -> Result<Option<DequeuedCall>, Box<dyn std::error::Error + Send + Sync>> {
|
|
564
|
+
// Recovered work wins first. The legacy List is then drained before new
|
|
565
|
+
// Stream traffic; producers never route new work to both transports.
|
|
566
|
+
if let Some(reclaimed) = queue
|
|
567
|
+
.reclaim_stale(consumer_name, reclaim_idle, 1)
|
|
568
|
+
.await?
|
|
569
|
+
.into_iter()
|
|
570
|
+
.next()
|
|
571
|
+
{
|
|
572
|
+
return Ok(Some(reclaimed));
|
|
573
|
+
}
|
|
574
|
+
if let Some(legacy_queue) = legacy_queue {
|
|
575
|
+
if let Some(legacy) = legacy_queue.dequeue(consumer_name).await? {
|
|
576
|
+
info!(function = %queue.function_name(), "draining legacy List item during Stream migration");
|
|
577
|
+
return Ok(Some(legacy));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
queue.dequeue(consumer_name).await
|
|
581
|
+
}
|
|
582
|
+
|
|
303
583
|
/// Run a single queued call to completion, write its results to the call's
|
|
304
584
|
/// stream, then deregister + release its lease. Always releases the lease,
|
|
305
585
|
/// even if execution or stream writes fail or panic.
|
|
@@ -307,15 +587,20 @@ pub async fn run(
|
|
|
307
587
|
async fn run_one_call(
|
|
308
588
|
handler: Arc<PythonHandler>,
|
|
309
589
|
queue: FunctionQueue,
|
|
590
|
+
input_queue: BoundFunctionQueue,
|
|
310
591
|
mut conn: ConnectionManager,
|
|
311
|
-
|
|
592
|
+
delivery: DequeuedCall,
|
|
312
593
|
lease: SemaphoreLease,
|
|
313
|
-
|
|
594
|
+
live_calls: LiveCalls,
|
|
314
595
|
flush_timeout: Duration,
|
|
315
596
|
metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
|
|
316
597
|
) {
|
|
598
|
+
let DequeuedCall {
|
|
599
|
+
call: work,
|
|
600
|
+
receipt,
|
|
601
|
+
} = delivery;
|
|
317
602
|
let call_id = work.call_id.clone();
|
|
318
|
-
match AssertUnwindSafe(execute_and_write(
|
|
603
|
+
let completed = match AssertUnwindSafe(execute_and_write(
|
|
319
604
|
&handler,
|
|
320
605
|
&queue,
|
|
321
606
|
&mut conn,
|
|
@@ -326,11 +611,12 @@ async fn run_one_call(
|
|
|
326
611
|
.catch_unwind()
|
|
327
612
|
.await
|
|
328
613
|
{
|
|
329
|
-
Ok(Ok(())) =>
|
|
614
|
+
Ok(Ok(())) => true,
|
|
330
615
|
Ok(Err(e)) => {
|
|
331
616
|
// The call's result stream may be incomplete; the call's 24h stream TTL
|
|
332
617
|
// and the meta status are the backstop for a stuck poller.
|
|
333
618
|
error!(call_id = %call_id, error = %e, "failed to execute/write queued call");
|
|
619
|
+
false
|
|
334
620
|
}
|
|
335
621
|
Err(panic_payload) => {
|
|
336
622
|
let panic_message = panic_payload
|
|
@@ -343,12 +629,41 @@ async fn run_one_call(
|
|
|
343
629
|
panic = %panic_message,
|
|
344
630
|
"panic while executing/writing queued call"
|
|
345
631
|
);
|
|
632
|
+
false
|
|
346
633
|
}
|
|
347
|
-
}
|
|
634
|
+
};
|
|
348
635
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
636
|
+
finish_delivery(
|
|
637
|
+
&input_queue,
|
|
638
|
+
&receipt,
|
|
639
|
+
completed,
|
|
640
|
+
&call_id,
|
|
641
|
+
lease,
|
|
642
|
+
&live_calls,
|
|
643
|
+
)
|
|
644
|
+
.await;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async fn finish_delivery(
|
|
648
|
+
queue: &BoundFunctionQueue,
|
|
649
|
+
receipt: &QueueReceipt,
|
|
650
|
+
completed: bool,
|
|
651
|
+
call_id: &str,
|
|
652
|
+
lease: SemaphoreLease,
|
|
653
|
+
live_calls: &LiveCalls,
|
|
654
|
+
) {
|
|
655
|
+
// Stop renewing before ack. If completion or ack failed, the Stream item
|
|
656
|
+
// becomes reclaimable after the idle timeout; List behavior is unchanged.
|
|
657
|
+
live_calls.remove(lease.token());
|
|
658
|
+
if completed {
|
|
659
|
+
match queue.acknowledge(receipt).await {
|
|
660
|
+
Ok(true) => {}
|
|
661
|
+
Ok(false) => warn!(%call_id, "delivery ownership was lost before acknowledgement"),
|
|
662
|
+
Err(error) => {
|
|
663
|
+
warn!(%call_id, %error, "delivery acknowledgement failed; item will be reclaimed")
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
352
667
|
if let Err(e) = lease.release().await {
|
|
353
668
|
warn!(call_id = %call_id, error = %e, "failed to release semaphore slot (will auto-recover after ttl)");
|
|
354
669
|
}
|
|
@@ -357,17 +672,23 @@ async fn run_one_call(
|
|
|
357
672
|
/// Coalesced counterpart to [`run_one_call`]: submit the call into the shared
|
|
358
673
|
/// [`CoalescingQueue`], write its single result blob to the call's stream, then
|
|
359
674
|
/// deregister + release its lease (always, even on failure).
|
|
675
|
+
#[allow(clippy::too_many_arguments)]
|
|
360
676
|
async fn run_one_coalesced_call(
|
|
361
677
|
queue: FunctionQueue,
|
|
678
|
+
input_queue: BoundFunctionQueue,
|
|
362
679
|
coalescing_queue: Arc<CoalescingQueue>,
|
|
363
680
|
mut conn: ConnectionManager,
|
|
364
|
-
|
|
681
|
+
delivery: DequeuedCall,
|
|
365
682
|
lease: SemaphoreLease,
|
|
366
|
-
|
|
683
|
+
live_calls: LiveCalls,
|
|
367
684
|
metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
|
|
368
685
|
) {
|
|
686
|
+
let DequeuedCall {
|
|
687
|
+
call: work,
|
|
688
|
+
receipt,
|
|
689
|
+
} = delivery;
|
|
369
690
|
let call_id = work.call_id.clone();
|
|
370
|
-
|
|
691
|
+
let completed = match coalesced_execute_and_write(
|
|
371
692
|
&coalescing_queue,
|
|
372
693
|
&queue,
|
|
373
694
|
&mut conn,
|
|
@@ -376,17 +697,24 @@ async fn run_one_coalesced_call(
|
|
|
376
697
|
)
|
|
377
698
|
.await
|
|
378
699
|
{
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
700
|
+
Ok(()) => true,
|
|
701
|
+
Err(e) => {
|
|
702
|
+
// The call's result stream may be incomplete; the 24h stream TTL and
|
|
703
|
+
// the meta status are the backstop for a stuck poller.
|
|
704
|
+
error!(call_id = %call_id, error = %e, "failed to execute/write coalesced queued call");
|
|
705
|
+
false
|
|
706
|
+
}
|
|
707
|
+
};
|
|
383
708
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
709
|
+
finish_delivery(
|
|
710
|
+
&input_queue,
|
|
711
|
+
&receipt,
|
|
712
|
+
completed,
|
|
713
|
+
&call_id,
|
|
714
|
+
lease,
|
|
715
|
+
&live_calls,
|
|
716
|
+
)
|
|
717
|
+
.await;
|
|
390
718
|
}
|
|
391
719
|
|
|
392
720
|
/// Submit one call into the shared coalescing queue and relay its single
|
|
@@ -666,14 +994,15 @@ async fn flush_new(
|
|
|
666
994
|
|
|
667
995
|
/// Spawn the single per-pod renewer: every `SEM_RENEW_INTERVAL` it heartbeats
|
|
668
996
|
/// all currently-held lease tokens in one `renew_many` round-trip. A reclaimed
|
|
669
|
-
/// token means this pod stalled past `sem_ttl` without renewing
|
|
670
|
-
///
|
|
671
|
-
///
|
|
672
|
-
///
|
|
997
|
+
/// token means this pod stalled past `sem_ttl` without renewing. Stream
|
|
998
|
+
/// receipts are renewed in grouped round trips too; ownership loss is surfaced
|
|
999
|
+
/// because a GIL-blocked handler cannot be aborted, while the reclaimed call may
|
|
1000
|
+
/// already be running on another pod.
|
|
673
1001
|
fn spawn_renewer(
|
|
1002
|
+
queue: BoundFunctionQueue,
|
|
674
1003
|
mut conn: ConnectionManager,
|
|
675
1004
|
sem_key: String,
|
|
676
|
-
|
|
1005
|
+
live_calls: LiveCalls,
|
|
677
1006
|
sem_ttl: Duration,
|
|
678
1007
|
) {
|
|
679
1008
|
tokio::spawn(async move {
|
|
@@ -681,7 +1010,11 @@ fn spawn_renewer(
|
|
|
681
1010
|
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
|
682
1011
|
loop {
|
|
683
1012
|
ticker.tick().await;
|
|
684
|
-
let
|
|
1013
|
+
let active: Vec<(String, QueueReceipt)> = live_calls
|
|
1014
|
+
.iter()
|
|
1015
|
+
.map(|entry| (entry.key().clone(), entry.value().clone()))
|
|
1016
|
+
.collect();
|
|
1017
|
+
let tokens: Vec<String> = active.iter().map(|(token, _)| token.clone()).collect();
|
|
685
1018
|
if tokens.is_empty() {
|
|
686
1019
|
continue;
|
|
687
1020
|
}
|
|
@@ -695,6 +1028,16 @@ fn spawn_renewer(
|
|
|
695
1028
|
Ok(_) => {}
|
|
696
1029
|
Err(e) => warn!(error = %e, "batch lease renew failed"),
|
|
697
1030
|
}
|
|
1031
|
+
let receipts: Vec<QueueReceipt> =
|
|
1032
|
+
active.into_iter().map(|(_, receipt)| receipt).collect();
|
|
1033
|
+
match queue.heartbeat_many(&receipts).await {
|
|
1034
|
+
Ok(lost) if !lost.is_empty() => warn!(
|
|
1035
|
+
count = lost.len(),
|
|
1036
|
+
"Stream deliveries reclaimed while calls were still running"
|
|
1037
|
+
),
|
|
1038
|
+
Ok(_) => {}
|
|
1039
|
+
Err(error) => warn!(%error, "batch Stream delivery renewal failed"),
|
|
1040
|
+
}
|
|
698
1041
|
}
|
|
699
1042
|
});
|
|
700
1043
|
}
|
|
@@ -736,6 +1079,8 @@ mod tests {
|
|
|
736
1079
|
std::env::remove_var(ENV_POLL_BACKOFF_INITIAL_MS);
|
|
737
1080
|
std::env::remove_var(ENV_POLL_BACKOFF_MAX_SECS);
|
|
738
1081
|
std::env::remove_var(ENV_FLUSH_TIMEOUT_MS);
|
|
1082
|
+
std::env::remove_var(ENV_QUEUE_PROTOCOL);
|
|
1083
|
+
std::env::remove_var(ENV_STREAM_READ_COUNT);
|
|
739
1084
|
|
|
740
1085
|
let config = SelfConsumerConfig::from_env().expect("config should parse");
|
|
741
1086
|
assert_eq!(config.sem_ttl, Duration::from_secs(DEFAULT_SEM_TTL_SECS));
|
|
@@ -751,6 +1096,8 @@ mod tests {
|
|
|
751
1096
|
config.flush_timeout,
|
|
752
1097
|
Duration::from_millis(DEFAULT_FLUSH_TIMEOUT_MS)
|
|
753
1098
|
);
|
|
1099
|
+
assert_eq!(config.queue_protocol, QueueProtocol::ListV1);
|
|
1100
|
+
assert_eq!(config.stream_read_count, 1);
|
|
754
1101
|
}
|
|
755
1102
|
|
|
756
1103
|
#[test]
|
|
@@ -785,6 +1132,39 @@ mod tests {
|
|
|
785
1132
|
assert_eq!(config.max_concurrent, 1);
|
|
786
1133
|
}
|
|
787
1134
|
|
|
1135
|
+
#[test]
|
|
1136
|
+
fn test_stream_queue_protocol_from_env() {
|
|
1137
|
+
let _env = lock_env();
|
|
1138
|
+
|
|
1139
|
+
std::env::set_var(ENV_REDIS_URL, "redis://localhost");
|
|
1140
|
+
std::env::set_var(ENV_TENANT, "test_tenant");
|
|
1141
|
+
std::env::set_var(ENV_FUNCTION_NAME, "test_fn");
|
|
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);
|
|
1145
|
+
|
|
1146
|
+
let config = SelfConsumerConfig::from_env().expect("config should parse");
|
|
1147
|
+
assert_eq!(config.queue_protocol, QueueProtocol::StreamV1);
|
|
1148
|
+
assert_eq!(config.stream_read_count, DEFAULT_STREAM_READ_COUNT);
|
|
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);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
788
1168
|
#[test]
|
|
789
1169
|
fn test_invalid_timeout_values_use_defaults() {
|
|
790
1170
|
let _env = lock_env();
|
|
@@ -894,7 +1274,7 @@ mod tests {
|
|
|
894
1274
|
|
|
895
1275
|
// Verify notify_handle can be cloned (Arc behavior)
|
|
896
1276
|
let _notify_handle_clone = notify_handle.clone();
|
|
897
|
-
|
|
1277
|
+
assert!(!std::ptr::eq(¬ify_handle, &_notify_handle_clone));
|
|
898
1278
|
// But they should point to the same underlying Notify
|
|
899
1279
|
}
|
|
900
1280
|
|
|
@@ -977,42 +1357,42 @@ mod tests {
|
|
|
977
1357
|
// Integration tests for live token tracking and core dispatch logic
|
|
978
1358
|
#[tokio::test]
|
|
979
1359
|
async fn test_live_tokens_insert_and_remove() {
|
|
980
|
-
let
|
|
1360
|
+
let live_calls: LiveCalls = Arc::new(DashMap::new());
|
|
981
1361
|
let token1 = "lease-token-1";
|
|
982
1362
|
let token2 = "lease-token-2";
|
|
983
1363
|
|
|
984
1364
|
// Simulate dispatch: register tokens as calls are admitted
|
|
985
|
-
|
|
986
|
-
|
|
1365
|
+
live_calls.insert(token1.to_string(), QueueReceipt::ListV1);
|
|
1366
|
+
live_calls.insert(token2.to_string(), QueueReceipt::ListV1);
|
|
987
1367
|
|
|
988
|
-
assert!(
|
|
989
|
-
assert!(
|
|
990
|
-
assert_eq!(
|
|
1368
|
+
assert!(live_calls.contains_key(token1));
|
|
1369
|
+
assert!(live_calls.contains_key(token2));
|
|
1370
|
+
assert_eq!(live_calls.iter().count(), 2, "should track both tokens");
|
|
991
1371
|
|
|
992
1372
|
// Simulate cleanup: remove as calls complete
|
|
993
|
-
|
|
994
|
-
assert!(!
|
|
995
|
-
assert!(
|
|
996
|
-
assert_eq!(
|
|
1373
|
+
live_calls.remove(token1);
|
|
1374
|
+
assert!(!live_calls.contains_key(token1));
|
|
1375
|
+
assert!(live_calls.contains_key(token2));
|
|
1376
|
+
assert_eq!(live_calls.iter().count(), 1);
|
|
997
1377
|
|
|
998
1378
|
// Final cleanup
|
|
999
|
-
|
|
1000
|
-
assert_eq!(
|
|
1379
|
+
live_calls.remove(token2);
|
|
1380
|
+
assert_eq!(live_calls.iter().count(), 0, "all tokens cleaned up");
|
|
1001
1381
|
}
|
|
1002
1382
|
|
|
1003
1383
|
#[tokio::test]
|
|
1004
1384
|
async fn test_live_tokens_concurrent_access() {
|
|
1005
|
-
let
|
|
1385
|
+
let live_calls: LiveCalls = Arc::new(DashMap::new());
|
|
1006
1386
|
let mut handles = vec![];
|
|
1007
1387
|
|
|
1008
1388
|
// Spawn 10 tasks that each insert and remove a token
|
|
1009
1389
|
for i in 0..10 {
|
|
1010
|
-
let
|
|
1390
|
+
let calls = live_calls.clone();
|
|
1011
1391
|
let handle = tokio::spawn(async move {
|
|
1012
1392
|
let token = format!("token-{}", i);
|
|
1013
|
-
|
|
1393
|
+
calls.insert(token.clone(), QueueReceipt::ListV1);
|
|
1014
1394
|
tokio::time::sleep(Duration::from_millis(1)).await;
|
|
1015
|
-
|
|
1395
|
+
calls.remove(&token);
|
|
1016
1396
|
});
|
|
1017
1397
|
handles.push(handle);
|
|
1018
1398
|
}
|
|
@@ -1024,7 +1404,7 @@ mod tests {
|
|
|
1024
1404
|
|
|
1025
1405
|
// All tokens should be cleaned up
|
|
1026
1406
|
assert_eq!(
|
|
1027
|
-
|
|
1407
|
+
live_calls.iter().count(),
|
|
1028
1408
|
0,
|
|
1029
1409
|
"concurrent inserts/removes should leave set empty"
|
|
1030
1410
|
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.9.1"
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/tracing.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
@@ -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"
|
|
File without changes
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk-remote-call-rs/Cargo.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/__init__.py
RENAMED
|
File without changes
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/__main__.py
RENAMED
|
File without changes
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_gen/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/_native.pyi
RENAMED
|
File without changes
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/arrow_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/server.py
RENAMED
|
File without changes
|
{chalk_remote_call_python-1.8.7 → chalk_remote_call_python-1.9.1}/chalk_remote_call/servicer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|