Skip to content

2.10.0 - #1304

Open
subkanthi wants to merge 95 commits into
developfrom
2.10.0
Open

2.10.0#1304
subkanthi wants to merge 95 commits into
developfrom
2.10.0

Conversation

@subkanthi

Copy link
Copy Markdown
Collaborator

2.10.0 release

subkanthi and others added 30 commits April 7, 2026 17:36
Snapshot of sink-connector/python from minguyen/postgres (PR #1245) for
review and merge independent of the Postgres/Java connector changes.
Includes Python-focused .gitignore entries for the toolkit.

Made-with: Cursor
…e-records-are-not-persisted

Fixed operation Delete records not getting persisted.
…db_time-current-time

Added db_time column.
…e-deserialization-rce-via-crafted-config-file-cve-2022-1471

Upgrade snakeyaml to 2.2 to avoid CVE
Added REST API authentication and updated test.
improve version_history.md
Add ClickHouseErrorClassifier that categorizes ClickHouse exceptions as
FATAL or RETRIABLE based on error codes extracted from exception messages.

Fatal errors (auth failures, schema mismatches, resource limits) will
never succeed on retry. Previously, the connector silently swallowed
all exceptions in ClickHouseBatchRunnable.run(), retaining the failed
batch in currentBatch and retrying it indefinitely. This blocked binlog
position advancement, causing silent data loss across all tables in the
replication pipeline.

Changes:
- New ClickHouseErrorClassifier utility with error code extraction,
  fatal/retriable classification, and cause-chain traversal
- Modified ClickHouseBatchRunnable.run() catch block to classify errors:
  - FATAL: log error, clear currentBatch, rethrow to stop the executor
  - RETRIABLE/UNKNOWN: log warning, allow normal retry on next run
- Comprehensive unit tests for all classified error codes

Fixes #1310
Related: #1308
…t-failure-v2

Classify ClickHouse errors and stop on fatal failures instead of retrying forever
… RENAME

enterTruncateTable() produced 'db.db.table' when MySQL used fully-qualified
table names because it prepended databaseName without checking for dots.

enterDropTable() omitted the databaseName prefix entirely.

enterRenameTable() used && instead of || when checking for dots.

All three now follow the enterAlterTable() pattern: check for dot, split,
and prepend configured databaseName.

Added unit tests for all three fixes.

Fixes #1308
…base-prefix-v2

Fix double database prefix in DDL replication for TRUNCATE, DROP, and RENAME
ClickHouseSinkTask.preCommit() is a pass-through: it returns the offsets Kafka
Connect delivered to put(), regardless of whether those records were actually
inserted into ClickHouse. Inserts are asynchronous (ClickHouseBatchRunnable
drains an in-memory queue on a schedule), so a task crash/restart after
preCommit but before the durable insert silently skips the consumed-but-not-
inserted records -> data loss, with no error surfaced.

The existing durable gating (DebeziumOffsetManagement.acknowledgeRecords) only
works in the embedded Debezium engine path, where it calls
RecordCommitter.markProcessed(). In Kafka Connect mode the RecordCommitter is
null, so that path is a no-op and offset commit relies entirely on preCommit().

Track, per TopicPartition, the highest offset durably inserted:
- ClickHouseBatchRunnable advances a shared watermark (merge max) after each
  successful flush, from the partitionToOffsetMap it already computes.
- ClickHouseSinkTask.put() seeds a per-partition baseline (firstOffset - 1) so
  preCommit holds at the resume point until a real insert advances it (rather
  than trusting the delivered offsets, which would still lose records if CH is
  down from task start).
- preCommit() returns min(deliveredOffset, durable + 1) per partition.

No data loss on crash/restart; at-least-once redelivery of records inserted but
not yet committed, deduplicated by ReplacingMergeTree(version).

Known limitation: the watermark uses max() per partition, correct in legacy
single-threaded batch processing (FIFO per partition => max == contiguous). With
hash-based multi-threaded routing, a higher-offset batch could complete before a
lower one and over-advance the watermark; that path would need a contiguous
(gap-aware) watermark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…slight-change

Updated show_replica_status view
…connector into 1305-mysql-alter-from-smallint-unsigned-to-bigint-unsigned
…-unsigned-to-bigint-unsigned

Fix unsigned mapping(workaround debezium bug)
subkanthi and others added 26 commits July 28, 2026 16:25
…mismatch

Adding integration test to reproduce reinsert after delete.
…ert-version-mismatch

Revert "Adding integration test to reproduce reinsert after delete."
…st-part-of-github-actions-for-checksum-validations

Add integration tests to validate checksums using pytest.
…connector into sysbench_automated_tests

# Conflicts:
#	sink-connector-lightweight/tests/checksum/conftest.py
Drop ReplicationHistoryHandler and ClickHouseStruct diffs so PR #1354 only ships the sysbench checksum tests.

evert:
…ng, per-table LOCK TABLES READ lifecycle

Python tooling only - no Java changes.

- fstr() in mysql_table_checksum.py, clickhouse_table_checksum.py, mysql_table_count.py was `eval(f"f'{template}'")`: any text reaching the template executed as Python. Replaced with literal `{partition_expression}` substitution; mysql_table_checksum_test.py (previously a placeholder that asserted True == False on every run) now pins that the eval cannot come back.
- Thread-pool result loops read future.result() BEFORE checking future.exception(), so the first exception propagated un-logged and later ones were lost; now handled per-future with the table name logged.
- top_level_table_checksum.py: per-table `LOCK TABLES ... READ` with try/finally unlock + connection close (lock released even when a checksum subprocess dies). New tests/test_table_locking.py covers the lock lifecycle.
- db/clickhouse.py: cursor closed in finally; yaml.safe_load; credentials no longer logged in debug output.
- db/mysql.py: user/password URL-quoted so special characters in credentials do not break the SQLAlchemy URL.
- tests/__main__.py: parameterized INSERT instead of %-interpolated SQL.

Verified: `python -m unittest db_compare.tests.mysql_table_checksum_test db_compare.tests.test_table_locking` - all pass.

Part of the split of #1353 into independently mergeable sub-PRs (each <= 10 files), so the 2.10.0 branch can absorb the fixes incrementally.
…run, shlex-quote shell args

Python tooling only.

- mysql_dumper.py / clickhouse_loader.py logged the full shell command line including `--password <secret>`. Secrets are now registered at the point they enter a command line and masked by exact value (register_secret()/redact_password()); parsing shell quoting is not reliable because shlex.quote() splits a password containing a quote into concatenated segments.
- Password and config-file arguments are shlex.quote()d instead of hand-quoted.
- clickhouse_loader.py: the dry_run argument was accepted but hardcoded to False at the mysqlshell call site - now passed through; timezone fallback loop returned the last iterated zone instead of UTC on no-match.

Part of the split of #1353 into independently mergeable sub-PRs (each <= 10 files), so the 2.10.0 branch can absorb the fixes incrementally.
…lation

Integration-test helpers only (testflows); no shipped code.

cluster.py interpolated SQL into a double-quoted shell word (`echo -e "{sql}" | mysql`), which corrupts payloads containing quotes/backslashes and is injection-prone. SQL now travels through a temp file. Because ~90 call sites spell identifiers with backslash-escaped backticks relying on bash's double-quote unescaping, the temp-file path performs the same removal itself - exactly the four characters bash unescapes inside double quotes (backtick, dollar, double-quote, backslash), leaving \n, \N, \% untouched.

Companion tweaks in common.py and the steps modules keep the suite consistent with that path.

Part of the split of #1353 into independently mergeable sub-PRs (each <= 10 files), so the 2.10.0 branch can absorb the fixes incrementally.
…able.drop.truncate

Helm chart only.

- snapshot.mode becomes configurable (connector.snapshotMode), defaulting to schema_only - the chart had it pinned to initial, which re-snapshots on every fresh deploy.
- disable.drop.truncate becomes configurable (connector.disableDropTruncate), defaulting to true. Uses a key-presence check rather than Sprig `default` because `default` treats boolean false as empty - `--set connector.disableDropTruncate=false` would silently render "true".

Part of the split of #1353 into independently mergeable sub-PRs (each <= 10 files), so the 2.10.0 branch can absorb the fixes incrementally.
… hardening

SourceRecordParserService hardening for malformed/edge-case source records (null value schema, missing fields) so a poison record fails loudly with context instead of an opaque NPE. New SourceRecordParserServiceTest pins the behavior.

Part of the split of #1353 into independently mergeable sub-PRs (each <= 10 files), so the 2.10.0 branch can absorb the fixes incrementally.
[split 4/22] helm: restore snapshot.mode=schema_only default, add configurable disable.drop.truncate
…mula

Bulk DELETE + re-INSERT (e.g. a nightly refresh) could permanently lose rows:
after an offset rewind, Debezium re-delivers the DELETE with a fresh
processing timestamp, so it received a HIGHER _version than the later
re-INSERT and ReplacingMergeTree kept the row stuck is_deleted=1.

The fix anchors the version sequence to the SOURCE commit timestamp
(source.ts_ms), which is identical on every re-delivery, so a re-delivered
DELETE keeps its original version and can never out-rank the re-INSERT.

Unlike the reverted #1347 (and #1355), the emitted formula is UNCHANGED
from 2.8.0: ts_ms * 1_000_000 + counter. Values stay in the same numeric
domain, so the binary remains a drop-in replacement in BOTH directions:
upgrade from 2.8.0 is safe, and rollback to 2.8.0/2.10.0 remains possible
(no un-supersedable version domain is introduced).

The intra-second counter is keyed exclusively on the source commit clock -
never on the binlog file name or position - so it is preserved across
binary log rotations: two commits in the same second on either side of a
rotation keep incrementing the same counter and cannot collide or invert.
The anchor is global (survives batch boundaries) and never moves backward,
closing the duplicate-_version race where a re-delivered older timestamp
re-armed the counter reset.

Also fixes the snapshot marker parsing: Debezium emits an enum string
("true", "first", "last", "incremental", ...), not a boolean;
Boolean.parseBoolean misclassified "first"/"last" as streaming records.
Snapshot records keep the historical processing-time anchor (their
source.ts_ms is a snapshot read time with different clock semantics, and
snapshots are re-read, not re-delivered, so #1346 does not apply to them).

SourceTsVersionAnchorTest pins all of it: the 2.8.0 domain, the #1346
delete/re-insert scenario, counter preservation across rotation, the
monotonic anchor, and the no-source-ts fallback. All 6 tests fail against
the previous logic and pass with the fix.
…all directives with edge-case tests

Restores the historically intended (and upstream-removed in 686cfea)
initial-seed semantics, per aadant's directives on PR #1378:

1. The intra-second counter is kept even if the binary log rotates.
   Already implemented (counter keyed exclusively on the source commit
   clock; file name / position play no role). Now additionally proven
   inside a re-publication scenario where the rotation happens between
   the re-published DELETE and re-INSERT
   (republicationPreservesOrderAcrossRotation).

2. The first time you resume, the counter is 500m. On the first batch
   after start/resume (sequenceAnchorTs == 0), the counter is now seeded
   at SEQUENCE_START_INITIAL (500,000,000) instead of continuing from
   SEQUENCE_START (1,000,000,000). Debezium re-publishes every event
   after the last committed offset on resume; those re-published events
   were already written pre-restart with counters in the 1000m range, so
   the 500m seed guarantees every re-published duplicate ranks strictly
   BELOW its original write and can never supersede it. The first >1s
   source-clock advance resets to SEQUENCE_START, returning to the
   normal domain (initialSeedEscapesToNormalDomainAfterOneSecond).
   Both counter sites (streaming loop and addVersion) are seeded.

3. Edge cases including re-publication, all pinned in
   SourceTsVersionAnchorTest (9 tests):
   - resumeSeedsCounterAtInitial: restart mid-second; re-published event
     gets 500m+1 and ranks below the pre-restart 1000m write
   - republicationPreservesOrderAcrossRotation: crash + resume replays
     DELETE and re-INSERT across a binlog rotation; DELETE < re-INSERT
     preserved, replayed versions never out-rank originals
   - counterKeptAcrossBinlogRotation, anchorNeverMovesBackward,
     counterResetsOnlyOnSourceClockAdvance, staysInTwoEightZeroDomain,
     redeliveredDeleteCannotOutrankReinsert, fallsBackToProcessingTimestamp

Proof of test sensitivity: with the seed reverted (pr1378head code), 6 of
9 tests fail; with it applied, 9/9 pass. Full offline suite (63 classes)
run on both this commit and unmodified pr1378head: failing-class sets are
IDENTICAL (47 pre-existing Docker-unavailable/environment classes) - zero
regressions introduced. Version280CompatibilityTest (2.8.0 domain pin),
DebeziumChangeEventCaptureTest and SequenceNumberRaceTest all pass: 500m
and 1000m counters both stay within the ts_ms*1e6+counter domain.
…um-safety

[split 1/22] python: remove eval() from checksum fstr(), fix silent error swallowing, per-table LOCK TABLES READ lifecycle
[split 2/22] python: mask passwords in db_dump/db_load command logging, honor dry_run, shlex-quote shell args
…-datatype

[split 5/22] lightweight: SourceRecordParserService null-schema and error handling hardening
@subkanthi subkanthi closed this Aug 7, 2026
@subkanthi subkanthi reopened this Aug 7, 2026
Fix #1346: redelivery-stable _version anchored to source commit timestamp - keeps the 2.8.0 formula (upgrade- and rollback-safe)
…-helpers

[split 3/22] testflows: pass SQL to clients via temp file instead of shell interpolation
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.

6 participants