918 lines
32 KiB
C++
918 lines
32 KiB
C++
/*
|
|
* NativeCdc.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 "fdbclient/DatabaseContext.h"
|
|
#include "fdbclient/Knobs.h"
|
|
#include "fdbclient/NativeCdc.h"
|
|
#include "fdbclient/SystemData.h"
|
|
#include "NativeCdcInternal.h"
|
|
#include "flow/CodeProbe.h"
|
|
#include "flow/Error.h"
|
|
#include "flow/Trace.h"
|
|
#include "flow/UnitTest.h"
|
|
|
|
namespace {
|
|
|
|
using CDCTagId = uint16_t;
|
|
|
|
constexpr uint32_t maxNativeCdcTagCount = static_cast<uint32_t>(std::numeric_limits<CDCTagId>::max()) + 1;
|
|
|
|
bool validNativeCdcTagCount(int tagCount) {
|
|
return tagCount > 0 && static_cast<uint32_t>(tagCount) <= maxNativeCdcTagCount;
|
|
}
|
|
|
|
void validateNativeCdcEnabled(bool enabled) {
|
|
if (!enabled) {
|
|
CODE_PROBE(true, "Native CDC registration rejected while feature disabled");
|
|
throw client_invalid_operation();
|
|
}
|
|
}
|
|
|
|
class NativeCdcIdentifierAllocator {
|
|
bool sawStream = false;
|
|
CDCStreamId maxStreamId = 0;
|
|
std::unordered_map<CDCTagId, uint32_t> tagStreamCounts;
|
|
|
|
public:
|
|
void observeStreamId(CDCStreamId streamId) {
|
|
sawStream = true;
|
|
maxStreamId = std::max(maxStreamId, streamId);
|
|
}
|
|
|
|
void observeTag(Tag tag) {
|
|
ASSERT_WE_THINK(tag.locality == tagLocalityCDC);
|
|
++tagStreamCounts[tag.id];
|
|
}
|
|
|
|
std::pair<CDCStreamId, Tag> allocate(int tagCount) const {
|
|
if (sawStream && maxStreamId == std::numeric_limits<CDCStreamId>::max()) {
|
|
throw operation_failed();
|
|
}
|
|
|
|
const CDCStreamId streamId = sawStream ? maxStreamId + 1 : 1;
|
|
if (!validNativeCdcTagCount(tagCount)) {
|
|
throw invalid_option_value();
|
|
}
|
|
uint32_t leastStreams = std::numeric_limits<uint32_t>::max();
|
|
CDCTagId selectedTagId = 0;
|
|
// TODO: Use data-distributor-observed per-tag write throughput to rebalance CDC tags, including
|
|
// migrating active streams with versioned tag-history assignments.
|
|
for (uint32_t tagId = 0; tagId < static_cast<uint32_t>(tagCount); ++tagId) {
|
|
auto count = tagStreamCounts.find(static_cast<CDCTagId>(tagId));
|
|
const uint32_t streamCount = count == tagStreamCounts.end() ? 0 : count->second;
|
|
if (streamCount < leastStreams) {
|
|
leastStreams = streamCount;
|
|
selectedTagId = static_cast<CDCTagId>(tagId);
|
|
}
|
|
}
|
|
return { streamId, Tag(tagLocalityCDC, selectedTagId) };
|
|
}
|
|
};
|
|
|
|
void validateNativeCdcStream(KeyRef const& name, KeyRangeRef const& keys) {
|
|
if (name.empty() || keys.empty() || !normalKeys.contains(keys)) {
|
|
throw client_invalid_operation();
|
|
}
|
|
}
|
|
|
|
Future<Optional<UID>> getNativeCdcProxyAssignment(Transaction* tr, CDCStreamId streamId) {
|
|
RangeResult assignments = co_await tr->getRange(cdcProxyRangeFor(streamId), 2);
|
|
ASSERT_LE(assignments.size(), 1);
|
|
if (assignments.empty()) {
|
|
co_return Optional<UID>();
|
|
}
|
|
const auto [assignedStreamId, proxyId] = decodeCDCProxyKey(assignments[0].key);
|
|
ASSERT_WE_THINK(assignedStreamId == streamId);
|
|
co_return proxyId;
|
|
}
|
|
|
|
Future<Tag> getNativeCdcCurrentTag(Transaction* tr, CDCStreamId streamId) {
|
|
// Tag-history keys sort by their big-endian assignment version, so the final
|
|
// key in this stream's prefix range contains its current tag.
|
|
RangeResult history = co_await tr->getRange(cdcTagHistoryRangeFor(streamId), 1, Snapshot::False, Reverse::True);
|
|
if (history.empty()) {
|
|
throw client_invalid_operation();
|
|
}
|
|
co_return decodeCDCTagHistoryKey(history.front().key).tag;
|
|
}
|
|
|
|
// TODO: Persist current per-tag ownership so registration does not reconstruct it by scanning all active streams.
|
|
Future<Optional<UID>> getNativeCdcProxyAssignmentForTag(Transaction* tr, Tag targetTag) {
|
|
std::set<CDCStreamId> activeStreamIds;
|
|
Key begin = cdcStreamKeys.begin;
|
|
while (begin < cdcStreamKeys.end) {
|
|
RangeResult streams = co_await tr->getRange(KeyRangeRef(begin, cdcStreamKeys.end), CLIENT_KNOBS->TOO_MANY);
|
|
for (const auto& stream : streams) {
|
|
activeStreamIds.insert(decodeCDCStreamKey(stream.key));
|
|
}
|
|
if (!streams.more) {
|
|
break;
|
|
}
|
|
begin = keyAfter(streams.back().key);
|
|
}
|
|
|
|
std::unordered_map<CDCStreamId, Tag> currentTags;
|
|
begin = cdcTagHistoryKeys.begin;
|
|
while (begin < cdcTagHistoryKeys.end) {
|
|
RangeResult histories =
|
|
co_await tr->getRange(KeyRangeRef(begin, cdcTagHistoryKeys.end), CLIENT_KNOBS->TOO_MANY);
|
|
for (const auto& history : histories) {
|
|
const CDCTagHistoryEntry decoded = decodeCDCTagHistoryKey(history.key);
|
|
if (activeStreamIds.contains(decoded.streamId)) {
|
|
currentTags[decoded.streamId] = decoded.tag;
|
|
}
|
|
}
|
|
if (!histories.more) {
|
|
break;
|
|
}
|
|
begin = keyAfter(histories.back().key);
|
|
}
|
|
|
|
for (const auto& [streamId, tag] : currentTags) {
|
|
if (tag == targetTag) {
|
|
Optional<UID> proxyId = co_await getNativeCdcProxyAssignment(tr, streamId);
|
|
if (proxyId.present()) {
|
|
co_return proxyId;
|
|
}
|
|
}
|
|
}
|
|
co_return Optional<UID>();
|
|
}
|
|
|
|
void signalNativeCdcProxyAssignmentChange(Transaction* tr) {
|
|
// Assignment updates are low-rate control-plane operations. A single
|
|
// coalescing signal lets the cluster controller rescan all durable owners.
|
|
tr->set(cdcProxyAssignmentChangeKey,
|
|
BinaryWriter::toValue(deterministicRandom()->randomUniqueID(),
|
|
IncludeVersion(ProtocolVersion::withNativeCdc())));
|
|
}
|
|
|
|
Future<Void> observeNativeCdcMetadata(Transaction* tr, NativeCdcIdentifierAllocator* allocator) {
|
|
Optional<Value> maxStreamId = co_await tr->get(cdcMaxStreamIdKey);
|
|
if (maxStreamId.present()) {
|
|
allocator->observeStreamId(decodeCDCMaxStreamIdValue(maxStreamId.get()));
|
|
}
|
|
|
|
std::set<CDCStreamId> activeStreamIds;
|
|
Key begin = cdcStreamKeys.begin;
|
|
while (begin < cdcStreamKeys.end) {
|
|
RangeResult streams = co_await tr->getRange(KeyRangeRef(begin, cdcStreamKeys.end), CLIENT_KNOBS->TOO_MANY);
|
|
for (const auto& kv : streams) {
|
|
const CDCStreamId streamId = decodeCDCStreamKey(kv.key);
|
|
activeStreamIds.insert(streamId);
|
|
allocator->observeStreamId(streamId);
|
|
}
|
|
if (!streams.more) {
|
|
break;
|
|
}
|
|
begin = keyAfter(streams.back().key);
|
|
}
|
|
|
|
std::unordered_map<CDCStreamId, Tag> currentTags;
|
|
begin = cdcTagHistoryKeys.begin;
|
|
while (begin < cdcTagHistoryKeys.end) {
|
|
RangeResult histories =
|
|
co_await tr->getRange(KeyRangeRef(begin, cdcTagHistoryKeys.end), CLIENT_KNOBS->TOO_MANY);
|
|
for (const auto& kv : histories) {
|
|
const CDCTagHistoryEntry history = decodeCDCTagHistoryKey(kv.key);
|
|
allocator->observeStreamId(history.streamId);
|
|
if (activeStreamIds.contains(history.streamId)) {
|
|
currentTags[history.streamId] = history.tag;
|
|
}
|
|
}
|
|
if (!histories.more) {
|
|
break;
|
|
}
|
|
begin = keyAfter(histories.back().key);
|
|
}
|
|
for (const auto& tagAssignment : currentTags) {
|
|
allocator->observeTag(tagAssignment.second);
|
|
}
|
|
}
|
|
|
|
bool retryNativeCdcProxyRequest(Error const& error) {
|
|
return error.code() == error_code_wrong_shard_server || error.code() == error_code_broken_promise ||
|
|
error.code() == error_code_connection_failed || error.code() == error_code_request_maybe_delivered;
|
|
}
|
|
|
|
bool rewindUnacknowledgedCursorAfterProxyReplacement(CDCCursor* currentPosition,
|
|
Version lastAcknowledgedVersion,
|
|
Optional<UID>* deliveryProxyId,
|
|
UID currentProxyId) {
|
|
const bool proxyReplaced = deliveryProxyId->present() && deliveryProxyId->get() != currentProxyId;
|
|
*deliveryProxyId = currentProxyId;
|
|
if (!proxyReplaced || currentPosition->lastConsumedVersion <= lastAcknowledgedVersion) {
|
|
return false;
|
|
}
|
|
currentPosition->lastConsumedVersion = lastAcknowledgedVersion;
|
|
return true;
|
|
}
|
|
|
|
// TODO: Have the cluster controller rebalance stream ownership using aggregate CDC proxy throughput and
|
|
// update cdcProxyKeys and ClientDBInfo assignments; registration currently chooses any available proxy.
|
|
Optional<CDCProxyInterface> selectAvailableNativeCdcProxy(ClientDBInfo const& clientInfo, Optional<UID> previousProxy) {
|
|
for (const auto& proxy : clientInfo.cdcProxies) {
|
|
if (!previousProxy.present() || proxy.id() != previousProxy.get()) {
|
|
return proxy;
|
|
}
|
|
}
|
|
if (!clientInfo.cdcProxies.empty()) {
|
|
return clientInfo.cdcProxies.front();
|
|
}
|
|
return Optional<CDCProxyInterface>();
|
|
}
|
|
|
|
bool containsNativeCdcProxy(ClientDBInfo const& clientInfo, UID proxyId) {
|
|
return std::any_of(clientInfo.cdcProxies.begin(),
|
|
clientInfo.cdcProxies.end(),
|
|
[proxyId](CDCProxyInterface const& proxy) { return proxy.id() == proxyId; });
|
|
}
|
|
|
|
Future<bool> nativeCdcStreamStillExists(Database cx, CDCStreamId streamId) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::READ_LOCK_AWARE);
|
|
tr.setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
|
co_return (co_await tr.get(cdcStreamKeyFor(streamId))).present();
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Optional<CDCStreamId>> findNativeCdcStreamId(Database cx, Key name) {
|
|
if (name.empty()) {
|
|
throw client_invalid_operation();
|
|
}
|
|
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::READ_LOCK_AWARE);
|
|
tr.setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
|
Optional<Value> streamId = co_await tr.get(cdcStreamNameKeyFor(name));
|
|
if (!streamId.present()) {
|
|
co_return Optional<CDCStreamId>();
|
|
}
|
|
co_return decodeCDCStreamNameValue(streamId.get());
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<CDCStreamId> getNativeCdcStreamId(Database cx, Key name) {
|
|
Optional<CDCStreamId> streamId = co_await findNativeCdcStreamId(cx, name);
|
|
if (!streamId.present()) {
|
|
throw client_invalid_operation();
|
|
}
|
|
co_return streamId.get();
|
|
}
|
|
|
|
Future<CDCProxyInterface> getNativeCdcStreamProxy(Database cx, CDCStreamId streamId) {
|
|
if (streamId == 0) {
|
|
throw client_invalid_operation();
|
|
}
|
|
|
|
while (true) {
|
|
const ClientDBInfo& clientInfo = cx->clientInfo->get();
|
|
auto assigned = clientInfo.streamToCDCProxyId.find(streamId);
|
|
if (assigned != clientInfo.streamToCDCProxyId.end()) {
|
|
for (const auto& proxy : clientInfo.cdcProxies) {
|
|
if (proxy.id() == assigned->second) {
|
|
co_return proxy;
|
|
}
|
|
}
|
|
}
|
|
if (!(co_await nativeCdcStreamStillExists(cx, streamId))) {
|
|
CODE_PROBE(true, "Native CDC client rejected operation after stream removal");
|
|
throw client_invalid_operation();
|
|
}
|
|
co_await delay(CLIENT_KNOBS->WRONG_SHARD_SERVER_DELAY, cx->taskID);
|
|
}
|
|
}
|
|
|
|
bool nativeCdcNameMatchesStream(Optional<Value> const& currentId, CDCStreamId streamId) {
|
|
return currentId.present() && decodeCDCStreamNameValue(currentId.get()) == streamId;
|
|
}
|
|
|
|
Future<bool> namedNativeCdcStreamStillExists(Database cx, Key name, CDCStreamId streamId) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::READ_LOCK_AWARE);
|
|
tr.setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
|
Optional<Value> currentId = co_await tr.get(cdcStreamNameKeyFor(name));
|
|
co_return nativeCdcNameMatchesStream(currentId, streamId);
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Optional<CDCProxyInterface>> getNativeCdcStreamProxyForRemoval(Database cx, Key name, CDCStreamId streamId) {
|
|
while (true) {
|
|
const ClientDBInfo& clientInfo = cx->clientInfo->get();
|
|
auto assigned = clientInfo.streamToCDCProxyId.find(streamId);
|
|
if (assigned != clientInfo.streamToCDCProxyId.end()) {
|
|
for (const auto& proxy : clientInfo.cdcProxies) {
|
|
if (proxy.id() == assigned->second) {
|
|
co_return proxy;
|
|
}
|
|
}
|
|
}
|
|
if (!(co_await namedNativeCdcStreamStillExists(cx, name, streamId))) {
|
|
co_return Optional<CDCProxyInterface>();
|
|
}
|
|
co_await delay(CLIENT_KNOBS->WRONG_SHARD_SERVER_DELAY, cx->taskID);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Future<CDCStreamId> registerNativeCdcStream(Database cx, Key name, KeyRange keys, UID proxyId) {
|
|
validateNativeCdcStream(name, keys);
|
|
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
tr.setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
|
|
const Key nameKey = cdcStreamNameKeyFor(name);
|
|
Optional<Value> currentId = co_await tr.get(nameKey);
|
|
if (currentId.present()) {
|
|
const CDCStreamId streamId = decodeCDCStreamNameValue(currentId.get());
|
|
Optional<Value> currentKeys = co_await tr.get(cdcStreamKeyFor(streamId));
|
|
if (!currentKeys.present() || decodeCDCStreamKeysValue(currentKeys.get()) != keys) {
|
|
throw client_invalid_operation();
|
|
}
|
|
if (!(co_await getNativeCdcProxyAssignment(&tr, streamId)).present()) {
|
|
CODE_PROBE(true, "Native CDC registration restores missing stream owner", probe::decoration::rare);
|
|
const Tag tag = co_await getNativeCdcCurrentTag(&tr, streamId);
|
|
Optional<UID> sharedTagProxy = co_await getNativeCdcProxyAssignmentForTag(&tr, tag);
|
|
CODE_PROBE(sharedTagProxy.present(),
|
|
"Native CDC shared-tag streams use one owner",
|
|
probe::decoration::rare);
|
|
const UID selectedProxy = sharedTagProxy.present() ? sharedTagProxy.get() : proxyId;
|
|
tr.set(cdcProxyKeyFor(streamId, selectedProxy), Value());
|
|
signalNativeCdcProxyAssignmentChange(&tr);
|
|
co_await tr.commit();
|
|
}
|
|
co_return streamId;
|
|
}
|
|
|
|
// Disabling CDC stops new admission, but existing registrations and
|
|
// owner repair must remain available so durable streams can drain.
|
|
const bool nativeCdcEnabled = cx->clientInfo->get().nativeCdcEnabled;
|
|
const int nativeCdcTagCount = cx->clientInfo->get().nativeCdcTagCount;
|
|
validateNativeCdcEnabled(nativeCdcEnabled);
|
|
NativeCdcIdentifierAllocator allocator;
|
|
co_await observeNativeCdcMetadata(&tr, &allocator);
|
|
const auto [streamId, tag] = allocator.allocate(nativeCdcTagCount);
|
|
// The read version is a conservative lower bound for tag routing.
|
|
// The versionstamped minimum below is the commit version, and stream
|
|
// initialization takes their maximum before exposing mutations.
|
|
const Version registrationVersion = co_await tr.getReadVersion();
|
|
|
|
tr.set(nameKey, cdcStreamNameValue(streamId));
|
|
tr.set(cdcMaxStreamIdKey, cdcMaxStreamIdValue(streamId));
|
|
tr.set(cdcStreamKeyFor(streamId), cdcStreamKeysValue(keys));
|
|
tr.set(cdcTagHistoryKeyFor(streamId, registrationVersion, tag), Value());
|
|
tr.atomicOp(
|
|
cdcMinVersionKeyFor(streamId), cdcVersionstampedMinVersionValue(), MutationRef::SetVersionstampedValue);
|
|
Optional<UID> sharedTagProxy = co_await getNativeCdcProxyAssignmentForTag(&tr, tag);
|
|
const UID selectedProxy = sharedTagProxy.present() ? sharedTagProxy.get() : proxyId;
|
|
tr.set(cdcProxyKeyFor(streamId, selectedProxy), Value());
|
|
signalNativeCdcProxyAssignmentChange(&tr);
|
|
co_await tr.commit();
|
|
co_return streamId;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<bool> removeNativeCdcStream(Database cx, Key name, CDCStreamId streamId, UID proxyId) {
|
|
if (name.empty() || streamId == 0) {
|
|
throw client_invalid_operation();
|
|
}
|
|
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::LOCK_AWARE);
|
|
tr.setOption(FDBTransactionOptions::ACCESS_SYSTEM_KEYS);
|
|
|
|
const Key nameKey = cdcStreamNameKeyFor(name);
|
|
Optional<Value> currentId = co_await tr.get(nameKey);
|
|
if (!nativeCdcNameMatchesStream(currentId, streamId)) {
|
|
CODE_PROBE(currentId.present(),
|
|
"Native CDC preserves a replacement stream during removal retry",
|
|
probe::decoration::rare);
|
|
if (currentId.present()) {
|
|
TraceEvent("NativeCdcRemovalPreservesReplacement")
|
|
.detail("RemovedStreamId", streamId)
|
|
.detail("ReplacementStreamId", decodeCDCStreamNameValue(currentId.get()));
|
|
}
|
|
co_return false;
|
|
}
|
|
|
|
Optional<UID> assignedProxy = co_await getNativeCdcProxyAssignment(&tr, streamId);
|
|
if (!assignedProxy.present() || assignedProxy.get() != proxyId) {
|
|
CODE_PROBE(true, "Native CDC rejects removal through a stale owner", probe::decoration::rare);
|
|
throw wrong_shard_server();
|
|
}
|
|
|
|
std::set<Tag> removedTags;
|
|
const KeyRange historyRange = cdcTagHistoryRangeFor(streamId);
|
|
Key begin = historyRange.begin;
|
|
while (begin < historyRange.end) {
|
|
RangeResult history =
|
|
co_await tr.getRange(KeyRangeRef(begin, historyRange.end), CLIENT_KNOBS->TOO_MANY);
|
|
for (const auto& entry : history) {
|
|
removedTags.insert(decodeCDCTagHistoryKey(entry.key).tag);
|
|
}
|
|
if (!history.more) {
|
|
break;
|
|
}
|
|
begin = keyAfter(history.back().key);
|
|
}
|
|
|
|
tr.clear(nameKey);
|
|
tr.clear(cdcStreamKeyFor(streamId));
|
|
for (const Tag& tag : removedTags) {
|
|
tr.set(cdcRetiredTagPopKeyFor(tag), Value());
|
|
tr.atomicOp(cdcRetiredTagPopVersionKeyFor(tag),
|
|
cdcVersionstampedMinVersionValue(),
|
|
MutationRef::SetVersionstampedValue);
|
|
}
|
|
tr.clear(cdcTagHistoryRangeFor(streamId));
|
|
tr.clear(cdcMinVersionKeyFor(streamId));
|
|
tr.clear(cdcProxyRangeFor(streamId));
|
|
if (assignedProxy.present()) {
|
|
signalNativeCdcProxyAssignmentChange(&tr);
|
|
}
|
|
co_await tr.commit();
|
|
CODE_PROBE(!removedTags.empty(), "Native CDC removal records final tagged pop work");
|
|
co_return true;
|
|
} catch (Error& e) {
|
|
if (e.code() == error_code_wrong_shard_server) {
|
|
throw;
|
|
}
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<std::vector<NativeCdcStreamInfo>> listNativeCdcStreams(Database cx) {
|
|
Transaction tr(cx);
|
|
while (true) {
|
|
Error err;
|
|
try {
|
|
tr.setOption(FDBTransactionOptions::READ_LOCK_AWARE);
|
|
tr.setOption(FDBTransactionOptions::READ_SYSTEM_KEYS);
|
|
|
|
std::vector<std::pair<Key, CDCStreamId>> names;
|
|
Key begin = cdcStreamNameKeys.begin;
|
|
while (begin < cdcStreamNameKeys.end) {
|
|
RangeResult page =
|
|
co_await tr.getRange(KeyRangeRef(begin, cdcStreamNameKeys.end), CLIENT_KNOBS->TOO_MANY);
|
|
for (const auto& kv : page) {
|
|
names.emplace_back(decodeCDCStreamNameKey(kv.key), decodeCDCStreamNameValue(kv.value));
|
|
}
|
|
if (!page.more) {
|
|
break;
|
|
}
|
|
begin = keyAfter(page.back().key);
|
|
}
|
|
|
|
std::unordered_map<CDCStreamId, KeyRange> streamKeys;
|
|
begin = cdcStreamKeys.begin;
|
|
while (begin < cdcStreamKeys.end) {
|
|
RangeResult page = co_await tr.getRange(KeyRangeRef(begin, cdcStreamKeys.end), CLIENT_KNOBS->TOO_MANY);
|
|
for (const auto& kv : page) {
|
|
streamKeys.emplace(decodeCDCStreamKey(kv.key), decodeCDCStreamKeysValue(kv.value));
|
|
}
|
|
if (!page.more) {
|
|
break;
|
|
}
|
|
begin = keyAfter(page.back().key);
|
|
}
|
|
|
|
std::unordered_map<CDCStreamId, Version> minVersions;
|
|
begin = cdcMinVersionKeys.begin;
|
|
while (begin < cdcMinVersionKeys.end) {
|
|
RangeResult page =
|
|
co_await tr.getRange(KeyRangeRef(begin, cdcMinVersionKeys.end), CLIENT_KNOBS->TOO_MANY);
|
|
for (const auto& kv : page) {
|
|
minVersions.emplace(decodeCDCMinVersionKey(kv.key), decodeCDCMinVersionValue(kv.value));
|
|
}
|
|
if (!page.more) {
|
|
break;
|
|
}
|
|
begin = keyAfter(page.back().key);
|
|
}
|
|
|
|
std::vector<NativeCdcStreamInfo> result;
|
|
result.reserve(names.size());
|
|
for (auto& [name, streamId] : names) {
|
|
auto keys = streamKeys.find(streamId);
|
|
auto minVersion = minVersions.find(streamId);
|
|
if (keys != streamKeys.end() && minVersion != minVersions.end()) {
|
|
result.push_back(
|
|
NativeCdcStreamInfo{ std::move(name), streamId, keys->second, minVersion->second });
|
|
}
|
|
}
|
|
co_return result;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Void> reassignNativeCdcStreams(Database cx, UID oldProxyId, UID newProxyId) {
|
|
if (oldProxyId == newProxyId) {
|
|
co_return;
|
|
}
|
|
|
|
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);
|
|
|
|
bool changed = false;
|
|
Key begin = cdcProxyKeys.begin;
|
|
while (begin < cdcProxyKeys.end) {
|
|
RangeResult assignments =
|
|
co_await tr.getRange(KeyRangeRef(begin, cdcProxyKeys.end), CLIENT_KNOBS->TOO_MANY);
|
|
for (const auto& assignment : assignments) {
|
|
const auto [streamId, proxyId] = decodeCDCProxyKey(assignment.key);
|
|
if (proxyId == oldProxyId) {
|
|
tr.clear(assignment.key);
|
|
tr.set(cdcProxyKeyFor(streamId, newProxyId), Value());
|
|
changed = true;
|
|
}
|
|
}
|
|
if (!assignments.more) {
|
|
break;
|
|
}
|
|
begin = keyAfter(assignments.back().key);
|
|
}
|
|
|
|
if (changed) {
|
|
CODE_PROBE(true, "Native CDC reassigns streams after proxy replacement");
|
|
signalNativeCdcProxyAssignmentChange(&tr);
|
|
co_await tr.commit();
|
|
}
|
|
co_return;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<Version> acknowledgeNativeCdcStream(Database cx,
|
|
CDCStreamId streamId,
|
|
Version consumedThrough,
|
|
Version knownAvailableThrough) {
|
|
if (streamId == 0 || consumedThrough < 0 || consumedThrough >= std::numeric_limits<Version>::max() - 1) {
|
|
throw client_invalid_operation();
|
|
}
|
|
const Version minUnpoppedVersion = consumedThrough + 1;
|
|
|
|
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> minVersionValue = co_await tr.get(cdcMinVersionKeyFor(streamId));
|
|
if (!minVersionValue.present()) {
|
|
throw client_invalid_operation();
|
|
}
|
|
|
|
const Version minVersion = decodeCDCMinVersionValue(minVersionValue.get());
|
|
if (minUnpoppedVersion <= minVersion) {
|
|
CODE_PROBE(true, "Native CDC preserves a durable duplicate acknowledgement");
|
|
co_return minVersion;
|
|
}
|
|
|
|
const Version readVersion = co_await tr.getReadVersion();
|
|
if (consumedThrough > readVersion && consumedThrough > knownAvailableThrough) {
|
|
CODE_PROBE(true, "Native CDC rejects unproven acknowledgement progress");
|
|
throw client_invalid_operation();
|
|
}
|
|
|
|
tr.set(cdcMinVersionKeyFor(streamId), cdcMinVersionValue(minUnpoppedVersion));
|
|
co_await tr.commit();
|
|
co_return minUnpoppedVersion;
|
|
} catch (Error& e) {
|
|
err = e;
|
|
}
|
|
co_await tr.onError(err);
|
|
}
|
|
}
|
|
|
|
Future<CDCStreamId> registerNativeCdcStreamClient(Database cx, Key name, KeyRange keys) {
|
|
validateNativeCdcStream(name, keys);
|
|
Optional<UID> previousProxy;
|
|
while (true) {
|
|
Future<Void> proxyChanged = cx->clientInfo->onChange();
|
|
Optional<CDCProxyInterface> selectedProxy;
|
|
bool registrationEnabled;
|
|
UID clientInfoId;
|
|
{
|
|
const ClientDBInfo& clientInfo = cx->clientInfo->get();
|
|
selectedProxy = selectAvailableNativeCdcProxy(clientInfo, previousProxy);
|
|
registrationEnabled = clientInfo.nativeCdcEnabled;
|
|
clientInfoId = clientInfo.id;
|
|
}
|
|
if (!registrationEnabled) {
|
|
Optional<CDCStreamId> existingStream = co_await findNativeCdcStreamId(cx, name);
|
|
if (cx->clientInfo->get().id != clientInfoId) {
|
|
CODE_PROBE(true, "Native CDC registration retries after client info changes during existence check");
|
|
continue;
|
|
}
|
|
if (!existingStream.present()) {
|
|
validateNativeCdcEnabled(registrationEnabled);
|
|
}
|
|
}
|
|
if (!selectedProxy.present()) {
|
|
co_await proxyChanged;
|
|
continue;
|
|
}
|
|
CDCProxyInterface proxy = selectedProxy.get();
|
|
try {
|
|
Future<ErrorOr<CDCRegisterStreamReply>> request =
|
|
proxy.registerStream.tryGetReply(CDCRegisterStreamRequest(name, keys));
|
|
// Assignment publications for other streams also change ClientDBInfo. Keep this request alive while its
|
|
// proxy remains published; abandoning it can let a server-side retry recreate the stream after removal.
|
|
while (true) {
|
|
auto result = co_await race(throwErrorOr(request), proxyChanged);
|
|
if (result.index() == 0) {
|
|
co_return std::get<0>(result).streamId;
|
|
}
|
|
|
|
proxyChanged = cx->clientInfo->onChange();
|
|
if (!containsNativeCdcProxy(cx->clientInfo->get(), proxy.id())) {
|
|
break;
|
|
}
|
|
CODE_PROBE(true, "Native CDC registration preserves request across unrelated client info change");
|
|
}
|
|
} catch (Error& error) {
|
|
if (!retryNativeCdcProxyRequest(error)) {
|
|
throw;
|
|
}
|
|
}
|
|
previousProxy = proxy.id();
|
|
co_await delay(CLIENT_KNOBS->WRONG_SHARD_SERVER_DELAY, cx->taskID);
|
|
}
|
|
}
|
|
|
|
Future<std::vector<NativeCdcStreamInfo>> listNativeCdcStreamsClient(Database cx) {
|
|
co_return co_await listNativeCdcStreams(cx);
|
|
}
|
|
|
|
Future<Void> removeNativeCdcStreamClient(Database cx, Key name) {
|
|
if (name.empty()) {
|
|
throw client_invalid_operation();
|
|
}
|
|
|
|
Optional<CDCStreamId> streamId = co_await findNativeCdcStreamId(cx, name);
|
|
if (!streamId.present()) {
|
|
co_return;
|
|
}
|
|
|
|
while (true) {
|
|
Optional<CDCProxyInterface> proxy = co_await getNativeCdcStreamProxyForRemoval(cx, name, streamId.get());
|
|
if (!proxy.present()) {
|
|
co_return;
|
|
}
|
|
try {
|
|
Future<Void> proxyChanged = cx->clientInfo->onChange();
|
|
auto result = co_await race(
|
|
throwErrorOr(proxy.get().removeStream.tryGetReply(CDCRemoveStreamRequest(name, streamId.get()))),
|
|
proxyChanged);
|
|
if (result.index() == 0) {
|
|
co_return;
|
|
}
|
|
} catch (Error& error) {
|
|
if (!retryNativeCdcProxyRequest(error)) {
|
|
throw;
|
|
}
|
|
}
|
|
co_await delay(CLIENT_KNOBS->WRONG_SHARD_SERVER_DELAY, cx->taskID);
|
|
}
|
|
}
|
|
|
|
Future<Reference<NativeCdcConsumer>> createNativeCdcConsumer(Database cx, Key name) {
|
|
const CDCStreamId streamId = co_await getNativeCdcStreamId(cx, name);
|
|
co_return makeReference<NativeCdcConsumer>(cx, CDCCursor(streamId, invalidVersion), invalidVersion);
|
|
}
|
|
|
|
Reference<NativeCdcConsumer> resumeNativeCdcConsumer(Database cx, CDCCursor position) {
|
|
return makeReference<NativeCdcConsumer>(cx, position);
|
|
}
|
|
|
|
Future<CDCConsumeReply> NativeCdcConsumer::consumeImpl(Reference<NativeCdcConsumer> self) {
|
|
try {
|
|
while (true) {
|
|
CDCProxyInterface proxy = co_await getNativeCdcStreamProxy(self->cx, self->currentPosition.streamId);
|
|
if (rewindUnacknowledgedCursorAfterProxyReplacement(
|
|
&self->currentPosition, self->lastAcknowledgedVersion, &self->deliveryProxyId, proxy.id())) {
|
|
self->knownAvailableThrough = self->lastAcknowledgedVersion;
|
|
CODE_PROBE(true, "Native CDC consumer rewinds unacknowledged cursor after proxy replacement");
|
|
}
|
|
try {
|
|
CDCConsumeReply reply =
|
|
co_await throwErrorOr(proxy.consume.tryGetReply(CDCConsumeRequest(self->currentPosition)));
|
|
if (reply.lastConsumedVersion == self->currentPosition.lastConsumedVersion && reply.mutations.empty()) {
|
|
// The server lease bounds abandoned long polls. Renew it transparently so the public consume
|
|
// operation remains a long poll without accumulating server actors after client cancellation.
|
|
CODE_PROBE(true, "Native CDC consume renews an idle server lease");
|
|
continue;
|
|
}
|
|
self->knownAvailableThrough = reply.lastConsumedVersion;
|
|
self->currentPosition.lastConsumedVersion = reply.lastConsumedVersion;
|
|
self->operationOutstanding = false;
|
|
co_return reply;
|
|
} catch (Error& error) {
|
|
if (!retryNativeCdcProxyRequest(error)) {
|
|
throw;
|
|
}
|
|
CODE_PROBE(true, "Native CDC consume retries after proxy request failure");
|
|
}
|
|
co_await delay(CLIENT_KNOBS->WRONG_SHARD_SERVER_DELAY, self->cx->taskID);
|
|
}
|
|
} catch (Error&) {
|
|
self->operationOutstanding = false;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
Future<CDCConsumeReply> NativeCdcConsumer::consume() {
|
|
if (operationOutstanding) {
|
|
CODE_PROBE(true, "Native CDC consumer rejects overlapping operations", probe::decoration::rare);
|
|
return Future<CDCConsumeReply>(client_invalid_operation());
|
|
}
|
|
operationOutstanding = true;
|
|
return consumeImpl(Reference<NativeCdcConsumer>::addRef(this));
|
|
}
|
|
|
|
Future<Void> NativeCdcConsumer::acknowledgeImpl(Reference<NativeCdcConsumer> self) {
|
|
try {
|
|
if (self->currentPosition.streamId == 0 || self->currentPosition.lastConsumedVersion < 0 ||
|
|
self->currentPosition.lastConsumedVersion == std::numeric_limits<Version>::max()) {
|
|
throw client_invalid_operation();
|
|
}
|
|
const Version acknowledgedVersion = self->currentPosition.lastConsumedVersion;
|
|
const Version durableMinVersion = co_await acknowledgeNativeCdcStream(
|
|
self->cx, self->currentPosition.streamId, acknowledgedVersion, self->knownAvailableThrough);
|
|
self->lastAcknowledgedVersion = std::max(self->lastAcknowledgedVersion, durableMinVersion - 1);
|
|
// The durable transaction completes before the proxy RPC. FoundationDB's
|
|
// transaction ordering guarantees the proxy's subsequent metadata read
|
|
// observes this acknowledgement.
|
|
|
|
while (true) {
|
|
CDCProxyInterface proxy = co_await getNativeCdcStreamProxy(self->cx, self->currentPosition.streamId);
|
|
try {
|
|
Future<Void> proxyChanged = self->cx->clientInfo->onChange();
|
|
auto result = co_await race(throwErrorOr(proxy.ack.tryGetReply(
|
|
CDCAckRequest(self->currentPosition.streamId, acknowledgedVersion))),
|
|
proxyChanged);
|
|
if (result.index() == 0) {
|
|
self->operationOutstanding = false;
|
|
co_return;
|
|
}
|
|
} catch (Error& error) {
|
|
if (!retryNativeCdcProxyRequest(error)) {
|
|
throw;
|
|
}
|
|
}
|
|
co_await delay(CLIENT_KNOBS->WRONG_SHARD_SERVER_DELAY, self->cx->taskID);
|
|
}
|
|
} catch (Error&) {
|
|
self->operationOutstanding = false;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
Future<Void> NativeCdcConsumer::acknowledge() {
|
|
if (operationOutstanding) {
|
|
CODE_PROBE(true, "Native CDC consumer rejects overlapping operations", probe::decoration::rare);
|
|
return Future<Void>(client_invalid_operation());
|
|
}
|
|
operationOutstanding = true;
|
|
return acknowledgeImpl(Reference<NativeCdcConsumer>::addRef(this));
|
|
}
|
|
|
|
TEST_CASE("/NativeCDC/LifecycleAllocation") {
|
|
ASSERT(!validNativeCdcTagCount(-1));
|
|
ASSERT(!validNativeCdcTagCount(0));
|
|
ASSERT(validNativeCdcTagCount(1));
|
|
ASSERT(validNativeCdcTagCount(std::numeric_limits<uint16_t>::max() + 1u));
|
|
ASSERT(!validNativeCdcTagCount(std::numeric_limits<uint16_t>::max() + 2u));
|
|
|
|
NativeCdcIdentifierAllocator allocator;
|
|
auto [initialId, initialTag] = allocator.allocate(CLIENT_KNOBS->NATIVE_CDC_TAG_COUNT);
|
|
ASSERT_EQ(initialId, 1);
|
|
ASSERT_EQ(initialTag, Tag(tagLocalityCDC, 0));
|
|
|
|
allocator.observeStreamId(9);
|
|
allocator.observeTag(initialTag);
|
|
allocator.observeTag(Tag(tagLocalityCDC, 2));
|
|
auto [nextId, nextTag] = allocator.allocate(CLIENT_KNOBS->NATIVE_CDC_TAG_COUNT);
|
|
ASSERT_EQ(nextId, 10);
|
|
ASSERT_EQ(nextTag, Tag(tagLocalityCDC, 1));
|
|
|
|
NativeCdcIdentifierAllocator publishedPoolAllocator;
|
|
publishedPoolAllocator.observeTag(Tag(tagLocalityCDC, 0));
|
|
// The cluster-controller-published pool is authoritative even when it differs from this process's knob.
|
|
auto [publishedPoolId, publishedPoolTag] = publishedPoolAllocator.allocate(1);
|
|
ASSERT_EQ(publishedPoolId, 1);
|
|
ASSERT_EQ(publishedPoolTag, Tag(tagLocalityCDC, 0));
|
|
|
|
NativeCdcIdentifierAllocator fullPoolAllocator;
|
|
for (uint32_t tagId = 0; tagId < static_cast<uint32_t>(CLIENT_KNOBS->NATIVE_CDC_TAG_COUNT); ++tagId) {
|
|
fullPoolAllocator.observeTag(Tag(tagLocalityCDC, static_cast<uint16_t>(tagId)));
|
|
}
|
|
auto [sharedId, sharedTag] = fullPoolAllocator.allocate(CLIENT_KNOBS->NATIVE_CDC_TAG_COUNT);
|
|
ASSERT_EQ(sharedId, 1);
|
|
ASSERT_EQ(sharedTag, Tag(tagLocalityCDC, 0));
|
|
|
|
return Void();
|
|
}
|
|
|
|
TEST_CASE("/NativeCDC/ConsumerRewindsUnacknowledgedCursorOnProxyReplacement") {
|
|
CDCCursor cursor(1, invalidVersion);
|
|
Optional<UID> deliveryProxyId;
|
|
const UID firstProxy(1, 2);
|
|
const UID secondProxy(3, 4);
|
|
|
|
ASSERT(!rewindUnacknowledgedCursorAfterProxyReplacement(&cursor, invalidVersion, &deliveryProxyId, firstProxy));
|
|
cursor.lastConsumedVersion = 100;
|
|
ASSERT(!rewindUnacknowledgedCursorAfterProxyReplacement(&cursor, invalidVersion, &deliveryProxyId, firstProxy));
|
|
ASSERT(rewindUnacknowledgedCursorAfterProxyReplacement(&cursor, invalidVersion, &deliveryProxyId, secondProxy));
|
|
ASSERT_EQ(cursor.lastConsumedVersion, invalidVersion);
|
|
|
|
cursor.lastConsumedVersion = 100;
|
|
ASSERT(rewindUnacknowledgedCursorAfterProxyReplacement(&cursor, 80, &deliveryProxyId, firstProxy));
|
|
ASSERT_EQ(cursor.lastConsumedVersion, 80);
|
|
ASSERT(!rewindUnacknowledgedCursorAfterProxyReplacement(&cursor, 80, &deliveryProxyId, secondProxy));
|
|
ASSERT_EQ(cursor.lastConsumedVersion, 80);
|
|
|
|
return Void();
|
|
}
|
|
|
|
TEST_CASE("/NativeCDC/RemovalMatchesOriginalStream") {
|
|
const CDCStreamId originalStreamId = 1;
|
|
ASSERT(nativeCdcNameMatchesStream(Optional<Value>(cdcStreamNameValue(originalStreamId)), originalStreamId));
|
|
ASSERT(!nativeCdcNameMatchesStream(Optional<Value>(cdcStreamNameValue(originalStreamId + 1)), originalStreamId));
|
|
ASSERT(!nativeCdcNameMatchesStream(Optional<Value>(), originalStreamId));
|
|
|
|
return Void();
|
|
}
|