Artur Malchanau created CASSANDRA-21819:
-------------------------------------------

             Summary: AutoRepair repair_session_timeout does not abort the 
timed-out repair, leaking a hung incremental repair session that blocks all 
later incremental repairs of its ranges
                 Key: CASSANDRA-21819
                 URL: https://issues.apache.org/jira/browse/CASSANDRA-21819
             Project: Apache Cassandra
          Issue Type: Bug
          Components: Consistency/Repair
            Reporter: Artur Malchanau
         Attachments: AutoRepairSessionTimeoutTest-repro.patch

h3. Summary

When an auto-repair task exceeds {{{}repair_session_timeout{}}}, AutoRepair 
"cancels" it with {{Future.cancel(true)}} and retries the same ranges. That 
cancel is a no-op: {{RepairCoordinator.run()}} only wires an asynchronous 
future chain and returns right away, so the executor future is already done and 
{{cancel()}} returns {{{}false{}}}. The repair keeps running, or keeps hanging, 
in the background:
 * the incremental repair (IR) session stays {{REPAIRING}} on every 
participant, and its sstables stay {{{}pendingRepair{}}};
 * every retry, and every later incremental repair of the same ranges, fails in 
the prepare phase with {{{}...encountered intersecting sstables belonging to 
another incremental repair session (<old id>){}}};
 * the {{RepairSession}} stays registered on the coordinator.

Nothing ever cleans this up:
 * LocalSessions never auto-fails the session, because the participants keep 
answering each other's status requests with {{{}REPAIRING{}}}, and since 
CASSANDRA-20312 each such answer refreshes {{{}lastUpdate{}}};
 * the coordinator has no timeout of its own on merkle-tree responses.

In our production cluster the session stayed hung for 7 days, until an operator 
ran {{{}nodetool repair_admin cancel{}}}. Incremental repair of the affected 
ranges was blocked for that whole time.
h3. Root cause

[AutoRepair.java (5.0.9), lines 
344-398|https://github.com/apache/cassandra/blob/cassandra-5.0.9/src/java/org/apache/cassandra/repair/autorepair/AutoRepair.java#L344-L398]:
{code:java}
f = repairRunnableExecutors.get(repairType).submit(task);    // task = 
RepairCoordinator
listener.await(config.getRepairSessionTimeout(repairType)); // times out -> 
success = false
...
boolean cancellationStatus = f.cancel(true);                // always false: 
run() already returned
// sleep repair_retry_backoff, then submit a NEW RepairCoordinator for the same 
ranges
{code}
[RepairCoordinator.run()/runMayThrow()|https://github.com/apache/cassandra/blob/cassandra-5.0.9/src/java/org/apache/cassandra/repair/RepairCoordinator.java#L273-L309]
 starts prepare -> repair -> finalize as a future chain and returns. Nothing in 
AutoRepair ever fails the parent repair, its {{{}RepairSession}}s, or the IR 
{{CoordinatorSession{}}}. As a result, no {{FailSession}} is sent and the 
participants keep the sstables in {{{}pendingRepair{}}}.

The same logic is present on the current {{{}cassandra-5.0{}}}, 
{{cassandra-6.0}} and {{trunk}} branches. Trunk only adds an {{isShutDown}} 
branch, which uses the same {{{}f.cancel(true){}}}.
h3. How the session got stuck in production (trigger; not the bug itself)

5.0.9, three DCs, incremental auto-repair, {{repair.retries}} not configured 
(default: disabled).
 * 10:16:48 UTC: the coordinator (node C) starts IR session 
{{8681eb40-cd38-11f1-b81f-4153cf3313e6}} with 9 participants. Prepare succeeds, 
and all 54 VALIDATION_REQs (6 tables x 9 replicas) are acknowledged.
 * 10:17:00: the LARGE_MESSAGES connections from two remote-DC replicas to node 
C fail to connect ({{{}connection timed out after 2000 ms{}}}). While a 
connection is failing to connect, {{OutboundConnection}} rejects any message 
that doesn't fit the per-connection send queue, so three 8 MiB merkle trees are 
dropped:
{noformat}
WARN  [AntiEntropyStage:1] 2026-10-21T10:17:00,569 NoSpamLogger.java:107 - 
/R1:7000->/C:7000-LARGE_MESSAGES-[no-channel] overloaded; dropping 8.019MiB 
message (queue: 4.488MiB local, 500.008KiB endpoint, 500.008KiB global)
WARN  [AntiEntropyStage:1] 2026-10-21T10:17:00,298 NoSpamLogger.java:107 - 
/R2:7000->/C:7000-LARGE_MESSAGES-[no-channel] overloaded; dropping 8.315MiB 
message (queue: 12.507MiB local, 8.507MiB endpoint, 8.507MiB global)
{noformat}
The connections came back about 3s later. With retries disabled, the drop is 
never retried and the coordinator is never told; this matches CASSANDRA-20560, 
which was closed as expected without retries. The 
{{Connection.LargeMessageDroppedTasksDueToOverload}} metrics confirm 2+1 drops 
at exactly that time.

 * 13:16:48: AutoRepair's 3h timeout fires; the cancel is a no-op, and the 
retry immediately collides:
{noformat}
WARN  [AutoRepair-Repair-incremental:1] 2026-10-21T13:16:48,372 
AutoRepair.java:370 - Repair failed for range 
-2779633433731331207--2763034864604948441 for cell tables [...] with 
cancellationStatus: false retrying after 30 seconds...
ERROR [AntiEntropyStage:1] 2026-10-21T13:17:59,740 AutoRepair.java:581 - Repair 
failure for repair INCREMENTAL: [...] Repair command #89 failed with error 
Incremental repair session cf929050-cd51-11f1-b81f-4153cf3313e6 has failed
{noformat}

 * For the next 7 days, every incremental auto-repair touching those ranges 
failed with:
{noformat}
WARN  [Repair-46a1cca0-...:2] 2026-10-28T08:55:08,385 
PendingAntiCompaction.java:210 - Prepare phase for incremental repair session 
46a1cca0-d2ad-11f1-b81f-4153cf3313e6 has failed because it encountered 
intersecting sstables belonging to another incremental repair session 
(8681eb40-cd38-11f1-b81f-4153cf3313e6). ...
{noformat}

 ** {{repair_admin list}} on all 9 participants showed {{8681eb40...}} as 
{{{}REPAIRING{}}}, with "last activity" always below one hour; the status-check 
keep-alive kept it alive.
 ** {{Table.BytesPendingRepair}} on the coordinator stayed at about 364 MB the 
whole time.
 ** A second session in the same cluster 
({{{}349f6f90-cd39-11f1-9a8e-0962703fd491{}}}, a different coordinator, a 
dropped merkle tree in the same minute) had been hung in the same way.

h3. Steps to reproduce

The attached in-JVM dtest {{AutoRepairSessionTimeoutTest}} reproduces this 
deterministically in about 2 minutes:
 # 3 nodes, RF=3, one table with 1000 rows, flushed.
 # Incremental auto-repair runs on node1 only, with {{{}repair_session_timeout: 
60s{}}}, {{repair_max_retries: 1}} and {{{}repair_retry_backoff: 1s{}}}. These 
two are set in the incremental override, because the global values are ignored 
on 5.0.9 (CASSANDRA-21748).
 # The LocalSessions timers are shortened (status check 5s, auto-fail 15s, 
cleanup 5s) to show that they don't recover the session either.
 # A message filter drops exactly one {{VALIDATION_RSP}} (node2 -> node1) for 
the test table. {{repair.retries}} is left at its default (disabled).

{code:java}
git checkout cassandra-5.0.9
git apply AutoRepairSessionTimeoutTest-repro.patch
export JAVA_HOME=<JDK 17>
ant jar
ant test-jvm-dtest-some 
-Dtest.name=org.apache.cassandra.distributed.test.repair.AutoRepairSessionTimeoutTest
{code}
Observed on 5.0.9 (all intermediate checks pass; the final check, which asserts 
the expected behaviour, fails):
{noformat}
[P1] T0: dropped VALIDATION_RSP 2->1 for incremental repair session 
d76f47ca-d2f9-11f1-89c1-17ed1be92f21
[P2] session d76f47ca-... still REPAIRING on all nodes at T0+35s        (> 2x 
the 15s LocalSessions auto-fail timeout)
[P3] node1: WARN  ... AutoRepair.java:370 - Repair failed for range 
-9223372036854775808--3074457345618258603 for distributed_test_keyspace tables 
[tbl] with cancellationStatus: false retrying after 1 seconds...
[P4] WARN  ... PendingAntiCompaction.java:210 - Prepare phase for incremental 
repair session fbcc35b0-... has failed because it encountered intersecting 
sstables belonging to another incremental repair session 
(d76f47ca-d2f9-11f1-89c1-17ed1be92f21) ...
[P5] node1: ERROR ... AutoRepair.java:394 - Repair failed for range ... after 2 
retries, total assignments: 2,processed assignments: 1, cancellationStatus: 
false
[FINAL] node1 ActiveRepairService sessionCount=1
junit.framework.AssertionFailedError: AutoRepair gave up after 
repair_session_timeout, but incremental repair session 
d76f47ca-d2f9-11f1-89c1-17ed1be92f21 is still REPAIRING on node1; its 
pendingRepair sstables block every later incremental repair of these ranges 
expected:<[FAILED]> but was:<[REPAIRING]>
{noformat}
Notes on the test:
 * It uses the in-JVM message path, without the NETWORK/GOSSIP features.
 * The matcher finds the table by scanning the raw message bytes for the 
RepairJobDesc keyspace/table strings, because a {{ValidationResponse}} can't be 
deserialized in the test classloader.
 * It has only been run on 5.0.9 so far.

h3. Expected behaviour

When AutoRepair gives up on a repair after {{{}repair_session_timeout{}}}, that 
repair should actually be aborted:
 * the IR session is failed on all participants ({{{}FailSession{}}}), so its 
{{pendingRepair}} sstables are released back to unrepaired;
 * the coordinator's {{{}RepairSession{}}}/{{{}ParentRepairSession{}}} are 
removed;
 * the retry starts from a clean state.

h3. Suggested fix
 # On timeout, abort the specific repair instead of calling 
{{{}f.cancel(true){}}}. Use the parent session id 
({{{}RepairCoordinator.state.id{}}}) to:
 ** force-shutdown that parent's \{{RepairSession}}s, so the coordinator chain 
completes exceptionally;
 ** fail the IR {{{}CoordinatorSession{}}}, which sends {{FailSession}} to the 
participants;
 ** remove the {{{}ParentRepairSession{}}}.
This may need a small {{{}RepairCoordinator{}}}/{{{}ActiveRepairService{}}} API 
for aborting a single repair, like {{terminateSessions()}} but scoped to one 
repair.
 # Log the real outcome of the abort; the current {{cancellationStatus}} is 
always {{false}} and misleading.
 # Optionally: don't start a retry for the same ranges while the previous IR 
session is still non-terminal.
 # Optionally: when incremental auto-repair is enabled while {{repair.retries}} 
(merkle_tree_response) is disabled, warn at startup or document it prominently. 
A single dropped merkle tree otherwise hangs the repair indefinitely 
(CASSANDRA-20560).



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to