blob: ad701d575957a309211342354d2354769950a263 [file] [view]
import ChangeLog from '../changelog/connector-edge-socket.md';
# EdgeSocket
> A streaming source connector that accepts data pushed by lightweight edge collectors over socket.
## Supported Engines
> SeaTunnel Zeta
## Key features
- [ ] [batch](../../introduction/concepts/connector-v2-features.md)
- [x] [stream](../../introduction/concepts/connector-v2-features.md)
- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
- [ ] [column projection](../../introduction/concepts/connector-v2-features.md)
- [ ] [parallelism](../../introduction/concepts/connector-v2-features.md)
- [ ] [support user-defined split](../../introduction/concepts/connector-v2-features.md)
## Description
EdgeSocket listens on a TCP port inside a Zeta worker and accepts connections from edge collectors.
Each collector pushes batches of records over the socket, and the source enqueues them for downstream processing.
Because the source binds a fixed TCP port, parallelism must be set to 1. If parallelism were greater than 1, multiple readers would attempt to bind the same port and all but one would fail.
The source uses a push model: collectors dial in to the source, not the other way around.
This means the network must allow collector → worker TCP connectivity.
:::caution Single Collector Restriction
Each EdgeSocket source reader accepts exactly one collector connection at a time.
Connection behavior, REJECTED handling, and retry guidance are described in the "Collector Protocol" section below.
If multiple collectors need to push data into the same job, create separate jobs for each collector.
:::
## Options
| name | type | required | default | description |
|-----------------------|---------|----------|---------|-------------|
| port | Integer | Yes | - | TCP port the source binds on the Zeta worker. |
| token | String | Yes | - | Shared token for collector authentication. Collectors must send `__AUTH__:<token>` as the first line before any data. |
| auth_type | String | No | TOKEN | Authentication mode. Only TOKEN is supported in the current release. |
| packet_mode | String | No | RAW | Ingress framing mode. RAW: each line is a plain-text payload. PACKET: each line is a JSON envelope (see [Packet Protocol](#packet-protocol)). |
| secret_key | String | No | - | Base64-encoded AES-256 key (32 bytes). Required when packet_mode = PACKET and encryption = AES_GCM. Both sides must use the same value. |
| local_queue_capacity | Integer | No | 1024 | Maximum number of records held in the in-memory queue. Must be greater than 0. See [Tuning Guide](#tuning-guide). |
| queue_backpressure_watermark_ratio | Double | No | 0.9 | High-water mark ratio of local_queue_capacity. When queue size reaches ceil(capacity × ratio), the source replies QUEUE_FULL without decoding the payload. Must be in (0, 1]. |
| queue_full_retry_after_ms | Integer | No | 500 | Backoff in milliseconds embedded in QUEUE_FULL responses (format `QUEUE_FULL:<ms>`). Must be greater than 0. |
| max_retries | Integer | No | 3 | How many times the source retries binding the port on failure before giving up. Set to -1 for unlimited retries. |
| reconnect_interval_ms | Integer | No | 1000 | Milliseconds to wait between source-side port bind retry attempts; this controls bind retries after source startup failure, not collector reconnect intervals. |
| accept_timeout_ms | Integer | No | 1000 | Socket accept/read timeout in milliseconds. The source loops on timeout so it can check its own state; this does not mean the source exits on timeout. |
| endpoint | String | No | - | Externally reachable address of this ingress in host:port format (for example a K8s LoadBalancer DNS, VPC EIP, or NAT address). This field does not change the local bind address (always 0.0.0.0:port); it is for observability only. |
| schema | Config | No | - | Output schema definition. When omitted, the source outputs a single STRING field named value. When set, the incoming payload is parsed as JSON and mapped to the declared fields. See [Schema Mode](#schema-mode). |
| common-options | - | No | - | Common source options. See [Source Common Options](../common-options/source-common-options.md). |
## Quick Start
### Minimal configuration
```hocon
source {
EdgeSocket {
port = 9999
token = "my-edge-token"
}
}
```
### Full example with downstream sink
```hocon
env {
parallelism = 1
job.mode = "STREAMING"
}
source {
EdgeSocket {
port = 9999
token = "my-edge-token"
local_queue_capacity = 2048
}
}
sink {
Kafka {
topic = "edge-events"
bootstrap.servers = "kafka:9092"
}
}
```
## Schema Mode
By default the source emits one STRING field (value) containing the raw payload line.
To parse JSON payloads into typed fields, declare a schema:
```hocon
source {
EdgeSocket {
port = 9999
token = "my-edge-token"
schema {
fields {
user_id = "int"
event = "string"
ts = "long"
}
}
}
}
```
Incoming payload must be valid JSON matching the declared fields; a schema mismatch or parse failure causes the job to fail with an exception (fail-fast). The source does not silently drop bad records.
## Network Setup
The source listens passively; collectors must be able to reach the worker's TCP port.
:::tip Source task migration and address stability
EdgeSocket binds the local port of the worker that runs the source task. After a job restart, if the source task migrates to another worker, the collector target address becomes invalid immediately.
Deployment requirements:
- K8s: use a LoadBalancer Service (recommended). See [K8s deployment](#k8s-deployment).
- VM / bare-metal: use Zeta tag_filter to pin the source to a fixed worker.
:::
<details>
<summary>VM / bare-metal: pin source via tag_filter</summary>
Step 1 — label the target worker in hazelcast.yaml:
```yaml
hazelcast:
member-attributes:
edge-ingress:
type: string
value: "true"
```
Step 2 — add tag_filter to the job so the source always lands on that worker:
```hocon
env {
job.mode = "STREAMING"
tag_filter {
edge-ingress = "true"
}
}
source {
EdgeSocket {
port = 10091
token = "edge-token"
endpoint = "192.168.1.10:10091"
}
}
```
The job scheduler allocates slots only on workers whose attributes fully match all tag_filter entries.
</details>
### Common deployment scenarios
| Deployment | Reachable by default | Recommendation | What to do |
|---|---|---|---|
| Collector and worker in the same VPC / private network | Yes | - | Configure the collector to dial worker-ip:port directly. |
| Collector and worker in different VPCs (routing opened) | Yes | - | Configure collector to dial the worker IP. Consider setting endpoint for logging clarity. |
| Worker behind VPC EIP or NAT | Yes (via public address) | - | Configure the collector to dial the EIP/NAT address. Set endpoint = eip:port. |
| Worker behind K8s LoadBalancer | Yes (via LB address) | Recommended | Configure the collector to dial the LB DNS or IP. Set endpoint = lb-dns:port. |
| Worker on K8s with node taint + tag pinning | Yes (via node IP) | Not recommended | Pin pod to a fixed node via taint/nodeSelector + SeaTunnel tag_filter. Fragile — node failure blocks the job. |
| Worker in a network with no inbound path at all | No | - | Expose a reachable ingress first (EIP / LB / NAT / reverse tunnel), then configure collectors to dial that ingress. |
### K8s deployment
There are two main approaches for exposing the EdgeSocket port in Kubernetes:
#### Approach 1: LoadBalancer / ELB (recommended)
Create a Service of type LoadBalancer pointing to the Zeta worker pod. The collector dials the external LB address, and traffic is routed to the correct pod automatically.
This is the recommended approach because:
- The collector address remains stable regardless of pod rescheduling.
- No need to constrain pod scheduling with node affinity or taints.
- Works naturally with managed Kubernetes (EKS, GKE, AKS, etc.).
```hocon
source {
EdgeSocket {
port = 10091
token = "edge-token"
local_queue_capacity = 2048
endpoint = "edge-lb.prod.example.com:10091"
}
}
```
#### Approach 2: Node taint + SeaTunnel tag (not recommended)
Use a Kubernetes node taint / nodeSelector to pin the Zeta worker pod to a specific node, combined with SeaTunnel's tag_filter to ensure the source task always lands on that worker. The collector dials the node's IP directly.
:::warning
This approach is not recommended for production because:
- It couples the job to a specific physical node; if the node goes down, the job cannot failover.
- It wastes cluster resources by restricting scheduling flexibility.
- It requires manual network management (firewall rules per node).
Only use this approach when a LoadBalancer is not available (e.g. on-prem bare-metal K8s without MetalLB).
:::
```hocon
env {
job.mode = "STREAMING"
tag_filter {
edge-ingress = "true"
}
}
source {
EdgeSocket {
port = 10091
token = "edge-token"
endpoint = "192.168.1.50:10091"
}
}
```
### VM / bare-metal example
```hocon
source {
EdgeSocket {
port = 10091
token = "edge-token"
# Collector dials this worker's private IP directly; no endpoint needed
}
}
```
## Collector Protocol
Collectors connect to the source over plain TCP and use a line-based text protocol.
### Connection
The source accepts exactly one collector at a time. Two mechanisms enforce this:
1. Once a collector session is established, the server socket is suspended (closed). Subsequent connection attempts fail at the TCP level (Connection refused).
2. In a narrow race window — if a second collector was already waiting in the TCP backlog when the first was accepted — the source replies REJECTED at application level.
After the active collector disconnects, the server socket reopens and a new collector can connect.
### Authentication
After a successful TCP connection, the first line sent by the collector must be:
```
__AUTH__:<token>
```
The source replies:
- ACK — authentication succeeded; the collector can now send data.
- AUTH_FAILED — wrong token; the collector must reconnect with the correct token.
### Sending batches
After authentication, send one batch per line:
```
__BATCH__:<batchId>:<payload>
```
The source replies:
- RECEIVED — batch accepted and queued.
- `QUEUE_FULL:<ms>` — queue reached the backpressure watermark; wait at least `<ms>` milliseconds (from queue_full_retry_after_ms) and resend the same batch unchanged. The source does not decode the payload in this case.
- BAD_REQUEST — the request format is invalid (unrecognized command prefix, missing payload separator, etc.). The collector should correct the request format and resend.
- INVALID_PARAM — a request parameter is invalid (e.g. a non-positive batchId). The collector should correct the parameter and resend.
- RETRY — the internal queue is full (extremely rare; occurs only during a watermark race window). The collector should apply exponential back-off and resend the same batch.
- DECODE_FAILED — payload decoding failed (e.g. corrupted compressed data, or invalid JSON in PACKET mode). The collector should verify the payload content and resend after correction.
- DECRYPT_FAILED — payload decryption failed. The collector should stop sending and verify that both sides use the same secret_key.
### Backpressure
When queue size reaches ceil(local_queue_capacity × queue_backpressure_watermark_ratio) (default 90%), the source replies `QUEUE_FULL:<ms>` at application level without decoding the payload. `<ms>` comes from queue_full_retry_after_ms (default 500).
QUEUE_FULL differs from RETRY: QUEUE_FULL is a backpressure signal triggered when downstream consumption cannot keep up, at the high-water mark threshold. RETRY is returned only when the queue's physical capacity is exhausted (extremely rare during the watermark race window). BAD_REQUEST and INVALID_PARAM indicate protocol format or parameter errors. DECODE_FAILED and DECRYPT_FAILED indicate that the payload content cannot be processed correctly.
### Polling for checkpoint confirmation
RECEIVED means the batch is enqueued only, not checkpoint-confirmed. If the worker restarts before the next completed checkpoint, records still in the in-memory queue are lost. `__COMMIT__` is optional; collectors can use only `__BATCH__` → RECEIVED. If `__COMMIT__` is enabled, keep local buffers until `ACK:<watermark>` and evict by the returned watermark.
**batchId must be globally monotonic** for the lifetime of the logical source, including reconnects and worker restarts. After a restart, the collector must resume from a batchId strictly above the last `ACK:<watermark>` it received — never restart from 1. This lets the source use its committed watermark as an unambiguous ACK boundary.
```
__COMMIT__:<batchId>
```
The source replies:
- PENDING — batch received but not yet checkpoint-confirmed. Keep the batch; poll again.
- `ACK:<watermarkBatchId>` — all batches up to watermarkBatchId are checkpoint-confirmed. Discard buffered batches whose batchId ≤ watermarkBatchId.
- RESEND — this batchId falls within the previous session's received batch-id range but is absent from the current session state (e.g. the worker restarted and the batch was lost from the in-memory queue). Resend via `__BATCH__:<batchId>:<payload>` before polling `__COMMIT__` again.
- RETRY — no `__BATCH__` has been received for this batchId yet on this reader. Wait and poll `__COMMIT__` again after the batch has been sent.
- BAD_REQUEST — the request format is invalid. The collector should correct the format and resend.
- INVALID_PARAM — the batchId is invalid (non-positive integer). The collector should correct the parameter and resend.
Example send loop (includes optional `__COMMIT__`):
```
→ __AUTH__:my-token
← ACK
→ __BATCH__:1:{"event":"pageview","user":"alice"}
← RECEIVED
→ __COMMIT__:1
← PENDING (wait, then retry commit)
→ __COMMIT__:1
← ACK:1 (confirmed; move on to next batch)
```
### Response Code Reference
The following table lists connection outcomes and every application-level response the source can send, with required collector actions in the last column.
| Response | Triggered by | Meaning | Collector action |
|---|---|---|---|
| ACK | `__AUTH__` | Authentication succeeded. | Begin sending batches. |
| AUTH_FAILED | `__AUTH__` | Wrong or missing token. | Reconnect with the correct token. |
| Connection refused | Connection | Server socket is suspended; connect fails at TCP level (see [Connection](#connection)). | Verify network reachability, active collector state, and listener status before reconnecting. |
| REJECTED | Connection | Accepted through TCP backlog race while another collector is active (see [Connection](#connection)). | Stop immediately; verify no other collector instance is running. Do not retry automatically. |
| RECEIVED | `__BATCH__` | Record accepted and queued. | Continue sending. If checkpoint-watermark-aligned buffer eviction is required, enable `__COMMIT__`, poll until ACK, and evict by the returned watermark. |
| `QUEUE_FULL:<ms>` | `__BATCH__` | Queue reached backpressure watermark (see [Backpressure](#backpressure)). | Wait at least `<ms>` milliseconds, then resend the same batch unchanged. |
| BAD_REQUEST | `__BATCH__` or `__COMMIT__` | Request format is invalid (unrecognized command prefix, missing payload separator, etc.). | Correct the request format and resend. |
| INVALID_PARAM | `__BATCH__` or `__COMMIT__` | A request parameter is invalid (e.g. non-positive batchId). | Correct the parameter and resend. |
| RETRY | `__BATCH__` or `__COMMIT__` | `__BATCH__`: internal queue is full (extremely rare; occurs only during the watermark race window). `__COMMIT__`: no `__BATCH__` has been received for this batchId yet. | For `__BATCH__`: apply exponential back-off, then resend the same batch unchanged. For `__COMMIT__`: wait and poll again after the batch has been sent. |
| DECODE_FAILED | `__BATCH__` | Payload decoding failed (e.g. corrupted compressed data, or invalid JSON in PACKET mode). | Verify the payload content and resend after correction. |
| DECRYPT_FAILED | `__BATCH__` | Decryption failed, typically because the collector's key does not match the source's secret_key. | Stop sending and verify both sides use the same secret_key. |
| PENDING | `__COMMIT__` | Batch reached the source but is not yet covered by a completed checkpoint. | Keep the batch in local buffer; wait and poll `__COMMIT__` again. |
| RESEND | `__COMMIT__` | This batchId falls within the previous session's received batch-id range but is absent from the current session state (e.g. the worker restarted and the batch was lost). | Resend the batch via `__BATCH__:<batchId>:<payload>` before polling `__COMMIT__` again. |
| ACK:watermarkBatchId | `__COMMIT__` | All batches with batchId ≤ watermarkBatchId are checkpoint-confirmed. | Discard buffered batches whose batchId ≤ watermarkBatchId. |
> Connection refused is a TCP connect failure, not a line-protocol response; it is included alongside REJECTED to document both connection outcomes in the single-collector model.
>
> batchId format: decimal positive integer in Java long range (1 – 9223372036854775807). Non-numeric, zero, or negative values cause an INVALID_PARAM response.
>
> **batchId monotonicity**: values must increase monotonically for the full lifetime of the logical source, including reconnects and worker restarts. After reconnecting, resume from a value strictly above the last `ACK:<watermark>` received — never reset to 1.
>
> `ACK:<watermarkBatchId>` note: the returned number is the source's current checkpoint watermark, and it can be higher than the queried batchId. Always use the returned watermark for buffer eviction.
### Collector Interaction Sequence
The diagram below shows the full collector lifecycle with all response branches and corresponding actions.
```mermaid
sequenceDiagram
participant C as Collector
participant S as EdgeSocket Source
Note over C,S: Phase 1 - Connection
C->>S: TCP connect
alt Server socket suspended
Note over C: TCP connect fails - investigate before reconnecting
else TCP backlog race window
S-->>C: REJECTED
Note over C: Stop - verify no other collector instance
end
Note over C,S: Phase 2 - Authentication
C->>S: __AUTH__:my-edge-token
alt Auth success
S-->>C: ACK
else Wrong token
S-->>C: AUTH_FAILED
Note over C: Disconnect, fix token and reconnect
end
Note over C,S: Phase 3 - Send Data
loop Send records
Note over C: RAW mode: plain text / PACKET mode: JSON with optional AES-GCM
C->>S: __BATCH__:batchId:payload
alt Enqueued
S-->>C: RECEIVED
Note over C: Continue sending - RECEIVED is the Phase 1 success signal
else Backpressure watermark reached
S-->>C: QUEUE_FULL:ms
Note over C: Wait ms then resend same batch
else Invalid request format
S-->>C: BAD_REQUEST
Note over C: Correct the request format and resend
else Invalid parameter
S-->>C: INVALID_PARAM
Note over C: Correct the parameter and resend
else Internal queue full (extremely rare)
S-->>C: RETRY
Note over C: Exponential back-off then resend
else Payload decode failure
S-->>C: DECODE_FAILED
Note over C: Verify payload content and resend after correction
else Decryption failed
S-->>C: DECRYPT_FAILED
Note over C: Stop - verify secret_key on both sides
end
end
opt Phase 4 - Poll __COMMIT__ (optional)
Note over C,S: Parallel with Phase 3 — batchId must be globally monotonic across restarts
loop Poll __COMMIT__
C->>S: __COMMIT__:batchId
alt PENDING
S-->>C: PENDING
Note over C: Keep buffer, retry __COMMIT__ later
else ACK
S-->>C: ACK:watermarkBatchId
Note over C: Evict buffer where batchId <= watermark
else RESEND
S-->>C: RESEND
Note over C: Batch lost on restart — resend via __BATCH__ then re-poll
else RETRY
S-->>C: RETRY
Note over C: No __BATCH__ received yet — wait and re-poll
else BAD_REQUEST or INVALID_PARAM
S-->>C: BAD_REQUEST / INVALID_PARAM
Note over C: Correct the request format or parameter and resend
end
end
end
```
Phases 3 and 4 can run in parallel. Phase 4 is required only when `__COMMIT__` is used; omitting Phase 4 is still protocol-compliant, and `__BATCH__` → RECEIVED alone is valid. Sending the next `__BATCH__` does not require waiting for ACK.
### Packet Protocol
When packet_mode = PACKET, each batch payload must be a JSON envelope:
```json
{
"version": 1,
"payload": "<base64-encoded bytes>",
"compression": "NONE|GZIP|ZLIB|DEFLATE",
"encryption": "NONE|AES_GCM",
"iv": "<base64-encoded IV, required when encryption = AES_GCM>"
}
```
Processing order: decrypt on ingress → decompress on queue poll → decode as UTF-8 string.
### Encrypted Transport
When packet_mode = PACKET and encryption is needed, the collector encrypts the payload with AES-256-GCM and the source decrypts it using the same key.
#### Key Generation
```bash
# Generate a 32-byte AES-256 key and Base64 encode it
openssl rand -base64 32
```
#### Source Configuration
```hocon
source {
EdgeSocket {
port = 9999
token = "my-edge-token"
packet_mode = "PACKET"
secret_key = "your-base64-encoded-32-byte-key"
}
}
```
#### Collector-Side Encryption Example (Java)
```java
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
byte[] secretKeyBytes = Base64.getDecoder().decode("your-base64-encoded-32-byte-key");
byte[] payload = "hello".getBytes(StandardCharsets.UTF_8);
// Generate 12-byte random IV
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
// AES-GCM encrypt
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secretKeyBytes, "AES"), new GCMParameterSpec(128, iv));
byte[] ciphertext = cipher.doFinal(payload);
// Build PACKET JSON
String packetJson = String.format(
"{\"version\":1,\"payload\":\"%s\",\"compression\":\"NONE\",\"encryption\":\"AES_GCM\",\"iv\":\"%s\"}",
Base64.getEncoder().encodeToString(ciphertext),
Base64.getEncoder().encodeToString(iv));
// Send via __BATCH__
writer.write("__BATCH__:1:" + packetJson + "\n");
```
## Tuning Guide
### local_queue_capacity
When a checkpoint is triggered, the entire in-memory queue is serialized into the checkpoint state. The checkpoint state size is approximately:
```
checkpoint_state_peak ≈ local_queue_capacity × enqueued_message_size × 3
```
The factor of 3 accounts for: the queue itself, the serialized byte array, and the Hazelcast IMap replica.
:::caution Enqueued message size ≠ raw message size
The queue stores the post-protocol-processing byte[]. When packet_mode = PACKET with compression enabled (compression = GZIP / ZLIB / DEFLATE), the enqueued data is the compressed bytes, which can be significantly smaller than the original payload.
Use the enqueued (post-compression) size for estimation. When baseline data is missing, sample real collector messages and measure their compressed size first.
:::
Keep checkpoint state under 10 MB. Checkpoint state is replicated across the cluster through Hazelcast IMap; larger states increase memory and network overhead. The final threshold is bounded by cluster hardware and network bandwidth.
Recommended settings (based on enqueued message size):
| Scenario | Enqueued Message Size | Recommended Value | Estimated Checkpoint State |
|----------|----------------------|-------------------|---------------------------|
| Lightweight metrics / heartbeats | < 256 bytes | 1024 (default) | < 1 MB |
| Standard log collection | 1–2 KB | 1024 (default) | 1–2 MB |
| Large JSON (or still large after compression) | 5–10 KB | 512 | 2.5–5 MB |
| Extra-large nested structures | > 10 KB | Calculate with formula | Depends on actual enqueued size |
Formula: local_queue_capacity ≤ 10 MB ÷ enqueued_message_size ÷ 3
For `> 10 KB` payloads, sample real enqueued message sizes first, then apply the formula to compute local_queue_capacity instead of relying on a fixed preset.
:::tip
If the collector receives frequent QUEUE_FULL responses, the downstream pipeline is consuming slower than the ingress rate. Prioritize tuning Sink throughput, or adjust local_queue_capacity and queue_backpressure_watermark_ratio using the formula above. QUEUE_FULL is returned before payload decoding to prevent retry storms under backpressure. RETRY is returned only when the queue's physical capacity is exhausted. DECODE_FAILED and DECRYPT_FAILED indicate that the payload content cannot be processed correctly.
:::
## Changelog
<ChangeLog />