Merge branch 'cassandra-4.1' into trunk

This commit is contained in:
Jacek Lewandowski 2022-09-16 09:56:02 +02:00
commit 56ea39ec70
11 changed files with 378 additions and 133 deletions

View File

@ -96,11 +96,8 @@ Merged from 4.0:
4.0.7
* Better handle null state in Gossip schema migration to avoid NPE (CASSANDRA-17864)
* HintedHandoffAddRemoveNodesTest now accounts for the fact that StorageMetrics.totalHints is not updated synchronously w/ writes (CASSANDRA-16679)
4.0.7
* Avoid getting hanging repairs due to repair message timeouts (CASSANDRA-17613)
* Fix resetting schema (CASSANDRA-17819)
4.0.6
* Prevent infinite loop in repair coordinator on FailSession (CASSANDRA-17834)

View File

@ -213,6 +213,9 @@ New features
When the node is restarted with UUID based generation identifiers enabled, each newly created sstable will have
a UUID based generation identifier and such files are not readable by previous Cassandra versions. In the future
those new identifiers will become enabled by default.
- Resetting schema behavior has changed in 4.1 so that: 1) resetting schema is prohibited when there is no live node
where the schema could be fetched from, and 2) truncating local schema keyspace is postponed to the moment when
the node receives schema from some other node.
Upgrading
---------

View File

@ -235,6 +235,8 @@ public enum CassandraRelevantProperties
MEMTABLE_OVERHEAD_SIZE("cassandra.memtable.row_overhead_size", "-1"),
MEMTABLE_OVERHEAD_COMPUTE_STEPS("cassandra.memtable_row_overhead_computation_step", "100000"),
MIGRATION_DELAY("cassandra.migration_delay_ms", "60000"),
/** Defines how often schema definitions are pulled from the other nodes */
SCHEMA_PULL_INTERVAL_MS("cassandra.schema_pull_interval_ms", "60000"),
PAXOS_REPAIR_RETRY_TIMEOUT_IN_MS("cassandra.paxos_repair_retry_timeout_millis", "60000"),

View File

@ -20,6 +20,7 @@ package org.apache.cassandra.schema;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@ -46,6 +47,8 @@ import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResu
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.apache.cassandra.utils.concurrent.Awaitable;
import static org.apache.cassandra.schema.MigrationCoordinator.MAX_OUTSTANDING_VERSION_REQUESTS;
@ -60,6 +63,8 @@ public class DefaultSchemaUpdateHandler implements SchemaUpdateHandler, IEndpoin
private final BiConsumer<SchemaTransformationResult, Boolean> updateCallback;
private volatile DistributedSchema schema = DistributedSchema.EMPTY;
private volatile AsyncPromise<Void> requestedReset;
private MigrationCoordinator createMigrationCoordinator(MessagingService messagingService)
{
return new MigrationCoordinator(messagingService,
@ -67,8 +72,8 @@ public class DefaultSchemaUpdateHandler implements SchemaUpdateHandler, IEndpoin
ScheduledExecutors.scheduledTasks,
MAX_OUTSTANDING_VERSION_REQUESTS,
Gossiper.instance,
() -> schema.getVersion(),
(from, mutations) -> applyMutations(mutations));
this::getSchemaVersionForCoordinator,
this::applyMutationsFromCoordinator);
}
public DefaultSchemaUpdateHandler(BiConsumer<SchemaTransformationResult, Boolean> updateCallback)
@ -85,8 +90,23 @@ public class DefaultSchemaUpdateHandler implements SchemaUpdateHandler, IEndpoin
this.updateCallback = updateCallback;
this.migrationCoordinator = migrationCoordinator == null ? createMigrationCoordinator(messagingService) : migrationCoordinator;
Gossiper.instance.register(this);
SchemaPushVerbHandler.instance.register(msg -> applyMutations(msg.payload));
SchemaPullVerbHandler.instance.register(msg -> messagingService.send(msg.responseWith(getSchemaMutations()), msg.from()));
SchemaPushVerbHandler.instance.register(msg -> {
synchronized (this)
{
if (requestedReset == null)
applyMutations(msg.payload);
}
});
SchemaPullVerbHandler.instance.register(msg -> {
try
{
messagingService.send(msg.responseWith(getSchemaMutations()), msg.from());
}
catch (RuntimeException ex)
{
logger.error("Failed to send schema mutations to " + msg.from(), ex);
}
});
}
public synchronized void start()
@ -198,6 +218,7 @@ public class DefaultSchemaUpdateHandler implements SchemaUpdateHandler, IEndpoin
DistributedSchema after = new DistributedSchema(afterKeyspaces, version);
SchemaTransformationResult update = new SchemaTransformationResult(before, after, diff);
logger.info("Applying schema change due to received mutations: {}", update);
updateSchema(update, false);
return update;
}
@ -232,16 +253,23 @@ public class DefaultSchemaUpdateHandler implements SchemaUpdateHandler, IEndpoin
private void updateSchema(SchemaTransformationResult update, boolean local)
{
this.schema = update.after;
logger.debug("Schema updated: {}", update);
updateCallback.accept(update, true);
if (!local)
if (!update.diff.isEmpty())
{
migrationCoordinator.announce(update.after.getVersion());
this.schema = update.after;
logger.debug("Schema updated: {}", update);
updateCallback.accept(update, true);
if (!local)
{
migrationCoordinator.announce(update.after.getVersion());
}
}
else
{
logger.debug("Schema update is empty - skipping");
}
}
private synchronized SchemaTransformationResult reload()
private synchronized void reload()
{
DistributedSchema before = this.schema;
DistributedSchema after = new DistributedSchema(SchemaKeyspace.fetchNonSystemKeyspaces(), SchemaKeyspace.calculateSchemaDigest());
@ -249,29 +277,76 @@ public class DefaultSchemaUpdateHandler implements SchemaUpdateHandler, IEndpoin
SchemaTransformationResult update = new SchemaTransformationResult(before, after, diff);
updateSchema(update, false);
return update;
}
@Override
public SchemaTransformationResult reset(boolean local)
public void reset(boolean local)
{
if (local)
return reload();
Collection<Mutation> mutations = migrationCoordinator.pullSchemaFromAnyNode().awaitThrowUncheckedOnInterrupt().getNow();
return applyMutations(mutations);
{
reload();
}
else
{
migrationCoordinator.reset();
if (!migrationCoordinator.awaitSchemaRequests(CassandraRelevantProperties.MIGRATION_DELAY.getLong()))
{
logger.error("Timeout exceeded when waiting for schema from other nodes");
}
}
}
/**
* When clear is called the update handler will flag that the clear was requested. It means that migration
* coordinator will think that we have empty schema version and will apply whatever it receives from other nodes.
* When a first attempt to apply mutations from other node is called, it will first clear the schema and apply
* the mutations on a truncated table. The flag is then reset.
* <p>
* This way the clear is postponed until we really fetch any schema we can use as a replacement. Otherwise, nothing
* will happen. We will simply reset the flag after the timeout and throw exceptions to the caller.
*
* @return
*/
@Override
public synchronized void clear()
public Awaitable clear()
{
SchemaKeyspace.truncate();
this.schema = DistributedSchema.EMPTY;
synchronized (this)
{
if (requestedReset == null)
{
requestedReset = new AsyncPromise<>();
migrationCoordinator.reset();
}
return requestedReset;
}
}
private UUID getSchemaVersionForCoordinator()
{
if (requestedReset != null)
return SchemaConstants.emptyVersion;
else
return schema.getVersion();
}
private synchronized void applyMutationsFromCoordinator(InetAddressAndPort from, Collection<Mutation> mutations)
{
if (requestedReset != null && !mutations.isEmpty())
{
schema = DistributedSchema.EMPTY;
SchemaKeyspace.truncate();
requestedReset.setSuccess(null);
requestedReset = null;
}
applyMutations(mutations);
}
private synchronized Collection<Mutation> getSchemaMutations()
{
return SchemaKeyspace.convertSchemaToMutations();
if (requestedReset != null)
return Collections.emptyList();
else
return SchemaKeyspace.convertSchemaToMutations();
}
public Map<UUID, Set<InetAddressAndPort>> getOutstandingSchemaVersions()

View File

@ -20,22 +20,22 @@ package org.apache.cassandra.schema;
import java.lang.management.ManagementFactory;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
@ -69,13 +69,13 @@ import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.Simulate;
import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.apache.cassandra.utils.concurrent.Future;
import org.apache.cassandra.utils.concurrent.ImmediateFuture;
import org.apache.cassandra.utils.concurrent.WaitQueue;
import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORED_SCHEMA_CHECK_ENDPOINTS;
import static org.apache.cassandra.config.CassandraRelevantProperties.IGNORED_SCHEMA_CHECK_VERSIONS;
import static org.apache.cassandra.config.CassandraRelevantProperties.SCHEMA_PULL_INTERVAL_MS;
import static org.apache.cassandra.net.Verb.SCHEMA_PUSH_REQ;
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
import static org.apache.cassandra.utils.Simulate.With.MONITORS;
@ -86,6 +86,10 @@ import static org.apache.cassandra.utils.concurrent.WaitQueue.newWaitQueue;
* schema. It performs periodic checks and if there is a schema version mismatch between the current node and the other
* node, it pulls the schema and applies the changes locally through the callback.
*
* In particular the Migration Coordinator keeps track of all schema versions reported from each node in the cluster.
* As long as a certain version is advertised by some node, it is being tracked. As long as a version is tracked,
* the migration coordinator tries to fetch it by its periodic job.
*
* It works in close cooperation with {@link DefaultSchemaUpdateHandler} which is responsible for maintaining local
* schema metadata stored in {@link SchemaKeyspace}.
*/
@ -150,12 +154,27 @@ public class MigrationCoordinator
{
final UUID version;
/**
* The set of endpoints containing this schema version
*/
final Set<InetAddressAndPort> endpoints = Sets.newConcurrentHashSet();
/**
* The set of endpoints from which we are already fetching the schema
*/
final Set<InetAddressAndPort> outstandingRequests = Sets.newConcurrentHashSet();
/**
* The queue of endpoints from which we are going to fetch the schema
*/
final Deque<InetAddressAndPort> requestQueue = new ArrayDeque<>();
/**
* Threads waiting for schema synchronization are waiting until this object is signalled
*/
private final WaitQueue waitQueue = newWaitQueue();
/**
* Whether this schema version have been received
*/
volatile boolean receivedSchema;
VersionInfo(UUID version)
@ -181,6 +200,18 @@ public class MigrationCoordinator
{
return receivedSchema;
}
@Override
public String toString()
{
return "VersionInfo{" +
"version=" + version +
", outstandingRequests=" + outstandingRequests +
", requestQueue=" + requestQueue +
", waitQueue.waiting=" + waitQueue.getWaiting() +
", receivedSchema=" + receivedSchema +
'}';
}
}
private final Map<UUID, VersionInfo> versionInfo = new HashMap<>();
@ -221,18 +252,23 @@ public class MigrationCoordinator
void start()
{
logger.info("Starting migration coordinator and scheduling pulling schema versions every {}", Duration.ofMillis(SCHEMA_PULL_INTERVAL_MS.getLong()));
announce(schemaVersion.get());
periodicPullTask.updateAndGet(curTask -> curTask == null
? periodicCheckExecutor.scheduleWithFixedDelay(this::pullUnreceivedSchemaVersions, 1, 1, TimeUnit.MINUTES)
? periodicCheckExecutor.scheduleWithFixedDelay(this::pullUnreceivedSchemaVersions, SCHEMA_PULL_INTERVAL_MS.getLong(), SCHEMA_PULL_INTERVAL_MS.getLong(), TimeUnit.MILLISECONDS)
: curTask);
}
private synchronized void pullUnreceivedSchemaVersions()
{
logger.debug("Pulling unreceived schema versions...");
for (VersionInfo info : versionInfo.values())
{
if (info.wasReceived() || info.outstandingRequests.size() > 0)
{
logger.trace("Skipping pull of schema {} because it has been already recevied, or it is being received ({})", info.version, info);
continue;
}
maybePullSchema(info);
}
@ -241,16 +277,25 @@ public class MigrationCoordinator
private synchronized Future<Void> maybePullSchema(VersionInfo info)
{
if (info.endpoints.isEmpty() || info.wasReceived() || !shouldPullSchema(info.version))
{
logger.trace("Not pulling schema {} because it was received, there is no endpoint to provide it, or we should not pull it ({})", info.version, info);
return FINISHED_FUTURE;
}
if (info.outstandingRequests.size() >= maxOutstandingVersionRequests)
{
logger.trace("Not pulling schema {} because the number of outstanding requests has been exceeded ({} >= {})", info.version, info.outstandingRequests.size(), maxOutstandingVersionRequests);
return FINISHED_FUTURE;
}
for (int i = 0, isize = info.requestQueue.size(); i < isize; i++)
{
InetAddressAndPort endpoint = info.requestQueue.remove();
if (!info.endpoints.contains(endpoint))
{
logger.trace("Skipping request of schema {} from {} because the endpoint does not have that schema any longer", info.version, endpoint);
continue;
}
if (shouldPullFromEndpoint(endpoint) && info.outstandingRequests.add(endpoint))
{
@ -259,6 +304,7 @@ public class MigrationCoordinator
else
{
// return to queue
logger.trace("Could not pull schema {} from {} - the request will be added back to the queue", info.version, endpoint);
info.requestQueue.offer(endpoint);
}
}
@ -287,30 +333,33 @@ public class MigrationCoordinator
UUID localSchemaVersion = schemaVersion.get();
if (localSchemaVersion == null)
{
logger.debug("Not pulling schema for version {}, because local schama version is not known yet", version);
logger.debug("Not pulling schema {} because the local schama version is not known yet", version);
return false;
}
if (localSchemaVersion.equals(version))
{
logger.debug("Not pulling schema for version {}, because schema versions match: " +
"local={}, remote={}",
version,
DistributedSchema.schemaVersionToString(localSchemaVersion),
DistributedSchema.schemaVersionToString(version));
logger.debug("Not pulling schema {} because it is the same as the local schema", version);
return false;
}
return true;
}
private boolean shouldPullFromEndpoint(InetAddressAndPort endpoint)
{
if (endpoint.equals(FBUtilities.getBroadcastAddressAndPort()))
{
logger.trace("Not pulling schema from local endpoint");
return false;
}
EndpointState state = gossiper.getEndpointStateForEndpoint(endpoint);
if (state == null)
{
logger.trace("Not pulling schema from endpoint {} because its state is unknown", endpoint);
return false;
}
VersionedValue releaseVersionValue = state.getApplicationState(ApplicationState.RELEASE_VERSION);
if (releaseVersionValue == null)
@ -373,22 +422,34 @@ public class MigrationCoordinator
synchronized Future<Void> reportEndpointVersion(InetAddressAndPort endpoint, UUID version)
{
logger.debug("Reported schema {} at endpoint {}", version, endpoint);
if (ignoredEndpoints.contains(endpoint) || IGNORED_VERSIONS.contains(version))
{
endpointVersions.remove(endpoint);
removeEndpointFromVersion(endpoint, null);
logger.debug("Discarding endpoint {} or schema {} because either endpoint or schema version were marked as ignored", endpoint, version);
return FINISHED_FUTURE;
}
UUID current = endpointVersions.put(endpoint, version);
if (current != null && current.equals(version))
{
logger.trace("Skipping report of schema {} from {} because we already know that", version, endpoint);
return FINISHED_FUTURE;
}
VersionInfo info = versionInfo.computeIfAbsent(version, VersionInfo::new);
if (Objects.equals(schemaVersion.get(), version))
{
info.markReceived();
logger.trace("Schema {} from {} has been marked as recevied because it is equal the local schema", version, endpoint);
}
else
{
info.requestQueue.addFirst(endpoint);
}
info.endpoints.add(endpoint);
info.requestQueue.addFirst(endpoint);
logger.trace("Added endpoint {} to schema {}: {}", endpoint, info.version, info);
// disassociate this endpoint from its (now) previous schema version
removeEndpointFromVersion(endpoint, current);
@ -407,16 +468,70 @@ public class MigrationCoordinator
return;
info.endpoints.remove(endpoint);
logger.trace("Removed endpoint {} from schema {}: {}", endpoint, version, info);
if (info.endpoints.isEmpty())
{
info.waitQueue.signalAll();
versionInfo.remove(version);
logger.trace("Removed schema info: {}", info);
}
}
private void clearVersionsInfo()
{
Iterator<Map.Entry<UUID, VersionInfo>> it = versionInfo.entrySet().iterator();
while (it.hasNext())
{
Map.Entry<UUID, VersionInfo> entry = it.next();
it.remove();
entry.getValue().waitQueue.signal();
}
}
private void reportCurrentSchemaVersionOnEndpoint(InetAddressAndPort endpoint)
{
if (FBUtilities.getBroadcastAddressAndPort().equals(endpoint))
{
reportEndpointVersion(endpoint, schemaVersion.get());
}
else
{
EndpointState state = gossiper.getEndpointStateForEndpoint(endpoint);
if (state != null)
{
UUID v = state.getSchemaVersion();
if (v != null)
{
reportEndpointVersion(endpoint, v);
}
}
}
}
/**
* Resets the migration coordinator by notifying all waiting threads and removing all the existing version info.
* Then, it is populated with the information about schema versions on different endpoints provided by Gossiper.
* Each version is marked as unreceived so the migration coordinator will start pulling schemas from other nodes.
*/
synchronized void reset()
{
logger.info("Resetting migration coordinator...");
// clear all the managed information
this.endpointVersions.clear();
clearVersionsInfo();
// now report again the versions we are aware of
gossiper.getLiveMembers().forEach(this::reportCurrentSchemaVersionOnEndpoint);
}
synchronized void removeAndIgnoreEndpoint(InetAddressAndPort endpoint)
{
logger.debug("Removing and ignoring endpoint {}", endpoint);
Preconditions.checkArgument(endpoint != null);
// TODO The endpoint address is now ignored but when a node with the same address is added again later,
// there will be no way to include it in schema synchronization other than restarting each other node
// see https://issues.apache.org/jira/browse/CASSANDRA-17883 for details
ignoredEndpoints.add(endpoint);
Set<UUID> versions = ImmutableSet.copyOf(versionInfo.keySet());
for (UUID version : versions)
@ -430,49 +545,19 @@ public class MigrationCoordinator
FutureTask<Void> task = new FutureTask<>(() -> pullSchema(endpoint, new Callback(endpoint, info)));
if (shouldPullImmediately(endpoint, info.version))
{
logger.debug("Pulling {} immediately from {}", info, endpoint);
submitToMigrationIfNotShutdown(task);
}
else
{
logger.debug("Postponing pull of {} from {} for {}ms", info, endpoint, MIGRATION_DELAY_IN_MS);
ScheduledExecutors.nonPeriodicTasks.schedule(() -> submitToMigrationIfNotShutdown(task), MIGRATION_DELAY_IN_MS, TimeUnit.MILLISECONDS);
}
return task;
}
private Future<Collection<Mutation>> pullSchemaFrom(InetAddressAndPort endpoint)
{
AsyncPromise<Collection<Mutation>> result = new AsyncPromise<>();
return submitToMigrationIfNotShutdown(() -> pullSchema(endpoint, new RequestCallback<Collection<Mutation>>()
{
@Override
public void onResponse(Message<Collection<Mutation>> msg)
{
result.setSuccess(msg.payload);
}
@Override
public void onFailure(InetAddressAndPort from, RequestFailureReason failureReason)
{
result.setFailure(new RuntimeException("Failed to get schema from " + from + ". The failure reason was: " + failureReason));
}
@Override
public boolean invokeOnFailure()
{
return true;
}
})).flatMap(ignored -> result);
}
Future<Collection<Mutation>> pullSchemaFromAnyNode()
{
Optional<InetAddressAndPort> endpoint = gossiper.getLiveMembers()
.stream()
.filter(this::shouldPullFromEndpoint)
.findFirst();
return endpoint.map(this::pullSchemaFrom).orElse(ImmediateFuture.success(Collections.emptyList()));
}
void announce(UUID schemaVersion)
{
if (gossiper.isEnabled())

View File

@ -27,6 +27,8 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult;
import org.apache.cassandra.utils.ByteArrayUtil;
import org.apache.cassandra.utils.concurrent.Awaitable;
import org.apache.cassandra.utils.concurrent.ImmediateFuture;
/**
* Update handler which works only in memory. It does not load or save the schema anywhere. It is used in client mode
@ -77,17 +79,18 @@ public class OfflineSchemaUpdateHandler implements SchemaUpdateHandler
}
@Override
public SchemaTransformationResult reset(boolean local)
public void reset(boolean local)
{
if (!local)
throw new UnsupportedOperationException();
return apply(ignored -> SchemaKeyspace.fetchNonSystemKeyspaces(), local);
apply(ignored -> SchemaKeyspace.fetchNonSystemKeyspaces(), local);
}
@Override
public synchronized void clear()
public synchronized Awaitable clear()
{
this.schema = DistributedSchema.EMPTY;
return ImmediateFuture.success(true);
}
}

View File

@ -19,6 +19,7 @@ package org.apache.cassandra.schema;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Supplier;
@ -34,6 +35,7 @@ import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.LocalStrategy;
@ -41,14 +43,16 @@ import org.apache.cassandra.schema.KeyspaceMetadata.KeyspaceDiff;
import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult;
import org.apache.cassandra.service.PendingRangeCalculatorService;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.concurrent.Awaitable;
import org.apache.cassandra.utils.concurrent.LoadingMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.lang.String.format;
import static com.google.common.collect.Iterables.size;
import static java.lang.String.format;
import static org.apache.cassandra.config.DatabaseDescriptor.isDaemonInitialized;
import static org.apache.cassandra.config.DatabaseDescriptor.isToolInitialized;
@ -611,18 +615,28 @@ public class Schema implements SchemaProvider
* Clear all locally stored schema information and fetch schema from another node.
* Called by user (via JMX) who wants to get rid of schema disagreement.
*/
public synchronized void resetLocalSchema()
public void resetLocalSchema()
{
logger.debug("Clearing local schema...");
updateHandler.clear();
logger.debug("Clearing local schema keyspace instances...");
distributedKeyspaces.forEach(this::unload);
updateVersion(SchemaConstants.emptyVersion);
if (Gossiper.instance.getLiveMembers().stream().allMatch(ep -> FBUtilities.getBroadcastAddressAndPort().equals(ep)))
throw new InvalidRequestException("Cannot reset local schema when there are no other live nodes");
Awaitable clearCompletion = updateHandler.clear();
try
{
if (!clearCompletion.await(StorageService.SCHEMA_DELAY_MILLIS, TimeUnit.MILLISECONDS))
{
throw new RuntimeException("Schema reset failed - no schema received from other nodes");
}
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
throw new RuntimeException("Failed to reset schema - the thread has been interrupted");
}
SchemaDiagnostics.schemaCleared(this);
updateHandler.reset(false);
logger.info("Local schema reset is complete.");
logger.info("Local schema reset completed");
}
private void merge(KeyspacesDiff diff, boolean removeData)

View File

@ -21,6 +21,7 @@ package org.apache.cassandra.schema;
import java.time.Duration;
import org.apache.cassandra.schema.SchemaTransformation.SchemaTransformationResult;
import org.apache.cassandra.utils.concurrent.Awaitable;
/**
* Schema update handler is responsible for maintaining the shared schema and synchronizing it with other nodes in
@ -63,14 +64,15 @@ public interface SchemaUpdateHandler
* refreshed, the callbacks provided in the factory method are executed, and the updated schema version is announced.
*
* @param local whether we should reset with locally stored schema or fetch the schema from other nodes
* @return transformation result
*/
SchemaTransformationResult reset(boolean local);
void reset(boolean local);
/**
* Clears the locally stored schema entirely. After this operation the schema is equal to {@link DistributedSchema#EMPTY}.
* The method does not execute any callback. It is indended to reinitialize the schema later using the method
* {@link #reset(boolean)}.
* Marks the local schema to be cleared and refreshed. Since calling this method, the update handler tries to obtain
* a fresh schema definition from a remote source. Once the schema definition is received, the local schema is
* replaced (instead of being merged which usually happens when the update is received).
* <p/>
* The returned awaitable is fulfilled when the schema is received and applied.
*/
void clear();
Awaitable clear();
}

View File

@ -18,20 +18,35 @@
package org.apache.cassandra.distributed.test;
import java.time.Duration;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.IIsolatedExecutor.SerializableCallable;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.SchemaConstants;
import org.assertj.core.api.Assertions;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionFactory;
import static java.time.Duration.ofSeconds;
import static org.apache.cassandra.utils.FBUtilities.getBroadcastAddressAndPort;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class SchemaTest extends TestBaseImpl
{
public static final String TABLE_ONE = "tbl_one";
public static final String TABLE_TWO = "tbl_two";
@Test
public void readRepair() throws Throwable
{
@ -84,7 +99,7 @@ public class SchemaTest extends TestBaseImpl
Throwable cause = e;
while (cause != null)
{
if (cause.getMessage() != null && cause.getMessage().contains("Unknown column "+name+" during deserialization"))
if (cause.getMessage() != null && cause.getMessage().contains("Unknown column " + name + " during deserialization"))
causeIsUnknownColumn = true;
cause = cause.getCause();
}
@ -92,32 +107,94 @@ public class SchemaTest extends TestBaseImpl
}
}
/**
* The purpose of this test is to verify manual schema reset functinality.
* <p>
* There is a 2-node cluster and a TABLE_ONE created. The schema version is agreed on both nodes. Then the 2nd node
* is shutdown. We introduce a disagreement by dropping TABLE_ONE and creating TABLE_TWO on the 1st node. Therefore,
* the 1st node has a newer schema version with TABLE_TWO, while the shutdown 2nd node has older schema version with
* TABLE_ONE.
* <p>
* At this point, if we just started the 2nd node, it would sync its schema by getting fresh mutations from the 1st
* node which would result in both nodes having only the definition of TABLE_TWO.
* <p>
* However, before starting the 2nd node the schema is reset on the 1st node, so the 1st node will discard its local
* schema whenever it manages to fetch a schema definition from some other node (the 2nd node in this case).
* It is expected to end up with both nodes having only the definition of TABLE_ONE.
* <p>
* In the second phase of the test we simply break the schema on the 1st node and call reset to fetch the schema
* definition it from the 2nd node.
*/
@Test
public void schemaReset() throws Throwable
{
CassandraRelevantProperties.MIGRATION_DELAY.setLong(10000);
CassandraRelevantProperties.SCHEMA_PULL_INTERVAL_MS.setLong(10000);
try (Cluster cluster = init(Cluster.build(2).withConfig(cfg -> cfg.with(Feature.GOSSIP, Feature.NETWORK)).start()))
{
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk INT PRIMARY KEY, v TEXT)");
assertTrue(cluster.get(1).callOnInstance(() -> Schema.instance.getTableMetadata(KEYSPACE, "tbl") != null));
assertTrue(cluster.get(2).callOnInstance(() -> Schema.instance.getTableMetadata(KEYSPACE, "tbl") != null));
// create TABLE_ONE and make sure it is propagated
cluster.schemaChange(String.format("CREATE TABLE %s.%s (pk INT PRIMARY KEY, v TEXT)", KEYSPACE, TABLE_ONE));
assertTrue(checkTablesPropagated(cluster.get(1), true, false));
assertTrue(checkTablesPropagated(cluster.get(2), true, false));
// shutdown the 2nd node and make sure that the 1st does not see it any longer as alive
cluster.get(2).shutdown().get();
await(30).until(() -> cluster.get(1).callOnInstance(() -> {
return Gossiper.instance.getLiveMembers()
.stream()
.allMatch(e -> e.equals(getBroadcastAddressAndPort()));
}));
// when schema is removed and there is no other node to fetch it from, node 1 should be left with clean schema
cluster.get(1).runOnInstance(() -> Schema.instance.resetLocalSchema());
assertTrue(cluster.get(1).callOnInstance(() -> Schema.instance.getTableMetadata(KEYSPACE, "tbl") == null));
// when there is no node to fetch the schema from, reset local schema should immediately fail
Assertions.assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> {
cluster.get(1).runOnInstance(() -> Schema.instance.resetLocalSchema());
}).withMessageContaining("Cannot reset local schema when there are no other live nodes");
// when the other node is started, schema should be back in sync
// now, let's make a disagreement, the shutdown node 2 has a definition of TABLE_ONE, while the running
// node 1 will have a definition of TABLE_TWO
cluster.coordinator(1).execute(String.format("DROP TABLE %s.%s", KEYSPACE, TABLE_ONE), ConsistencyLevel.ONE);
cluster.coordinator(1).execute(String.format("CREATE TABLE %s.%s (pk INT PRIMARY KEY, v TEXT)", KEYSPACE, TABLE_TWO), ConsistencyLevel.ONE);
await(30).until(() -> checkTablesPropagated(cluster.get(1), false, true));
// Schema.resetLocalSchema is guarded by some conditions which would not let us reset schema if there is no
// live node in the cluster, therefore we simply call SchemaUpdateHandler.clear (this is the only real thing
// being done by Schema.resetLocalSchema under the hood)
SerializableCallable<Boolean> clear = () -> Schema.instance.updateHandler.clear().awaitUninterruptibly(1, TimeUnit.MINUTES);
Future<Boolean> clear1 = cluster.get(1).asyncCallsOnInstance(clear).call();
assertFalse(clear1.isDone());
// when the 2nd node is started, schema should be back in sync
cluster.get(2).startup();
Awaitility.waitAtMost(Duration.ofMinutes(1))
.pollDelay(Duration.ofSeconds(1))
.until(() -> cluster.get(1).callOnInstance(() -> Schema.instance.getTableMetadata(KEYSPACE, "tbl") != null));
await(30).until(() -> clear1.isDone() && clear1.get());
// when schema is removed and there is a node to fetch it from, node 1 should immediatelly restore the schema
// this proves that reset schema works on the 1st node - the most recent change should be discarded because
// it receives the schema from the 2nd node and applies it on empty schema
await(60).until(() -> checkTablesPropagated(cluster.get(1), true, false));
// now let's break schema locally and let it be reset
cluster.get(1).runOnInstance(() -> Schema.instance.getLocalKeyspaces()
.get(SchemaConstants.SCHEMA_KEYSPACE_NAME)
.get().tables.forEach(t -> ColumnFamilyStore.getIfExists(t.keyspace, t.name).truncateBlockingWithoutSnapshot()));
// when schema is removed and there is a node to fetch it from, the 1st node should immediately restore it
cluster.get(1).runOnInstance(() -> Schema.instance.resetLocalSchema());
assertTrue(cluster.get(1).callOnInstance(() -> Schema.instance.getTableMetadata(KEYSPACE, "tbl") != null));
// note that we should not wait for this to be true because resetLocalSchema is blocking
// and after successfully completing it, the schema should be already back in sync
assertTrue(checkTablesPropagated(cluster.get(1), true, false));
assertTrue(checkTablesPropagated(cluster.get(2), true, false));
}
}
private static ConditionFactory await(int seconds)
{
return Awaitility.await().atMost(ofSeconds(seconds)).pollDelay(ofSeconds(1));
}
private static boolean checkTablesPropagated(IInvokableInstance instance, boolean one, boolean two)
{
return instance.callOnInstance(() -> {
return (Schema.instance.getTableMetadata(KEYSPACE, TABLE_ONE) != null ^ !one)
&& (Schema.instance.getTableMetadata(KEYSPACE, TABLE_TWO) != null ^ !two);
});
}
}

View File

@ -53,7 +53,8 @@ public class JMXGetterCheckTest extends TestBaseImpl
private static final Set<String> IGNORE_OPERATIONS = ImmutableSet.of(
"org.apache.cassandra.db:type=StorageService:stopDaemon", // halts the instance, which then causes the JVM to exit
"org.apache.cassandra.db:type=StorageService:drain", // don't drain, it stops things which can cause other APIs to be unstable as we are in a stopped state
"org.apache.cassandra.db:type=StorageService:stopGossiping" // if we stop gossip this can causes other issues, so avoid
"org.apache.cassandra.db:type=StorageService:stopGossiping", // if we stop gossip this can cause other issues, so avoid
"org.apache.cassandra.db:type=StorageService:resetLocalSchema" // this will fail when there are no other nodes which can serve schema
);
@Test

View File

@ -42,7 +42,6 @@ import org.slf4j.LoggerFactory;
import org.apache.cassandra.concurrent.ImmediateExecutor;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.exceptions.RequestFailureReason;
import org.apache.cassandra.gms.ApplicationState;
import org.apache.cassandra.gms.EndpointState;
import org.apache.cassandra.gms.Gossiper;
@ -63,7 +62,6 @@ import org.mockito.internal.creation.MockSettingsImpl;
import static com.google.common.util.concurrent.Futures.getUnchecked;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
@ -398,25 +396,27 @@ public class MigrationCoordinatorTest
}
@Test
public void pullSchemaFromAnyNode() throws UnknownHostException
public void reset() throws UnknownHostException
{
Collection<Mutation> mutations = Arrays.asList(mock(Mutation.class));
Wrapper wrapper = new Wrapper();
// no live nodes
when(wrapper.gossiper.getLiveMembers()).thenReturn(Collections.emptySet());
Collection<Mutation> result = wrapper.coordinator.pullSchemaFromAnyNode().syncThrowUncheckedOnInterrupt().getNow();
assertThat(result).isEmpty();
wrapper.localSchemaVersion = SchemaConstants.emptyVersion;
EndpointState invalidVersionState = mock(EndpointState.class);
when(invalidVersionState.getApplicationState(ApplicationState.RELEASE_VERSION)).thenReturn(VersionedValue.unsafeMakeVersionedValue("3.0", 0));
when(invalidVersionState.getSchemaVersion()).thenReturn(V1);
EndpointState validVersionState = mock(EndpointState.class);
when(validVersionState.getApplicationState(ApplicationState.RELEASE_VERSION)).thenReturn(VersionedValue.unsafeMakeVersionedValue(FBUtilities.getReleaseVersionString(), 0));
when(validVersionState.getSchemaVersion()).thenReturn(V2);
EndpointState localVersionState = mock(EndpointState.class);
when(localVersionState.getApplicationState(ApplicationState.RELEASE_VERSION)).thenReturn(VersionedValue.unsafeMakeVersionedValue(FBUtilities.getReleaseVersionString(), 0));
when(localVersionState.getSchemaVersion()).thenReturn(SchemaConstants.emptyVersion);
// some nodes
InetAddressAndPort thisNode = wrapper.configureMocksForEndpoint(FBUtilities.getBroadcastAddressAndPort(), validVersionState, MessagingService.current_version, false);
InetAddressAndPort thisNode = wrapper.configureMocksForEndpoint(FBUtilities.getBroadcastAddressAndPort(), localVersionState, MessagingService.current_version, false);
InetAddressAndPort noStateNode = wrapper.configureMocksForEndpoint("10.0.0.1:8000", null, MessagingService.current_version, false);
InetAddressAndPort diffMajorVersionNode = wrapper.configureMocksForEndpoint("10.0.0.2:8000", invalidVersionState, MessagingService.current_version, false);
InetAddressAndPort unkonwnNode = wrapper.configureMocksForEndpoint("10.0.0.2:8000", validVersionState, null, false);
@ -436,22 +436,8 @@ public class MigrationCoordinatorTest
callback.onResponse(Message.remoteResponse(regularNode1, Verb.SCHEMA_PULL_RSP, mutations));
return null;
}).when(wrapper.messagingService).sendWithCallback(any(Message.class), any(InetAddressAndPort.class), any(RequestCallback.class));
result = wrapper.coordinator.pullSchemaFromAnyNode().syncThrowUncheckedOnInterrupt().getNow();
assertThat(result).isEqualTo(mutations);
// failures
doAnswer(a -> {
Message msg = a.getArgument(0, Message.class);
InetAddressAndPort endpoint = a.getArgument(1, InetAddressAndPort.class);
RequestCallback callback = a.getArgument(2, RequestCallback.class);
assertThat(msg.verb()).isEqualTo(Verb.SCHEMA_PULL_REQ);
assertThat(endpoint).isEqualTo(regularNode1);
callback.onFailure(regularNode1, RequestFailureReason.UNKNOWN);
return null;
}).when(wrapper.messagingService).sendWithCallback(any(Message.class), any(InetAddressAndPort.class), any(RequestCallback.class));
assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> wrapper.coordinator.pullSchemaFromAnyNode().syncThrowUncheckedOnInterrupt().getNow())
.withMessageContaining("Failed to get schema from");
wrapper.coordinator.reset();
assertThat(wrapper.mergedSchemasFrom).anyMatch(ep -> regularNode1.equals(ep) || regularNode2.equals(ep));
assertThat(wrapper.mergedSchemasFrom).hasSize(1);
}
}