Fix issues with snapshot generation for CHAMP (#4730)

This commit is contained in:
Julien Maffre 2022-12-14 17:30:24 +00:00 коммит произвёл GitHub
Родитель c3b910ff0c
Коммит 16a5defab0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 152 добавлений и 142 удалений

Просмотреть файл

@ -1,4 +1,4 @@
___ ___
(- *) (O o) | Y D O
(- *) (O o) | Y ^ O
( V ) < V > O +---'---'
/--x-m- /--m-m---xXx--/--yy---

Просмотреть файл

@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Fixed
- Session consistency is now provided even across elections. If session consistency would be broken, the inconsistent request will return an error and the TLS session will be terminated.
- Fixed issue where invalid snapshots could be generated depending on the pattern of additions/removals of keys in a given key-value map (#4730).
## [4.0.0-dev0]

Просмотреть файл

@ -271,6 +271,7 @@ namespace logger
static inline void default_init()
{
get_loggers().clear();
add_text_console_logger();
}
#endif

Просмотреть файл

@ -126,8 +126,10 @@ namespace champ
const auto& entry = bin[i];
if (k == entry->key)
{
const auto diff = map::get_serialized_size_with_padding(entry->key) +
map::get_serialized_size_with_padding(entry->value);
bin[i] = std::make_shared<Entry<K, V>>(k, v);
return map::get_size(k) + map::get_size(v);
return diff;
}
}
bin.push_back(std::make_shared<Entry<K, V>>(k, v));
@ -143,8 +145,8 @@ namespace champ
const auto& entry = bin[i];
if (k == entry->key)
{
const auto diff =
map::get_size(entry->key) + map::get_size(entry->value);
const auto diff = map::get_serialized_size_with_padding(entry->key) +
map::get_serialized_size_with_padding(entry->value);
bin.erase(bin.begin() + i);
return diff;
}
@ -211,6 +213,7 @@ namespace champ
return node_as<SubNodes<K, V, H>>(c_idx)->getp(depth + 1, hash, k);
}
// Returns serialised size of overwritten (k,v) if k exists, 0 otherwise
size_t put_mut(SmallIndex depth, Hash hash, const K& k, const V& v)
{
const auto idx = mask(hash, depth);
@ -246,8 +249,8 @@ namespace champ
const auto& entry0 = node_as<Entry<K, V>>(c_idx);
if (k == entry0->key)
{
auto current_size = map::get_size_with_padding(entry0->key) +
map::get_size_with_padding(entry0->value);
auto current_size = map::get_serialized_size_with_padding(entry0->key) +
map::get_serialized_size_with_padding(entry0->value);
nodes[c_idx] = std::make_shared<Entry<K, V>>(k, v);
return current_size;
}
@ -297,6 +300,7 @@ namespace champ
std::make_shared<SubNodes<K, V, H>>(std::move(node)), r);
}
// Returns serialised size of removed (k,v) if k exists, 0 otherwise
size_t remove_mut(SmallIndex depth, Hash hash, const K& k)
{
const auto idx = mask(hash, depth);
@ -311,8 +315,8 @@ namespace champ
if (entry->key != k)
return 0;
const auto diff = map::get_size_with_padding(entry->key) +
map::get_size_with_padding(entry->value);
const auto diff = map::get_serialized_size_with_padding(entry->key) +
map::get_serialized_size_with_padding(entry->value);
nodes.erase(nodes.begin() + c_idx);
data_map = data_map.clear(idx);
return diff;
@ -437,9 +441,10 @@ namespace champ
if (r.second == 0)
size_++;
const auto size_change =
(map::get_size_with_padding(key) + map::get_size_with_padding(value)) -
const auto size_change = (map::get_serialized_size_with_padding(key) +
map::get_serialized_size_with_padding(value)) -
r.second;
return Map(std::move(r.first), size_, size_change + serialized_size);
}
@ -492,17 +497,14 @@ namespace champ
{
std::vector<KVTuple> ordered_state;
ordered_state.reserve(map.size());
size_t size = 0;
size_t serialized_size = 0;
map.foreach([&](auto& key, auto& value) {
map.foreach([&ordered_state, &serialized_size](auto& key, auto& value) {
K* k = &key;
V* v = &value;
uint32_t ks = map::get_size(key);
uint32_t vs = map::get_size(value);
uint32_t key_size = ks + map::get_padding(ks);
uint32_t value_size = vs + map::get_padding(vs);
size += (key_size + value_size);
uint32_t key_size = map::get_serialized_size_with_padding(key);
uint32_t value_size = map::get_serialized_size_with_padding(value);
serialized_size += (key_size + value_size);
ordered_state.emplace_back(k, static_cast<Hash>(H()(key)), v);
@ -517,9 +519,10 @@ namespace champ
});
CCF_ASSERT_FMT(
size == map.get_serialized_size(),
"size:{}, map->size:{} ==> count:{}, vect:{}",
size,
serialized_size == map.get_serialized_size(),
"Serialized size:{}, map.get_serialized_size():{} (map count:{}, "
"ordered state count:{})",
serialized_size,
map.get_serialized_size(),
map.size(),
ordered_state.size());
@ -527,15 +530,19 @@ namespace champ
for (const auto& p : ordered_state)
{
// Serialize the key
uint32_t key_size = map::serialize(*p.k, data, size);
map::add_padding(key_size, data, size);
uint32_t key_size = map::serialize(*p.k, data, serialized_size);
map::add_padding(key_size, data, serialized_size);
// Serialize the value
uint32_t value_size = map::serialize(*p.v, data, size);
map::add_padding(value_size, data, size);
uint32_t value_size = map::serialize(*p.v, data, serialized_size);
map::add_padding(value_size, data, serialized_size);
}
CCF_ASSERT_FMT(size == 0, "buffer not filled, remaining:{}", size);
CCF_ASSERT_FMT(
serialized_size == 0,
"Serialization buffer is not complete, remaining:{}/{}",
serialized_size,
map.get_serialized_size());
}
};

Просмотреть файл

@ -38,7 +38,7 @@ namespace map
}
template <class T>
static size_t get_size_with_padding(const T& t)
static size_t get_serialized_size_with_padding(const T& t)
{
const uint32_t t_size = get_size(t);
return t_size + get_padding(t_size);

Просмотреть файл

@ -38,8 +38,8 @@ namespace rb
_rgt(rgt)
{
total_size = 1;
total_serialized_size =
map::get_size_with_padding(key) + map::get_size_with_padding(val);
total_serialized_size = map::get_serialized_size_with_padding(key) +
map::get_serialized_size_with_padding(val);
if (lft)
{
total_size += lft->size();

Просмотреть файл

@ -2,8 +2,12 @@
// Licensed under the Apache 2.0 License.
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "ccf/byte_vector.h"
#include "ccf/ds/logger.h"
#include "ccf/kv/serialisers/serialised_entry.h"
#include "ds/champ_map.h"
#include "ds/rb_map.h"
#include "ds/std_formatters.h"
#include "kv/untyped_change_set.h"
#include <doctest/doctest.h>
#include <random>
@ -18,8 +22,9 @@ struct CollisionHash
}
};
using K = uint64_t;
using V = uint64_t;
using K = kv::serialisers::SerialisedEntry;
using V = kv::serialisers::SerialisedEntry;
constexpr static size_t max_key_value_size = 128;
namespace map
{
@ -103,7 +108,7 @@ struct Put : public Op<M>
std::string str()
{
auto ss = std::stringstream();
ss << "Put(" << H<K>()(k) << ", " << v << ")";
ss << "Put(" << H<K>()(k) << ", value of size " << v.size() << ")";
return ss.str();
}
};
@ -157,8 +162,9 @@ std::vector<std::unique_ptr<Op<M>>> gen_ops(size_t n)
std::vector<std::unique_ptr<Op<M>>> ops;
std::vector<K> keys;
for (V v = 0; v < n; ++v)
for (size_t i = 0; i < n; ++i)
{
V v(gen() % max_key_value_size, 'v');
std::unique_ptr<Op<M>> op;
auto op_i = keys.empty() ? 0 : gen_op(gen);
switch (op_i)
@ -166,7 +172,7 @@ std::vector<std::unique_ptr<Op<M>>> gen_ops(size_t n)
case 0:
case 1: // insert
{
auto k = gen();
K k(gen() % max_key_value_size, 'k');
keys.push_back(k);
op = std::make_unique<Put<M>>(k, v);
@ -254,128 +260,75 @@ template <class M>
static const M gen_map(size_t size)
{
M map;
for (size_t i = 0; i < size; ++i)
Model model;
auto ops = gen_ops<M>(size);
for (auto& op : ops)
{
map = map.put(i, i);
auto r = op->apply(model, map);
map = r.second;
}
return map;
}
TEST_CASE_TEMPLATE("Snapshot map", M, RBMap, ChampMap)
TEST_CASE_TEMPLATE("Snapshot map", M, ChampMap, RBMap)
{
std::vector<KVPair> results;
uint32_t num_elements = 100;
auto map = gen_map<M>(num_elements);
size_t ops_count = 2048;
auto map = gen_map<M>(ops_count);
std::map<K, V> contents; // Ordered content as Champ is unordered
std::vector<uint8_t> serialised_snapshot;
INFO("Check initial content of map");
M new_map;
INFO("Record content of source map");
{
map.foreach([&results](const auto& key, const auto& value) {
results.push_back({key, value});
map.foreach([&contents](const auto& key, const auto& value) {
contents[key] = value;
return true;
});
REQUIRE_EQ(num_elements, results.size());
REQUIRE_EQ(map.size(), num_elements);
REQUIRE_EQ(map.size(), contents.size());
}
INFO("Populate second map and compare");
{
std::set<K> keys;
M new_map;
for (const auto& p : results)
{
REQUIRE_LT(p.k, num_elements);
keys.insert(p.k);
new_map = new_map.put(p.k, p.v);
}
REQUIRE_EQ(num_elements, new_map.size());
REQUIRE_EQ(num_elements, keys.size());
}
INFO("Serialize map to array");
INFO("Generate snapshot");
{
auto snapshot = map.make_snapshot();
std::vector<uint8_t> s(map.get_serialized_size());
snapshot->serialize(s.data());
serialised_snapshot.resize(map.get_serialized_size());
snapshot->serialize(serialised_snapshot.data());
auto new_map = map::deserialize_map<M>(s);
INFO("Ensure serialised state is byte identical");
{
auto snapshot_2 = map.make_snapshot();
std::vector<uint8_t> serialised_snapshot_2(map.get_serialized_size());
snapshot_2->serialize(serialised_snapshot_2.data());
REQUIRE_EQ(serialised_snapshot, serialised_snapshot_2);
}
}
std::set<K> keys;
new_map.foreach([&keys](const auto& key, const auto& value) {
keys.insert(key);
REQUIRE_EQ(key, value);
return true;
});
INFO("Apply snapshot to target map");
{
new_map = map::deserialize_map<M>(serialised_snapshot);
REQUIRE_EQ(map.size(), new_map.size());
REQUIRE_EQ(map.size(), keys.size());
// Check that new entries can be added to deserialised map
uint32_t offset = 1000;
for (uint32_t i = offset; i < offset + num_elements; ++i)
std::map<K, V> new_contents;
new_map.foreach([&new_contents](const auto& key, const auto& value) {
new_contents[key] = value;
return true;
});
REQUIRE_EQ(contents.size(), new_contents.size());
REQUIRE_EQ(contents, new_contents);
}
INFO("Check that new entries can be added to target map");
{
Model new_model;
auto new_ops = gen_ops<M>(ops_count);
for (auto& op : new_ops)
{
new_map = new_map.put(i, i);
}
REQUIRE_EQ(new_map.size(), map.size() + num_elements);
for (uint32_t i = offset; i < offset + num_elements; ++i)
{
auto p = new_map.get(i);
REQUIRE(p.has_value());
REQUIRE(p.value() == i);
auto r = op->apply(new_model, new_map);
new_map = r.second;
}
}
INFO("Ensure serialized state is byte identical");
{
auto snapshot_1 = map.make_snapshot();
std::vector<uint8_t> s_1(map.get_serialized_size());
snapshot_1->serialize(s_1.data());
auto snapshot_2 = map.make_snapshot();
std::vector<uint8_t> s_2(map.get_serialized_size());
snapshot_2->serialize(s_2.data());
REQUIRE_EQ(s_1, s_2);
}
INFO("Snapshot is immutable");
{
size_t current_size = map.size();
auto snapshot = map.make_snapshot();
std::vector<uint8_t> s_1(map.get_serialized_size());
snapshot->serialize(s_1.data());
// Add entry in map
auto key = current_size + 1;
REQUIRE(map.get(key) == std::nullopt);
map = map.put(key, key);
// Even though map has been updated, snapshot is not modified
std::vector<uint8_t> s_2(s_1.size());
snapshot->serialize(s_2.data());
REQUIRE_EQ(s_1, s_2);
}
}
using SerialisedKey = ccf::ByteVector;
using SerialisedValue = ccf::ByteVector;
TEST_CASE_TEMPLATE(
"Serialize map with different key sizes",
M,
UntypedChampMap<SerialisedKey, SerialisedValue>,
UntypedRBMap<SerialisedKey, SerialisedValue>)
{
M map;
SerialisedKey key(16);
SerialisedValue long_key(128);
SerialisedValue value(8);
SerialisedValue long_value(256);
map = map.put(key, value);
map = map.put(long_key, long_value);
auto snapshot = map.make_snapshot();
std::vector<uint8_t> s(map.get_serialized_size());
snapshot->serialize(s.data());
}
template <typename M>
@ -390,6 +343,55 @@ std::map<K, V> get_all_entries(const M& map)
return entries;
}
TEST_CASE_TEMPLATE("Snapshot is immutable", M, ChampMap, RBMap)
{
size_t ops_count = 2048;
auto map = gen_map<M>(ops_count);
// Take snapshot at original state
auto snapshot = map.make_snapshot();
size_t serialised_snapshot_before_size = snapshot->get_serialized_size();
std::vector<uint8_t> serialised_snapshot_before(
serialised_snapshot_before_size);
snapshot->serialize(serialised_snapshot_before.data());
INFO("Meanwhile, modify map");
{
// Remove operation is not yet implemented for RBMap
if constexpr (std::is_same_v<M, ChampMap>)
{
auto all_entries = get_all_entries(map);
auto& key_to_remove = all_entries.begin()->first;
map = map.remove(key_to_remove);
}
// Modify existing key with value that must be different from what `gen_map`
// populated the map with
auto all_entries = get_all_entries(map);
auto& key_to_add = all_entries.begin()->first;
map = map.put(key_to_add, V(max_key_value_size * 2, 'x'));
}
INFO("Even though map has been updated, original snapshot is not modified");
{
REQUIRE_EQ(
snapshot->get_serialized_size(), serialised_snapshot_before_size);
std::vector<uint8_t> serialised_snapshot_after(
serialised_snapshot_before_size);
snapshot->serialize(serialised_snapshot_after.data());
REQUIRE_EQ(serialised_snapshot_before, serialised_snapshot_after);
}
INFO("But new snapshot is different");
{
auto new_snapshot = map.make_snapshot();
size_t serialised_snapshot_new_size = new_snapshot->get_serialized_size();
std::vector<uint8_t> serialised_snapshot_new(serialised_snapshot_new_size);
new_snapshot->serialize(serialised_snapshot_new.data());
REQUIRE_NE(serialised_snapshot_before, serialised_snapshot_new);
}
}
template <class S, class T>
void verify_snapshot_compatibility(const S& source_map, T& target_map)
{
@ -432,11 +434,11 @@ void forall_threshold(const M& map, size_t threshold)
{
size_t iterations_count = 0;
map.foreach([&iterations_count, threshold](const K& k, const V& v) {
iterations_count++;
if (iterations_count >= threshold)
{
return false;
}
iterations_count++;
return true;
});
REQUIRE(iterations_count == threshold);
@ -445,8 +447,8 @@ void forall_threshold(const M& map, size_t threshold)
TEST_CASE_TEMPLATE("Foreach", M, RBMap, ChampMap)
{
size_t size = 100;
size_t threshold = size / 2;
auto map = gen_map<M>(size);
size_t threshold = map.size() / 2;
forall_threshold(map, threshold);
}

Просмотреть файл

@ -170,7 +170,7 @@ TEST_CASE("Old snapshots" * doctest::test_suite("snapshot"))
// Test that this code can still parse snapshots produced by old versions of
// the code
// NB: These raw strings are base64 encodings from
// `sencond_serialised_snapshot` in the "Simple snapshot" test
// `second_serialised_snapshot` in the "Simple snapshot" test
std::string raw_snapshot_b64;
SUBCASE("Tombstone deletions")
{

Просмотреть файл

@ -313,6 +313,7 @@ namespace kv::untyped
void serialise(KvStoreSerialiser& s) override
{
LOG_TRACE_FMT("Serialising snapshot for map: {}", name);
s.start_map(name, security_domain);
s.serialise_entry_version(version);

Просмотреть файл

@ -339,9 +339,7 @@ def test_expired_certs(network, args):
stack.enter_context(network.partitioner.partition([primary]))
# Restore connectivity between backups and wait for election
network.wait_for_primary_unanimity(
nodes=[backup_a, backup_b], min_view=r.view
)
network.wait_for_primary_unanimity(nodes=[backup_a, backup_b], min_view=r.view)
# Should now be able to make progress
check_can_progress(backup_a)