diff --git a/contrib/TestHarness/Program.cs b/contrib/TestHarness/Program.cs index 93b14d176c..8d58d8fe2a 100644 --- a/contrib/TestHarness/Program.cs +++ b/contrib/TestHarness/Program.cs @@ -261,7 +261,7 @@ namespace SummarizeTest testFile = random.Choice(uniqueFiles); string oldBinaryVersionLowerBound = "0.0.0"; string lastFolderName = Path.GetFileName(Path.GetDirectoryName(testFile)); - if (lastFolderName.Contains("from_")) // Only perform upgrade tests from certain versions + if (lastFolderName.Contains("from_") || lastFolderName.Contains("to_")) // Only perform upgrade/downgrade tests from certain versions { oldBinaryVersionLowerBound = lastFolderName.Split('_').Last(); } @@ -295,14 +295,17 @@ namespace SummarizeTest if (testDir.EndsWith("restarting")) { + bool isDowngrade = Path.GetFileName(Path.GetDirectoryName(testFile)).Contains("to_"); + string firstServerName = isDowngrade ? fdbserverName : oldServerName; + string secondServerName = isDowngrade ? oldServerName : fdbserverName; int expectedUnseed = -1; int unseed; string uid = Guid.NewGuid().ToString(); - bool useNewPlugin = oldServerName == fdbserverName || versionGreaterThanOrEqual(oldServerName.Split('-').Last(), "5.2.0"); - result = RunTest(oldServerName, useNewPlugin ? tlsPluginFile : tlsPluginFile_5_1, summaryFileName, errorFileName, seed, buggify, testFile + "-1.txt", runDir, uid, expectedUnseed, out unseed, out retryableError, logOnRetryableError, useValgrind, false, true, oldServerName, traceToStdout); + bool useNewPlugin = (oldServerName == fdbserverName) || versionGreaterThanOrEqual(oldServerName.Split('-').Last(), "5.2.0"); + result = RunTest(firstServerName, useNewPlugin ? tlsPluginFile : tlsPluginFile_5_1, summaryFileName, errorFileName, seed, buggify, testFile + "-1.txt", runDir, uid, expectedUnseed, out unseed, out retryableError, logOnRetryableError, useValgrind, false, true, oldServerName, traceToStdout); if (result == 0) { - result = RunTest(fdbserverName, tlsPluginFile, summaryFileName, errorFileName, seed+1, buggify, testFile + "-2.txt", runDir, uid, expectedUnseed, out unseed, out retryableError, logOnRetryableError, useValgrind, true, false, oldServerName, traceToStdout); + result = RunTest(secondServerName, tlsPluginFile, summaryFileName, errorFileName, seed+1, buggify, testFile + "-2.txt", runDir, uid, expectedUnseed, out unseed, out retryableError, logOnRetryableError, useValgrind, true, false, oldServerName, traceToStdout); } } else diff --git a/fdbserver/CMakeLists.txt b/fdbserver/CMakeLists.txt index dcf6b04d86..f6a4017ff8 100644 --- a/fdbserver/CMakeLists.txt +++ b/fdbserver/CMakeLists.txt @@ -139,6 +139,7 @@ set(FDBSERVER_SRCS workloads/DDMetricsExclude.actor.cpp workloads/DiskDurability.actor.cpp workloads/DiskDurabilityTest.actor.cpp + workloads/Downgrade.actor.cpp workloads/DummyWorkload.actor.cpp workloads/ExternalWorkload.actor.cpp workloads/FastTriggeredWatches.actor.cpp diff --git a/fdbserver/workloads/Downgrade.actor.cpp b/fdbserver/workloads/Downgrade.actor.cpp new file mode 100644 index 0000000000..4ec5704f6f --- /dev/null +++ b/fdbserver/workloads/Downgrade.actor.cpp @@ -0,0 +1,170 @@ +/* + * Downgrade.actor.cpp + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2013-2020 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/NativeAPI.actor.h" +#include "fdbserver/TesterInterface.actor.h" +#include "fdbserver/workloads/workloads.actor.h" +#include "flow/serialize.h" +#include "flow/actorcompiler.h" // This must be the last #include. + +struct DowngradeWorkload : TestWorkload { + + static constexpr const char* NAME = "Downgrade"; + Key oldKey, newKey; + int numObjects; + + DowngradeWorkload(WorkloadContext const& wcx) : TestWorkload(wcx) { + oldKey = getOption(options, LiteralStringRef("oldKey"), LiteralStringRef("oldKey")); + newKey = getOption(options, LiteralStringRef("newKey"), LiteralStringRef("newKey")); + numObjects = getOption(options, LiteralStringRef("numOptions"), deterministicRandom()->randomInt(0,100)); + } + + struct _Struct { + static constexpr FileIdentifier file_identifier = 2340487; + int oldField = 0; + }; + + struct OldStruct : public _Struct { + void setFields() { oldField = 1; } + bool isSet() const { return oldField == 1; } + + template + void serialize(Archive& ar) { + serializer(ar, oldField); + } + }; + + struct NewStruct : public _Struct { + int newField = 0; + + bool isSet() const { + return oldField == 1 && newField == 2; + } + void setFields() { + oldField = 1; + newField = 2; + } + + template + void serialize(Archive& ar) { + serializer(ar, oldField, newField); + } + }; + + ACTOR static Future writeOld(Database cx, int numObjects, Key key) { + BinaryWriter writer(IncludeVersion(currentProtocolVersion)); + std::vector data(numObjects); + for (auto& oldObject : data) { + oldObject.setFields(); + } + writer << data; + state Value value = writer.toValue(); + + state Transaction tr(cx); + loop { + try { + tr.set(key, value); + wait(tr.commit()); + return Void(); + } catch (Error& e) { + wait(tr.onError(e)); + } + } + } + + ACTOR static Future writeNew(Database cx, int numObjects, Key key) { + ProtocolVersion protocolVersion = currentProtocolVersion; + protocolVersion.addObjectSerializerFlag(); + ObjectWriter writer(IncludeVersion(protocolVersion)); + std::vector data(numObjects); + for (auto& newObject : data) { + newObject.setFields(); + } + writer.serialize(data); + state Value value = writer.toStringRef(); + + state Transaction tr(cx); + loop { + try { + tr.set(key, value); + wait(tr.commit()); + return Void(); + } catch (Error& e) { + wait(tr.onError(e)); + } + } + } + + ACTOR static Future readData(Database cx, int numObjects, Key key) { + state Transaction tr(cx); + state Value value; + + loop { + try { + Optional _value = wait(tr.get(key)); + ASSERT(_value.present()); + value = _value.get(); + break; + } catch (Error& e) { + wait(tr.onError(e)); + } + } + + { + // use BinaryReader + BinaryReader reader(value, IncludeVersion()); + std::vector data; + reader >> data; + ASSERT(data.size() == numObjects); + for (const auto& oldObject : data) { + ASSERT(oldObject.isSet()); + } + } + { + // use ArenaReader + ArenaReader reader(Arena(), value, IncludeVersion()); + std::vector data; + reader >> data; + ASSERT(data.size() == numObjects); + for (const auto& oldObject : data) { + ASSERT(oldObject.isSet()); + } + } + return Void(); + } + + std::string description() override { return NAME; } + + Future setup(Database const& cx) override { + return clientId ? Void() : (writeOld(cx, numObjects, oldKey) && writeNew(cx, numObjects, newKey)); + } + + Future start(Database const& cx) override { + return clientId ? Void() : (readData(cx, numObjects, oldKey) && readData(cx, numObjects, newKey)); + } + + Future check(Database const& cx) override { + // Failures are checked with assertions + return true; + } + void getMetrics(vector& m) override {} +}; + +WorkloadFactory DowngradeWorkloadFactory(DowngradeWorkload::NAME); diff --git a/flow/ObjectSerializer.h b/flow/ObjectSerializer.h index fbeee1e67d..e9326e61ad 100644 --- a/flow/ObjectSerializer.h +++ b/flow/ObjectSerializer.h @@ -68,6 +68,7 @@ struct SaveContext { template class _ObjectReader { +protected: ProtocolVersion mProtocolVersion; public: @@ -79,8 +80,19 @@ public: const uint8_t* data = static_cast(this)->data(); LoadContext context(static_cast(this)); if(read_file_identifier(data) != file_identifier) { - TraceEvent(SevError, "MismatchedFileIdentifier").detail("Expected", file_identifier).detail("Read", read_file_identifier(data)); - ASSERT(false); + // Some file identifiers are changed in 7.0, so file identifier mismatches + // are expected during a downgrade from 7.0 to 6.3 + bool expectMismatch = mProtocolVersion >= ProtocolVersion(0x0FDB00B070000000LL); + { + TraceEvent te(expectMismatch ? SevInfo : SevError, "MismatchedFileIdentifier"); + if (expectMismatch) { + te.suppressFor(1.0); + } + te.detail("Expected", file_identifier).detail("Read", read_file_identifier(data)); + } + if (!expectMismatch) { + ASSERT(false); + } } load_members(data, context, items...); } diff --git a/flow/ProtocolVersion.h b/flow/ProtocolVersion.h index b63c91561f..90d046f150 100644 --- a/flow/ProtocolVersion.h +++ b/flow/ProtocolVersion.h @@ -135,3 +135,6 @@ constexpr ProtocolVersion currentProtocolVersion(0x0FDB00B063010001LL); // This assert is intended to help prevent incrementing the leftmost digits accidentally. It will probably need to // change when we reach version 10. static_assert(currentProtocolVersion.version() < 0x0FDB00B100000000LL, "Unexpected protocol version"); + +// Downgrades are only supported for one minor version +constexpr ProtocolVersion minInvalidProtocolVersion(0x0FDB00B071000000LL); diff --git a/flow/serialize.h b/flow/serialize.h index 34a2c95b4e..07da973943 100644 --- a/flow/serialize.h +++ b/flow/serialize.h @@ -78,7 +78,7 @@ inline typename Archive::WRITER& operator << (Archive& ar, const Item& item ) { template inline typename Archive::READER& operator >> (Archive& ar, Item& item ) { - load(ar, item); + ar.deserialize(item); return ar; } @@ -286,10 +286,8 @@ struct _IncludeVersion { TraceEvent(SevWarnAlways, "InvalidSerializationVersion").error(err).detailf("Version", "%llx", v.versionWithFlags()); throw err; } - if (v > currentProtocolVersion) { - // For now, no forward compatibility whatsoever is supported. In the future, this check may be weakened for - // particular data structures (e.g. to support mismatches between client and server versions when the client - // must deserialize zookeeper and database structures) + if (v >= minInvalidProtocolVersion) { + // Downgrades are only supported for one minor version auto err = incompatible_protocol_version(); TraceEvent(SevError, "FutureProtocolVersion").error(err).detailf("Version", "%llx", v.versionWithFlags()); throw err; @@ -520,51 +518,28 @@ private: } }; - -class ArenaReader { +template +class _Reader { public: static const int isDeserializing = 1; static constexpr bool isSerializing = false; - typedef ArenaReader READER; + using READER = Impl; - const void* readBytes( int bytes ) { - const char* b = begin; - const char* e = b + bytes; - ASSERT( e <= end ); - begin = e; - return b; - } - - const void* peekBytes( int bytes ) { - ASSERT( begin + bytes <= end ); + const void *peekBytes(int bytes) { + ASSERT(begin + bytes <= end); return begin; } - void serializeBytes(void* data, int bytes) { - memcpy(data, readBytes(bytes), bytes); - } - - const uint8_t* arenaRead( int bytes ) { - return (const uint8_t*)readBytes(bytes); - } - - StringRef arenaReadAll() const { - return StringRef(reinterpret_cast(begin), end - begin); + void serializeBytes(void *data, int bytes) { + memcpy(data, static_cast(this)->readBytes(bytes), bytes); } template void serializeBinaryItem( T& t ) { - t = *(T*)readBytes(sizeof(T)); + t = *(T*)(static_cast(this)->readBytes(sizeof(T))); } - template - ArenaReader( Arena const& arena, const StringRef& input, VersionOptions vo ) : m_pool(arena), check(NULL) { - begin = (const char*)input.begin(); - end = begin + input.size(); - vo.read(*this); - } - - Arena& arena() { return m_pool; } + Arena &arena() { return m_pool; } ProtocolVersion protocolVersion() const { return m_protocolVersion; } void setProtocolVersion(ProtocolVersion pv) { m_protocolVersion = pv; } @@ -575,96 +550,129 @@ public: check = begin; } - void rewind() { - ASSERT(check != NULL); - begin = check; - check = NULL; - } - -private: - const char *begin, *end, *check; - Arena m_pool; - ProtocolVersion m_protocolVersion; -}; - -class BinaryReader { -public: - static const int isDeserializing = 1; - static constexpr bool isSerializing = false; - typedef BinaryReader READER; - - const void* readBytes( int bytes ); - - const void* peekBytes( int bytes ) { - ASSERT( begin + bytes <= end ); - return begin; - } - - void serializeBytes(void* data, int bytes) { - memcpy(data, readBytes(bytes), bytes); - } - - template - void serializeBinaryItem( T& t ) { - t = *(T*)readBytes(sizeof(T)); - } - - const uint8_t* arenaRead( int bytes ) { - // Reads and returns the next bytes. - // The returned pointer has the lifetime of this.arena() - // Could be implemented zero-copy if [begin,end) was in this.arena() already; for now is a copy - if (!bytes) return NULL; - uint8_t* dat = new (arena()) uint8_t[ bytes ]; - serializeBytes( dat, bytes ); - return dat; - } - - template - BinaryReader( const void* data, int length, VersionOptions vo ) { - begin = (const char*)data; - end = begin + length; - check = nullptr; - vo.read(*this); - } - template - BinaryReader( const StringRef& s, VersionOptions vo ) { begin = (const char*)s.begin(); end = begin + s.size(); vo.read(*this); } - template - BinaryReader( const std::string& v, VersionOptions vo ) { begin = v.c_str(); end = begin + v.size(); vo.read(*this); } - - Arena& arena() { return m_pool; } - - template - static T fromStringRef( StringRef sr, VersionOptions vo ) { - T t; - BinaryReader r(sr, vo); - r >> t; - return t; - } - - ProtocolVersion protocolVersion() const { return m_protocolVersion; } - void setProtocolVersion(ProtocolVersion pv) { m_protocolVersion = pv; } - - void assertEnd() { ASSERT( begin == end ); } - - bool empty() const { return begin == end; } - - void checkpoint() { - check = begin; - } - void rewind() { ASSERT(check != nullptr); begin = check; check = nullptr; } +protected: + _Reader(const char* begin, const char* end) : begin(begin), end(end) {} + _Reader(const char* begin, const char* end, const Arena& arena) : begin(begin), end(end), m_pool(arena) {} -private: - const char *begin, *end, *check; + const char *begin, *end; + const char* check = nullptr; Arena m_pool; ProtocolVersion m_protocolVersion; }; +class ArenaReader : public _Reader { + Optional arenaObjectReader; + +public: + const void* readBytes( int bytes ) { + const char* b = begin; + const char* e = b + bytes; + ASSERT( e <= end ); + begin = e; + return b; + } + + const uint8_t* arenaRead( int bytes ) { + return (const uint8_t*)readBytes(bytes); + } + + StringRef arenaReadAll() const { + return StringRef(reinterpret_cast(begin), end - begin); + } + + template + ArenaReader(Arena const& arena, const StringRef& input, VersionOptions vo) + : _Reader(reinterpret_cast(input.begin()), reinterpret_cast(input.end()), arena) { + vo.read(*this); + if (m_protocolVersion.hasObjectSerializerFlag()) { + arenaObjectReader = ArenaObjectReader(arena, input, vo); + } + } + + template + void deserialize(T& t) { + if constexpr (HasFileIdentifier::value) { + if (arenaObjectReader.present()) { + arenaObjectReader.get().deserialize(t); + } else { + load(*this, t); + } + } else { + load(*this, t); + } + } +}; + +class BinaryReader : public _Reader { + Optional objectReader; + +public: + const void* readBytes( int bytes ); + + const uint8_t* arenaRead( int bytes ) { + // Reads and returns the next bytes. + // The returned pointer has the lifetime of this.arena() + // Could be implemented zero-copy if [begin,end) was in this.arena() already; for now is a copy + if (!bytes) return nullptr; + uint8_t* dat = new (arena()) uint8_t[ bytes ]; + serializeBytes( dat, bytes ); + return dat; + } + + template + static T fromStringRef( StringRef sr, VersionOptions vo ) { + T t; + BinaryReader r(sr, vo); + r >> t; + return t; + } + + void assertEnd() { ASSERT(begin == end); } + + template + BinaryReader(const void* data, int length, VersionOptions vo) + : _Reader(reinterpret_cast(data), reinterpret_cast(data) + length) { + readVersion(vo); + } + template + BinaryReader(const StringRef& s, VersionOptions vo) + : _Reader(reinterpret_cast(s.begin()), reinterpret_cast(s.end())) { + readVersion(vo); + } + template + BinaryReader(const std::string& s, VersionOptions vo) : _Reader(s.c_str(), s.c_str() + s.size()) { + readVersion(vo); + } + + template + void deserialize(T &t) { + if constexpr (HasFileIdentifier::value) { + if (objectReader.present()) { + objectReader.get().deserialize(t); + } else { + load(*this, t); + } + } else { + load(*this, t); + } + } + +private: + template + void readVersion(VersionOptions vo) { + vo.read(*this); + if (m_protocolVersion.hasObjectSerializerFlag()) { + objectReader = ObjectReader(reinterpret_cast(begin), AssumeVersion(m_protocolVersion)); + } + } +}; + struct SendBuffer { uint8_t const* data; SendBuffer* next; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7d2ac821b9..124c724093 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -151,6 +151,7 @@ if(WITH_PYTHON) add_fdb_test(TEST_FILES rare/ConflictRangeRYOWCheck.txt) add_fdb_test(TEST_FILES rare/CycleRollbackClogged.txt) add_fdb_test(TEST_FILES rare/CycleWithKills.txt) + add_fdb_test(TEST_FILES rare/Downgrade.txt) add_fdb_test(TEST_FILES rare/FuzzTest.txt) add_fdb_test(TEST_FILES rare/InventoryTestHeavyWrites.txt) add_fdb_test(TEST_FILES rare/LargeApiCorrectness.txt) @@ -189,6 +190,9 @@ if(WITH_PYTHON) add_fdb_test( TEST_FILES restarting/from_5.2.0/ClientTransactionProfilingCorrectness-1.txt restarting/from_5.2.0/ClientTransactionProfilingCorrectness-2.txt) + add_fdb_test( + TEST_FILES restarting/to_6.3.5/CycleTestRestart-1.txt + restarting/to_6.3.5/CycleTestRestart-2.txt) add_fdb_test(TEST_FILES slow/ApiCorrectness.txt) add_fdb_test(TEST_FILES slow/ApiCorrectnessAtomicRestore.txt) add_fdb_test(TEST_FILES slow/ApiCorrectnessSwitchover.txt) diff --git a/tests/rare/Downgrade.txt b/tests/rare/Downgrade.txt new file mode 100644 index 0000000000..9dfe605de4 --- /dev/null +++ b/tests/rare/Downgrade.txt @@ -0,0 +1,4 @@ +testTitle=Downgrade + testName=Downgrade + oldKey=oldKey + newKey=newKey diff --git a/tests/restarting/to_6.3.5/CycleTestRestart-1.txt b/tests/restarting/to_6.3.5/CycleTestRestart-1.txt new file mode 100644 index 0000000000..647c2f3fe3 --- /dev/null +++ b/tests/restarting/to_6.3.5/CycleTestRestart-1.txt @@ -0,0 +1,30 @@ +testTitle=Clogged + clearAfterTest=false + testName=Cycle + transactionsPerSecond=500.0 + nodeCount=2500 + testDuration=10.0 + expectedRate=0 + + testName=RandomClogging + testDuration=10.0 + + testName=Rollback + meanDelay=10.0 + testDuration=10.0 + + testName=Attrition + machinesToKill=10 + machinesToLeave=3 + reboot=true + testDuration=10.0 + + testName=Attrition + machinesToKill=10 + machinesToLeave=3 + reboot=true + testDuration=10.0 + + testName=SaveAndKill + restartInfoLocation=simfdb/restartInfo.ini + testDuration=10.0 diff --git a/tests/restarting/to_6.3.5/CycleTestRestart-2.txt b/tests/restarting/to_6.3.5/CycleTestRestart-2.txt new file mode 100644 index 0000000000..7d498f2be1 --- /dev/null +++ b/tests/restarting/to_6.3.5/CycleTestRestart-2.txt @@ -0,0 +1,26 @@ +testTitle=Clogged + runSetup=false + testName=Cycle + transactionsPerSecond=2500.0 + nodeCount=2500 + testDuration=10.0 + expectedRate=0 + + testName=RandomClogging + testDuration=10.0 + + testName=Rollback + meanDelay=10.0 + testDuration=10.0 + + testName=Attrition + machinesToKill=10 + machinesToLeave=3 + reboot=true + testDuration=10.0 + + testName=Attrition + machinesToKill=10 + machinesToLeave=3 + reboot=true + testDuration=10.0