CCR: Following primary should process NoOps once (#34408)

This is a follow-up for #34288.

Relates #34412
This commit is contained in:
Nhat Nguyen 2018-10-19 21:10:13 -04:00 committed by GitHub
parent ba87c543c0
commit d90b6730c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 87 additions and 37 deletions

View File

@ -60,6 +60,7 @@ import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver;
import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndSeqNo; import org.elasticsearch.common.lucene.uid.VersionsAndSeqNoResolver.DocIdAndSeqNo;
import org.elasticsearch.common.metrics.CounterMetric; import org.elasticsearch.common.metrics.CounterMetric;
import org.elasticsearch.common.util.concurrent.AbstractRunnable; import org.elasticsearch.common.util.concurrent.AbstractRunnable;
import org.elasticsearch.common.util.concurrent.KeyedLock;
import org.elasticsearch.common.util.concurrent.ReleasableLock; import org.elasticsearch.common.util.concurrent.ReleasableLock;
import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.core.internal.io.IOUtils;
import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexSettings;
@ -155,6 +156,7 @@ public class InternalEngine extends Engine {
private final LastRefreshedCheckpointListener lastRefreshedCheckpointListener; private final LastRefreshedCheckpointListener lastRefreshedCheckpointListener;
private final AtomicBoolean trackTranslogLocation = new AtomicBoolean(false); private final AtomicBoolean trackTranslogLocation = new AtomicBoolean(false);
private final KeyedLock<Long> noOpKeyedLock = new KeyedLock<>();
@Nullable @Nullable
private final String historyUUID; private final String historyUUID;
@ -1407,32 +1409,42 @@ public class InternalEngine extends Engine {
assert readLock.isHeldByCurrentThread() || writeLock.isHeldByCurrentThread(); assert readLock.isHeldByCurrentThread() || writeLock.isHeldByCurrentThread();
assert noOp.seqNo() > SequenceNumbers.NO_OPS_PERFORMED; assert noOp.seqNo() > SequenceNumbers.NO_OPS_PERFORMED;
final long seqNo = noOp.seqNo(); final long seqNo = noOp.seqNo();
try { try (Releasable ignored = noOpKeyedLock.acquire(seqNo)) {
Exception failure = null; final NoOpResult noOpResult;
if (softDeleteEnabled) { final Optional<Exception> preFlightError = preFlightCheckForNoOp(noOp);
try { if (preFlightError.isPresent()) {
final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newNoopTombstoneDoc(noOp.reason()); noOpResult = new NoOpResult(getPrimaryTerm(), noOp.seqNo(), preFlightError.get());
tombstone.updateSeqID(noOp.seqNo(), noOp.primaryTerm()); } else {
// A noop tombstone does not require a _version but it's added to have a fully dense docvalues for the version field. Exception failure = null;
// 1L is selected to optimize the compression because it might probably be the most common value in version field. if (softDeleteEnabled) {
tombstone.version().setLongValue(1L); try {
assert tombstone.docs().size() == 1 : "Tombstone should have a single doc [" + tombstone + "]"; final ParsedDocument tombstone = engineConfig.getTombstoneDocSupplier().newNoopTombstoneDoc(noOp.reason());
final ParseContext.Document doc = tombstone.docs().get(0); tombstone.updateSeqID(noOp.seqNo(), noOp.primaryTerm());
assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null // A noop tombstone does not require a _version but it's added to have a fully dense docvalues for the version field.
: "Noop tombstone document but _tombstone field is not set [" + doc + " ]"; // 1L is selected to optimize the compression because it might probably be the most common value in version field.
doc.add(softDeletesField); tombstone.version().setLongValue(1L);
indexWriter.addDocument(doc); assert tombstone.docs().size() == 1 : "Tombstone should have a single doc [" + tombstone + "]";
} catch (Exception ex) { final ParseContext.Document doc = tombstone.docs().get(0);
if (maybeFailEngine("noop", ex)) { assert doc.getField(SeqNoFieldMapper.TOMBSTONE_NAME) != null
throw ex; : "Noop tombstone document but _tombstone field is not set [" + doc + " ]";
doc.add(softDeletesField);
indexWriter.addDocument(doc);
} catch (Exception ex) {
if (maybeFailEngine("noop", ex)) {
throw ex;
}
failure = ex;
} }
failure = ex;
} }
} if (failure == null) {
final NoOpResult noOpResult = failure != null ? new NoOpResult(getPrimaryTerm(), noOp.seqNo(), failure) : new NoOpResult(getPrimaryTerm(), noOp.seqNo()); noOpResult = new NoOpResult(getPrimaryTerm(), noOp.seqNo());
if (noOp.origin().isFromTranslog() == false) { } else {
final Translog.Location location = translog.add(new Translog.NoOp(noOp.seqNo(), noOp.primaryTerm(), noOp.reason())); noOpResult = new NoOpResult(getPrimaryTerm(), noOp.seqNo(), failure);
noOpResult.setTranslogLocation(location); }
if (noOp.origin().isFromTranslog() == false && noOpResult.getResultType() == Result.Type.SUCCESS) {
final Translog.Location location = translog.add(new Translog.NoOp(noOp.seqNo(), noOp.primaryTerm(), noOp.reason()));
noOpResult.setTranslogLocation(location);
}
} }
noOpResult.setTook(System.nanoTime() - noOp.startTime()); noOpResult.setTook(System.nanoTime() - noOp.startTime());
noOpResult.freeze(); noOpResult.freeze();
@ -1444,6 +1456,14 @@ public class InternalEngine extends Engine {
} }
} }
/**
* Executes a pre-flight check for a given NoOp.
* If this method returns a non-empty result, the engine won't process this NoOp and returns a failure.
*/
protected Optional<Exception> preFlightCheckForNoOp(final NoOp noOp) throws IOException {
return Optional.empty();
}
@Override @Override
public void refresh(String source) throws EngineException { public void refresh(String source) throws EngineException {
refresh(source, SearcherScope.EXTERNAL); refresh(source, SearcherScope.EXTERNAL);
@ -2354,8 +2374,14 @@ public class InternalEngine extends Engine {
* @return true if the given operation was processed; otherwise false. * @return true if the given operation was processed; otherwise false.
*/ */
protected final boolean hasBeenProcessedBefore(Operation op) { protected final boolean hasBeenProcessedBefore(Operation op) {
assert op.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO : "operation is not assigned seq_no"; if (Assertions.ENABLED) {
assert versionMap.assertKeyedLockHeldByCurrentThread(op.uid().bytes()); assert op.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO : "operation is not assigned seq_no";
if (op.operationType() == Operation.TYPE.NO_OP) {
assert noOpKeyedLock.isHeldByCurrentThread(op.seqNo());
} else {
assert versionMap.assertKeyedLockHeldByCurrentThread(op.uid().bytes());
}
}
return localCheckpointTracker.contains(op.seqNo()); return localCheckpointTracker.contains(op.seqNo());
} }

View File

@ -2978,6 +2978,7 @@ public class InternalEngineTests extends EngineTestCase {
private void maybeThrowFailure() throws IOException { private void maybeThrowFailure() throws IOException {
if (failureToThrow.get() != null) { if (failureToThrow.get() != null) {
Exception failure = failureToThrow.get().get(); Exception failure = failureToThrow.get().get();
clearFailure(); // one shot
if (failure instanceof RuntimeException) { if (failure instanceof RuntimeException) {
throw (RuntimeException) failure; throw (RuntimeException) failure;
} else if (failure instanceof IOException) { } else if (failure instanceof IOException) {

View File

@ -26,6 +26,7 @@ import org.elasticsearch.index.seqno.SequenceNumbers;
import org.elasticsearch.xpack.ccr.CcrSettings; import org.elasticsearch.xpack.ccr.CcrSettings;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import java.util.OptionalLong; import java.util.OptionalLong;
/** /**
@ -111,9 +112,14 @@ public final class FollowingEngine extends InternalEngine {
} }
@Override @Override
public NoOpResult noOp(NoOp noOp) { protected Optional<Exception> preFlightCheckForNoOp(NoOp noOp) throws IOException {
// TODO: Make sure we process NoOp once. if (noOp.origin() == Operation.Origin.PRIMARY && hasBeenProcessedBefore(noOp)) {
return super.noOp(noOp); // See the comment in #indexingStrategyForOperation for the explanation why we can safely skip this operation.
final OptionalLong existingTerm = lookupPrimaryTerm(noOp.seqNo());
return Optional.of(new AlreadyProcessedFollowingEngineException(shardId, noOp.seqNo(), existingTerm));
} else {
return super.preFlightCheckForNoOp(noOp);
}
} }
@Override @Override

View File

@ -25,6 +25,7 @@ import org.elasticsearch.common.unit.ByteSizeUnit;
import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexSettings;
import org.elasticsearch.index.engine.Engine;
import org.elasticsearch.index.engine.EngineFactory; import org.elasticsearch.index.engine.EngineFactory;
import org.elasticsearch.index.replication.ESIndexLevelReplicationTestCase; import org.elasticsearch.index.replication.ESIndexLevelReplicationTestCase;
import org.elasticsearch.index.seqno.SeqNoStats; import org.elasticsearch.index.seqno.SeqNoStats;
@ -240,6 +241,14 @@ public class ShardFollowTaskReplicationTests extends ESIndexLevelReplicationTest
followerGroup.startAll(); followerGroup.startAll();
leaderGroup.appendDocs(between(10, 100)); leaderGroup.appendDocs(between(10, 100));
leaderGroup.refresh("test"); leaderGroup.refresh("test");
for (int numNoOps = between(1, 10), i = 0; i < numNoOps; i++) {
long seqNo = leaderGroup.getPrimary().seqNoStats().getMaxSeqNo() + 1;
Engine.NoOp noOp = new Engine.NoOp(seqNo, leaderGroup.getPrimary().getOperationPrimaryTerm(),
Engine.Operation.Origin.REPLICA, threadPool.relativeTimeInMillis(), "test-" + i);
for (IndexShard shard : leaderGroup) {
getEngine(shard).noOp(noOp);
}
}
for (String deleteId : randomSubsetOf(IndexShardTestCase.getShardDocUIDs(leaderGroup.getPrimary()))) { for (String deleteId : randomSubsetOf(IndexShardTestCase.getShardDocUIDs(leaderGroup.getPrimary()))) {
BulkItemResponse resp = leaderGroup.delete(new DeleteRequest("test", "type", deleteId)); BulkItemResponse resp = leaderGroup.delete(new DeleteRequest("test", "type", deleteId));
assertThat(resp.getFailure(), nullValue()); assertThat(resp.getFailure(), nullValue());
@ -276,11 +285,14 @@ public class ShardFollowTaskReplicationTests extends ESIndexLevelReplicationTest
SeqNoStats followerSeqNoStats = followerGroup.getPrimary().seqNoStats(); SeqNoStats followerSeqNoStats = followerGroup.getPrimary().seqNoStats();
shardFollowTask.start(followerGroup.getPrimary().getHistoryUUID(), leadingPrimary.getGlobalCheckpoint(), shardFollowTask.start(followerGroup.getPrimary().getHistoryUUID(), leadingPrimary.getGlobalCheckpoint(),
leadingPrimary.getMaxSeqNoOfUpdatesOrDeletes(), followerSeqNoStats.getGlobalCheckpoint(), followerSeqNoStats.getMaxSeqNo()); leadingPrimary.getMaxSeqNoOfUpdatesOrDeletes(), followerSeqNoStats.getGlobalCheckpoint(), followerSeqNoStats.getMaxSeqNo());
assertBusy(() -> { try {
assertThat(followerGroup.getPrimary().getGlobalCheckpoint(), equalTo(leadingPrimary.getGlobalCheckpoint())); assertBusy(() -> {
assertConsistentHistoryBetweenLeaderAndFollower(leaderGroup, followerGroup); assertThat(followerGroup.getPrimary().getGlobalCheckpoint(), equalTo(leadingPrimary.getGlobalCheckpoint()));
}); assertConsistentHistoryBetweenLeaderAndFollower(leaderGroup, followerGroup);
shardFollowTask.markAsCompleted(); });
} finally {
shardFollowTask.markAsCompleted();
}
} }
} }

View File

@ -103,8 +103,10 @@ public class BulkShardOperationsTests extends IndexShardTestCase {
final Translog.Operation op; final Translog.Operation op;
if (randomBoolean()) { if (randomBoolean()) {
op = new Translog.Index("_doc", id, seqno++, primaryTerm, 0, SOURCE, null, -1); op = new Translog.Index("_doc", id, seqno++, primaryTerm, 0, SOURCE, null, -1);
} else { } else if (randomBoolean()) {
op = new Translog.Delete("_doc", id, new Term("_id", Uid.encodeId(id)), seqno++, primaryTerm, 0); op = new Translog.Delete("_doc", id, new Term("_id", Uid.encodeId(id)), seqno++, primaryTerm, 0);
} else {
op = new Translog.NoOp(seqno++, primaryTerm, "test-" + i);
} }
if (randomBoolean()) { if (randomBoolean()) {
firstBulk.add(op); firstBulk.add(op);

View File

@ -306,7 +306,7 @@ public class FollowingEngineTests extends ESTestCase {
private Engine.Result applyOperation(Engine engine, Engine.Operation op, private Engine.Result applyOperation(Engine engine, Engine.Operation op,
long primaryTerm, Engine.Operation.Origin origin) throws IOException { long primaryTerm, Engine.Operation.Origin origin) throws IOException {
final VersionType versionType = origin == Engine.Operation.Origin.PRIMARY ? op.versionType() : null; final VersionType versionType = origin == Engine.Operation.Origin.PRIMARY ? VersionType.EXTERNAL : null;
final Engine.Result result; final Engine.Result result;
if (op instanceof Engine.Index) { if (op instanceof Engine.Index) {
Engine.Index index = (Engine.Index) op; Engine.Index index = (Engine.Index) op;
@ -572,9 +572,12 @@ public class FollowingEngineTests extends ESTestCase {
if (randomBoolean()) { if (randomBoolean()) {
operations.add(new Engine.Index(EngineTestCase.newUid(doc), doc, i, primaryTerm.get(), 1L, operations.add(new Engine.Index(EngineTestCase.newUid(doc), doc, i, primaryTerm.get(), 1L,
VersionType.EXTERNAL, Engine.Operation.Origin.PRIMARY, threadPool.relativeTimeInMillis(), -1, true)); VersionType.EXTERNAL, Engine.Operation.Origin.PRIMARY, threadPool.relativeTimeInMillis(), -1, true));
} else { } else if (randomBoolean()) {
operations.add(new Engine.Delete(doc.type(), doc.id(), EngineTestCase.newUid(doc), i, primaryTerm.get(), 1L, operations.add(new Engine.Delete(doc.type(), doc.id(), EngineTestCase.newUid(doc), i, primaryTerm.get(), 1L,
VersionType.EXTERNAL, Engine.Operation.Origin.PRIMARY, threadPool.relativeTimeInMillis())); VersionType.EXTERNAL, Engine.Operation.Origin.PRIMARY, threadPool.relativeTimeInMillis()));
} else {
operations.add(new Engine.NoOp(i, primaryTerm.get(), Engine.Operation.Origin.PRIMARY,
threadPool.relativeTimeInMillis(), "test-" + i));
} }
} }
Randomness.shuffle(operations); Randomness.shuffle(operations);