peiyu created FLINK-40795:
-----------------------------

             Summary: Re-added table with drifted schema is silently skipped: 
downstream table not re-created and stale schema state causes write failures
                 Key: FLINK-40795
                 URL: https://issues.apache.org/jira/browse/FLINK-40795
             Project: Flink
          Issue Type: Bug
          Components: Flink CDC
    Affects Versions: cdc-3.6.0
            Reporter: peiyu


## Symptom

In a pipeline with `scan.newly-added-table.enabled: true` (verified with MySQL 
-> Paimon),
when a table is removed from the pipeline configuration and later added back, 
while its
upstream schema drifted in between (e.g. a column was dropped while it was out 
of capture):

1. The downstream table is NOT re-created automatically, even though the source 
re-snapshots
   the table and emits a `CreateTableEvent` carrying the latest schema.
2. If the downstream table is created manually with the current upstream 
schema, the job
   fails with metadata/schema mismatch errors when writing data, because the 
pipeline keeps
   coercing records into the stale schema recorded in state.

## Steps to Reproduce

1. Run a MySQL -> Paimon pipeline with `scan.newly-added-table.enabled: true` 
capturing
   table `t`; take a savepoint and stop.
2. Remove `t` from the pipeline's `tables` configuration, and drop the Paimon 
table `t`.
3. Restart from the savepoint, run a few checkpoints, stop with a savepoint 
again.
4. Drop a column from `t` in MySQL (while it is out of capture), then add `t` 
back to the
   pipeline configuration.
5. Restart from the savepoint.

Actual: the source re-snapshots `t`, but the Paimon table is never created. The 
coordinator
logs "Schema change event CreateTableEvent{...} is redundant for current schema 
..., just
skip it."
Expected: `t` is re-created downstream with its current schema and data flows 
normally.

The same reproduces at unit level on current master: restore a regular 
`SchemaCoordinator`
from a checkpoint containing schema V1 of table `t`, then send 
`CreateTableEvent(t, V2)`
with V2 != V1. `SchemaChangeResponse#getAppliedSchemaChangeEvents()` is empty 
and the
`MetadataApplier` is never invoked.

## Root Cause

1. The MySQL enumerator DOES clean up its own state when the config no longer 
matches `t`
   (`MySqlSnapshotSplitAssigner#captureNewlyAddedTables`, removal branch), so a 
re-added
   `t` is correctly detected as newly-added, re-snapshotted, and the source 
emits a
   `CreateTableEvent` carrying the current (drifted) schema. This part works as 
intended.

2. The `SchemaCoordinator`'s schema state, however, is never cleaned up:
   `SchemaManager#originalSchemas` / `#evolvedSchemas` (checkpointed) have no 
removal API
   at all, so the stale V1 schema of the removed table survives across 
savepoints.

3. `SchemaCoordinator#applySchemaChange` gates every event through
   `SchemaUtils#isSchemaChangeEventRedundant`, whose `CreateTableEvent` branch 
decides
   "redundant" based only on the presence of *some* recorded schema:

       createTableEvent -> {
           // It has been applied if such table already exists
           return latestSchema.isPresent();
       },

   The recorded schema is never compared with the incoming one, so the 
`CreateTableEvent`
   carrying the drifted schema is silently dropped:
   - `MetadataApplier#applySchemaChange` is never invoked -> the downstream 
table is not
     created (symptom 1);
   - neither the original nor the evolved schema state is updated, and the 
stale evolved
     schema is pushed back to `SchemaOperator` (`refreshedEvolvedSchemas`) and 
to sink
     writers (`DataSinkWriterOperator#emitLatestSchema`), so records get 
coerced into the
     stale shape and written against a table with a different schema -> 
metadata mismatch
     (symptom 2).

The redundancy check was introduced by FLINK-36690 (restructured by 
FLINK-37278) to skip
duplicated `CreateTableEvent`s emitted in the snapshot stage. Skipping an 
*identical*
duplicate is correct; skipping one that carries a drifted schema against stale 
state is not.

## Scope

- Regular schema evolution topology (default for MySQL and most sources); 
3.3.0-3.6.0 and
  master are affected. 3.2 and earlier do not contain the redundancy check.
- Any flow that re-snapshots a known table can hit this, including an ALTER 
landing
  mid-snapshot so that duplicated CreateTableEvents carry different schemas.
- The distributed topology `SchemaCoordinator` has a similar gap 
(`getLeastCommonSchema`
  based derivation never narrows, and a `CreateTableEvent` is only emitted when 
no evolved
  schema exists); it can be tracked separately.
- Known limitation remaining after the proposed fix: if the schema did NOT 
drift (identical
  `CreateTableEvent`) but the downstream table was dropped externally, the 
framework cannot
  distinguish it from a snapshot-stage duplicate and will not re-create it.

## Proposed Fix

1. `SchemaUtils#isSchemaChangeEventRedundant`: treat a `CreateTableEvent` as 
redundant only
   when the recorded schema is identical to the incoming event's schema.

2. `SchemaCoordinator#deduceEvolvedSchemaChanges` (regular): when a 
`CreateTableEvent`
   targets a sink table that already has an evolved schema, emit
   `CreateTableEvent(targetSchema)` (idempotent create-or-ignore, covering an 
externally
   dropped table) followed by alignment events from
   `SchemaMergingUtils.getSchemaDifference(currentEvolvedSchema, targetSchema)` 
instead of
   forwarding the raw event. `targetSchema` honors the configured behavior: the 
incoming
   schema for EVOLVE / TRY_EVOLVE / EXCEPTION; `getLeastCommonSchema` (never 
narrowing) for
   LENIENT; the frozen current schema for IGNORE. Only the resulting 
`CreateTableEvent` is
   propagated downstream; alignment events are applied to the external system 
only, and the
   evolved-schema state update is guarded by the same redundancy check so 
alignment events
   are not applied to the state twice.

3. Tests: coordinator-level scenarios (EVOLVE with dropped/added column, 
LENIENT, IGNORE,
   identical-duplicate skip) plus unit tests for the utility change.

## Workaround (before a fixed release)

Create the downstream table manually with the schema recorded in the job state 
- i.e. the
OLD schema including the column(s) dropped upstream, as nullable - instead of 
the current
upstream schema. The pipeline coerces records into that schema (filling dropped 
columns
with null) and writes succeed.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to