[
https://issues.apache.org/jira/browse/FLINK-41452?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
ASF GitHub Bot updated FLINK-41452:
-----------------------------------
Labels: pull-request-available (was: )
> Multi-Flink-version compatibility refactor
> ------------------------------------------
>
> Key: FLINK-41452
> URL: https://issues.apache.org/jira/browse/FLINK-41452
> Project: Flink
> Issue Type: Improvement
> Components: Flink CDC
> Affects Versions: cdc-3.7.0
> Reporter: peiyu
> Priority: Major
> Labels: pull-request-available
>
> h1. Multi-Flink-version compatibility refactor
> h2. 1. Background
> h3. 1.1 The support matrix keeps shrinking
> Flink CDC's Flink support matrix has been contracting: 3.1 supported 4 Flink
> versions (1.16-1.19), from 3.5 only 1.19/1.20 remained, and 3.6/3.7 support
> just 1.20 + 2.2. The marginal cost of adding one Flink version is "a full
> round of build configuration plus profile surgery", and that cost curve
> directly determines how wide the matrix can be.
> h3. 1.2 Four structural problems with the current mechanism
> The current mechanism (3.7 / 3.8-SNAPSHOT) is "single source tree +
> whole-tree Maven profile switching + dual-FQCN shadow modules":
> * *Profile fragmentation.* Generation switching (1.20 vs 2.2) is scattered
> across the local profiles of 7 modules; the root pom's {{flink2}} profile
> does not even switch {{flink.version}}; the runtime module needs a
> property-activated {{*:*}} exclusion profile to neutralize the default one;
> test classpaths are hand-assembled via a {{copy-flink2-guava}} hack.
> * *The guava package-name trap.* 89+ files hardcode {{import
> org.apache.flink.shaded.guava31.*}}; the {{flink-shaded-guava}} package
> prefix evolves with Flink (1.20 -> guava31, 2.2 -> guava33). Under the flink2
> build, main modules compile against 31.1 while dist packages 33.4 (verified
> via {{dependency:tree}}), and at runtime we are saved only by the guava31
> compatibility bundle retained in the Flink 2.2 distribution (FLINK-39288 /
> flink-cdc PR #4339) - an implicit dependency with no contract behind it.
> * *Shadow-class linkage risk without per-version verification.* The dist and
> SQL connector fat jars contain 30+ {{org.apache.flink.*}} shadow classes
> (backports / copies). Linking correctness depends entirely on the shadowed
> classes happening to be binary-compatible with the real Flink APIs, and there
> is no systematic verification.
> * *Main-code reflection into Flink internals.* {{DataSinkWriterOperator}}
> reflectively probes the 3-arg {{SinkWriterOperator}} constructor and falls
> back to the 4-arg one; the composer reflectively reads the private
> {{StreamExecutionEnvironment.configuration}} field. These are silent breakage
> points on every Flink upgrade.
> Two adjacent gaps: the external connector dependency matrix is incomplete
> (the doris / starrocks / hudi external connectors are hardcoded to the 1.20
> line, which is "fake compatibility" under flink2 builds without an explicit
> declaration), and source-connector e2e coverage is a single 1.20 CI job.
> h3. 1.3 Prior art
> Paimon and Fluss have converged on the same pattern: *common code compiled
> against a per-generation baseline + one thin module per supported Flink minor
> + one artifact jar per version*, and neither depends on
> {{flink-shaded-guava}} (both relocate guava into their own package space).
> They differ in how the common layer is compiled: Paimon compiles it per
> generation ({{paimon-flink1-common}} / {{paimon-flink2-common}}), while Fluss
> compiles everything once against a single baseline and routes all differences
> through Adapter overrides plus backports. This proposal follows the Paimon
> shape: flink-cdc's legacy {{SourceFunction}} ecosystem (the source
> connectors, {{DebeziumSourceFunction}}, the legacy branches of 8
> TableSources) and the 2.x-new APIs ({{With*Topology}},
> {{StateRecoveryOptions}}) present a bidirectional sensitivity surface far
> larger than Fluss's (a young project with no legacy); a single baseline would
> maximize shadow-class linkage risk, whereas per-generation compilation links
> every line's shadowed classes against that line's real Flink API.
> h2. 2. Proposal: the target architecture
> h3. 2.1 Three-layer compatibility structure (following Paimon)
> {noformat}
> flink-cdc-flink/ # Flink version compatibility layer
> ├── flink-cdc-flink1-common/ # generation-common layer (compiled
> against 1.20.3):
> │ # 2.x API backports + 1.20-line
> Adapters
> ├── flink-cdc-flink2-common/ # generation-common layer (compiled
> against 2.2.0):
> │ # copies of APIs removed in 2.x +
> 2.2-line Adapters
> ├── flink-cdc-flink-1.20/ # version thin module (skeleton +
> exact-version UTs;
> │ # will host intra-generation minor
> differences)
> └── flink-cdc-flink-2.2/
> {noformat}
> * *The root pom is the only generation switch point.* The {{flink1}}
> (default) and {{flink2}} profiles centralize all generation-scoped
> properties; modules keep zero local profiles (the 7 modules' fragmented
> profiles are gone).
> * *Version-sensitive APIs must go through Adapters.* Main code may not
> directly reference Flink APIs that fork across generations. The two
> generations' Adapters share FQCN and have fully identical self-declared
> member signatures (contract in 4.2); the forks are digested inside the
> compatibility layer. Enforced by a bytecode-scan guard
> ({{ReflectionGuardTest}}) plus a review checklist.
> * *Guava self-governance.* A {{flink-cdc-shaded-guava}} module relocates
> guava to {{org.apache.flink.cdc.shaded.guava.*}} (no version prefix, so the
> guava31->33 prefix drift cannot repeat), the root pom switches to it
> globally, and fat jars ship their own copy. This proceeds in synergy with
> FLINK-35071.
> h3. 2.2 Artifact layout
> ||Artifact class||GAV shape||Basis||
> |Intermediate modules (common / runtime / composer / cli / python / debezium
> / source-connector code modules)|Single GAV (e.g.
> {{flink-cdc-runtime:3.8.0}}), deployed exactly once|Cross-generation
> jar-level comparison: bytecode differences reduced to Flink annotation
> {{@Retention}} differences (2 bytes, zero runtime impact) - see 4.1|
> |Pipeline distribution|{{flink-cdc-dist-flink-1.20}} /
> {{flink-cdc-dist-flink-2.2}}|Shaded content is strongly generation-bound|
> |9 SQL source-connector fat jars|{{flink-sql-connector-mysql-cdc-flink-1.20}}
> / {{-flink-2.2}}, etc.|Same|
> |4 pipeline-connector fat jars (kafka, paimon, elasticsearch,
> iceberg)|{{flink-cdc-pipeline-connector-kafka-flink-1.20}} / {{-flink-2.2}},
> etc.|The shaded external connector lines fork across generations (kafka fat
> jar: 2918 vs 4521 entries)|
> |The other 11 pipeline-connector fat jars|Single GAV kept|Jar-level
> comparison proves cross-generation content equivalence
> (annotation/metadata-only differences, or byte-identical)|
> *Invariant:* the repository no longer contains any artifact whose content
> varies across Flink generations while its GAV carries no generation identity.
> The Flink line is carried by the artifactId suffix and the version string
> stays clean ({{3.8.0}}) - consistent with Paimon
> ({{paimon-flink-1.20:1.4.2}}) and Fluss.
> h3. 2.3 Compatibility promises
> * {{@Public}} APIs are untouched.
> * DataStream API users' dependency coordinates do not change (the thin
> single-GAV artifacts are usable on both generations - cross-generation
> equivalence is verified).
> * Zero runtime behavior change: every fat jar was compared class-by-class
> (name + CRC32) against the pre-refactor artifact of the corresponding Flink
> line; class content is identical (a placeholder class aside).
> * JDK 11 baseline, Maven 3.8.6+, single reactor - unchanged.
> h2. 3. Implementation status
> Implementation is complete on a feature branch: six phases, one commit each -
> complete, independently reviewable change units. This is not a request for
> permission to proceed, but a request to review the direction and the concrete
> shape.
> ||Phase||Scope of the commit||
> |1|Guava self-governance: a {{flink-cdc-shaded-guava}} module relocating
> guava to {{org.apache.flink.cdc.shaded.guava.*}} (no version prefix) + a
> one-shot import replacement across 89+ files + NOTICE/LICENSE updates|
> |2|Build hygiene: the root pom becomes the single generation switch point;
> the 7 modules' fragmented local profiles, the neutralizing profiles and the
> {{copy-flink2-guava}} classpath hack are removed; starrocks/hudi flink2
> output gating with CI and README declarations|
> |3|Compatibility layer reorg: the {{flink-cdc-flink/}} aggregate with the two
> generation-common modules (migrated and renamed, zero class-content change)
> and the two version thin modules (skeleton + exact-version UTs)|
> |4|Per-version dist: {{flink-cdc-dist-flink-1.20}} / {{-flink-2.2}} property
> gating + exact-match Yarn jar lookup + release-script adaptation +
> transition-period legacy-named artifacts|
> |5|Per-version fat jars: 9 SQL source connectors + the 4 pipeline connectors
> whose external dependency lines fork across generations (kafka / paimon /
> elasticsearch / iceberg) + the 2.x source e2e CI job|
> |6|Version-sensitive surface convergence: main-code reflection into
> {{org.apache.flink}} internals eliminated (legacy {{SinkFunction}} /
> {{SourceFunction}} call sites routed through Adapters; legacy Adapter
> signatures aligned to minimal common interfaces) + a bytecode-scan guard +
> bilingual documentation refresh|
> h2. 4. Key evidence
> h3. 4.1 Cross-generation bytecode equivalence of the intermediate modules
> (the basis for deploy-once)
> Method: both generations fully built with the same JDK and Maven; thin jars
> compared entry by entry (CRC32 + size); divergent classes dissected through
> their javap constant pools.
> * Three difference families were identified: (1) *annotation retention* -
> Flink annotation {{@Retention}} differs across generations and javac compiles
> the retention into consumer classes (constant 2-byte delta, zero runtime
> impact; cannot be removed and need not be); (2) the legacy subpackage
> migration; (3) divergent signatures of the legacy Adapters. Families (2) and
> (3) have been converged through Adapters.
> * Post-convergence state: runtime 3/314 classes differ (all family 1); cli
> and python are byte-identical; composer differs only in the
> {{flink-cdc-build.properties}} resource (consumed for Yarn-deploy jar lookup,
> and only inside the dist fat jars, which are per-version - it does not affect
> thin-jar cross-generation usability).
> * Conclusion: intermediate modules deployed from the flink1 line are fully
> usable by flink2 downstream (all Flink dependencies are {{provided}} and
> non-transitive). The release flow in section 5 is grounded in this evidence.
> h3. 4.2 The Adapter signature contract
> Auditing all 17 Adapter pairs (at the compiled-artifact level, via javap)
> produced this rule:
> * *Contract:* the two generations' Adapters must have fully identical
> self-declared member signatures (same FQCN, same parameters, same returns).
> * *Boundary:* differences on the inheritance chain (parent FQCNs decided by
> Flink's own API, e.g. 2.x promoting {{StatefulSink$StatefulSinkWriter}} to a
> top-level interface) are the bridge class's job and are digested by the
> per-line fat jars - they do not violate the contract.
> * *Enforcement:* new direct references from main code to version-sensitive
> Flink APIs are caught by the bytecode-scan guard and the review checklist.
> h3. 4.3 Artifact equivalence certification
> * Phase-5 acceptance: 22 key jars compared entry by entry against the
> pre-refactor baseline (fat jars differ only by +GAV metadata / -old metadata;
> class content unchanged).
> * Pipeline connectors: the 8 per-version fat jars are class-level identical
> to the pre-refactor artifacts of their corresponding lines (the new-GAV pom
> entries and a DummyDocs placeholder class aside).
> * The 4-fork vs 11-non-fork split was determined by a full jar-level scan of
> all 15 pipeline connectors (e.g. the fluss fat jar is cross-generation
> identical - its shaded {{fluss-client:1.0.0}} is version-pinned and the
> generation-specific properties are used only in tests).
> h3. 4.4 Reflection elimination
> All main-code reflection into {{org.apache.flink}} internals has been
> converged into the compatibility layer (constructors, fields, setup calls,
> {{Class.forName}} probes), enforced by a guard test that scans bytecode
> constant-pool signatures (the violation whitelist is empty). Reflection that
> instantiates UDFs by user-supplied class name is exempt - its target is user
> code, not Flink.
> h2. 5. Release process (finalized; community confirmation requested)
> Two invocations of {{deploy_staging_jars.sh}}, one per Flink line:
> # *flink1 line* ({{FLINK_VERSION=1.20.3}}): full deploy. The intermediate
> modules enter staging exactly once here (their content is cross-generation
> equivalent, see 4.1), together with all {{-flink-1.20}} artifacts and the 11
> single-GAV pipeline connector fat jars.
> # *flink2 line* ({{FLINK_VERSION=2.2.0}}): a full {{install}} first
> (compile/verification as usual, populates the local repository), then a
> {{-pl}}-restricted deploy of the per-version artifacts
> ({{flink-cdc-dist-flink-2.2}} + 9 {{flink-sql-connector-*-flink-2.2}} + 4
> {{flink-cdc-pipeline-connector-*-flink-2.2}}). Deliberately *without*
> {{-am}}: otherwise the shared-GAV upstream modules would be deployed a second
> time and rejected by Nexus staging (same GAV, different checksum).
> A side benefit: the flink2 line's {{-pl}} restriction naturally keeps the
> starrocks/hudi flink2 thin shells out of staging. During a transition period
> the release also emits legacy-named files (e.g.
> {{flink-cdc-dist-3.8.0-1.20.jar}}) so download links stay alive; by default
> for one minor cycle.
> h2. 6. How the change will be submitted
> Everything is submitted as a *single PR* containing the six phase commits
> above:
> * Reviewers can review commit by commit - each commit is an independent,
> behavior-preserving change that can be reverted on its own without entangling
> the rest (the phases share only build ordering, no revert coupling).
> * The PR description will carry per-commit verification summaries and link
> back to this ticket.
> * If the reviewers prefer multiple PRs, the chained merge order is simply the
> commit order (1 -> 6).
> h2. 7. Decision points (feedback most valuable here)
> ||#||Topic||Implemented as||Alternative and its costs||
> |D1|Artifact naming|artifactId suffix + clean version string (the
> Paimon/Fluss shape)|Keep the old version-suffix convention ({{3.8.0-1.20}}):
> costs one extra full release build per added Flink version, makes
> single-build multi-artifact impossible for same-generation minors (e.g. 2.2 +
> 2.3), and requires migrating all coordinates, docs and download pages back|
> |D2|Release strategy|Two-line deploy: flink1 full deploy once + flink2
> restricted to a per-version {{-pl}} list; intermediate modules deployed
> exactly once|There is no substantive alternative - a second full deploy would
> be rejected by Nexus staging, and the rejection is protective (a flink1-line
> pipeline fat jar would be unusable on a 2.x cluster). Feedback is requested
> on the acceptability of the flow itself|
> |D3|Guava shade|CDC-owned package name, no version prefix|Synergy with
> FLINK-35071; the cost is roughly +3 MB per fat jar|
> h2. 8. Settled positions (for awareness, not up for vote)
> * *starrocks / hudi:* no fat jars are produced under the flink2 build
> (upstream has no 2.x external connector artifacts); CI and README state "1.20
> only".
> * *doris:* stays on the 1.20-line external dependency (the upstream
> flink2-line connector is Java 17 bytecode, conflicting with the JDK 11
> baseline); it still compiles, tests and packages as before, with docs marked
> "not verified on Flink 2.x".
> * *Adapter narrow-read:* Adapter parameters take the minimal common interface
> (e.g. {{RuntimeContext}}, not {{StreamingRuntimeContext}}); the type a call
> site happens to hold must not leak into the signature.
> * *Single source tree:* no {{src/main/flink1}}-style versioned source
> directories.
> h2. 9. Verification matrix (the acceptance bar for the PR)
> Dual-generation full builds + full UTs + repository gates (spotless /
> checkstyle) as the baseline; representative e2e runs on CI (two jobs added:
> {{source_e2e_2_x}}, {{shade_guards_2_x}}); artifact equivalence certified
> through jar-level comparison. Pre-existing environment-only failures
> unrelated to this refactor are enumerated in the PR description.
> h2. 10. Known leftovers (disclosed as they are)
> * Runtime-ization of composer's {{flink-cdc-build.properties}} (optional
> hardening; Yarn jar lookup is already served correctly by the per-version
> dist jars).
> * The tutorial pages (mysql-to-kafka, migrate-paimon-cdc-action, etc.) still
> carry download-link tables in the old naming scheme and need a full-page
> refresh (within the Phase 6 docs scope or a separate docs ticket).
> * The shade configuration is duplicated across the per-version submodules of
> the 4 pipeline connectors (marked keep-in-sync) - the simplest viable shape
> under Maven's hard rules (a jar module cannot declare {{<modules>}}; a parent
> must be pom-packaged); it can be converged later if Maven relaxes or a
> plugin-level abstraction is introduced.
> ----
> AI-assisted tooling was used during implementation and documentation; in line
> with the ASF Generative Tooling Guidance this will be disclosed via the
> Generated-by trailer in the PR.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)