Commit b2872d1
committed
session: fix pool renewal race causing double statement execution
When two or more nodes are bootstrapped concurrently the Python driver
can execute the same CQL statement twice, causing spurious "already
exists" errors in the caller. This has been observed as flaky test
failures across the ScyllaDB test suite for the past two years, and
worked around by using idempotent DDL forms (IF NOT EXISTS / IF EXISTS)
in dozens of tests.
Root cause
----------
The race unfolds as follows:
1. Two on_add notifications arrive at roughly the same time, one for
each new node. Each one calls session.add_or_renew_pool(), which
submits run_add_or_renew_pool() to the thread pool and returns.
Both submissions are in-flight concurrently.
2. The first add_or_renew_pool() finishes and calls _finalize_add(),
which notifies load-balancing policies and then calls
session.update_created_pools() for every live session.
3. update_created_pools() iterates all known hosts. For the second
host, whose run_add_or_renew_pool() has not yet completed, it sees
self._pools.get(host) == None (or a shut-down pool) and therefore
submits *another* run_add_or_renew_pool() for that host.
4. Now two tasks are connecting to the same host. The first one
finishes and installs pool-A in self._pools, then runs a statement
(e.g. CREATE ROLE) that is in-flight on pool-A.
5. The second task finishes, reads the stale `previous = self._pools.get(host)`
value (captured *before* the lock was taken — another bug), installs
pool-B and then shuts down pool-A. The in-flight CREATE ROLE request
is orphaned; the driver retries it on pool-B. The server executes it
a second time and returns "Role ... already exists".
Fix
---
Three coordinated changes to cassandra/cluster.py:
* Session.__init__: add self._pending_pool_futures = {}, a dict mapping
host -> entry (with future, creation_id, distance,
is_host_addition_cell) for any in-flight pool creation, guarded by
_lock.
* add_or_renew_pool: before submitting run_add_or_renew_pool(), check
_pending_pool_futures under _lock. If an in-flight future already
exists for the host with the same distance, return it immediately —
this is the primary fix that prevents the duplicate submission from
update_created_pools. If is_host_addition=True on the new call but
the existing entry has False, upgrade it in-place via a shared
is_host_addition_cell so the closure passes the correct flag to
signal_connection_failure() and _HostReconnectionHandler dispatches
through on_add() instead of on_up() on reconnect. If the distance
changed, submit a fresh task (the old HostConnection was constructed
with stale distance, e.g. no connections for REMOTE with
connect_to_remote_hosts=False).
Each submission gets a unique creation_id token. The closure checks
_pending_pool_futures[host].creation_id before installing its pool:
if remove_pool() ran and a fresher creation was submitted while this
task was connecting, the stale task discards its pool rather than
overwriting the fresher one.
Additionally, move the `previous = self._pools.get(host)` read inside
the lock so the live-pool check is atomic with the installation of the
new pool: if a concurrent creation has already installed a live pool
by the time we finish connecting, discard our new pool instead of
replacing the live one (defense-in-depth).
Cleanup of _pending_pool_futures is handled by a done_callback
registered on the future. The callback acquires _lock and only clears
the entry if it still holds the same creation_id it was registered
on, so a concurrent remove_pool followed by a new add_or_renew_pool
is not affected. The entry is stored before calling submit() so that
the closure always finds a valid creation_id in the dict, even when
the executor runs the task synchronously.
* remove_pool: clear _pending_pool_futures[host] under _lock so that
if a host is removed and immediately re-added, add_or_renew_pool
submits a fresh creation rather than reusing a stale done future.
Tests
-----
Five new unit tests are added in PoolRenewalRaceTest
(tests/unit/test_cluster.py). They exercise the new code paths without
requiring a real cluster connection by constructing a minimal Session
via object.__new__ and mocking the executor and profile manager.
The tests use the new dict-based entry format for _pending_pool_futures:
* test_add_or_renew_pool_reuses_inflight_future: places a pending
entry in _pending_pool_futures and verifies that add_or_renew_pool
returns the existing future without submitting a new task.
* test_add_or_renew_pool_discards_duplicate_when_live_pool_exists:
exercises the real production code path by patching HostConnection
to a lightweight stub and using a synchronous executor shim that
runs the submitted callable inline. Pre-installs a live pool for
the host, then calls add_or_renew_pool() and asserts that the live
pool is not replaced and the newly connected stub pool is shut down.
* test_remove_pool_clears_pending_future: verifies that remove_pool
clears _pending_pool_futures so the next add_or_renew_pool call
submits a fresh task.
* test_done_callback_clears_pending_future: verifies that the
done_callback fires and removes the entry from _pending_pool_futures
once the future completes.
* test_done_callback_does_not_clear_newer_future: verifies the
creation_id guard — an old future's callback does not evict a newer
entry installed in its place after a remove_pool + add_or_renew_pool.
Fixes: #3171 parent cd9f525 commit b2872d1
2 files changed
Lines changed: 335 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2615 | 2615 | | |
2616 | 2616 | | |
2617 | 2617 | | |
| 2618 | + | |
| 2619 | + | |
| 2620 | + | |
| 2621 | + | |
| 2622 | + | |
| 2623 | + | |
2618 | 2624 | | |
2619 | 2625 | | |
2620 | 2626 | | |
| |||
3240 | 3246 | | |
3241 | 3247 | | |
3242 | 3248 | | |
| 3249 | + | |
| 3250 | + | |
| 3251 | + | |
| 3252 | + | |
| 3253 | + | |
| 3254 | + | |
| 3255 | + | |
| 3256 | + | |
| 3257 | + | |
| 3258 | + | |
| 3259 | + | |
| 3260 | + | |
| 3261 | + | |
| 3262 | + | |
| 3263 | + | |
3243 | 3264 | | |
3244 | 3265 | | |
3245 | 3266 | | |
3246 | 3267 | | |
3247 | 3268 | | |
3248 | | - | |
| 3269 | + | |
3249 | 3270 | | |
3250 | 3271 | | |
3251 | 3272 | | |
3252 | 3273 | | |
3253 | 3274 | | |
3254 | 3275 | | |
3255 | 3276 | | |
3256 | | - | |
| 3277 | + | |
3257 | 3278 | | |
3258 | 3279 | | |
3259 | | - | |
3260 | 3280 | | |
3261 | 3281 | | |
3262 | 3282 | | |
| |||
3271 | 3291 | | |
3272 | 3292 | | |
3273 | 3293 | | |
3274 | | - | |
| 3294 | + | |
3275 | 3295 | | |
3276 | 3296 | | |
3277 | 3297 | | |
3278 | 3298 | | |
3279 | | - | |
| 3299 | + | |
| 3300 | + | |
| 3301 | + | |
| 3302 | + | |
| 3303 | + | |
| 3304 | + | |
| 3305 | + | |
| 3306 | + | |
| 3307 | + | |
| 3308 | + | |
| 3309 | + | |
| 3310 | + | |
| 3311 | + | |
| 3312 | + | |
| 3313 | + | |
| 3314 | + | |
| 3315 | + | |
| 3316 | + | |
| 3317 | + | |
| 3318 | + | |
| 3319 | + | |
| 3320 | + | |
| 3321 | + | |
| 3322 | + | |
| 3323 | + | |
| 3324 | + | |
| 3325 | + | |
| 3326 | + | |
| 3327 | + | |
| 3328 | + | |
3280 | 3329 | | |
3281 | 3330 | | |
3282 | 3331 | | |
3283 | 3332 | | |
3284 | 3333 | | |
3285 | 3334 | | |
3286 | 3335 | | |
3287 | | - | |
| 3336 | + | |
| 3337 | + | |
| 3338 | + | |
| 3339 | + | |
| 3340 | + | |
| 3341 | + | |
| 3342 | + | |
| 3343 | + | |
| 3344 | + | |
| 3345 | + | |
| 3346 | + | |
| 3347 | + | |
| 3348 | + | |
| 3349 | + | |
| 3350 | + | |
| 3351 | + | |
| 3352 | + | |
| 3353 | + | |
| 3354 | + | |
| 3355 | + | |
| 3356 | + | |
| 3357 | + | |
| 3358 | + | |
| 3359 | + | |
| 3360 | + | |
| 3361 | + | |
| 3362 | + | |
| 3363 | + | |
| 3364 | + | |
| 3365 | + | |
| 3366 | + | |
| 3367 | + | |
| 3368 | + | |
| 3369 | + | |
| 3370 | + | |
| 3371 | + | |
| 3372 | + | |
| 3373 | + | |
| 3374 | + | |
| 3375 | + | |
| 3376 | + | |
| 3377 | + | |
| 3378 | + | |
| 3379 | + | |
| 3380 | + | |
| 3381 | + | |
| 3382 | + | |
| 3383 | + | |
| 3384 | + | |
| 3385 | + | |
| 3386 | + | |
| 3387 | + | |
| 3388 | + | |
| 3389 | + | |
| 3390 | + | |
3288 | 3391 | | |
3289 | 3392 | | |
3290 | | - | |
| 3393 | + | |
| 3394 | + | |
| 3395 | + | |
| 3396 | + | |
| 3397 | + | |
| 3398 | + | |
3291 | 3399 | | |
3292 | 3400 | | |
3293 | 3401 | | |
| |||
0 commit comments