1483 lines
58 KiB
C++
1483 lines
58 KiB
C++
/*
|
|
* NativeCdcEndToEnd.cpp
|
|
*
|
|
* This source file is part of the FoundationDB open source project
|
|
*
|
|
* Copyright 2013-2026 Apple Inc. and the FoundationDB project authors
|
|
*
|
|
* Licensed 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.
|
|
*/
|
|
|
|
#include <algorithm>
|
|
#include <limits>
|
|
#include <set>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include <boost/functional/hash.hpp>
|
|
|
|
#include "fdbclient/DatabaseContext.h"
|
|
#include "fdbclient/NativeCdc.h"
|
|
#include "fdbclient/SystemData.h"
|
|
#include "fdbserver/core/RecoveryState.h"
|
|
#include "fdbserver/core/ServerDBInfo.h"
|
|
#include "fdbserver/tester/workloads.h"
|
|
#include "fdbrpc/simulator.h"
|
|
#include "flow/DeterministicRandom.h"
|
|
|
|
// Exercises native CDC by registering overlapping streams, writing mutations, consuming and acknowledging them,
|
|
// and checking delivery, retention, assignment publication, failure recovery, and drain behavior. Test options
|
|
// select focused scenarios such as proxy replacement, memory bounds, retired tags, and restart-after-disable drains.
|
|
class NativeCdcEndToEndWorkload : public TestWorkload {
|
|
struct ExpectedWrite {
|
|
Version committedVersion;
|
|
std::set<Version> observedVersions;
|
|
};
|
|
|
|
struct KeyValueHash {
|
|
size_t operator()(const std::pair<Key, Value>& item) const {
|
|
size_t hash = 0;
|
|
boost::hash_combine(hash, std::hash<Key>{}(item.first));
|
|
boost::hash_combine(hash, std::hash<Value>{}(item.second));
|
|
return hash;
|
|
}
|
|
};
|
|
|
|
struct StreamState {
|
|
Key name;
|
|
KeyRange keys;
|
|
Reference<NativeCdcConsumer> consumer;
|
|
std::unordered_map<std::pair<Key, Value>, ExpectedWrite, KeyValueHash> expected;
|
|
};
|
|
|
|
int initialStreamCount;
|
|
int minStreamCount;
|
|
int maxStreamCount;
|
|
int keyCount;
|
|
int writesPerRound;
|
|
int rounds;
|
|
int assignmentPublicationChecks;
|
|
bool testProxyReplacement;
|
|
bool injectUndeliveredProxyHalt;
|
|
bool testMemoryBound;
|
|
bool testReplyChunking;
|
|
bool testOversizedPeek;
|
|
bool testDurableAckScan;
|
|
bool testDelayedRetention;
|
|
bool testRetiredRecovery;
|
|
bool testRetiredSharedTagSnapshot;
|
|
bool prepareRestartDrain;
|
|
bool drainAfterRestart;
|
|
int memoryTestValueBytes;
|
|
double retentionValidationDelay;
|
|
double drainProbability;
|
|
double delayBetweenRounds;
|
|
double operationTimeout;
|
|
int nextStreamNumber = 0;
|
|
Version retentionMarkerVersion = invalidVersion;
|
|
std::vector<StreamState> streams;
|
|
|
|
Key keyForIndex(int index) const { return Key(StringRef(format("native-cdc-e2e/data/%04d", index))); }
|
|
|
|
KeyRange randomOverlappingRange() const {
|
|
const int middle = keyCount / 2;
|
|
const int begin = deterministicRandom()->randomInt(0, middle + 1);
|
|
const int end = deterministicRandom()->randomInt(middle + 1, keyCount + 1);
|
|
return KeyRange(KeyRangeRef(keyForIndex(begin), keyForIndex(end)));
|
|
}
|
|
|
|
Future<Version> writeValues(Database cx, std::vector<std::pair<Key, Value>> values) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
for (const auto& [key, value] : values) {
|
|
tr.set(key, value);
|
|
}
|
|
co_await tr.commit();
|
|
co_return tr.getCommittedVersion();
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Version> writeValue(Database cx, Key key, Value value) {
|
|
std::vector<std::pair<Key, Value>> values;
|
|
values.emplace_back(std::move(key), std::move(value));
|
|
co_return co_await writeValues(cx, std::move(values));
|
|
}
|
|
|
|
Future<Version> getReadVersion(Database cx) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
co_return co_await tr.getReadVersion();
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Void> acknowledgeDurablyWithoutProxy(Database cx, CDCStreamId streamId, Version consumedThrough) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
tr.setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
tr.setOption(FDBTransactionOptions::PRIORITY_SYSTEM_IMMEDIATE);
|
|
Optional<Value> current = co_await tr.get(cdcMinVersionKeyFor(streamId));
|
|
if (!current.present()) {
|
|
throw client_invalid_operation();
|
|
}
|
|
const Version minVersion = consumedThrough + 1;
|
|
if (minVersion <= decodeCDCMinVersionValue(current.get())) {
|
|
co_return;
|
|
}
|
|
tr.set(cdcMinVersionKeyFor(streamId), cdcMinVersionValue(minVersion));
|
|
co_await tr.commit();
|
|
co_return;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
void resumeConsumerAfterDirectAcknowledgement(Database cx, CDCStreamId streamId, Version consumedThrough) {
|
|
ASSERT_EQ(streams.size(), 1);
|
|
streams.front().consumer = resumeNativeCdcConsumer(cx, CDCCursor(streamId, consumedThrough));
|
|
CODE_PROBE(true, "Native CDC durable acknowledgement test updates its resumed consumer cursor");
|
|
}
|
|
|
|
Future<Void> consumeThroughValue(Reference<NativeCdcConsumer> consumer, Version committed, Key key, Value value) {
|
|
bool observed = false;
|
|
const double deadline = now() + operationTimeout;
|
|
while (consumer->position().lastConsumedVersion < committed) {
|
|
const Version previous = consumer->position().lastConsumedVersion;
|
|
CDCConsumeReply reply = co_await timeoutError(consumer->consume(), operationTimeout);
|
|
if (reply.lastConsumedVersion == previous) {
|
|
ASSERT_LT(now(), deadline);
|
|
co_await delay(0.1);
|
|
continue;
|
|
}
|
|
ASSERT_GT(reply.lastConsumedVersion, previous);
|
|
for (const auto& versioned : reply.mutations) {
|
|
ASSERT_GT(versioned.version, previous);
|
|
ASSERT_LE(versioned.version, reply.lastConsumedVersion);
|
|
for (const auto& mutation : versioned.mutations) {
|
|
if (versioned.version == committed && mutation.type == MutationRef::SetValue &&
|
|
mutation.param1 == key && mutation.param2 == value) {
|
|
observed = true;
|
|
}
|
|
}
|
|
}
|
|
co_await timeoutError(consumer->acknowledge(), operationTimeout);
|
|
}
|
|
ASSERT(observed);
|
|
}
|
|
|
|
Future<CDCProxyInterface> waitForAssignedProxy(Database cx,
|
|
CDCStreamId streamId,
|
|
Optional<UID> previousProxy = Optional<UID>()) {
|
|
while (true) {
|
|
Future<Void> changed = cx->clientInfo->onChange();
|
|
Optional<CDCProxyInterface> assignedProxy;
|
|
{
|
|
const ClientDBInfo& clientInfo = cx->clientInfo->get();
|
|
auto assignment = clientInfo.streamToCDCProxyId.find(streamId);
|
|
if (assignment != clientInfo.streamToCDCProxyId.end() &&
|
|
(!previousProxy.present() || assignment->second != previousProxy.get())) {
|
|
auto proxy = std::find_if(
|
|
clientInfo.cdcProxies.begin(),
|
|
clientInfo.cdcProxies.end(),
|
|
[&](CDCProxyInterface const& candidate) { return candidate.id() == assignment->second; });
|
|
if (proxy != clientInfo.cdcProxies.end()) {
|
|
assignedProxy = *proxy;
|
|
}
|
|
}
|
|
}
|
|
if (assignedProxy.present()) {
|
|
co_return assignedProxy.get();
|
|
}
|
|
co_await changed;
|
|
}
|
|
}
|
|
|
|
Future<Void> addStream(Database cx, KeyRange keys) {
|
|
StreamState stream;
|
|
stream.name = Key(StringRef(format("native-cdc-e2e/stream/%04d", nextStreamNumber++)));
|
|
stream.keys = std::move(keys);
|
|
co_await timeoutError(registerNativeCdcStreamClient(cx, stream.name, stream.keys), operationTimeout);
|
|
stream.consumer = co_await timeoutError(createNativeCdcConsumer(cx, stream.name), operationTimeout);
|
|
streams.push_back(std::move(stream));
|
|
}
|
|
|
|
Future<Void> addStream(Database cx) { return addStream(cx, randomOverlappingRange()); }
|
|
|
|
Future<Void> initializeStreams(Database cx) {
|
|
for (int i = 0; i < initialStreamCount; ++i) {
|
|
co_await addStream(cx);
|
|
}
|
|
if (testDelayedRetention) {
|
|
std::vector<std::pair<Key, Value>> marker;
|
|
marker.emplace_back(keyForIndex(keyCount / 2), "retained-across-region-failure"_sr);
|
|
retentionMarkerVersion = co_await writeValues(cx, marker);
|
|
recordExpectedWrites(marker, retentionMarkerVersion);
|
|
}
|
|
}
|
|
|
|
Future<Void> initializeOversizedPeekStreams(Database cx) {
|
|
ASSERT_GE(keyCount, 4);
|
|
co_await addStream(cx, KeyRange(KeyRangeRef(keyForIndex(0), keyForIndex(2))));
|
|
co_await addStream(cx, KeyRange(KeyRangeRef(keyForIndex(2), keyForIndex(4))));
|
|
}
|
|
|
|
Future<Void> initializeReplyChunkingStream(Database cx) {
|
|
ASSERT_GE(keyCount, 2);
|
|
co_await addStream(cx, KeyRange(KeyRangeRef(keyForIndex(0), keyForIndex(keyCount))));
|
|
}
|
|
|
|
Future<Void> validatePublicLifecycle(Database cx) {
|
|
const Key name = "native-cdc-e2e/lifecycle"_sr;
|
|
const KeyRange keys(KeyRangeRef("native-cdc-e2e/lifecycle/"_sr, "native-cdc-e2e/lifecycle0"_sr));
|
|
const KeyRange conflictingKeys(KeyRangeRef("native-cdc-e2e/lifecycle/"_sr, "native-cdc-e2e/lifecycle1"_sr));
|
|
|
|
const CDCStreamId streamId =
|
|
co_await timeoutError(registerNativeCdcStreamClient(cx, name, keys), operationTimeout);
|
|
ASSERT_EQ(co_await timeoutError(registerNativeCdcStreamClient(cx, name, keys), operationTimeout), streamId);
|
|
|
|
bool conflictingRegistrationRejected = false;
|
|
try {
|
|
co_await timeoutError(registerNativeCdcStreamClient(cx, name, conflictingKeys), operationTimeout);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_client_invalid_operation) {
|
|
throw;
|
|
}
|
|
conflictingRegistrationRejected = true;
|
|
}
|
|
ASSERT_EQ(conflictingRegistrationRejected, true);
|
|
|
|
const std::vector<NativeCdcStreamInfo> listed =
|
|
co_await timeoutError(listNativeCdcStreamsClient(cx), operationTimeout);
|
|
auto found = std::find_if(
|
|
listed.begin(), listed.end(), [&](NativeCdcStreamInfo const& stream) { return stream.name == name; });
|
|
ASSERT_EQ(found != listed.end(), true);
|
|
ASSERT_EQ(found->streamId, streamId);
|
|
ASSERT_EQ(found->keys, keys);
|
|
|
|
bool futureConsumeRejected = false;
|
|
try {
|
|
co_await timeoutError(
|
|
resumeNativeCdcConsumer(cx, CDCCursor(streamId, std::numeric_limits<Version>::max() - 2))->consume(),
|
|
operationTimeout);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_client_invalid_operation) {
|
|
throw;
|
|
}
|
|
futureConsumeRejected = true;
|
|
}
|
|
ASSERT_EQ(futureConsumeRejected, true);
|
|
|
|
bool unprovenConsumeRejected = false;
|
|
try {
|
|
const Version unprovenVersion = co_await getReadVersion(cx);
|
|
co_await timeoutError(resumeNativeCdcConsumer(cx, CDCCursor(streamId, unprovenVersion))->consume(),
|
|
operationTimeout);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_client_invalid_operation) {
|
|
throw;
|
|
}
|
|
unprovenConsumeRejected = true;
|
|
}
|
|
ASSERT_EQ(unprovenConsumeRejected, true);
|
|
|
|
bool futureAcknowledgeRejected = false;
|
|
try {
|
|
co_await timeoutError(
|
|
resumeNativeCdcConsumer(cx, CDCCursor(streamId, std::numeric_limits<Version>::max() - 2))
|
|
->acknowledge(),
|
|
operationTimeout);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_client_invalid_operation) {
|
|
throw;
|
|
}
|
|
futureAcknowledgeRejected = true;
|
|
}
|
|
ASSERT_EQ(futureAcknowledgeRejected, true);
|
|
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, name), operationTimeout);
|
|
|
|
bool staleAcknowledgeRejected = false;
|
|
try {
|
|
co_await timeoutError(resumeNativeCdcConsumer(cx, CDCCursor(streamId, 0))->acknowledge(), operationTimeout);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_client_invalid_operation) {
|
|
throw;
|
|
}
|
|
staleAcknowledgeRejected = true;
|
|
}
|
|
ASSERT_EQ(staleAcknowledgeRejected, true);
|
|
}
|
|
|
|
Future<Void> validateClearClipping(Database cx) {
|
|
const Key name = "native-cdc-e2e/clear-stream"_sr;
|
|
const KeyRange keys(KeyRangeRef("native-cdc-e2e/clear/c"_sr, "native-cdc-e2e/clear/m"_sr));
|
|
const KeyRange lowerClear(KeyRangeRef("native-cdc-e2e/clear/a"_sr, "native-cdc-e2e/clear/f"_sr));
|
|
const KeyRange upperClear(KeyRangeRef("native-cdc-e2e/clear/j"_sr, "native-cdc-e2e/clear/z"_sr));
|
|
const KeyRange expectedLower(KeyRangeRef(keys.begin, lowerClear.end));
|
|
const KeyRange expectedUpper(KeyRangeRef(upperClear.begin, keys.end));
|
|
|
|
co_await timeoutError(registerNativeCdcStreamClient(cx, name, keys), operationTimeout);
|
|
Reference<NativeCdcConsumer> consumer =
|
|
co_await timeoutError(createNativeCdcConsumer(cx, name), operationTimeout);
|
|
|
|
Version committed;
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.clear(lowerClear);
|
|
tr.clear(upperClear);
|
|
co_await tr.commit();
|
|
committed = tr.getCommittedVersion();
|
|
break;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
|
|
bool sawLower = false;
|
|
bool sawUpper = false;
|
|
while (consumer->position().lastConsumedVersion < committed) {
|
|
CDCConsumeReply reply = co_await timeoutError(consumer->consume(), operationTimeout);
|
|
for (const auto& versioned : reply.mutations) {
|
|
if (versioned.version != committed) {
|
|
continue;
|
|
}
|
|
for (const auto& mutation : versioned.mutations) {
|
|
ASSERT_EQ(mutation.type, MutationRef::ClearRange);
|
|
const KeyRangeRef cleared(mutation.param1, mutation.param2);
|
|
if (cleared.begin == expectedLower.begin && cleared.end == expectedLower.end) {
|
|
sawLower = true;
|
|
} else if (cleared.begin == expectedUpper.begin && cleared.end == expectedUpper.end) {
|
|
sawUpper = true;
|
|
} else {
|
|
ASSERT(false);
|
|
}
|
|
}
|
|
}
|
|
co_await timeoutError(consumer->acknowledge(), operationTimeout);
|
|
}
|
|
ASSERT(sawLower);
|
|
ASSERT(sawUpper);
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, name), operationTimeout);
|
|
}
|
|
|
|
Future<Void> validateAssignmentPublicationOnce(Database cx, int check) {
|
|
const KeyRange keys(KeyRangeRef("native-cdc-e2e/assignment/data/"_sr, "native-cdc-e2e/assignment/data0"_sr));
|
|
const Key name = Key(StringRef(format("native-cdc-e2e/assignment/%04d", check)));
|
|
const Key key = Key(StringRef(format("native-cdc-e2e/assignment/data/%04d", check)));
|
|
const Value value = Value(StringRef(format("assignment-value/%04d", check)));
|
|
|
|
co_await delay(0.1);
|
|
const CDCStreamId streamId =
|
|
co_await timeoutError(registerNativeCdcStreamClient(cx, name, keys), operationTimeout);
|
|
Reference<NativeCdcConsumer> consumer =
|
|
co_await timeoutError(createNativeCdcConsumer(cx, name), operationTimeout);
|
|
ASSERT_EQ(consumer->position().streamId, streamId);
|
|
|
|
const Version committed = co_await writeValue(cx, key, value);
|
|
co_await consumeThroughValue(consumer, committed, key, value);
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, name), operationTimeout);
|
|
}
|
|
|
|
Future<Void> validateAssignmentPublication(Database cx) {
|
|
for (int check = 0; check < assignmentPublicationChecks; ++check) {
|
|
co_await validateAssignmentPublicationOnce(cx, check);
|
|
}
|
|
}
|
|
|
|
Future<Void> waitForIndependentProxyPublications(Database cx, std::vector<CDCProxyInterface> originalProxies) {
|
|
bool sawPartialPublication = false;
|
|
while (true) {
|
|
Future<Void> changed = cx->clientInfo->onChange();
|
|
const ClientDBInfo& clientInfo = cx->clientInfo->get();
|
|
int originalEndpoints = 0;
|
|
for (const auto& proxy : originalProxies) {
|
|
if (std::find(clientInfo.cdcProxies.begin(), clientInfo.cdcProxies.end(), proxy) !=
|
|
clientInfo.cdcProxies.end()) {
|
|
++originalEndpoints;
|
|
}
|
|
}
|
|
if (originalEndpoints == 1) {
|
|
sawPartialPublication = true;
|
|
}
|
|
if (originalEndpoints == 0) {
|
|
ASSERT(sawPartialPublication);
|
|
co_return;
|
|
}
|
|
co_await changed;
|
|
}
|
|
}
|
|
|
|
Future<Void> haltProxyUntilReplaced(Database cx, CDCProxyInterface proxy, bool dropFirstAttempt) {
|
|
while (true) {
|
|
{
|
|
const auto& proxies = cx->clientInfo->get().cdcProxies;
|
|
if (std::find(proxies.begin(), proxies.end(), proxy) == proxies.end()) {
|
|
co_return;
|
|
}
|
|
}
|
|
|
|
// The focused scenario can drop one halt before delivery and immediately retry its published proxy.
|
|
const bool droppedAttempt = std::exchange(dropFirstAttempt, false);
|
|
ErrorOr<Void> halted = request_maybe_delivered();
|
|
if (!droppedAttempt) {
|
|
halted = co_await proxy.haltForTesting.tryGetReply(HaltCDCProxyRequest());
|
|
}
|
|
if (halted.present()) {
|
|
co_return;
|
|
}
|
|
CODE_PROBE(true, "Native CDC retries an undelivered proxy halt");
|
|
if (!droppedAttempt) {
|
|
co_await delay(0.1);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<Void> validateProxyReplacement(Database cx) {
|
|
const Key name = "native-cdc-e2e/proxy-replacement"_sr;
|
|
const KeyRange keys(
|
|
KeyRangeRef("native-cdc-e2e/proxy-replacement/"_sr, "native-cdc-e2e/proxy-replacement0"_sr));
|
|
const Key key = "native-cdc-e2e/proxy-replacement/value"_sr;
|
|
const Value value = "replacement-value"_sr;
|
|
|
|
const CDCStreamId streamId =
|
|
co_await timeoutError(registerNativeCdcStreamClient(cx, name, keys), operationTimeout);
|
|
Reference<NativeCdcConsumer> consumer =
|
|
co_await timeoutError(createNativeCdcConsumer(cx, name), operationTimeout);
|
|
CDCProxyInterface original = co_await timeoutError(waitForAssignedProxy(cx, streamId), operationTimeout);
|
|
const std::vector<CDCProxyInterface> originalProxies = cx->clientInfo->get().cdcProxies;
|
|
ASSERT_EQ(originalProxies.size(), 2);
|
|
ASSERT(std::find(originalProxies.begin(), originalProxies.end(), original) != originalProxies.end());
|
|
Future<Void> publications = waitForIndependentProxyPublications(cx, originalProxies);
|
|
|
|
std::vector<Future<Void>> halts;
|
|
halts.reserve(originalProxies.size());
|
|
bool dropFirstHalt = injectUndeliveredProxyHalt;
|
|
for (const auto& proxy : originalProxies) {
|
|
halts.push_back(haltProxyUntilReplaced(cx, proxy, std::exchange(dropFirstHalt, false)));
|
|
}
|
|
co_await timeoutError(waitForAll(halts), operationTimeout);
|
|
co_await timeoutError(publications, operationTimeout);
|
|
CODE_PROBE(true, "Native CDC publishes successful proxy replacements independently");
|
|
|
|
CDCProxyInterface replacement =
|
|
co_await timeoutError(waitForAssignedProxy(cx, streamId, original.id()), operationTimeout);
|
|
ASSERT_NE(original.id(), replacement.id());
|
|
|
|
const Version committed = co_await writeValue(cx, key, value);
|
|
co_await consumeThroughValue(consumer, committed, key, value);
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, name), operationTimeout);
|
|
}
|
|
|
|
Future<Void> consumeMemoryMarker(Reference<NativeCdcConsumer> consumer,
|
|
Version committed,
|
|
Key key,
|
|
Value value,
|
|
Reference<AsyncVar<int>> completed,
|
|
Future<Void> releaseAcknowledgements) {
|
|
bool observed = false;
|
|
while (consumer->position().lastConsumedVersion < committed) {
|
|
CDCConsumeReply reply = co_await timeoutError(consumer->consume(), operationTimeout);
|
|
for (const auto& versioned : reply.mutations) {
|
|
for (const auto& mutation : versioned.mutations) {
|
|
if (versioned.version == committed && mutation.type == MutationRef::SetValue &&
|
|
mutation.param1 == key && mutation.param2 == value) {
|
|
observed = true;
|
|
}
|
|
}
|
|
}
|
|
if (consumer->position().lastConsumedVersion < committed) {
|
|
co_await timeoutError(consumer->acknowledge(), operationTimeout);
|
|
}
|
|
}
|
|
ASSERT(observed);
|
|
completed->set(completed->get() + 1);
|
|
co_await releaseAcknowledgements;
|
|
co_await timeoutError(consumer->acknowledge(), operationTimeout);
|
|
}
|
|
|
|
Future<CDCProxyBufferStatus> getProxyStatus(CDCProxyInterface proxy) {
|
|
co_return co_await timeoutError(proxy.getBufferStatusForTesting.getReply(GetCDCProxyBufferStatusRequest()),
|
|
operationTimeout);
|
|
}
|
|
|
|
Future<std::pair<CDCProxyInterface, CDCProxyBufferStatus>> getAssignedProxyStatus(Database cx,
|
|
CDCStreamId streamId) {
|
|
while (true) {
|
|
Future<Void> changed = cx->clientInfo->onChange();
|
|
Optional<CDCProxyInterface> proxy;
|
|
{
|
|
const ClientDBInfo& clientInfo = cx->clientInfo->get();
|
|
auto assignment = clientInfo.streamToCDCProxyId.find(streamId);
|
|
if (assignment != clientInfo.streamToCDCProxyId.end()) {
|
|
auto found = std::find_if(
|
|
clientInfo.cdcProxies.begin(),
|
|
clientInfo.cdcProxies.end(),
|
|
[&](CDCProxyInterface const& candidate) { return candidate.id() == assignment->second; });
|
|
if (found != clientInfo.cdcProxies.end()) {
|
|
proxy = *found;
|
|
}
|
|
}
|
|
}
|
|
if (!proxy.present()) {
|
|
co_await changed;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
auto result = co_await race(getProxyStatus(proxy.get()), changed);
|
|
if (result.index() == 0) {
|
|
const ClientDBInfo& clientInfo = cx->clientInfo->get();
|
|
auto assignment = clientInfo.streamToCDCProxyId.find(streamId);
|
|
if (assignment != clientInfo.streamToCDCProxyId.end() && assignment->second == proxy.get().id()) {
|
|
co_return std::make_pair(proxy.get(), std::get<0>(result));
|
|
}
|
|
}
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_broken_promise && e.code() != error_code_connection_failed &&
|
|
e.code() != error_code_request_maybe_delivered) {
|
|
throw;
|
|
}
|
|
}
|
|
co_await delay(0);
|
|
}
|
|
}
|
|
|
|
void updateObservedProxy(CDCProxyInterface& proxy, CDCProxyInterface current) {
|
|
if (proxy.id() != current.id()) {
|
|
CODE_PROBE(true, "Native CDC memory validation follows proxy replacement");
|
|
proxy = current;
|
|
}
|
|
}
|
|
|
|
void recordDurableAckProxyReplacement() {
|
|
CODE_PROBE(true, "Native CDC durable acknowledgement validation retries after proxy replacement");
|
|
}
|
|
|
|
Future<CDCProxyBufferStatus> getCurrentProxyStatus(Database cx,
|
|
CDCStreamId streamId,
|
|
CDCProxyInterface* observedProxy) {
|
|
auto proxyStatus = co_await timeoutError(getAssignedProxyStatus(cx, streamId), operationTimeout);
|
|
updateObservedProxy(*observedProxy, proxyStatus.first);
|
|
co_return proxyStatus.second;
|
|
}
|
|
|
|
Future<Void> startBlockedConsume(Database cx,
|
|
CDCStreamId streamId,
|
|
Reference<NativeCdcConsumer> consumer,
|
|
CDCProxyInterface proxy,
|
|
Future<CDCConsumeReply>* outstanding) {
|
|
*outstanding = consumer->consume();
|
|
const double deadline = now() + operationTimeout;
|
|
while (true) {
|
|
CDCProxyBufferStatus status = co_await getCurrentProxyStatus(cx, streamId, &proxy);
|
|
if (outstanding->isReady()) {
|
|
co_await *outstanding;
|
|
co_await timeoutError(consumer->acknowledge(), operationTimeout);
|
|
*outstanding = consumer->consume();
|
|
continue;
|
|
}
|
|
if (status.activeConsumeRequests > 0 && status.readDemand > 0) {
|
|
co_return;
|
|
}
|
|
ASSERT_LT(now(), deadline);
|
|
co_await delay(0.01);
|
|
}
|
|
}
|
|
|
|
Future<Void> waitForNoActiveConsumes(Database cx, CDCStreamId streamId, CDCProxyInterface* proxy) {
|
|
const double deadline = now() + operationTimeout;
|
|
while (true) {
|
|
CDCProxyBufferStatus status = co_await getCurrentProxyStatus(cx, streamId, proxy);
|
|
if (status.activeConsumeRequests == 0 && status.readDemand == 0) {
|
|
co_return;
|
|
}
|
|
ASSERT_LT(now(), deadline);
|
|
co_await delay(0.01);
|
|
}
|
|
}
|
|
|
|
Future<Void> expectConcurrentConsumeRejected(CDCProxyInterface proxy, CDCCursor cursor) {
|
|
Optional<Error> error;
|
|
try {
|
|
co_await throwErrorOr(proxy.consume.tryGetReply(CDCConsumeRequest(cursor)));
|
|
} catch (Error& e) {
|
|
error = e;
|
|
}
|
|
ASSERT(error.present());
|
|
ASSERT_EQ(error.get().code(), error_code_client_invalid_operation);
|
|
}
|
|
|
|
Future<Void> validateConsumeLeaseAndExclusivity(Database cx, CDCStreamId streamId, CDCProxyInterface* proxy) {
|
|
ASSERT(!streams.empty());
|
|
// Acknowledgements advance one durable frontier for the whole stream. Exercise cancellation and exclusivity on
|
|
// the tracked consumer so later workload phases do not retain a cursor behind acknowledgements made here.
|
|
Reference<NativeCdcConsumer> idleConsumer = streams.front().consumer;
|
|
const Version idleStartVersion = idleConsumer->position().lastConsumedVersion;
|
|
Future<CDCConsumeReply> idleConsume;
|
|
co_await startBlockedConsume(cx, streamId, idleConsumer, *proxy, &idleConsume);
|
|
|
|
// Assignment publications for unrelated streams used to abandon the client reply without canceling the
|
|
// corresponding server actor. The active request and read demand must remain bounded at one.
|
|
for (int i = 0; i < 4; ++i) {
|
|
Key name = Key(StringRef(format("native-cdc-e2e/lease/%04d", i)));
|
|
Key key = keyForIndex(keyCount / 2);
|
|
co_await timeoutError(registerNativeCdcStreamClient(cx, name, KeyRangeRef(key, keyAfter(key))),
|
|
operationTimeout);
|
|
CDCProxyBufferStatus status = co_await getCurrentProxyStatus(cx, streamId, proxy);
|
|
ASSERT_LE(status.activeConsumeRequests, 1);
|
|
ASSERT_LE(status.readDemand, 1);
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, name), operationTimeout);
|
|
status = co_await getCurrentProxyStatus(cx, streamId, proxy);
|
|
ASSERT_LE(status.activeConsumeRequests, 1);
|
|
ASSERT_LE(status.readDemand, 1);
|
|
}
|
|
|
|
// The long poll can complete while unrelated assignment publications are in flight. Preserve any resulting
|
|
// cursor progress before direct proxy requests so a replacement owner can validate it.
|
|
if (idleConsume.isReady()) {
|
|
co_await idleConsume;
|
|
if (idleConsumer->position().lastConsumedVersion > idleStartVersion) {
|
|
co_await timeoutError(idleConsumer->acknowledge(), operationTimeout);
|
|
}
|
|
} else {
|
|
idleConsume.cancel();
|
|
}
|
|
co_await waitForNoActiveConsumes(cx, streamId, proxy);
|
|
|
|
CDCCursor currentCursor = idleConsumer->position();
|
|
// Send both requests without yielding. The first request marks the stream active before its metadata read, so
|
|
// the second request deterministically exercises server-side exclusivity even while versions advance.
|
|
co_await getCurrentProxyStatus(cx, streamId, proxy);
|
|
Future<ErrorOr<CDCConsumeReply>> first = proxy->consume.tryGetReply(CDCConsumeRequest(currentCursor));
|
|
co_await expectConcurrentConsumeRejected(*proxy, currentCursor);
|
|
first.cancel();
|
|
co_await waitForNoActiveConsumes(cx, streamId, proxy);
|
|
}
|
|
|
|
Future<Void> requestPopsUntilStopped(Database cx, Reference<AsyncVar<bool>> stopped) {
|
|
while (!stopped->get()) {
|
|
co_await setAllProxyPopsPaused(cx, false);
|
|
co_await delay(0);
|
|
}
|
|
}
|
|
|
|
Future<Void> validatePopProgressUnderContinuousRequests(Database cx,
|
|
CDCStreamId streamId,
|
|
CDCProxyInterface* proxy) {
|
|
auto initialProxyStatus = co_await timeoutError(getAssignedProxyStatus(cx, streamId), operationTimeout);
|
|
bool followedProxyReplacement = proxy->id() != initialProxyStatus.first.id();
|
|
updateObservedProxy(*proxy, initialProxyStatus.first);
|
|
const CDCProxyBufferStatus initial = initialProxyStatus.second;
|
|
auto stopped = makeReference<AsyncVar<bool>>(false);
|
|
Future<Void> requester = requestPopsUntilStopped(cx, stopped);
|
|
const double deadline = now() + operationTimeout;
|
|
while (true) {
|
|
const UID previousProxy = proxy->id();
|
|
const CDCProxyBufferStatus status = co_await getCurrentProxyStatus(cx, streamId, proxy);
|
|
followedProxyReplacement |= previousProxy != proxy->id();
|
|
if (status.popCompletions > initial.popCompletions) {
|
|
ASSERT_GT(status.popRequests, initial.popRequests);
|
|
break;
|
|
}
|
|
ASSERT_LT(now(), deadline);
|
|
co_await delay(0.01);
|
|
}
|
|
CODE_PROBE(followedProxyReplacement,
|
|
"Native CDC pop progress validation follows proxy replacement",
|
|
probe::decoration::rare);
|
|
stopped->set(true);
|
|
co_await timeoutError(requester, operationTimeout);
|
|
}
|
|
|
|
Future<Void> validateProxyMemoryBound(Database cx) {
|
|
ASSERT(!streams.empty());
|
|
const Key key = keyForIndex(keyCount / 2);
|
|
const Value value(std::string(memoryTestValueBytes, 'x'));
|
|
const Version committed = co_await writeValue(cx, key, value);
|
|
|
|
const CDCStreamId firstStreamId = streams.front().consumer->position().streamId;
|
|
CDCProxyInterface proxy = co_await timeoutError(waitForAssignedProxy(cx, firstStreamId), operationTimeout);
|
|
for (const auto& stream : streams) {
|
|
const CDCProxyInterface assigned =
|
|
co_await timeoutError(waitForAssignedProxy(cx, stream.consumer->position().streamId), operationTimeout);
|
|
ASSERT_EQ(assigned.id(), proxy.id());
|
|
}
|
|
|
|
Promise<Void> releaseAcknowledgements;
|
|
auto completed = makeReference<AsyncVar<int>>(0);
|
|
std::vector<Future<Void>> consumers;
|
|
consumers.reserve(streams.size());
|
|
for (const auto& stream : streams) {
|
|
consumers.push_back(consumeMemoryMarker(
|
|
stream.consumer, committed, key, value, completed, releaseAcknowledgements.getFuture()));
|
|
}
|
|
|
|
CDCProxyBufferStatus status;
|
|
const double deadline = now() + operationTimeout;
|
|
while (true) {
|
|
auto proxyStatus = co_await timeoutError(getAssignedProxyStatus(cx, firstStreamId), operationTimeout);
|
|
updateObservedProxy(proxy, proxyStatus.first);
|
|
status = proxyStatus.second;
|
|
if (completed->get() > 0 && (completed->get() == static_cast<int>(streams.size()) || status.waiters > 0)) {
|
|
break;
|
|
}
|
|
ASSERT_LT(now(), deadline);
|
|
co_await delay(0.01);
|
|
}
|
|
ASSERT_GT(status.bufferedBytes, 0);
|
|
ASSERT_LE(status.bufferedBytes, status.bufferLimit);
|
|
ASSERT_LE(status.activePermits, status.bufferLimit);
|
|
ASSERT_LE(status.peakActivePermits, status.bufferLimit);
|
|
ASSERT_GE(status.activePermits, status.bufferedBytes);
|
|
|
|
releaseAcknowledgements.send(Void());
|
|
co_await timeoutError(waitForAll(consumers), operationTimeout);
|
|
auto proxyStatus = co_await timeoutError(getAssignedProxyStatus(cx, firstStreamId), operationTimeout);
|
|
updateObservedProxy(proxy, proxyStatus.first);
|
|
status = proxyStatus.second;
|
|
ASSERT_EQ(status.bufferedBytes, 0);
|
|
ASSERT_LE(status.activePermits, status.bufferLimit);
|
|
ASSERT_LE(status.peakActivePermits, status.bufferLimit);
|
|
co_await validateConsumeLeaseAndExclusivity(cx, firstStreamId, &proxy);
|
|
co_await validatePopProgressUnderContinuousRequests(cx, firstStreamId, &proxy);
|
|
}
|
|
|
|
Future<Void> validateReplyChunking(Database cx) {
|
|
ASSERT_EQ(streams.size(), 1);
|
|
ASSERT_GE(keyCount, 2);
|
|
auto& stream = streams.front();
|
|
struct ExpectedVersion {
|
|
Version version;
|
|
std::vector<std::pair<Key, Value>> values;
|
|
};
|
|
std::vector<ExpectedVersion> expected;
|
|
for (int i = 0; i < 8; ++i) {
|
|
std::vector<std::pair<Key, Value>> values;
|
|
values.emplace_back(keyForIndex(0), Value(std::string(memoryTestValueBytes, static_cast<char>('a' + i))));
|
|
values.emplace_back(keyForIndex(1), Value(std::string(memoryTestValueBytes, static_cast<char>('A' + i))));
|
|
const Version committed = co_await writeValues(cx, values);
|
|
if (!expected.empty()) {
|
|
ASSERT_GT(committed, expected.back().version);
|
|
}
|
|
expected.push_back(ExpectedVersion{ committed, std::move(values) });
|
|
}
|
|
const Version lastVersion = expected.back().version;
|
|
|
|
// Prime one proxy-owned retained buffer without acknowledging. The first consume starts with several complete
|
|
// TLog versions available, so it also exercises raw-peek truncation before the resumed consumer exercises the
|
|
// proxy-to-client reply bound.
|
|
const double primeDeadline = now() + operationTimeout;
|
|
while (stream.consumer->position().lastConsumedVersion < lastVersion) {
|
|
const Version previous = stream.consumer->position().lastConsumedVersion;
|
|
CDCConsumeReply reply = co_await timeoutError(stream.consumer->consume(), operationTimeout);
|
|
// The priming consumer intentionally does not acknowledge. If its delivery proxy is replaced, the
|
|
// consumer rewinds to its durable cursor and replays retained versions through the replacement proxy.
|
|
if (reply.lastConsumedVersion < previous) {
|
|
ASSERT_LT(now(), primeDeadline);
|
|
continue;
|
|
}
|
|
if (reply.lastConsumedVersion == previous) {
|
|
ASSERT_LT(now(), primeDeadline);
|
|
co_await delay(0.1);
|
|
continue;
|
|
}
|
|
ASSERT_GT(reply.lastConsumedVersion, previous);
|
|
}
|
|
|
|
Reference<NativeCdcConsumer> resumed =
|
|
resumeNativeCdcConsumer(cx, CDCCursor(stream.consumer->position().streamId, invalidVersion));
|
|
std::set<Version> observedVersions;
|
|
int replyCount = 0;
|
|
bool checkedFirstReply = false;
|
|
const double resumeDeadline = now() + operationTimeout;
|
|
while (resumed->position().lastConsumedVersion < lastVersion) {
|
|
const Version previous = resumed->position().lastConsumedVersion;
|
|
CDCConsumeReply reply = co_await timeoutError(resumed->consume(), operationTimeout);
|
|
if (reply.lastConsumedVersion == previous) {
|
|
ASSERT_LT(now(), resumeDeadline);
|
|
co_await delay(0.1);
|
|
continue;
|
|
}
|
|
ASSERT_GT(reply.lastConsumedVersion, previous);
|
|
if (!checkedFirstReply) {
|
|
ASSERT_LT(reply.lastConsumedVersion, lastVersion);
|
|
checkedFirstReply = true;
|
|
}
|
|
for (const auto& versioned : reply.mutations) {
|
|
ASSERT_GT(versioned.version, previous);
|
|
ASSERT_LE(versioned.version, reply.lastConsumedVersion);
|
|
auto expectedVersion = std::find_if(expected.begin(), expected.end(), [&](const auto& item) {
|
|
return item.version == versioned.version;
|
|
});
|
|
ASSERT(expectedVersion != expected.end());
|
|
ASSERT(observedVersions.insert(versioned.version).second);
|
|
ASSERT_EQ(versioned.mutations.size(), expectedVersion->values.size());
|
|
for (const auto& [key, value] : expectedVersion->values) {
|
|
const bool found = std::any_of(
|
|
versioned.mutations.begin(), versioned.mutations.end(), [&](const MutationRef& mutation) {
|
|
return mutation.type == MutationRef::SetValue && mutation.param1 == key &&
|
|
mutation.param2 == value;
|
|
});
|
|
ASSERT(found);
|
|
}
|
|
}
|
|
for (const auto& item : expected) {
|
|
if (item.version <= reply.lastConsumedVersion) {
|
|
ASSERT(observedVersions.contains(item.version));
|
|
}
|
|
}
|
|
co_await timeoutError(resumed->acknowledge(), operationTimeout);
|
|
++replyCount;
|
|
}
|
|
ASSERT(checkedFirstReply);
|
|
ASSERT_GT(replyCount, 1);
|
|
ASSERT_EQ(observedVersions.size(), expected.size());
|
|
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, stream.name), operationTimeout);
|
|
streams.clear();
|
|
co_await timeoutError(waitForRetiredTagCleanup(cx), operationTimeout);
|
|
}
|
|
|
|
Future<Void> validateOversizedPeek(Database cx) {
|
|
ASSERT_EQ(streams.size(), 2);
|
|
// Both mutations share one CDC tag and commit version, so the raw TLog reply exceeds its cap. Each mutation
|
|
// matches only one stream and fits that stream's filtered-batch limit, keeping the rejection path unambiguous.
|
|
std::vector<std::pair<Key, Value>> values;
|
|
values.emplace_back(keyForIndex(1), Value(std::string(memoryTestValueBytes, 'x')));
|
|
values.emplace_back(keyForIndex(3), Value(std::string(memoryTestValueBytes, 'y')));
|
|
ASSERT(streams[0].keys.contains(values[0].first));
|
|
ASSERT(!streams[0].keys.contains(values[1].first));
|
|
ASSERT(!streams[1].keys.contains(values[0].first));
|
|
ASSERT(streams[1].keys.contains(values[1].first));
|
|
const Version committed = co_await writeValues(cx, values);
|
|
|
|
// Register a third stream after the oversized version. It shares the tag but starts at a later frontier and
|
|
// must not inherit the failure of consumers that still require the oversized reply.
|
|
co_await addStream(cx, KeyRange(KeyRangeRef(keyForIndex(4), keyForIndex(6))));
|
|
const Key laterKey = keyForIndex(5);
|
|
const Value laterValue = "after-oversized-reply"_sr;
|
|
const Version laterCommitted = co_await writeValue(cx, laterKey, laterValue);
|
|
ASSERT_EQ(streams.size(), 3);
|
|
|
|
int rejected = 0;
|
|
for (int i = 0; i < 2; ++i) {
|
|
auto& stream = streams[i];
|
|
const double deadline = now() + operationTimeout;
|
|
while (stream.consumer->position().lastConsumedVersion < committed) {
|
|
try {
|
|
co_await timeoutError(stream.consumer->consume(), operationTimeout);
|
|
co_await timeoutError(stream.consumer->acknowledge(), operationTimeout);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_server_overloaded) {
|
|
throw;
|
|
}
|
|
++rejected;
|
|
break;
|
|
}
|
|
ASSERT_LT(now(), deadline);
|
|
}
|
|
}
|
|
ASSERT_EQ(rejected, 2);
|
|
co_await consumeThroughValue(streams.back().consumer, laterCommitted, laterKey, laterValue);
|
|
CODE_PROBE(true, "Native CDC raw peek failure is scoped to the blocked stream frontier");
|
|
CODE_PROBE(true, "Native CDC rejects a TLog response larger than its raw peek reservation");
|
|
for (const auto& stream : streams) {
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, stream.name), operationTimeout);
|
|
}
|
|
streams.clear();
|
|
co_await timeoutError(waitForRetiredTagCleanup(cx), operationTimeout);
|
|
}
|
|
|
|
Future<Void> validateDurableAcknowledgementScan(Database cx) {
|
|
ASSERT_EQ(streams.size(), 1);
|
|
const Key key = keyForIndex(keyCount / 2);
|
|
const CDCStreamId streamId = streams.front().consumer->position().streamId;
|
|
while (true) {
|
|
CDCProxyInterface reconcileSetupProxy =
|
|
co_await timeoutError(waitForAssignedProxy(cx, streamId), operationTimeout);
|
|
const Value reconcileValue = "durable-ack-consume-reconcile"_sr;
|
|
const Version reconcileCommitted = co_await writeValue(cx, key, reconcileValue);
|
|
while (streams.front().consumer->position().lastConsumedVersion < reconcileCommitted) {
|
|
co_await timeoutError(streams.front().consumer->consume(), operationTimeout);
|
|
if (streams.front().consumer->position().lastConsumedVersion < reconcileCommitted) {
|
|
co_await timeoutError(streams.front().consumer->acknowledge(), operationTimeout);
|
|
}
|
|
}
|
|
|
|
co_await delay(1.0);
|
|
auto initialProxyStatus = co_await timeoutError(getAssignedProxyStatus(cx, streamId), operationTimeout);
|
|
if (initialProxyStatus.first.id() != reconcileSetupProxy.id()) {
|
|
recordDurableAckProxyReplacement();
|
|
continue;
|
|
}
|
|
const CDCProxyBufferStatus initial = initialProxyStatus.second;
|
|
ASSERT_GT(initial.bufferedBytes, 0);
|
|
|
|
// A consume can observe the durable frontier before the explicit acknowledgement RPC arrives. Hold TLog
|
|
// pops so this path must release acknowledged batches and permits itself instead of relying on the periodic
|
|
// pop scan.
|
|
co_await setAllProxyPopsPaused(cx, true);
|
|
co_await timeoutError(acknowledgeDurablyWithoutProxy(cx, streamId, reconcileCommitted), operationTimeout);
|
|
resumeConsumerAfterDirectAcknowledgement(cx, streamId, reconcileCommitted);
|
|
CDCProxyInterface observedProxy = initialProxyStatus.first;
|
|
Future<ErrorOr<CDCConsumeReply>> reconcileRequest =
|
|
observedProxy.consume.tryGetReply(CDCConsumeRequest(streams.front().consumer->position()));
|
|
const double reconcileDeadline = now() + operationTimeout;
|
|
bool proxyReplaced = false;
|
|
while (true) {
|
|
auto currentProxyStatus = co_await timeoutError(getAssignedProxyStatus(cx, streamId), operationTimeout);
|
|
if (currentProxyStatus.first.id() != observedProxy.id()) {
|
|
proxyReplaced = true;
|
|
break;
|
|
}
|
|
if (currentProxyStatus.second.bufferedBytes < initial.bufferedBytes) {
|
|
break;
|
|
}
|
|
ASSERT_LT(now(), reconcileDeadline);
|
|
co_await delay(0.01);
|
|
}
|
|
if (proxyReplaced) {
|
|
co_await setAllProxyPopsPaused(cx, false);
|
|
recordDurableAckProxyReplacement();
|
|
continue;
|
|
}
|
|
|
|
const Value scanValue = "durable-ack-periodic-scan"_sr;
|
|
const Version scanCommitted = co_await writeValue(cx, key, scanValue);
|
|
bool reconcileRequestFailed = false;
|
|
try {
|
|
co_await timeoutError(throwErrorOr(reconcileRequest), operationTimeout);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_wrong_shard_server && e.code() != error_code_broken_promise &&
|
|
e.code() != error_code_connection_failed && e.code() != error_code_request_maybe_delivered) {
|
|
throw;
|
|
}
|
|
reconcileRequestFailed = true;
|
|
}
|
|
co_await setAllProxyPopsPaused(cx, false);
|
|
if (reconcileRequestFailed) {
|
|
recordDurableAckProxyReplacement();
|
|
continue;
|
|
}
|
|
|
|
CDCProxyInterface scanConsumeProxy =
|
|
co_await timeoutError(waitForAssignedProxy(cx, streamId), operationTimeout);
|
|
while (streams.front().consumer->position().lastConsumedVersion < scanCommitted) {
|
|
co_await timeoutError(streams.front().consumer->consume(), operationTimeout);
|
|
}
|
|
co_await delay(1.0);
|
|
initialProxyStatus = co_await timeoutError(getAssignedProxyStatus(cx, streamId), operationTimeout);
|
|
if (initialProxyStatus.first.id() != scanConsumeProxy.id()) {
|
|
recordDurableAckProxyReplacement();
|
|
continue;
|
|
}
|
|
const CDCProxyBufferStatus scanInitial = initialProxyStatus.second;
|
|
ASSERT_GT(scanInitial.bufferedBytes, 0);
|
|
co_await timeoutError(acknowledgeDurablyWithoutProxy(cx, streamId, scanCommitted), operationTimeout);
|
|
resumeConsumerAfterDirectAcknowledgement(cx, streamId, scanCommitted);
|
|
|
|
const double deadline = now() + operationTimeout;
|
|
bool unrelatedPopRequest = false;
|
|
while (true) {
|
|
auto currentProxyStatus = co_await timeoutError(getAssignedProxyStatus(cx, streamId), operationTimeout);
|
|
if (currentProxyStatus.first.id() != initialProxyStatus.first.id()) {
|
|
proxyReplaced = true;
|
|
break;
|
|
}
|
|
const CDCProxyBufferStatus& status = currentProxyStatus.second;
|
|
if (status.popRequests != scanInitial.popRequests) {
|
|
unrelatedPopRequest = true;
|
|
CODE_PROBE(true,
|
|
"Native CDC durable acknowledgement scan retries after an unrelated proxy pop wake");
|
|
break;
|
|
}
|
|
if (status.bufferedBytes < scanInitial.bufferedBytes &&
|
|
status.popCompletions > scanInitial.popCompletions) {
|
|
break;
|
|
}
|
|
ASSERT_LT(now(), deadline);
|
|
co_await delay(0.01);
|
|
}
|
|
if (proxyReplaced || unrelatedPopRequest) {
|
|
if (proxyReplaced) {
|
|
recordDurableAckProxyReplacement();
|
|
}
|
|
continue;
|
|
}
|
|
CODE_PROBE(true, "Native CDC durable acknowledgement progresses without a proxy notification");
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, streams.front().name), operationTimeout);
|
|
streams.clear();
|
|
co_await timeoutError(waitForRetiredTagCleanup(cx), operationTimeout);
|
|
co_return;
|
|
}
|
|
}
|
|
|
|
Future<Void> waitForRetiredTagState(Database cx, bool present) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::READ_LOCK_AWARE);
|
|
tr.setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
|
RangeResult markers = co_await tr.getRange(cdcRetiredTagPopKeys, 1);
|
|
RangeResult versions = co_await tr.getRange(cdcRetiredTagPopVersionKeys, 1);
|
|
if ((present && !markers.empty() && !versions.empty()) ||
|
|
(!present && markers.empty() && versions.empty())) {
|
|
co_return;
|
|
}
|
|
tr.reset();
|
|
co_await delay(0.1);
|
|
continue;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Void> waitForRetiredTagCleanup(Database cx) { return waitForRetiredTagState(cx, false); }
|
|
|
|
Future<Void> setAllProxyPopsPaused(Database cx, bool paused, bool afterSnapshot = false) {
|
|
while (true) {
|
|
Future<Void> changed = cx->clientInfo->onChange();
|
|
const std::vector<CDCProxyInterface> proxies = cx->clientInfo->get().cdcProxies;
|
|
if (proxies.empty()) {
|
|
co_await changed;
|
|
continue;
|
|
}
|
|
|
|
std::vector<Future<Void>> requests;
|
|
requests.reserve(proxies.size());
|
|
for (const auto& proxy : proxies) {
|
|
requests.push_back(
|
|
proxy.setPopsPausedForTesting.getReply(SetCDCProxyPopsPausedRequest(paused, afterSnapshot)));
|
|
}
|
|
auto result = co_await race(waitForAll(requests), changed);
|
|
if (result.index() == 0 && proxies == cx->clientInfo->get().cdcProxies) {
|
|
co_return;
|
|
}
|
|
CODE_PROBE(true, "Native CDC workload retries pop control after proxy replacement");
|
|
}
|
|
}
|
|
|
|
Future<CDCProxyBufferStatus> getSingleProxyStatus(Database cx) {
|
|
while (true) {
|
|
const std::vector<CDCProxyInterface> proxies = cx->clientInfo->get().cdcProxies;
|
|
if (proxies.empty()) {
|
|
co_await cx->clientInfo->onChange();
|
|
continue;
|
|
}
|
|
ASSERT_EQ(proxies.size(), 1);
|
|
co_return co_await timeoutError(
|
|
proxies.front().getBufferStatusForTesting.getReply(GetCDCProxyBufferStatusRequest()), operationTimeout);
|
|
}
|
|
}
|
|
|
|
Future<Void> waitForPopSnapshotPause(Database cx, int64_t previousPauses) {
|
|
const double deadline = now() + operationTimeout;
|
|
while (true) {
|
|
const CDCProxyBufferStatus status = co_await getSingleProxyStatus(cx);
|
|
ASSERT(status.popsPausedAfterSnapshot);
|
|
if (status.popSnapshotsPaused > previousPauses) {
|
|
co_return;
|
|
}
|
|
ASSERT_LT(now(), deadline);
|
|
co_await delay(0.01);
|
|
}
|
|
}
|
|
|
|
Future<Void> validateRetiredSharedTagSnapshot(Database cx) {
|
|
ASSERT(streams.empty());
|
|
ASSERT_EQ(cx->clientInfo->get().nativeCdcTagCount, 1);
|
|
const CDCProxyBufferStatus initialStatus = co_await getSingleProxyStatus(cx);
|
|
co_await setAllProxyPopsPaused(cx, true, true);
|
|
co_await waitForPopSnapshotPause(cx, initialStatus.popSnapshotsPaused);
|
|
|
|
const Key key = keyForIndex(keyCount / 2);
|
|
const KeyRange keys(KeyRangeRef(key, keyAfter(key)));
|
|
co_await addStream(cx, keys);
|
|
const Value value = "retired-shared-tag-snapshot"_sr;
|
|
const Version committed = co_await writeValue(cx, key, value);
|
|
|
|
// This second stream shares the only configured tag. Removing it advances the retired watermark after the
|
|
// paused snapshot, while the first stream still needs the mutation above.
|
|
co_await addStream(cx, keys);
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, streams.back().name), operationTimeout);
|
|
streams.pop_back();
|
|
|
|
co_await setAllProxyPopsPaused(cx, false);
|
|
co_await consumeThroughValue(streams.front().consumer, committed, key, value);
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, streams.front().name), operationTimeout);
|
|
streams.clear();
|
|
co_await timeoutError(waitForRetiredTagCleanup(cx), operationTimeout);
|
|
CODE_PROBE(true, "Native CDC retired pop snapshot preserves a newly shared live stream");
|
|
}
|
|
|
|
Future<Void> waitForTransactionSystemRecoveryAfter(uint64_t recoveryCount) {
|
|
while (dbInfo->get().recoveryCount <= recoveryCount ||
|
|
dbInfo->get().recoveryState < RecoveryState::ACCEPTING_COMMITS) {
|
|
co_await dbInfo->onChange();
|
|
}
|
|
}
|
|
|
|
Future<Void> waitForTransactionSystemAvailable() {
|
|
while (dbInfo->get().recoveryState < RecoveryState::ACCEPTING_COMMITS) {
|
|
co_await dbInfo->onChange();
|
|
}
|
|
}
|
|
|
|
Future<Void> waitForFullyRecovered() {
|
|
while (dbInfo->get().recoveryState != RecoveryState::FULLY_RECOVERED) {
|
|
co_await dbInfo->onChange();
|
|
}
|
|
}
|
|
|
|
Future<Void> forceTransactionSystemRecovery() {
|
|
ASSERT(g_network->isSimulated());
|
|
const uint64_t recoveryCount = dbInfo->get().recoveryCount;
|
|
while (true) {
|
|
const auto masterMachine = dbInfo->get().master.locality.machineId();
|
|
if (g_simulator->killMachine(masterMachine, ISimulator::KillType::Reboot, true)) {
|
|
break;
|
|
}
|
|
co_await (dbInfo->onChange() || delay(1.0));
|
|
}
|
|
co_await timeoutError(waitForTransactionSystemRecoveryAfter(recoveryCount), operationTimeout);
|
|
}
|
|
|
|
Future<Void> validateRetiredCleanupAcrossRecovery(Database cx) {
|
|
ASSERT_EQ(streams.size(), 1);
|
|
co_await timeoutError(waitForTransactionSystemAvailable(), operationTimeout);
|
|
co_await setAllProxyPopsPaused(cx, true);
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, streams.back().name), operationTimeout);
|
|
streams.clear();
|
|
co_await timeoutError(waitForRetiredTagState(cx, true), operationTimeout);
|
|
TraceEvent("NativeCdcRetiredMarkerCreated").log();
|
|
|
|
co_await forceTransactionSystemRecovery();
|
|
TraceEvent("NativeCdcRetiredRecoveryComplete").log();
|
|
co_await setAllProxyPopsPaused(cx, false);
|
|
co_await timeoutError(waitForRetiredTagCleanup(cx), operationTimeout);
|
|
TraceEvent("NativeCdcRetiredCleanupComplete").log();
|
|
co_await timeoutError(waitForFullyRecovered(), operationTimeout);
|
|
CODE_PROBE(true, "Native CDC retired tag cleanup allows recovery to complete");
|
|
}
|
|
|
|
Future<Void> prepareRestartDrainState(Database cx) {
|
|
ASSERT_EQ(streams.size(), 1);
|
|
co_await writeValue(cx, keyForIndex(keyCount / 2), "native-cdc-restart-drain"_sr);
|
|
CODE_PROBE(true, "Native CDC restart marker is durable before save and kill");
|
|
}
|
|
|
|
Future<Void> prepareRestartDrainSetup(Database cx) {
|
|
co_await initializeStreams(cx);
|
|
co_await prepareRestartDrainState(cx);
|
|
}
|
|
|
|
Future<Void> drainRestartState(Database cx) {
|
|
while (cx->clientInfo->get().nativeCdcEnabled) {
|
|
co_await cx->clientInfo->onChange();
|
|
}
|
|
const Key name = "native-cdc-e2e/stream/0000"_sr;
|
|
Reference<NativeCdcConsumer> consumer =
|
|
co_await timeoutError(createNativeCdcConsumer(cx, name), operationTimeout);
|
|
bool observed = false;
|
|
while (!observed) {
|
|
CDCConsumeReply reply = co_await timeoutError(consumer->consume(), operationTimeout);
|
|
for (const auto& versioned : reply.mutations) {
|
|
for (const auto& mutation : versioned.mutations) {
|
|
if (mutation.type == MutationRef::SetValue && mutation.param1 == keyForIndex(keyCount / 2) &&
|
|
mutation.param2 == "native-cdc-restart-drain"_sr) {
|
|
observed = true;
|
|
}
|
|
}
|
|
}
|
|
co_await timeoutError(consumer->acknowledge(), operationTimeout);
|
|
}
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, name), operationTimeout);
|
|
co_await timeoutError(waitForRetiredTagCleanup(cx), operationTimeout);
|
|
const std::vector<NativeCdcStreamInfo> remainingStreams =
|
|
co_await timeoutError(listNativeCdcStreamsClient(cx), operationTimeout);
|
|
ASSERT(remainingStreams.empty());
|
|
|
|
Optional<Error> registrationError;
|
|
try {
|
|
co_await timeoutError(
|
|
registerNativeCdcStreamClient(cx, "native-cdc-e2e/disabled-registration"_sr, normalKeys),
|
|
operationTimeout);
|
|
} catch (Error& e) {
|
|
registrationError = e;
|
|
}
|
|
ASSERT(registrationError.present());
|
|
ASSERT_EQ(registrationError.get().code(), error_code_client_invalid_operation);
|
|
}
|
|
|
|
void recordExpectedWrites(std::vector<std::pair<Key, Value>> const& values, Version committedVersion) {
|
|
for (auto& stream : streams) {
|
|
for (const auto& [key, value] : values) {
|
|
if (stream.keys.contains(key)) {
|
|
const auto inserted =
|
|
stream.expected.emplace(std::make_pair(key, value), ExpectedWrite{ committedVersion, {} });
|
|
ASSERT(inserted.second);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<Void> drainThrough(StreamState* stream, Version throughVersion) {
|
|
const double deadline = now() + operationTimeout;
|
|
while (stream->consumer->position().lastConsumedVersion < throughVersion) {
|
|
const Version previous = stream->consumer->position().lastConsumedVersion;
|
|
CDCConsumeReply reply = co_await timeoutError(stream->consumer->consume(), operationTimeout);
|
|
if (reply.lastConsumedVersion == previous) {
|
|
ASSERT_LT(now(), deadline);
|
|
co_await delay(0.1);
|
|
continue;
|
|
}
|
|
ASSERT_GT(reply.lastConsumedVersion, previous);
|
|
for (const auto& versioned : reply.mutations) {
|
|
ASSERT_GT(versioned.version, previous);
|
|
ASSERT_LE(versioned.version, reply.lastConsumedVersion);
|
|
for (const auto& mutation : versioned.mutations) {
|
|
ASSERT_EQ(mutation.type, MutationRef::SetValue);
|
|
ASSERT(stream->keys.contains(mutation.param1));
|
|
const auto found =
|
|
stream->expected.find(std::make_pair(Key(mutation.param1), Value(mutation.param2)));
|
|
ASSERT(found != stream->expected.end());
|
|
ASSERT_LE(versioned.version, found->second.committedVersion);
|
|
CODE_PROBE(versioned.version < found->second.committedVersion,
|
|
"Native CDC validation accepts a committed retry before the returned commit version");
|
|
ASSERT(found->second.observedVersions.insert(versioned.version).second);
|
|
}
|
|
}
|
|
co_await timeoutError(stream->consumer->acknowledge(), operationTimeout);
|
|
}
|
|
for (const auto& expected : stream->expected) {
|
|
if (expected.second.committedVersion <= throughVersion) {
|
|
ASSERT(expected.second.observedVersions.contains(expected.second.committedVersion));
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<Void> consumeUntilRemoved(Reference<NativeCdcConsumer> consumer) {
|
|
while (true) {
|
|
try {
|
|
co_await timeoutError(consumer->consume(), operationTimeout);
|
|
co_await timeoutError(consumer->acknowledge(), operationTimeout);
|
|
} catch (Error& e) {
|
|
if (e.code() != error_code_client_invalid_operation) {
|
|
throw;
|
|
}
|
|
co_return;
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<Void> removeStream(Database cx, int index, Version throughVersion) {
|
|
ASSERT_GT(index, 0);
|
|
co_await drainThrough(&streams[index], throughVersion);
|
|
Reference<NativeCdcConsumer> pendingConsumer = resumeNativeCdcConsumer(cx, streams[index].consumer->position());
|
|
Future<Void> pendingConsume = consumeUntilRemoved(pendingConsumer);
|
|
co_await delay(0.1);
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, streams[index].name), operationTimeout);
|
|
co_await timeoutError(pendingConsume, operationTimeout);
|
|
streams.erase(streams.begin() + index);
|
|
}
|
|
|
|
Future<Void> run(Database cx) {
|
|
if (testRetiredSharedTagSnapshot) {
|
|
co_await validateRetiredSharedTagSnapshot(cx);
|
|
co_return;
|
|
}
|
|
if (testOversizedPeek) {
|
|
co_await validateOversizedPeek(cx);
|
|
co_return;
|
|
}
|
|
if (testReplyChunking) {
|
|
co_await validateReplyChunking(cx);
|
|
co_return;
|
|
}
|
|
if (testDurableAckScan) {
|
|
co_await validateDurableAcknowledgementScan(cx);
|
|
co_return;
|
|
}
|
|
if (testDelayedRetention) {
|
|
ASSERT_NE(retentionMarkerVersion, invalidVersion);
|
|
co_await delay(retentionValidationDelay);
|
|
for (auto& stream : streams) {
|
|
co_await drainThrough(&stream, retentionMarkerVersion);
|
|
}
|
|
}
|
|
co_await validatePublicLifecycle(cx);
|
|
co_await validateClearClipping(cx);
|
|
co_await validateAssignmentPublication(cx);
|
|
if (testProxyReplacement) {
|
|
co_await validateProxyReplacement(cx);
|
|
}
|
|
if (testMemoryBound) {
|
|
co_await validateProxyMemoryBound(cx);
|
|
}
|
|
Version mostRecentWrite = invalidVersion;
|
|
for (int round = 0; round < rounds; ++round) {
|
|
if (round > 0 && static_cast<int>(streams.size()) > minStreamCount &&
|
|
(round % 3 == 0 || deterministicRandom()->random01() < 0.35)) {
|
|
const int removalIndex = deterministicRandom()->randomInt(1, static_cast<int>(streams.size()));
|
|
co_await removeStream(cx, removalIndex, mostRecentWrite);
|
|
}
|
|
if (static_cast<int>(streams.size()) < maxStreamCount &&
|
|
(round % 2 == 0 || deterministicRandom()->random01() < 0.35)) {
|
|
co_await addStream(cx);
|
|
}
|
|
|
|
std::set<int> chosenKeys{ keyCount / 2 };
|
|
while (static_cast<int>(chosenKeys.size()) < writesPerRound) {
|
|
chosenKeys.insert(deterministicRandom()->randomInt(0, keyCount));
|
|
}
|
|
std::vector<std::pair<Key, Value>> values;
|
|
for (int index : chosenKeys) {
|
|
values.emplace_back(keyForIndex(index), Value(StringRef(format("round/%04d/key/%04d", round, index))));
|
|
}
|
|
mostRecentWrite = co_await writeValues(cx, values);
|
|
recordExpectedWrites(values, mostRecentWrite);
|
|
|
|
// streams[0] intentionally stays behind while other streams are removed.
|
|
for (int i = 1; i < static_cast<int>(streams.size()); ++i) {
|
|
if (deterministicRandom()->random01() < drainProbability) {
|
|
co_await drainThrough(&streams[i], mostRecentWrite);
|
|
}
|
|
}
|
|
co_await delay(delayBetweenRounds);
|
|
}
|
|
|
|
for (auto& stream : streams) {
|
|
co_await drainThrough(&stream, mostRecentWrite);
|
|
}
|
|
while (streams.size() > (testRetiredRecovery ? 1 : 0)) {
|
|
co_await timeoutError(removeNativeCdcStreamClient(cx, streams.back().name), operationTimeout);
|
|
streams.pop_back();
|
|
}
|
|
if (testRetiredRecovery) {
|
|
co_await validateRetiredCleanupAcrossRecovery(cx);
|
|
} else {
|
|
co_await timeoutError(waitForRetiredTagCleanup(cx), operationTimeout);
|
|
}
|
|
}
|
|
|
|
public:
|
|
static constexpr auto NAME = "NativeCdcEndToEnd";
|
|
|
|
explicit NativeCdcEndToEndWorkload(WorkloadContext const& wcx) : TestWorkload(wcx) {
|
|
initialStreamCount = getOption(options, "initialStreamCount"_sr, 12);
|
|
minStreamCount = getOption(options, "minStreamCount"_sr, 6);
|
|
maxStreamCount = getOption(options, "maxStreamCount"_sr, 20);
|
|
keyCount = getOption(options, "keyCount"_sr, 16);
|
|
writesPerRound = getOption(options, "writesPerRound"_sr, 5);
|
|
rounds = getOption(options, "rounds"_sr, 30);
|
|
assignmentPublicationChecks = getOption(options, "assignmentPublicationChecks"_sr, 0);
|
|
testProxyReplacement = getOption(options, "testProxyReplacement"_sr, false);
|
|
injectUndeliveredProxyHalt = getOption(options, "injectUndeliveredProxyHalt"_sr, false);
|
|
testMemoryBound = getOption(options, "testMemoryBound"_sr, false);
|
|
testReplyChunking = getOption(options, "testReplyChunking"_sr, false);
|
|
testOversizedPeek = getOption(options, "testOversizedPeek"_sr, false);
|
|
testDurableAckScan = getOption(options, "testDurableAckScan"_sr, false);
|
|
testDelayedRetention = getOption(options, "testDelayedRetention"_sr, false);
|
|
testRetiredRecovery = getOption(options, "testRetiredRecovery"_sr, false);
|
|
testRetiredSharedTagSnapshot = getOption(options, "testRetiredSharedTagSnapshot"_sr, false);
|
|
prepareRestartDrain = getOption(options, "prepareRestartDrain"_sr, false);
|
|
drainAfterRestart = getOption(options, "drainAfterRestart"_sr, false);
|
|
memoryTestValueBytes = getOption(options, "memoryTestValueBytes"_sr, 1024);
|
|
retentionValidationDelay = getOption(options, "retentionValidationDelay"_sr, 0.0);
|
|
drainProbability = getOption(options, "drainProbability"_sr, 0.25);
|
|
delayBetweenRounds = getOption(options, "delayBetweenRounds"_sr, 0.5);
|
|
operationTimeout = getOption(options, "operationTimeout"_sr, 120.0);
|
|
ASSERT_GE(minStreamCount, 1);
|
|
ASSERT_GE(initialStreamCount, minStreamCount);
|
|
ASSERT_GE(maxStreamCount, initialStreamCount);
|
|
ASSERT_GE(keyCount, 2);
|
|
ASSERT_GE(writesPerRound, 1);
|
|
ASSERT_LE(writesPerRound, keyCount);
|
|
ASSERT_GE(assignmentPublicationChecks, 0);
|
|
ASSERT(!injectUndeliveredProxyHalt || testProxyReplacement);
|
|
ASSERT_GT(memoryTestValueBytes, 0);
|
|
ASSERT_GE(retentionValidationDelay, 0.0);
|
|
ASSERT(!(prepareRestartDrain && drainAfterRestart));
|
|
ASSERT(!(testReplyChunking && (testOversizedPeek || testDurableAckScan)));
|
|
ASSERT(!(testOversizedPeek && testDurableAckScan));
|
|
ASSERT(!(testRetiredSharedTagSnapshot && testRetiredRecovery));
|
|
}
|
|
|
|
// RandomRangeLock can outlive this bounded CDC workload and mask its progress check.
|
|
void disableFailureInjectionWorkloads(std::set<std::string>& out) const override { out.insert("RandomRangeLock"); }
|
|
|
|
Future<Void> setup(Database const& cx) override {
|
|
if (clientId != 0) {
|
|
return Void();
|
|
}
|
|
if (drainAfterRestart) {
|
|
return Void();
|
|
}
|
|
if (prepareRestartDrain) {
|
|
return prepareRestartDrainSetup(cx);
|
|
}
|
|
if (testRetiredSharedTagSnapshot) {
|
|
return Void();
|
|
}
|
|
if (testOversizedPeek) {
|
|
return initializeOversizedPeekStreams(cx);
|
|
}
|
|
if (testReplyChunking) {
|
|
return initializeReplyChunkingStream(cx);
|
|
}
|
|
return initializeStreams(cx);
|
|
}
|
|
|
|
Future<Void> start(Database const& cx) override {
|
|
if (clientId != 0) {
|
|
return Void();
|
|
}
|
|
if (prepareRestartDrain) {
|
|
return Void();
|
|
}
|
|
if (drainAfterRestart) {
|
|
return drainRestartState(cx);
|
|
}
|
|
return run(cx);
|
|
}
|
|
|
|
Future<bool> check(Database const& cx) override { return true; }
|
|
|
|
void getMetrics(std::vector<PerfMetric>& m) override {}
|
|
};
|
|
|
|
WorkloadFactory<NativeCdcEndToEndWorkload> NativeCdcEndToEndWorkloadFactory;
|