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

MartijnVisser pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-connector-http.git


The following commit(s) were added to refs/heads/main by this push:
     new 70e18b8  [FLINK-40278][connector-http] Document and test that HTTP 
sink retries resend the whole batch
70e18b8 is described below

commit 70e18b8d2f61b130a9ab09253f38af278228e965
Author: bharathreddygunapati <[email protected]>
AuthorDate: Sat Aug 1 20:32:19 2026 +0530

    [FLINK-40278][connector-http] Document and test that HTTP sink retries 
resend the whole batch
    
    Generated-by: Cursor (auto mode) for the initial change; Claude Code 
(Claude Fable 5.1) for the review follow-ups
---
 docs/content.zh/docs/connectors/datastream/http.md |  21 +-
 docs/content.zh/docs/connectors/table/http.md      |  21 ++
 docs/content/docs/connectors/datastream/http.md    |  21 +-
 docs/content/docs/connectors/table/http.md         |  21 ++
 .../sink/httpclient/JavaNetSinkHttpClientTest.java | 285 +++++++++++++++++++++
 5 files changed, 367 insertions(+), 2 deletions(-)

diff --git a/docs/content.zh/docs/connectors/datastream/http.md 
b/docs/content.zh/docs/connectors/datastream/http.md
index ce9e6f5..7281853 100644
--- a/docs/content.zh/docs/connectors/datastream/http.md
+++ b/docs/content.zh/docs/connectors/datastream/http.md
@@ -37,6 +37,7 @@ The HTTP Sink connector supports the Flink DataStream API.
 * [Apache HTTP Connector](#apache-http-connector)
   * [Working with HTTP sink Flink streaming 
API](#working-with-http-sink-flink-streaming-api)
     * [Sink Connector options](#sink-connector-options-)
+    * [Retries and handling errors (Sink)](#retries-and-handling-errors-sink)
     * [Batch submission mode](#batch-submission-mode)
     * [Single submission mode](#single-submission-mode)
     * [Http headers](#http-headers)
@@ -82,7 +83,25 @@ These options are specified on the builder using the 
setProperty method.
 | http.sink.writer.request.mode                             | optional | Sets 
the Http Sink request submission mode. Two modes are available: `single` and 
`batch`. Defaults to `batch` if not specified. |
 | http.sink.request.batch.size                              | optional | 
Applicable only for `http.sink.writer.request.mode = batch`. Sets number of 
individual events/requests that will be submitted as one HTTP request by HTTP 
sink. The default value is 500 which is same as HTTP Sink `maxBatchSize` |
 
-
+### Retries and handling errors (Sink)
+Retries are disabled by default: `http.sink.max-retries` defaults to `0`, so 
the first failed request fails the job.
+
+When `http.sink.max-retries` is greater than `0`, the sink retries requests 
that fail before a response is received (for
+example, connection errors) and responses whose status code is listed in 
`http.sink.retry-codes`. The delay between
+retries is set by `http.sink.retry-strategy.type` and the matching fixed-delay 
or exponential-delay options. Any other
+response that is not successful or ignored is not retried and fails the job. A 
request that still fails with a retryable
+error after the last retry also fails the job.
+
+With the legacy `http.sink.error.code` or `http.sink.error.code.exclude` 
properties, no response status code is retried:
+a status code in the error list fails the job and every other status code is 
treated as success. Only requests that fail
+before a response is received are retried.
+
+In batch submission mode, each HTTP request carries several records and gets 
one response status, which applies to every
+record in that request. A retryable response retries all of those records, and 
a fatal response or an exhausted retry
+fails all of them. The sink does not retry individual records inside a batch. 
Use an idempotent endpoint if retried
+records must not be applied twice. When one flush is split into several HTTP 
requests (because of
+`http.sink.request.batch.size` or different HTTP methods), each response 
applies only to the records in its own request.
+Retried records are batched again, so they can be grouped into different HTTP 
requests than in the first attempt.
 
 ### Request submission
 HTTP Sink by default submits events in batch. The submission mode can be 
changed using `http.sink.writer.request.mode` property using `single` or 
`batch` as property value.
diff --git a/docs/content.zh/docs/connectors/table/http.md 
b/docs/content.zh/docs/connectors/table/http.md
index 6c25f63..75ab264 100644
--- a/docs/content.zh/docs/connectors/table/http.md
+++ b/docs/content.zh/docs/connectors/table/http.md
@@ -61,6 +61,7 @@ The HTTP source connector supports [Lookup 
Joins](https://nightlies.apache.org/f
     * [HTTP Sink](#http-sink)
     * [Sink Connector Options](#sink-connector-options)
     * [Sink table HTTP status codes](#sink-table-http-status-codes)
+    * [Retries and handling errors (Sink)](#retries-and-handling-errors-sink)
     * [Request submission](#request-submission)
     * [Batch submission mode](#batch-submission-mode)
     * [Single submission mode](#single-submission-mode)
@@ -632,6 +633,26 @@ If either legacy property is set together with 
`http.sink.success-codes` or `htt
 - `http.sink.retry-codes` (default `500,503,504`) for retryable responses
 - `http.sink.ignored-response-codes` for responses that should be treated as 
successful without retrying
 
+### Retries and handling errors (Sink)
+Retries are disabled by default: `http.sink.max-retries` defaults to `0`, so 
the first failed request fails the job.
+
+When `http.sink.max-retries` is greater than `0`, the sink retries requests 
that fail before a response is received (for
+example, connection errors) and responses whose status code is listed in 
`http.sink.retry-codes`. The delay between
+retries is set by `http.sink.retry-strategy.type` and the matching fixed-delay 
or exponential-delay options. Any other
+response that is not successful or ignored is not retried and fails the job. A 
request that still fails with a retryable
+error after the last retry also fails the job.
+
+With the legacy `http.sink.error.code` or `http.sink.error.code.exclude` 
properties, no response status code is retried:
+a status code in the error list fails the job and every other status code is 
treated as success. Only requests that fail
+before a response is received are retried.
+
+In batch submission mode, each HTTP request carries several records and gets 
one response status, which applies to every
+record in that request. A retryable response retries all of those records, and 
a fatal response or an exhausted retry
+fails all of them. The sink does not retry individual records inside a batch. 
Use an idempotent endpoint if retried
+records must not be applied twice. When one flush is split into several HTTP 
requests (because of
+`http.sink.request.batch.size` or different HTTP methods), each response 
applies only to the records in its own request.
+Retried records are batched again, so they can be grouped into different HTTP 
requests than in the first attempt.
+
 ### Request submission
 HTTP Sink by default submits events in batch. The submission mode can be 
changed using `http.sink.writer.request.mode` property using `single` or 
`batch` as property value.
 
diff --git a/docs/content/docs/connectors/datastream/http.md 
b/docs/content/docs/connectors/datastream/http.md
index ce9e6f5..7281853 100644
--- a/docs/content/docs/connectors/datastream/http.md
+++ b/docs/content/docs/connectors/datastream/http.md
@@ -37,6 +37,7 @@ The HTTP Sink connector supports the Flink DataStream API.
 * [Apache HTTP Connector](#apache-http-connector)
   * [Working with HTTP sink Flink streaming 
API](#working-with-http-sink-flink-streaming-api)
     * [Sink Connector options](#sink-connector-options-)
+    * [Retries and handling errors (Sink)](#retries-and-handling-errors-sink)
     * [Batch submission mode](#batch-submission-mode)
     * [Single submission mode](#single-submission-mode)
     * [Http headers](#http-headers)
@@ -82,7 +83,25 @@ These options are specified on the builder using the 
setProperty method.
 | http.sink.writer.request.mode                             | optional | Sets 
the Http Sink request submission mode. Two modes are available: `single` and 
`batch`. Defaults to `batch` if not specified. |
 | http.sink.request.batch.size                              | optional | 
Applicable only for `http.sink.writer.request.mode = batch`. Sets number of 
individual events/requests that will be submitted as one HTTP request by HTTP 
sink. The default value is 500 which is same as HTTP Sink `maxBatchSize` |
 
-
+### Retries and handling errors (Sink)
+Retries are disabled by default: `http.sink.max-retries` defaults to `0`, so 
the first failed request fails the job.
+
+When `http.sink.max-retries` is greater than `0`, the sink retries requests 
that fail before a response is received (for
+example, connection errors) and responses whose status code is listed in 
`http.sink.retry-codes`. The delay between
+retries is set by `http.sink.retry-strategy.type` and the matching fixed-delay 
or exponential-delay options. Any other
+response that is not successful or ignored is not retried and fails the job. A 
request that still fails with a retryable
+error after the last retry also fails the job.
+
+With the legacy `http.sink.error.code` or `http.sink.error.code.exclude` 
properties, no response status code is retried:
+a status code in the error list fails the job and every other status code is 
treated as success. Only requests that fail
+before a response is received are retried.
+
+In batch submission mode, each HTTP request carries several records and gets 
one response status, which applies to every
+record in that request. A retryable response retries all of those records, and 
a fatal response or an exhausted retry
+fails all of them. The sink does not retry individual records inside a batch. 
Use an idempotent endpoint if retried
+records must not be applied twice. When one flush is split into several HTTP 
requests (because of
+`http.sink.request.batch.size` or different HTTP methods), each response 
applies only to the records in its own request.
+Retried records are batched again, so they can be grouped into different HTTP 
requests than in the first attempt.
 
 ### Request submission
 HTTP Sink by default submits events in batch. The submission mode can be 
changed using `http.sink.writer.request.mode` property using `single` or 
`batch` as property value.
diff --git a/docs/content/docs/connectors/table/http.md 
b/docs/content/docs/connectors/table/http.md
index 85822ea..5b0a171 100644
--- a/docs/content/docs/connectors/table/http.md
+++ b/docs/content/docs/connectors/table/http.md
@@ -61,6 +61,7 @@ The HTTP source connector supports [Lookup 
Joins](https://nightlies.apache.org/f
     * [HTTP Sink](#http-sink)
     * [Sink Connector Options](#sink-connector-options)
     * [Sink table HTTP status codes](#sink-table-http-status-codes)
+    * [Retries and handling errors (Sink)](#retries-and-handling-errors-sink)
     * [Request submission](#request-submission)
     * [Batch submission mode](#batch-submission-mode)
     * [Single submission mode](#single-submission-mode)
@@ -633,6 +634,26 @@ If either legacy property is set together with 
`http.sink.success-codes` or `htt
 - `http.sink.retry-codes` (default `500,503,504`) for retryable responses
 - `http.sink.ignored-response-codes` for responses that should be treated as 
successful without retrying
 
+### Retries and handling errors (Sink)
+Retries are disabled by default: `http.sink.max-retries` defaults to `0`, so 
the first failed request fails the job.
+
+When `http.sink.max-retries` is greater than `0`, the sink retries requests 
that fail before a response is received (for
+example, connection errors) and responses whose status code is listed in 
`http.sink.retry-codes`. The delay between
+retries is set by `http.sink.retry-strategy.type` and the matching fixed-delay 
or exponential-delay options. Any other
+response that is not successful or ignored is not retried and fails the job. A 
request that still fails with a retryable
+error after the last retry also fails the job.
+
+With the legacy `http.sink.error.code` or `http.sink.error.code.exclude` 
properties, no response status code is retried:
+a status code in the error list fails the job and every other status code is 
treated as success. Only requests that fail
+before a response is received are retried.
+
+In batch submission mode, each HTTP request carries several records and gets 
one response status, which applies to every
+record in that request. A retryable response retries all of those records, and 
a fatal response or an exhausted retry
+fails all of them. The sink does not retry individual records inside a batch. 
Use an idempotent endpoint if retried
+records must not be applied twice. When one flush is split into several HTTP 
requests (because of
+`http.sink.request.batch.size` or different HTTP methods), each response 
applies only to the records in its own request.
+Retried records are batched again, so they can be grouped into different HTTP 
requests than in the first attempt.
+
 ### Request submission
 HTTP Sink by default submits events in batch. The submission mode can be 
changed using `http.sink.writer.request.mode` property using `single` or 
`batch` as property value.
 
diff --git 
a/flink-connector-http/src/test/java/org/apache/flink/connector/http/sink/httpclient/JavaNetSinkHttpClientTest.java
 
b/flink-connector-http/src/test/java/org/apache/flink/connector/http/sink/httpclient/JavaNetSinkHttpClientTest.java
index fa70916..9fb5eae 100644
--- 
a/flink-connector-http/src/test/java/org/apache/flink/connector/http/sink/httpclient/JavaNetSinkHttpClientTest.java
+++ 
b/flink-connector-http/src/test/java/org/apache/flink/connector/http/sink/httpclient/JavaNetSinkHttpClientTest.java
@@ -28,6 +28,7 @@ import 
org.apache.flink.connector.http.table.sink.Slf4jHttpPostRequestCallback;
 import org.apache.flink.connector.http.utils.HttpHeaderUtils;
 
 import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -41,9 +42,12 @@ import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.net.http.HttpClient;
 import java.net.http.HttpResponse;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Properties;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Stream;
@@ -55,9 +59,12 @@ import static 
org.apache.flink.connector.http.table.sink.HttpDynamicSinkConnecto
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 /** Test for {@link JavaNetSinkHttpClient }. */
@@ -94,6 +101,14 @@ class JavaNetSinkHttpClientTest {
         
lenient().when(httpClientBuilder.executor(any())).thenReturn(httpClientBuilder);
     }
 
+    private final List<JavaNetSinkHttpClient> clientsToClose = new 
ArrayList<>();
+
+    @AfterEach
+    public void closeClients() {
+        clientsToClose.forEach(JavaNetSinkHttpClient::close);
+        clientsToClose.clear();
+    }
+
     private static Stream<Arguments> provideSubmitterFactory() {
         return Stream.of(
                 Arguments.of(new PerRequestRequestSubmitterFactory()),
@@ -378,6 +393,276 @@ class JavaNetSinkHttpClientTest {
         assertThat(submitterClosed).isTrue();
     }
 
+    @Test
+    public void perRequestModeRetriesOnlyTheFailedEntry() {
+        HttpSinkRequestEntry successfulEntry = new 
HttpSinkRequestEntry("POST", new byte[] {1});
+        HttpSinkRequestEntry retryableEntry = new HttpSinkRequestEntry("POST", 
new byte[] {2});
+        HttpClient httpClient = mock(HttpClient.class);
+        doReturn(httpResponse(200), httpResponse(500), httpResponse(200))
+                .when(httpClient)
+                .sendAsync(any(), any());
+        List<List<HttpSinkRequestEntry>> attempts = new 
CopyOnWriteArrayList<>();
+
+        Properties properties = new Properties();
+        properties.setProperty(SINK_MAX_RETRIES, "1");
+        properties.setProperty(SINK_RETRY_FIXED_DELAY_DELAY, "1ms");
+        JavaNetSinkHttpClient client =
+                retryingClient(
+                        sinkConfig(properties),
+                        recordingSubmitterFactory(httpClient, false, 
attempts));
+
+        var response =
+                client.putRequests(List.of(successfulEntry, retryableEntry), 
"http://localhost";)
+                        .join();
+
+        assertThat(attempts).hasSize(2);
+        assertSameEntries(attempts.get(0), successfulEntry, retryableEntry);
+        assertSameEntries(attempts.get(1), retryableEntry);
+        assertSameEntries(response.getSuccessfulRequests(), successfulEntry, 
retryableEntry);
+        assertThat(response.getRetriableFailedRequests()).isEmpty();
+        assertThat(response.getFatalFailedRequests()).isEmpty();
+    }
+
+    @Test
+    public void batchModeRetriesAllOriginalEntriesOfRetryableBatch() {
+        List<HttpSinkRequestEntry> batch = batchEntries();
+        HttpClient httpClient = mock(HttpClient.class);
+        doReturn(httpResponse(500), 
httpResponse(200)).when(httpClient).sendAsync(any(), any());
+        List<List<HttpSinkRequestEntry>> attempts = new 
CopyOnWriteArrayList<>();
+
+        JavaNetSinkHttpClient client =
+                retryingClient(
+                        sinkConfig(batchProperties("1")),
+                        recordingSubmitterFactory(httpClient, true, attempts));
+
+        var response = client.putRequests(batch, "http://localhost";).join();
+
+        verify(httpClient, times(2)).sendAsync(any(), any());
+        assertThat(attempts).hasSize(2);
+        assertSameEntries(attempts.get(0), batch.toArray(new 
HttpSinkRequestEntry[0]));
+        assertSameEntries(attempts.get(1), batch.toArray(new 
HttpSinkRequestEntry[0]));
+        assertSameEntries(
+                response.getSuccessfulRequests(), batch.toArray(new 
HttpSinkRequestEntry[0]));
+        assertThat(response.getRetriableFailedRequests()).isEmpty();
+        assertThat(response.getFatalFailedRequests()).isEmpty();
+    }
+
+    @Test
+    public void batchModeReturnsAllEntriesAsRetryableWhenRetriesAreExhausted() 
{
+        List<HttpSinkRequestEntry> batch = batchEntries();
+        HttpClient httpClient = mock(HttpClient.class);
+        doReturn(httpResponse(500), 
httpResponse(500)).when(httpClient).sendAsync(any(), any());
+        List<List<HttpSinkRequestEntry>> attempts = new 
CopyOnWriteArrayList<>();
+
+        JavaNetSinkHttpClient client =
+                retryingClient(
+                        sinkConfig(batchProperties("1")),
+                        recordingSubmitterFactory(httpClient, true, attempts));
+
+        var response = client.putRequests(batch, "http://localhost";).join();
+
+        assertThat(attempts).hasSize(2);
+        assertSameEntries(attempts.get(1), batch.toArray(new 
HttpSinkRequestEntry[0]));
+        assertThat(response.getSuccessfulRequests()).isEmpty();
+        assertSameEntries(
+                response.getRetriableFailedRequests(), batch.toArray(new 
HttpSinkRequestEntry[0]));
+        assertThat(response.getFatalFailedRequests()).isEmpty();
+    }
+
+    @Test
+    public void batchModeFailsAllEntriesOfFatalBatchWithoutRetry() {
+        List<HttpSinkRequestEntry> batch = batchEntries();
+        HttpClient httpClient = mock(HttpClient.class);
+        doReturn(httpResponse(400)).when(httpClient).sendAsync(any(), any());
+        List<List<HttpSinkRequestEntry>> attempts = new 
CopyOnWriteArrayList<>();
+
+        JavaNetSinkHttpClient client =
+                retryingClient(
+                        sinkConfig(batchProperties("1")),
+                        recordingSubmitterFactory(httpClient, true, attempts));
+
+        var response = client.putRequests(batch, "http://localhost";).join();
+
+        verify(httpClient, times(1)).sendAsync(any(), any());
+        assertThat(attempts).hasSize(1);
+        assertThat(response.getSuccessfulRequests()).isEmpty();
+        assertThat(response.getRetriableFailedRequests()).isEmpty();
+        assertSameEntries(
+                response.getFatalFailedRequests(), batch.toArray(new 
HttpSinkRequestEntry[0]));
+    }
+
+    @Test
+    public void batchModeCompletesAllEntriesOfIgnoredBatch() {
+        List<HttpSinkRequestEntry> batch = batchEntries();
+        HttpClient httpClient = mock(HttpClient.class);
+        doReturn(httpResponse(404)).when(httpClient).sendAsync(any(), any());
+        List<List<HttpSinkRequestEntry>> attempts = new 
CopyOnWriteArrayList<>();
+
+        Properties properties = batchProperties("1");
+        properties.setProperty(SINK_HTTP_IGNORED_RESPONSE_CODES.key(), "404");
+        JavaNetSinkHttpClient client =
+                retryingClient(
+                        sinkConfig(properties),
+                        recordingSubmitterFactory(httpClient, true, attempts));
+
+        var response = client.putRequests(batch, "http://localhost";).join();
+
+        assertThat(attempts).hasSize(1);
+        assertSameEntries(
+                response.getIgnoredRequests(), batch.toArray(new 
HttpSinkRequestEntry[0]));
+        assertThat(response.getRetriableFailedRequests()).isEmpty();
+        assertThat(response.getFatalFailedRequests()).isEmpty();
+    }
+
+    @Test
+    public void batchModeRetriesOnlyTheFailedRequestWhenFlushIsSplit() {
+        HttpSinkRequestEntry first = new HttpSinkRequestEntry("POST", new 
byte[] {1});
+        HttpSinkRequestEntry second = new HttpSinkRequestEntry("POST", new 
byte[] {2});
+        HttpSinkRequestEntry third = new HttpSinkRequestEntry("POST", new 
byte[] {3});
+        HttpClient httpClient = mock(HttpClient.class);
+        // first attempt: [first, second] -> 500, [third] -> 200; retry: 
[first, second] -> 200
+        doReturn(httpResponse(500), httpResponse(200), httpResponse(200))
+                .when(httpClient)
+                .sendAsync(any(), any());
+        List<List<HttpSinkRequestEntry>> attempts = new 
CopyOnWriteArrayList<>();
+        List<List<HttpSinkRequestEntry>> httpRequests = new 
CopyOnWriteArrayList<>();
+
+        Properties properties = batchProperties("1");
+        
properties.setProperty(HttpConnectorConfigConstants.SINK_HTTP_BATCH_REQUEST_SIZE,
 "2");
+        JavaNetSinkHttpClient client =
+                retryingClient(
+                        sinkConfig(properties, 
recordingCallback(httpRequests)),
+                        recordingSubmitterFactory(httpClient, true, attempts));
+
+        var response = client.putRequests(List.of(first, second, third), 
"http://localhost";).join();
+
+        assertThat(attempts).hasSize(2);
+        assertSameEntries(attempts.get(0), first, second, third);
+        assertSameEntries(attempts.get(1), first, second);
+        assertThat(httpRequests).hasSize(3);
+        assertSameEntries(httpRequests.get(0), first, second);
+        assertSameEntries(httpRequests.get(1), third);
+        assertSameEntries(httpRequests.get(2), first, second);
+        assertSameEntriesInAnyOrder(response.getSuccessfulRequests(), first, 
second, third);
+        assertThat(response.getRetriableFailedRequests()).isEmpty();
+        assertThat(response.getFatalFailedRequests()).isEmpty();
+    }
+
+    @Test
+    public void batchModeRegroupsRetriedEntriesIntoNewHttpRequests() {
+        HttpSinkRequestEntry firstPost = new HttpSinkRequestEntry("POST", new 
byte[] {1});
+        HttpSinkRequestEntry put = new HttpSinkRequestEntry("PUT", new byte[] 
{2});
+        HttpSinkRequestEntry secondPost = new HttpSinkRequestEntry("POST", new 
byte[] {3});
+        HttpClient httpClient = mock(HttpClient.class);
+        // first attempt splits on the method change: [firstPost] -> 500, 
[put] -> 200,
+        // [secondPost] -> 500; the retry sends both POST entries as one 
request -> 200
+        doReturn(httpResponse(500), httpResponse(200), httpResponse(500), 
httpResponse(200))
+                .when(httpClient)
+                .sendAsync(any(), any());
+        List<List<HttpSinkRequestEntry>> attempts = new 
CopyOnWriteArrayList<>();
+        List<List<HttpSinkRequestEntry>> httpRequests = new 
CopyOnWriteArrayList<>();
+
+        JavaNetSinkHttpClient client =
+                retryingClient(
+                        sinkConfig(batchProperties("1"), 
recordingCallback(httpRequests)),
+                        recordingSubmitterFactory(httpClient, true, attempts));
+
+        var response =
+                client.putRequests(List.of(firstPost, put, secondPost), 
"http://localhost";).join();
+
+        verify(httpClient, times(4)).sendAsync(any(), any());
+        assertThat(attempts).hasSize(2);
+        assertSameEntries(attempts.get(1), firstPost, secondPost);
+        assertThat(httpRequests).hasSize(4);
+        assertSameEntries(httpRequests.get(0), firstPost);
+        assertSameEntries(httpRequests.get(1), put);
+        assertSameEntries(httpRequests.get(2), secondPost);
+        assertSameEntries(httpRequests.get(3), firstPost, secondPost);
+        assertSameEntriesInAnyOrder(response.getSuccessfulRequests(), 
firstPost, put, secondPost);
+        assertThat(response.getRetriableFailedRequests()).isEmpty();
+        assertThat(response.getFatalFailedRequests()).isEmpty();
+    }
+
+    private JavaNetSinkHttpClient retryingClient(
+            HttpSinkConfig sinkConfig, RequestSubmitterFactory 
requestSubmitterFactory) {
+        JavaNetSinkHttpClient client =
+                new JavaNetSinkHttpClient(sinkConfig, headerPreprocessor, 
requestSubmitterFactory);
+        clientsToClose.add(client);
+        return client;
+    }
+
+    private static HttpPostRequestCallback<HttpRequest> recordingCallback(
+            List<List<HttpSinkRequestEntry>> httpRequests) {
+        return (response, request, endpointUrl, headerMap) ->
+                httpRequests.add(request.getRequestEntries());
+    }
+
+    private static List<HttpSinkRequestEntry> batchEntries() {
+        return List.of(
+                new HttpSinkRequestEntry("POST", new byte[] {1}),
+                new HttpSinkRequestEntry("POST", new byte[] {2}),
+                new HttpSinkRequestEntry("POST", new byte[] {3}));
+    }
+
+    private static Properties batchProperties(String maxRetries) {
+        Properties properties = new Properties();
+        
properties.setProperty(HttpConnectorConfigConstants.SINK_HTTP_BATCH_REQUEST_SIZE,
 "50");
+        properties.setProperty(SINK_MAX_RETRIES, maxRetries);
+        properties.setProperty(SINK_RETRY_FIXED_DELAY_DELAY, "1ms");
+        return properties;
+    }
+
+    private static RequestSubmitterFactory recordingSubmitterFactory(
+            HttpClient httpClient, boolean batchMode, 
List<List<HttpSinkRequestEntry>> attempts) {
+        return (sinkConfig, headersAndValues) -> {
+            RequestSubmitter delegate =
+                    batchMode
+                            ? new BatchRequestSubmitter(
+                                    sinkConfig,
+                                    headersAndValues,
+                                    httpClient,
+                                    Executors.newSingleThreadExecutor())
+                            : new PerRequestSubmitter(
+                                    sinkConfig,
+                                    headersAndValues,
+                                    httpClient,
+                                    Executors.newSingleThreadExecutor());
+            return new RequestSubmitter() {
+                @Override
+                public List<CompletableFuture<JavaNetHttpResponseWrapper>> 
submit(
+                        String endpointUrl, List<HttpSinkRequestEntry> 
requestToSubmit) {
+                    attempts.add(List.copyOf(requestToSubmit));
+                    return delegate.submit(endpointUrl, requestToSubmit);
+                }
+
+                @Override
+                public void close() {
+                    delegate.close();
+                }
+            };
+        };
+    }
+
+    private static CompletableFuture<HttpResponse<String>> httpResponse(int 
statusCode) {
+        HttpResponse<String> response = mock(HttpResponse.class);
+        when(response.statusCode()).thenReturn(statusCode);
+        return CompletableFuture.completedFuture(response);
+    }
+
+    private static void assertSameEntries(
+            List<HttpSinkRequestEntry> actual, HttpSinkRequestEntry... 
expected) {
+        assertThat(actual)
+                .usingElementComparator((left, right) -> left == right ? 0 : 1)
+                .containsExactly(expected);
+    }
+
+    private static void assertSameEntriesInAnyOrder(
+            List<HttpSinkRequestEntry> actual, HttpSinkRequestEntry... 
expected) {
+        assertThat(actual)
+                .usingElementComparator((left, right) -> left == right ? 0 : 1)
+                .containsExactlyInAnyOrder(expected);
+    }
+
     private static CompletableFuture<JavaNetHttpResponseWrapper> 
responseFuture(
             HttpSinkRequestEntry requestEntry, int statusCode) {
         HttpResponse<String> response = mock(HttpResponse.class);

Reply via email to