8000 fix: wait for ALTER TABLE visibility before cache invalidation (#1222) by minguyen9988 · Pull Request #1321 · Altinity/clickhouse-sink-connector · GitHub
[go: up one dir, main page]

Skip to content

fix: wait for ALTER TABLE visibility before cache invalidation (#1222) - #1321

Merged
minguyen9988 merged 5 commits into
2.10.0from
fix/ddl-schema-change-race-condition
Aug 21, 2026
Merged

minguyen9988 merged 5 commits into
2.10.0from
fix/ddl-schema-change-race-condition

Conversation

@minguyen9988
Copy link
Copy Markdown
Collaborator

Fixes #1319 (and the root cause behind #1222 that PR #1223 did not fully address).

Problem

After executing ALTER TABLE (ADD/DROP COLUMN), the connector immediately signals cache invalidation via CacheInvalidationManager. The batch insert thread then rebuilds its column metadata cache by querying system.columns. However, the ALTER TABLE may not have propagated to system.columns yet, causing the new DbWriter to get stale column metadata and silently drop values for newly added columns.

Under concurrent load, approximately 37% of newly added columns had their values silently lost (22 out of 60 columns showed NULL instead of the inserted value).

Solution

Added DDLSchemaChangeWaiter, a utility that polls system.columns after each ALTER TABLE until the expected columns appear (for ADD COLUMN) or disappear (for DROP COLUMN) before returning from executeDDL(). The visibility wait occurs before cache invalidation proceeds, ensuring the batch thread always sees the updated schema.

Changes

  • New: DDLSchemaChangeWaiter.java -- Polls system.columns with configurable timeout (default 30s) and poll interval (default 100ms). Falls back to a 500ms delay for unparseable ALTER TABLE types.

  • Modified: DebeziumChangeEventCapture.java -- Added call to DDLSchemaChangeWaiter.waitForSchemaVisibility() after each DDL query in executeDDL().

  • New: DDLSchemaChangeWaiterTest.java -- Unit tests covering DDL parsing, null safety, case insensitivity, and non-ALTER-TABLE passthrough.

Test Plan

  • Unit tests for DDL parsing regex extraction and edge cases
  • Verified code path: schema visibility wait occurs before cache invalidation
  • Race condition was reproduced with 60 concurrent ALTER + INSERT cycles showing 37% silent data loss; the fix ensures system.columns reflects the change before proceeding

8000
Arnaud Adant and others added 3 commits June 19, 2026 23:54
After executing an ALTER TABLE (ADD/DROP COLUMN), poll system.columns
until the schema change is visible before allowing the batch insert
thread to rebuild its column metadata cache. Without this wait, the
new DbWriter may read stale column metadata and silently drop values
for newly added columns.

The original fix (PR #1223) added CacheInvalidationManager to signal
between the DDL and batch threads, but did not ensure the ALTER TABLE
had propagated before the signal. Under concurrent load, ~37%% of
newly added columns had their values silently lost.

Adds DDLSchemaChangeWaiter, a utility that:
- Parses ADD/DROP COLUMN from the DDL statement
- Polls system.columns until the expected columns appear/disappear
- Times out after 30s with a warning (configurable)
- Falls back to a 500ms delay for unparseable ALTER TABLE types

Includes unit tests for DDL parsing and null-safety.
@aadant
aadant commented Jun 26, 2026
Copy link
Copy Markdown
Collaborator

@subkanthi what do you think ? do you like the implementation ?

@subkanthi
subkanthi changed the base branch from develop to 2.10.0 June 26, 2026 20:36
@subkanthi
Copy link
Copy Markdown
Collaborator

@subkanthi what do you think ? do you like the implementation ?

    /** Default max wait time (ms) for a schema change to become visible. */
    public static final long DEFAULT_TIMEOUT_MS = 30_000;

    /** Default polling interval (ms) between visibility checks. */
    public static final long DEFAULT_POLL_INTERVAL_MS = 100;

maybe we can make this as config variables.

Per review feedback from @subkanthi:
- Made DDL schema change timeout and poll interval configurable as
  connector config variables (ddl.schema.change.timeout.ms and
  ddl.schema.change.poll.interval.ms) instead of using hardcoded defaults
- Added DDLSchemaChangeWaiterRaceConditionTest with 19 tests across 5
  categories proving the race condition exists (without the waiter) and
  is fixed (with the waiter), including a 20-thread concurrent stress test

See GitHub issue #1319
@minguyen9988
Copy link
Copy Markdown
Collaborator Author

@subkanthi Done — addressed in commit 7392086.

Both values are now configurable as connector config variables:

  • ddl.schema.change.timeout.ms (default: 30000) — maximum time to wait for ALTER TABLE visibility in system.columns
  • ddl.schema.change.poll.interval.ms (default: 100) — polling interval between visibility checks

Added to ClickHouseSinkConnectorConfigVariables and ClickHouseSinkConnectorConfig (ConfigDef), read in DebeziumChangeEventCapture.executeDDL() via config.getLong().

Also added DDLSchemaChangeWaiterRaceConditionTest with 19 tests across 5 categories:

  • Column extraction regression tests (6 tests)
  • Null safety and boundary conditions (4 tests)
  • Configuration and defaults (2 tests)
  • Schema visibility waiting with mocked JDBC (4 tests) — proves the waiter correctly blocks until columns appear/disappear, and times out gracefully
  • Race condition simulation (3 tests) — demonstrates the bug without the waiter, proves the fix with the waiter, and runs a 20-thread concurrent stress test to ensure zero stale reads under load

…aceConditionTest

The sink-connector module does not include Mockito as a test dependency.
Rewrite the entire test class using manual JDBC stub implementations
(StubConnection, StubStatement, StubResultSet) that implement the full
JDBC interfaces with default no-op methods.

Test categories:
- Column extraction from ALTER TABLE DDL (ADD/DROP, single/multi-column)
- Null safety and edge cases
- Configuration variable integration
- Visibility wait with pluggable query behavior via stubs
- Race condition simulation (bug reproduction, fix verification, stress test)

Fixes CI compilation error: package org.mockito does not exist
@minguyen9988
Copy link
Copy Markdown
Collaborator Author

Fixed the CI compilation error (package org.mockito does not exist).

The sink-connector module doesn't include Mockito as a test dependency, so I rewrote the test class to use manual JDBC stub implementations instead:

  • StubConnection, StubStatement, StubResultSet — inner classes that implement the full JDBC interfaces with default no-op methods, with pluggable query behavior via a QueryCallback functional interface
  • No new test dependencies required — uses only JUnit Jupiter (already in pom.xml)

Test coverage is unchanged — 22 test methods covering column extraction, null safety, config integration, visibility wait behavior (timeout, immediate visibility, staggered propagation, JDBC exception resilience), and race condition simulation (bug reproduction, fix verification, 20-thread stress test).

@minguyen9988
minguyen9988 merged commit ec4faa5 into 2.10.0 Aug 21, 2026
9 checks passed
@minguyen9988
minguyen9988 deleted the fix/ddl-schema-change-race-condition branch August 21, 2026 13:32
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.

Schema change race condition persists in all versions through 2.9.1 (original fix #1223 insufficient)

3 participants

0