This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 3048642177 [Improve][Connector-V2] Migrate EdgeSocket validation to
declarative OptionRule (#11384)
3048642177 is described below
commit 304864217759e227fbbf384329eee0a046c2a33f
Author: zhiwei.niu <[email protected]>
AuthorDate: Sun Jul 12 22:47:46 2026 +0800
[Improve][Connector-V2] Migrate EdgeSocket validation to declarative
OptionRule (#11384)
---
docs/en/connectors/source/EdgeSocket.md | 1 +
docs/zh/connectors/source/EdgeSocket.md | 1 +
.../edgesocket/config/EdgeSocketConfig.java | 50 +--------
.../edgesocket/config/EdgeSocketSourceOptions.java | 26 +++--
.../edgesocket/source/EdgeSocketSourceFactory.java | 109 +++++++++++++++++-
.../edgesocket/EdgeSocketFactoryTest.java | 124 ++++++++++++++++++++-
.../edgesocket/config/EdgeSocketConfigTest.java | 118 --------------------
.../connector/edgesocket/AbstractEdgeSocketIT.java | 78 +++++++++++++
.../connector/edgesocket/EdgeSocketMysql8_4IT.java | 37 ++++++
9 files changed, 356 insertions(+), 188 deletions(-)
diff --git a/docs/en/connectors/source/EdgeSocket.md
b/docs/en/connectors/source/EdgeSocket.md
index ad701d5759..b56c5d469a 100644
--- a/docs/en/connectors/source/EdgeSocket.md
+++ b/docs/en/connectors/source/EdgeSocket.md
@@ -360,6 +360,7 @@ The following table lists connection outcomes and every
application-level respon
The diagram below shows the full collector lifecycle with all response
branches and corresponding actions.
```mermaid
+%%{init: {"theme": "base", "themeVariables": {"background": "#0f1d33",
"primaryColor": "#0c2530", "primaryBorderColor": "#2dd4bf", "primaryTextColor":
"#f8fbff", "actorBkg": "#0c2530", "actorBorder": "#2dd4bf", "actorTextColor":
"#f8fbff", "activationBkgColor": "#1f1a34", "activationBorderColor": "#8d7cf6",
"noteBkgColor": "#1f1a34", "noteBorderColor": "#8d7cf6", "noteTextColor":
"#f8fbff", "signalColor": "#5db8e2", "signalTextColor": "#f8fbff",
"labelBoxBkgColor": "#0f1d33", "labelBoxBo [...]
sequenceDiagram
participant C as Collector
participant S as EdgeSocket Source
diff --git a/docs/zh/connectors/source/EdgeSocket.md
b/docs/zh/connectors/source/EdgeSocket.md
index 69a61b0d4a..51bea57f3e 100644
--- a/docs/zh/connectors/source/EdgeSocket.md
+++ b/docs/zh/connectors/source/EdgeSocket.md
@@ -357,6 +357,7 @@ Source 回复:
下图展示采集端完整生命周期,包含所有响应分支和对应操作。
```mermaid
+%%{init: {"theme": "base", "themeVariables": {"background": "#0f1d33",
"primaryColor": "#0c2530", "primaryBorderColor": "#2dd4bf", "primaryTextColor":
"#f8fbff", "actorBkg": "#0c2530", "actorBorder": "#2dd4bf", "actorTextColor":
"#f8fbff", "activationBkgColor": "#1f1a34", "activationBorderColor": "#8d7cf6",
"noteBkgColor": "#1f1a34", "noteBorderColor": "#8d7cf6", "noteTextColor":
"#f8fbff", "signalColor": "#5db8e2", "signalTextColor": "#f8fbff",
"labelBoxBkgColor": "#0f1d33", "labelBoxBo [...]
sequenceDiagram
participant C as Collector
participant S as EdgeSocket Source
diff --git
a/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketConfig.java
b/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketConfig.java
index 048be6d25e..b51357923e 100644
---
a/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketConfig.java
+++
b/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketConfig.java
@@ -52,61 +52,15 @@ public class EdgeSocketConfig implements Serializable {
this.maxNumRetries = config.get(EdgeSocketSourceOptions.MAX_RETRIES);
this.reconnectIntervalMs =
config.get(EdgeSocketSourceOptions.RECONNECT_INTERVAL_MS);
this.acceptTimeoutMs =
config.get(EdgeSocketSourceOptions.ACCEPT_TIMEOUT_MS);
- this.packetMode =
-
EdgeSocketPacketMode.from(config.get(EdgeSocketSourceOptions.PACKET_MODE));
+ this.packetMode = config.get(EdgeSocketSourceOptions.PACKET_MODE);
this.secretKey =
config.getOptional(EdgeSocketSourceOptions.SECRET_KEY).orElse(null);
- this.authType =
EdgeSocketAuthType.from(config.get(EdgeSocketSourceOptions.AUTH_TYPE));
+ this.authType = config.get(EdgeSocketSourceOptions.AUTH_TYPE);
this.token =
config.getOptional(EdgeSocketSourceOptions.TOKEN).orElse(null);
if (this.endpoint != null && this.endpoint.trim().isEmpty()) {
this.endpoint = null;
}
- if (this.endpoint != null) {
- int separatorIndex = this.endpoint.lastIndexOf(':');
- if (separatorIndex <= 0 || separatorIndex >=
this.endpoint.length() - 1) {
- throw new IllegalArgumentException(
- "Invalid endpoint: " + this.endpoint + ", expected
format host:port");
- }
- String endpointPort = this.endpoint.substring(separatorIndex + 1);
- try {
- Integer.parseInt(endpointPort);
- } catch (NumberFormatException parseException) {
- throw new IllegalArgumentException(
- "Invalid endpoint port in endpoint: " + this.endpoint,
parseException);
- }
- }
- if (this.localQueueCapacity <= 0) {
- throw new IllegalArgumentException(
- String.format(
- "Invalid local_queue_capacity: %s, it must be
greater than 0",
- this.localQueueCapacity));
- }
- if (this.queueBackpressureWatermarkRatio <= 0 ||
this.queueBackpressureWatermarkRatio > 1) {
- throw new IllegalArgumentException(
- String.format(
- "Invalid queue_backpressure_watermark_ratio: %s, "
- + "it must be in (0, 1]",
- this.queueBackpressureWatermarkRatio));
- }
- if (this.queueFullRetryAfterMs <= 0) {
- throw new IllegalArgumentException(
- String.format(
- "Invalid queue_full_retry_after_ms: %s, it must be
greater than 0",
- this.queueFullRetryAfterMs));
- }
if (this.packetMode == EdgeSocketPacketMode.PACKET && this.secretKey
!= null) {
this.secretKeyBytes = Base64.getDecoder().decode(this.secretKey);
- if (this.secretKeyBytes.length != 32) {
- throw new IllegalArgumentException(
- String.format(
- "Invalid secret_key: AES-256 requires exactly
32 bytes, "
- + "but got %d bytes after Base64
decoding",
- this.secretKeyBytes.length));
- }
- }
- if (this.authType == EdgeSocketAuthType.TOKEN
- && (this.token == null || this.token.trim().isEmpty())) {
- throw new IllegalArgumentException(
- "Invalid token: token is required and cannot be empty when
auth_type is TOKEN");
}
}
}
diff --git
a/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketSourceOptions.java
b/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketSourceOptions.java
index 2670b36bc6..df726e0acb 100644
---
a/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketSourceOptions.java
+++
b/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketSourceOptions.java
@@ -19,6 +19,10 @@ package
org.apache.seatunnel.connectors.seatunnel.edgesocket.config;
import org.apache.seatunnel.api.configuration.Option;
import org.apache.seatunnel.api.configuration.Options;
+import org.apache.seatunnel.api.configuration.SingleChoiceOption;
+import
org.apache.seatunnel.connectors.seatunnel.edgesocket.serialize.EdgeSocketPacketMode;
+
+import java.util.Arrays;
public class EdgeSocketSourceOptions extends EdgeSocketCommonOptions {
private static final int DEFAULT_LOCAL_QUEUE_CAPACITY = 1024;
@@ -27,9 +31,6 @@ public class EdgeSocketSourceOptions extends
EdgeSocketCommonOptions {
private static final int DEFAULT_MAX_RETRIES = 3;
private static final int DEFAULT_RECONNECT_INTERVAL_MS = 1000;
private static final int DEFAULT_ACCEPT_TIMEOUT_MS = 1000;
- private static final String DEFAULT_PACKET_MODE = "RAW";
- private static final String DEFAULT_AUTH_TYPE = "TOKEN";
-
public static final Option<Integer> LOCAL_QUEUE_CAPACITY =
Options.key("local_queue_capacity")
.intType()
@@ -80,13 +81,15 @@ public class EdgeSocketSourceOptions extends
EdgeSocketCommonOptions {
"Socket accept timeout in milliseconds, default is
"
+ DEFAULT_ACCEPT_TIMEOUT_MS);
- public static final Option<String> PACKET_MODE =
+ public static final SingleChoiceOption<EdgeSocketPacketMode> PACKET_MODE =
Options.key("packet_mode")
- .stringType()
- .defaultValue(DEFAULT_PACKET_MODE)
+ .singleChoice(
+ EdgeSocketPacketMode.class,
+ Arrays.asList(EdgeSocketPacketMode.values()))
+ .defaultValue(EdgeSocketPacketMode.RAW)
.withDescription(
"Incoming packet mode, supported values: RAW,
PACKET. Default is "
- + DEFAULT_PACKET_MODE);
+ + EdgeSocketPacketMode.RAW);
public static final Option<String> SECRET_KEY =
Options.key("secret_key")
@@ -102,11 +105,12 @@ public class EdgeSocketSourceOptions extends
EdgeSocketCommonOptions {
.withDescription(
"Token value used by TOKEN auth_type. This option
is required when auth_type is TOKEN.");
- public static final Option<String> AUTH_TYPE =
+ public static final SingleChoiceOption<EdgeSocketAuthType> AUTH_TYPE =
Options.key("auth_type")
- .stringType()
- .defaultValue(DEFAULT_AUTH_TYPE)
+ .singleChoice(
+ EdgeSocketAuthType.class,
Arrays.asList(EdgeSocketAuthType.values()))
+ .defaultValue(EdgeSocketAuthType.TOKEN)
.withDescription(
"Authentication type for ingress connection.
Currently supported value: TOKEN. Default is "
- + DEFAULT_AUTH_TYPE);
+ + EdgeSocketAuthType.TOKEN);
}
diff --git
a/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/source/EdgeSocketSourceFactory.java
b/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/source/EdgeSocketSourceFactory.java
index 58fe5c45d3..95f1e09b18 100644
---
a/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/source/EdgeSocketSourceFactory.java
+++
b/seatunnel-connectors-v2/connector-edge-socket/src/main/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/source/EdgeSocketSourceFactory.java
@@ -17,7 +17,11 @@
package org.apache.seatunnel.connectors.seatunnel.edgesocket.source;
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConditionExtension;
+import org.apache.seatunnel.api.configuration.util.Conditions;
import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
import org.apache.seatunnel.api.options.ConnectorCommonOptions;
import org.apache.seatunnel.api.source.SeaTunnelSource;
import org.apache.seatunnel.api.source.SourceSplit;
@@ -26,10 +30,12 @@ import org.apache.seatunnel.api.table.factory.Factory;
import org.apache.seatunnel.api.table.factory.TableSourceFactory;
import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext;
import
org.apache.seatunnel.connectors.seatunnel.edgesocket.config.EdgeSocketSourceOptions;
+import
org.apache.seatunnel.connectors.seatunnel.edgesocket.serialize.EdgeSocketPacketMode;
import com.google.auto.service.AutoService;
import java.io.Serializable;
+import java.util.Base64;
@AutoService(Factory.class)
public class EdgeSocketSourceFactory implements TableSourceFactory {
@@ -42,19 +48,44 @@ public class EdgeSocketSourceFactory implements
TableSourceFactory {
@Override
public OptionRule optionRule() {
return OptionRule.builder()
- .required(EdgeSocketSourceOptions.PORT,
EdgeSocketSourceOptions.TOKEN)
+ .required(
+ EdgeSocketSourceOptions.PORT,
+ Conditions.greaterThan(EdgeSocketSourceOptions.PORT,
0))
+ .required(
+ EdgeSocketSourceOptions.TOKEN,
+ Conditions.notBlank(EdgeSocketSourceOptions.TOKEN))
.optional(
EdgeSocketSourceOptions.ENDPOINT,
- ConnectorCommonOptions.SCHEMA,
+ Conditions.extension(
+ EdgeSocketSourceOptions.ENDPOINT, new
EndpointValidator()))
+ .optional(ConnectorCommonOptions.SCHEMA)
+ .optional(
EdgeSocketSourceOptions.LOCAL_QUEUE_CAPACITY,
+
Conditions.greaterThan(EdgeSocketSourceOptions.LOCAL_QUEUE_CAPACITY, 0))
+ .optional(
EdgeSocketSourceOptions.QUEUE_BACKPRESSURE_WATERMARK_RATIO,
+ Conditions.greaterThan(
+
EdgeSocketSourceOptions.QUEUE_BACKPRESSURE_WATERMARK_RATIO,
+ 0.0)
+ .and(
+ Conditions.lessOrEqual(
+ EdgeSocketSourceOptions
+
.QUEUE_BACKPRESSURE_WATERMARK_RATIO,
+ 1.0)))
+ .optional(
EdgeSocketSourceOptions.QUEUE_FULL_RETRY_AFTER_MS,
+ Conditions.greaterThan(
+
EdgeSocketSourceOptions.QUEUE_FULL_RETRY_AFTER_MS, 0))
+ .optional(
+ EdgeSocketSourceOptions.SECRET_KEY,
+ Conditions.extension(
+ EdgeSocketSourceOptions.SECRET_KEY, new
SecretKeyValidator()))
+ .optional(
+ EdgeSocketSourceOptions.PACKET_MODE,
+ EdgeSocketSourceOptions.AUTH_TYPE,
EdgeSocketSourceOptions.MAX_RETRIES,
EdgeSocketSourceOptions.RECONNECT_INTERVAL_MS,
- EdgeSocketSourceOptions.ACCEPT_TIMEOUT_MS,
- EdgeSocketSourceOptions.PACKET_MODE,
- EdgeSocketSourceOptions.SECRET_KEY,
- EdgeSocketSourceOptions.AUTH_TYPE)
+ EdgeSocketSourceOptions.ACCEPT_TIMEOUT_MS)
.build();
}
@@ -69,4 +100,70 @@ public class EdgeSocketSourceFactory implements
TableSourceFactory {
public Class<? extends SeaTunnelSource> getSourceClass() {
return EdgeSocketSource.class;
}
+
+ private static class EndpointValidator implements
ConditionExtension<String> {
+
+ @Override
+ public String description() {
+ return "must be blank or in host:port format";
+ }
+
+ @Override
+ public boolean evaluate(ReadonlyConfig config, String endpoint) {
+ if (endpoint == null || endpoint.trim().isEmpty()) {
+ return true;
+ }
+ int separatorIndex = endpoint.lastIndexOf(':');
+ if (separatorIndex <= 0 || separatorIndex >= endpoint.length() -
1) {
+ throw new OptionValidationException(
+ "Invalid endpoint: %s, expected format host:port",
endpoint);
+ }
+ String endpointPort = endpoint.substring(separatorIndex + 1);
+ try {
+ Integer.parseInt(endpointPort);
+ } catch (NumberFormatException parseException) {
+ throw new OptionValidationException(
+ String.format("Invalid endpoint port in endpoint: %s",
endpoint),
+ parseException);
+ }
+ return true;
+ }
+ }
+
+ private static class SecretKeyValidator implements
ConditionExtension<String> {
+
+ @Override
+ public String description() {
+ return "must decode to exactly 32 bytes when packet_mode is
PACKET";
+ }
+
+ @Override
+ public boolean evaluate(ReadonlyConfig config, String secretKey) {
+ if (secretKey == null) {
+ return true;
+ }
+ EdgeSocketPacketMode packetMode;
+ try {
+ packetMode = config.get(EdgeSocketSourceOptions.PACKET_MODE);
+ } catch (IllegalArgumentException exception) {
+ return true;
+ }
+ if (packetMode != EdgeSocketPacketMode.PACKET) {
+ return true;
+ }
+ byte[] secretKeyBytes;
+ try {
+ secretKeyBytes = Base64.getDecoder().decode(secretKey);
+ } catch (IllegalArgumentException exception) {
+ throw new OptionValidationException("Invalid secret_key: not
Base64 encoded");
+ }
+ if (secretKeyBytes.length != 32) {
+ throw new OptionValidationException(
+ "Invalid secret_key: AES-256 requires exactly 32
bytes, "
+ + "but got %d bytes after Base64 decoding",
+ secretKeyBytes.length);
+ }
+ return true;
+ }
+ }
}
diff --git
a/seatunnel-connectors-v2/connector-edge-socket/src/test/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/EdgeSocketFactoryTest.java
b/seatunnel-connectors-v2/connector-edge-socket/src/test/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/EdgeSocketFactoryTest.java
index 3cc3075242..1985fcab2a 100644
---
a/seatunnel-connectors-v2/connector-edge-socket/src/test/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/EdgeSocketFactoryTest.java
+++
b/seatunnel-connectors-v2/connector-edge-socket/src/test/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/EdgeSocketFactoryTest.java
@@ -18,7 +18,9 @@
package org.apache.seatunnel.connectors.seatunnel.edgesocket;
import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConfigValidator;
import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
import org.apache.seatunnel.api.options.ConnectorCommonOptions;
import org.apache.seatunnel.api.source.Boundedness;
import org.apache.seatunnel.api.table.catalog.CatalogTable;
@@ -59,6 +61,105 @@ class EdgeSocketFactoryTest {
"optionRule must include queue_full_retry_after_ms");
}
+ @Test
+ void optionRuleShouldAcceptValidConfig() {
+ assertValidOptionRule(validConfig());
+ }
+
+ @Test
+ void optionRuleShouldRejectNonPositivePort() {
+ Map<String, Object> configMap = validConfig();
+ configMap.put(EdgeSocketCommonOptions.PORT.key(), 0);
+
+ assertInvalidOptionRule(configMap);
+
+ configMap.put(EdgeSocketCommonOptions.PORT.key(), -1);
+ assertInvalidOptionRule(configMap);
+ }
+
+ @Test
+ void optionRuleShouldRejectBlankToken() {
+ Map<String, Object> configMap = validConfig();
+ configMap.put(EdgeSocketSourceOptions.TOKEN.key(), " ");
+
+ assertInvalidOptionRule(configMap);
+
+ configMap = validConfig();
+ configMap.remove(EdgeSocketSourceOptions.TOKEN.key());
+ assertInvalidOptionRule(configMap);
+ }
+
+ @Test
+ void optionRuleShouldRejectInvalidEndpoint() {
+ Map<String, Object> configMap = validConfig();
+ configMap.put(EdgeSocketCommonOptions.ENDPOINT.key(),
"edge.lb.example.com");
+
+ assertInvalidOptionRule(configMap);
+
+ configMap.put(EdgeSocketCommonOptions.ENDPOINT.key(),
"edge.lb.example.com:abc");
+ assertInvalidOptionRule(configMap);
+ }
+
+ @Test
+ void optionRuleShouldAllowBlankEndpoint() {
+ Map<String, Object> configMap = validConfig();
+ configMap.put(EdgeSocketCommonOptions.ENDPOINT.key(), " ");
+
+ assertValidOptionRule(configMap);
+ }
+
+ @Test
+ void optionRuleShouldRejectInvalidBackpressureOptions() {
+ Map<String, Object> configMap = validConfig();
+ configMap.put(EdgeSocketSourceOptions.LOCAL_QUEUE_CAPACITY.key(), 0);
+ assertInvalidOptionRule(configMap);
+
+ configMap = validConfig();
+
configMap.put(EdgeSocketSourceOptions.QUEUE_BACKPRESSURE_WATERMARK_RATIO.key(),
0.0);
+ assertInvalidOptionRule(configMap);
+
+ configMap = validConfig();
+
configMap.put(EdgeSocketSourceOptions.QUEUE_BACKPRESSURE_WATERMARK_RATIO.key(),
1.01);
+ assertInvalidOptionRule(configMap);
+
+ configMap = validConfig();
+ configMap.put(EdgeSocketSourceOptions.QUEUE_FULL_RETRY_AFTER_MS.key(),
0);
+ assertInvalidOptionRule(configMap);
+ }
+
+ @Test
+ void optionRuleShouldRejectInvalidPacketAndAuthMode() {
+ Map<String, Object> configMap = validConfig();
+ configMap.put(EdgeSocketSourceOptions.PACKET_MODE.key(),
"unsupported");
+ assertInvalidOptionRuleOrType(configMap);
+
+ configMap = validConfig();
+ configMap.put(EdgeSocketSourceOptions.AUTH_TYPE.key(), "unsupported");
+ assertInvalidOptionRuleOrType(configMap);
+ }
+
+ @Test
+ void optionRuleShouldValidateSecretKeyOnlyInPacketMode() {
+ Map<String, Object> configMap = validConfig();
+ configMap.put(EdgeSocketSourceOptions.PACKET_MODE.key(), "PACKET");
+ configMap.put(EdgeSocketSourceOptions.SECRET_KEY.key(), "c2hvcnQ=");
+
+ assertInvalidOptionRule(configMap);
+
+ configMap.put(EdgeSocketSourceOptions.SECRET_KEY.key(),
"invalid-not-base64!!!");
+ assertInvalidOptionRule(configMap);
+
+ configMap.put(
+ EdgeSocketSourceOptions.SECRET_KEY.key(),
+ "dGVzdC1zZWNyZXQta2V5LTMyLWJ5dGVzLWFlczI1NiE=");
+ assertValidOptionRule(configMap);
+
+ configMap = validConfig();
+ configMap.put(EdgeSocketSourceOptions.PACKET_MODE.key(), "RAW");
+ configMap.put(EdgeSocketSourceOptions.SECRET_KEY.key(), "c2hvcnQ=");
+ assertValidOptionRule(configMap);
+ }
+
@Test
void shouldUseNullableEndpointAndQueueCapacity() {
Map<String, Object> configMap = new HashMap<>();
@@ -109,15 +210,28 @@ class EdgeSocketFactoryTest {
Assertions.assertEquals("edge.lb.example.com:10091",
config.getEndpoint());
}
- @Test
- void shouldRejectInvalidEndpoint() {
+ private static Map<String, Object> validConfig() {
Map<String, Object> configMap = new HashMap<>();
configMap.put(EdgeSocketCommonOptions.PORT.key(), 10001);
configMap.put(EdgeSocketSourceOptions.TOKEN.key(), "token");
- configMap.put(EdgeSocketCommonOptions.ENDPOINT.key(),
"edge.lb.example.com");
+ return configMap;
+ }
+ private static void assertValidOptionRule(Map<String, Object> configMap) {
+ Assertions.assertDoesNotThrow(() -> validateOptionRule(configMap));
+ }
+
+ private static void assertInvalidOptionRule(Map<String, Object> configMap)
{
Assertions.assertThrows(
- IllegalArgumentException.class,
- () -> new EdgeSocketConfig(ReadonlyConfig.fromMap(configMap)));
+ OptionValidationException.class, () ->
validateOptionRule(configMap));
+ }
+
+ private static void assertInvalidOptionRuleOrType(Map<String, Object>
configMap) {
+ Assertions.assertThrows(RuntimeException.class, () ->
validateOptionRule(configMap));
+ }
+
+ private static void validateOptionRule(Map<String, Object> configMap) {
+ ConfigValidator.of(ReadonlyConfig.fromMap(configMap))
+ .validate(new EdgeSocketSourceFactory().optionRule());
}
}
diff --git
a/seatunnel-connectors-v2/connector-edge-socket/src/test/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketConfigTest.java
b/seatunnel-connectors-v2/connector-edge-socket/src/test/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketConfigTest.java
index a7404e98e2..1ea1e301da 100644
---
a/seatunnel-connectors-v2/connector-edge-socket/src/test/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketConfigTest.java
+++
b/seatunnel-connectors-v2/connector-edge-socket/src/test/java/org/apache/seatunnel/connectors/seatunnel/edgesocket/config/EdgeSocketConfigTest.java
@@ -46,42 +46,6 @@ class EdgeSocketConfigTest {
Assertions.assertEquals("collector.example.com:8080",
config.getEndpoint());
}
- @Test
- void endpointWithoutPortThrows() {
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketCommonOptions.ENDPOINT.key(), "no-port-host");
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
- Assertions.assertTrue(ex.getMessage().contains("expected format
host:port"));
- }
-
- @Test
- void endpointWithNonNumericPortThrows() {
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketCommonOptions.ENDPOINT.key(), "host:abc");
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
- Assertions.assertTrue(ex.getMessage().contains("Invalid endpoint
port"));
- }
-
- @Test
- void endpointColonOnlyThrows() {
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketCommonOptions.ENDPOINT.key(), ":8080");
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
- Assertions.assertTrue(ex.getMessage().contains("expected format
host:port"));
- }
-
- @Test
- void endpointTrailingColonThrows() {
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketCommonOptions.ENDPOINT.key(), "host:");
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
- Assertions.assertTrue(ex.getMessage().contains("expected format
host:port"));
- }
-
@Test
void blankEndpointTreatedAsNull() {
Map<String, Object> map = minimalConfig();
@@ -90,24 +54,6 @@ class EdgeSocketConfigTest {
Assertions.assertNull(config.getEndpoint());
}
- @Test
- void localQueueCapacityZeroThrows() {
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketSourceOptions.LOCAL_QUEUE_CAPACITY.key(), 0);
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
-
Assertions.assertTrue(ex.getMessage().contains("local_queue_capacity"));
- }
-
- @Test
- void localQueueCapacityNegativeThrows() {
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketSourceOptions.LOCAL_QUEUE_CAPACITY.key(), -5);
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
- Assertions.assertTrue(ex.getMessage().contains("greater than 0"));
- }
-
@Test
void validSecretKey32Bytes() {
byte[] key = "test-secret-key-32-bytes-aes256!".getBytes();
@@ -123,34 +69,6 @@ class EdgeSocketConfigTest {
Assertions.assertEquals(32, config.getSecretKeyBytes().length);
}
- @Test
- void secretKeyTooShortThrows() {
- byte[] shortKey = "only-16-bytes!!!".getBytes();
- Assertions.assertEquals(16, shortKey.length);
- String base64Key = Base64.getEncoder().encodeToString(shortKey);
-
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketSourceOptions.PACKET_MODE.key(), "PACKET");
- map.put(EdgeSocketSourceOptions.SECRET_KEY.key(), base64Key);
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
- Assertions.assertTrue(ex.getMessage().contains("exactly 32 bytes"));
- }
-
- @Test
- void secretKeyTooLongThrows() {
- byte[] longKey =
"this-key-is-48-bytes-long-and-way-too-big-for-it".getBytes();
- Assertions.assertEquals(48, longKey.length);
- String base64Key = Base64.getEncoder().encodeToString(longKey);
-
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketSourceOptions.PACKET_MODE.key(), "PACKET");
- map.put(EdgeSocketSourceOptions.SECRET_KEY.key(), base64Key);
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
- Assertions.assertTrue(ex.getMessage().contains("exactly 32 bytes"));
- }
-
@Test
void secretKeyIgnoredInRawMode() {
Map<String, Object> map = minimalConfig();
@@ -186,15 +104,6 @@ class EdgeSocketConfigTest {
Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
}
- @Test
- void missingTokenThrowsWhenAuthTypeIsToken() {
- Map<String, Object> map = minimalConfig();
- map.remove(EdgeSocketSourceOptions.TOKEN.key());
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
- Assertions.assertTrue(ex.getMessage().contains("token is required"));
- }
-
@Test
void defaultBackpressureOptions() {
EdgeSocketConfig config = createConfig(minimalConfig());
@@ -202,33 +111,6 @@ class EdgeSocketConfigTest {
Assertions.assertEquals(500, config.getQueueFullRetryAfterMs());
}
- @Test
- void invalidQueueBackpressureWatermarkRatioThrows() {
- Map<String, Object> map = minimalConfig();
-
map.put(EdgeSocketSourceOptions.QUEUE_BACKPRESSURE_WATERMARK_RATIO.key(), 1.5);
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
-
Assertions.assertTrue(ex.getMessage().contains("queue_backpressure_watermark_ratio"));
- }
-
- @Test
- void invalidQueueFullRetryAfterMsThrows() {
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketSourceOptions.QUEUE_FULL_RETRY_AFTER_MS.key(), 0);
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
-
Assertions.assertTrue(ex.getMessage().contains("queue_full_retry_after_ms"));
- }
-
- @Test
- void blankTokenThrowsWhenAuthTypeIsToken() {
- Map<String, Object> map = minimalConfig();
- map.put(EdgeSocketSourceOptions.TOKEN.key(), " ");
- IllegalArgumentException ex =
- Assertions.assertThrows(IllegalArgumentException.class, () ->
createConfig(map));
- Assertions.assertTrue(ex.getMessage().contains("token is required"));
- }
-
private static Map<String, Object> minimalConfig() {
Map<String, Object> map = new HashMap<>();
map.put(EdgeSocketCommonOptions.PORT.key(), 9999);
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-edge-socket-e2e/src/test/java/org/apache/seatunnel/e2e/connector/edgesocket/AbstractEdgeSocketIT.java
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-edge-socket-e2e/src/test/java/org/apache/seatunnel/e2e/connector/edgesocket/AbstractEdgeSocketIT.java
index ba43cdde96..60cbe7dec8 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-edge-socket-e2e/src/test/java/org/apache/seatunnel/e2e/connector/edgesocket/AbstractEdgeSocketIT.java
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-edge-socket-e2e/src/test/java/org/apache/seatunnel/e2e/connector/edgesocket/AbstractEdgeSocketIT.java
@@ -169,6 +169,56 @@ public abstract class AbstractEdgeSocketIT extends
TestSuiteBase implements Test
throw new IllegalStateException("Send records to edge socket timed
out");
}
+ protected List<Long>
sendRecordsThroughCollectorAndCollectWatermarks(List<String> messages)
+ throws Exception {
+ if (messages == null || messages.isEmpty()) {
+ throw new IllegalArgumentException("Messages should not be empty");
+ }
+ if (edgeSocketForwarderContainer == null) {
+ throw new IllegalStateException("Edge socket forwarder container
is not initialized");
+ }
+ String forwarderHost = edgeSocketForwarderContainer.getHost();
+ int forwarderPort =
edgeSocketForwarderContainer.getMappedPort(EDGE_FORWARDER_PORT);
+ long deadlineMillis = System.currentTimeMillis() +
TimeUnit.SECONDS.toMillis(60);
+ while (System.currentTimeMillis() < deadlineMillis) {
+ try (Socket socket = new Socket(forwarderHost, forwarderPort);
+ BufferedWriter writer =
+ new BufferedWriter(
+ new OutputStreamWriter(
+ socket.getOutputStream(),
StandardCharsets.UTF_8));
+ BufferedReader reader =
+ new BufferedReader(
+ new InputStreamReader(
+ socket.getInputStream(),
StandardCharsets.UTF_8))) {
+ socket.setSoTimeout(3000);
+ writeLine(writer, "__AUTH__:" + AUTH_TOKEN);
+ String authReply = readLine(reader);
+ Assertions.assertEquals("ACK", authReply, "Auth response
should be ACK");
+
+ List<Long> watermarks = new ArrayList<>();
+ long batchId = 1L;
+ for (String message : messages) {
+ sendMessageWithRetry(writer, reader, batchId, message);
+ watermarks.add(
+ awaitBatchCheckpointAckAndReturnWatermark(writer,
reader, batchId));
+ batchId++;
+ }
+ return watermarks;
+ } catch (SocketTimeoutException timeoutException) {
+ if (System.currentTimeMillis() >= deadlineMillis) {
+ throw timeoutException;
+ }
+ TimeUnit.MILLISECONDS.sleep(200);
+ } catch (IOException ioException) {
+ if (System.currentTimeMillis() >= deadlineMillis) {
+ throw ioException;
+ }
+ TimeUnit.MILLISECONDS.sleep(500);
+ }
+ }
+ throw new IllegalStateException("Send records to edge socket timed
out");
+ }
+
protected void awaitSinkContainsExpectedMessages(List<String>
expectedMessages) {
if (expectedMessages == null || expectedMessages.isEmpty()) {
throw new IllegalArgumentException("Expected messages should not
be empty");
@@ -379,6 +429,34 @@ public abstract class AbstractEdgeSocketIT extends
TestSuiteBase implements Test
"Timeout waiting checkpoint ACK for batch: " +
expectedBatchId);
}
+ private long awaitBatchCheckpointAckAndReturnWatermark(
+ BufferedWriter writer, BufferedReader reader, long
expectedBatchId) throws Exception {
+ long deadlineMillis = System.currentTimeMillis() +
TimeUnit.SECONDS.toMillis(30);
+ while (System.currentTimeMillis() < deadlineMillis) {
+ writeLine(writer, EDGE_COMMIT_PREFIX + expectedBatchId);
+ String reply = readLine(reader);
+ if ("PENDING".equals(reply)) {
+ TimeUnit.MILLISECONDS.sleep(200);
+ continue;
+ }
+ if (reply.startsWith("ACK:")) {
+ long ackedBatchId =
Long.parseLong(reply.substring("ACK:".length()));
+ if (ackedBatchId >= expectedBatchId) {
+ return ackedBatchId;
+ }
+ TimeUnit.MILLISECONDS.sleep(200);
+ continue;
+ }
+ if ("RETRY".equals(reply)) {
+ TimeUnit.MILLISECONDS.sleep(200);
+ continue;
+ }
+ throw new IllegalStateException("Unexpected commit response: " +
reply);
+ }
+ throw new IllegalStateException(
+ "Timeout waiting checkpoint ACK for batch: " +
expectedBatchId);
+ }
+
/**
* Send messages using PACKET mode framing (base64 + NONE compression, no
encryption). Each
* plain-text message is base64-encoded and wrapped in a JSON envelope
before sending.
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-edge-socket-e2e/src/test/java/org/apache/seatunnel/e2e/connector/edgesocket/EdgeSocketMysql8_4IT.java
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-edge-socket-e2e/src/test/java/org/apache/seatunnel/e2e/connector/edgesocket/EdgeSocketMysql8_4IT.java
index 27d7b6d9d9..ba39d59c1a 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-edge-socket-e2e/src/test/java/org/apache/seatunnel/e2e/connector/edgesocket/EdgeSocketMysql8_4IT.java
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-edge-socket-e2e/src/test/java/org/apache/seatunnel/e2e/connector/edgesocket/EdgeSocketMysql8_4IT.java
@@ -36,6 +36,7 @@ import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -169,6 +170,42 @@ public class EdgeSocketMysql8_4IT extends
AbstractEdgeSocketIT {
}
}
+ @TestTemplate
+ public void testEdgeSocketCommitWatermarkAdvances(TestContainer container)
throws Exception {
+ String jobId = String.valueOf(JobIdGenerator.newJobId());
+ CompletableFuture<Container.ExecResult> jobFuture =
+ CompletableFuture.supplyAsync(
+ () -> {
+ try {
+ return container.executeJob(
+ "/edge_socket_source_to_mysql84.conf",
jobId);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ try {
+ Awaitility.await()
+ .atMost(60, TimeUnit.SECONDS)
+ .untilAsserted(
+ () ->
assertJobRunningOrSubmissionFailed(container, jobId, jobFuture));
+ ensureEdgeSocketForwarder();
+
+ List<String> sourceMessages = buildPlainTextMessages(3,
"watermark-value");
+ List<String> expectedMessages =
buildExpectedTransformedMessages(sourceMessages);
+ List<Long> watermarks =
sendRecordsThroughCollectorAndCollectWatermarks(sourceMessages);
+
+ Assertions.assertEquals(Arrays.asList(1L, 2L, 3L), watermarks);
+ awaitSinkContainsExpectedMessages(expectedMessages);
+ } finally {
+ Container.ExecResult cancelResult = container.cancelJob(jobId);
+ Assertions.assertEquals(0, cancelResult.getExitCode(),
cancelResult.getStderr());
+
+ Container.ExecResult jobResult = waitForJobResult(jobFuture);
+ Assertions.assertEquals(0, jobResult.getExitCode(),
jobResult.getStderr());
+ }
+ }
+
@TestTemplate
public void testEdgeSocketPacketModeToMysql84(TestContainer container)
throws Exception {
String jobId = String.valueOf(JobIdGenerator.newJobId());