chalk-remote-call-python 1.10.0__tar.gz → 1.10.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.
Files changed (68) hide show
  1. {chalk_remote_call_python-1.10.0/chalk_remote_call_python.egg-info → chalk_remote_call_python-1.10.2}/PKG-INFO +1 -1
  2. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/Cargo.lock +1 -1
  3. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml +1 -1
  4. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/self_consumer.rs +233 -30
  5. chalk_remote_call_python-1.10.2/chalk_remote_call/_version.py +1 -0
  6. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2/chalk_remote_call_python.egg-info}/PKG-INFO +1 -1
  7. chalk_remote_call_python-1.10.0/chalk_remote_call/_version.py +0 -1
  8. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/MANIFEST.in +0 -0
  9. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/README.md +0 -0
  10. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/Cargo.toml +0 -0
  11. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/Cargo.toml +0 -0
  12. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.auth.v1.rs +0 -0
  13. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.common.v1.rs +0 -0
  14. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.rs +0 -0
  15. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.tonic.rs +0 -0
  16. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.utils.v1.rs +0 -0
  17. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/descriptor.bin +0 -0
  18. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-proto/src/lib.rs +0 -0
  19. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/async_service.rs +0 -0
  20. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/coalesce.rs +0 -0
  21. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/lib.rs +0 -0
  22. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/metrics.rs +0 -0
  23. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/python_bridge.rs +0 -0
  24. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/server.rs +0 -0
  25. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/chalk-remote-call-server/src/service.rs +0 -0
  26. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk-remote-call-rs/rust-toolchain.toml +0 -0
  27. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/__init__.py +0 -0
  28. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/__main__.py +0 -0
  29. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/__init__.py +0 -0
  30. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/__init__.py +0 -0
  31. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
  32. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
  33. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +0 -0
  34. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +0 -0
  35. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
  36. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
  37. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +0 -0
  38. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +0 -0
  39. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
  40. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
  41. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +0 -0
  42. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +0 -0
  43. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
  44. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
  45. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +0 -0
  46. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +0 -0
  47. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +0 -0
  48. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +0 -0
  49. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +0 -0
  50. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +0 -0
  51. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/_native.pyi +0 -0
  52. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/arrow_utils.py +0 -0
  53. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/cli.py +0 -0
  54. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/errors.py +0 -0
  55. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/handler_loader.py +0 -0
  56. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/input_transform.py +0 -0
  57. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/logging.py +0 -0
  58. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/server.py +0 -0
  59. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/servicer.py +0 -0
  60. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call/tracing.py +0 -0
  61. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/SOURCES.txt +0 -0
  62. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/dependency_links.txt +0 -0
  63. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/entry_points.txt +0 -0
  64. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/requires.txt +0 -0
  65. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/chalk_remote_call_python.egg-info/top_level.txt +0 -0
  66. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/pyproject.toml +0 -0
  67. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.2}/setup.cfg +0 -0
  68. {chalk_remote_call_python-1.10.0 → chalk_remote_call_python-1.10.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.10.0
3
+ Version: 1.10.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
@@ -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=c6eed6bd7bdad1f2a7568ef206e570c719c4c64f#c6eed6bd7bdad1f2a7568ef206e570c719c4c64f"
514
+ source = "git+https://github.com/chalk-ai/chalk-private.git?rev=1460144ea659debbaee9dccadcd4d7dbf383ccf8#1460144ea659debbaee9dccadcd4d7dbf383ccf8"
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 = "c6eed6bd7bdad1f2a7568ef206e570c719c4c64f" }
40
+ chalk-queue = { git = "https://github.com/chalk-ai/chalk-private.git", rev = "1460144ea659debbaee9dccadcd4d7dbf383ccf8" }
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" }
@@ -39,7 +39,7 @@ use std::time::{Duration, Instant};
39
39
 
40
40
  use chalk_queue::semaphore::renew_many;
41
41
  use chalk_queue::stream_writer;
42
- use chalk_queue::types::QueuedCall;
42
+ use chalk_queue::types::{DeliveryAttempt, QueuedCall};
43
43
  use chalk_queue::{
44
44
  BoundFunctionQueue, DequeuedCall, FunctionQueue, QueueProtocol, QueueReceipt, SemaphoreLease,
45
45
  };
@@ -108,6 +108,7 @@ const PEER: &str = "fnq-self-consumer";
108
108
  enum ResultDestination {
109
109
  PerCall {
110
110
  stream_key: String,
111
+ result_ttl: Duration,
111
112
  },
112
113
  Batch {
113
114
  stream_key: String,
@@ -125,10 +126,18 @@ impl ResultDestination {
125
126
  batch_result.row_index,
126
127
  )
127
128
  });
128
- Self::from_routing(queue.stream_key_for(call_uuid), batch)
129
+ Self::from_routing(
130
+ queue.stream_key_for(call_uuid),
131
+ Duration::from_secs(work.result_ttl_seconds),
132
+ batch,
133
+ )
129
134
  }
130
135
 
131
- fn from_routing(per_call_stream_key: String, batch: Option<(String, String, u64)>) -> Self {
136
+ fn from_routing(
137
+ per_call_stream_key: String,
138
+ result_ttl: Duration,
139
+ batch: Option<(String, String, u64)>,
140
+ ) -> Self {
132
141
  match batch {
133
142
  Some((stream_key, call_id, row_index)) => Self::Batch {
134
143
  stream_key,
@@ -137,6 +146,7 @@ impl ResultDestination {
137
146
  },
138
147
  None => Self::PerCall {
139
148
  stream_key: per_call_stream_key,
149
+ result_ttl,
140
150
  },
141
151
  }
142
152
  }
@@ -148,7 +158,7 @@ impl ResultDestination {
148
158
  data: &[u8],
149
159
  ) -> Result<(), redis::RedisError> {
150
160
  match self {
151
- Self::PerCall { stream_key } => {
161
+ Self::PerCall { stream_key, .. } => {
152
162
  stream_writer::write_chunk(conn, stream_key, seq, data).await
153
163
  }
154
164
  Self::Batch {
@@ -169,7 +179,10 @@ impl ResultDestination {
169
179
  seq: u64,
170
180
  ) -> Result<(), redis::RedisError> {
171
181
  match self {
172
- Self::PerCall { stream_key } => stream_writer::write_end(conn, stream_key, seq).await,
182
+ Self::PerCall {
183
+ stream_key,
184
+ result_ttl,
185
+ } => stream_writer::write_end_with_ttl(conn, stream_key, seq, *result_ttl).await,
173
186
  Self::Batch {
174
187
  stream_key,
175
188
  call_id,
@@ -191,9 +204,20 @@ impl ResultDestination {
191
204
  traceback: Option<&str>,
192
205
  ) -> Result<(), redis::RedisError> {
193
206
  match self {
194
- Self::PerCall { stream_key } => {
195
- stream_writer::write_error(conn, stream_key, seq, exc_type, message, traceback)
196
- .await
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
197
221
  }
198
222
  Self::Batch {
199
223
  stream_key,
@@ -665,25 +689,83 @@ async fn run_one_call(
665
689
  flush_timeout: Duration,
666
690
  metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
667
691
  ) {
692
+ let attempt = match input_queue.delivery_attempt(&delivery).await {
693
+ Ok(attempt) => attempt,
694
+ Err(error) => {
695
+ error!(call_id = %delivery.call.call_id, %error, "failed to resolve delivery attempt");
696
+ finish_delivery(
697
+ &input_queue,
698
+ &delivery.receipt,
699
+ false,
700
+ &delivery.call.call_id,
701
+ lease,
702
+ &live_calls,
703
+ )
704
+ .await;
705
+ return;
706
+ }
707
+ };
668
708
  let DequeuedCall {
669
709
  call: work,
670
710
  receipt,
671
711
  } = delivery;
672
712
  let call_id = work.call_id.clone();
713
+ let final_attempt = match attempt {
714
+ DeliveryAttempt::Ready {
715
+ attempt,
716
+ max_attempts,
717
+ } => {
718
+ info!(%call_id, attempt, max_attempts, "executing queued call attempt");
719
+ attempt == max_attempts
720
+ }
721
+ DeliveryAttempt::Exhausted {
722
+ attempts,
723
+ max_attempts,
724
+ } => {
725
+ let message = format!(
726
+ "retry attempts exhausted after {attempts} deliveries ({max_attempts} executions allowed)"
727
+ );
728
+ let completed = write_terminal_error(
729
+ &queue,
730
+ &mut conn,
731
+ &work,
732
+ "RetryAttemptsExhausted",
733
+ &message,
734
+ None,
735
+ )
736
+ .await
737
+ .is_ok();
738
+ finish_delivery(
739
+ &input_queue,
740
+ &receipt,
741
+ completed,
742
+ &call_id,
743
+ lease,
744
+ &live_calls,
745
+ )
746
+ .await;
747
+ return;
748
+ }
749
+ DeliveryAttempt::OwnershipLost => {
750
+ finish_delivery(&input_queue, &receipt, false, &call_id, lease, &live_calls).await;
751
+ return;
752
+ }
753
+ };
673
754
  let completed = match AssertUnwindSafe(execute_and_write(
674
755
  &handler,
675
756
  &queue,
676
757
  &mut conn,
677
- work,
758
+ work.clone(),
678
759
  flush_timeout,
760
+ final_attempt,
679
761
  metrics_pipeline.as_ref(),
680
762
  ))
681
763
  .catch_unwind()
682
764
  .await
683
765
  {
684
- Ok(Ok(())) => true,
766
+ Ok(Ok(completed)) => completed,
685
767
  Ok(Err(e)) => {
686
- // The call's result stream may be incomplete; the call's 24h stream TTL
768
+ // The call's result stream may be incomplete; the enqueue safety TTL
687
769
  // and the meta status are the backstop for a stuck poller.
688
770
  error!(call_id = %call_id, error = %e, "failed to execute/write queued call");
689
771
  false
@@ -699,7 +781,20 @@ async fn run_one_call(
699
781
  panic = %panic_message,
700
782
  "panic while executing/writing queued call"
701
783
  );
702
- false
784
+ if final_attempt {
785
+ write_terminal_error(
786
+ &queue,
787
+ &mut conn,
788
+ &work,
789
+ "RemoteFunctionPanic",
790
+ panic_message,
791
+ None,
792
+ )
793
+ .await
794
+ .is_ok()
795
+ } else {
796
+ false
797
+ }
703
798
  }
704
799
  };
705
800
 
@@ -753,24 +848,82 @@ async fn run_one_coalesced_call(
753
848
  live_calls: LiveCalls,
754
849
  metrics_pipeline: Option<Arc<PublishingMetricsPipeline>>,
755
850
  ) {
851
+ let attempt = match input_queue.delivery_attempt(&delivery).await {
852
+ Ok(attempt) => attempt,
853
+ Err(error) => {
854
+ error!(call_id = %delivery.call.call_id, %error, "failed to resolve delivery attempt");
855
+ finish_delivery(
856
+ &input_queue,
857
+ &delivery.receipt,
858
+ false,
859
+ &delivery.call.call_id,
860
+ lease,
861
+ &live_calls,
862
+ )
863
+ .await;
864
+ return;
865
+ }
866
+ };
756
867
  let DequeuedCall {
757
868
  call: work,
758
869
  receipt,
759
870
  } = delivery;
760
871
  let call_id = work.call_id.clone();
872
+ let final_attempt = match attempt {
873
+ DeliveryAttempt::Ready {
874
+ attempt,
875
+ max_attempts,
876
+ } => {
877
+ info!(%call_id, attempt, max_attempts, "executing coalesced queued call attempt");
878
+ attempt == max_attempts
879
+ }
880
+ DeliveryAttempt::Exhausted {
881
+ attempts,
882
+ max_attempts,
883
+ } => {
884
+ let message = format!(
885
+ "retry attempts exhausted after {attempts} deliveries ({max_attempts} executions allowed)"
886
+ );
887
+ let completed = write_terminal_error(
888
+ &queue,
889
+ &mut conn,
890
+ &work,
891
+ "RetryAttemptsExhausted",
892
+ &message,
893
+ None,
894
+ )
895
+ .await
896
+ .is_ok();
897
+ finish_delivery(
898
+ &input_queue,
899
+ &receipt,
900
+ completed,
901
+ &call_id,
902
+ lease,
903
+ &live_calls,
904
+ )
905
+ .await;
906
+ return;
907
+ }
908
+ DeliveryAttempt::OwnershipLost => {
909
+ finish_delivery(&input_queue, &receipt, false, &call_id, lease, &live_calls).await;
910
+ return;
911
+ }
912
+ };
761
913
  let completed = match coalesced_execute_and_write(
762
914
  &coalescing_queue,
763
915
  &queue,
764
916
  &mut conn,
765
917
  work,
918
+ final_attempt,
766
919
  metrics_pipeline.as_ref(),
767
920
  )
768
921
  .await
769
922
  {
770
- Ok(()) => true,
923
+ Ok(completed) => completed,
771
924
  Err(e) => {
772
- // The call's result stream may be incomplete; the 24h stream TTL and
773
- // the meta status are the backstop for a stuck poller.
925
+ // The call's result stream may be incomplete; the enqueue safety TTL
926
+ // and the meta status are the backstop for a stuck poller.
774
927
  error!(call_id = %call_id, error = %e, "failed to execute/write coalesced queued call");
775
928
  false
776
929
  }
@@ -797,8 +950,9 @@ async fn coalesced_execute_and_write(
797
950
  queue: &FunctionQueue,
798
951
  conn: &mut ConnectionManager,
799
952
  work: QueuedCall,
953
+ final_attempt: bool,
800
954
  metrics_pipeline: Option<&Arc<PublishingMetricsPipeline>>,
801
- ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
955
+ ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
802
956
  let call_uuid = Uuid::parse_str(&work.call_id)?;
803
957
  let result_destination = ResultDestination::for_call(queue, &work, call_uuid);
804
958
  let ipc_bytes = work.call.feather_bytes()?;
@@ -857,6 +1011,10 @@ async fn coalesced_execute_and_write(
857
1011
  info!(call_id = %work.call_id, "coalesced queued call completed");
858
1012
  }
859
1013
  Some(Err(status)) => {
1014
+ if !final_attempt {
1015
+ warn!(call_id = %work.call_id, error = %status, "coalesced queued call failed; leaving delivery pending for retry");
1016
+ return Ok(false);
1017
+ }
860
1018
  // Parity with the enqueue/poll path: only a message string is
861
1019
  // available, so exc_type is a fixed placeholder.
862
1020
  result_destination
@@ -873,6 +1031,10 @@ async fn coalesced_execute_and_write(
873
1031
  // happen (it always sends one Ok/Err per caller), but treat a
874
1032
  // missing response as a failure rather than leaving the poller hung.
875
1033
  let msg = "coalesced dispatch produced no response";
1034
+ if !final_attempt {
1035
+ warn!(call_id = %work.call_id, "{msg}; leaving delivery pending for retry");
1036
+ return Ok(false);
1037
+ }
876
1038
  result_destination
877
1039
  .write_error(conn, 1, "RemoteFunctionError", msg, None)
878
1040
  .await?;
@@ -882,7 +1044,7 @@ async fn coalesced_execute_and_write(
882
1044
  error!(call_id = %work.call_id, "{msg}");
883
1045
  }
884
1046
  }
885
- Ok(())
1047
+ Ok(true)
886
1048
  }
887
1049
 
888
1050
  // Insert trace ID into headers if none already present
@@ -910,8 +1072,9 @@ async fn execute_and_write(
910
1072
  conn: &mut ConnectionManager,
911
1073
  work: QueuedCall,
912
1074
  flush_timeout: Duration,
1075
+ final_attempt: bool,
913
1076
  metrics_pipeline: Option<&Arc<PublishingMetricsPipeline>>,
914
- ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1077
+ ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
915
1078
  let call_uuid = Uuid::parse_str(&work.call_id)?;
916
1079
  let result_destination = ResultDestination::for_call(queue, &work, call_uuid);
917
1080
  let ipc_bytes = work.call.feather_bytes()?;
@@ -923,13 +1086,9 @@ async fn execute_and_write(
923
1086
  let _ = queue.update_status(call_uuid, "running").await;
924
1087
  info!(call_id = %work.call_id, function = %function_name, "running queued call");
925
1088
 
926
- // The handler appends each emitted chunk into `buffer` in real time — the
927
- // Python bridge calls the emitter per generator-yield. Stream those chunks
928
- // to the result stream *as they appear* rather than holding them until the
929
- // handler returns: we wait on the notify (signaled on each append) and flush
930
- // immediately when chunks arrive, so a slow generator's early yields reach
931
- // the client without waiting for the whole call to finish. A timeout fallback
932
- // ensures we make progress if the notification pipeline breaks.
1089
+ // The Python bridge appends each generator yield into `buffer`. Calls
1090
+ // without retries flush incrementally; retry-enabled calls retain chunks
1091
+ // until the attempt succeeds so a later attempt cannot duplicate output.
933
1092
  let buffer = ChunkBuffer::new().into_shared();
934
1093
  let handler_fut = handler.call_into_buffer(
935
1094
  ipc_bytes,
@@ -944,6 +1103,9 @@ async fn execute_and_write(
944
1103
  // seq 0. `flushed` is the count of chunks already written to the stream.
945
1104
  let mut seq: u64 = 1;
946
1105
  let mut flushed: usize = 0;
1106
+ // A retry must not duplicate generator chunks. Retry-enabled calls buffer
1107
+ // output until the handler succeeds or reaches its final attempt.
1108
+ let buffer_until_terminal = work.max_retries.is_some();
947
1109
 
948
1110
  // Completion is driven by the handler future resolving, NOT by the buffer
949
1111
  // emitter being torn down — so a handler that pins its emitter (e.g. via a
@@ -960,10 +1122,12 @@ async fn execute_and_write(
960
1122
  biased;
961
1123
  res = &mut handler_fut => break res,
962
1124
  _ = tokio::time::timeout(flush_timeout, notify_handle.notified()) => {
963
- if let Err(e) = flush_new(conn, &result_destination, &buffer, &mut flushed, &mut seq).await {
964
- // Non-fatal: keep draining; the cursor didn't advance past the
965
- // failed chunk, so the next wait retries it without duplicating.
966
- warn!(call_id = %work.call_id, error = %e, "failed to flush partial results; retrying");
1125
+ if !buffer_until_terminal {
1126
+ if let Err(e) = flush_new(conn, &result_destination, &buffer, &mut flushed, &mut seq).await {
1127
+ // Non-fatal: keep draining; the cursor didn't advance past the
1128
+ // failed chunk, so the next wait retries it without duplicating.
1129
+ warn!(call_id = %work.call_id, error = %e, "failed to flush partial results; retrying");
1130
+ }
967
1131
  }
968
1132
  }
969
1133
  }
@@ -982,6 +1146,11 @@ async fn execute_and_write(
982
1146
  );
983
1147
  }
984
1148
 
1149
+ if result.is_err() && !final_attempt {
1150
+ warn!(call_id = %work.call_id, "queued call failed; leaving delivery pending for retry");
1151
+ return Ok(false);
1152
+ }
1153
+
985
1154
  // Flush whatever the handler produced after the loop exits (on the fast path
986
1155
  // this is everything). This happens even if the handler panicked mid-execution,
987
1156
  // so partial output is always preserved. Parity with the enqueue/poll path: a
@@ -1025,6 +1194,34 @@ async fn execute_and_write(
1025
1194
  );
1026
1195
  }
1027
1196
  }
1197
+ Ok(true)
1198
+ }
1199
+
1200
+ async fn write_terminal_error(
1201
+ queue: &FunctionQueue,
1202
+ conn: &mut ConnectionManager,
1203
+ work: &QueuedCall,
1204
+ exc_type: &str,
1205
+ message: &str,
1206
+ traceback: Option<&str>,
1207
+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1208
+ let call_uuid = Uuid::parse_str(&work.call_id)?;
1209
+ let result_destination = ResultDestination::for_call(queue, work, call_uuid);
1210
+ result_destination
1211
+ .write_error(conn, 1, exc_type, message, traceback)
1212
+ .await?;
1213
+ let summary: String = message.chars().take(200).collect();
1214
+ let processing_trace_id = work
1215
+ .metadata
1216
+ .otel_headers
1217
+ .get("traceparent")
1218
+ .and_then(|value| value.split('-').nth(1))
1219
+ .filter(|value| value.len() == 32)
1220
+ .map(str::to_string);
1221
+ let _ = queue
1222
+ .complete_status(call_uuid, "failed", &summary, processing_trace_id)
1223
+ .await;
1224
+ error!(call_id = %work.call_id, %message, "queued call reached terminal failure");
1028
1225
  Ok(())
1029
1226
  }
1030
1227
 
@@ -1128,9 +1325,14 @@ mod tests {
1128
1325
  #[test]
1129
1326
  fn result_destination_preserves_per_call_stream() {
1130
1327
  assert_eq!(
1131
- ResultDestination::from_routing("tenant:gen:call-1:stream".to_string(), None),
1328
+ ResultDestination::from_routing(
1329
+ "tenant:gen:call-1:stream".to_string(),
1330
+ Duration::from_secs(60),
1331
+ None,
1332
+ ),
1132
1333
  ResultDestination::PerCall {
1133
1334
  stream_key: "tenant:gen:call-1:stream".to_string(),
1335
+ result_ttl: Duration::from_secs(60),
1134
1336
  }
1135
1337
  );
1136
1338
  }
@@ -1140,6 +1342,7 @@ mod tests {
1140
1342
  assert_eq!(
1141
1343
  ResultDestination::from_routing(
1142
1344
  "unused-per-call-stream".to_string(),
1345
+ Duration::from_secs(60),
1143
1346
  Some((
1144
1347
  "tenant:batch:batch-1:results".to_string(),
1145
1348
  "call-1".to_string(),
@@ -0,0 +1 @@
1
+ __version__ = "1.10.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.10.0
3
+ Version: 1.10.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
@@ -1 +0,0 @@
1
- __version__ = "1.10.0"