Ke Han created CASSANDRA-21794:
----------------------------------
Summary: Commit log replay after an interrupted DROP TABLE
discards whole mutations that also touched the dropped table (5.0: acknowledged
rows of other tables in the same logged batch are lost; trunk: the node does
not start)
Key: CASSANDRA-21794
URL: https://issues.apache.org/jira/browse/CASSANDRA-21794
Project: Apache Cassandra
Issue Type: Bug
Components: Local/Commit Log, Local/Startup and Shutdown
Reporter: Ke Han
Affects Version/s: 5.0.9, 5.0.x, 7.0 (trunk); by code inspection also 6.0 (same
CoordinatorBehindException path as trunk) and 4.1.x (same per-mutation skip as
5.0)
Environment: Single node, Linux 5.15 x86_64, ext4, OpenJDK 11, commitlog_sync:
batch, disk_failure_policy: stop (default). Built from source: trunk 95b178a1
(2026-10-17) and cassandra-5.0 dd48703a (2026-10-17); first seen on the 5.0.9
docker image.
Description:
*What happens*
A logged batch that writes the same partition key to two tables of one keyspace
is stored as one Mutation per partition key, carrying a PartitionUpdate for
each table. DROP TABLE a calls ColumnFamilyStore.onTableDropped ->
CommitLog.forceRecycleAllSegments, which flushes every table with data in the
current segments, so normally the replay never needs those mutations again. If
that forced flush does not complete (an I/O error with disk_failure_policy:
stop, a crash, a power loss) while the drop itself has already been persisted,
the rows of the other table b exist only in the commit log, and the replay
handles the mixed mutations as a whole:
- cassandra-5.0: PartitionUpdate deserialization throws UnknownTableException
for a's update, CommitLogReader.readMutation catches it and skips the entire
mutation ("Skipped N mutations from unknown (probably removed) CF"), so the
acknowledged b rows in it are lost silently.
- trunk: the deserialization throws CoordinatorBehindException instead
(PartitionUpdate.serializer, lines 801-814: the local epoch is after the
mutation's epoch), which readMutation does not treat as an unknown table;
start-up stops with "Exiting due to error while processing commit log during
initialization". This happens for any leftover mutation of the dropped table,
not only mixed ones. With -Dcassandra.commitlog.ignorereplayerrors=true the
node starts and the mixed mutations are dropped, losing the b rows as on 5.0.
*Reproduce* (cqlsh and nodetool against a single node with commitlog_sync:
batch; strace >= 4.16, run as root or with ptrace permission)
{code:bash}
bin/cqlsh -e "
CREATE KEYSPACE ks WITH replication = \{'class': 'SimpleStrategy',
'replication_factor': 1};
CREATE TABLE ks.a (id int PRIMARY KEY, v text);
CREATE TABLE ks.b (id int PRIMARY KEY, v text);
BEGIN BATCH
INSERT INTO ks.a (id, v) VALUES (1, 'a1'); INSERT INTO ks.a (id, v) VALUES (2,
'a2'); INSERT INTO ks.a (id, v) VALUES (3, 'a3');
INSERT INTO ks.a (id, v) VALUES (4, 'a4'); INSERT INTO ks.a (id, v) VALUES (5,
'a5');
INSERT INTO ks.b (id, v) VALUES (1, 'b1'); INSERT INTO ks.b (id, v) VALUES (2,
'b2'); INSERT INTO ks.b (id, v) VALUES (3, 'b3');
INSERT INTO ks.b (id, v) VALUES (4, 'b4'); INSERT INTO ks.b (id, v) VALUES (5,
'b5'); INSERT INTO ks.b (id, v) VALUES (6, 'b6');
INSERT INTO ks.b (id, v) VALUES (7, 'b7'); INSERT INTO ks.b (id, v) VALUES (8,
'b8'); INSERT INTO ks.b (id, v) VALUES (9, 'b9');
INSERT INTO ks.b (id, v) VALUES (10, 'b10');
APPLY BATCH;
SELECT count(*) FROM ks.b;" # 10
# one EIO on the first fsync of b's table directory (the forced flush of b
creates its flush txn log there);
# use signal=KILL instead of error=EIO for a crash at the same point
BDIR=$(ls -d /var/lib/cassandra/data/ks/b-*)
strace -f -o /tmp/strace.log -e trace=fsync -e inject=fsync:error=EIO:when=1 -P
$BDIR -p $(pgrep -f CassandraDaemon) &
sleep 5
bin/cqlsh -e "DROP TABLE ks.a;" # FSWriteError, "Stopping transports as
disk_failure_policy is stop"
kill %1; kill -9 $(pgrep -f CassandraDaemon)
bin/cassandra # restart
bin/cqlsh -e "SELECT id, v FROM ks.b;"
{code}
*Actual*
cassandra-5.0 dd48703a (same with signal=KILL instead of EIO):
{noformat}
WARN [main] CommitLogReplayer.java:247 - Skipped 5 mutations from unknown
(probably removed) CF with id c76c6560-cb2e-11f1-af99-b54fecb308d4
INFO [main] CommitLog.java:210 - Log replay complete, 13 replayed mutations in
526 ms
id | v
----+-----
10 | b10
8 | b8
7 | b7
6 | b6
9 | b9
(5 rows)
{noformat}
Rows 1..5 of b, acknowledged by the batch long before the DROP, are gone.
Control: the same script with a's inserts using ids 101..105 (no shared
partition keys) also skips 5 mutations (a's own) and returns all 10 rows of b.
trunk 95b178a1 (EIO or SIGKILL at the same point; also with the ids 101..105
control):
{noformat}
ERROR [main] DefaultDiskErrorsHandler.java:177 - Exiting due to error while
processing commit log during initialization.
org.apache.cassandra.db.commitlog.CommitLogReadHandler$CommitLogReadException:
Unexpected error deserializing mutation; saved to
/tmp/mutation4107001581964003181dat. This may be caused by replaying a
mutation against a table with the same name but incompatible schema. Exception
follows: org.apache.cassandra.exceptions.CoordinatorBehindException: Couldn't
find table with id tid:4. If a table was just created, this is likely due to
the schema not being fully propagated. Please wait for schema agreement on
table creation.
at
org.apache.cassandra.db.commitlog.CommitLogReader.readMutation(CommitLogReader.java:472)
at
org.apache.cassandra.db.commitlog.CommitLogReader.readSection(CommitLogReader.java:405)
at
org.apache.cassandra.db.commitlog.CommitLogReader.readCommitLogSegment(CommitLogReader.java:251)
...
at org.apache.cassandra.service.CassandraDaemon.setup(CassandraDaemon.java:362)
{noformat}
The node does not start. Restarted with
-Dcassandra.commitlog.ignorereplayerrors=true it starts ("Ignoring commit log
replay error" for each mixed mutation) and SELECT returns only ids 6..10.
*Expected*
All 10 rows of b after the restart, and a node that starts. Mutations for a
table that was dropped after they were written are expected garbage; the replay
should skip only the PartitionUpdate of the dropped table and apply the rest of
the mutation (on trunk, a mutation older than the drop should be treated like
an unknown table during local replay, not as a coordinator that is behind).
*Code (trunk 95b178a1)*
- src/java/org/apache/cassandra/db/commitlog/CommitLogReader.java:446-458
catches UnknownTableException around Mutation.serializer.deserialize and
returns, dropping the whole mutation; any other exception goes to the
"Unexpected error deserializing mutation" path (460-480), which is fatal unless
ignorereplayerrors is set.
- src/java/org/apache/cassandra/db/Mutation.java:598-612 deserializes the
PartitionUpdates one by one; the first unknown table aborts the whole mutation.
- src/java/org/apache/cassandra/db/partitions/PartitionUpdate.java:801-814
turns UnknownTableException into CoordinatorBehindException when the local
epoch is after the mutation's epoch, which is what happens when replaying a
mutation written before the DROP (observed in every run).
- src/java/org/apache/cassandra/db/ColumnFamilyStore.java:3414 (onTableDropped)
is where the forced flush happens; nothing on start-up accounts for it not
having completed.
- cassandra-5.0 dd48703a: CommitLogReader.java:438 (catch
UnknownTableException), CommitLogReplayer.java:247 (the "Skipped ... probably
removed" warning).
*Related*
- CASSANDRA-1353 (fixed 2010): "commit log recovery is broken when the CL
contains mutations to CFs that have been dropped". The skipping that exists
today works per mutation, not per table.
- CASSANDRA-9179 (fixed): replay skips mutations of a dropped-and-recreated
table; by design for that table, it does not cover other tables in the same
mutation.
- CASSANDRA-16878 (Review In Progress): a race in commit log replay can cause
rejected mutations; different cause.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]