diff --git a/CHANGES.txt b/CHANGES.txt
index 974b9ceaa..297fcbf41 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
0.5.0
-----
+ * Add sidecar.instance.id Spark conf to append an instanceId query parameter to outbound sidecar requests, fixing 421 errors when Sidecar is behind a load balancer (CASSANALYTICS-177)
* Upgrade sidecar version to 0.4.0
* Exclude IP address from RingInstance equality so node replacement does not fail bulk write jobs (CASSANALYTICS-175)
* Regenerate bloom filters for CQLSSTableWriter (CASSANALYTICS-167)
diff --git a/analytics-sidecar-client-common/src/main/java/org/apache/cassandra/sidecar/common/http/SidecarQueryParamNames.java b/analytics-sidecar-client-common/src/main/java/org/apache/cassandra/sidecar/common/http/SidecarQueryParamNames.java
new file mode 100644
index 000000000..6b615d261
--- /dev/null
+++ b/analytics-sidecar-client-common/src/main/java/org/apache/cassandra/sidecar/common/http/SidecarQueryParamNames.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.common.http;
+
+/**
+ * Custom query parameter names for sidecar HTTP requests.
+ */
+public final class SidecarQueryParamNames
+{
+ /**
+ * {@code "instanceId"} query parameter. When present on an outbound sidecar request it carries
+ * the job-level instance identifier supplied by the client (see the Spark conf key
+ * {@code spark.cassandra_analytics.sidecar.instance.id}).
+ *
+ *
Requires a Sidecar server >= 0.2.0 (see {@code AbstractHandler#host}, introduced in
+ * CASSSIDECAR-208); older servers do not resolve this parameter and requests will fall back to
+ * Host-header-based instance resolution.
+ */
+ public static final String INSTANCE_ID = "instanceId";
+
+ private SidecarQueryParamNames()
+ {
+ }
+}
diff --git a/analytics-sidecar-client/src/main/java/org/apache/cassandra/sidecar/client/HttpClientConfig.java b/analytics-sidecar-client/src/main/java/org/apache/cassandra/sidecar/client/HttpClientConfig.java
index 5d2714b3c..ae2042367 100644
--- a/analytics-sidecar-client/src/main/java/org/apache/cassandra/sidecar/client/HttpClientConfig.java
+++ b/analytics-sidecar-client/src/main/java/org/apache/cassandra/sidecar/client/HttpClientConfig.java
@@ -38,6 +38,7 @@ public class HttpClientConfig
public static final String DEFAULT_TRUST_STORE_TYPE = "JKS";
public static final String DEFAULT_KEY_STORE_TYPE = "PKCS12";
public static final String DEFAULT_CASSANDRA_ROLE = null;
+ public static final Integer DEFAULT_INSTANCE_ID = null;
private final long timeoutMillis;
private final boolean ssl;
@@ -54,6 +55,7 @@ public class HttpClientConfig
private final String keyStorePassword;
private final String keyStoreType;
private final String cassandraRole;
+ private final Integer instanceId;
private HttpClientConfig(Builder> builder)
{
@@ -72,6 +74,7 @@ private HttpClientConfig(Builder> builder)
keyStorePassword = builder.keyStorePassword;
keyStoreType = builder.keyStoreType;
cassandraRole = builder.cassandraRole;
+ instanceId = builder.instanceId;
}
/**
@@ -192,6 +195,15 @@ public String cassandraRole()
return cassandraRole;
}
+ /**
+ * @return the job-level sidecar instance identifier, or {@code null} to omit the {@code instanceId} query parameter
+ */
+ @Nullable
+ public Integer instanceId()
+ {
+ return instanceId;
+ }
+
/**
* {@code HttpClient} builder static inner class.
*
@@ -214,6 +226,7 @@ public static class Builder>
private String keyStorePassword;
private String keyStoreType = DEFAULT_KEY_STORE_TYPE;
private String cassandraRole = DEFAULT_CASSANDRA_ROLE;
+ private Integer instanceId = DEFAULT_INSTANCE_ID;
/**
* @return a reference to itself
@@ -412,6 +425,26 @@ public T cassandraRole(String cassandraRole)
return self();
}
+ /**
+ * Sets the {@code instanceId} query parameter appended to every outbound sidecar request,
+ * and returns a reference to this Builder enabling method chaining. Non-null values must
+ * be greater than or equal to {@code 0}.
+ *
+ * @param instanceId the {@code instanceId} to set, or {@code null} to disable it
+ * @return a reference to this Builder
+ */
+ public T instanceId(Integer instanceId)
+ {
+ // Re-validated in BulkSparkConf.getSidecarInstanceId() to surface a Spark-conf-specific
+ // error message early; keep this constraint (>= 0) in sync with that check.
+ if (instanceId != null && instanceId < 0)
+ {
+ throw new IllegalArgumentException("instanceId must be greater than or equal to 0");
+ }
+ this.instanceId = instanceId;
+ return self();
+ }
+
/**
* Returns a {@code SidecarClientConfig} built from the parameters previously set.
*
diff --git a/analytics-sidecar-client/src/test/java/org/apache/cassandra/sidecar/client/HttpClientConfigTest.java b/analytics-sidecar-client/src/test/java/org/apache/cassandra/sidecar/client/HttpClientConfigTest.java
index a2c1db659..0a839a65d 100644
--- a/analytics-sidecar-client/src/test/java/org/apache/cassandra/sidecar/client/HttpClientConfigTest.java
+++ b/analytics-sidecar-client/src/test/java/org/apache/cassandra/sidecar/client/HttpClientConfigTest.java
@@ -23,6 +23,7 @@
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
/**
@@ -159,4 +160,40 @@ void testCassandraRole()
HttpClientConfig config = new HttpClientConfig.Builder<>().cassandraRole("custom_role").build();
assertThat(config.cassandraRole()).isEqualTo("custom_role");
}
+
+ @Test
+ void testInstanceIdDefaultIsNull()
+ {
+ HttpClientConfig config = new HttpClientConfig.Builder<>().build();
+ assertThat(config.instanceId()).isNull();
+ }
+
+ @Test
+ void testInstanceId()
+ {
+ HttpClientConfig config = new HttpClientConfig.Builder<>().instanceId(42).build();
+ assertThat(config.instanceId()).isEqualTo(42);
+ }
+
+ @Test
+ void testInstanceIdZeroIsAllowed()
+ {
+ HttpClientConfig config = new HttpClientConfig.Builder<>().instanceId(0).build();
+ assertThat(config.instanceId()).isEqualTo(0);
+ }
+
+ @Test
+ void testInstanceIdNullDisablesIt()
+ {
+ HttpClientConfig config = new HttpClientConfig.Builder<>().instanceId(null).build();
+ assertThat(config.instanceId()).isNull();
+ }
+
+ @Test
+ void testInstanceIdNegativeThrows()
+ {
+ assertThatThrownBy(() -> new HttpClientConfig.Builder<>().instanceId(-1))
+ .isExactlyInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("instanceId must be greater than or equal to 0");
+ }
}
diff --git a/analytics-sidecar-vertx-client/src/main/java/org/apache/cassandra/sidecar/client/VertxHttpClient.java b/analytics-sidecar-vertx-client/src/main/java/org/apache/cassandra/sidecar/client/VertxHttpClient.java
index f4c3890df..f4e455dec 100644
--- a/analytics-sidecar-vertx-client/src/main/java/org/apache/cassandra/sidecar/client/VertxHttpClient.java
+++ b/analytics-sidecar-vertx-client/src/main/java/org/apache/cassandra/sidecar/client/VertxHttpClient.java
@@ -58,6 +58,7 @@
import org.apache.cassandra.sidecar.common.request.UploadableRequest;
import static org.apache.cassandra.sidecar.common.http.SidecarHttpHeaderNames.AUTH_ROLE;
+import static org.apache.cassandra.sidecar.common.http.SidecarQueryParamNames.INSTANCE_ID;
import static org.apache.cassandra.sidecar.common.utils.StringUtils.isNullOrEmpty;
/**
@@ -252,6 +253,13 @@ protected HttpRequest vertxRequest(SidecarInstance sidecarInstance, Requ
sidecarInstance.hostname(),
request.requestURI());
+ if (config.instanceId() != null)
+ {
+ vertxRequest = vertxRequest.addQueryParam(INSTANCE_ID, String.valueOf(config.instanceId()));
+ LOGGER.debug("Appended {}={} to request uri. originalUri={}, finalUri={}",
+ INSTANCE_ID, config.instanceId(), request.requestURI(), vertxRequest.uri());
+ }
+
vertxRequest = applyHeaders(vertxRequest, request.headers());
Map customHeaders = context.customHeaders();
diff --git a/analytics-sidecar-vertx-client/src/test/java/org/apache/cassandra/sidecar/client/VertxHttpClientTest.java b/analytics-sidecar-vertx-client/src/test/java/org/apache/cassandra/sidecar/client/VertxHttpClientTest.java
index 8d8d0952d..8d1a4a125 100644
--- a/analytics-sidecar-vertx-client/src/test/java/org/apache/cassandra/sidecar/client/VertxHttpClientTest.java
+++ b/analytics-sidecar-vertx-client/src/test/java/org/apache/cassandra/sidecar/client/VertxHttpClientTest.java
@@ -22,11 +22,15 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import io.netty.handler.codec.http.HttpMethod;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.ext.web.client.HttpRequest;
+import org.apache.cassandra.sidecar.common.request.Request;
+
import static org.apache.cassandra.sidecar.common.http.SidecarHttpHeaderNames.AUTH_ROLE;
+import static org.apache.cassandra.sidecar.common.http.SidecarQueryParamNames.INSTANCE_ID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -56,16 +60,61 @@ public void testAuthHeaderSet()
HttpClientConfig config = httpClientConfigBuilder().cassandraRole("custom_role").build();
try (VertxHttpClient client = new VertxHttpClient(vertx, config))
{
- SidecarInstance instance = mock(SidecarInstance.class);
- when(instance.port()).thenReturn(9043);
- when(instance.hostname()).thenReturn("localhost");
RequestContext context = new RequestContext.Builder().ringRequest().build();
- HttpRequest request = client.vertxRequest(instance, context);
+ HttpRequest request = client.vertxRequest(mockInstance(), context);
assertThat(request.headers()).isNotEmpty();
assertThat(request.headers().get(AUTH_ROLE)).isEqualTo("custom_role");
}
}
+ @Test
+ public void testInstanceIdQueryParamAppended()
+ {
+ HttpClientConfig config = httpClientConfigBuilder().instanceId(42).build();
+ try (VertxHttpClient client = new VertxHttpClient(vertx, config))
+ {
+ RequestContext context = new RequestContext.Builder().ringRequest().build();
+ HttpRequest request = client.vertxRequest(mockInstance(), context);
+ assertThat(request.queryParams().get(INSTANCE_ID)).isEqualTo("42");
+ }
+ }
+
+ @Test
+ public void testInstanceIdQueryParamNotAppendedWhenNull()
+ {
+ HttpClientConfig config = httpClientConfigBuilder().build();
+ try (VertxHttpClient client = new VertxHttpClient(vertx, config))
+ {
+ RequestContext context = new RequestContext.Builder().ringRequest().build();
+ HttpRequest request = client.vertxRequest(mockInstance(), context);
+ assertThat(request.queryParams().contains(INSTANCE_ID)).isFalse();
+ }
+ }
+
+ @Test
+ public void testInstanceIdQueryParamAppendedWithExistingQueryParams()
+ {
+ HttpClientConfig config = httpClientConfigBuilder().instanceId(7).build();
+ try (VertxHttpClient client = new VertxHttpClient(vertx, config))
+ {
+ Request mockRequest = mock(Request.class);
+ when(mockRequest.method()).thenReturn(HttpMethod.GET);
+ when(mockRequest.requestURI()).thenReturn("/api/v1/ring?existingParam=value");
+ RequestContext context = new RequestContext.Builder().request(mockRequest).build();
+ HttpRequest request = client.vertxRequest(mockInstance(), context);
+ assertThat(request.queryParams().get("existingParam")).isEqualTo("value");
+ assertThat(request.queryParams().get(INSTANCE_ID)).isEqualTo("7");
+ }
+ }
+
+ private SidecarInstance mockInstance()
+ {
+ SidecarInstance instance = mock(SidecarInstance.class);
+ when(instance.port()).thenReturn(9043);
+ when(instance.hostname()).thenReturn("localhost");
+ return instance;
+ }
+
private HttpClientConfig.Builder> httpClientConfigBuilder()
{
return new HttpClientConfig.Builder<>()
diff --git a/cassandra-analytics-core/src/main/java/org/apache/cassandra/clients/AnalyticsSidecarClient.java b/cassandra-analytics-core/src/main/java/org/apache/cassandra/clients/AnalyticsSidecarClient.java
index 2096a60a9..d198a5abd 100644
--- a/cassandra-analytics-core/src/main/java/org/apache/cassandra/clients/AnalyticsSidecarClient.java
+++ b/cassandra-analytics-core/src/main/java/org/apache/cassandra/clients/AnalyticsSidecarClient.java
@@ -19,6 +19,9 @@
package org.apache.cassandra.clients;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import o.a.c.sidecar.client.shaded.io.vertx.core.Vertx;
import o.a.c.sidecar.client.shaded.io.vertx.core.VertxOptions;
import o.a.c.sidecar.client.shaded.client.HttpClientConfig;
@@ -36,6 +39,8 @@
public class AnalyticsSidecarClient
{
+ private static final Logger LOGGER = LoggerFactory.getLogger(AnalyticsSidecarClient.class);
+
private AnalyticsSidecarClient()
{
}
@@ -45,20 +50,12 @@ public static SidecarClient from(SidecarInstancesProvider sidecarInstancesProvid
Vertx vertx = Vertx.vertx(new VertxOptions().setUseDaemonThread(true)
.setWorkerPoolSize(conf.getMaxHttpConnections()));
- String userAgent = transportModeBasedWriterUserAgent(conf.getTransportInfo().getTransport());
- HttpClientConfig httpClientConfig = new HttpClientConfig.Builder<>()
- .timeoutMillis(conf.getHttpResponseTimeoutMs())
- .idleTimeoutMillis(conf.getHttpConnectionTimeoutMs())
- .userAgent(userAgent)
- .keyStoreInputStream(conf.getKeyStore())
- .keyStorePassword(conf.getKeyStorePassword())
- .keyStoreType(conf.getKeyStoreTypeOrDefault())
- .trustStoreInputStream(conf.getTrustStore())
- .trustStorePassword(conf.getTrustStorePasswordOrDefault())
- .trustStoreType(conf.getTrustStoreTypeOrDefault())
- .ssl(conf.hasKeystoreAndKeystorePassword())
- .cassandraRole(conf.getCassandraRole())
- .build();
+ HttpClientConfig httpClientConfig = buildHttpClientConfig(conf);
+ if (httpClientConfig.instanceId() != null)
+ {
+ LOGGER.info("Sidecar HTTP client configured with instanceId={} (applied to every outbound sidecar request)",
+ httpClientConfig.instanceId());
+ }
StartupValidator.instance().register(new SslValidation(conf));
StartupValidator.instance().register(new BulkWriterKeyStoreValidation(conf));
@@ -74,6 +71,25 @@ public static SidecarClient from(SidecarInstancesProvider sidecarInstancesProvid
return Sidecar.buildClient(sidecarConfig, vertx, httpClientConfig, sidecarInstancesProvider);
}
+ static HttpClientConfig buildHttpClientConfig(BulkSparkConf conf)
+ {
+ String userAgent = transportModeBasedWriterUserAgent(conf.getTransportInfo().getTransport());
+ return new HttpClientConfig.Builder<>()
+ .timeoutMillis(conf.getHttpResponseTimeoutMs())
+ .idleTimeoutMillis(conf.getHttpConnectionTimeoutMs())
+ .userAgent(userAgent)
+ .keyStoreInputStream(conf.getKeyStore())
+ .keyStorePassword(conf.getKeyStorePassword())
+ .keyStoreType(conf.getKeyStoreTypeOrDefault())
+ .trustStoreInputStream(conf.getTrustStore())
+ .trustStorePassword(conf.getTrustStorePasswordOrDefault())
+ .trustStoreType(conf.getTrustStoreTypeOrDefault())
+ .ssl(conf.hasKeystoreAndKeystorePassword())
+ .cassandraRole(conf.getCassandraRole())
+ .instanceId(conf.getSidecarInstanceId())
+ .build();
+ }
+
static String transportModeBasedWriterUserAgent(DataTransport transport)
{
switch (transport)
diff --git a/cassandra-analytics-core/src/main/java/org/apache/cassandra/spark/bulkwriter/BulkSparkConf.java b/cassandra-analytics-core/src/main/java/org/apache/cassandra/spark/bulkwriter/BulkSparkConf.java
index a01c2d1e3..02b0e92ad 100644
--- a/cassandra-analytics-core/src/main/java/org/apache/cassandra/spark/bulkwriter/BulkSparkConf.java
+++ b/cassandra-analytics-core/src/main/java/org/apache/cassandra/spark/bulkwriter/BulkSparkConf.java
@@ -125,6 +125,7 @@ public class BulkSparkConf implements Serializable
public static final String SIDECAR_REQUEST_RETRY_DELAY_MILLIS = SETTING_PREFIX + "sidecar.request.retries.delay.milliseconds";
public static final String SIDECAR_REQUEST_MAX_RETRY_DELAY_MILLIS = SETTING_PREFIX + "sidecar.request.retries.max.delay.milliseconds";
public static final String SIDECAR_REQUEST_TIMEOUT_SECONDS = SETTING_PREFIX + "sidecar.request.timeout.seconds";
+ public static final String SIDECAR_INSTANCE_ID = SETTING_PREFIX + "sidecar.instance.id";
public static final String SKIP_CLEAN = SETTING_PREFIX + "job.skip_clean";
public static final String USE_OPENSSL = SETTING_PREFIX + "use_openssl";
// defines the max number of consecutive retries allowed in the ring monitor
@@ -579,6 +580,21 @@ public int getSidecarRequestTimeoutSeconds()
return getInt(SIDECAR_REQUEST_TIMEOUT_SECONDS, DEFAULT_SIDECAR_REQUEST_TIMEOUT_SECONDS);
}
+ @Nullable
+ public Integer getSidecarInstanceId()
+ {
+ Integer value = getOptionalInt(SIDECAR_INSTANCE_ID).orElse(null);
+ // Validated here (not just in HttpClientConfig.Builder.instanceId()) to surface a
+ // Spark-conf-specific error message before any HTTP client is constructed. Keep this
+ // constraint (>= 0) in sync with that check.
+ if (value != null && value < 0)
+ {
+ throw new IllegalArgumentException("Spark conf " + SIDECAR_INSTANCE_ID
+ + " must be a non-negative integer; got " + value);
+ }
+ return value;
+ }
+
public int getHttpConnectionTimeoutMs()
{
return getInt(HTTP_CONNECTION_TIMEOUT, DEFAULT_HTTP_CONNECTION_TIMEOUT);
diff --git a/cassandra-analytics-core/src/test/java/org/apache/cassandra/clients/AnalyticsSidecarClientTest.java b/cassandra-analytics-core/src/test/java/org/apache/cassandra/clients/AnalyticsSidecarClientTest.java
new file mode 100644
index 000000000..641df2432
--- /dev/null
+++ b/cassandra-analytics-core/src/test/java/org/apache/cassandra/clients/AnalyticsSidecarClientTest.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cassandra.clients;
+
+import java.util.Map;
+
+import com.google.common.collect.Maps;
+import org.junit.jupiter.api.Test;
+
+import o.a.c.sidecar.client.shaded.client.HttpClientConfig;
+import org.apache.cassandra.spark.bulkwriter.BulkSparkConf;
+import org.apache.cassandra.spark.bulkwriter.WriterOptions;
+import org.apache.spark.SparkConf;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link AnalyticsSidecarClient}
+ */
+class AnalyticsSidecarClientTest
+{
+ @Test
+ void testBuildHttpClientConfigDefaultInstanceIdIsNull()
+ {
+ BulkSparkConf conf = new BulkSparkConf(new SparkConf(), defaultOptions());
+ HttpClientConfig httpClientConfig = AnalyticsSidecarClient.buildHttpClientConfig(conf);
+ assertThat(httpClientConfig.instanceId()).isNull();
+ }
+
+ @Test
+ void testBuildHttpClientConfigWiresInstanceId()
+ {
+ SparkConf sparkConf = new SparkConf().set(BulkSparkConf.SIDECAR_INSTANCE_ID, "9");
+ BulkSparkConf conf = new BulkSparkConf(sparkConf, defaultOptions());
+ HttpClientConfig httpClientConfig = AnalyticsSidecarClient.buildHttpClientConfig(conf);
+ assertThat(httpClientConfig.instanceId()).isEqualTo(9);
+ }
+
+ private Map defaultOptions()
+ {
+ Map options = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
+ options.put(WriterOptions.SIDECAR_CONTACT_POINTS.name(), "127.0.0.1");
+ options.put(WriterOptions.KEYSPACE.name(), "ks");
+ options.put(WriterOptions.TABLE.name(), "table");
+ options.put(WriterOptions.KEYSTORE_PASSWORD.name(), "dummy_password");
+ // Base64 of "dummy"; getKeyStore() only decodes it, it never parses the bytes as a real keystore.
+ options.put(WriterOptions.KEYSTORE_BASE64_ENCODED.name(), "ZHVtbXk=");
+ return options;
+ }
+}
diff --git a/cassandra-analytics-core/src/test/java/org/apache/cassandra/spark/bulkwriter/BulkSparkConfTest.java b/cassandra-analytics-core/src/test/java/org/apache/cassandra/spark/bulkwriter/BulkSparkConfTest.java
index 944f0a186..24a7ea5d1 100644
--- a/cassandra-analytics-core/src/test/java/org/apache/cassandra/spark/bulkwriter/BulkSparkConfTest.java
+++ b/cassandra-analytics-core/src/test/java/org/apache/cassandra/spark/bulkwriter/BulkSparkConfTest.java
@@ -169,6 +169,46 @@ void testSkipClean()
assertThat(bulkSparkConf.getSkipClean()).isTrue();
}
+ @Test
+ void testDefaultSidecarInstanceId()
+ {
+ assertThat(bulkSparkConf.getSidecarInstanceId()).isNull();
+ }
+
+ @Test
+ void testSidecarInstanceId()
+ {
+ sparkConf.set(BulkSparkConf.SIDECAR_INSTANCE_ID, "3");
+ assertThat(bulkSparkConf.getSidecarInstanceId()).isEqualTo(3);
+ }
+
+ @Test
+ void testSidecarInstanceIdZeroIsAllowed()
+ {
+ sparkConf.set(BulkSparkConf.SIDECAR_INSTANCE_ID, "0");
+ assertThat(bulkSparkConf.getSidecarInstanceId()).isEqualTo(0);
+ }
+
+ @Test
+ void testSidecarInstanceIdNegativeThrows()
+ {
+ sparkConf.set(BulkSparkConf.SIDECAR_INSTANCE_ID, "-1");
+ assertThatThrownBy(() -> bulkSparkConf.getSidecarInstanceId())
+ .isExactlyInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Spark conf " + BulkSparkConf.SIDECAR_INSTANCE_ID
+ + " must be a non-negative integer; got -1");
+ }
+
+ @Test
+ void testSidecarInstanceIdNonIntegerThrows()
+ {
+ sparkConf.set(BulkSparkConf.SIDECAR_INSTANCE_ID, "notanint");
+ assertThatThrownBy(() -> bulkSparkConf.getSidecarInstanceId())
+ .isExactlyInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Spark conf " + BulkSparkConf.SIDECAR_INSTANCE_ID
+ + " is not set to a valid integer string");
+ }
+
@Test
void testDefaultSidecarPort()
{