chalk-remote-call-python 1.9.2__tar.gz → 1.10.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {chalk_remote_call_python-1.9.2/chalk_remote_call_python.egg-info → chalk_remote_call_python-1.10.1}/PKG-INFO +6 -3
  2. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/README.md +5 -2
  3. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/Cargo.lock +1 -1
  4. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml +1 -1
  5. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-server/src/self_consumer.rs +194 -31
  6. chalk_remote_call_python-1.10.1/chalk_remote_call/_version.py +1 -0
  7. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/cli.py +14 -4
  8. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/server.py +8 -7
  9. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1/chalk_remote_call_python.egg-info}/PKG-INFO +6 -3
  10. chalk_remote_call_python-1.9.2/chalk_remote_call/_version.py +0 -1
  11. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/MANIFEST.in +0 -0
  12. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/Cargo.toml +0 -0
  13. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-proto/Cargo.toml +0 -0
  14. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.auth.v1.rs +0 -0
  15. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.common.v1.rs +0 -0
  16. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.rs +0 -0
  17. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.tonic.rs +0 -0
  18. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.utils.v1.rs +0 -0
  19. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/descriptor.bin +0 -0
  20. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-proto/src/lib.rs +0 -0
  21. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-server/src/async_service.rs +0 -0
  22. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-server/src/coalesce.rs +0 -0
  23. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-server/src/lib.rs +0 -0
  24. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-server/src/metrics.rs +0 -0
  25. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-server/src/python_bridge.rs +0 -0
  26. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-server/src/server.rs +0 -0
  27. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/chalk-remote-call-server/src/service.rs +0 -0
  28. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk-remote-call-rs/rust-toolchain.toml +0 -0
  29. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/__init__.py +0 -0
  30. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/__main__.py +0 -0
  31. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/__init__.py +0 -0
  32. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/__init__.py +0 -0
  33. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
  34. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
  35. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +0 -0
  36. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +0 -0
  37. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
  38. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
  39. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +0 -0
  40. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +0 -0
  41. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
  42. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
  43. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +0 -0
  44. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +0 -0
  45. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
  46. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
  47. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +0 -0
  48. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +0 -0
  49. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +0 -0
  50. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +0 -0
  51. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +0 -0
  52. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +0 -0
  53. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/_native.pyi +0 -0
  54. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/arrow_utils.py +0 -0
  55. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/errors.py +0 -0
  56. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/handler_loader.py +0 -0
  57. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/input_transform.py +0 -0
  58. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/logging.py +0 -0
  59. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/servicer.py +0 -0
  60. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call/tracing.py +0 -0
  61. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call_python.egg-info/SOURCES.txt +0 -0
  62. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call_python.egg-info/dependency_links.txt +0 -0
  63. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call_python.egg-info/entry_points.txt +0 -0
  64. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call_python.egg-info/requires.txt +0 -0
  65. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/chalk_remote_call_python.egg-info/top_level.txt +0 -0
  66. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/pyproject.toml +0 -0
  67. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/setup.cfg +0 -0
  68. {chalk_remote_call_python-1.9.2 → chalk_remote_call_python-1.10.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.9.2
3
+ Version: 1.10.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
@@ -110,8 +110,11 @@ python -m chalk_remote_call --handler my_handler.handler
110
110
 
111
111
  The CLI writes Python logs to stderr as one JSON object per line, with separate
112
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.
113
+ `exception.stacktrace` field, which the log collector uses for the traceback.
114
+ CLI logging is configured before importing runtime dependencies. Import failures,
115
+ invalid environment configuration, and exceptions escaping startup or the server
116
+ emit a single ERROR record with the full traceback and exit with status 1.
117
+ Calls to `serve()` from Python retain the embedding application's logging configuration.
115
118
 
116
119
  Missing optional StatsD endpoints or a metrics-bus topic do not emit warnings.
117
120
  Failures initializing configured metrics sinks still warn. Routine customer
@@ -72,8 +72,11 @@ python -m chalk_remote_call --handler my_handler.handler
72
72
 
73
73
  The CLI writes Python logs to stderr as one JSON object per line, with separate
74
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.
75
+ `exception.stacktrace` field, which the log collector uses for the traceback.
76
+ CLI logging is configured before importing runtime dependencies. Import failures,
77
+ invalid environment configuration, and exceptions escaping startup or the server
78
+ emit a single ERROR record with the full traceback and exit with status 1.
79
+ Calls to `serve()` from Python retain the embedding application's logging configuration.
77
80
 
78
81
  Missing optional StatsD endpoints or a metrics-bus topic do not emit warnings.
79
82
  Failures initializing configured metrics sinks still warn. Routine customer
@@ -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=9c61f6a938d8f885f138217ac64149e29d7a691e#9c61f6a938d8f885f138217ac64149e29d7a691e"
514
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=41458754e6916245a092ddc09562bcaf4dea373e#41458754e6916245a092ddc09562bcaf4dea373e"
515
515
  dependencies = [
516
516
  "base64 0.22.1",
517
517
  "redis",
@@ -37,6 +37,6 @@ futures-util = "0.3"
37
37
  # `[patch.crates-io]` here it builds against vanilla crates.io redis (it only
38
38
  # uses standard APIs). Metrics and StatsD share a revision so their client types agree.
39
39
  [target.'cfg(target_os = "linux")'.dependencies]
40
- chalk-queue = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "9c61f6a938d8f885f138217ac64149e29d7a691e" }
40
+ chalk-queue = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "41458754e6916245a092ddc09562bcaf4dea373e" }
41
41
  chalk_metrics = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "a0f8932ec10d3b8340af3e98cf63b800d60b4926" }
42
42
  chalk_statsd = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "a0f8932ec10d3b8340af3e98cf63b800d60b4926" }
@@ -102,6 +102,136 @@ const STREAM_RECLAIM_INTERVAL: Duration = Duration::from_secs(10);
102
102
  /// inbound network peer on this path.
103
103
  const PEER: &str = "fnq-self-consumer";
104
104
 
105
+ /// Selects where a queued call publishes its result without allowing a shared
106
+ /// batch stream to lose the call identity needed by the collector.
107
+ #[derive(Debug, PartialEq, Eq)]
108
+ enum ResultDestination {
109
+ PerCall {
110
+ stream_key: String,
111
+ result_ttl: Duration,
112
+ },
113
+ Batch {
114
+ stream_key: String,
115
+ call_id: String,
116
+ row_index: u64,
117
+ },
118
+ }
119
+
120
+ impl ResultDestination {
121
+ fn for_call(queue: &FunctionQueue, work: &QueuedCall, call_uuid: Uuid) -> Self {
122
+ let batch = work.batch_result.as_ref().map(|batch_result| {
123
+ (
124
+ queue.batch_stream_key_for(&batch_result.batch_id),
125
+ work.call_id.clone(),
126
+ batch_result.row_index,
127
+ )
128
+ });
129
+ Self::from_routing(
130
+ queue.stream_key_for(call_uuid),
131
+ Duration::from_secs(work.result_ttl_seconds),
132
+ batch,
133
+ )
134
+ }
135
+
136
+ fn from_routing(
137
+ per_call_stream_key: String,
138
+ result_ttl: Duration,
139
+ batch: Option<(String, String, u64)>,
140
+ ) -> Self {
141
+ match batch {
142
+ Some((stream_key, call_id, row_index)) => Self::Batch {
143
+ stream_key,
144
+ call_id,
145
+ row_index,
146
+ },
147
+ None => Self::PerCall {
148
+ stream_key: per_call_stream_key,
149
+ result_ttl,
150
+ },
151
+ }
152
+ }
153
+
154
+ async fn write_chunk(
155
+ &self,
156
+ conn: &mut ConnectionManager,
157
+ seq: u64,
158
+ data: &[u8],
159
+ ) -> Result<(), redis::RedisError> {
160
+ match self {
161
+ Self::PerCall { stream_key, .. } => {
162
+ stream_writer::write_chunk(conn, stream_key, seq, data).await
163
+ }
164
+ Self::Batch {
165
+ stream_key,
166
+ call_id,
167
+ row_index,
168
+ } => {
169
+ stream_writer::BatchStreamWriter::new(stream_key, call_id, *row_index)
170
+ .write_chunk(conn, seq, data)
171
+ .await
172
+ }
173
+ }
174
+ }
175
+
176
+ async fn write_end(
177
+ &self,
178
+ conn: &mut ConnectionManager,
179
+ seq: u64,
180
+ ) -> Result<(), redis::RedisError> {
181
+ match self {
182
+ Self::PerCall {
183
+ stream_key,
184
+ result_ttl,
185
+ } => stream_writer::write_end_with_ttl(conn, stream_key, seq, *result_ttl).await,
186
+ Self::Batch {
187
+ stream_key,
188
+ call_id,
189
+ row_index,
190
+ } => {
191
+ stream_writer::BatchStreamWriter::new(stream_key, call_id, *row_index)
192
+ .write_end(conn, seq)
193
+ .await
194
+ }
195
+ }
196
+ }
197
+
198
+ async fn write_error(
199
+ &self,
200
+ conn: &mut ConnectionManager,
201
+ seq: u64,
202
+ exc_type: &str,
203
+ message: &str,
204
+ traceback: Option<&str>,
205
+ ) -> Result<(), redis::RedisError> {
206
+ match self {
207
+ Self::PerCall {
208
+ stream_key,
209
+ result_ttl,
210
+ } => {
211
+ stream_writer::write_error_with_ttl(
212
+ conn,
213
+ stream_key,
214
+ seq,
215
+ exc_type,
216
+ message,
217
+ traceback,
218
+ *result_ttl,
219
+ )
220
+ .await
221
+ }
222
+ Self::Batch {
223
+ stream_key,
224
+ call_id,
225
+ row_index,
226
+ } => {
227
+ stream_writer::BatchStreamWriter::new(stream_key, call_id, *row_index)
228
+ .write_error(conn, seq, exc_type, message, traceback)
229
+ .await
230
+ }
231
+ }
232
+ }
233
+ }
234
+
105
235
  /// Configuration for the self-consumer, read from the environment.
106
236
  ///
107
237
  /// [`from_env`](Self::from_env) returns `None` when `CHALK_FNQ_REDIS_URL` is
@@ -577,7 +707,7 @@ async fn run_one_call(
577
707
  {
578
708
  Ok(Ok(())) => true,
579
709
  Ok(Err(e)) => {
580
- // The call's result stream may be incomplete; the call's 24h stream TTL
710
+ // The call's result stream may be incomplete; the enqueue safety TTL
581
711
  // and the meta status are the backstop for a stuck poller.
582
712
  error!(call_id = %call_id, error = %e, "failed to execute/write queued call");
583
713
  false
@@ -663,8 +793,8 @@ async fn run_one_coalesced_call(
663
793
  {
664
794
  Ok(()) => true,
665
795
  Err(e) => {
666
- // The call's result stream may be incomplete; the 24h stream TTL and
667
- // the meta status are the backstop for a stuck poller.
796
+ // The call's result stream may be incomplete; the enqueue safety TTL
797
+ // and the meta status are the backstop for a stuck poller.
668
798
  error!(call_id = %call_id, error = %e, "failed to execute/write coalesced queued call");
669
799
  false
670
800
  }
@@ -694,7 +824,7 @@ async fn coalesced_execute_and_write(
694
824
  metrics_pipeline: Option<&Arc<PublishingMetricsPipeline>>,
695
825
  ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
696
826
  let call_uuid = Uuid::parse_str(&work.call_id)?;
697
- let stream_key = queue.stream_key_for(call_uuid);
827
+ let result_destination = ResultDestination::for_call(queue, &work, call_uuid);
698
828
  let ipc_bytes = work.call.feather_bytes()?;
699
829
  let function_name = work.call.name.clone();
700
830
  let mut metadata = work.metadata.otel_headers.clone();
@@ -736,8 +866,10 @@ async fn coalesced_execute_and_write(
736
866
 
737
867
  match response {
738
868
  Some(Ok(resp)) => {
739
- stream_writer::write_chunk(conn, &stream_key, 1, &resp.feather_stream).await?;
740
- stream_writer::write_end(conn, &stream_key, 2).await?;
869
+ result_destination
870
+ .write_chunk(conn, 1, &resp.feather_stream)
871
+ .await?;
872
+ result_destination.write_end(conn, 2).await?;
741
873
  let _ = queue
742
874
  .complete_status(
743
875
  call_uuid,
@@ -751,15 +883,9 @@ async fn coalesced_execute_and_write(
751
883
  Some(Err(status)) => {
752
884
  // Parity with the enqueue/poll path: only a message string is
753
885
  // available, so exc_type is a fixed placeholder.
754
- stream_writer::write_error(
755
- conn,
756
- &stream_key,
757
- 1,
758
- "RemoteFunctionError",
759
- status.message(),
760
- None,
761
- )
762
- .await?;
886
+ result_destination
887
+ .write_error(conn, 1, "RemoteFunctionError", status.message(), None)
888
+ .await?;
763
889
  let summary: String = status.message().chars().take(200).collect();
764
890
  let _ = queue
765
891
  .complete_status(call_uuid, "failed", &summary, Some(processing_trace_id))
@@ -771,7 +897,8 @@ async fn coalesced_execute_and_write(
771
897
  // happen (it always sends one Ok/Err per caller), but treat a
772
898
  // missing response as a failure rather than leaving the poller hung.
773
899
  let msg = "coalesced dispatch produced no response";
774
- stream_writer::write_error(conn, &stream_key, 1, "RemoteFunctionError", msg, None)
900
+ result_destination
901
+ .write_error(conn, 1, "RemoteFunctionError", msg, None)
775
902
  .await?;
776
903
  let _ = queue
777
904
  .complete_status(call_uuid, "failed", msg, Some(processing_trace_id))
@@ -810,7 +937,7 @@ async fn execute_and_write(
810
937
  metrics_pipeline: Option<&Arc<PublishingMetricsPipeline>>,
811
938
  ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
812
939
  let call_uuid = Uuid::parse_str(&work.call_id)?;
813
- let stream_key = queue.stream_key_for(call_uuid);
940
+ let result_destination = ResultDestination::for_call(queue, &work, call_uuid);
814
941
  let ipc_bytes = work.call.feather_bytes()?;
815
942
  let function_name = work.call.name.clone();
816
943
  let mut metadata = work.metadata.otel_headers.clone();
@@ -857,7 +984,7 @@ async fn execute_and_write(
857
984
  biased;
858
985
  res = &mut handler_fut => break res,
859
986
  _ = tokio::time::timeout(flush_timeout, notify_handle.notified()) => {
860
- if let Err(e) = flush_new(conn, &stream_key, &buffer, &mut flushed, &mut seq).await {
987
+ if let Err(e) = flush_new(conn, &result_destination, &buffer, &mut flushed, &mut seq).await {
861
988
  // Non-fatal: keep draining; the cursor didn't advance past the
862
989
  // failed chunk, so the next wait retries it without duplicating.
863
990
  warn!(call_id = %work.call_id, error = %e, "failed to flush partial results; retrying");
@@ -883,13 +1010,13 @@ async fn execute_and_write(
883
1010
  // this is everything). This happens even if the handler panicked mid-execution,
884
1011
  // so partial output is always preserved. Parity with the enqueue/poll path: a
885
1012
  // generator that yields then raises surfaces its chunks followed by the error.
886
- if let Err(e) = flush_new(conn, &stream_key, &buffer, &mut flushed, &mut seq).await {
1013
+ if let Err(e) = flush_new(conn, &result_destination, &buffer, &mut flushed, &mut seq).await {
887
1014
  warn!(call_id = %work.call_id, error = %e, "failed to flush final chunks");
888
1015
  }
889
1016
 
890
1017
  match result {
891
1018
  Ok(()) => {
892
- stream_writer::write_end(conn, &stream_key, seq).await?;
1019
+ result_destination.write_end(conn, seq).await?;
893
1020
  let _ = queue
894
1021
  .complete_status(
895
1022
  call_uuid,
@@ -901,15 +1028,15 @@ async fn execute_and_write(
901
1028
  info!(call_id = %work.call_id, chunks = flushed, "queued call completed");
902
1029
  }
903
1030
  Err(e) => {
904
- stream_writer::write_error(
905
- conn,
906
- &stream_key,
907
- seq,
908
- "RemoteFunctionError",
909
- &e.to_string(),
910
- Some(e.details()),
911
- )
912
- .await?;
1031
+ result_destination
1032
+ .write_error(
1033
+ conn,
1034
+ seq,
1035
+ "RemoteFunctionError",
1036
+ &e.to_string(),
1037
+ Some(e.details()),
1038
+ )
1039
+ .await?;
913
1040
  let summary: String = e.to_string().chars().take(200).collect();
914
1041
  let _ = queue
915
1042
  .complete_status(call_uuid, "failed", &summary, Some(processing_trace_id))
@@ -932,7 +1059,7 @@ async fn execute_and_write(
932
1059
  /// failed chunk, so a retry re-sends only the unwritten tail (no gaps, no dupes).
933
1060
  async fn flush_new(
934
1061
  conn: &mut ConnectionManager,
935
- stream_key: &str,
1062
+ result_destination: &ResultDestination,
936
1063
  buffer: &Arc<Mutex<ChunkBuffer>>,
937
1064
  flushed: &mut usize,
938
1065
  seq: &mut u64,
@@ -947,7 +1074,7 @@ async fn flush_new(
947
1074
  .collect()
948
1075
  };
949
1076
  for chunk in &new {
950
- stream_writer::write_chunk(conn, stream_key, *seq, chunk).await?;
1077
+ result_destination.write_chunk(conn, *seq, chunk).await?;
951
1078
  *seq += 1;
952
1079
  *flushed += 1;
953
1080
  }
@@ -1022,6 +1149,41 @@ mod tests {
1022
1149
  assert_eq!(DEFAULT_FLUSH_TIMEOUT_MS, 100);
1023
1150
  }
1024
1151
 
1152
+ #[test]
1153
+ fn result_destination_preserves_per_call_stream() {
1154
+ assert_eq!(
1155
+ ResultDestination::from_routing(
1156
+ "tenant:gen:call-1:stream".to_string(),
1157
+ Duration::from_secs(60),
1158
+ None,
1159
+ ),
1160
+ ResultDestination::PerCall {
1161
+ stream_key: "tenant:gen:call-1:stream".to_string(),
1162
+ result_ttl: Duration::from_secs(60),
1163
+ }
1164
+ );
1165
+ }
1166
+
1167
+ #[test]
1168
+ fn result_destination_routes_batch_call_with_correlation() {
1169
+ assert_eq!(
1170
+ ResultDestination::from_routing(
1171
+ "unused-per-call-stream".to_string(),
1172
+ Duration::from_secs(60),
1173
+ Some((
1174
+ "tenant:batch:batch-1:results".to_string(),
1175
+ "call-1".to_string(),
1176
+ 42,
1177
+ )),
1178
+ ),
1179
+ ResultDestination::Batch {
1180
+ stream_key: "tenant:batch:batch-1:results".to_string(),
1181
+ call_id: "call-1".to_string(),
1182
+ row_index: 42,
1183
+ }
1184
+ );
1185
+ }
1186
+
1025
1187
  #[test]
1026
1188
  fn test_config_from_env_returns_none_when_redis_url_unset() {
1027
1189
  let _env = lock_env();
@@ -1043,6 +1205,7 @@ mod tests {
1043
1205
  std::env::remove_var(ENV_FLUSH_TIMEOUT_MS);
1044
1206
  std::env::remove_var(ENV_QUEUE_PROTOCOL);
1045
1207
  std::env::remove_var(ENV_STREAM_READ_COUNT);
1208
+ std::env::remove_var(ENV_MAX_CONCURRENT);
1046
1209
 
1047
1210
  let config = SelfConsumerConfig::from_env().expect("config should parse");
1048
1211
  assert_eq!(config.sem_ttl, Duration::from_secs(DEFAULT_SEM_TTL_SECS));
@@ -0,0 +1 @@
1
+ __version__ = "1.10.1"
@@ -6,7 +6,6 @@ import os
6
6
  import sys
7
7
 
8
8
  from chalk_remote_call.handler_loader import load_function, load_handler
9
- from chalk_remote_call.input_transform import parse_input_args
10
9
  from chalk_remote_call.logging import JsonFormatter
11
10
  from chalk_remote_call.server import serve
12
11
 
@@ -23,6 +22,17 @@ def _env_int(name: str) -> int | None:
23
22
 
24
23
 
25
24
  def main() -> None:
25
+ log_handler = logging.StreamHandler(sys.stderr)
26
+ log_handler.setFormatter(JsonFormatter())
27
+ logging.basicConfig(level=logging.INFO, handlers=[log_handler], force=True)
28
+ try:
29
+ _run()
30
+ except Exception:
31
+ logging.getLogger(__name__).exception("Remote-call server failed")
32
+ sys.exit(1)
33
+
34
+
35
+ def _run() -> None:
26
36
  parser = argparse.ArgumentParser(
27
37
  prog="chalk-remote-call",
28
38
  description="Start a gRPC server implementing the Chalk RemoteCallService.",
@@ -100,9 +110,9 @@ def main() -> None:
100
110
  )
101
111
  args = parser.parse_args()
102
112
 
103
- log_handler = logging.StreamHandler(sys.stderr)
104
- log_handler.setFormatter(JsonFormatter())
105
- logging.basicConfig(level=getattr(logging, args.log_level), handlers=[log_handler])
113
+ logging.getLogger().setLevel(args.log_level)
114
+
115
+ from chalk_remote_call.input_transform import parse_input_args
106
116
 
107
117
  # Load handler
108
118
  try:
@@ -5,13 +5,6 @@ import os
5
5
  from collections.abc import Callable
6
6
  from typing import Any, Literal
7
7
 
8
- from chalk_remote_call._native import start_server
9
- from chalk_remote_call.servicer import (
10
- process_batches,
11
- process_batches_coalesced,
12
- process_batches_coalesced_combined,
13
- )
14
-
15
8
  logger = logging.getLogger(__name__)
16
9
 
17
10
  _BATCHING_MODE_ENV = "CHALK_REMOTE_CALL_BATCHING_MODE"
@@ -75,6 +68,14 @@ def serve(
75
68
  CHALK_REMOTE_CALL_BATCHING_MODE env var when set; otherwise the
76
69
  env var (or "per_caller") wins.
77
70
  """
71
+ # Keep package imports lightweight so the CLI can configure logging before dependency failures.
72
+ from chalk_remote_call._native import start_server
73
+ from chalk_remote_call.servicer import (
74
+ process_batches,
75
+ process_batches_coalesced,
76
+ process_batches_coalesced_combined,
77
+ )
78
+
78
79
  if on_startup is not None:
79
80
  logger.info("Running startup hook...")
80
81
  on_startup()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.9.2
3
+ Version: 1.10.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
@@ -110,8 +110,11 @@ python -m chalk_remote_call --handler my_handler.handler
110
110
 
111
111
  The CLI writes Python logs to stderr as one JSON object per line, with separate
112
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.
113
+ `exception.stacktrace` field, which the log collector uses for the traceback.
114
+ CLI logging is configured before importing runtime dependencies. Import failures,
115
+ invalid environment configuration, and exceptions escaping startup or the server
116
+ emit a single ERROR record with the full traceback and exit with status 1.
117
+ Calls to `serve()` from Python retain the embedding application's logging configuration.
115
118
 
116
119
  Missing optional StatsD endpoints or a metrics-bus topic do not emit warnings.
117
120
  Failures initializing configured metrics sinks still warn. Routine customer
@@ -1 +0,0 @@
1
- __version__ = "1.9.2"