This is an automated email from the ASF dual-hosted git repository.

numinnex pushed a commit to branch connectors_sink_delivery
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/connectors_sink_delivery by 
this push:
     new bd4dc3eec address review
bd4dc3eec is described below

commit bd4dc3eecd19f22f6a90974e2862592095974d1d
Author: Grzegorz Koszyk <[email protected]>
AuthorDate: Wed Nov 4 16:16:15 2026 +0100

    address review
---
 core/connectors/runtime/src/sink.rs        |  7 ++--
 core/connectors/sinks/http_sink/README.md  |  1 +
 core/connectors/sinks/http_sink/src/lib.rs | 55 +++++++++++++++++++++++-------
 3 files changed, 47 insertions(+), 16 deletions(-)

diff --git a/core/connectors/runtime/src/sink.rs 
b/core/connectors/runtime/src/sink.rs
index 60177fa35..e6033e382 100644
--- a/core/connectors/runtime/src/sink.rs
+++ b/core/connectors/runtime/src/sink.rs
@@ -364,8 +364,9 @@ pub(crate) fn spawn_consume_tasks(
                     .set_error(&plugin_key, &error.to_string(), Some(&metrics))
                     .await;
             }
-            // The polls also committed the batches the consumer still holds,
-            // and only a shutdown moves the offsets back over them.
+            // At most once, the polls also committed the batches the consumer
+            // still holds, and only a shutdown moves the offsets back over 
them.
+            // At least once, the polls store no offset.
             if let Err(error) = consumer.shutdown().await {
                 error!(
                     "Failed to shut down the consumer of sink connector with 
ID: {plugin_id}: {error}"
@@ -1026,7 +1027,7 @@ pub(crate) async fn process_messages(
                 Ok(next) => current_message = next,
                 Err(error) => {
                     error!(
-                        "Transform '{:?}' failed for sink connector with ID: 
{plugin_id}, stream: {}, topic: {}: {error}",
+                        "Transform '{:?}' failed for sink connector with ID: 
{plugin_id}, stream: {}, topic: {}, offset: {message_offset}: {error}",
                         transform.r#type(),
                         topic_metadata.stream,
                         topic_metadata.topic
diff --git a/core/connectors/sinks/http_sink/README.md 
b/core/connectors/sinks/http_sink/README.md
index b15c1142d..2570bf852 100644
--- a/core/connectors/sinks/http_sink/README.md
+++ b/core/connectors/sinks/http_sink/README.md
@@ -273,6 +273,7 @@ With the guarantee enabled:
 
 - A message that `ndjson` or `json_array` mode would skip for a serialization 
error fails the call instead.
 - `individual` and `raw` modes stop at the first message that fails, after the 
retries above, and at a payload above `max_payload_size_bytes`, so a later 
message is not sent before an earlier one is delivered.
+- An `ndjson` or `json_array` batch above `max_payload_size_bytes` stops the 
sink, since sending the same batch again cannot make it smaller. Lower 
`batch_length` or raise `max_payload_size_bytes`, then restart the sink.
 - A failure after retries with a non-transient status, such as 400 or 404, 
stops the sink. A transient one is retried by the runtime, which hands over 
again every message of the partition since its last flush. Without 
`flush_interval` that is the failed batch. With it, the batches accepted before 
the failure go again too, although the receiver already has them.
 - The receiver can therefore see a message more than once, and should 
deduplicate by `iggy_stream`, `iggy_topic`, `iggy_partition_id` and 
`iggy_offset`. `raw` mode and `include_metadata = false` send none of these 
fields, so the receiver has no key to deduplicate by.
 
diff --git a/core/connectors/sinks/http_sink/src/lib.rs 
b/core/connectors/sinks/http_sink/src/lib.rs
index 3f272a64a..81e691d88 100644
--- a/core/connectors/sinks/http_sink/src/lib.rs
+++ b/core/connectors/sinks/http_sink/src/lib.rs
@@ -829,7 +829,7 @@ impl HttpSink {
             );
             // Count all successfully-serialized messages as errors (skipped 
already counted individually)
             self.errors_count.fetch_add(count, Ordering::Relaxed);
-            return Err(Error::HttpRequestFailed(format!(
+            return Err(Error::InvalidRecordValue(format!(
                 "NDJSON batch exceeds max size: {} bytes",
                 body.len()
             )));
@@ -928,7 +928,7 @@ impl HttpSink {
             );
             // Count all successfully-serialized messages as errors (skipped 
already counted individually)
             self.errors_count.fetch_add(count, Ordering::Relaxed);
-            return Err(Error::HttpRequestFailed(format!(
+            return Err(Error::InvalidRecordValue(format!(
                 "JSON array batch exceeds max size: {} bytes",
                 body.len()
             )));
@@ -1223,11 +1223,14 @@ impl Sink for HttpSink {
     /// mitigates this by aborting early, but a fail-succeed-fail pattern can 
bypass it.
     ///
     /// **Runtime note**: The FFI boundary in `sdk/src/sink.rs` maps 
`consume()`'s `Result` to
-    /// `i32` (0=ok, 1=err), but the runtime's `process_messages()` in 
`runtime/src/sink.rs`
-    /// logs and counts a nonzero code, records no processed messages for the 
run that
-    /// returned it, and continues polling. A batch that mixes payload 
variants is split
-    /// into one `consume()` call per run, so the other runs of that poll 
still count as
-    /// processed. Returning `Err` does not trigger a runtime retry.
+    /// an `i32` status: 0 for `Ok`, 2 for an error that `Error::is_permanent` 
reports and 1 for
+    /// any other error. At most once, the runtime's `process_messages()` in
+    /// `runtime/src/sink.rs` logs and counts a nonzero status, records no 
processed messages
+    /// for the run that returned it, and continues polling, so an `Err` is 
never retried. A
+    /// batch that mixes payload variants is split into one `consume()` call 
per run, so the
+    /// other runs of that poll still count as processed. At least once, a 1 
hands the messages
+    /// to `consume()` again from the first one not yet flushed, after a 
backoff, while a 2, or
+    /// a 1 past `retry_max_attempts`, stops the delivery of the topic.
     async fn consume(
         &self,
         topic_metadata: &TopicMetadata,
@@ -2111,14 +2114,14 @@ mod tests {
         }
     }
 
-    /// A raw-mode sink pointed at a closed port, so every request that it 
sends fails at once
-    /// and `send_attempts` counts the requests.
-    async fn given_unreachable_raw_sink(at_least_once: bool) -> HttpSink {
+    /// A sink pointed at a closed port, so every request that it sends fails 
at once and
+    /// `send_attempts` counts the requests.
+    async fn given_unreachable_sink(batch_mode: BatchMode, at_least_once: 
bool) -> HttpSink {
         let mut sink = HttpSink::new(
             1,
             HttpSinkConfig {
                 url: "http://127.0.0.1:9/ingest".to_string(),
-                batch_mode: Some(BatchMode::Raw),
+                batch_mode: Some(batch_mode),
                 max_payload_size_bytes: Some(8),
                 max_retries: Some(1),
                 health_check_enabled: Some(false),
@@ -2135,7 +2138,7 @@ mod tests {
 
     #[tokio::test]
     async fn 
given_at_least_once_when_a_payload_is_too_large_should_fail_before_sending_the_rest()
 {
-        let sink = given_unreachable_raw_sink(true).await;
+        let sink = given_unreachable_sink(BatchMode::Raw, true).await;
 
         let result = sink
             .consume(
@@ -2154,7 +2157,7 @@ mod tests {
 
     #[tokio::test]
     async fn 
given_at_most_once_when_a_payload_is_too_large_should_skip_it_and_send_the_rest()
 {
-        let sink = given_unreachable_raw_sink(false).await;
+        let sink = given_unreachable_sink(BatchMode::Raw, false).await;
 
         let result = sink
             .consume(
@@ -2168,6 +2171,32 @@ mod tests {
         assert_eq!(sink.send_attempts.load(Ordering::Relaxed), 1);
     }
 
+    #[tokio::test]
+    async fn 
given_a_batch_mode_when_the_batch_is_too_large_should_fail_permanently_without_sending()
+     {
+        for batch_mode in [BatchMode::NdJson, BatchMode::JsonArray] {
+            let sink = given_unreachable_sink(batch_mode, true).await;
+
+            let result = sink
+                .consume(
+                    &given_topic_metadata(),
+                    given_messages_metadata(),
+                    vec![given_raw_message(0, 64)],
+                )
+                .await;
+
+            assert!(
+                matches!(&result, Err(error) if error.is_permanent()),
+                "{batch_mode}: got {result:?}"
+            );
+            assert_eq!(
+                sink.send_attempts.load(Ordering::Relaxed),
+                0,
+                "{batch_mode}"
+            );
+        }
+    }
+
     #[test]
     fn 
given_a_final_status_when_classified_should_be_transient_only_where_the_middleware_retries()
     {

Reply via email to