[FLINK-39722][pipeline-connector][fluss] Support Fluss CDC Yaml Source. - #4494
[FLINK-39722][pipeline-connector][fluss] Support Fluss CDC Yaml Source.#4494loserwang1024 wants to merge 4 commits into
Conversation
d1f6013 to
80f622c
Compare
80f622c to
5a7ce32
Compare
|
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:
FlussRecordDeserializer.convertFlussField covers the 16 scalar types and throws for everything else: java.lang.UnsupportedOperationException: Unsupported Fluss data type for deserialization: ArrayType 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 /
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 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{... 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? |
a93c81f to
291705c
Compare
support it now.
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 |
| public SourceReader<T, FlussSplitBase> createReader(SourceReaderContext readerContext) { | ||
| FlussSourceReaderMetrics sourceReaderMetrics = | ||
| new FlussSourceReaderMetrics(readerContext.metricGroup()); | ||
| WrapperFlussMetricRegistry metricRegistry = |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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())); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
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:
...*...*Documentation
Was generative AI tooling used to co-author this PR?