mirror of https://github.com/apache/cassandra
Merge branch 'cassandra-5.0' into trunk
This commit is contained in:
commit
ab80b9e7c8
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue