chalk-remote-call-python 1.9.1__tar.gz → 1.9.2__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.9.1 → chalk_remote_call_python-1.9.2}/PKG-INFO +31 -1
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/README.md +30 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/Cargo.lock +7 -6
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml +4 -3
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-server/src/async_service.rs +5 -5
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-server/src/coalesce.rs +1 -2
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-server/src/metrics.rs +16 -13
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-server/src/python_bridge.rs +38 -3
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-server/src/self_consumer.rs +55 -93
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-server/src/server.rs +1 -2
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-server/src/service.rs +1 -5
- chalk_remote_call_python-1.9.2/chalk_remote_call/_version.py +1 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/cli.py +10 -10
- chalk_remote_call_python-1.9.2/chalk_remote_call/errors.py +102 -0
- chalk_remote_call_python-1.9.2/chalk_remote_call/logging.py +29 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/servicer.py +26 -38
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call_python.egg-info/PKG-INFO +31 -1
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call_python.egg-info/SOURCES.txt +2 -0
- chalk_remote_call_python-1.9.1/chalk_remote_call/_version.py +0 -1
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/MANIFEST.in +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/Cargo.toml +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-proto/Cargo.toml +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.auth.v1.rs +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.common.v1.rs +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.rs +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.tonic.rs +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.utils.v1.rs +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/descriptor.bin +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/lib.rs +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/chalk-remote-call-server/src/lib.rs +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk-remote-call-rs/rust-toolchain.toml +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/__main__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_native.pyi +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/arrow_utils.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/handler_loader.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/input_transform.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/server.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/tracing.py +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call_python.egg-info/dependency_links.txt +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call_python.egg-info/entry_points.txt +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call_python.egg-info/requires.txt +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call_python.egg-info/top_level.txt +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/pyproject.toml +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/setup.cfg +0 -0
- {chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/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.
|
|
3
|
+
Version: 1.9.2
|
|
4
4
|
Summary: Chalk remote call Python runtime interface client
|
|
5
5
|
Author: Chalk AI, Inc.
|
|
6
6
|
Project-URL: Homepage, https://chalk.ai
|
|
@@ -108,6 +108,16 @@ python -m chalk_remote_call --handler my_handler.handler
|
|
|
108
108
|
| `--on-shutdown` | | | Dotted path to a shutdown function |
|
|
109
109
|
| `--log-level` | | `INFO` | `DEBUG`, `INFO`, `WARNING`, or `ERROR` |
|
|
110
110
|
|
|
111
|
+
The CLI writes Python logs to stderr as one JSON object per line, with separate
|
|
112
|
+
`timestamp`, `severity`, `logger`, and `message` fields. Exceptions include an
|
|
113
|
+
`exception.stacktrace` field. Calls to `serve()` from Python retain the embedding
|
|
114
|
+
application's logging configuration.
|
|
115
|
+
|
|
116
|
+
Missing optional StatsD endpoints or a metrics-bus topic do not emit warnings.
|
|
117
|
+
Failures initializing configured metrics sinks still warn. Routine customer
|
|
118
|
+
StatsD configuration messages are emitted at DEBUG by the shared metrics library;
|
|
119
|
+
set `RUST_LOG=info,chalk_metrics::metrics::config=debug` to include them.
|
|
120
|
+
|
|
111
121
|
### Environment variables
|
|
112
122
|
|
|
113
123
|
| Variable | Description |
|
|
@@ -212,3 +222,23 @@ To check the Rust code independently:
|
|
|
212
222
|
```bash
|
|
213
223
|
PYO3_PYTHON=python3 cargo check --manifest-path chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml
|
|
214
224
|
```
|
|
225
|
+
|
|
226
|
+
## Remote exception transport
|
|
227
|
+
|
|
228
|
+
Handler failures retain their original exception type, message, traceback,
|
|
229
|
+
exception chains, and exception groups. Runtime dispatch frames are excluded
|
|
230
|
+
from the user traceback; full diagnostics are retained in server logs.
|
|
231
|
+
|
|
232
|
+
All call modes carry a JSON payload with `chalk_remote_error: 1` and an
|
|
233
|
+
`exception` object containing the `ChalkException` fields `kind`, `message`,
|
|
234
|
+
`stacktrace`, and `internal_stacktrace`. Direct calls carry the payload as the
|
|
235
|
+
unescaped gRPC status message; queued calls carry it in the error message. The
|
|
236
|
+
local async service additionally populates `ChalkError.exception`. This permits
|
|
237
|
+
existing queue transports to forward structured errors without a protobuf
|
|
238
|
+
migration. Clients must decode the versioned payload, not parse gRPC debug text
|
|
239
|
+
or classify user errors by their message. Unknown versions remain opaque.
|
|
240
|
+
|
|
241
|
+
Payloads are bounded for gRPC header limits. Truncation is marked explicitly,
|
|
242
|
+
with complete tracebacks kept in server logs. Exception objects and locals are
|
|
243
|
+
never pickled or sent. Chalk SDK-generated handlers identify their dispatch
|
|
244
|
+
frames, and notebook source maps identify the deployed snapshot's lines.
|
|
@@ -70,6 +70,16 @@ python -m chalk_remote_call --handler my_handler.handler
|
|
|
70
70
|
| `--on-shutdown` | | | Dotted path to a shutdown function |
|
|
71
71
|
| `--log-level` | | `INFO` | `DEBUG`, `INFO`, `WARNING`, or `ERROR` |
|
|
72
72
|
|
|
73
|
+
The CLI writes Python logs to stderr as one JSON object per line, with separate
|
|
74
|
+
`timestamp`, `severity`, `logger`, and `message` fields. Exceptions include an
|
|
75
|
+
`exception.stacktrace` field. Calls to `serve()` from Python retain the embedding
|
|
76
|
+
application's logging configuration.
|
|
77
|
+
|
|
78
|
+
Missing optional StatsD endpoints or a metrics-bus topic do not emit warnings.
|
|
79
|
+
Failures initializing configured metrics sinks still warn. Routine customer
|
|
80
|
+
StatsD configuration messages are emitted at DEBUG by the shared metrics library;
|
|
81
|
+
set `RUST_LOG=info,chalk_metrics::metrics::config=debug` to include them.
|
|
82
|
+
|
|
73
83
|
### Environment variables
|
|
74
84
|
|
|
75
85
|
| Variable | Description |
|
|
@@ -174,3 +184,23 @@ To check the Rust code independently:
|
|
|
174
184
|
```bash
|
|
175
185
|
PYO3_PYTHON=python3 cargo check --manifest-path chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml
|
|
176
186
|
```
|
|
187
|
+
|
|
188
|
+
## Remote exception transport
|
|
189
|
+
|
|
190
|
+
Handler failures retain their original exception type, message, traceback,
|
|
191
|
+
exception chains, and exception groups. Runtime dispatch frames are excluded
|
|
192
|
+
from the user traceback; full diagnostics are retained in server logs.
|
|
193
|
+
|
|
194
|
+
All call modes carry a JSON payload with `chalk_remote_error: 1` and an
|
|
195
|
+
`exception` object containing the `ChalkException` fields `kind`, `message`,
|
|
196
|
+
`stacktrace`, and `internal_stacktrace`. Direct calls carry the payload as the
|
|
197
|
+
unescaped gRPC status message; queued calls carry it in the error message. The
|
|
198
|
+
local async service additionally populates `ChalkError.exception`. This permits
|
|
199
|
+
existing queue transports to forward structured errors without a protobuf
|
|
200
|
+
migration. Clients must decode the versioned payload, not parse gRPC debug text
|
|
201
|
+
or classify user errors by their message. Unknown versions remain opaque.
|
|
202
|
+
|
|
203
|
+
Payloads are bounded for gRPC header limits. Truncation is marked explicitly,
|
|
204
|
+
with complete tracebacks kept in server logs. Exception objects and locals are
|
|
205
|
+
never pickled or sent. Chalk SDK-generated handlers identify their dispatch
|
|
206
|
+
frames, and notebook source maps identify the deployed snapshot's lines.
|
{chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/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=a0f8932ec10d3b8340af3e98cf63b800d60b4926#a0f8932ec10d3b8340af3e98cf63b800d60b4926"
|
|
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=a0f8932ec10d3b8340af3e98cf63b800d60b4926#a0f8932ec10d3b8340af3e98cf63b800d60b4926"
|
|
503
503
|
dependencies = [
|
|
504
504
|
"prost 0.11.9",
|
|
505
505
|
"prost-build",
|
|
@@ -547,6 +547,7 @@ dependencies = [
|
|
|
547
547
|
"prost 0.13.5",
|
|
548
548
|
"pyo3",
|
|
549
549
|
"redis",
|
|
550
|
+
"serde_json",
|
|
550
551
|
"tokio",
|
|
551
552
|
"tokio-stream",
|
|
552
553
|
"tonic 0.12.3",
|
|
@@ -560,7 +561,7 @@ dependencies = [
|
|
|
560
561
|
[[package]]
|
|
561
562
|
name = "chalk_metric_definitions"
|
|
562
563
|
version = "0.0.0"
|
|
563
|
-
source = "git+https://github.com/chalk-ai/chalk-private.git?rev=
|
|
564
|
+
source = "git+https://github.com/chalk-ai/chalk-private.git?rev=a0f8932ec10d3b8340af3e98cf63b800d60b4926#a0f8932ec10d3b8340af3e98cf63b800d60b4926"
|
|
564
565
|
dependencies = [
|
|
565
566
|
"chalk-protos",
|
|
566
567
|
"metrics-generator",
|
|
@@ -570,7 +571,7 @@ dependencies = [
|
|
|
570
571
|
[[package]]
|
|
571
572
|
name = "chalk_metrics"
|
|
572
573
|
version = "0.0.0"
|
|
573
|
-
source = "git+https://github.com/chalk-ai/chalk-private.git?rev=
|
|
574
|
+
source = "git+https://github.com/chalk-ai/chalk-private.git?rev=a0f8932ec10d3b8340af3e98cf63b800d60b4926#a0f8932ec10d3b8340af3e98cf63b800d60b4926"
|
|
574
575
|
dependencies = [
|
|
575
576
|
"async-trait",
|
|
576
577
|
"chalk-core",
|
|
@@ -604,7 +605,7 @@ dependencies = [
|
|
|
604
605
|
[[package]]
|
|
605
606
|
name = "chalk_statsd"
|
|
606
607
|
version = "0.0.0"
|
|
607
|
-
source = "git+https://github.com/chalk-ai/chalk-private.git?rev=
|
|
608
|
+
source = "git+https://github.com/chalk-ai/chalk-private.git?rev=a0f8932ec10d3b8340af3e98cf63b800d60b4926#a0f8932ec10d3b8340af3e98cf63b800d60b4926"
|
|
608
609
|
dependencies = [
|
|
609
610
|
"chalk-core",
|
|
610
611
|
"chrono",
|
|
@@ -1867,7 +1868,7 @@ dependencies = [
|
|
|
1867
1868
|
[[package]]
|
|
1868
1869
|
name = "metrics-generator"
|
|
1869
1870
|
version = "0.1.0"
|
|
1870
|
-
source = "git+https://github.com/chalk-ai/chalk-private.git?rev=
|
|
1871
|
+
source = "git+https://github.com/chalk-ai/chalk-private.git?rev=a0f8932ec10d3b8340af3e98cf63b800d60b4926#a0f8932ec10d3b8340af3e98cf63b800d60b4926"
|
|
1871
1872
|
dependencies = [
|
|
1872
1873
|
"codegen",
|
|
1873
1874
|
"serde",
|
|
@@ -21,6 +21,7 @@ arrow = { version = "54", default-features = false, features = ["ipc"] }
|
|
|
21
21
|
tracing = "0.1"
|
|
22
22
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
|
23
23
|
bytes = "1"
|
|
24
|
+
serde_json = "1"
|
|
24
25
|
uuid = { version = "1", features = ["v4"] }
|
|
25
26
|
dashmap = "5"
|
|
26
27
|
futures-util = "0.3"
|
|
@@ -34,8 +35,8 @@ futures-util = "0.3"
|
|
|
34
35
|
# chalk-private commit, mirroring how chalkcompute_remote_client depends on
|
|
35
36
|
# chalk_remote_call_client. chalk-queue declares `redis = "1.0.2"`; with no
|
|
36
37
|
# `[patch.crates-io]` here it builds against vanilla crates.io redis (it only
|
|
37
|
-
# uses standard APIs).
|
|
38
|
+
# uses standard APIs). Metrics and StatsD share a revision so their client types agree.
|
|
38
39
|
[target.'cfg(target_os = "linux")'.dependencies]
|
|
39
40
|
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 = "
|
|
41
|
-
chalk_statsd = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "
|
|
41
|
+
chalk_metrics = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "a0f8932ec10d3b8340af3e98cf63b800d60b4926" }
|
|
42
|
+
chalk_statsd = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "a0f8932ec10d3b8340af3e98cf63b800d60b4926" }
|
|
@@ -39,7 +39,7 @@ struct CallState {
|
|
|
39
39
|
/// and `status`/`error` can be updated without blocking emits.
|
|
40
40
|
chunks: Arc<Mutex<ChunkBuffer>>,
|
|
41
41
|
/// Populated only when `status == Failed`; surfaced as a `ChalkError`.
|
|
42
|
-
error: Option<
|
|
42
|
+
error: Option<ChalkError>,
|
|
43
43
|
/// When the call reached a terminal status. Used only by the orphan sweeper
|
|
44
44
|
/// to decide when an un-drained terminal entry is safe to reclaim.
|
|
45
45
|
completed_at: Option<Instant>,
|
|
@@ -189,7 +189,7 @@ impl AsyncRemoteCallService for AsyncRemoteCallServiceImpl {
|
|
|
189
189
|
}
|
|
190
190
|
Err(e) => {
|
|
191
191
|
s.status = RemoteCallStatus::Failed;
|
|
192
|
-
s.error = Some(e.
|
|
192
|
+
s.error = Some(e.chalk_error());
|
|
193
193
|
error!(
|
|
194
194
|
call_id = %id,
|
|
195
195
|
exception.stacktrace = e.details(),
|
|
@@ -246,10 +246,10 @@ impl AsyncRemoteCallService for AsyncRemoteCallServiceImpl {
|
|
|
246
246
|
drop(buf);
|
|
247
247
|
|
|
248
248
|
let errors = if status == RemoteCallStatus::Failed {
|
|
249
|
-
vec![ChalkError {
|
|
250
|
-
message:
|
|
249
|
+
vec![error.unwrap_or_else(|| ChalkError {
|
|
250
|
+
message: "remote call failed".to_string(),
|
|
251
251
|
..Default::default()
|
|
252
|
-
}]
|
|
252
|
+
})]
|
|
253
253
|
} else {
|
|
254
254
|
Vec::new()
|
|
255
255
|
};
|
|
@@ -193,8 +193,7 @@ impl CoalescingQueue {
|
|
|
193
193
|
chalk_metrics::metrics::tags::Mode::Sync,
|
|
194
194
|
);
|
|
195
195
|
}
|
|
196
|
-
let status =
|
|
197
|
-
Status::unknown(format!("Exception raised during handler execution: {e}"));
|
|
196
|
+
let status = Status::unknown(e.to_string());
|
|
198
197
|
for tx in response_txs {
|
|
199
198
|
let _ = tx.send(Err(status.clone())).await;
|
|
200
199
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//!
|
|
2
|
-
//!
|
|
3
|
-
//!
|
|
1
|
+
//! Function-call metrics for synchronous gRPC calls and self-consumed queue work.
|
|
2
|
+
//! A shared `chalk_metrics` publishing pipeline emits call counts and
|
|
3
|
+
//! processing latency, independently of whether Redis queue consumption is enabled.
|
|
4
4
|
//!
|
|
5
5
|
//! The External Function "Function Calls Dequeued" chart reads TimescaleDB
|
|
6
6
|
//! `metrics1`, which is fed only by the chalk_metrics PubSub bus (statsd goes to
|
|
@@ -15,8 +15,9 @@ use chalk_metrics::metrics::well_known_metrics::WellKnownMetricName;
|
|
|
15
15
|
use chalk_metrics::{
|
|
16
16
|
make_pubsub_publisher, BatchConfig, BusPublisher, MetricsPipeline, PublishingMetricsPipeline,
|
|
17
17
|
};
|
|
18
|
+
use chalk_statsd::datadog::DatadogError;
|
|
18
19
|
use chalk_statsd::metrics::MetricsClient;
|
|
19
|
-
use tracing::{
|
|
20
|
+
use tracing::{debug, warn};
|
|
20
21
|
|
|
21
22
|
// Batch tuning — chalk_function_queue's defaults (METRICS_BATCH_* ), inlined.
|
|
22
23
|
const BATCH_MAX_MESSAGES: usize = 1000;
|
|
@@ -24,26 +25,27 @@ const BATCH_MAX_BYTES: usize = 10 * 1024 * 1024; // 10 MB
|
|
|
24
25
|
const BATCH_MAX_LATENCY: Duration = Duration::from_millis(100);
|
|
25
26
|
|
|
26
27
|
/// Build and start the metrics pipeline from the environment, mirroring the
|
|
27
|
-
/// fnq-server / catalog-consumer init. Returns `None`
|
|
28
|
-
/// object can't be constructed; the self-consumer then runs without metrics.
|
|
28
|
+
/// fnq-server / catalog-consumer init. Returns `None` if startup fails.
|
|
29
29
|
///
|
|
30
30
|
/// The bus publisher is what reaches the metrics store (and thus the dashboard);
|
|
31
|
-
///
|
|
32
|
-
///
|
|
31
|
+
/// StatsD and the bus are optional sinks. Missing configuration is quiet;
|
|
32
|
+
/// failures initializing a configured sink remain warnings.
|
|
33
33
|
pub async fn init_pipeline() -> Option<Arc<PublishingMetricsPipeline>> {
|
|
34
34
|
let environment_id = std::env::var("CHALK_ENVIRONMENT_ID").unwrap_or_default();
|
|
35
35
|
|
|
36
36
|
let statsd = Arc::new(
|
|
37
37
|
MetricsClient::new_from_default_environment().unwrap_or_else(|e| {
|
|
38
|
-
|
|
38
|
+
if !matches!(
|
|
39
|
+
e.downcast_ref::<DatadogError>(),
|
|
40
|
+
Some(DatadogError::ConnectionInfoNotFound)
|
|
41
|
+
) {
|
|
42
|
+
warn!(error = %e, "failed to init statsd client; statsd metrics disabled");
|
|
43
|
+
}
|
|
39
44
|
MetricsClient::new_null()
|
|
40
45
|
}),
|
|
41
46
|
);
|
|
42
47
|
|
|
43
48
|
let publishers: Vec<Arc<dyn BusPublisher>> = build_bus_publisher().await.into_iter().collect();
|
|
44
|
-
if publishers.is_empty() {
|
|
45
|
-
warn!("no metrics bus publisher configured; function call metrics will not reach the dashboard");
|
|
46
|
-
}
|
|
47
49
|
|
|
48
50
|
let metrics_pipeline: Arc<PublishingMetricsPipeline> =
|
|
49
51
|
Arc::new(PublishingMetricsPipeline::new_with_env_var_defaults(
|
|
@@ -54,8 +56,9 @@ pub async fn init_pipeline() -> Option<Arc<PublishingMetricsPipeline>> {
|
|
|
54
56
|
));
|
|
55
57
|
if let Err(e) = metrics_pipeline.start().await {
|
|
56
58
|
warn!(error = %e, "failed to start metrics pipeline; metrics disabled");
|
|
59
|
+
return None;
|
|
57
60
|
}
|
|
58
|
-
|
|
61
|
+
debug!("function-call metrics pipeline started");
|
|
59
62
|
Some(metrics_pipeline)
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
use std::collections::HashMap;
|
|
2
2
|
use std::sync::{Arc, Mutex};
|
|
3
3
|
|
|
4
|
+
use chalk_remote_call_proto::chalk::common::v1::{ChalkError, ChalkException};
|
|
4
5
|
use chalk_remote_call_proto::chalk::runtime::v1::CallFunctionResponse;
|
|
5
6
|
use pyo3::prelude::*;
|
|
6
7
|
use pyo3::types::{PyBytes, PyDict, PyList};
|
|
@@ -321,28 +322,62 @@ fn invoke_process_fn(
|
|
|
321
322
|
#[derive(Debug)]
|
|
322
323
|
pub struct PythonError {
|
|
323
324
|
details: String,
|
|
325
|
+
wire_message: String,
|
|
324
326
|
}
|
|
325
327
|
|
|
326
328
|
impl PythonError {
|
|
327
329
|
fn new(message: String) -> Self {
|
|
328
|
-
Self {
|
|
330
|
+
Self {
|
|
331
|
+
details: message.clone(),
|
|
332
|
+
wire_message: message,
|
|
333
|
+
}
|
|
329
334
|
}
|
|
330
335
|
|
|
331
336
|
pub fn details(&self) -> &str {
|
|
332
337
|
&self.details
|
|
333
338
|
}
|
|
339
|
+
|
|
340
|
+
pub fn chalk_error(&self) -> ChalkError {
|
|
341
|
+
let exception = serde_json::from_str::<serde_json::Value>(&self.wire_message)
|
|
342
|
+
.ok()
|
|
343
|
+
.and_then(|payload| {
|
|
344
|
+
let exception = payload.get("exception")?;
|
|
345
|
+
Some(ChalkException {
|
|
346
|
+
kind: exception.get("kind")?.as_str()?.to_string(),
|
|
347
|
+
message: exception.get("message")?.as_str()?.to_string(),
|
|
348
|
+
stacktrace: exception.get("stacktrace")?.as_str()?.to_string(),
|
|
349
|
+
internal_stacktrace: exception
|
|
350
|
+
.get("internal_stacktrace")?
|
|
351
|
+
.as_str()?
|
|
352
|
+
.to_string(),
|
|
353
|
+
})
|
|
354
|
+
});
|
|
355
|
+
ChalkError {
|
|
356
|
+
message: self.wire_message.clone(),
|
|
357
|
+
exception,
|
|
358
|
+
..Default::default()
|
|
359
|
+
}
|
|
360
|
+
}
|
|
334
361
|
}
|
|
335
362
|
|
|
336
363
|
impl std::fmt::Display for PythonError {
|
|
337
364
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
338
|
-
write!(f, "{}", self.
|
|
365
|
+
write!(f, "{}", self.wire_message)
|
|
339
366
|
}
|
|
340
367
|
}
|
|
341
368
|
|
|
342
369
|
fn into_python_error(py: Python<'_>, err: PyErr) -> PythonError {
|
|
343
370
|
let summary = err.to_string();
|
|
344
371
|
let details = format_python_traceback(py, &err).unwrap_or_else(|_| summary.clone());
|
|
345
|
-
|
|
372
|
+
let wire_message = py
|
|
373
|
+
.import("chalk_remote_call.errors")
|
|
374
|
+
.and_then(|module| module.call_method1("serialize_exception", (err.value(py),)))
|
|
375
|
+
.and_then(|value| value.extract::<String>())
|
|
376
|
+
.unwrap_or_else(|_| details.clone());
|
|
377
|
+
PythonError {
|
|
378
|
+
details,
|
|
379
|
+
wire_message,
|
|
380
|
+
}
|
|
346
381
|
}
|
|
347
382
|
|
|
348
383
|
fn format_python_traceback(py: Python<'_>, err: &PyErr) -> PyResult<String> {
|
|
@@ -231,8 +231,6 @@ pub async fn run(
|
|
|
231
231
|
let conn = ConnectionManager::new(client).await?;
|
|
232
232
|
let queue = FunctionQueue::new(conn.clone(), config.tenant.clone());
|
|
233
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));
|
|
236
234
|
let sem = queue.semaphore(&config.function_name, config.max_concurrent, config.sem_ttl);
|
|
237
235
|
let sem_key = queue.sem_key_for(&config.function_name);
|
|
238
236
|
let live_calls: LiveCalls = Arc::new(DashMap::new());
|
|
@@ -242,9 +240,12 @@ pub async fn run(
|
|
|
242
240
|
Uuid::new_v4()
|
|
243
241
|
);
|
|
244
242
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
if config.queue_protocol == QueueProtocol::StreamV1 {
|
|
244
|
+
// XGROUP CREATE is a startup operation. Keeping it out of every XREAD
|
|
245
|
+
// and XAUTOCLAIM avoids two avoidable Redis round trips per dispatch
|
|
246
|
+
// window.
|
|
247
|
+
input_queue.initialize_consumer_group().await?;
|
|
248
|
+
}
|
|
248
249
|
|
|
249
250
|
spawn_renewer(
|
|
250
251
|
input_queue.clone(),
|
|
@@ -265,11 +266,12 @@ pub async fn run(
|
|
|
265
266
|
);
|
|
266
267
|
|
|
267
268
|
if config.queue_protocol == QueueProtocol::StreamV1 {
|
|
269
|
+
let legacy_queue = queue.bind(&config.function_name, QueueProtocol::ListV1);
|
|
268
270
|
return run_stream_dispatch_loop(
|
|
269
271
|
&config,
|
|
270
272
|
queue,
|
|
271
273
|
input_queue,
|
|
272
|
-
legacy_queue,
|
|
274
|
+
Some(legacy_queue),
|
|
273
275
|
sem,
|
|
274
276
|
conn,
|
|
275
277
|
consumer_name,
|
|
@@ -281,13 +283,42 @@ pub async fn run(
|
|
|
281
283
|
.await;
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
run_list_dispatch_loop(
|
|
287
|
+
&config,
|
|
288
|
+
queue,
|
|
289
|
+
input_queue,
|
|
290
|
+
sem,
|
|
291
|
+
conn,
|
|
292
|
+
consumer_name,
|
|
293
|
+
live_calls,
|
|
294
|
+
handler,
|
|
295
|
+
metrics_pipeline,
|
|
296
|
+
coalescing_queue,
|
|
297
|
+
)
|
|
298
|
+
.await
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Consume the legacy Redis List queue one call at a time. Streams use their
|
|
302
|
+
/// own batched dispatch loop above so they never fall through this path.
|
|
303
|
+
#[allow(clippy::too_many_arguments)]
|
|
304
|
+
async fn run_list_dispatch_loop(
|
|
305
|
+
config: &SelfConsumerConfig,
|
|
306
|
+
queue: FunctionQueue,
|
|
307
|
+
input_queue: BoundFunctionQueue,
|
|
308
|
+
sem: chalk_queue::DistributedSemaphore,
|
|
309
|
+
conn: ConnectionManager,
|
|
310
|
+
consumer_name: String,
|
|
311
|
+
live_calls: LiveCalls,
|
|
312
|
+
handler: Arc<PythonHandler>,
|
|
313
|
+
metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
|
|
314
|
+
coalescing_queue: Option<Arc<CoalescingQueue>>,
|
|
315
|
+
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
284
316
|
let mut backoff = config.poll_backoff_initial;
|
|
285
317
|
loop {
|
|
286
318
|
// Idle-gate: if there is no work, don't take (then immediately release)
|
|
287
319
|
// a semaphore slot — that churn would race genuine acquirers on other
|
|
288
|
-
// pods.
|
|
289
|
-
|
|
290
|
-
match pending_depth(&input_queue, legacy_queue.as_ref()).await {
|
|
320
|
+
// pods.
|
|
321
|
+
match input_queue.current_depth().await {
|
|
291
322
|
Ok(0) => {
|
|
292
323
|
tokio::time::sleep(backoff).await;
|
|
293
324
|
backoff = (backoff * 2).min(config.poll_backoff_max);
|
|
@@ -318,14 +349,7 @@ pub async fn run(
|
|
|
318
349
|
}
|
|
319
350
|
};
|
|
320
351
|
|
|
321
|
-
let delivery = match
|
|
322
|
-
&input_queue,
|
|
323
|
-
legacy_queue.as_ref(),
|
|
324
|
-
&consumer_name,
|
|
325
|
-
config.sem_ttl,
|
|
326
|
-
)
|
|
327
|
-
.await
|
|
328
|
-
{
|
|
352
|
+
let delivery = match input_queue.dequeue(&consumer_name).await {
|
|
329
353
|
Ok(Some(delivery)) => delivery,
|
|
330
354
|
// Raced another consumer (or our own idle-gate) to an empty queue:
|
|
331
355
|
// give the slot back and retry.
|
|
@@ -344,41 +368,18 @@ pub async fn run(
|
|
|
344
368
|
|
|
345
369
|
backoff = config.poll_backoff_initial;
|
|
346
370
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
cq.clone(),
|
|
360
|
-
conn.clone(),
|
|
361
|
-
delivery,
|
|
362
|
-
lease,
|
|
363
|
-
live_calls.clone(),
|
|
364
|
-
metrics_pipeline.clone(),
|
|
365
|
-
));
|
|
366
|
-
}
|
|
367
|
-
// Batching off: stream the handler's output chunk-by-chunk.
|
|
368
|
-
None => {
|
|
369
|
-
tokio::spawn(run_one_call(
|
|
370
|
-
handler.clone(),
|
|
371
|
-
queue.clone(),
|
|
372
|
-
input_queue.clone(),
|
|
373
|
-
conn.clone(),
|
|
374
|
-
delivery,
|
|
375
|
-
lease,
|
|
376
|
-
live_calls.clone(),
|
|
377
|
-
config.flush_timeout,
|
|
378
|
-
metrics_pipeline.clone(),
|
|
379
|
-
));
|
|
380
|
-
}
|
|
381
|
-
}
|
|
371
|
+
dispatch_delivery(
|
|
372
|
+
queue.clone(),
|
|
373
|
+
input_queue.clone(),
|
|
374
|
+
coalescing_queue.clone(),
|
|
375
|
+
handler.clone(),
|
|
376
|
+
conn.clone(),
|
|
377
|
+
delivery,
|
|
378
|
+
lease,
|
|
379
|
+
live_calls.clone(),
|
|
380
|
+
config.flush_timeout,
|
|
381
|
+
metrics_pipeline.clone(),
|
|
382
|
+
);
|
|
382
383
|
// Loop immediately — dispatch as fast as we can acquire + pop. The
|
|
383
384
|
// semaphore (not this loop) is what bounds in-flight work.
|
|
384
385
|
}
|
|
@@ -543,43 +544,6 @@ fn dispatch_delivery(
|
|
|
543
544
|
}
|
|
544
545
|
}
|
|
545
546
|
|
|
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
|
-
|
|
583
547
|
/// Run a single queued call to completion, write its results to the call's
|
|
584
548
|
/// stream, then deregister + release its lease. Always releases the lease,
|
|
585
549
|
/// even if execution or stream writes fail or panic.
|
|
@@ -937,15 +901,13 @@ async fn execute_and_write(
|
|
|
937
901
|
info!(call_id = %work.call_id, chunks = flushed, "queued call completed");
|
|
938
902
|
}
|
|
939
903
|
Err(e) => {
|
|
940
|
-
// The formatted Python traceback is available, but the concrete
|
|
941
|
-
// exception type is not carried separately across this boundary.
|
|
942
904
|
stream_writer::write_error(
|
|
943
905
|
conn,
|
|
944
906
|
&stream_key,
|
|
945
907
|
seq,
|
|
946
908
|
"RemoteFunctionError",
|
|
947
|
-
e.
|
|
948
|
-
|
|
909
|
+
&e.to_string(),
|
|
910
|
+
Some(e.details()),
|
|
949
911
|
)
|
|
950
912
|
.await?;
|
|
951
913
|
let summary: String = e.to_string().chars().take(200).collect();
|
|
@@ -80,8 +80,7 @@ pub async fn run_server(
|
|
|
80
80
|
|
|
81
81
|
#[cfg(target_os = "linux")]
|
|
82
82
|
if let Some(cfg) = crate::self_consumer::SelfConsumerConfig::from_env() {
|
|
83
|
-
//
|
|
84
|
-
// main init) so the self-consumer can emit function-call metrics.
|
|
83
|
+
// Share the synchronous gRPC server's function-call metrics pipeline.
|
|
85
84
|
let consumer_handler = python_handler.clone();
|
|
86
85
|
let consumer_pipeline = pipeline.clone();
|
|
87
86
|
let consumer_coalescing = coalescing_queue.clone();
|
|
@@ -144,11 +144,7 @@ impl RemoteCallService for RemoteCallServiceImpl {
|
|
|
144
144
|
"Python handler error: {}",
|
|
145
145
|
e.details()
|
|
146
146
|
);
|
|
147
|
-
let _ = error_tx
|
|
148
|
-
.send(Err(Status::unknown(format!(
|
|
149
|
-
"Exception raised during handler execution: {e}"
|
|
150
|
-
))))
|
|
151
|
-
.await;
|
|
147
|
+
let _ = error_tx.send(Err(Status::unknown(e.to_string()))).await;
|
|
152
148
|
}
|
|
153
149
|
}
|
|
154
150
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.9.2"
|
|
@@ -7,6 +7,7 @@ import sys
|
|
|
7
7
|
|
|
8
8
|
from chalk_remote_call.handler_loader import load_function, load_handler
|
|
9
9
|
from chalk_remote_call.input_transform import parse_input_args
|
|
10
|
+
from chalk_remote_call.logging import JsonFormatter
|
|
10
11
|
from chalk_remote_call.server import serve
|
|
11
12
|
|
|
12
13
|
|
|
@@ -99,16 +100,15 @@ def main() -> None:
|
|
|
99
100
|
)
|
|
100
101
|
args = parser.parse_args()
|
|
101
102
|
|
|
102
|
-
logging.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
103
|
+
log_handler = logging.StreamHandler(sys.stderr)
|
|
104
|
+
log_handler.setFormatter(JsonFormatter())
|
|
105
|
+
logging.basicConfig(level=getattr(logging, args.log_level), handlers=[log_handler])
|
|
106
106
|
|
|
107
107
|
# Load handler
|
|
108
108
|
try:
|
|
109
109
|
handler, auto_startup, auto_shutdown = load_handler(args.handler)
|
|
110
|
-
except Exception
|
|
111
|
-
|
|
110
|
+
except Exception:
|
|
111
|
+
logging.getLogger(__name__).exception("Error loading handler")
|
|
112
112
|
sys.exit(1)
|
|
113
113
|
|
|
114
114
|
# Determine startup function
|
|
@@ -116,8 +116,8 @@ def main() -> None:
|
|
|
116
116
|
if args.on_startup:
|
|
117
117
|
try:
|
|
118
118
|
on_startup = load_function(args.on_startup, label="Startup")
|
|
119
|
-
except Exception
|
|
120
|
-
|
|
119
|
+
except Exception:
|
|
120
|
+
logging.getLogger(__name__).exception("Error loading startup function")
|
|
121
121
|
sys.exit(1)
|
|
122
122
|
elif auto_startup is not None:
|
|
123
123
|
on_startup = auto_startup
|
|
@@ -127,8 +127,8 @@ def main() -> None:
|
|
|
127
127
|
if args.on_shutdown:
|
|
128
128
|
try:
|
|
129
129
|
on_shutdown = load_function(args.on_shutdown, label="Shutdown")
|
|
130
|
-
except Exception
|
|
131
|
-
|
|
130
|
+
except Exception:
|
|
131
|
+
logging.getLogger(__name__).exception("Error loading shutdown function")
|
|
132
132
|
sys.exit(1)
|
|
133
133
|
elif auto_shutdown is not None:
|
|
134
134
|
on_shutdown = auto_shutdown
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Versioned, data-only remote exception transport shared by all call modes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import json
|
|
7
|
+
import traceback
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _user_stack(tb: TracebackType | None) -> traceback.StackSummary:
|
|
12
|
+
frames = []
|
|
13
|
+
entered_user_code = False
|
|
14
|
+
while tb is not None:
|
|
15
|
+
frame = tb.tb_frame
|
|
16
|
+
module = str(frame.f_globals.get("__name__", ""))
|
|
17
|
+
runner_frame = not entered_user_code and module in {
|
|
18
|
+
"asyncio.base_events",
|
|
19
|
+
"asyncio.runners",
|
|
20
|
+
"concurrent.futures._base",
|
|
21
|
+
"concurrent.futures.thread",
|
|
22
|
+
}
|
|
23
|
+
if not (
|
|
24
|
+
runner_frame
|
|
25
|
+
or module == "chalk_remote_call.servicer"
|
|
26
|
+
or module == "chalk_remote_call.errors"
|
|
27
|
+
or module.startswith("chalkcompute.")
|
|
28
|
+
or frame.f_globals.get("__chalk_generated_handler__") is True
|
|
29
|
+
):
|
|
30
|
+
entered_user_code = True
|
|
31
|
+
filename, lineno = frame.f_code.co_filename, tb.tb_lineno
|
|
32
|
+
source_map = frame.f_globals.get("__chalk_source_map__", {})
|
|
33
|
+
mapped = source_map.get(lineno) if isinstance(source_map, dict) else None
|
|
34
|
+
if isinstance(mapped, tuple) and len(mapped) == 3:
|
|
35
|
+
filename, lineno, source = mapped
|
|
36
|
+
else:
|
|
37
|
+
source = None
|
|
38
|
+
frames.append(traceback.FrameSummary(filename, lineno, frame.f_code.co_name, line=source))
|
|
39
|
+
tb = tb.tb_next
|
|
40
|
+
return traceback.StackSummary.from_list(frames)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _filter_traceback(summary: traceback.TracebackException, error: BaseException) -> None:
|
|
44
|
+
summary.stack = _user_stack(error.__traceback__)
|
|
45
|
+
if summary.__cause__ is not None and error.__cause__ is not None:
|
|
46
|
+
_filter_traceback(summary.__cause__, error.__cause__)
|
|
47
|
+
if summary.__context__ is not None and error.__context__ is not None:
|
|
48
|
+
_filter_traceback(summary.__context__, error.__context__)
|
|
49
|
+
children = getattr(error, "exceptions", ())
|
|
50
|
+
for child_summary, child in zip(getattr(summary, "exceptions", None) or (), children, strict=False):
|
|
51
|
+
_filter_traceback(child_summary, child)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def serialize_exception(error: BaseException) -> str:
|
|
55
|
+
"""Keep chains and groups, without pickling exceptions or their locals."""
|
|
56
|
+
summary = traceback.TracebackException.from_exception(error, capture_locals=False)
|
|
57
|
+
internal = "".join(summary.format())
|
|
58
|
+
_filter_traceback(summary, error)
|
|
59
|
+
payload: dict[str, object] = {
|
|
60
|
+
"chalk_remote_error": 1,
|
|
61
|
+
"exception": {
|
|
62
|
+
"kind": type(error).__name__[:256],
|
|
63
|
+
"message": _safe_message(error),
|
|
64
|
+
"stacktrace": "".join(summary.format()),
|
|
65
|
+
"internal_stacktrace": internal,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
# Include a trace ID when serialization runs with an active trace context.
|
|
69
|
+
try:
|
|
70
|
+
trace = importlib.import_module("opentelemetry.trace")
|
|
71
|
+
context = trace.get_current_span().get_span_context()
|
|
72
|
+
if context.is_valid:
|
|
73
|
+
payload["trace_id"] = format(context.trace_id, "032x")
|
|
74
|
+
except ImportError:
|
|
75
|
+
pass
|
|
76
|
+
# gRPC status messages live in headers. Keep the complete diagnostic in
|
|
77
|
+
# server logs, and bound the wire payload below common 8 KiB header limits.
|
|
78
|
+
encoded = json.dumps(payload, ensure_ascii=True)
|
|
79
|
+
exception = payload["exception"]
|
|
80
|
+
assert isinstance(exception, dict)
|
|
81
|
+
while _status_header_size(encoded) > 6000:
|
|
82
|
+
field = max(exception, key=lambda key: _status_header_size(json.dumps(exception[key], ensure_ascii=True)))
|
|
83
|
+
text = exception[field]
|
|
84
|
+
keep = len(text) // 4
|
|
85
|
+
if keep < 20:
|
|
86
|
+
break
|
|
87
|
+
exception[field] = text[:keep] + "\n... remote traceback truncated ...\n" + text[-keep:]
|
|
88
|
+
encoded = json.dumps(payload, ensure_ascii=True)
|
|
89
|
+
return encoded
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _status_header_size(message: str) -> int:
|
|
93
|
+
# Tonic percent-encodes these ASCII characters in grpc-message. The JSON
|
|
94
|
+
# serializer already escapes controls and non-ASCII code points.
|
|
95
|
+
return len(message) + 2 * sum(char in ' "#%<>`?{}' for char in message)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _safe_message(error: BaseException) -> str:
|
|
99
|
+
try:
|
|
100
|
+
return str(error)
|
|
101
|
+
except Exception:
|
|
102
|
+
return "<exception message unavailable>"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
_RECORD_FIELDS = frozenset(logging.makeLogRecord({}).__dict__) | {"message", "asctime"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JsonFormatter(logging.Formatter):
|
|
12
|
+
"""Emit one JSON object per line using fields recognized by Chalk's log collector."""
|
|
13
|
+
|
|
14
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
15
|
+
fields: dict[str, Any] = {key: value for key, value in vars(record).items() if key not in _RECORD_FIELDS}
|
|
16
|
+
fields.update(
|
|
17
|
+
timestamp=datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
|
|
18
|
+
severity=record.levelname,
|
|
19
|
+
logger=record.name,
|
|
20
|
+
message=record.getMessage(),
|
|
21
|
+
)
|
|
22
|
+
# Keep tracebacks out of the message and escape their newlines so the collector sees one event.
|
|
23
|
+
if record.exc_info:
|
|
24
|
+
fields["exception.stacktrace"] = self.formatException(record.exc_info)
|
|
25
|
+
elif record.exc_text:
|
|
26
|
+
fields["exception.stacktrace"] = record.exc_text
|
|
27
|
+
if record.stack_info:
|
|
28
|
+
fields["stack_info"] = self.formatStack(record.stack_info)
|
|
29
|
+
return json.dumps(fields, default=str)
|
{chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/servicer.py
RENAMED
|
@@ -200,34 +200,28 @@ def process_batches(
|
|
|
200
200
|
raise ValueError(f"Input transformation failed: {e}") from e
|
|
201
201
|
|
|
202
202
|
with remote_function_invocation_span(context_metadata):
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
result = _run_async(result)
|
|
208
|
-
except Exception as e:
|
|
209
|
-
raise RuntimeError(f"Exception raised during handler execution: {e}") from e
|
|
203
|
+
result = handler(event, context_metadata)
|
|
204
|
+
# If the handler is async, await the coroutine.
|
|
205
|
+
if inspect.iscoroutine(result):
|
|
206
|
+
result = _run_async(result)
|
|
210
207
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
result_batch = _coerce_to_record_batch(item)
|
|
216
|
-
append_or_emit(encode_record_batch(result_batch))
|
|
217
|
-
elif inspect.isasyncgen(result):
|
|
218
|
-
# Async generator: stream when possible, otherwise preserve
|
|
219
|
-
# the existing list-returning bridge contract.
|
|
220
|
-
if emit is not None:
|
|
221
|
-
_drain_async_gen(result, emit)
|
|
222
|
-
else:
|
|
223
|
-
for item in _collect_async_gen(result):
|
|
224
|
-
result_batch = _coerce_to_record_batch(item)
|
|
225
|
-
results.append(encode_record_batch(result_batch))
|
|
226
|
-
else:
|
|
227
|
-
result_batch = _coerce_to_record_batch(result)
|
|
208
|
+
if inspect.isgenerator(result):
|
|
209
|
+
# Sync generator: encode each yielded value as a separate chunk.
|
|
210
|
+
for item in result:
|
|
211
|
+
result_batch = _coerce_to_record_batch(item)
|
|
228
212
|
append_or_emit(encode_record_batch(result_batch))
|
|
229
|
-
|
|
230
|
-
|
|
213
|
+
elif inspect.isasyncgen(result):
|
|
214
|
+
# Async generator: stream when possible, otherwise preserve
|
|
215
|
+
# the existing list-returning bridge contract.
|
|
216
|
+
if emit is not None:
|
|
217
|
+
_drain_async_gen(result, emit)
|
|
218
|
+
else:
|
|
219
|
+
for item in _collect_async_gen(result):
|
|
220
|
+
result_batch = _coerce_to_record_batch(item)
|
|
221
|
+
results.append(encode_record_batch(result_batch))
|
|
222
|
+
else:
|
|
223
|
+
result_batch = _coerce_to_record_batch(result)
|
|
224
|
+
append_or_emit(encode_record_batch(result_batch))
|
|
231
225
|
|
|
232
226
|
return results
|
|
233
227
|
|
|
@@ -300,12 +294,9 @@ def process_batches_coalesced(
|
|
|
300
294
|
# Dispatch. Contexts are passed as a parallel list — handler can correlate
|
|
301
295
|
# per-caller metadata with each event by index.
|
|
302
296
|
with remote_function_invocation_span(context_metadatas, coalesced_count=len(context_metadatas)):
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
result = _run_async(result)
|
|
307
|
-
except Exception as e:
|
|
308
|
-
raise RuntimeError(f"Exception raised during handler execution: {e}") from e
|
|
297
|
+
result = handler(events, context_metadatas)
|
|
298
|
+
if inspect.iscoroutine(result):
|
|
299
|
+
result = _run_async(result)
|
|
309
300
|
|
|
310
301
|
if not isinstance(result, list):
|
|
311
302
|
raise TypeError(f"Coalesced handler must return a list, got {type(result).__name__}")
|
|
@@ -402,12 +393,9 @@ def process_batches_coalesced_combined(
|
|
|
402
393
|
combined = combined.rename_columns(names)
|
|
403
394
|
|
|
404
395
|
with remote_function_invocation_span(context_metadatas, coalesced_count=len(context_metadatas)):
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
result = _run_async(result)
|
|
409
|
-
except Exception as e:
|
|
410
|
-
raise RuntimeError(f"Exception raised during handler execution: {e}") from e
|
|
396
|
+
result = handler(combined, offsets, context_metadatas)
|
|
397
|
+
if inspect.iscoroutine(result):
|
|
398
|
+
result = _run_async(result)
|
|
411
399
|
|
|
412
400
|
if isinstance(result, pa.RecordBatch):
|
|
413
401
|
result_rb = result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chalk-remote-call-python
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.2
|
|
4
4
|
Summary: Chalk remote call Python runtime interface client
|
|
5
5
|
Author: Chalk AI, Inc.
|
|
6
6
|
Project-URL: Homepage, https://chalk.ai
|
|
@@ -108,6 +108,16 @@ python -m chalk_remote_call --handler my_handler.handler
|
|
|
108
108
|
| `--on-shutdown` | | | Dotted path to a shutdown function |
|
|
109
109
|
| `--log-level` | | `INFO` | `DEBUG`, `INFO`, `WARNING`, or `ERROR` |
|
|
110
110
|
|
|
111
|
+
The CLI writes Python logs to stderr as one JSON object per line, with separate
|
|
112
|
+
`timestamp`, `severity`, `logger`, and `message` fields. Exceptions include an
|
|
113
|
+
`exception.stacktrace` field. Calls to `serve()` from Python retain the embedding
|
|
114
|
+
application's logging configuration.
|
|
115
|
+
|
|
116
|
+
Missing optional StatsD endpoints or a metrics-bus topic do not emit warnings.
|
|
117
|
+
Failures initializing configured metrics sinks still warn. Routine customer
|
|
118
|
+
StatsD configuration messages are emitted at DEBUG by the shared metrics library;
|
|
119
|
+
set `RUST_LOG=info,chalk_metrics::metrics::config=debug` to include them.
|
|
120
|
+
|
|
111
121
|
### Environment variables
|
|
112
122
|
|
|
113
123
|
| Variable | Description |
|
|
@@ -212,3 +222,23 @@ To check the Rust code independently:
|
|
|
212
222
|
```bash
|
|
213
223
|
PYO3_PYTHON=python3 cargo check --manifest-path chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml
|
|
214
224
|
```
|
|
225
|
+
|
|
226
|
+
## Remote exception transport
|
|
227
|
+
|
|
228
|
+
Handler failures retain their original exception type, message, traceback,
|
|
229
|
+
exception chains, and exception groups. Runtime dispatch frames are excluded
|
|
230
|
+
from the user traceback; full diagnostics are retained in server logs.
|
|
231
|
+
|
|
232
|
+
All call modes carry a JSON payload with `chalk_remote_error: 1` and an
|
|
233
|
+
`exception` object containing the `ChalkException` fields `kind`, `message`,
|
|
234
|
+
`stacktrace`, and `internal_stacktrace`. Direct calls carry the payload as the
|
|
235
|
+
unescaped gRPC status message; queued calls carry it in the error message. The
|
|
236
|
+
local async service additionally populates `ChalkError.exception`. This permits
|
|
237
|
+
existing queue transports to forward structured errors without a protobuf
|
|
238
|
+
migration. Clients must decode the versioned payload, not parse gRPC debug text
|
|
239
|
+
or classify user errors by their message. Unknown versions remain opaque.
|
|
240
|
+
|
|
241
|
+
Payloads are bounded for gRPC header limits. Truncation is marked explicitly,
|
|
242
|
+
with complete tracebacks kept in server logs. Exception objects and locals are
|
|
243
|
+
never pickled or sent. Chalk SDK-generated handlers identify their dispatch
|
|
244
|
+
frames, and notebook source maps identify the deployed snapshot's lines.
|
|
@@ -28,8 +28,10 @@ chalk_remote_call/_native.pyi
|
|
|
28
28
|
chalk_remote_call/_version.py
|
|
29
29
|
chalk_remote_call/arrow_utils.py
|
|
30
30
|
chalk_remote_call/cli.py
|
|
31
|
+
chalk_remote_call/errors.py
|
|
31
32
|
chalk_remote_call/handler_loader.py
|
|
32
33
|
chalk_remote_call/input_transform.py
|
|
34
|
+
chalk_remote_call/logging.py
|
|
33
35
|
chalk_remote_call/server.py
|
|
34
36
|
chalk_remote_call/servicer.py
|
|
35
37
|
chalk_remote_call/tracing.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.9.1"
|
|
File without changes
|
{chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/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
|
{chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/__init__.py
RENAMED
|
File without changes
|
{chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/__main__.py
RENAMED
|
File without changes
|
{chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/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.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/_native.pyi
RENAMED
|
File without changes
|
{chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/arrow_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/server.py
RENAMED
|
File without changes
|
{chalk_remote_call_python-1.9.1 → chalk_remote_call_python-1.9.2}/chalk_remote_call/tracing.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
|