8000 Synchronize CAS test clock and retry callbacks by k-morozov · Pull Request #2801 · Altinity/ClickHouse · GitHub
[go: up one dir, main page]

Skip to content

Synchronize CAS test clock and retry callbacks - #2801

Draft
k-morozov wants to merge 3 commits into
antalya-26.6from
fix/cas-test-callback-locks
Draft

k-morozov wants to merge 3 commits into
antalya-26.6from
fix/cas-test-callback-locks

Conversation

@k-morozov
@k-morozov k-morozov commented Nov 5, 2026 •
Copy link
Copy Markdown

ThreadSanitizer reported a data race in
CASDecommission.FenceLossOnAListPageStillRunsTheConfirm. The test installs clock callbacks after Pool::openForDecommission has started the lease renewer. The main thread assigns the clock's std::function while the renewer calls the same object from CasOperation::replace. Owning the captured clock keeps that clock alive, but does not make replacement of the enclosing std::function safe.

Protect the clock and request-sleep callbacks in CasRequests with separate SharedMutex instances. Callback calls take SharedLockGuard, and test setters take UniqueLock. Hold shared ownership until the callback returns, including exception unwinding, so a setter cannot replace or destroy a callable that another thread is still using. Separate mutexes allow a sleep callback to issue a request that reads the clock without acquiring the same mutex again.

Keep the existing now_ms and sleep_ms call expressions. These names now refer to private methods that acquire the guards, while the stored callbacks become now_ms_fn and sleep_ms_fn. Existing request loops, hot-key handling, and pause therefore use the protected accessors without changes to their call sites or retry logic.

Apply the same protection to CasRefLedger::recovery_retry_sleep_fn. Both setCasRetrySleepForTest and setRefRecoveryRetrySleepForTest replace this callback under exclusive ownership. Recovery holds shared ownership during backoff, after releasing the table's state_mutex. The callback guard is released before the exception path reacquires state_mutex, preserving the existing recovery cleanup order.

Use the project's TSA-aware lock wrappers and mark all three callback fields with TSA_GUARDED_BY. Document that the mutexes protect callback replacement by tests while background work is running. Shared ownership still allows concurrent callback calls; captured mutable state must remain thread-safe.

Fix the shared callback APIs rather than only the failing decommission call site. This also covers other tests that replace callbacks on an already open pool, while preserving the existing decommission callback selection and clock precedence.

CI: https://github.com/Altinity/ClickHouse/actions/runs/41911689821
Related: #2798

Validation: git diff --check passed. Source review covered callback accesses, recovery exception cleanup, and existing reentrant test callbacks. Builds, tests, and a TSan rerun were not run as requested.

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes to CHANGELOG.md):

Fixed a data race when CAS tests replace clock and retry callbacks while background tasks are running.

Documentation entry for user-facing changes

...

CI/CD Options

Exclude tests:

  • Fast test
  • Integration Tests
  • Stateless tests
  • Stateful tests
  • Unit tests
  • Performance tests
  • Aarch64 tests
  • All with ASAN
  • All with TSAN
  • All with MSAN
  • All with UBSAN
  • All with Coverage
  • All Regression
  • Disable CI Cache

Regression jobs to run:

  • Fast suites (mostly <1h)
  • Aggregate Functions (2h)
  • Alter (1.5h)
  • Benchmark (30m)
  • CAS (content-addressed storage; Antalya only)
  • ClickHouse Keeper (1h)
  • Iceberg (2h)
  • LDAP (1h)
  • OAuth (5m)
  • Parquet (1.5h)
  • RBAC (1.5h)
  • SSL Server (1h)
  • S3 (2h)
  • S3 Export (2h)
  • Swarms (30m)
  • Tiered Storage (2h)

ThreadSanitizer reported a data race in
`CASDecommission.FenceLossOnAListPageStillRunsTheConfirm`. The test installs
clock callbacks after `Pool::openForDecommission` has started the lease
renewer. The main thread assigns the clock's `std::function` while the
renewer calls the same object from `CasOperation::replace`. Owning the
captured clock keeps that clock alive, but does not make replacement of
the enclosing `std::function` safe.

Protect the clock and request-sleep callbacks in `CasRequests` with
separate `SharedMutex` instances. Callback calls take `SharedLockGuard`,
and test setters take `UniqueLock`. Hold shared ownership until the
callback returns, including exception unwinding, so a setter cannot
replace or destroy a callable that another thread is still using.
Separate mutexes allow a sleep callback to issue a request that reads
the clock without acquiring the same mutex again.

Keep the existing `now_ms` and `sleep_ms` call expressions. These names
now refer to private methods that acquire the guards, while the stored
callbacks become `now_ms_fn` and `sleep_ms_fn`. Existing request loops,
hot-key handling, and `pause` therefore use the protected accessors
without changes to their call sites or retry logic.

Apply the same protection to `CasRefLedger::recovery_retry_sleep_fn`.
Both `setCasRetrySleepForTest` and `setRefRecoveryRetrySleepForTest`
replace this callback under exclusive ownership. Recovery holds shared
ownership during backoff, after releasing the table's `state_mutex`.
The callback guard is released before the exception path reacquires
`state_mutex`, preserving the existing recovery cleanup order.

Use the project's TSA-aware lock wrappers and mark all three callback
fields with `TSA_GUARDED_BY`. Document that the mutexes protect callback
replacement by tests while background work is running. Shared ownership
still allows concurrent callback calls; captured mutable state must
remain thread-safe.

Fix the shared callback APIs rather than only the failing decommission
call site. This also covers other tests that replace callbacks on an
already open pool, while preserving the existing decommission callback
selection and clock precedence.

CI: https://github.com/Altinity/ClickHouse/actions/runs/41911689821
Related: #2798

Validation: `git diff --check` passed. Source review covered callback
accesses, recovery exception cleanup, and existing reentrant test
callbacks. Builds, tests, and a TSan rerun were not run as requested.

Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
@github-actions
github-actions Bot commented Nov 5, 2026 •
Copy link
Copy Markdown

Workflow [PR], commit [3963b4f]

Calling the stored `std::function` does not change the `CasRequests`
object. The callback mutex is already `mutable`, so shared locking works
in a const method while still protecting callback replacement.

Keep the callback storage non-mutable and retain the existing lock scope.

CI: https://github.com/Altinity/ClickHouse/actions/runs/41911689821

Validation: `git diff --check` passed. Builds and tests were not run as
requested.

Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>
The original TSan race in
`CASDecommission.FenceLossOnAListPageStillRunsTheConfirm` came from
replacing an admin pool's clock callback after
`Pool::openForDecommission` had already started its lease renewer.
The previous repair protected callback replacement and invocation with
shared mutexes, adding locking to ordinary production requests as well.

Use initialization order to remove that overlap. All three request
planes already receive `PoolConfig::boot_ms_fn` and
`PoolConfig::retry_sleep_fn` in the `Pool` constructor. Initialize the
ref recovery retry callback from the same configuration before
`mountWritable` can start workers, and remove the post-open admin
setters from `decommissionPoolMember`.

Remove the callback mutexes and their guards from `CasRequests` and
`CasRefLedger`. Restore direct `std::function` calls under the existing
`now_ms` and `sleep_ms` names. Callback setters require setup without
concurrent callback use; they no longer support replacement during
active work. Captured mutable state still needs its own synchronization.

Keep the local `control_requests` setup: its callbacks are installed
before the first `admit`, and the admin workers use separate engines.
The admin planes now use the configured callbacks. Explicit drain
callbacks fill empty configuration fields and still directly configure
the local control engine. No existing caller supplies different
callbacks through both channels.

Make the first async completion check fatal in
`CASGCTeardownStop.OpenPlaneSleepReturnsAtOnceOnceTeardownBegan`.
After a timeout, the test must not continue replacing a sleep callback
while the earlier async `pause` may still be running.

Validation: `git diff --check` passed. Independent source reviews
covered setter callers, worker startup, detached snapshot and recovery
tasks, callback ownership, and timeout paths. Builds, tests, and a TSan
rerun were not run as requested.

CI: https://github.com/Altinity/ClickHouse/actions/runs/41911689821
Related: #2798
Signed-off-by: Konstantin Morozov <just.morozov.k@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TSan data race: CASDecommission.FenceLossOnAListPageStillRunsTheConfirm replaces the request clock under the lease renewer

1 participant

0