/* * IdempotencyId.actor.cpp * * This source file is part of the FoundationDB open source project * * Copyright 2013-2022 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 "fdbclient/IdempotencyId.actor.h" #include "fdbclient/ReadYourWrites.h" #include "fdbclient/SystemData.h" #include "flow/UnitTest.h" #include "flow/actorcompiler.h" // this has to be the last include struct IdempotencyIdKVBuilderImpl { Optional commitVersion; Optional batchIndexHighOrderByte; BinaryWriter value{ IncludeVersion() }; }; IdempotencyIdKVBuilder::IdempotencyIdKVBuilder() : impl(PImpl::create()) {} void IdempotencyIdKVBuilder::setCommitVersion(Version commitVersion) { impl->commitVersion = commitVersion; } void IdempotencyIdKVBuilder::add(const IdempotencyIdRef& id, uint16_t batchIndex) { ASSERT(id.valid()); if (impl->batchIndexHighOrderByte.present()) { ASSERT((batchIndex >> 8) == impl->batchIndexHighOrderByte.get()); } else { impl->batchIndexHighOrderByte = batchIndex >> 8; impl->value << int64_t(now()); } StringRef s = id.asStringRefUnsafe(); impl->value << uint8_t(s.size()); impl->value.serializeBytes(s); impl->value << uint8_t(batchIndex); // Low order byte of batchIndex } Optional IdempotencyIdKVBuilder::buildAndClear() { ASSERT(impl->commitVersion.present()); if (!impl->batchIndexHighOrderByte.present()) { return {}; } Value v = impl->value.toValue(); KeyRef key = makeIdempotencySingleKeyRange(v.arena(), impl->commitVersion.get(), impl->batchIndexHighOrderByte.get()).begin; impl->value = BinaryWriter(IncludeVersion()); impl->batchIndexHighOrderByte = Optional(); Optional result = KeyValue(); result.get().arena() = v.arena(); result.get().key = key; result.get().value = v; return result; } IdempotencyIdKVBuilder::~IdempotencyIdKVBuilder() = default; Optional kvContainsIdempotencyId(const KeyValueRef& kv, const IdempotencyIdRef& id) { ASSERT(id.valid()); StringRef needle = id.asStringRefUnsafe(); StringRef haystack = kv.value; #ifndef _WIN32 // The common case is that the kv does not contain the idempotency id, so early return if memmem is available if (memmem(haystack.begin(), haystack.size(), needle.begin(), needle.size()) == nullptr) { return {}; } #endif // Even if id is a substring of value, it may still not actually contain it. BinaryReader reader(kv.value.begin(), kv.value.size(), IncludeVersion()); int64_t timestamp; // ignored reader >> timestamp; while (!reader.empty()) { uint8_t length; reader >> length; StringRef candidate{ reinterpret_cast(reader.readBytes(length)), length }; uint8_t lowOrderBatchIndex; reader >> lowOrderBatchIndex; if (candidate == needle) { Version commitVersion; uint8_t highOrderBatchIndex; decodeIdempotencyKey(kv.key, commitVersion, highOrderBatchIndex); return CommitResult{ commitVersion, static_cast((uint16_t(highOrderBatchIndex) << 8) | uint16_t(lowOrderBatchIndex)) }; } } return {}; } void forceLinkIdempotencyIdTests() {} namespace { IdempotencyIdRef generate(Arena& arena) { int length = deterministicRandom()->coinflip() ? deterministicRandom()->randomInt(16, 256) : 16; StringRef id = makeString(length, arena); deterministicRandom()->randomBytes(mutateString(id), length); return IdempotencyIdRef(id); } } // namespace TEST_CASE("/fdbclient/IdempotencyId/basic") { Arena arena; uint16_t firstBatchIndex = deterministicRandom()->randomUInt32(); firstBatchIndex &= 0xff7f; // ensure firstBatchIndex+5 won't change the higher order byte uint16_t batchIndex = firstBatchIndex; Version commitVersion = deterministicRandom()->randomInt64(0, std::numeric_limits::max()); std::vector idVector; // Reference std::unordered_set idSet; // Make sure hash+equals works IdempotencyIdKVBuilder builder; // Check kv data format builder.setCommitVersion(commitVersion); for (int i = 0; i < 5; ++i) { auto id = generate(arena); idVector.emplace_back(id); idSet.emplace(id); builder.add(id, batchIndex++); } batchIndex = firstBatchIndex; Optional kvOpt = builder.buildAndClear(); ASSERT(kvOpt.present()); const auto& kv = kvOpt.get(); ASSERT(idSet.size() == idVector.size()); for (const auto& id : idVector) { auto commitResult = kvContainsIdempotencyId(kv, id); ASSERT(commitResult.present()); ASSERT(commitResult.get().commitVersion == commitVersion); ASSERT(commitResult.get().batchIndex == batchIndex++); ASSERT(idSet.find(id) != idSet.end()); idSet.erase(id); ASSERT(idSet.find(id) == idSet.end()); } ASSERT(idSet.size() == 0); ASSERT(!kvContainsIdempotencyId(kv, generate(arena)).present()); return Void(); } TEST_CASE("/fdbclient/IdempotencyId/serialization") { ASSERT(ObjectReader::fromStringRef(ObjectWriter::toValue(IdempotencyIdRef(), Unversioned()), Unversioned()) == IdempotencyIdRef()); for (int i = 0; i < 1000; ++i) { Arena arena; auto id = generate(arena); auto serialized = ObjectWriter::toValue(id, Unversioned()); IdempotencyIdRef t; ObjectReader reader(serialized.begin(), Unversioned()); reader.deserialize(t); ASSERT(t == id); } return Void(); } KeyRangeRef makeIdempotencySingleKeyRange(Arena& arena, Version version, uint8_t highOrderBatchIndex) { static const auto size = idempotencyIdKeys.begin.size() + sizeof(version) + sizeof(highOrderBatchIndex) + /*\x00*/ 1; StringRef second = makeString(size, arena); auto* dst = mutateString(second); memcpy(dst, idempotencyIdKeys.begin.begin(), idempotencyIdKeys.begin.size()); dst += idempotencyIdKeys.begin.size(); version = bigEndian64(version); memcpy(dst, &version, sizeof(version)); dst += sizeof(version); *dst++ = highOrderBatchIndex; *dst++ = 0; ASSERT_EQ(dst - second.begin(), size); return KeyRangeRef(second.removeSuffix("\x00"_sr), second); } void decodeIdempotencyKey(KeyRef key, Version& commitVersion, uint8_t& highOrderBatchIndex) { BinaryReader reader(key, Unversioned()); reader.readBytes(idempotencyIdKeys.begin.size()); reader >> commitVersion; commitVersion = bigEndian64(commitVersion); reader >> highOrderBatchIndex; }