Merge branch 'cassandra-5.0' into trunk

This commit is contained in:
Stefan Miklosovic 2026-04-10 10:42:59 +02:00
commit ab80b9e7c8
No known key found for this signature in database
GPG Key ID: 32F35CB2F546D93E
4 changed files with 112 additions and 13 deletions

View File

@ -5,6 +5,7 @@
Merged from 5.0: Merged from 5.0:
* Backport Automated Repair Inside Cassandra for CEP-37 (CASSANDRA-21138) * Backport Automated Repair Inside Cassandra for CEP-37 (CASSANDRA-21138)
Merged from 4.1: Merged from 4.1:
* Harden data resurrection startup check with atomic heartbeat file write with fallback (CASSANDRA-21290)
Merged from 4.0: Merged from 4.0:
* Rate limit password changes (CASSANDRA-21202) * Rate limit password changes (CASSANDRA-21202)

View File

@ -19,6 +19,7 @@
package org.apache.cassandra.service; package org.apache.cassandra.service;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -46,6 +47,7 @@ import org.apache.cassandra.io.util.File;
import org.apache.cassandra.schema.KeyspaceMetadata; import org.apache.cassandra.schema.KeyspaceMetadata;
import org.apache.cassandra.schema.SchemaKeyspace; import org.apache.cassandra.schema.SchemaKeyspace;
import org.apache.cassandra.utils.Clock; import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.Hex;
import org.apache.cassandra.utils.JsonUtils; import org.apache.cassandra.utils.JsonUtils;
import org.apache.cassandra.utils.Pair; import org.apache.cassandra.utils.Pair;
@ -54,7 +56,6 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet; import static java.util.stream.Collectors.toSet;
import static org.apache.cassandra.exceptions.StartupException.ERR_WRONG_DISK_STATE;
import static org.apache.cassandra.exceptions.StartupException.ERR_WRONG_MACHINE_STATE; import static org.apache.cassandra.exceptions.StartupException.ERR_WRONG_MACHINE_STATE;
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis; import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
@ -87,12 +88,24 @@ public class DataResurrectionCheck implements StartupCheck
public void serializeToJsonFile(File outputFile) throws IOException public void serializeToJsonFile(File outputFile) throws IOException
{ {
JsonUtils.serializeToJsonFile(this, outputFile); JsonUtils.serializeToJsonFileAtomic(this, outputFile);
} }
public static Heartbeat deserializeFromJsonFile(File file) throws IOException public static Heartbeat deserializeFromJsonFile(File file) throws IOException
{ {
return JsonUtils.deserializeFromJsonFile(Heartbeat.class, file); byte[] bytes = Files.readAllBytes(file.toPath());
try
{
return JsonUtils.deserializeFromJsonBytes(Heartbeat.class, bytes);
}
catch (IOException ex)
{
int maxLogBytes = Math.min(bytes.length, 1024);
String hexContent = bytes.length > 0 ? Hex.bytesToHex(bytes, 0, maxLogBytes) : "(empty)";
LOGGER.error("Failed to deserialize heartbeat file {} (length: {} bytes, first {} bytes hex: {})",
file, bytes.length, maxLogBytes, hexContent, ex);
throw ex;
}
} }
@Override @Override
@ -186,7 +199,10 @@ public class DataResurrectionCheck implements StartupCheck
} }
catch (IOException ex) catch (IOException ex)
{ {
throw new StartupException(ERR_WRONG_DISK_STATE, "Failed to deserialize heartbeat file " + heartbeatFile); LOGGER.warn("Failed to deserialize heartbeat file {}. Falling back to file last modified time.",
heartbeatFile, ex);
Instant lastModified = Instant.ofEpochMilli(heartbeatFile.lastModified());
heartbeat = new Heartbeat(lastModified);
} }
if (heartbeat.lastHeartbeat == null) if (heartbeat.lastHeartbeat == null)
@ -310,7 +326,7 @@ public class DataResurrectionCheck implements StartupCheck
List<TableGCPeriod> getTablesGcPeriods(String userKeyspace) List<TableGCPeriod> getTablesGcPeriods(String userKeyspace)
{ {
Optional<KeyspaceMetadata> keyspaceMetadata = SchemaKeyspace.fetchNonSystemKeyspaces().get(userKeyspace); Optional<KeyspaceMetadata> keyspaceMetadata = SchemaKeyspace.fetchNonSystemKeyspaces().get(userKeyspace);
if (!keyspaceMetadata.isPresent()) if (keyspaceMetadata.isEmpty())
return Collections.emptyList(); return Collections.emptyList();
KeyspaceMetadata ksmd = keyspaceMetadata.get(); KeyspaceMetadata ksmd = keyspaceMetadata.get();

View File

@ -194,6 +194,38 @@ public final class JsonUtils
} }
} }
public static void serializeToJsonFileAtomic(Object object, File outputFile) throws IOException
{
// Try to write then perform atomic move so that file can't be corrupted
// by process crash in the middle of the writing.
File tempFile = new File(outputFile.path() + ".tmp");
try
{
// Serialize to bytes first so we can flush and fsync before close.
// Jackson's writeValue(OutputStream, ...) auto-closes the stream,
// which would prevent us from calling sync() afterwards.
byte[] data = JSON_OBJECT_PRETTY_WRITER.writeValueAsBytes(object);
try (FileOutputStreamPlus out = tempFile.newOutputStream(OVERWRITE))
{
out.write(data);
// Force data to disk before rename to ensure durability.
// Without this, a crash after rename but before OS flushes to disk
// can leave the file with zero-filled or corrupted blocks.
out.sync();
}
tempFile.move(outputFile);
// Fsync the parent directory to ensure the rename is durable.
// Without this, a crash after rename can revert to the old directory entry.
// See: https://transactional.blog/how-to-learn/disk-io
SyncUtil.trySyncDir(outputFile.parent());
}
catch (IOException ex)
{
tempFile.deleteIfExists();
throw ex;
}
}
public static <T> T deserializeFromJsonFile(Class<T> tClass, File file) throws IOException public static <T> T deserializeFromJsonFile(Class<T> tClass, File file) throws IOException
{ {
try (FileInputStreamPlus in = file.newInputStream()) try (FileInputStreamPlus in = file.newInputStream())
@ -202,6 +234,11 @@ public final class JsonUtils
} }
} }
public static <T> T deserializeFromJsonBytes(Class<T> tClass, byte[] bytes) throws IOException
{
return JSON_OBJECT_MAPPER.readValue(bytes, tClass);
}
/** /**
* Handles unquoting and case-insensitivity in map keys. * Handles unquoting and case-insensitivity in map keys.
*/ */

View File

@ -18,11 +18,13 @@
package org.apache.cassandra.service; package org.apache.cassandra.service;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileStore; import java.nio.file.FileStore;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.nio.file.spi.FileSystemProvider; import java.nio.file.spi.FileSystemProvider;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
@ -30,13 +32,13 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.vdurmont.semver4j.Semver; import com.vdurmont.semver4j.Semver;
import org.junit.After; import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Before; import org.junit.Before;
@ -95,14 +97,13 @@ public class StartupChecksTest
StartupChecks startupChecks; StartupChecks startupChecks;
Path sstableDir; Path sstableDir;
static File heartbeatFile; File heartbeatFile;
StartupChecksConfiguration options = new StartupChecksConfiguration(new StartupChecks().withDefaultTests(), new HashMap<>()); StartupChecksConfiguration options = new StartupChecksConfiguration(new StartupChecks().withDefaultTests(), new HashMap<>());
@BeforeClass @BeforeClass
public static void setupServer() public static void setupServer()
{ {
heartbeatFile = createTempFile("cassandra-heartbeat-", "");
SchemaLoader.prepareServer(); SchemaLoader.prepareServer();
} }
@ -118,6 +119,8 @@ public class StartupChecksTest
sstableDir = Paths.get(dataDir.absolutePath(), "Keyspace1", "Standard1"); sstableDir = Paths.get(dataDir.absolutePath(), "Keyspace1", "Standard1");
Files.createDirectories(sstableDir); Files.createDirectories(sstableDir);
heartbeatFile = createTempFile("cassandra-heartbeat-" + UUID.randomUUID(), "");
options.enable("check_data_resurrection"); options.enable("check_data_resurrection");
options.getConfig("check_data_resurrection") options.getConfig("check_data_resurrection")
.put(HEARTBEAT_FILE_CONFIG_PROPERTY, heartbeatFile.absolutePath()); .put(HEARTBEAT_FILE_CONFIG_PROPERTY, heartbeatFile.absolutePath());
@ -129,11 +132,6 @@ public class StartupChecksTest
public void tearDown() throws IOException public void tearDown() throws IOException
{ {
new File(sstableDir).deleteRecursive(); new File(sstableDir).deleteRecursive();
}
@AfterClass
public static void tearDownClass()
{
heartbeatFile.delete(); heartbeatFile.delete();
} }
@ -250,6 +248,53 @@ public class StartupChecksTest
verifyFailure(startupChecks, "Invalid tables: abc.def"); verifyFailure(startupChecks, "Invalid tables: abc.def");
} }
@Test
public void testDataResurrectionCheckLastModifiedFallback() throws Exception
{
DataResurrectionCheck check = new DataResurrectionCheck() {
@Override
List<String> getKeyspaces()
{
return singletonList("test_ks");
}
@Override
List<TableGCPeriod> getTablesGcPeriods(String userKeyspace)
{
return singletonList(new TableGCPeriod("test_table", 10));
}
};
int originalHintWindow = DatabaseDescriptor.getMaxHintWindow();
try
{
DatabaseDescriptor.setMaxHintWindow(5 * 1000);
// Empty file
Files.write(heartbeatFile.toPath(), "".getBytes(StandardCharsets.UTF_8));
Instant recentTimestamp = Instant.ofEpochMilli(Clock.Global.currentTimeMillis());
Files.setLastModifiedTime(heartbeatFile.toPath(), FileTime.from(recentTimestamp));
startupChecks.withTest(check);
verifySuccess(startupChecks);
}
finally
{
DatabaseDescriptor.setMaxHintWindow(originalHintWindow);
}
}
private void verifySuccess(StartupChecks tests) {
try
{
tests.verify(options);
}
catch (StartupException e)
{
fail("Failed startup check with error: " + e.getMessage());
}
}
@Test @Test
public void testKernelBug1057843Check() throws Exception public void testKernelBug1057843Check() throws Exception
{ {