mirror of https://github.com/apache/cassandra
168 lines
9.2 KiB
Java
168 lines
9.2 KiB
Java
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package org.apache.cassandra.repair;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.stream.Collectors;
|
|
|
|
import com.google.common.collect.ImmutableList;
|
|
import org.junit.Test;
|
|
|
|
import accord.utilsfork.Gen;
|
|
import accord.utilsfork.Gens;
|
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
|
import org.apache.cassandra.config.RetrySpec;
|
|
import org.apache.cassandra.locator.InetAddressAndPort;
|
|
import org.apache.cassandra.repair.messages.RepairOption;
|
|
import org.apache.cassandra.repair.state.Completable;
|
|
import org.apache.cassandra.streaming.StreamEventHandler;
|
|
import org.apache.cassandra.streaming.StreamState;
|
|
import org.apache.cassandra.utils.Closeable;
|
|
import org.assertj.core.api.AbstractStringAssert;
|
|
import org.assertj.core.api.Assertions;
|
|
|
|
import static accord.utilsfork.Property.qt;
|
|
|
|
public class FailingRepairFuzzTest extends FuzzTestBase
|
|
{
|
|
private enum RepairJobStage { VALIDATION, SYNC }
|
|
|
|
@Test
|
|
public void failingRepair()
|
|
{
|
|
// to avoid unlucky timing issues, retry until success; given enough retries we should eventually become success
|
|
DatabaseDescriptor.getRepairRetrySpec().maxAttempts = new RetrySpec.MaxAttempt(Integer.MAX_VALUE);
|
|
Gen<RepairJobStage> stageGen = Gens.enums().all(RepairJobStage.class);
|
|
qt().withPure(false).withExamples(10).check(rs -> {
|
|
Cluster cluster = new Cluster(rs);
|
|
enableMessageFaults(cluster);
|
|
|
|
Gen<Cluster.Node> coordinatorGen = Gens.pick(cluster.nodes.keySet()).map(cluster.nodes::get);
|
|
|
|
List<Closeable> closeables = new ArrayList<>();
|
|
for (int example = 0; example < 100; example++)
|
|
{
|
|
Cluster.Node coordinator = coordinatorGen.next(rs);
|
|
|
|
// exclude accord repair as this test breaks validation/sync; which accord doesn't have
|
|
RepairOption options = repairOption(rs, coordinator, KEYSPACE, TABLES);
|
|
RepairCoordinator repair = coordinator.repair(KEYSPACE, options, false);
|
|
repair.run();
|
|
InetAddressAndPort failingAddress = pickParticipant(rs, coordinator, repair);
|
|
Cluster.Node failingNode = cluster.nodes.get(failingAddress);
|
|
RepairJobStage stage = stageGen.next(rs);
|
|
// because of local syncs reaching out to the failing address, a different address may actually be what failed
|
|
Set<InetAddressAndPort> syncFailedAddresses = new HashSet<>();
|
|
switch (stage)
|
|
{
|
|
case VALIDATION:
|
|
{
|
|
closeables.add(failingNode.doValidation((cfs, validator) -> {
|
|
long delayNanos = rs.nextLong(TimeUnit.MILLISECONDS.toNanos(5), TimeUnit.MINUTES.toNanos(1));
|
|
cluster.unorderedScheduled.schedule(() -> validator.fail(new SimulatedFault("Validation failed")), delayNanos, TimeUnit.NANOSECONDS);
|
|
}));
|
|
}
|
|
break;
|
|
case SYNC:
|
|
{
|
|
closeables.add(failingNode.doValidation((cfs, validator) -> addMismatch(rs, cfs, validator)));
|
|
List<InetAddressAndPort> addresses = ImmutableList.<InetAddressAndPort>builder().add(coordinator.addressAndPort).addAll(repair.state.getNeighborsAndRanges().participants).build();
|
|
for (InetAddressAndPort address : addresses)
|
|
{
|
|
closeables.add(cluster.nodes.get(address).doSync(plan -> {
|
|
long delayNanos = rs.nextLong(TimeUnit.SECONDS.toNanos(5), TimeUnit.MINUTES.toNanos(10));
|
|
cluster.unorderedScheduled.schedule(() -> {
|
|
if (address == failingAddress || plan.getCoordinator().getPeers().contains(failingAddress))
|
|
{
|
|
syncFailedAddresses.add(address);
|
|
SimulatedFault fault = new SimulatedFault("Sync failed");
|
|
for (StreamEventHandler handler : plan.handlers())
|
|
handler.onFailure(fault);
|
|
}
|
|
else
|
|
{
|
|
StreamState success = new StreamState(plan.planId(), plan.streamOperation(), Collections.emptySet());
|
|
for (StreamEventHandler handler : plan.handlers())
|
|
handler.onSuccess(success);
|
|
}
|
|
}, delayNanos, TimeUnit.NANOSECONDS);
|
|
return null;
|
|
}));
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException("Unknown stage: " + stage);
|
|
}
|
|
|
|
cluster.processAll();
|
|
Assertions.assertThat(repair.state.isComplete()).describedAs("Repair job did not complete, and no work is pending...").isTrue();
|
|
Assertions.assertThat(repair.state.getResult().kind).describedAs("Unexpected state: %s -> %s; example %d", repair.state, repair.state.getResult(), example).isEqualTo(Completable.Result.Kind.FAILURE);
|
|
switch (stage)
|
|
{
|
|
case VALIDATION:
|
|
{
|
|
// Got VALIDATION_REQ failure from Ero2.MJ.N8kkw2.w3iFYdDw.HJiVYC32.mWb.b.xwi3tZ.s5k1l.mb.asTy_7QmQ.Q3.u.kjgh.GKjx.g1aKfkjB.YlyKg9.DyQszn7F.Ox2DMYIph.xlgH.EV.A9yEz2J.l6UHdC.C6FYLXE.J0CNHBH./[4905:e9f:2f00:e418:baac:b8d9:9ff9:6604]:33363: UNKNOWN"
|
|
Assertions.assertThat(repair.state.getResult().message)
|
|
.describedAs("Unexpected state: %s -> %s; example %d", repair.state, repair.state.getResult(), example)
|
|
// ValidationResponse with null tree seen
|
|
.containsAnyOf("Validation failed in " + failingAddress,
|
|
// ack was dropped and on retry the participate detected dup so rejected as the task failed
|
|
"Got VALIDATION_REQ failure from " + failingAddress + ": UNKNOWN");
|
|
}
|
|
break;
|
|
case SYNC:
|
|
AbstractStringAssert<?> a = Assertions.assertThat(repair.state.getResult().message).describedAs("Unexpected state: %s -> %s; example %d", repair.state, repair.state.getResult(), example);
|
|
// SymmetricRemoteSyncTask + AsymmetricRemoteSyncTask
|
|
// ... Sync failed between /[81fc:714:2c56:a2d3:faf3:eb7c:e4dd:cb9e]:54401 and /220.3.10.72:21402
|
|
// LocalSyncTask
|
|
// ... failed with error Sync failed
|
|
// Dedup nack, but may be remote or local sync!
|
|
// ... Got SYNC_REQ failure from ...: UNKNOWN
|
|
String failingMsg = repair.state.getResult().message;
|
|
if (failingMsg.contains("Sync failed between"))
|
|
{
|
|
a.contains("Sync failed between").contains(failingAddress.toString());
|
|
}
|
|
else if (failingMsg.contains("Got SYNC_REQ failure from"))
|
|
{
|
|
Assertions.assertThat(syncFailedAddresses).isNotEmpty();
|
|
a.containsAnyOf(syncFailedAddresses.stream().map(s -> "Got SYNC_REQ failure from " + s + ": UNKNOWN").collect(Collectors.toList()).toArray(String[]::new));
|
|
}
|
|
else
|
|
{
|
|
a.contains("failed with error Sync failed");
|
|
}
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException("Unknown stage: " + stage);
|
|
}
|
|
assertParticipateResult(cluster, repair, Completable.Result.Kind.FAILURE);
|
|
closeables.forEach(Closeable::close);
|
|
closeables.clear();
|
|
}
|
|
});
|
|
}
|
|
}
|