Skip to content

[ISSUE-817] Add geaflow-dsl-connector-doris (Stream Load sink + partitioned source)#819

Open
E2ern1ty wants to merge 6 commits into
apache:masterfrom
E2ern1ty:feature/add-doris-connector
Open

[ISSUE-817] Add geaflow-dsl-connector-doris (Stream Load sink + partitioned source)#819
E2ern1ty wants to merge 6 commits into
apache:masterfrom
E2ern1ty:feature/add-doris-connector

Conversation

@E2ern1ty

Copy link
Copy Markdown

What changes were proposed in this pull request?

Closes #817.

Add a new geaflow-dsl-connector-doris connector so graph-computing results can be written to / read from Apache Doris efficiently. Doris speaks the MySQL protocol so it is reachable today via the JDBC connector, but JDBC only does row-by-row INSERT and misses Doris's high-throughput load path — Stream Load. This connector adds it.

Sink (Stream Load)

  • Buffers rows in write() and flushes a whole batch via a single HTTP PUT to the FE /_stream_load endpoint once the row-count or byte-size threshold is reached, or on window finish().
  • Follows the FE→BE 307 redirect (re-sending the body), does basic auth, retries, and validates the response Status.
  • Default payload is JSON (Gson with serializeNulls + disableHtmlEscaping), which correctly handles newlines, quotes, backslashes, unicode, null and empty strings; CSV is available as a no-escape high-throughput option.
  • Multiple FEs can be configured; a failed request fails over to the next FE.

Source (partitioned reads)

  • Reads through the MySQL protocol and splits the scan into N partitions on a numeric column for parallel reads; the first partition also covers rows whose partition column is entirely NULL.
  • The GPL-licensed MySQL driver is not bundled (test scope only, like geaflow-dsl-connector-jdbc); users provide a MySQL-protocol driver on the runtime classpath.

Also: SPI registration, parent/aggregation pom + runtime wiring, and CN + EN docs with options, examples and benchmark numbers.

Addresses the maintainer's review points: safe CSV/JSON serialization; idempotent finish() and empty-buffer-safe flush; close() flushes the remaining buffer and is safe on repeated calls; partition.num is clamped; globally-unique labels via UUID; FE failover; and integration tests covering FE failover, table-not-found and full data-type round-trip fidelity.

How was this PR tested?

  • Tests have Added for the changes

  • Production environment verified

  • Unit tests (run in CI): serialization of special chars / null / empty string, config defaults, multi-FE url building, sink init validation. checkstyle 0 violations, RAT 0 unapproved.

  • Testcontainers integration test (opt-in, disabled by default so CI stays green): sink+source round-trip, FE failover, table-not-found, full data-type fidelity, and a throughput benchmark.

  • Verified end to end against a real single-node apache/doris:3.0.8-all: all four core integration cases pass. Benchmark: Stream Load loaded 500 rows in ~91ms (~5500 rows/s) vs ~11335ms (~44 rows/s) for row-by-row JDBC INSERT (~125x faster); 2000 rows via Stream Load took only ~97ms.

E2ern1ty added 5 commits July 22, 2026 18:58
…tioned source)

- Sink writes to Doris via Stream Load: buffer rows in write(), flush a whole
  batch on row/byte threshold or window finish(), giving much higher throughput
  than row-by-row JDBC INSERT. CSV payload by default, JSON supported.
- Source reads through the MySQL protocol with parallel partitioned reads.
- SPI registration + parent/aggregation pom wiring + runtime dependency.
- CN + EN docs and examples.
- Unit tests and a Testcontainers integration test with a throughput benchmark
  (self-skips when Docker/host-networking is unavailable).
The Doris Testcontainers integration test previously ran whenever Docker was
available on a Linux host, which is exactly the case on GitHub CI runners, so
it would try to pull the multi-GB Doris image and start a container during a
normal 'mvn test'. Make it opt-in: it only runs with -Ddoris.it.enabled=true
or when an external Doris is provided via -Ddoris.it.fenodes/-Ddoris.it.jdbcUrl,
and otherwise skips. Docs updated accordingly.
…E failover

Address the maintainer's review points on the Doris connector:
- serialization: default to json (Gson with serializeNulls + disableHtmlEscaping)
  so newlines, quotes, backslashes, unicode, null and empty strings round-trip
  correctly; document csv as a no-escape plain-split option.
- sink flush: flushBuffer stays safe on an empty buffer and is idempotent on
  finish(); close() now flushes the remaining buffer and is safe on repeated
  calls and when the client is not initialized.
- partition logic: clamp partition.num to at least 1 instead of throwing; the
  first partition already covers rows whose partition column is entirely NULL.
- label: kept globally unique via UUID.
- FE failover: DorisStreamLoad now takes the full FE list and rotates to the
  next FE on a failed request (retries at least once per FE).
- integration tests: add table-not-found, FE-failover and full data-type
  round-trip fidelity cases (still opt-in). Add unit tests for special-char /
  null / empty-string serialization and multi-FE url building.
- Make mysql-connector-java a test-scope dependency instead of compile, so the
  GPL-licensed driver is not shipped in the distribution (matching
  geaflow-dsl-connector-jdbc, which bundles no driver). The source only uses the
  standard java.sql API and loads the driver by name, so users must provide a
  MySQL-protocol driver on the runtime classpath; documented in CN + EN docs.
- Remove the unused COLON constant.

Verified: checkstyle 0 violations, RAT 0 unapproved (14 files), unit tests
23 run / 0 failed / 7 skipped (integration tests are opt-in).
Verified the connector end to end against a real single-node
apache/doris:3.0.8-all: the sink (Stream Load), the partitioned source, FE
failover, table-not-found and full data-type round-trip (newlines, quotes,
backslashes, chinese, emoji) integration tests all pass. Benchmark: Stream Load
loaded 500 rows in ~91ms (~5500 rows/s) vs ~11335ms (~44 rows/s) for row-by-row
JDBC INSERT, ~125x faster; 2000 rows via Stream Load took only ~97ms. Recorded
the reference numbers in the CN + EN docs.
}

@Override
public List<Partition> listPartitions() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem/Cause: Currently, only numeric range partitioning is supported. When columns are highly skewed, have no indexes, or are non-numeric (string/date), the partitioning strategy may lead to hotspots or uneven partitioning.

Recommendation: Provide multiple partitioning strategies (range, hash, sampling-based), allow users to manually specify partition boundaries, or use custom SQL subqueries. Emphasize the performance impact of unindexed partitioning columns in the documentation and recommend indexing/appropriate partitioning columns.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Added a source.partition.mode option (range/custom). custom takes semicolon-separated WHERE predicates via source.partition.clauses (one partition per predicate), which supports non-numeric/skewed columns and arbitrary conditions (e.g. date subranges), so users can specify partition boundaries manually. Kept numeric range as the default. Docs now warn that the range partition column should be indexed and evenly distributed (otherwise full scans/hotspots) and recommend the primary key. Added DorisTableSourceTest covering single/range/custom listing. Sampling-based auto splitting can be a follow-up.

}
}

private String generateLabel() {

@kitalkuyo-gita kitalkuyo-gita Jul 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial Success and Duplicate Writes (Label/Idempotency)

Problem/Cause: Stream Load uses labels/UUIDs for assistance, but after FE failover and retries, there is a risk of duplicate writes or semantic inconsistencies due to "partial success + retries" (Doris's behavior for the same label needs to be explicitly defined).

Recommendation: Clearly define the semantics of labels in the README and implementation (if the server supports idempotency/duplicate checks), and:
Log the label in the failure/retry strategy and check the status of the label on the server before retrying (if it is queryable).

Provide optional "idempotency/duplicate removal strategies" (e.g., allow-duplicates vs. check-label), or expose the number of retries and their behavior as configuration options.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the idempotency risk: the label is now generated once per batch and reused across every retry and FE failover attempt (previously a new label was created per attempt, which defeated Doris's dedup). Since Doris loads a given label at most once, a retry after a mistaken failure now hits "Label Already Exists", which is treated as success instead of writing the batch twice. The label is logged on every failure/retry. Documented these write semantics in the CN/EN docs. Retry behavior is configurable via sink.max.retries (kept at least one attempt per FE for failover).

// Rotate over the FE list so a failed request fails over to the next FE.
String url = loadUrls.get(attempt % loadUrls.size());
try {
doLoad(url, payload);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Connection/Read/Write Timeout and Large Payload Timeout

Problem/Cause: Stream Load request bodies can be very large. A default timeout that is too short can cause interruption; a timeout that is too long can easily exhaust resources in the event of network anomalies.

Recommendation: Set reasonable connection and read/write timeouts, and allow users to configure them. Provide chunked/streaming uploads for large requests (if Doris supports this) or add upload timeout instructions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Connect and read/write timeouts are both user-configurable (request.connect.timeout.ms, request.read.timeout.ms) and applied to the HttpClient RequestConfig. Raised the default read/write timeout to 60s so it covers uploading a large batch, and documented tuning guidance (increase for very large batches on slow networks, lower to fail faster on anomalies). The payload is already sent as a single streamed HTTP entity; batch size itself is bounded by sink.batch.rows/sink.batch.bytes so a batch stays within a reasonable size.

@kitalkuyo-gita

Copy link
Copy Markdown
Contributor

Hello @E2ern1ty, I'm glad you contributed to the community! I left some comments, please take a look and see if they need any changes.

…, timeouts

- Label/idempotency: generate one label per batch and reuse it across all
  retries and FE failover attempts (Doris loads a label at most once); treat
  'Label Already Exists' on retry as success to avoid duplicate writes; log the
  label on failures/retries. Documented the write semantics.
- Partition strategies: add source.partition.mode (range/custom). 'custom' takes
  semicolon-separated WHERE predicates (source.partition.clauses), one partition
  per predicate, supporting non-numeric/skewed columns and arbitrary conditions.
  Documented the performance impact of unindexed/skewed partition columns and
  recommended indexed, evenly-distributed columns.
- Timeouts: read/write timeout is configurable and its default is raised to 60s
  to cover large-batch uploads; documented tuning guidance.
- Tests: add DorisTableSourceTest covering single/range/custom partition listing.

@kitalkuyo-gita kitalkuyo-gita left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a Doris connector (stream-load batch writes)

2 participants