Skip to content

[FLINK-39722][pipeline-connector][fluss] Support Fluss CDC Yaml Source. - #4494

Open
loserwang1024 wants to merge 4 commits into
apache:masterfrom
loserwang1024:fluss-source
Open

[FLINK-39722][pipeline-connector][fluss] Support Fluss CDC Yaml Source.#4494
loserwang1024 wants to merge 4 commits into
apache:masterfrom
loserwang1024:fluss-source

Conversation

@loserwang1024

Copy link
Copy Markdown
Contributor

What is the purpose of this pull request?

Briefly describe the problem this PR fixes or the feature it introduces. Reference Flink JIRA ticket when possible.

Brief change log

Support Fluss yaml source.


Verifying this change

This change is a trivial rework / code cleanup without any test coverage.

This change added tests and can be verified as follows:

  • Added/Updated unit tests in ...*
  • Added/Updated integration tests in ...*
  • Manually tested by ...

Documentation

  • Does this pull request introduce a new feature? (yes / no)
  • If yes, how is the feature documented? (not applicable / docs / JavaDocs / not documented)

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

@fxbing

fxbing commented Aug 6, 2026

Copy link
Copy Markdown

Ran this PR (5a7ce32) end-to-end as a Fluss→Fluss pipeline, built against apache/fluss main (22cb1969). Build is fine and the PR's own tests are green (74 UT + 43 IT). Two issues showed up in E2E:

  1. Source deserializer doesn't handle ARRAY / MAP / ROW

FlussRecordDeserializer.convertFlussField covers the 16 scalar types and throws for everything else:

java.lang.UnsupportedOperationException: Unsupported Fluss data type for deserialization: ArrayType
at ...FlussRecordDeserializer.convertFlussField(FlussRecordDeserializer.java:396)
at ...FlussRecordDeserializer.convertFlussRowToCdcRecord(FlussRecordDeserializer.java:201)

Repro: a source table with tags ARRAY (same for MAP / ROW) — the job fails on the first record and goes into a restart loop. The sink side already supports these types via CdcAsFlussArray /
CdcAsFlussMap / CdcAsFlussRow, so the gap looks source-only.

  1. CreateTableEvent isn't reconciled against an existing sink table

If the sink table already exists with fewer columns than the source, FlussMetaDataApplier treats CreateTableEvent as create-if-not-exists and leaves the physical schema as is. Upstream-only columns are then
silently dropped while the job stays RUNNING with no error.

Repro: sink (id, payload), source (id, payload, extra_col) → the row lands as (1, 'cold-start') and extra_col is lost. The coordinator does pass the full schema down:

Step 3.5 - Corresponding schema changes are: [CreateTableEvent{... extra_col STRING ...}]

A worse variant: when the sink is missing a middle column (source (id, name, sale_quantity) vs sink (id, sale_quantity)), the write still succeeds with name projected away rather than failing fast.

Would it make sense for the metadata applier to reload the real TableInfo on CreateTableEvent, append missing nullable trailing columns, and fail fast when the difference can't be reconciled that way?

@loserwang1024

loserwang1024 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@fxbing

  1. Source deserializer doesn't handle ARRAY / MAP / ROW

support it now.

  1. CreateTableEvent isn't reconciled against an existing sink table

This is a sink behavior, thus is not included in this PR. I have raise a new issue to solve it: https://issues.apache.org/jira/browse/FLINK-40349

@leonardBang
leonardBang self-requested a review August 12, 2026 09:16
public SourceReader<T, FlussSplitBase> createReader(SourceReaderContext readerContext) {
FlussSourceReaderMetrics sourceReaderMetrics =
new FlussSourceReaderMetrics(readerContext.metricGroup());
WrapperFlussMetricRegistry metricRegistry =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WrapperFlussMetricRegistry wraps Fluss METERs as WrapperFlussMeter, but it never registers a MetricViewUpdater (or otherwise calls update() on the underlying MeterView). WrapperFlussMeter#getRate() therefore only exposes the cached value.

The new source creates this registry in FlussSource#createReader, so scanner rate metrics such as fetchRequestsPerSecond and remoteFetch* will remain at their initial 0 even while records are being fetched. Could we register and close a MetricViewUpdater for this registry, as the Fluss metric integration requires?

Co-Authored-By: Qoder <noreply@qoder.com>
AI-Contributed/Feature: 62/62
AI-Contributed/UT: 0/0

@leonardBang leonardBang 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.

Thanks @loserwang1024 for the contribution, I left some comments


<properties>
<fluss.version>0.9.0-incubating</fluss.version>
<fluss.version>1.0-SNAPSHOT</fluss.version>

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.

Could we clarify which publicly available Fluss artifact this PR is expected to build against?
As of August 20, 2026, the Apache snapshot repository still resolves the relevant Fluss artifacts to 1.0-20260802.061439-1; it does not currently show a newer publication. A locally installed artifact or private mirror would not be reproducible for community builds.

CompletableFuture<?> writeFuture =
write(writerMap.get(tablePath), opType, row, tablePath);
MultiTableWriteRecord writeRecord = toWriteRecord(opType, tablePath, row, schemaId);
LOG.info("------writeRecord " + writeRecord);

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.

Could we remove this per-record INFO log?

This is on the writer's hot path and eagerly converts every MultiTableWriteRecord to a string. Under normal production throughput it may generate a large amount of logging, add per-record overhead, and expose business row contents.

If diagnostic logging is needed, a guarded DEBUG log that avoids printing the complete record would be safer.

: this.flussSourceReaderMetricGroup.addGroup(
PARTITION_GROUP, String.valueOf(tableBucket.getPartitionId()));
final MetricGroup bucketGroup =
metricGroup.addGroup(BUCKET_GROUP, String.valueOf(tableBucket.getBucket()));

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.

Should the metric group include the table identity?

A CDC source reader may consume multiple tables, and different tables can have the same partition and bucket IDs. In that case they register the same metric identifier, so the gauges may collide or become indistinguishable.

Could we add the database/table path or table ID to the metric group and cover two tables with the same bucket number in a test?

*/
private void handleTableBucketChanges(List<FlussSplitBase> newSplits, Throwable error) {
if (error != null) {
LOG.error("Error creating splits for new table-buckets", error);

@leonardBang leonardBang Aug 20, 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.

Could this split-initialization failure be propagated rather than returning after logging it?
Failures while resolving startup offsets or creating snapshot/log splits currently leave the discovered buckets unassigned. In one-shot discovery mode the operation is never retried, so the job may remain RUNNING without producing data.
A test that injects an offset-initialization failure and verifies that the source fails, rather than silently becoming idle, would help cover this behavior.

*/
private void checkTableBucketChanges(List<TableBucketInfo> allBuckets, Throwable error) {
if (error != null) {
LOG.error("Error discovering subscribed table-buckets", error);

@leonardBang leonardBang Aug 20, 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.

Should the initial discovery failure be propagated to the coordinator instead of only being logged?
When periodic discovery is disabled, this callback is invoked only once. If table discovery or a metadata request fails, returning here leaves the job RUNNING without assigning any splits, and there is no subsequent retry.
Could we fail the job for initial/one-shot discovery failures? For periodic discovery, an explicit bounded retry policy may be more appropriate.

public List<Event> restoreState(TablePath tablePath, int schemaId, RowType rowType) {
ensureCacheInitialized();
// Multiple splits may read log with different schemaIds; only reserve the first one.
if (!latestSchemaIdCache.containsKey(tablePath)) {

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.

Could this split-initialization failure be propagated rather than returning after logging it?
Failures while resolving startup offsets or creating snapshot/log splits currently leave the discovered buckets unassigned. In one-shot discovery mode the operation is never retried, so the job may remain RUNNING without producing data.
A test that injects an offset-initialization failure and verifies that the source fails, rather than silently becoming idle, would help cover this behavior.


// A reader maybe subscribe multiple split, thus only inferred by the latest schema(also the
// widest)
if (isSchemaChangeEvent) {

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.

Would it make sense to project the row whenever initialRowType differs from latestRowType, rather than only when the current record advances the cached schema ID?

The Fluss MultiTableLogScanner uses dynamic schema resolution and preserves each record's original schema ID. With multiple buckets, records may therefore arrive in the order schema 1 → schema 2 → schema 1. After schema 2 updates the cache, the final schema 1 row is read using schema 2's field converters and currently fails with:
ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2

Could we project every older-schema row to the cached latest schema? It would also be helpful to add a regression test covering schema 1 → schema 2 → schema 1.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants