Skip to content

Commit 5519df3

Browse files
committed
Merge branch 'cassandra-5.0' into trunk
2 parents c9fe399 + e0ac46c commit 5519df3

4 files changed

Lines changed: 238 additions & 1 deletion

File tree

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ Merged from 4.1:
397397
* Enforce CQL message size limit on multiframe messages (CASSANDRA-20052)
398398
* Fix race condition in DecayingEstimatedHistogramReservoir during rescale (CASSANDRA-19365)
399399
Merged from 4.0:
400+
* Obsolete expired SSTables before compaction starts (CASSANDRA-19776)
400401
* Switch lz4-java to at.yawk.lz4 version due to CVE (CASSANDRA-20152)
401402
* Restrict BytesType compatibility to scalar types only (CASSANDRA-20982)
402403
* Backport fix to nodetool gcstats output for direct memory (CASSANDRA-21037)

src/java/org/apache/cassandra/db/compaction/CompactionTask.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ protected final void runMayThrow() throws Exception
188188
if (!fullyExpiredSSTables.isEmpty())
189189
{
190190
logger.debug("Compaction {} dropping expired sstables: {}", transaction.opIdString(), fullyExpiredSSTables);
191+
fullyExpiredSSTables.forEach(transaction::obsolete);
191192
actuallyCompact.removeAll(fullyExpiredSSTables);
192193
}
193194

test/unit/org/apache/cassandra/db/compaction/CompactionTaskTest.java

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
import java.io.IOException;
2222
import java.util.ArrayList;
2323
import java.util.Collections;
24+
import java.util.HashSet;
2425
import java.util.List;
2526
import java.util.Map;
2627
import java.util.Set;
28+
import java.util.concurrent.atomic.AtomicInteger;
2729

2830
import org.junit.Assert;
2931
import org.junit.Before;
@@ -32,20 +34,30 @@
3234

3335
import org.apache.cassandra.SchemaLoader;
3436
import org.apache.cassandra.Util;
37+
import org.apache.cassandra.config.DatabaseDescriptor;
3538
import org.apache.cassandra.cql3.QueryProcessor;
3639
import org.apache.cassandra.cql3.UntypedResultSet;
3740
import org.apache.cassandra.cql3.statements.schema.CreateTableStatement;
3841
import org.apache.cassandra.db.ColumnFamilyStore;
42+
import org.apache.cassandra.db.Directories;
3943
import org.apache.cassandra.db.SystemKeyspace;
44+
import org.apache.cassandra.db.compaction.writers.CompactionAwareWriter;
45+
import org.apache.cassandra.db.compaction.writers.MaxSSTableSizeWriter;
46+
import org.apache.cassandra.db.lifecycle.ILifecycleTransaction;
4047
import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
48+
import org.apache.cassandra.db.lifecycle.SSTableSet;
49+
import org.apache.cassandra.db.lifecycle.View;
50+
import org.apache.cassandra.db.lifecycle.WrappedLifecycleTransaction;
4151
import org.apache.cassandra.db.marshal.UTF8Type;
52+
import org.apache.cassandra.io.sstable.SSTableRewriter;
4253
import org.apache.cassandra.io.sstable.format.SSTableReader;
4354
import org.apache.cassandra.schema.KeyspaceParams;
4455
import org.apache.cassandra.schema.Schema;
4556
import org.apache.cassandra.schema.TableMetadata;
4657
import org.apache.cassandra.service.ActiveRepairService;
4758
import org.apache.cassandra.utils.FBUtilities;
4859
import org.apache.cassandra.utils.TimeUUID;
60+
import org.apache.cassandra.utils.concurrent.Refs;
4961
import org.apache.cassandra.utils.concurrent.Transactional;
5062

5163
import static java.lang.String.format;
@@ -57,20 +69,28 @@ public class CompactionTaskTest
5769
private static TableMetadata cfm;
5870
private static ColumnFamilyStore cfs;
5971

72+
private static TableMetadata gcGraceCfm;
73+
private static ColumnFamilyStore gcGraceCfs;
74+
6075
@BeforeClass
6176
public static void setUpClass() throws Exception
6277
{
6378
SchemaLoader.prepareServer();
6479
cfm = CreateTableStatement.parse("CREATE TABLE tbl (k INT PRIMARY KEY, v INT)", "ks").build();
65-
SchemaLoader.createKeyspace("ks", KeyspaceParams.simple(1), cfm);
80+
gcGraceCfm = CreateTableStatement.parse("CREATE TABLE tbl2 (k INT PRIMARY KEY, col1 INT, col2 INT, col3 INT, data TEXT) WITH gc_grace_seconds = 0", "ks").build();
81+
SchemaLoader.createKeyspace("ks", KeyspaceParams.simple(1), cfm, gcGraceCfm);
6682
cfs = Schema.instance.getColumnFamilyStoreInstance(cfm.id);
83+
gcGraceCfs = Schema.instance.getColumnFamilyStoreInstance(gcGraceCfm.id);
6784
}
6885

6986
@Before
7087
public void setUp() throws Exception
7188
{
7289
cfs.getCompactionStrategyManager().enable();
7390
cfs.truncateBlocking();
91+
92+
gcGraceCfs.getCompactionStrategyManager().enable();
93+
gcGraceCfs.truncateBlocking();
7494
}
7595

7696
@Test
@@ -142,6 +162,172 @@ public void compactionInterruption() throws Exception
142162
Assert.assertEquals(Transactional.AbstractTransactional.State.ABORTED, txn.state());
143163
}
144164

165+
/**
166+
* Test that even some SSTables are fully expired, we can still select and reference them
167+
* while they are part of compaction.
168+
*
169+
* @see <a href="https://issues.apache.org/jira/browse/CASSANDRA-19776>CASSANDRA-19776</a>
170+
*/
171+
@Test
172+
public void testFullyExpiredSSTablesAreNotReleasedPrematurely()
173+
{
174+
Assert.assertEquals(0, gcGraceCfs.getLiveSSTables().size());
175+
gcGraceCfs.getCompactionStrategyManager().disable();
176+
177+
// Use large SSTables (10+ MiB) so that switching to new output SSTables happens during
178+
// compaction. Without large enough SSTables, the output fits in one SSTable and no switch happens
179+
int numKeys = 5000;
180+
String data = "x".repeat(2048); // ~2KB padding per row
181+
182+
// Similar technique to get fully expired SSTables as in TTLExpiryTest#testAggressiveFullyExpired
183+
// SSTable 1 (will be fully expired - superseded by SSTable 2)
184+
for (int k = 0; k < numKeys; k++)
185+
{
186+
QueryProcessor.executeInternal("INSERT INTO ks.tbl2 (k, col1, data) VALUES (?, 1, ?) USING TIMESTAMP 1 AND TTL 1", k, data);
187+
QueryProcessor.executeInternal("INSERT INTO ks.tbl2 (k, col2, data) VALUES (?, 1, ?) USING TIMESTAMP 3 AND TTL 1", k, data);
188+
}
189+
Util.flush(gcGraceCfs);
190+
191+
// SSTable 2 (will be fully expired - superseded by SSTables 3 and 4)
192+
for (int k = 0; k < numKeys; k++)
193+
{
194+
QueryProcessor.executeInternal("INSERT INTO ks.tbl2 (k, col1, data) VALUES (?, 1, ?) USING TIMESTAMP 2 AND TTL 1", k, data);
195+
QueryProcessor.executeInternal("INSERT INTO ks.tbl2 (k, col2, data) VALUES (?, 1, ?) USING TIMESTAMP 5 AND TTL 1", k, data);
196+
}
197+
Util.flush(gcGraceCfs);
198+
199+
Set<SSTableReader> toBeObsolete = new HashSet<>(gcGraceCfs.getLiveSSTables());
200+
Assert.assertEquals(2, toBeObsolete.size());
201+
202+
// SSTable 3 (not fully expired)
203+
for (int k = 0; k < numKeys; k++)
204+
{
205+
QueryProcessor.executeInternal("INSERT INTO ks.tbl2 (k, col1, data) VALUES (?, 1, ?) USING TIMESTAMP 4 AND TTL 1", k, data);
206+
QueryProcessor.executeInternal("INSERT INTO ks.tbl2 (k, col3, data) VALUES (?, 1, ?) USING TIMESTAMP 7 AND TTL 1", k, data);
207+
}
208+
Util.flush(gcGraceCfs);
209+
210+
// SSTable 4 (not fully expired - col3 has longer TTL)
211+
for (int k = 0; k < numKeys; k++)
212+
{
213+
QueryProcessor.executeInternal("INSERT INTO ks.tbl2 (k, col3, data) VALUES (?, 1, ?) USING TIMESTAMP 6 AND TTL 3", k, data);
214+
QueryProcessor.executeInternal("INSERT INTO ks.tbl2 (k, col2, data) VALUES (?, 1, ?) USING TIMESTAMP 8 AND TTL 1", k, data);
215+
}
216+
Util.flush(gcGraceCfs);
217+
218+
Set<SSTableReader> sstables = gcGraceCfs.getLiveSSTables();
219+
Assert.assertEquals(4, sstables.size());
220+
221+
// Enable preemptive opening so that SSTableRewriter.switchWriter() calls checkpoint().
222+
int originalPreemptiveOpenInterval = DatabaseDescriptor.getSSTablePreemptiveOpenIntervalInMiB();
223+
DatabaseDescriptor.setSSTablePreemptiveOpenIntervalInMiB(2);
224+
225+
// collector of stacktraces to later check if checkpoint was called in switchWriter
226+
final List<StackTraceElement[]> stacks = new ArrayList<>();
227+
228+
try
229+
{
230+
AtomicInteger checkpointCount = new AtomicInteger(0);
231+
232+
// Hook into transaction's checkpoint and commit methods to verify that after
233+
// checkpointing, all SSTables (including fully expired ones) remain referenceable.
234+
ILifecycleTransaction txn = new WrappedLifecycleTransaction(gcGraceCfs.getTracker().tryModify(sstables, OperationType.COMPACTION))
235+
{
236+
@Override
237+
public void checkpoint()
238+
{
239+
stacks.add(Thread.currentThread().getStackTrace());
240+
241+
for (SSTableReader r : toBeObsolete)
242+
Assert.assertTrue(this.isObsolete(r));
243+
244+
assertAllSSTablesAreReferenceable();
245+
246+
super.checkpoint();
247+
248+
// This is the critical assertion: after checkpoint(), all SSTables in the
249+
// CANONICAL view must still be referenceable. Before the fix, fully expired
250+
// SSTables would lose their references here, causing selectAndReference() to
251+
// spin loop (CASSANDRA-19776).
252+
assertAllSSTablesAreReferenceable();
253+
254+
checkpointCount.incrementAndGet();
255+
}
256+
257+
@Override
258+
public Throwable commit(Throwable accumulate)
259+
{
260+
assertAllSSTablesAreReferenceable();
261+
return super.commit(accumulate);
262+
}
263+
264+
private void assertAllSSTablesAreReferenceable()
265+
{
266+
// This simulates what EstimatedPartitionCount metric and similar code paths do.
267+
// It is crucial that tryRef does not return null; a null result means some SSTables
268+
// are not referenceable, which would cause selectAndReference() to spin loop.
269+
ColumnFamilyStore.ViewFragment view = gcGraceCfs.select(View.selectFunction(SSTableSet.CANONICAL));
270+
Refs<SSTableReader> refs = Refs.tryRef(view.sstables);
271+
Assert.assertNotNull("Some SSTables in CANONICAL view are not referenceable (CASSANDRA-19776)", refs);
272+
refs.close();
273+
}
274+
};
275+
276+
// Use MaxSSTableSizeWriter with a small max size (2 MiB) to force output sstable switches during compaction.
277+
long maxSSTableSize = 2L * 1024 * 1024; // 2 MiB
278+
CompactionTask task = new CompactionTask(gcGraceCfs, txn, FBUtilities.nowInSeconds() + 2)
279+
{
280+
@Override
281+
public CompactionAwareWriter getCompactionAwareWriter(ColumnFamilyStore cfs,
282+
Directories directories,
283+
ILifecycleTransaction transaction,
284+
Set<SSTableReader> nonExpiredSSTables)
285+
{
286+
return new MaxSSTableSizeWriter(cfs, directories, transaction, nonExpiredSSTables,
287+
maxSSTableSize, 0);
288+
}
289+
};
290+
291+
try (CompactionController compactionController = task.getCompactionController(task.inputSSTables()))
292+
{
293+
Set<SSTableReader> fullyExpiredSSTables = compactionController.getFullyExpiredSSTables();
294+
Assert.assertEquals(2, fullyExpiredSSTables.size());
295+
task.execute(null);
296+
}
297+
298+
// Verify that checkpoint was called more than once, proving that output sstable switching
299+
// happened during compaction. Without MaxSSTableSizeWriter and large enough SSTables,
300+
// checkpoint would only be called at the end, which would not exercise the CASSANDRA-19776 fix.
301+
Assert.assertTrue("Expected checkpoint() to be called more than once during compaction, but was called "
302+
+ checkpointCount.get() + " time(s). Output sstable switching did not occur.",
303+
checkpointCount.get() > 1);
304+
}
305+
finally
306+
{
307+
DatabaseDescriptor.setSSTablePreemptiveOpenIntervalInMiB(originalPreemptiveOpenInterval);
308+
}
309+
310+
Assert.assertFalse(stacks.isEmpty());
311+
312+
boolean checkpointCalledInSSTableRewriter = false;
313+
314+
for (int i = 0; i < stacks.size(); i++)
315+
{
316+
for (StackTraceElement element : stacks.get(i))
317+
{
318+
if (element.getClassName().equals(SSTableRewriter.class.getName())
319+
&& (element.getMethodName().equals("switchWriter")
320+
|| element.getMethodName().equals("maybeReopenEarly")))
321+
{
322+
checkpointCalledInSSTableRewriter = true;
323+
break;
324+
}
325+
}
326+
}
327+
328+
Assert.assertTrue(checkpointCalledInSSTableRewriter);
329+
}
330+
145331
private static void mutateRepaired(SSTableReader sstable, long repairedAt, TimeUUID pendingRepair, boolean isTransient) throws IOException
146332
{
147333
sstable.descriptor.getMetadataSerializer().mutateRepairMetadata(sstable.descriptor, repairedAt, pendingRepair, isTransient);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.cassandra.db.lifecycle;
20+
21+
import org.apache.cassandra.db.compaction.OperationType;
22+
import org.apache.cassandra.io.sstable.format.SSTableReader;
23+
24+
import static java.util.Collections.emptySet;
25+
import static org.apache.cassandra.db.lifecycle.View.permitCompacting;
26+
import static org.apache.cassandra.db.lifecycle.View.updateCompacting;
27+
28+
/**
29+
* A {@link LifecycleTransaction} subclass that exposes the package-private constructor
30+
* for use in tests. This allows tests to create anonymous subclasses that override
31+
* {@link #checkpoint()} and {@link #doCommit(Throwable)} for verification purposes.
32+
* <p>
33+
* The constructor marks the given SSTables as compacting in the tracker (equivalent to
34+
* what {@link Tracker#tryModify(Iterable, OperationType)} does) before delegating to
35+
* the parent constructor.
36+
*/
37+
public class TestableLifecycleTransaction extends LifecycleTransaction
38+
{
39+
public TestableLifecycleTransaction(Tracker tracker, OperationType operationType, Iterable<SSTableReader> readers)
40+
{
41+
super(markCompactingAndReturn(tracker, readers), operationType, readers);
42+
}
43+
44+
private static Tracker markCompactingAndReturn(Tracker tracker, Iterable<SSTableReader> readers)
45+
{
46+
tracker.apply(permitCompacting(readers), updateCompacting(emptySet(), readers));
47+
return tracker;
48+
}
49+
}

0 commit comments

Comments
 (0)