Compare commits

...

135 Commits

Author SHA1 Message Date
Tyler Neely
aff9c47f04 Add simple property test example 2023-04-14 14:54:56 +00:00
Tyler Neely
e2e5a6032c Split cluster property test file into bulk_load.cpp and random_workload.cpp 2023-02-24 11:18:39 +00:00
Tyler Neely
f2e52afbeb Add TODOs related to distributed retry limits in the RequestRouter 2023-02-24 10:48:39 +00:00
Tyler Neely
864e77cbeb Clean up RequestRouter log messages 2023-02-24 10:45:56 +00:00
Tyler Neely
ae4b41cc37 Check-in bulk-load property test 2023-02-24 10:43:52 +00:00
Tyler Neely
d9e7685e05 Clean up test_cluster.hpp 2023-02-24 10:42:56 +00:00
Tyler Neely
df062e82c3 Clean up ShardWorker log messages 2023-02-24 10:42:35 +00:00
Tyler Neely
c13a406b25 Clean up log statements in the ShardRsm 2023-02-24 10:40:20 +00:00
Tyler Neely
ae81a77550 Clean-up log messages more 2023-02-24 10:37:41 +00:00
Tyler Neely
f3dc4c15c0 Clean up log messages 2023-02-24 10:29:44 +00:00
Tyler Neely
333f94ee85 Make shard split handling fully deterministic under testing 2023-02-24 09:40:47 +00:00
Tyler Neely
dedd959329 Improve determinism of Shutdown logic 2023-02-23 17:59:39 +00:00
Tyler Neely
3f28414be8 Fix rare race condition related to notification and polling of already-completed requests 2023-02-23 17:28:40 +00:00
Tyler Neely
84526cbf8e Add idempotency tokens to Vertex deltas, and make multiple requests that perform the same insert idempotent 2023-02-23 11:59:11 +00:00
Tyler Neely
13041aa069 Properly rename shards_map_ to shard_map_ 2023-02-23 11:57:41 +00:00
Tyler Neely
1816514bee Merge branch 'project-pineapples' of github.com:memgraph/memgraph into T-MG-split-shard 2023-02-22 09:35:48 +00:00
Tyler Neely
f68f0a525a Check-in some temporary debug statements to make conflict resolution lower friction 2023-02-22 09:32:29 +00:00
Tyler Neely
4fb4fc86b7 Handle a race condition when initializing a shard for the first time 2023-02-21 16:28:07 +00:00
Tyler Neely
3d2f9fdcbb Add retry logic for all remaining requests in the RequestRouter 2023-02-21 15:43:54 +00:00
Tyler Neely
a471d5d153 Handle several types of retries in the RequestRouter in response to expected timeouts 2023-02-20 16:48:24 +00:00
Tyler Neely
17da6cb701 Have the MATCH property test compare a std::set of values rather than the non-deduplicated vector of them 2023-02-20 16:22:37 +00:00
Tyler Neely
892632f35f During transaction Commit, don't assert that the transaction is not yet committed, because the request may retry 2023-02-20 12:37:00 +00:00
Tyler Neely
04e3c64851 Retry Commit requests from the RequestRouter when timeouts happen 2023-02-20 12:25:51 +00:00
Tyler Neely
778da8d324 If message conversion attempts for the ShardManager fail, drop the message because it probably relates to a timed out request 2023-02-20 10:12:02 +00:00
Tyler Neely
7c13bd8770 Start performing timeouts in the simulated cluster property test so that we can make progress when benign race conditions happen related to the shard map containing shards that don't exist yet 2023-02-17 18:02:11 +00:00
Tyler Neely
b8cadecaca Add new error type for stale ShardMap's for the Shard to signal that the requestor should retry with a new ShardMap 2023-02-17 15:02:32 +00:00
Tyler Neely
d30098fa75 Fix a variety of split-related bugs 2023-02-17 13:52:15 +00:00
Tyler Neely
6112390aec Fix a variety of split-related bugs 2023-02-17 13:00:02 +00:00
Tyler Neely
49ee735c43 Clean up tests and general pathway for split shards 2023-02-13 17:06:13 +00:00
Tyler Neely
5813186cdd Check-in minor debugging output for the next batch of race condition fixes 2023-02-10 01:11:18 +00:00
Tyler Neely
50d94826d8 High-level split path is now working as long as clients don't split half-way through 2023-02-10 01:10:16 +00:00
Tyler Neely
3f58c2a0cc Override split test to just do a single split for the time being 2023-02-10 00:04:14 +00:00
Tyler Neely
a01fa717f6 Clear passing assert canaries from split path 2023-02-09 23:40:30 +00:00
Tyler Neely
937b4e3118 Fix message type bug with splits 2023-02-09 23:36:20 +00:00
Tyler Neely
77e8757fea Remove several canary assertions that successfully are reached 2023-02-09 23:10:06 +00:00
Tyler Neely
5fb6338603 Fix a few more split bugs 2023-02-09 22:57:01 +00:00
Tyler Neely
62321e4ee9 Fix bug with Hlc operator++ implementation 2023-02-09 22:44:18 +00:00
Tyler Neely
bb03b18655 Fix a few bugs with splits 2023-02-09 22:16:38 +00:00
Tyler Neely
3281a1d68e Fix raft bug introduced a few patches back 2023-02-09 21:17:45 +00:00
Tyler Neely
79b5cc8098 Check-in in-progress coordinator refactor 2023-02-09 20:48:46 +00:00
Tyler Neely
6534b19479 Remove dead code from Coordinator related to splits 2023-02-08 22:40:02 +00:00
Tyler Neely
42bd711598 Refactor a number of request-related codepaths. Remove split file handling of splits. Add a number of intentionally failing asserts to track the path through the splits to make it faster to debug 2023-02-08 22:36:47 +00:00
Tyler Neely
b56baef3c1 Fix bug with UUID for splitting shards not being correctly sent to the Coordinator 2023-02-08 21:02:57 +00:00
Tyler Neely
315750903e Remove pre-splitting of simulations. Improve debug info while threading split logic through the system 2023-02-08 18:22:03 +00:00
Tyler Neely
54e69dbc2c Complete implementation of the split detection test 2023-02-08 17:18:18 +00:00
Tyler Neely
b152af1eb8 Remove the cmake configuration for the cypher query test that was merged into the main one 2023-02-08 17:06:33 +00:00
Tyler Neely
43be947007 Merge the cypher property test into the main cluster property test and try to return TypedValue objects when executing each query 2023-02-08 17:03:45 +00:00
jbajic
1e176da4b2 Merge branch 'T1149-MG-split-shard' into T-MG-split-shard 2023-02-08 16:00:38 +01:00
jbajic
177de2fa2e Fix benchmark test 2023-02-08 15:24:23 +01:00
jbajic
0f4e6d1b3b Fix shard split test 2023-02-08 12:52:41 +01:00
jbajic
99094b0a72 Merge branch 'T1149-MG-split-shard' into T-MG-split-shard 2023-02-08 11:54:22 +01:00
Tyler Neely
03f4684227 Check-in testing changes for the splits 2023-02-08 08:50:49 +00:00
jbajic
e6c80e7dc9 Fix when delta chain breaks 2023-02-07 22:56:10 +01:00
Tyler Neely
7988f4fe88 Thread the split threshold completely into the shard's Config 2023-02-07 18:56:52 +00:00
Tyler Neely
3064d779ec Generate shard split threshold and thread it into the coordinator's ShardMap 2023-02-07 18:03:14 +00:00
Tyler Neely
c13c46829e Merge branch 'project-pineapples' of github.com:memgraph/memgraph into T-MG-split-shard 2023-02-07 17:37:41 +00:00
Tyler Neely
97bc2030b0 Add new AssertShardsSplit simulator op 2023-02-07 16:08:47 +00:00
jbajic
5ee920eaf4 Add edges as parameter to benchmark tests 2023-02-07 15:32:08 +01:00
Jure Bajic
a71447e6a0 Update src/storage/v3/shard.cpp
Co-authored-by: Tyler Neely <tyler.neely@memgraph.io>
2023-02-07 14:12:21 +01:00
jbajic
dc48b4ae9b Fix edge in vertex_Edge 2023-02-07 13:31:05 +01:00
jbajic
82c7c85428 Add additional tests 2023-02-07 11:18:25 +01:00
jbajic
de15c9719c Fix delta chain prev pointer 2023-02-07 10:44:51 +01:00
jbajic
954df64d1d Add detla pruning 2023-02-03 13:42:38 +01:00
jbajic
90bcdc4e2b Rename uuid to id 2023-02-03 13:42:28 +01:00
jbajic
a95bac65c6 Extract logic of checking delta skips 2023-02-02 16:39:00 +01:00
jbajic
4619a87e98 Only follow head of chain 2023-02-02 16:07:16 +01:00
jbajic
bf44618b7a Compare prev ptr as well 2023-02-02 15:50:03 +01:00
jbajic
8f660b0d44 Add noexcept to hlc 2023-02-02 15:05:04 +01:00
jbajic
852e98fb51 Fix split logic 2023-02-02 14:00:08 +01:00
jbajic
6e9b057237 Enforce copyable and moveable on message 2023-02-02 11:46:15 +01:00
jbajic
c9290777f5 Fix delta assign 2023-02-02 11:04:37 +01:00
Tyler Neely
1f54981874 Transition splitting shards to non-splitting state as soon as the split destination has at least one CONSENSUS_PARTICIPANT peer 2023-02-01 18:18:04 +00:00
Tyler Neely
f347bd1a7a In the ShardManager, add newly initialized split shards to the initialized_but_not_confirmed_rsm_ set to send to the Coordinator in the next HeartbeatRequest 2023-02-01 11:38:35 +00:00
jbajic
97cab6650b Remove redundant index update 2023-01-31 15:21:39 +01:00
jbajic
221b273472 Fix tests 2023-01-31 14:53:47 +01:00
jbajic
01a6f9f2b1 Fix compilation on raft and messages 2023-01-31 14:22:07 +01:00
jbajic
a1d0d2c82e Fix messages errors 2023-01-30 22:55:08 +01:00
jbajic
30e87f34a4 Fix tests compilation errors 2023-01-30 22:02:45 +01:00
jbajic
324def6149 Fix query engine compilation errors 2023-01-30 20:01:04 +01:00
jbajic
40361b31fc Fix storagev3 compilation errors 2023-01-30 17:18:11 +01:00
jbajic
edc214ab2c Implement shard split initialization in shard manager and worker 2023-01-30 17:03:12 +01:00
jbajic
5c53b06fd0 Merge remote-tracking branch 'origin/tyler_local_shard_manager_messenger' into T-MG-split-shard 2023-01-30 13:18:25 +01:00
jbajic
c2ec41cb37 Rename ptr to iterator 2023-01-30 12:39:27 +01:00
Tyler Neely
fe756958b0 Check-in local shard manager sender code 2023-01-30 11:35:55 +00:00
jbajic
96ac7bd1c5 Move out index splitting 2023-01-30 12:06:09 +01:00
jbajic
78b5731a0e Protect delta id 2023-01-30 10:54:44 +01:00
jbajic
ea35f93db2 Convert pk before high check 2023-01-27 14:27:28 +01:00
jbajic
f6785c0405 Add partial split logic to shard worker and coordinator 2023-01-27 12:38:43 +01:00
jbajic
be9600835f Make AdjustClonedTransaction static 2023-01-26 15:18:30 +01:00
Tyler Neely
5f201f93d1 Forward split request messages to the local raft replica from the ShardManager 2023-01-26 13:59:47 +00:00
jbajic
c1639ef770 Vertify delta uuid 2023-01-26 14:57:30 +01:00
jbajic
dee88fd7a3 Fix delta not cloning id 2023-01-26 14:26:24 +01:00
Tyler Neely
6a6f3835ac Add basic request to connect the Shard and Coordinator split logic 2023-01-26 10:10:16 +00:00
Tyler Neely
fe008f4db8 Merge branch 'T1149-MG-split-shard' of github.com:memgraph/memgraph into T-MG-split-shard 2023-01-26 08:46:15 +00:00
Tyler Neely
56db0120da Merge branch 'T1151-MG-distributed-shard-split-logic' into T-MG-split-shard 2023-01-26 08:43:47 +00:00
Tyler Neely
ed31c952ac Merge branch 'T1149-MG-split-shard' into T-MG-split-shard 2023-01-26 08:43:43 +00:00
jbajic
a30095854e Add benchmark with specific number of transactions 2023-01-25 16:29:16 +01:00
jbajic
fbf4c0adee Assert edges 2023-01-25 16:01:44 +01:00
jbajic
75b5da9f07 Add split in shard rsm 2023-01-25 15:58:56 +01:00
Jure Bajic
2835376eda Merge branch 'project-pineapples' into T1149-MG-split-shard 2023-01-25 13:35:17 +01:00
jbajic
185b87ec85 Add final comments 2023-01-25 13:33:58 +01:00
jbajic
412b8c862f Add benchmark with gc 2023-01-25 13:24:41 +01:00
jbajic
87718aa084 Remove changes on license date 2023-01-25 12:14:30 +01:00
jbajic
1b64a45f10 Add split parameter into config 2023-01-25 12:03:36 +01:00
jbajic
0e517bb9f8 Make split shard benchmark 2023-01-25 11:54:02 +01:00
jbajic
3e23437f14 Add comments 2023-01-25 10:43:05 +01:00
jbajic
bd26af4271 Assert splitted part 2023-01-25 10:35:23 +01:00
jbajic
1db7447ac9 Add big test 2023-01-24 21:02:33 +01:00
jbajic
742393cd70 Fix index split 2023-01-24 20:49:04 +01:00
jbajic
45521bdba8 Add from SplitData 2023-01-23 18:04:48 +01:00
jbajic
97002a50d5 Create new shard 2023-01-23 13:23:21 +01:00
Tyler Neely
6e4d9b4b04 Add basic skeleton for the coordinator-half of the shard splits 2023-01-19 17:23:34 +00:00
jbajic
6ab76fb9ec Assert delta tag 2023-01-19 17:50:04 +01:00
jbajic
0ae5357399 Add delta check to tests 2023-01-19 17:28:19 +01:00
jbajic
28624aaa88 Copy all relevant transactions
Copy all transactions which are relevant for
entity MVCC value resolution.
2023-01-18 16:08:18 +01:00
jbajic
7d0e885f9a Unify CollectEntry method 2023-01-18 14:54:27 +01:00
jbajic
859dfb28eb Add index split 2023-01-18 13:41:54 +01:00
jbajic
348b45360b Extract splitter 2023-01-16 14:54:06 +01:00
jbajic
8a1dd54735 Merge branch 'project-pineapples' into T1149-MG-split-shard 2023-01-16 13:54:16 +01:00
jbajic
ff34bcf295 Fix deltas from commited transacations 2023-01-16 13:53:06 +01:00
jbajic
6de683d7f9 Ignore commited transactions 2023-01-16 09:51:06 +01:00
jbajic
db13ee96b6 Fix transaction split 2023-01-12 17:02:27 +01:00
jbajic
791972a6b8 Fix edge split test 2023-01-12 16:21:46 +01:00
jbajic
a97d994515 Fix vertice split 2023-01-12 16:18:16 +01:00
jbajic
282725fd0f Adjust pointers 2023-01-12 14:23:19 +01:00
jbajic
050d5efae7 Align deltas 2023-01-11 14:16:41 +01:00
jbajic
9dc16deae2 Copy deltas 2023-01-10 16:42:45 +01:00
jbajic
7ef4114835 Fix splitting of edges 2023-01-10 13:01:32 +01:00
jbajic
bf21cbc9a9 Split vetrices, edges and transactions 2022-12-16 11:57:42 +01:00
jbajic
cb83b94b16 Merge branch 'T1149-MG-split-shard' of github.com:memgraph/memgraph into T1149-MG-split-shard 2022-12-16 09:12:28 +01:00
jbajic
3d928b587d Merge branch 'T1185-MG-replace-skip-list' into T1149-MG-split-shard 2022-12-12 17:52:00 +01:00
jbajic
486da0bd1c Begin split funcionlity from shard side 2022-12-12 15:53:16 +01:00
jbajic
b968748d9f Remove redundandt shard properties 2022-12-12 15:53:16 +01:00
jbajic
153e9e2fac Begin split funcionlity from shard side 2022-12-07 14:13:14 +01:00
jbajic
59f4b89361 Remove redundandt shard properties 2022-12-06 12:34:36 +01:00
68 changed files with 3151 additions and 942 deletions

View File

@@ -30,6 +30,8 @@ enum class ErrorCode : uint8_t {
SCHEMA_VERTEX_UPDATE_PRIMARY_LABEL,
SCHEMA_VERTEX_SECONDARY_LABEL_IS_PRIMARY,
SCHEMA_VERTEX_PRIMARY_PROPERTIES_UNDEFINED,
// Distributed race conditions
STALE_SHARD_MAP,
OBJECT_NOT_FOUND,
};
@@ -62,6 +64,8 @@ constexpr std::string_view ErrorCodeToString(const ErrorCode code) {
return "SCHEMA_VERTEX_PRIMARY_PROPERTIES_UNDEFINED";
case ErrorCode::OBJECT_NOT_FOUND:
return "OBJECT_NOT_FOUND";
case ErrorCode::STALE_SHARD_MAP:
return "STALE_SHARD_MAP";
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -9,21 +9,272 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#include <coordinator/coordinator.hpp>
#include "coordinator/coordinator.hpp"
#include "query/v2/requests.hpp"
#include "storage/v3/value_conversions.hpp"
namespace memgraph::coordinator {
// 1. try to begin any suggested splits
// 2. mark all initialized RSMs as INITIALIZED in the ShardMap
// 3. assign any valid underreplicated shards to the Heartbeat sender
// 4. send any split requests that the Heartbeat sender should be applying
CoordinatorWriteResponses Coordinator::ApplyWrite(HeartbeatRequest &&heartbeat_request) {
spdlog::info("Coordinator handling HeartbeatRequest");
spdlog::trace("Coordinator handling HeartbeatRequest");
// add this storage engine to any under-replicated shards that it is not already a part of
HeartbeatResponse ret{};
auto initializing_rsms_for_shard_manager =
shard_map_.AssignShards(heartbeat_request.from_storage_manager, heartbeat_request.initialized_rsms);
bool initiated_split = false;
return HeartbeatResponse{
.shards_to_initialize = initializing_rsms_for_shard_manager,
};
// 1. try to begin any suggested splits
for (const auto &suggested_split_info : heartbeat_request.suggested_splits) {
const LabelId label_id = suggested_split_info.label_id;
auto &label_space = shard_map_.label_spaces.at(label_id);
const PrimaryKey splitting_shard_low_key =
storage::conversions::ConvertPropertyVector(suggested_split_info.splitting_shard_low_key);
const PrimaryKey split_key = storage::conversions::ConvertPropertyVector(suggested_split_info.split_key);
auto &shard = label_space.shards.at(splitting_shard_low_key);
const ShardId splitting_shard_id = std::make_pair(label_id, splitting_shard_low_key);
if (shard.pending_split.has_value() || shard.version != suggested_split_info.shard_version) {
spdlog::debug("Coordinator skipping split, already splitting: {}, shard.version: {}, suggested shard_version: {}",
shard.pending_split.has_value(), shard.version, suggested_split_info.shard_version);
continue;
}
MG_ASSERT(!label_space.shards.contains(split_key));
// begin the split process for this shard
const auto new_uuid_lhs = shard_map_.GetHlc();
const auto new_uuid_rhs = shard_map_.GetHlc();
spdlog::debug(
"Coordinator beginning new split process for shard {} after receiving a pending split. splitting into lhs: {} "
"and rhs: {} at split key {}",
shard.version.logical_id, new_uuid_lhs.logical_id, new_uuid_rhs.logical_id, split_key.back());
// bump current shard version and store pending split info
shard.version = new_uuid_lhs;
shard.pending_split = suggested_split_info;
MG_ASSERT(!splitting_shards_.contains(splitting_shard_id));
splitting_shards_.insert(splitting_shard_id);
initiated_split = true;
// copy this shard and store it in the ShardMap
ShardMetadata duplicated_shard{shard};
duplicated_shard.version = new_uuid_rhs;
duplicated_shard.pending_split.reset();
std::map<boost::uuids::uuid, boost::uuids::uuid> split_mapping = {};
for (auto &peer_metadata : duplicated_shard.peers) {
peer_metadata.status = Status::PENDING_SPLIT;
peer_metadata.split_from = peer_metadata.address.unique_id;
const auto new_uuid = shard_map_.NewShardUuid();
spdlog::debug("Coordinator allocating new rsm uuid: {} for new shard {} with low key {}", new_uuid, new_uuid_rhs,
split_key.back());
// store new uuid for the right side of each shard
rsm_split_from_.insert({new_uuid, splitting_shard_id});
split_mapping.emplace(peer_metadata.address.unique_id, new_uuid);
peer_metadata.address.unique_id = new_uuid;
}
// TODO(tyler) fix this good assertion
// if (high_key) {
// MG_ASSERT(converted_pk < *high_key, "Split point is beyond low key of the next shard");
// }
label_space.shards.insert({split_key, duplicated_shard});
// we will send the heartbeater its split request in stage 4 below
}
// 2. mark all initialized RSMs as INITIALIZED in the ShardMap
for (const auto &[initialized_rsm, shard_id] : heartbeat_request.initialized_rsms) {
auto [label_id, low_key] = shard_id;
auto &label_space = shard_map_.label_spaces.at(label_id);
auto &shard = label_space.shards.at(low_key);
// if even a single shard has been initialized after a split, its raft log has
// reached consensus and we can allow more splits again and remove the pending
// split state.
if (rsm_split_from_.contains(initialized_rsm)) {
auto split_shard_id = rsm_split_from_.at(initialized_rsm);
auto [split_label_id, split_low_key] = split_shard_id;
auto &split_shard = label_space.shards.at(split_low_key);
spdlog::debug("Coordinator clearing split for shard {}", split_shard.version);
MG_ASSERT(split_shard.pending_split.has_value());
MG_ASSERT(splitting_shards_.contains(split_shard_id));
split_shard.pending_split.reset();
splitting_shards_.erase(split_shard_id);
rsm_split_from_.erase(initialized_rsm);
}
size_t initialized_count = 0;
bool found = false;
for (auto &peer : shard.peers) {
if (peer.address.last_known_ip == heartbeat_request.from_storage_manager.last_known_ip &&
peer.address.last_known_port == heartbeat_request.from_storage_manager.last_known_port) {
// TODO(tyler) switch the conditional to match on rsm uuid and update the peer last_known_* address always
const int low_key_int = low_key[0].ValueInt();
// TODO(tyler) this is failing in simulation tests
spdlog::debug("Coordinator marking rsm {} for shard {} as initialized", initialized_rsm, shard.version);
MG_ASSERT(peer.address.unique_id == initialized_rsm,
"expected Coordinator uuid {} to equal peer uuid {} for shard version {} with low key {}",
peer.address.unique_id, initialized_rsm, shard.version, low_key_int);
peer.status = Status::CONSENSUS_PARTICIPANT;
ret.acknowledged_initialized_rsms.push_back(initialized_rsm);
found = true;
}
if (peer.status == Status::CONSENSUS_PARTICIPANT) {
initialized_count++;
}
}
MG_ASSERT(shard.peers.size() == 1);
MG_ASSERT(found, "did not find peer {} in shard version {}", heartbeat_request.from_storage_manager.unique_id,
shard.version);
if (initialized_count >= label_space.replication_factor) {
spdlog::debug("Coordinator clearing underreplicated shard with {} initialized peers", initialized_count);
underreplicated_shards_.erase(shard_id);
}
}
// 3. assign any valid underreplicated shards to the Heartbeat sender
for (auto &underreplicated_shard_id : underreplicated_shards_) {
auto [label_id, low_key] = underreplicated_shard_id;
auto &label_space = shard_map_.label_spaces.at(label_id);
auto &shard = label_space.shards.at(low_key);
bool needs_to_initialize = true;
// make sure we're not already a member
for (const auto &peer : shard.peers) {
if (peer.address.last_known_ip == heartbeat_request.from_storage_manager.last_known_ip &&
peer.address.last_known_port == heartbeat_request.from_storage_manager.last_known_port) {
needs_to_initialize = false;
if (peer.status == Status::INITIALIZING) {
spdlog::trace(
"Coordinator reminding ShardManager to initialize a shard that we have previously assigned it to");
Address address = heartbeat_request.from_storage_manager;
address.unique_id = peer.address.unique_id;
std::optional<PrimaryKey> high_key;
auto next = std::next(label_space.shards.find(low_key));
if (next != label_space.shards.end()) {
high_key = next->first;
}
ret.shards_to_initialize.push_back(ShardToInitialize{
.new_shard_version = shard.version,
.uuid = address.unique_id,
.label_id = label_id,
.min_key = low_key,
.max_key = high_key,
.schema = shard_map_.schemas[label_id],
.config = Config{.split =
Config::Split{
.max_shard_vertex_size = label_space.split_threshold,
}},
.id_to_names = shard_map_.IdToNames(),
});
}
}
}
if (!needs_to_initialize) {
// we are already a member of this shard. continue to next underreplicated shard to consider
continue;
}
// we are not already a member, so we can be assigned to this shard
Address address = heartbeat_request.from_storage_manager;
address.unique_id = shard_map_.NewShardUuid();
std::optional<PrimaryKey> high_key;
auto next = std::next(label_space.shards.find(low_key));
if (next != label_space.shards.end()) {
high_key = next->first;
}
spdlog::debug("Coordinator assigning new rsm uuid {} to shard version {} with low key {} to shard worker {}",
address.unique_id, shard.version, low_key.back(), address);
ret.shards_to_initialize.push_back(ShardToInitialize{
.new_shard_version = shard.version,
.uuid = address.unique_id,
.label_id = label_id,
.min_key = low_key,
.max_key = high_key,
.schema = shard_map_.schemas[label_id],
.config = Config{.split =
Config::Split{
.max_shard_vertex_size = label_space.split_threshold,
}},
.id_to_names = shard_map_.IdToNames(),
});
PeerMetadata peer_metadata = {
.address = address,
.status = Status::INITIALIZING,
};
shard.peers.emplace_back(peer_metadata);
}
// 4. send any split requests that the Heartbeat sender should be applying
for (const auto &[label_id, low_key] : splitting_shards_) {
// see if this machine is a PENDING_SPLIT peer for any of the splitting shards
const auto &label_space = shard_map_.label_spaces.at(label_id);
const auto &splitting_shard = label_space.shards.at(low_key);
MG_ASSERT(splitting_shard.pending_split.has_value());
const auto &suggested_split_info = splitting_shard.pending_split.value();
const auto split_key = storage::conversions::ConvertPropertyVector(suggested_split_info.split_key);
const auto &new_shard = label_space.shards.at(split_key);
for (const auto &peer : new_shard.peers) {
if (peer.status != Status::PENDING_SPLIT ||
peer.address.last_known_ip != heartbeat_request.from_storage_manager.last_known_ip ||
peer.address.last_known_port != heartbeat_request.from_storage_manager.last_known_port) {
spdlog::debug("Coordinator not splitting peer: status is: {}, peer address: {} storage manager address: {}",
peer.status, peer.address, heartbeat_request.from_storage_manager);
// not splitting or not us
continue;
}
spdlog::trace("Coordinator expecting heartbeating peer to split, so it will reply with a ShardToSplit message");
std::map<boost::uuids::uuid, boost::uuids::uuid> uuid_mapping{};
// need to iterate over all peers again to build the full uuid_mapping to
// send to this machine
for (const auto &peer_metadata2 : new_shard.peers) {
uuid_mapping.emplace(peer_metadata2.split_from, peer_metadata2.address.unique_id);
}
ret.shards_to_split.push_back(ShardToSplit{
.split_key = split_key,
.old_shard_version = suggested_split_info.shard_version,
.new_lhs_shard_version = splitting_shard.version,
.new_rhs_shard_version = new_shard.version,
.uuid_mapping = uuid_mapping,
});
}
}
if (initiated_split) {
MG_ASSERT(!ret.shards_to_split.empty(), "did not send a split request to the shard that suggested a split");
}
return ret;
}
CoordinatorWriteResponses Coordinator::ApplyWrite(HlcRequest &&hlc_request) {
@@ -37,6 +288,7 @@ CoordinatorWriteResponses Coordinator::ApplyWrite(HlcRequest &&hlc_request) {
.logical_id = ++highest_allocated_timestamp_,
// TODO(tyler) probably pass some more context to the Coordinator here
// so that we can use our wall clock and enforce monotonicity.
// Check it to ensure it's +/- 1 day of the coordinator's io_.Now()
// .coordinator_wall_clock = io_.Now(),
};
@@ -61,22 +313,6 @@ CoordinatorWriteResponses Coordinator::ApplyWrite(AllocateEdgeIdBatchRequest &&a
return res;
}
/// This splits the shard immediately beneath the provided
/// split key, keeping the assigned peers identical for now,
/// but letting them be gradually migrated over time.
CoordinatorWriteResponses Coordinator::ApplyWrite(SplitShardRequest &&split_shard_request) {
SplitShardResponse res{};
if (split_shard_request.previous_shard_map_version != shard_map_.shard_map_version) {
res.success = false;
} else {
res.success = shard_map_.SplitShard(split_shard_request.previous_shard_map_version, split_shard_request.label_id,
split_shard_request.split_key);
}
return res;
}
/// This adds the provided storage engine to the standby storage engine pool,
/// which can be used to rebalance storage over time.
CoordinatorWriteResponses Coordinator::ApplyWrite(
@@ -102,12 +338,16 @@ CoordinatorWriteResponses Coordinator::ApplyWrite(InitializeLabelRequest &&initi
std::optional<LabelId> new_label_id = shard_map_.InitializeNewLabel(
initialize_label_request.label_name, initialize_label_request.schema, initialize_label_request.replication_factor,
initialize_label_request.last_shard_map_version);
initialize_label_request.split_threshold, initialize_label_request.last_shard_map_version);
if (new_label_id) {
res.new_label_id = new_label_id.value();
res.fresher_shard_map = std::nullopt;
res.success = true;
auto min_key = SchemaToMinKey(initialize_label_request.schema);
auto shard_id = std::make_pair(new_label_id.value(), min_key);
underreplicated_shards_.insert(shard_id);
} else {
res.fresher_shard_map = shard_map_;
res.success = false;

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -20,13 +20,14 @@
#include <boost/uuid/uuid.hpp>
#include <coordinator/hybrid_logical_clock.hpp>
#include <coordinator/shard_map.hpp>
#include <io/simulator/simulator.hpp>
#include <io/time.hpp>
#include <io/transport.hpp>
#include <storage/v3/id_types.hpp>
#include <storage/v3/schemas.hpp>
#include "coordinator/hybrid_logical_clock.hpp"
#include "coordinator/shard_map.hpp"
#include "io/simulator/simulator.hpp"
#include "io/time.hpp"
#include "io/transport.hpp"
#include "query/v2/requests.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/schemas.hpp"
namespace memgraph::coordinator {
@@ -37,6 +38,8 @@ using memgraph::storage::v3::SchemaProperty;
using SimT = memgraph::io::simulator::SimulatorTransport;
using PrimaryKey = std::vector<PropertyValue>;
using ShardId = std::pair<LabelId, PrimaryKey>;
struct HlcRequest {
Hlc last_shard_map_version;
};
@@ -112,6 +115,7 @@ struct InitializeLabelRequest {
std::string label_name;
std::vector<SchemaProperty> schema;
size_t replication_factor;
uint64_t split_threshold;
Hlc last_shard_map_version;
};
@@ -121,28 +125,29 @@ struct InitializeLabelResponse {
std::optional<ShardMap> fresher_shard_map;
};
struct HeartbeatRequest {
Address from_storage_manager;
std::set<boost::uuids::uuid> initialized_rsms;
};
struct HeartbeatResponse {
std::vector<ShardToInitialize> shards_to_initialize;
};
using CoordinatorWriteRequests =
std::variant<HlcRequest, AllocateEdgeIdBatchRequest, SplitShardRequest, RegisterStorageEngineRequest,
DeregisterStorageEngineRequest, InitializeLabelRequest, AllocatePropertyIdsRequest, HeartbeatRequest>;
using CoordinatorWriteResponses = std::variant<HlcResponse, AllocateEdgeIdBatchResponse, SplitShardResponse,
RegisterStorageEngineResponse, DeregisterStorageEngineResponse,
InitializeLabelResponse, AllocatePropertyIdsResponse, HeartbeatResponse>;
std::variant<HlcRequest, AllocateEdgeIdBatchRequest, RegisterStorageEngineRequest, DeregisterStorageEngineRequest,
InitializeLabelRequest, AllocatePropertyIdsRequest, HeartbeatRequest>;
using CoordinatorWriteResponses = std::variant<HlcResponse, AllocateEdgeIdBatchResponse, RegisterStorageEngineResponse,
DeregisterStorageEngineResponse, InitializeLabelResponse,
AllocatePropertyIdsResponse, HeartbeatResponse>;
using CoordinatorReadRequests = std::variant<GetShardMapRequest>;
using CoordinatorReadResponses = std::variant<GetShardMapResponse>;
class Coordinator {
public:
explicit Coordinator(ShardMap sm) : shard_map_{std::move(sm)} {}
explicit Coordinator(ShardMap sm) : shard_map_{std::move(sm)} {
// Populate underreplicated_shards_
for (const auto &[label_id, label_space] : shard_map_.label_spaces) {
for (const auto &[low_key, shard] : label_space.shards) {
if (shard.peers.size() < label_space.replication_factor) {
ShardId shard_id = std::make_pair(label_id, low_key);
underreplicated_shards_.insert(shard_id);
}
}
}
}
// NOLINTNEXTLINE(readability-convert-member-functions-to-static
CoordinatorReadResponses Read(CoordinatorReadRequests requests) {
@@ -160,6 +165,11 @@ class Coordinator {
ShardMap shard_map_;
uint64_t highest_allocated_timestamp_{0};
std::set<ShardId> underreplicated_shards_;
std::map<boost::uuids::uuid, ShardId> rsm_split_from_;
std::set<ShardId> splitting_shards_;
std::map<Address, std::set<ShardId>> assigned_shards_;
/// Query engines need to periodically request batches of unique edge IDs.
uint64_t highest_allocated_edge_id_{0};
@@ -175,11 +185,6 @@ class Coordinator {
CoordinatorWriteResponses ApplyWrite(AllocateEdgeIdBatchRequest &&ahr);
/// This splits the shard immediately beneath the provided
/// split key, keeping the assigned peers identical for now,
/// but letting them be gradually migrated over time.
CoordinatorWriteResponses ApplyWrite(SplitShardRequest &&split_shard_request);
/// This adds the provided storage engine to the standby storage engine pool,
/// which can be used to rebalance storage over time.
static CoordinatorWriteResponses ApplyWrite(RegisterStorageEngineRequest && /* register_storage_engine_request */);

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -23,17 +23,19 @@ namespace memgraph::coordinator {
using Time = memgraph::io::Time;
/// Hybrid-logical clock
struct Hlc {
uint64_t logical_id = 0;
struct Hlc final {
uint64_t logical_id{0};
Time coordinator_wall_clock = Time::min();
auto operator<=>(const Hlc &other) const { return logical_id <=> other.logical_id; }
bool operator==(const Hlc &other) const = default;
bool operator<(const Hlc &other) const = default;
bool operator==(const uint64_t other) const { return logical_id == other; }
bool operator<(const uint64_t other) const { return logical_id < other; }
bool operator>=(const uint64_t other) const { return logical_id >= other; }
bool operator==(const Hlc &other) const noexcept = default;
bool operator<(const Hlc &other) const noexcept = default;
bool operator==(const uint64_t other) const noexcept { return logical_id == other; }
bool operator<(const uint64_t other) const noexcept { return logical_id < other; }
bool operator>=(const uint64_t other) const noexcept { return logical_id >= other; }
Hlc operator++() { return {.logical_id = ++logical_id, .coordinator_wall_clock = Time::min()}; }
friend std::ostream &operator<<(std::ostream &in, const Hlc &hlc) {
auto wall_clock = std::chrono::system_clock::to_time_t(hlc.coordinator_wall_clock);

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -9,15 +9,21 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#include <boost/uuid/uuid.hpp>
#include <map>
#include <optional>
#include <unordered_map>
#include <vector>
#include "common/types.hpp"
#include "coordinator/hybrid_logical_clock.hpp"
#include "coordinator/shard_map.hpp"
#include "query/v2/requests.hpp"
#include "spdlog/spdlog.h"
#include "storage/v3/config.hpp"
#include "storage/v3/schemas.hpp"
#include "storage/v3/temporal.hpp"
#include "storage/v3/value_conversions.hpp"
#include "utils/cast.hpp"
#include "utils/exceptions.hpp"
#include "utils/string.hpp"
@@ -68,6 +74,9 @@ PrimaryKey SchemaToMinKey(const std::vector<SchemaProperty> &schema) {
}
ShardMap ShardMap::Parse(std::istream &input_stream) {
const uint64_t default_replication_factor = 1;
const uint64_t default_split_threshold = 1'000'000;
ShardMap shard_map;
const auto read_size = [&input_stream] {
size_t size{0};
@@ -84,23 +93,17 @@ ShardMap ShardMap::Parse(std::istream &input_stream) {
const auto read_names = [&read_size, &read_word] {
const auto number_of_names = read_size();
spdlog::trace("Reading {} names", number_of_names);
spdlog::trace("ShardMap::Parse reading {} names", number_of_names);
std::vector<std::string> names;
names.reserve(number_of_names);
for (auto name_index = 0; name_index < number_of_names; ++name_index) {
names.push_back(read_word());
spdlog::trace("Read '{}'", names.back());
spdlog::trace("ShardMap::Parse read '{}'", names.back());
}
return names;
};
const auto read_line = [&input_stream] {
std::string line;
std::getline(input_stream, line);
return line;
};
const auto parse_type = [](const std::string &type) {
static const auto type_map = std::unordered_map<std::string, common::SchemaType>{
{"string", common::SchemaType::STRING}, {"int", common::SchemaType::INT}, {"bool", common::SchemaType::BOOL}};
@@ -110,45 +113,32 @@ ShardMap ShardMap::Parse(std::istream &input_stream) {
return it->second;
};
const auto parse_property_value = [](std::string text, const common::SchemaType type) {
if (type == common::SchemaType::STRING) {
return storage::v3::PropertyValue{std::move(text)};
}
if (type == common::SchemaType::INT) {
size_t processed{0};
int64_t value = std::stoll(text, &processed);
MG_ASSERT(processed == text.size() || text[processed] == ' ', "Invalid integer format: '{}'", text);
return storage::v3::PropertyValue{value};
}
LOG_FATAL("Not supported type: {}", utils::UnderlyingCast(type));
};
spdlog::debug("Reading properties");
spdlog::trace("ShardMap::Parse reading properties");
const auto properties = read_names();
MG_ASSERT(shard_map.AllocatePropertyIds(properties).size() == properties.size(),
"Unexpected number of properties created!");
spdlog::debug("Reading edge types");
spdlog::trace("ShardMap::Parse Reading edge types");
const auto edge_types = read_names();
MG_ASSERT(shard_map.AllocateEdgeTypeIds(edge_types).size() == edge_types.size(),
"Unexpected number of properties created!");
spdlog::debug("Reading primary labels");
spdlog::trace("ShardMap::Parse reading primary labels");
const auto number_of_primary_labels = read_size();
spdlog::debug("Reading {} primary labels", number_of_primary_labels);
spdlog::trace("ShardMap::Parse reading {} primary labels", number_of_primary_labels);
for (auto label_index = 0; label_index < number_of_primary_labels; ++label_index) {
const auto primary_label = read_word();
spdlog::debug("Reading primary label named '{}'", primary_label);
spdlog::trace("ShardMap::Parse reading primary label named '{}'", primary_label);
const auto number_of_primary_properties = read_size();
spdlog::debug("Reading {} primary properties", number_of_primary_properties);
spdlog::trace("ShardMap::Parse reading {} primary properties", number_of_primary_properties);
std::vector<std::string> pp_names;
std::vector<common::SchemaType> pp_types;
pp_names.reserve(number_of_primary_properties);
pp_types.reserve(number_of_primary_properties);
for (auto property_index = 0; property_index < number_of_primary_properties; ++property_index) {
pp_names.push_back(read_word());
spdlog::debug("Reading primary property named '{}'", pp_names.back());
spdlog::trace("ShardMap::Parse reading primary property named '{}'", pp_names.back());
pp_types.push_back(parse_type(read_word()));
}
auto pp_mapping = shard_map.AllocatePropertyIds(pp_names);
@@ -159,33 +149,10 @@ ShardMap ShardMap::Parse(std::istream &input_stream) {
schema.push_back(storage::v3::SchemaProperty{pp_mapping.at(pp_names[property_index]), pp_types[property_index]});
}
const auto hlc = shard_map.GetHlc();
MG_ASSERT(shard_map.InitializeNewLabel(primary_label, schema, 1, hlc).has_value(),
"Cannot initialize new label: {}", primary_label);
const auto number_of_split_points = read_size();
spdlog::debug("Reading {} split points", number_of_split_points);
[[maybe_unused]] const auto remainder_from_last_line = read_line();
for (auto split_point_index = 0; split_point_index < number_of_split_points; ++split_point_index) {
const auto line = read_line();
spdlog::debug("Read split point '{}'", line);
MG_ASSERT(line.front() == '[', "Invalid split file format!");
MG_ASSERT(line.back() == ']', "Invalid split file format!");
std::string_view line_view{line};
line_view.remove_prefix(1);
line_view.remove_suffix(1);
static constexpr std::string_view kDelimiter{","};
auto pk_values_as_text = utils::Split(line_view, kDelimiter);
std::vector<PropertyValue> pk;
pk.reserve(number_of_primary_properties);
MG_ASSERT(pk_values_as_text.size() == number_of_primary_properties,
"Split point contains invalid number of values '{}'", line);
for (auto property_index = 0; property_index < number_of_primary_properties; ++property_index) {
pk.push_back(parse_property_value(std::move(pk_values_as_text[property_index]), schema[property_index].type));
}
shard_map.SplitShard(shard_map.GetHlc(), shard_map.labels.at(primary_label), pk);
}
MG_ASSERT(
shard_map.InitializeNewLabel(primary_label, schema, default_replication_factor, default_split_threshold, hlc)
.has_value(),
"Cannot initialize new label: {}", primary_label);
}
return shard_map;
@@ -246,9 +213,7 @@ std::unordered_map<uint64_t, std::string> ShardMap::IdToNames() {
return id_to_names;
}
Hlc ShardMap::GetHlc() const noexcept { return shard_map_version; }
boost::uuids::uuid NewShardUuid(uint64_t shard_id) {
boost::uuids::uuid Uint64ToUuid(uint64_t u) {
return boost::uuids::uuid{0,
0,
0,
@@ -257,124 +222,28 @@ boost::uuids::uuid NewShardUuid(uint64_t shard_id) {
0,
0,
0,
static_cast<unsigned char>(shard_id >> 56U),
static_cast<unsigned char>(shard_id >> 48U),
static_cast<unsigned char>(shard_id >> 40U),
static_cast<unsigned char>(shard_id >> 32U),
static_cast<unsigned char>(shard_id >> 24U),
static_cast<unsigned char>(shard_id >> 16U),
static_cast<unsigned char>(shard_id >> 8U),
static_cast<unsigned char>(shard_id)};
static_cast<unsigned char>(u >> 56U),
static_cast<unsigned char>(u >> 48U),
static_cast<unsigned char>(u >> 40U),
static_cast<unsigned char>(u >> 32U),
static_cast<unsigned char>(u >> 24U),
static_cast<unsigned char>(u >> 16U),
static_cast<unsigned char>(u >> 8U),
static_cast<unsigned char>(u)};
}
std::vector<ShardToInitialize> ShardMap::AssignShards(Address storage_manager,
std::set<boost::uuids::uuid> initialized) {
std::vector<ShardToInitialize> ret{};
bool mutated = false;
for (auto &[label_id, label_space] : label_spaces) {
for (auto it = label_space.shards.begin(); it != label_space.shards.end(); it++) {
auto &[low_key, shard] = *it;
std::optional<PrimaryKey> high_key;
if (const auto next_it = std::next(it); next_it != label_space.shards.end()) {
high_key = next_it->first;
}
// TODO(tyler) avoid these triple-nested loops by having the heartbeat include better info
bool machine_contains_shard = false;
for (auto &aas : shard.peers) {
if (initialized.contains(aas.address.unique_id)) {
machine_contains_shard = true;
if (aas.status != Status::CONSENSUS_PARTICIPANT) {
mutated = true;
spdlog::info("marking shard as full consensus participant: {}", aas.address.unique_id);
aas.status = Status::CONSENSUS_PARTICIPANT;
}
} else {
const bool same_machine = aas.address.last_known_ip == storage_manager.last_known_ip &&
aas.address.last_known_port == storage_manager.last_known_port;
if (same_machine) {
machine_contains_shard = true;
spdlog::info("reminding shard manager that they should begin participating in shard");
ret.push_back(ShardToInitialize{
.uuid = aas.address.unique_id,
.label_id = label_id,
.min_key = low_key,
.max_key = high_key,
.schema = schemas[label_id],
.config = Config{},
.id_to_names = IdToNames(),
});
}
}
}
if (!machine_contains_shard && shard.peers.size() < label_space.replication_factor) {
// increment version for each new uuid for deterministic creation
IncrementShardMapVersion();
Address address = storage_manager;
// TODO(tyler) use deterministic UUID so that coordinators don't diverge here
address.unique_id = NewShardUuid(shard_map_version.logical_id);
spdlog::info("assigning shard manager to shard");
ret.push_back(ShardToInitialize{
.uuid = address.unique_id,
.label_id = label_id,
.min_key = low_key,
.max_key = high_key,
.schema = schemas[label_id],
.config = Config{},
.id_to_names = IdToNames(),
});
AddressAndStatus aas = {
.address = address,
.status = Status::INITIALIZING,
};
shard.peers.emplace_back(aas);
}
}
}
if (mutated) {
IncrementShardMapVersion();
}
return ret;
boost::uuids::uuid ShardMap::NewShardUuid() {
uint64_t shard_id = GetHlc().logical_id;
return Uint64ToUuid(shard_id);
}
bool ShardMap::SplitShard(Hlc previous_shard_map_version, LabelId label_id, const PrimaryKey &key) {
if (previous_shard_map_version != shard_map_version) {
return false;
}
auto &label_space = label_spaces.at(label_id);
auto &shards_in_map = label_space.shards;
MG_ASSERT(!shards_in_map.empty());
MG_ASSERT(!shards_in_map.contains(key));
MG_ASSERT(label_spaces.contains(label_id));
// Finding the ShardMetadata that the new PrimaryKey should map to.
auto prev = std::prev(shards_in_map.upper_bound(key));
ShardMetadata duplicated_shard = prev->second;
// Apply the split
shards_in_map[key] = duplicated_shard;
IncrementShardMapVersion();
return true;
}
Hlc ShardMap::GetHlc() noexcept { return ++shard_map_version; }
std::optional<LabelId> ShardMap::InitializeNewLabel(std::string label_name, std::vector<SchemaProperty> schema,
size_t replication_factor, Hlc last_shard_map_version) {
size_t replication_factor, uint64_t split_threshold,
Hlc last_shard_map_version) {
if (shard_map_version != last_shard_map_version || labels.contains(label_name)) {
MG_ASSERT(false, "failed to InitializeNewLabel");
return std::nullopt;
}
@@ -393,7 +262,9 @@ std::optional<LabelId> ShardMap::InitializeNewLabel(std::string label_name, std:
.schema = schema,
.shards = shards,
.replication_factor = replication_factor,
.split_threshold = split_threshold,
};
schemas[label_id] = std::move(schema);
label_spaces.emplace(label_id, label_space);
@@ -557,13 +428,13 @@ bool ShardMap::ClusterInitialized() const {
for (const auto &[label_id, label_space] : label_spaces) {
for (const auto &[low_key, shard] : label_space.shards) {
if (shard.peers.size() < label_space.replication_factor) {
spdlog::info("label_space below desired replication factor");
spdlog::trace("ShardMap::ClusterInitialized label_space below desired replication factor");
return false;
}
for (const auto &aas : shard.peers) {
if (aas.status != Status::CONSENSUS_PARTICIPANT) {
spdlog::info("shard member not yet a CONSENSUS_PARTICIPANT");
for (const auto &peer_metadata : shard.peers) {
if (peer_metadata.status != Status::CONSENSUS_PARTICIPANT) {
spdlog::trace("ShardMap::ClusterInitialized shard member not yet a CONSENSUS_PARTICIPANT");
return false;
}
}
@@ -573,4 +444,29 @@ bool ShardMap::ClusterInitialized() const {
return true;
}
size_t ShardMap::InitializedShards() const {
size_t count = 0;
for (const auto &[label_id, label_space] : label_spaces) {
for (const auto &[low_key, shard] : label_space.shards) {
if (shard.peers.size() < label_space.replication_factor) {
continue;
}
bool all_initialized = true;
for (const auto &peer_metadata : shard.peers) {
if (peer_metadata.status != Status::CONSENSUS_PARTICIPANT) {
all_initialized = false;
}
}
if (all_initialized) {
count += 1;
}
}
}
return count;
}
} // namespace memgraph::coordinator

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -14,6 +14,7 @@
#include <algorithm>
#include <limits>
#include <map>
#include <optional>
#include <set>
#include <vector>
@@ -23,6 +24,7 @@
#include "common/types.hpp"
#include "coordinator/hybrid_logical_clock.hpp"
#include "io/address.hpp"
#include "query/v2/requests.hpp"
#include "storage/v3/config.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/name_id_mapper.hpp"
@@ -47,39 +49,59 @@ using memgraph::storage::v3::SchemaProperty;
enum class Status : uint8_t {
CONSENSUS_PARTICIPANT,
INITIALIZING,
PENDING_SPLIT,
// TODO(tyler) this will possibly have more states,
// depending on the reconfiguration protocol that we
// implement.
};
struct AddressAndStatus {
struct PeerMetadata {
memgraph::io::Address address;
Status status;
boost::uuids::uuid split_from;
friend bool operator<(const AddressAndStatus &lhs, const AddressAndStatus &rhs) { return lhs.address < rhs.address; }
friend bool operator<(const PeerMetadata &lhs, const PeerMetadata &rhs) { return lhs.address < rhs.address; }
friend std::ostream &operator<<(std::ostream &in, const AddressAndStatus &address_and_status) {
in << "AddressAndStatus { address: ";
in << address_and_status.address;
if (address_and_status.status == Status::CONSENSUS_PARTICIPANT) {
in << ", status: CONSENSUS_PARTICIPANT }";
friend std::ostream &operator<<(std::ostream &in, const PeerMetadata &peer_metadata) {
in << "PeerMetadata { address: ";
in << peer_metadata.address;
if (peer_metadata.status == Status::CONSENSUS_PARTICIPANT) {
in << ", status: CONSENSUS_PARTICIPANT";
} else if (peer_metadata.status == Status::INITIALIZING) {
in << ", status: INITIALIZING";
} else if (peer_metadata.status == Status::PENDING_SPLIT) {
in << ", status: PENDING_SPLIT";
} else {
in << ", status: INITIALIZING }";
MG_ASSERT(false, "failed to update the operator<< implementation for Status");
}
in << ", split_from: " << peer_metadata.split_from << " }";
return in;
}
friend bool operator==(const AddressAndStatus &lhs, const AddressAndStatus &rhs) {
return lhs.address == rhs.address;
}
friend bool operator==(const PeerMetadata &lhs, const PeerMetadata &rhs) { return lhs.address == rhs.address; }
};
using PrimaryKey = std::vector<PropertyValue>;
using ShardId = std::pair<LabelId, PrimaryKey>;
struct ShardMetadata {
std::vector<AddressAndStatus> peers;
uint64_t version;
std::vector<PeerMetadata> peers;
Hlc version;
std::optional<msgs::SuggestedSplitInfo> pending_split;
bool Underreplicated(size_t replication_factor) { return peers.size() < replication_factor; }
bool ContainsPeer(io::PartialAddress machine) {
for (const auto &peer : peers) {
if (peer.address.last_known_ip == machine.ip && peer.address.last_known_port == machine.port) {
return true;
}
}
return false;
}
friend std::ostream &operator<<(std::ostream &in, const ShardMetadata &shard) {
using utils::print_helpers::operator<<;
@@ -93,7 +115,9 @@ struct ShardMetadata {
return in;
}
friend bool operator==(const ShardMetadata &lhs, const ShardMetadata &rhs) = default;
friend bool operator==(const ShardMetadata &lhs, const ShardMetadata &rhs) {
return lhs.version == rhs.version && lhs.peers == rhs.peers;
};
friend bool operator<(const ShardMetadata &lhs, const ShardMetadata &rhs) {
if (lhs.peers != rhs.peers) {
@@ -112,6 +136,7 @@ using PropertyMap = std::map<PropertyName, PropertyId>;
using EdgeTypeIdMap = std::map<EdgeTypeName, EdgeTypeId>;
struct ShardToInitialize {
Hlc new_shard_version;
boost::uuids::uuid uuid;
LabelId label_id;
PrimaryKey min_key;
@@ -121,6 +146,26 @@ struct ShardToInitialize {
std::unordered_map<uint64_t, std::string> id_to_names;
};
struct ShardToSplit {
PrimaryKey split_key;
Hlc old_shard_version;
Hlc new_lhs_shard_version;
Hlc new_rhs_shard_version;
std::map<boost::uuids::uuid, boost::uuids::uuid> uuid_mapping;
};
struct HeartbeatRequest {
Address from_storage_manager;
std::map<boost::uuids::uuid, ShardId> initialized_rsms;
std::set<msgs::SuggestedSplitInfo> suggested_splits;
};
struct HeartbeatResponse {
std::vector<ShardToInitialize> shards_to_initialize;
std::vector<ShardToSplit> shards_to_split;
std::vector<boost::uuids::uuid> acknowledged_initialized_rsms;
};
PrimaryKey SchemaToMinKey(const std::vector<SchemaProperty> &schema);
struct LabelSpace {
@@ -128,6 +173,7 @@ struct LabelSpace {
// Maps between the smallest primary key stored in the shard and the shard
std::map<PrimaryKey, ShardMetadata> shards;
size_t replication_factor;
uint64_t split_threshold;
friend std::ostream &operator<<(std::ostream &in, const LabelSpace &label_space) {
using utils::print_helpers::operator<<;
@@ -163,17 +209,15 @@ struct ShardMap {
// TODO(gabor) later we will want to update the wallclock time with
// the given Io<impl>'s time as well
Hlc IncrementShardMapVersion() noexcept;
Hlc GetHlc() const noexcept;
Hlc GetHlc() noexcept;
std::unordered_map<uint64_t, std::string> IdToNames();
// Returns the shard UUIDs that have been assigned but not yet acknowledged for this storage manager
std::vector<ShardToInitialize> AssignShards(Address storage_manager, std::set<boost::uuids::uuid> initialized);
bool SplitShard(Hlc previous_shard_map_version, LabelId label_id, const PrimaryKey &key);
std::optional<LabelId> InitializeNewLabel(std::string label_name, std::vector<SchemaProperty> schema,
size_t replication_factor, Hlc last_shard_map_version);
size_t replication_factor, uint64_t split_threshold,
Hlc last_shard_map_version);
boost::uuids::uuid NewShardUuid();
void AddServer(Address server_address);
@@ -199,6 +243,9 @@ struct ShardMap {
/// the CONSENSUS_PARTICIPANT state. Note that this does not necessarily mean that
/// there is also an active leader for each shard.
bool ClusterInitialized() const;
/// Returns the current count of all shards that are fully initialized.
size_t InitializedShards() const;
};
} // namespace memgraph::coordinator

View File

@@ -120,6 +120,10 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
case Error::SCHEMA_VERTEX_SECONDARY_LABEL_IS_PRIMARY:
case Error::SCHEMA_VERTEX_PRIMARY_PROPERTIES_UNDEFINED:
throw ExpressionRuntimeException("Unexpected schema violation when accessing {}.", accessed_object);
case Error::STALE_SHARD_MAP:
throw ExpressionRuntimeException(
"Cluster performed a Shard split or merge that invalidated the transaction's metadata. This should have "
"been handled in the RequestRouter.");
}
}

View File

@@ -457,6 +457,7 @@ TypedValueT Properties(const TypedValueT *args, int64_t nargs, const FunctionCon
case common::ErrorCode::SCHEMA_VERTEX_SECONDARY_LABEL_IS_PRIMARY:
case common::ErrorCode::SCHEMA_VERTEX_PRIMARY_PROPERTIES_UNDEFINED:
case common::ErrorCode::OBJECT_NOT_FOUND:
case common::ErrorCode::STALE_SHARD_MAP:
throw functions::FunctionRuntimeException("Unexpected error when getting properties.");
}
}
@@ -532,6 +533,7 @@ inline size_t UnwrapDegreeResult(storage::v3::ShardResult<size_t> maybe_degree)
case common::ErrorCode::SCHEMA_VERTEX_SECONDARY_LABEL_IS_PRIMARY:
case common::ErrorCode::SCHEMA_VERTEX_PRIMARY_PROPERTIES_UNDEFINED:
case common::ErrorCode::OBJECT_NOT_FOUND:
case common::ErrorCode::STALE_SHARD_MAP:
throw functions::FunctionRuntimeException("Unexpected error when getting node degree.");
}
}
@@ -719,6 +721,7 @@ TypedValueT Labels(const TypedValueT *args, int64_t nargs, const FunctionContext
case common::ErrorCode::SCHEMA_VERTEX_SECONDARY_LABEL_IS_PRIMARY:
case common::ErrorCode::SCHEMA_VERTEX_PRIMARY_PROPERTIES_UNDEFINED:
case common::ErrorCode::OBJECT_NOT_FOUND:
case common::ErrorCode::STALE_SHARD_MAP:
throw functions::FunctionRuntimeException("Unexpected error when getting labels.");
}
}

View File

@@ -14,7 +14,12 @@
namespace memgraph::io {
// Signifies that a retriable operation was unable to
// complete after a configured number of retries.
struct RetriesExhausted {};
struct RetriesExhausted {
friend std::ostream &operator<<(std::ostream &in, const RetriesExhausted & /* retries_exhausted */) {
in << "RetriesExhausted {}";
return in;
}
};
// Signifies that a request was unable to receive a response
// within some configured timeout duration. It is important
@@ -22,5 +27,21 @@ struct RetriesExhausted {};
// not signify that a request was not received or processed.
// It may be the case that the request was fully processed
// but that the response was not received.
struct TimedOut {};
struct TimedOut {
friend std::ostream &operator<<(std::ostream &in, const TimedOut & /* timed_out */) {
in << "TimedOut {}";
return in;
}
};
// This error signifies that a shard has been contacted that does
// not match our expected shard version, and that we should retry
// the operation so that we can ensure that we are talking to the
// correct shard.
struct ShardVersionMismatch {
friend std::ostream &operator<<(std::ostream &in, const ShardVersionMismatch & /* shard_version_mismatch */) {
in << "ShardVersionMismatch {}";
return in;
}
};
}; // namespace memgraph::io

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -20,6 +20,7 @@
#include "io/local_transport/local_transport_handle.hpp"
#include "io/time.hpp"
#include "io/transport.hpp"
#include "utils/concrete_msg_sender.hpp"
namespace memgraph::io::local_transport {
@@ -30,19 +31,19 @@ class LocalTransport {
explicit LocalTransport(std::shared_ptr<LocalTransportHandle> local_transport_handle)
: local_transport_handle_(std::move(local_transport_handle)) {}
template <Message RequestT, Message ResponseT>
template <utils::Message RequestT, utils::Message ResponseT>
ResponseFuture<ResponseT> Request(Address to_address, Address from_address, RequestT request,
std::function<void()> fill_notifier, Duration timeout) {
return local_transport_handle_->template SubmitRequest<RequestT, ResponseT>(
to_address, from_address, std::move(request), timeout, fill_notifier);
}
template <Message... Ms>
template <utils::Message... Ms>
requires(sizeof...(Ms) > 0) RequestResult<Ms...> Receive(Address receiver_address, Duration timeout) {
return local_transport_handle_->template Receive<Ms...>(receiver_address, timeout);
}
template <Message M>
template <utils::Message M>
void Send(Address to_address, Address from_address, RequestId request_id, M &&message) {
return local_transport_handle_->template Send<M>(to_address, from_address, request_id, std::forward<M>(message));
}

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -22,6 +22,7 @@
#include "io/message_histogram_collector.hpp"
#include "io/time.hpp"
#include "io/transport.hpp"
#include "utils/concrete_msg_sender.hpp"
namespace memgraph::io::local_transport {
@@ -30,7 +31,7 @@ class LocalTransportHandle {
mutable std::condition_variable cv_;
bool should_shut_down_ = false;
MessageHistogramCollector histograms_;
RequestId request_id_counter_ = 0;
RequestId request_id_counter_ = 1;
// the responses to requests that are being waited on
std::map<PromiseKey, DeadlineAndOpaquePromise> promises_;
@@ -67,7 +68,7 @@ class LocalTransportHandle {
return std::chrono::time_point_cast<std::chrono::microseconds>(nano_time);
}
template <Message... Ms>
template <utils::Message... Ms>
requires(sizeof...(Ms) > 0) RequestResult<Ms...> Receive(Address /* receiver_address */, Duration timeout) {
std::unique_lock lock(mu_);
@@ -103,7 +104,7 @@ class LocalTransportHandle {
return std::move(m_opt).value();
}
template <Message M>
template <utils::Message M>
void Send(Address to_address, Address from_address, RequestId request_id, M &&message) {
auto type_info = TypeInfoFor(message);
@@ -138,7 +139,7 @@ class LocalTransportHandle {
cv_.notify_all();
}
template <Message RequestT, Message ResponseT>
template <utils::Message RequestT, utils::Message ResponseT>
ResponseFuture<ResponseT> SubmitRequest(Address to_address, Address from_address, RequestT &&request,
Duration timeout, std::function<void()> fill_notifier) {
auto [future, promise] = memgraph::io::FuturePromisePairWithNotifications<ResponseResult<ResponseT>>(
@@ -155,7 +156,7 @@ class LocalTransportHandle {
const auto now = Now();
const Time deadline = now + timeout;
RequestId request_id = 0;
RequestId request_id;
{
std::unique_lock<std::mutex> lock(mu_);

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -14,6 +14,7 @@
#include <boost/core/demangle.hpp>
#include "io/transport.hpp"
#include "utils/concrete_msg_sender.hpp"
#include "utils/type_info_ref.hpp"
namespace memgraph::io {
@@ -48,7 +49,7 @@ struct OpaqueMessage {
/// Return is the full std::variant<Ts...> type that holds the
/// full parameter pack without interfering with recursive
/// narrowing expansion.
template <typename Return, Message Head, Message... Rest>
template <typename Return, utils::Message Head, utils::Message... Rest>
std::optional<Return> Unpack(std::any &&a) {
if (typeid(Head) == a.type()) {
Head concrete = std::any_cast<Head>(std::move(a));
@@ -67,12 +68,12 @@ struct OpaqueMessage {
/// parameter pack for the types that they want to compare
/// with the any and potentially include in the returned
/// variant.
template <Message... Ms>
template <utils::Message... Ms>
requires(sizeof...(Ms) > 0) std::optional<std::variant<Ms...>> VariantFromAny(std::any &&a) {
return Unpack<std::variant<Ms...>, Ms...>(std::move(a));
}
template <Message... Ms>
template <utils::Message... Ms>
requires(sizeof...(Ms) > 0) std::optional<RequestEnvelope<Ms...>> Take() && {
std::optional<std::variant<Ms...>> m_opt = VariantFromAny<Ms...>(std::move(message));

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -13,8 +13,8 @@
#include <variant>
#include <coordinator/coordinator.hpp>
#include <io/rsm/raft.hpp>
#include "coordinator/coordinator.hpp"
#include "io/rsm/raft.hpp"
#include "query/v2/requests.hpp"
#include "utils/concepts.hpp"
@@ -41,6 +41,6 @@ using CoordinatorMessages =
using ShardMessages = std::variant<ReadRequest<StorageReadRequest>, AppendRequest<StorageWriteRequest>, AppendResponse,
WriteRequest<StorageWriteRequest>, VoteRequest, VoteResponse>;
using ShardManagerMessages = std::variant<WriteResponse<CoordinatorWriteResponses>>;
using ShardManagerMessages = std::variant<msgs::SuggestedSplitInfo, msgs::InitializeSplitShard>;
} // namespace memgraph::io::messages

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -19,17 +19,23 @@
#include <map>
#include <set>
#include <thread>
#include <type_traits>
#include <vector>
#include <boost/core/demangle.hpp>
#include "io/message_conversion.hpp"
#include "io/rsm/shard_rsm.hpp"
#include "io/simulator/simulator.hpp"
#include "io/transport.hpp"
#include "query/v2/requests.hpp"
#include "utils/concepts.hpp"
namespace memgraph::io::rsm {
template <typename Type>
concept HasShouldSplit = std::is_member_function_pointer<decltype(&Type::ShouldSplit)>::value;
/// Timeout and replication tunables
using namespace std::chrono_literals;
static constexpr auto kMinimumElectionTimeout = 100ms;
@@ -342,15 +348,21 @@ class Raft {
}
}
private:
// Raft paper - 5.3
// When the entry has been safely replicated, the leader applies the
// entry to its state machine and returns the result of that
// execution to the client.
//
// "Safely replicated" is defined as being known to be present
// on at least a majority of all peers (inclusive of the Leader).
void BumpCommitIndexAndReplyToClients(Leader &leader) {
template <typename = void>
requires HasShouldSplit<ReplicatedState> std::optional<msgs::SuggestedSplitInfo> ShouldSplit() {
return replicated_state_.ShouldSplit();
}
private :
// Raft paper - 5.3
// When the entry has been safely replicated, the leader applies the
// entry to its state machine and returns the result of that
// execution to the client.
//
// "Safely replicated" is defined as being known to be present
// on at least a majority of all peers (inclusive of the Leader).
void
BumpCommitIndexAndReplyToClients(Leader &leader) {
auto confirmed_log_sizes = std::vector<LogSize>{};
// We include our own log size in the calculation of the log
@@ -402,13 +414,17 @@ class Raft {
const PendingClientRequest client_request = std::move(leader.pending_client_requests.at(apply_index));
leader.pending_client_requests.erase(apply_index);
const WriteResponse<WriteResponseValue> resp{
.success = true,
.write_return = std::move(write_return),
.raft_index = apply_index,
};
if (client_request.request_id == 0) {
Log("not replying to Raft request with explicit request ID of 0");
} else {
const WriteResponse<WriteResponseValue> resp{
.success = true,
.write_return = std::move(write_return),
.raft_index = apply_index,
};
io_.Send(client_request.address, client_request.request_id, std::move(resp));
io_.Send(client_request.address, client_request.request_id, std::move(resp));
}
}
}
@@ -624,7 +640,7 @@ class Raft {
MG_ASSERT(std::max(req.term, state_.term) == req.term);
}
const VoteResponse res{
VoteResponse res{
.term = std::max(req.term, state_.term),
.committed_log_size = state_.committed_log_size,
.vote_granted = new_leader,

View File

@@ -65,7 +65,7 @@ class RsmClient {
size_t addr_index = io_.Rand(addr_distrib);
leader_ = server_addrs_[addr_index];
spdlog::debug("selecting a random leader at index {} with address {}", addr_index, leader_.ToString());
spdlog::trace("RsmClient selecting a random leader at index {} with address {}", addr_index, leader_.ToString());
}
template <typename ResponseT>
@@ -73,7 +73,7 @@ class RsmClient {
if (response.retry_leader) {
MG_ASSERT(!response.success, "retry_leader should never be set for successful responses");
leader_ = response.retry_leader.value();
spdlog::debug("client redirected to leader server {}", leader_.ToString());
spdlog::trace("RsmClient redirected to leader server {}", leader_.ToString());
}
if (!response.success) {
SelectRandomLeader();
@@ -139,6 +139,10 @@ class RsmClient {
}
std::optional<BasicResult<TimedOut, ReadResponseT>> PollAsyncReadRequest(const ReadinessToken &readiness_token) {
if (!async_reads_.contains(readiness_token.GetId())) {
spdlog::debug("RsmClient async_reads_ does not contain Polled readiness token {}", readiness_token.GetId());
return std::nullopt;
}
auto &async_request = async_reads_.at(readiness_token.GetId());
if (!async_request.future.IsReady()) {
@@ -158,7 +162,7 @@ class RsmClient {
if (result_has_error && past_time_out) {
// TODO static assert the exact type of error.
spdlog::debug("client timed out while trying to communicate with leader server {}", leader_.ToString());
spdlog::debug("RsmClient timed out while trying to communicate with leader server {}", leader_.ToString());
async_reads_.erase(readiness_token.GetId());
return TimedOut{};
}
@@ -171,7 +175,8 @@ class RsmClient {
if (read_get_response.success) {
async_reads_.erase(readiness_token.GetId());
spdlog::debug("returning read_return for RSM request");
spdlog::trace("RsmClient returning read_return and erasing async state for RSM request for token {}",
readiness_token.GetId());
return std::move(read_get_response.read_return);
}
} else {
@@ -209,6 +214,10 @@ class RsmClient {
}
std::optional<BasicResult<TimedOut, WriteResponseT>> PollAsyncWriteRequest(const ReadinessToken &readiness_token) {
if (!async_writes_.contains(readiness_token.GetId())) {
spdlog::debug("RsmClient async_writes_ does not contain Polled readiness token {}", readiness_token.GetId());
return std::nullopt;
}
auto &async_request = async_writes_.at(readiness_token.GetId());
if (!async_request.future.IsReady()) {
@@ -228,7 +237,7 @@ class RsmClient {
if (result_has_error && past_time_out) {
// TODO static assert the exact type of error.
spdlog::debug("client timed out while trying to communicate with leader server {}", leader_.ToString());
spdlog::debug("RsmClient timed out while trying to communicate with leader server {}", leader_.ToString());
async_writes_.erase(readiness_token.GetId());
return TimedOut{};
}

View File

@@ -22,7 +22,30 @@ namespace memgraph::io::simulator {
void SimulatorHandle::ShutDown() {
std::unique_lock<std::mutex> lock(mu_);
if (should_shut_down_) {
spdlog::warn("Simulator's ShutDown method called multiple times.");
return;
}
while (true) {
const size_t blocked_servers = blocked_on_receive_.size();
const bool all_servers_blocked = blocked_servers == server_addresses_.size();
if (all_servers_blocked) {
spdlog::trace("quiescent state detected - {} out of {} servers now blocked on receive", blocked_servers,
server_addresses_.size());
break;
}
spdlog::trace("not returning from quiescent because we see {} blocked out of {}", blocked_servers,
server_addresses_.size());
cv_.wait(lock);
}
should_shut_down_ = true;
for (auto it = promises_.begin(); it != promises_.end();) {
auto &[promise_key, dop] = *it;
std::move(dop).promise.TimeOut();

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -32,6 +32,7 @@
#include "io/simulator/simulator_stats.hpp"
#include "io/time.hpp"
#include "io/transport.hpp"
#include "utils/concrete_msg_sender.hpp"
namespace memgraph::io::simulator {
@@ -58,15 +59,16 @@ class SimulatorHandle {
std::uniform_int_distribution<int> drop_distrib_{0, 99};
SimulatorConfig config_;
MessageHistogramCollector histograms_;
RequestId request_id_counter_{0};
RequestId request_id_counter_{1};
bool TimeoutPromisesPastDeadline() {
bool timed_anything_out = false;
const Time now = cluster_wide_time_microseconds_;
for (auto it = promises_.begin(); it != promises_.end();) {
auto &[promise_key, dop] = *it;
if (dop.deadline < now && config_.perform_timeouts) {
spdlog::trace("timing out request from requester {}.", promise_key.requester_address.ToString());
spdlog::trace("Simulator timing out request from requester {}.", promise_key.requester_address.ToString());
std::move(dop).promise.TimeOut();
it = promises_.erase(it);
@@ -76,6 +78,7 @@ class SimulatorHandle {
++it;
}
}
return timed_anything_out;
}
@@ -105,13 +108,13 @@ class SimulatorHandle {
bool ShouldShutDown() const;
template <Message Request, Message Response>
template <utils::Message Request, utils::Message Response>
ResponseFuture<Response> SubmitRequest(Address to_address, Address from_address, Request &&request, Duration timeout,
std::function<bool()> &&maybe_tick_simulator,
std::function<void()> &&fill_notifier) {
auto type_info = TypeInfoFor(request);
std::string demangled_name = boost::core::demangle(type_info.get().name());
spdlog::trace("simulator sending request {} to {}", demangled_name, to_address);
spdlog::trace("Simulator sending request {} to {}", demangled_name, to_address);
auto [future, promise] = memgraph::io::FuturePromisePairWithNotifications<ResponseResult<Response>>(
// set notifier for when the Future::Wait is called
@@ -155,7 +158,7 @@ class SimulatorHandle {
return std::move(future);
}
template <Message... Ms>
template <utils::Message... Ms>
requires(sizeof...(Ms) > 0) RequestResult<Ms...> Receive(const Address &receiver, Duration timeout) {
std::unique_lock<std::mutex> lock(mu_);
@@ -182,20 +185,21 @@ class SimulatorHandle {
if (!should_shut_down_) {
if (!blocked_on_receive_.contains(receiver)) {
blocked_on_receive_.emplace(receiver);
spdlog::trace("blocking receiver {}", receiver.ToPartialAddress().port);
spdlog::trace("Simulator blocking receiver until it receives something or a timeout happens {}",
receiver.ToPartialAddress().port);
cv_.notify_all();
}
cv_.wait(lock);
}
}
spdlog::trace("timing out receiver {}", receiver.ToPartialAddress().port);
spdlog::trace("Simulator timing out receiver {}", receiver.ToPartialAddress().port);
return TimedOut{};
}
template <Message M>
template <utils::Message M>
void Send(Address to_address, Address from_address, RequestId request_id, M message) {
spdlog::trace("sending message from {} to {}", from_address.last_known_port, to_address.last_known_port);
spdlog::trace("Simulator sending message from {} to {}", from_address.last_known_port, to_address.last_known_port);
auto type_info = TypeInfoFor(message);
{
std::unique_lock<std::mutex> lock(mu_);

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -18,6 +18,7 @@
#include "io/notifier.hpp"
#include "io/simulator/simulator_handle.hpp"
#include "io/time.hpp"
#include "utils/concrete_msg_sender.hpp"
namespace memgraph::io::simulator {
@@ -33,7 +34,7 @@ class SimulatorTransport {
SimulatorTransport(std::shared_ptr<SimulatorHandle> simulator_handle, Address address, uint64_t seed)
: simulator_handle_(simulator_handle), address_(address), rng_(std::mt19937{seed}) {}
template <Message RequestT, Message ResponseT>
template <utils::Message RequestT, utils::Message ResponseT>
ResponseFuture<ResponseT> Request(Address to_address, Address from_address, RequestT request,
std::function<void()> notification, Duration timeout) {
std::function<bool()> tick_simulator = [handle_copy = simulator_handle_] {
@@ -44,12 +45,12 @@ class SimulatorTransport {
to_address, from_address, std::move(request), timeout, std::move(tick_simulator), std::move(notification));
}
template <Message... Ms>
template <utils::Message... Ms>
requires(sizeof...(Ms) > 0) RequestResult<Ms...> Receive(Address receiver_address, Duration timeout) {
return simulator_handle_->template Receive<Ms...>(receiver_address, timeout);
}
template <Message M>
template <utils::Message M>
void Send(Address to_address, Address from_address, uint64_t request_id, M message) {
return simulator_handle_->template Send<M>(to_address, from_address, request_id, message);
}

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -22,21 +22,16 @@
#include "io/message_histogram_collector.hpp"
#include "io/notifier.hpp"
#include "io/time.hpp"
#include "utils/concrete_msg_sender.hpp"
#include "utils/result.hpp"
namespace memgraph::io {
using memgraph::utils::BasicResult;
// TODO(tyler) ensure that Message continues to represent
// reasonable constraints around message types over time,
// as we adapt things to use Thrift-generated message types.
template <typename T>
concept Message = std::same_as<T, std::decay_t<T>>;
using RequestId = uint64_t;
template <Message M>
template <utils::Message M>
struct ResponseEnvelope {
M message;
RequestId request_id;
@@ -45,16 +40,16 @@ struct ResponseEnvelope {
Duration response_latency;
};
template <Message M>
template <utils::Message M>
using ResponseResult = BasicResult<TimedOut, ResponseEnvelope<M>>;
template <Message M>
using ResponseFuture = memgraph::io::Future<ResponseResult<M>>;
template <utils::Message M>
using ResponseFuture = Future<ResponseResult<M>>;
template <Message M>
using ResponsePromise = memgraph::io::Promise<ResponseResult<M>>;
template <utils::Message M>
using ResponsePromise = Promise<ResponseResult<M>>;
template <Message... Ms>
template <utils::Message... Ms>
struct RequestEnvelope {
std::variant<Ms...> message;
RequestId request_id;
@@ -62,7 +57,7 @@ struct RequestEnvelope {
Address from_address;
};
template <Message... Ms>
template <utils::Message... Ms>
using RequestResult = BasicResult<TimedOut, RequestEnvelope<Ms...>>;
template <typename I>
@@ -82,7 +77,7 @@ class Io {
Duration GetDefaultTimeout() { return default_timeout_; }
/// Issue a request with an explicit timeout in microseconds provided. This tends to be used by clients.
template <Message RequestT, Message ResponseT>
template <utils::Message RequestT, utils::Message ResponseT>
ResponseFuture<ResponseT> RequestWithTimeout(Address address, RequestT request, Duration timeout) {
const Address from_address = address_;
std::function<void()> fill_notifier = nullptr;
@@ -92,7 +87,7 @@ class Io {
/// Issue a request that times out after the default timeout. This tends
/// to be used by clients.
template <Message RequestT, Message ResponseT>
template <utils::Message RequestT, utils::Message ResponseT>
ResponseFuture<ResponseT> Request(Address to_address, RequestT request) {
const Duration timeout = default_timeout_;
const Address from_address = address_;
@@ -102,7 +97,7 @@ class Io {
}
/// Issue a request that will notify a Notifier when it is filled or times out.
template <Message RequestT, Message ResponseT>
template <utils::Message RequestT, utils::Message ResponseT>
ResponseFuture<ResponseT> RequestWithNotification(Address to_address, RequestT request, Notifier notifier,
ReadinessToken readiness_token) {
const Duration timeout = default_timeout_;
@@ -113,7 +108,7 @@ class Io {
}
/// Issue a request that will notify a Notifier when it is filled or times out.
template <Message RequestT, Message ResponseT>
template <utils::Message RequestT, utils::Message ResponseT>
ResponseFuture<ResponseT> RequestWithNotificationAndTimeout(Address to_address, RequestT request, Notifier notifier,
ReadinessToken readiness_token, Duration timeout) {
const Address from_address = address_;
@@ -124,14 +119,14 @@ class Io {
/// Wait for an explicit number of microseconds for a request of one of the
/// provided types to arrive. This tends to be used by servers.
template <Message... Ms>
template <utils::Message... Ms>
RequestResult<Ms...> ReceiveWithTimeout(Duration timeout) {
return implementation_.template Receive<Ms...>(address_, timeout);
}
/// Wait the default number of microseconds for a request of one of the
/// provided types to arrive. This tends to be used by servers.
template <Message... Ms>
template <utils::Message... Ms>
requires(sizeof...(Ms) > 0) RequestResult<Ms...> Receive() {
const Duration timeout = default_timeout_;
return implementation_.template Receive<Ms...>(address_, timeout);
@@ -140,7 +135,7 @@ class Io {
/// Send a message in a best-effort fashion. This is used for messaging where
/// responses are not necessarily expected, and for servers to respond to requests.
/// If you need reliable delivery, this must be built on-top. TCP is not enough for most use cases.
template <Message M>
template <utils::Message M>
void Send(Address to_address, RequestId request_id, M message) {
Address from_address = address_;
return implementation_.template Send<M>(to_address, from_address, request_id, std::move(message));
@@ -161,6 +156,7 @@ class Io {
}
Address GetAddress() { return address_; }
void SetAddress(Address address) { address_ = address; }
Io<I> ForkLocal(boost::uuids::uuid uuid) {
@@ -173,5 +169,16 @@ class Io {
}
LatencyHistogramSummaries ResponseLatencies() { return implementation_.ResponseLatencies(); }
template <utils::Message M>
utils::Sender<M> GetSender(Address address) {
Io<I> io_copy = Io(implementation_, address_);
std::function<void(M)> sender = [address, io_copy](M &&message) mutable {
io_copy.template Send<M>(address, 0, std::forward<M>(message));
};
return utils::Sender{sender};
}
};
}; // namespace memgraph::io

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -80,7 +80,7 @@ class MachineManager {
config_(config),
coordinator_address_(io.GetAddress().ForkLocalCoordinator()),
shard_manager_{io.ForkLocal(io.GetAddress().ForkLocalShardManager().unique_id), config.shard_worker_threads,
coordinator_address_} {
coordinator_address_, config_.sync_message_handling} {
auto coordinator_io = io.ForkLocal(coordinator_address_.unique_id);
CoordinatorWorker coordinator_worker{coordinator_io, coordinator_queue_, coordinator};
coordinator_handle_ = std::jthread([coordinator = std::move(coordinator_worker)]() mutable { coordinator.Run(); });
@@ -93,6 +93,7 @@ class MachineManager {
~MachineManager() {
if (coordinator_handle_.joinable()) {
MaybeBlockOnSyncHandling();
coordinator_queue_.Push(coordinator::coordinator_worker::ShutDown{});
coordinator_handle_.join();
}
@@ -114,10 +115,7 @@ class MachineManager {
uint64_t next_us = next_cron_.time_since_epoch().count();
if (now >= next_cron_) {
spdlog::info("now {} >= next_cron_ {}", now_us, next_us);
next_cron_ = Cron();
} else {
spdlog::info("now {} < next_cron_ {}", now_us, next_us);
}
Duration receive_timeout = std::max(next_cron_, now) - now;
@@ -127,16 +125,17 @@ class MachineManager {
std::variant<ReadRequest<CoordinatorReadRequests>, AppendRequest<CoordinatorWriteRequests>, AppendResponse,
WriteRequest<CoordinatorWriteRequests>, VoteRequest, VoteResponse,
WriteResponse<CoordinatorWriteResponses>, ReadRequest<StorageReadRequest>,
AppendRequest<StorageWriteRequest>, WriteRequest<StorageWriteRequest>>;
AppendRequest<StorageWriteRequest>, WriteRequest<StorageWriteRequest>, msgs::SuggestedSplitInfo,
msgs::InitializeSplitShard>;
spdlog::info("MM waiting on Receive on address {}", io_.GetAddress().ToString());
spdlog::trace("MachineManager waiting on Receive on address {}", io_.GetAddress().ToString());
// Note: this parameter pack must be kept in-sync with the AllMessages parameter pack above
auto request_result = io_.template ReceiveWithTimeout<
ReadRequest<CoordinatorReadRequests>, AppendRequest<CoordinatorWriteRequests>, AppendResponse,
WriteRequest<CoordinatorWriteRequests>, VoteRequest, VoteResponse, WriteResponse<CoordinatorWriteResponses>,
ReadRequest<StorageReadRequest>, AppendRequest<StorageWriteRequest>, WriteRequest<StorageWriteRequest>>(
receive_timeout);
ReadRequest<StorageReadRequest>, AppendRequest<StorageWriteRequest>, WriteRequest<StorageWriteRequest>,
msgs::SuggestedSplitInfo, msgs::InitializeSplitShard>(receive_timeout);
if (request_result.HasError()) {
// time to do Cron
@@ -145,11 +144,13 @@ class MachineManager {
auto &&request_envelope = std::move(request_result.GetValue());
spdlog::info("MM got message to {}", request_envelope.to_address.ToString());
spdlog::trace("MachineManager received message addressed to {}", request_envelope.to_address.ToString());
// If message is for the coordinator, cast it to subset and pass it to the coordinator
bool to_coordinator = coordinator_address_ == request_envelope.to_address;
if (to_coordinator) {
spdlog::trace("MachineManager got message addressed to the coordinator");
std::optional<CoordinatorMessages> conversion_attempt =
ConvertVariant<AllMessages, ReadRequest<CoordinatorReadRequests>, AppendRequest<CoordinatorWriteRequests>,
AppendResponse, WriteRequest<CoordinatorWriteRequests>, VoteRequest, VoteResponse>(
@@ -157,8 +158,6 @@ class MachineManager {
MG_ASSERT(conversion_attempt.has_value(), "coordinator message conversion failed");
spdlog::info("got coordinator message");
CoordinatorMessages &&cm = std::move(conversion_attempt.value());
CoordinatorRouteMessage route_message{
@@ -172,14 +171,20 @@ class MachineManager {
}
bool to_sm = shard_manager_.GetAddress() == request_envelope.to_address;
spdlog::info("smm: {}", shard_manager_.GetAddress().ToString());
if (to_sm) {
spdlog::trace("MachineManager got shard manager message, addressed to {}", request_envelope.to_address);
std::optional<ShardManagerMessages> conversion_attempt =
ConvertVariant<AllMessages, WriteResponse<CoordinatorWriteResponses>>(std::move(request_envelope.message));
ConvertVariant<AllMessages, msgs::SuggestedSplitInfo, msgs::InitializeSplitShard>(
std::move(request_envelope.message));
MG_ASSERT(conversion_attempt.has_value(), "shard manager message conversion failed");
spdlog::info("got shard manager message");
if (!conversion_attempt.has_value()) {
spdlog::debug(
"MachineManager dropping message with an unexpected type addressed to the ShardManager, assuming it was "
"a HeartbeatResponse that "
"timed out");
continue;
}
ShardManagerMessages &&smm = std::move(conversion_attempt.value());
shard_manager_.Receive(std::forward<ShardManagerMessages>(smm), request_envelope.request_id,
@@ -197,7 +202,7 @@ class MachineManager {
MG_ASSERT(conversion_attempt.has_value(), "shard rsm message conversion failed for {} - incorrect message type",
request_envelope.to_address.ToString());
spdlog::info("got shard rsm message");
spdlog::trace("MachineManager forwarding message to shard rsm");
ShardMessages &&sm = std::move(conversion_attempt.value());
shard_manager_.Route(std::forward<ShardMessages>(sm), request_envelope.request_id, request_envelope.to_address,
@@ -221,7 +226,7 @@ class MachineManager {
}
Time Cron() {
spdlog::info("running MachineManager::Cron, address {}", io_.GetAddress().ToString());
spdlog::trace("MachineManager running Cron, address {}", io_.GetAddress().ToString());
coordinator_queue_.Push(coordinator::coordinator_worker::Cron{});
MaybeBlockOnSyncHandling();
Time ret = shard_manager_.Cron();

View File

@@ -622,13 +622,16 @@ int main(int argc, char **argv) {
if (FLAGS_split_file.empty()) {
const std::string property{"property"};
const std::string label{"label"};
// TODO(tyler) make this more easily configurable in the short-term
const auto split_threshold = 4;
auto prop_map = sm.AllocatePropertyIds(std::vector<std::string>{property});
auto edge_type_map = sm.AllocateEdgeTypeIds(std::vector<std::string>{"TO"});
std::vector<memgraph::storage::v3::SchemaProperty> schema{
{prop_map.at(property), memgraph::common::SchemaType::INT}};
sm.InitializeNewLabel(label, schema, 1, sm.shard_map_version);
sm.SplitShard(sm.GetHlc(), *sm.GetLabelId(label),
std::vector<memgraph::storage::v3::PropertyValue>{memgraph::storage::v3::PropertyValue{2}});
// TODO(tyler) remove pre-initialization completely along with split files
sm.InitializeNewLabel(label, schema, 1, split_threshold, sm.shard_map_version);
} else {
std::ifstream input{FLAGS_split_file, std::ios::in};
MG_ASSERT(input.is_open(), "Cannot open split file to read: {}", FLAGS_split_file);

View File

@@ -295,6 +295,7 @@ class Interpreter final {
void Abort();
const RequestRouterInterface *GetRequestRouter() const { return request_router_.get(); }
void InstallSimulatorTicker(std::function<bool()> &&tick_simulator) {
request_router_->InstallSimulatorTicker(tick_simulator);
}

View File

@@ -99,6 +99,7 @@ class RequestRouterInterface {
virtual ~RequestRouterInterface() = default;
virtual coordinator::Hlc RefreshShardMap() = 0;
virtual void StartTransaction() = 0;
virtual void Commit() = 0;
virtual std::vector<VertexAccessor> ScanVertices(std::optional<std::string> label) = 0;
@@ -150,77 +151,92 @@ class RequestRouter : public RequestRouterInterface {
notifier_.InstallSimulatorTicker(tick_simulator);
}
void StartTransaction() override {
coordinator::HlcRequest req{.last_shard_map_version = shards_map_.GetHlc()};
coordinator::Hlc RefreshShardMap() override {
coordinator::HlcRequest req{.last_shard_map_version = shard_map_.GetHlc()};
CoordinatorWriteRequests write_req = req;
spdlog::trace("sending hlc request to start transaction");
spdlog::trace("RequestRouter sending hlc request to get an HLC and refresh the ShardMap");
auto write_res = coord_cli_.SendWriteRequest(write_req);
spdlog::trace("received hlc response to start transaction");
if (write_res.HasError()) {
throw std::runtime_error("HLC request failed");
spdlog::trace("RequestRouter received hlc response to start transaction");
// TODO(tyler) enforce max retries here
while (write_res.HasError()) {
spdlog::debug("RequestRouter retrying HlcRequest to coordinator after timeout");
write_res = coord_cli_.SendWriteRequest(write_req);
}
auto coordinator_write_response = write_res.GetValue();
auto hlc_response = std::get<coordinator::HlcResponse>(coordinator_write_response);
// Transaction ID to be used later...
transaction_id_ = hlc_response.new_hlc;
if (hlc_response.fresher_shard_map) {
shards_map_ = hlc_response.fresher_shard_map.value();
shard_map_ = hlc_response.fresher_shard_map.value();
SetUpNameIdMappers();
}
return hlc_response.new_hlc;
}
void Commit() override {
coordinator::HlcRequest req{.last_shard_map_version = shards_map_.GetHlc()};
CoordinatorWriteRequests write_req = req;
spdlog::trace("sending hlc request before committing transaction");
auto write_res = coord_cli_.SendWriteRequest(write_req);
spdlog::trace("received hlc response before committing transaction");
if (write_res.HasError()) {
throw std::runtime_error("HLC request for commit failed");
}
auto coordinator_write_response = write_res.GetValue();
auto hlc_response = std::get<coordinator::HlcResponse>(coordinator_write_response);
if (hlc_response.fresher_shard_map) {
shards_map_ = hlc_response.fresher_shard_map.value();
SetUpNameIdMappers();
}
auto commit_timestamp = hlc_response.new_hlc;
void StartTransaction() override {
// Transaction ID to be used later...
transaction_id_ = RefreshShardMap();
}
bool CommitInner(coordinator::Hlc commit_timestamp) {
msgs::CommitRequest commit_req{.transaction_id = transaction_id_, .commit_timestamp = commit_timestamp};
for (const auto &[label, space] : shards_map_.label_spaces) {
for (const auto &[label, space] : shard_map_.label_spaces) {
for (const auto &[key, shard] : space.shards) {
auto &storage_client = GetStorageClientForShard(shard);
// TODO(kostasrim) Currently requests return the result directly. Adjust this when the API works MgFuture
// instead.
auto commit_response = storage_client.SendWriteRequest(commit_req);
// RETRY on timeouts?
// Sometimes this produces a timeout. Temporary solution is to use a while(true) as was done in shard_map test
if (commit_response.HasError()) {
throw std::runtime_error("Commit request timed out");
while (commit_response.HasError()) {
spdlog::debug("RequestRouter retrying Commit request due to timeout");
commit_response = storage_client.SendWriteRequest(commit_req);
}
msgs::WriteResponses write_response_variant = commit_response.GetValue();
auto &response = std::get<msgs::CommitResponse>(write_response_variant);
if (response.error) {
throw std::runtime_error("Commit request did not succeed");
if (response.error->code == common::ErrorCode::STALE_SHARD_MAP) {
RefreshShardMap();
// signal to caller that we should retry
return false;
} else {
spdlog::warn("RequestRouter throwing because of unhandled commit failure 3: {}",
common::ErrorCodeToString(response.error->code));
throw std::runtime_error("Commit request did not succeed");
}
}
}
}
return true;
}
void Commit() override {
spdlog::trace("sending hlc request before committing transaction");
auto commit_timestamp = RefreshShardMap();
// TODO(tyler) enforce max commit retries here when we go distributed
while (true) {
// Commit is idempotent, so it's fine to retry it to the same shards if it fails
bool success = CommitInner(commit_timestamp);
if (success) {
return;
}
}
}
storage::v3::EdgeTypeId NameToEdgeType(const std::string &name) const override {
return shards_map_.GetEdgeTypeId(name).value();
return shard_map_.GetEdgeTypeId(name).value();
}
storage::v3::PropertyId NameToProperty(const std::string &name) const override {
return shards_map_.GetPropertyId(name).value();
return shard_map_.GetPropertyId(name).value();
}
storage::v3::LabelId NameToLabel(const std::string &name) const override {
return shards_map_.GetLabelId(name).value();
return shard_map_.GetLabelId(name).value();
}
const std::string &PropertyToName(storage::v3::PropertyId id) const override {
@@ -232,8 +248,8 @@ class RequestRouter : public RequestRouterInterface {
}
bool IsPrimaryProperty(storage::v3::LabelId primary_label, storage::v3::PropertyId property) const override {
const auto schema_it = shards_map_.schemas.find(primary_label);
MG_ASSERT(schema_it != shards_map_.schemas.end(), "Invalid primary label id: {}", primary_label.AsUint());
const auto schema_it = shard_map_.schemas.find(primary_label);
MG_ASSERT(schema_it != shard_map_.schemas.end(), "Invalid primary label id: {}", primary_label.AsUint());
return std::find_if(schema_it->second.begin(), schema_it->second.end(), [property](const auto &schema_prop) {
return schema_prop.property_id == property;
@@ -241,94 +257,136 @@ class RequestRouter : public RequestRouterInterface {
}
const std::vector<coordinator::SchemaProperty> &GetSchemaForLabel(storage::v3::LabelId label) const override {
return shards_map_.schemas.at(label);
return shard_map_.schemas.at(label);
}
bool IsPrimaryLabel(storage::v3::LabelId label) const override { return shards_map_.label_spaces.contains(label); }
bool IsPrimaryLabel(storage::v3::LabelId label) const override { return shard_map_.label_spaces.contains(label); }
// TODO(kostasrim) Simplify return result
std::vector<VertexAccessor> ScanVertices(std::optional<std::string> label) override {
// create requests
auto requests_to_be_sent = RequestsForScanVertices(label);
// TODO(tyler) enforce max commit retries here when we go distributed
while (true) {
// create requests
auto requests_to_be_sent = RequestsForScanVertices(label);
spdlog::trace("created {} ScanVertices requests", requests_to_be_sent.size());
spdlog::trace("created {} ScanVertices requests", requests_to_be_sent.size());
// begin all requests in parallel
RunningRequests<msgs::ScanVerticesRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
auto &storage_client = GetStorageClientForShard(request.shard);
storage_client.SendAsyncReadRequest(request.request, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
}
spdlog::trace("sent {} ScanVertices requests in parallel", running_requests.size());
// drive requests to completion
auto responses = DriveReadResponses<msgs::ScanVerticesRequest, msgs::ScanVerticesResponse>(running_requests);
spdlog::trace("got back {} ScanVertices responses after driving to completion", responses.size());
// convert responses into VertexAccessor objects to return
std::vector<VertexAccessor> accessors;
accessors.reserve(responses.size());
for (auto &response : responses) {
for (auto &result_row : response.results) {
accessors.emplace_back(VertexAccessor(std::move(result_row.vertex), std::move(result_row.props), this));
// begin all requests in parallel
RunningRequests<msgs::ScanVerticesRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
auto &storage_client = GetStorageClientForShard(request.shard);
storage_client.SendAsyncReadRequest(request.request, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
}
}
spdlog::trace("sent {} ScanVertices requests in parallel", running_requests.size());
return accessors;
// drive requests to completion
auto responses_result =
DriveReadResponses<msgs::ScanVerticesRequest, msgs::ScanVerticesResponse>(running_requests);
if (responses_result.HasError()) {
spdlog::debug(
"RequestRouter refreshing ShardMap and re-sending requests for ScanVertices after Shard version mismatch");
RefreshShardMap();
continue;
}
auto responses = responses_result.GetValue();
spdlog::trace("got back {} ScanVertices responses after driving to completion", responses.size());
// convert responses into VertexAccessor objects to return
std::vector<VertexAccessor> accessors;
accessors.reserve(responses.size());
for (auto &response : responses) {
for (auto &result_row : response.results) {
accessors.emplace_back(VertexAccessor(std::move(result_row.vertex), std::move(result_row.props), this));
}
}
return accessors;
}
}
std::vector<msgs::CreateVerticesResponse> CreateVertices(std::vector<msgs::NewVertex> new_vertices) override {
MG_ASSERT(!new_vertices.empty());
// create requests
std::vector<ShardRequestState<msgs::CreateVerticesRequest>> requests_to_be_sent =
RequestsForCreateVertices(new_vertices);
spdlog::trace("created {} CreateVertices requests", requests_to_be_sent.size());
// begin all requests in parallel
RunningRequests<msgs::CreateVerticesRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
for (auto &new_vertex : request.request.new_vertices) {
new_vertex.label_ids.erase(new_vertex.label_ids.begin());
}
auto &storage_client = GetStorageClientForShard(request.shard);
storage_client.SendAsyncWriteRequest(request.request, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
for (auto &new_vertex : new_vertices) {
new_vertex.idempotency_token = idempotency_token_generator_++;
}
spdlog::trace("sent {} CreateVertices requests in parallel", running_requests.size());
// drive requests to completion
return DriveWriteResponses<msgs::CreateVerticesRequest, msgs::CreateVerticesResponse>(running_requests);
// TODO(tyler) enforce max commit retries here when we go distributed
while (true) {
// create requests
std::vector<ShardRequestState<msgs::CreateVerticesRequest>> requests_to_be_sent =
RequestsForCreateVertices(new_vertices);
spdlog::trace("created {} CreateVertices requests", requests_to_be_sent.size());
// begin all requests in parallel
RunningRequests<msgs::CreateVerticesRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
for (auto &new_vertex : request.request.new_vertices) {
new_vertex.label_ids.erase(new_vertex.label_ids.begin());
}
auto &storage_client = GetStorageClientForShard(request.shard);
storage_client.SendAsyncWriteRequest(request.request, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
}
spdlog::trace("sent {} CreateVertices requests in parallel", running_requests.size());
// drive requests to completion
auto result = DriveWriteResponses<msgs::CreateVerticesRequest, msgs::CreateVerticesResponse>(running_requests);
if (result.HasValue()) {
return result.GetValue();
} else {
// retry request that failed due to outdated ShardMap
RefreshShardMap();
}
}
}
std::vector<msgs::CreateExpandResponse> CreateExpand(std::vector<msgs::NewExpand> new_edges) override {
MG_ASSERT(!new_edges.empty());
// create requests
std::vector<ShardRequestState<msgs::CreateExpandRequest>> requests_to_be_sent =
RequestsForCreateExpand(std::move(new_edges));
// begin all requests in parallel
RunningRequests<msgs::CreateExpandRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
auto &storage_client = GetStorageClientForShard(request.shard);
msgs::WriteRequests req = request.request;
storage_client.SendAsyncWriteRequest(req, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
for (auto &new_edge : new_edges) {
new_edge.idempotency_token = idempotency_token_generator_++;
}
// drive requests to completion
return DriveWriteResponses<msgs::CreateExpandRequest, msgs::CreateExpandResponse>(running_requests);
// TODO(tyler) enforce max commit retries here when we go distributed
while (true) {
// create requests
std::vector<ShardRequestState<msgs::CreateExpandRequest>> requests_to_be_sent =
RequestsForCreateExpand(new_edges);
// begin all requests in parallel
RunningRequests<msgs::CreateExpandRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
auto &storage_client = GetStorageClientForShard(request.shard);
msgs::WriteRequests req = request.request;
storage_client.SendAsyncWriteRequest(req, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
}
// drive requests to completion
auto result = DriveWriteResponses<msgs::CreateExpandRequest, msgs::CreateExpandResponse>(running_requests);
if (result.HasValue()) {
return result.GetValue();
} else {
// retry request that failed due to outdated ShardMap
RefreshShardMap();
}
}
}
std::vector<msgs::ExpandOneResultRow> ExpandOne(msgs::ExpandOneRequest request) override {
@@ -338,81 +396,107 @@ class RequestRouter : public RequestRouterInterface {
// For each vertex U, the ExpandOne will result in <U, Edges>. The destination vertex and its properties
// must be fetched again with an ExpandOne(Edges.dst)
// create requests
std::vector<ShardRequestState<msgs::ExpandOneRequest>> requests_to_be_sent = RequestsForExpandOne(request);
// TODO(tyler) enforce max commit retries here when we go distributed
while (true) {
// create requests
std::vector<ShardRequestState<msgs::ExpandOneRequest>> requests_to_be_sent = RequestsForExpandOne(request);
// begin all requests in parallel
RunningRequests<msgs::ExpandOneRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
auto &storage_client = GetStorageClientForShard(request.shard);
msgs::ReadRequests req = request.request;
storage_client.SendAsyncReadRequest(req, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
// begin all requests in parallel
RunningRequests<msgs::ExpandOneRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
auto &storage_client = GetStorageClientForShard(request.shard);
msgs::ReadRequests req = request.request;
storage_client.SendAsyncReadRequest(req, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
}
// drive requests to completion
auto responses_result = DriveReadResponses<msgs::ExpandOneRequest, msgs::ExpandOneResponse>(running_requests);
if (responses_result.HasError()) {
spdlog::debug(
"RequestRouter refreshing ShardMap and re-sending requests for ExpandOne after Shard version mismatch");
RefreshShardMap();
continue;
}
auto responses = responses_result.GetValue();
// post-process responses
std::vector<msgs::ExpandOneResultRow> result_rows;
const auto total_row_count = std::accumulate(
responses.begin(), responses.end(), 0, [](const int64_t partial_count, const msgs::ExpandOneResponse &resp) {
return partial_count + resp.result.size();
});
result_rows.reserve(total_row_count);
for (auto &response : responses) {
result_rows.insert(result_rows.end(), std::make_move_iterator(response.result.begin()),
std::make_move_iterator(response.result.end()));
}
return result_rows;
}
// drive requests to completion
auto responses = DriveReadResponses<msgs::ExpandOneRequest, msgs::ExpandOneResponse>(running_requests);
// post-process responses
std::vector<msgs::ExpandOneResultRow> result_rows;
const auto total_row_count = std::accumulate(responses.begin(), responses.end(), 0,
[](const int64_t partial_count, const msgs::ExpandOneResponse &resp) {
return partial_count + resp.result.size();
});
result_rows.reserve(total_row_count);
for (auto &response : responses) {
result_rows.insert(result_rows.end(), std::make_move_iterator(response.result.begin()),
std::make_move_iterator(response.result.end()));
}
return result_rows;
}
std::vector<msgs::GetPropertiesResultRow> GetProperties(msgs::GetPropertiesRequest requests) override {
requests.transaction_id = transaction_id_;
// create requests
std::vector<ShardRequestState<msgs::GetPropertiesRequest>> requests_to_be_sent =
RequestsForGetProperties(std::move(requests));
// begin all requests in parallel
RunningRequests<msgs::GetPropertiesRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
auto &storage_client = GetStorageClientForShard(request.shard);
msgs::ReadRequests req = request.request;
storage_client.SendAsyncReadRequest(req, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
// TODO(tyler) enforce max commit retries here when we go distributed
while (true) {
// create requests
std::vector<ShardRequestState<msgs::GetPropertiesRequest>> requests_to_be_sent =
RequestsForGetProperties(requests);
// begin all requests in parallel
RunningRequests<msgs::GetPropertiesRequest> running_requests = {};
running_requests.reserve(requests_to_be_sent.size());
for (size_t i = 0; i < requests_to_be_sent.size(); i++) {
auto &request = requests_to_be_sent[i];
io::ReadinessToken readiness_token{i};
auto &storage_client = GetStorageClientForShard(request.shard);
msgs::ReadRequests req = request.request;
storage_client.SendAsyncReadRequest(req, notifier_, readiness_token);
running_requests.emplace(readiness_token.GetId(), request);
}
// drive requests to completion
auto responses_result =
DriveReadResponses<msgs::GetPropertiesRequest, msgs::GetPropertiesResponse>(running_requests);
if (responses_result.HasError()) {
spdlog::debug(
"RequestRouter refreshing ShardMap and re-sending requests for GetProperties after Shard version mismatch");
RefreshShardMap();
continue;
}
auto responses = responses_result.GetValue();
// post-process responses
std::vector<msgs::GetPropertiesResultRow> result_rows;
for (auto &&response : responses) {
std::move(response.result_row.begin(), response.result_row.end(), std::back_inserter(result_rows));
}
return result_rows;
}
// drive requests to completion
auto responses = DriveReadResponses<msgs::GetPropertiesRequest, msgs::GetPropertiesResponse>(running_requests);
// post-process responses
std::vector<msgs::GetPropertiesResultRow> result_rows;
for (auto &&response : responses) {
std::move(response.result_row.begin(), response.result_row.end(), std::back_inserter(result_rows));
}
return result_rows;
}
std::optional<storage::v3::PropertyId> MaybeNameToProperty(const std::string &name) const override {
return shards_map_.GetPropertyId(name);
return shard_map_.GetPropertyId(name);
}
std::optional<storage::v3::EdgeTypeId> MaybeNameToEdgeType(const std::string &name) const override {
return shards_map_.GetEdgeTypeId(name);
return shard_map_.GetEdgeTypeId(name);
}
std::optional<storage::v3::LabelId> MaybeNameToLabel(const std::string &name) const override {
return shards_map_.GetLabelId(name);
return shard_map_.GetLabelId(name);
}
private:
@@ -420,12 +504,13 @@ class RequestRouter : public RequestRouterInterface {
const std::vector<msgs::NewVertex> &new_vertices) {
std::map<ShardMetadata, msgs::CreateVerticesRequest> per_shard_request_table;
for (auto &new_vertex : new_vertices) {
for (const auto &new_vertex : new_vertices) {
MG_ASSERT(!new_vertex.label_ids.empty(), "No label_ids provided for new vertex in RequestRouter::CreateVertices");
auto shard = shards_map_.GetShardForKey(new_vertex.label_ids[0].id,
storage::conversions::ConvertPropertyVector(new_vertex.primary_key));
auto shard = shard_map_.GetShardForKey(new_vertex.label_ids[0].id,
storage::conversions::ConvertPropertyVector(new_vertex.primary_key));
if (!per_shard_request_table.contains(shard)) {
msgs::CreateVerticesRequest create_v_rqst{.transaction_id = transaction_id_};
msgs::CreateVerticesRequest create_v_rqst{.transaction_id = transaction_id_,
.shard_map_version = shard_map_.shard_map_version};
per_shard_request_table.insert(std::pair(shard, std::move(create_v_rqst)));
}
per_shard_request_table[shard].new_vertices.push_back(std::move(new_vertex));
@@ -445,7 +530,7 @@ class RequestRouter : public RequestRouterInterface {
}
std::vector<ShardRequestState<msgs::CreateExpandRequest>> RequestsForCreateExpand(
std::vector<msgs::NewExpand> new_expands) {
const std::vector<msgs::NewExpand> &new_expands) {
std::map<ShardMetadata, msgs::CreateExpandRequest> per_shard_request_table;
auto ensure_shard_exists_in_table = [&per_shard_request_table,
transaction_id = transaction_id_](const ShardMetadata &shard) {
@@ -456,14 +541,14 @@ class RequestRouter : public RequestRouterInterface {
};
for (auto &new_expand : new_expands) {
const auto shard_src_vertex = shards_map_.GetShardForKey(
const auto shard_src_vertex = shard_map_.GetShardForKey(
new_expand.src_vertex.first.id, storage::conversions::ConvertPropertyVector(new_expand.src_vertex.second));
const auto shard_dest_vertex = shards_map_.GetShardForKey(
const auto shard_dest_vertex = shard_map_.GetShardForKey(
new_expand.dest_vertex.first.id, storage::conversions::ConvertPropertyVector(new_expand.dest_vertex.second));
ensure_shard_exists_in_table(shard_src_vertex);
if (shard_src_vertex != shard_dest_vertex) {
if (shard_src_vertex.peers != shard_dest_vertex.peers) {
ensure_shard_exists_in_table(shard_dest_vertex);
per_shard_request_table[shard_dest_vertex].new_expands.push_back(new_expand);
}
@@ -487,12 +572,12 @@ class RequestRouter : public RequestRouterInterface {
const std::optional<std::string> &label) {
std::vector<coordinator::Shards> multi_shards;
if (label) {
const auto label_id = shards_map_.GetLabelId(*label);
const auto label_id = shard_map_.GetLabelId(*label);
MG_ASSERT(label_id);
MG_ASSERT(IsPrimaryLabel(*label_id));
multi_shards = {shards_map_.GetShardsForLabel(*label)};
multi_shards = {shard_map_.GetShardsForLabel(*label)};
} else {
multi_shards = shards_map_.GetAllShards();
multi_shards = shard_map_.GetAllShards();
}
std::vector<ShardRequestState<msgs::ScanVerticesRequest>> requests = {};
@@ -503,6 +588,7 @@ class RequestRouter : public RequestRouterInterface {
msgs::ScanVerticesRequest request;
request.transaction_id = transaction_id_;
request.shard_map_version = shard_map_.shard_map_version;
request.start_id.second = storage::conversions::ConvertValueVector(key);
ShardRequestState<msgs::ScanVerticesRequest> shard_request_state{
@@ -525,7 +611,7 @@ class RequestRouter : public RequestRouterInterface {
for (auto &vertex : request.src_vertices) {
auto shard =
shards_map_.GetShardForKey(vertex.first.id, storage::conversions::ConvertPropertyVector(vertex.second));
shard_map_.GetShardForKey(vertex.first.id, storage::conversions::ConvertPropertyVector(vertex.second));
if (!per_shard_request_table.contains(shard)) {
per_shard_request_table.insert(std::pair(shard, top_level_rqst_template));
}
@@ -547,7 +633,7 @@ class RequestRouter : public RequestRouterInterface {
}
std::vector<ShardRequestState<msgs::GetPropertiesRequest>> RequestsForGetProperties(
msgs::GetPropertiesRequest &&request) {
const msgs::GetPropertiesRequest &request) {
std::map<ShardMetadata, msgs::GetPropertiesRequest> per_shard_request_table;
auto top_level_rqst_template = request;
top_level_rqst_template.transaction_id = transaction_id_;
@@ -556,7 +642,7 @@ class RequestRouter : public RequestRouterInterface {
for (auto &&vertex : request.vertex_ids) {
auto shard =
shards_map_.GetShardForKey(vertex.first.id, storage::conversions::ConvertPropertyVector(vertex.second));
shard_map_.GetShardForKey(vertex.first.id, storage::conversions::ConvertPropertyVector(vertex.second));
if (!per_shard_request_table.contains(shard)) {
per_shard_request_table.insert(std::pair(shard, top_level_rqst_template));
}
@@ -565,7 +651,7 @@ class RequestRouter : public RequestRouterInterface {
for (auto &[vertex, maybe_edge] : request.vertices_and_edges) {
auto shard =
shards_map_.GetShardForKey(vertex.first.id, storage::conversions::ConvertPropertyVector(vertex.second));
shard_map_.GetShardForKey(vertex.first.id, storage::conversions::ConvertPropertyVector(vertex.second));
if (!per_shard_request_table.contains(shard)) {
per_shard_request_table.insert(std::pair(shard, top_level_rqst_template));
}
@@ -594,7 +680,7 @@ class RequestRouter : public RequestRouterInterface {
}
StorageClient &GetStorageClientForShard(const std::string &label, const CompoundKey &key) {
auto shard = shards_map_.GetShardForKey(label, key);
auto shard = shard_map_.GetShardForKey(label, key);
return GetStorageClientForShard(std::move(shard));
}
@@ -611,16 +697,21 @@ class RequestRouter : public RequestRouterInterface {
}
template <typename RequestT, typename ResponseT>
std::vector<ResponseT> DriveReadResponses(RunningRequests<RequestT> &running_requests) {
utils::BasicResult<io::ShardVersionMismatch, std::vector<ResponseT>> DriveReadResponses(
RunningRequests<RequestT> &running_requests) {
// Store responses in a map based on the corresponding request
// offset, so that they can be reassembled in the correct order
// even if they came back in randomized orders.
std::map<size_t, ResponseT> response_map;
spdlog::trace("waiting on readiness for token");
spdlog::trace("waiting on readiness for token in DriveReadResponses");
size_t polls = 0;
while (response_map.size() < running_requests.size()) {
auto ready = notifier_.Await();
spdlog::trace("got readiness for token {}", ready.GetId());
MG_ASSERT(polls++ / running_requests.size() < 1000,
"polled over 1000 times per operation (almost certainly due to a bug) when performing request");
auto &request = running_requests.at(ready.GetId());
auto &storage_client = GetStorageClientForShard(request.shard);
@@ -632,13 +723,23 @@ class RequestRouter : public RequestRouterInterface {
}
if (poll_result->HasError()) {
throw std::runtime_error("RequestRouter Read request timed out");
storage_client.SendAsyncReadRequest(request.request, notifier_, ready);
continue;
}
msgs::ReadResponses response_variant = poll_result->GetValue();
auto response = std::get<ResponseT>(response_variant);
if (response.error) {
throw std::runtime_error("RequestRouter Read request did not succeed");
if (response.error->code == common::ErrorCode::STALE_SHARD_MAP) {
RefreshShardMap();
// signal to caller that we should retry
return io::ShardVersionMismatch{};
} else {
spdlog::warn("throwing in DriveReadResponses because of unhandled error: {}",
common::ErrorCodeToString(response.error->code));
throw std::runtime_error("RequestRouter Read request did not succeed");
}
}
// the readiness token has an ID based on the request vector offset
@@ -659,7 +760,8 @@ class RequestRouter : public RequestRouterInterface {
}
template <typename RequestT, typename ResponseT>
std::vector<ResponseT> DriveWriteResponses(RunningRequests<RequestT> &running_requests) {
utils::BasicResult<io::ShardVersionMismatch, std::vector<ResponseT>> DriveWriteResponses(
RunningRequests<RequestT> &running_requests) {
// Store responses in a map based on the corresponding request
// offset, so that they can be reassembled in the correct order
// even if they came back in randomized orders.
@@ -678,13 +780,23 @@ class RequestRouter : public RequestRouterInterface {
}
if (poll_result->HasError()) {
throw std::runtime_error("RequestRouter Write request timed out");
storage_client.SendAsyncWriteRequest(request.request, notifier_, ready);
continue;
}
msgs::WriteResponses response_variant = poll_result->GetValue();
auto response = std::get<ResponseT>(response_variant);
if (response.error) {
throw std::runtime_error("RequestRouter Write request did not succeed");
if (response.error->code == common::ErrorCode::STALE_SHARD_MAP) {
RefreshShardMap();
// signal to caller that we should retry
return io::ShardVersionMismatch{};
} else {
spdlog::warn("throwing in DriveWriteResponses because of unhandled error: {}",
common::ErrorCodeToString(response.error->code));
throw std::runtime_error("RequestRouter Write request did not succeed");
}
}
// the readiness token has an ID based on the request vector offset
@@ -706,17 +818,17 @@ class RequestRouter : public RequestRouterInterface {
void SetUpNameIdMappers() {
std::unordered_map<uint64_t, std::string> id_to_name;
for (const auto &[name, id] : shards_map_.labels) {
for (const auto &[name, id] : shard_map_.labels) {
id_to_name.emplace(id.AsUint(), name);
}
labels_.StoreMapping(std::move(id_to_name));
id_to_name.clear();
for (const auto &[name, id] : shards_map_.properties) {
for (const auto &[name, id] : shard_map_.properties) {
id_to_name.emplace(id.AsUint(), name);
}
properties_.StoreMapping(std::move(id_to_name));
id_to_name.clear();
for (const auto &[name, id] : shards_map_.edge_types) {
for (const auto &[name, id] : shard_map_.edge_types) {
id_to_name.emplace(id.AsUint(), name);
}
edge_types_.StoreMapping(std::move(id_to_name));
@@ -739,7 +851,7 @@ class RequestRouter : public RequestRouterInterface {
return {};
}
ShardMap shards_map_;
ShardMap shard_map_;
storage::v3::NameIdMapper properties_;
storage::v3::NameIdMapper edge_types_;
storage::v3::NameIdMapper labels_;
@@ -748,6 +860,7 @@ class RequestRouter : public RequestRouterInterface {
io::Io<TTransport> io_;
coordinator::Hlc transaction_id_;
io::Notifier notifier_ = {};
std::atomic<uint64_t> idempotency_token_generator_;
// TODO(kostasrim) Add batch prefetching
};
@@ -800,15 +913,19 @@ class SimulatedRequestRouterFactory : public RequestRouterFactory {
io::Address unique_local_addr_query;
// The simulated RR should not introduce stochastic behavior.
random_uuid = boost::uuids::uuid{3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
unique_local_addr_query = {.unique_id = boost::uuids::uuid{4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
random_uuid = boost::uuids::uuid{3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
unique_local_addr_query = {.unique_id = boost::uuids::uuid{4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
auto io = simulator_->Register(unique_local_addr_query);
auto query_io = io.ForkLocal(random_uuid);
return std::make_unique<RequestRouter<TransportType>>(
auto ret = std::make_unique<RequestRouter<TransportType>>(
coordinator::CoordinatorClient<TransportType>(query_io, coordinator_address, {coordinator_address}),
std::move(io));
ret->InstallSimulatorTicker(simulator_->GetSimulatorTickClosure());
return ret;
}
};

View File

@@ -12,6 +12,7 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <iostream>
#include <map>
#include <memory>
@@ -21,10 +22,13 @@
#include <variant>
#include <vector>
#include <boost/uuid/uuid.hpp>
#include "coordinator/hybrid_logical_clock.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/result.hpp"
#include "storage/v3/shard.hpp"
#include "utils/fnv.hpp"
namespace memgraph::msgs {
@@ -340,6 +344,7 @@ enum class StorageView { OLD = 0, NEW = 1 };
struct ScanVerticesRequest {
Hlc transaction_id;
Hlc shard_map_version;
// This should be optional
VertexId start_id;
// The empty optional means return all of the properties, while an empty list means do not return any properties
@@ -487,6 +492,7 @@ struct UpdateEdgeProp {
* Vertices
*/
struct NewVertex {
uint64_t idempotency_token;
std::vector<Label> label_ids;
PrimaryKey primary_key;
// This should be a map
@@ -495,6 +501,7 @@ struct NewVertex {
struct CreateVerticesRequest {
Hlc transaction_id;
Hlc shard_map_version;
std::vector<NewVertex> new_vertices;
};
@@ -528,6 +535,7 @@ struct UpdateVerticesResponse {
// No need for specifying direction since it has to be in one, and src and dest
// vertices clearly communicate the direction
struct NewExpand {
uint64_t idempotency_token;
EdgeId id;
EdgeType type;
VertexId src_vertex;
@@ -571,13 +579,65 @@ struct CommitResponse {
std::optional<ShardError> error;
};
struct SuggestedSplitInfo {
boost::uuids::uuid shard_to_split_uuid;
LabelId label_id;
PrimaryKey splitting_shard_low_key;
PrimaryKey split_key;
Hlc shard_version;
friend bool operator<(const SuggestedSplitInfo &lhs, const SuggestedSplitInfo &rhs) {
if (lhs.shard_to_split_uuid != rhs.shard_to_split_uuid) {
return lhs.shard_to_split_uuid < rhs.shard_to_split_uuid;
}
/*
TODO(tyler) fix this
if (lhs.split_key != rhs.split_key) {
return lhs.split_key < rhs.split_key;
}
*/
return lhs.shard_version < rhs.shard_version;
}
};
struct SplitInfo {
PrimaryKey split_key;
boost::uuids::uuid right_side_uuid;
Hlc shard_version;
};
struct SplitRequest {
PrimaryKey split_key;
Hlc old_shard_version;
Hlc new_lhs_shard_version;
Hlc new_rhs_shard_version;
std::map<boost::uuids::uuid, boost::uuids::uuid> uuid_mapping;
};
struct InitializeSplitShard {
// TODO(jbajic) Make it unique by solving that is not copyable int std::any
std::shared_ptr<storage::v3::Shard> shard;
std::map<boost::uuids::uuid, boost::uuids::uuid> uuid_mapping;
};
struct InitializeSplitShardByUUID {
std::shared_ptr<storage::v3::Shard> shard;
boost::uuids::uuid shard_uuid;
};
struct SplitResponse {};
using ReadRequests = std::variant<ExpandOneRequest, GetPropertiesRequest, ScanVerticesRequest>;
using ReadResponses = std::variant<ExpandOneResponse, GetPropertiesResponse, ScanVerticesResponse>;
using WriteRequests = std::variant<CreateVerticesRequest, DeleteVerticesRequest, UpdateVerticesRequest,
CreateExpandRequest, DeleteEdgesRequest, UpdateEdgesRequest, CommitRequest>;
using WriteResponses = std::variant<CreateVerticesResponse, DeleteVerticesResponse, UpdateVerticesResponse,
CreateExpandResponse, DeleteEdgesResponse, UpdateEdgesResponse, CommitResponse>;
using WriteRequests =
std::variant<CreateVerticesRequest, DeleteVerticesRequest, UpdateVerticesRequest, CreateExpandRequest,
DeleteEdgesRequest, UpdateEdgesRequest, CommitRequest, SplitRequest>;
using WriteResponses =
std::variant<CreateVerticesResponse, DeleteVerticesResponse, UpdateVerticesResponse, CreateExpandResponse,
DeleteEdgesResponse, UpdateEdgesResponse, CommitResponse, SplitResponse>;
} // namespace memgraph::msgs

View File

@@ -18,6 +18,7 @@ set(storage_v3_src_files
bindings/typed_value.cpp
expr.cpp
vertex.cpp
splitter.cpp
request_helper.cpp)
# ######################

View File

@@ -81,8 +81,10 @@ class DbAccessor final {
storage::v3::ShardResult<EdgeAccessor> InsertEdge(VertexAccessor *from, VertexAccessor *to,
const storage::v3::EdgeTypeId &edge_type) {
static constexpr auto kDummyGid = storage::v3::Gid::FromUint(0);
auto maybe_edge = accessor_->CreateEdge(from->Id(storage::v3::View::NEW).GetValue(),
to->Id(storage::v3::View::NEW).GetValue(), edge_type, kDummyGid);
const uint64_t dummy_idempotency_token = 0;
auto maybe_edge =
accessor_->CreateEdge(from->Id(storage::v3::View::NEW).GetValue(), to->Id(storage::v3::View::NEW).GetValue(),
edge_type, kDummyGid, dummy_idempotency_token);
if (maybe_edge.HasError()) return {maybe_edge.GetError()};
return EdgeAccessor(*maybe_edge);
}

View File

@@ -30,6 +30,10 @@ struct Config {
io::Duration reclamation_interval{};
} gc;
struct Split {
uint64_t max_shard_vertex_size{500'000};
} split;
struct Items {
bool properties_on_edges{true};
} items;

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -13,12 +13,14 @@
#include <cstdint>
#include <memory>
#include "storage/v3/edge_ref.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/vertex.hpp"
#include "storage/v3/vertex_id.hpp"
#include "utils/logging.hpp"
#include "utils/synchronized.hpp"
namespace memgraph::storage::v3 {
@@ -27,6 +29,11 @@ struct Edge;
struct Delta;
struct CommitInfo;
inline uint64_t GetNextDeltaId() noexcept {
static utils::Synchronized<uint64_t, utils::SpinLock> delta_id{0};
return delta_id.WithLock([](auto &id) { return id++; });
}
// This class stores one of three pointers (`Delta`, `Vertex` and `Edge`)
// without using additional memory for storing the type. The type is stored in
// the pointer itself in the lower bits. All of those structures contain large
@@ -158,46 +165,59 @@ struct Delta {
struct RemoveInEdgeTag {};
struct RemoveOutEdgeTag {};
Delta(DeleteObjectTag /*unused*/, CommitInfo *commit_info, uint64_t command_id)
: action(Action::DELETE_OBJECT), commit_info(commit_info), command_id(command_id) {}
Delta(DeleteObjectTag /*unused*/, CommitInfo *commit_info, uint64_t delta_id, uint64_t command_id,
uint64_t idempotency_token)
: action(Action::DELETE_OBJECT),
id(delta_id),
commit_info(commit_info),
command_id(command_id),
idempotency_token(idempotency_token) {}
Delta(RecreateObjectTag /*unused*/, CommitInfo *commit_info, uint64_t command_id)
: action(Action::RECREATE_OBJECT), commit_info(commit_info), command_id(command_id) {}
Delta(RecreateObjectTag /*unused*/, CommitInfo *commit_info, uint64_t delta_id, uint64_t command_id)
: action(Action::RECREATE_OBJECT), id(delta_id), commit_info(commit_info), command_id(command_id) {}
Delta(AddLabelTag /*unused*/, LabelId label, CommitInfo *commit_info, uint64_t command_id)
: action(Action::ADD_LABEL), commit_info(commit_info), command_id(command_id), label(label) {}
Delta(AddLabelTag /*unused*/, LabelId label, CommitInfo *commit_info, uint64_t delta_id, uint64_t command_id)
: action(Action::ADD_LABEL), id(delta_id), commit_info(commit_info), command_id(command_id), label(label) {}
Delta(RemoveLabelTag /*unused*/, LabelId label, CommitInfo *commit_info, uint64_t command_id)
: action(Action::REMOVE_LABEL), commit_info(commit_info), command_id(command_id), label(label) {}
Delta(RemoveLabelTag /*unused*/, LabelId label, CommitInfo *commit_info, uint64_t delta_id, uint64_t command_id)
: action(Action::REMOVE_LABEL), id(delta_id), commit_info(commit_info), command_id(command_id), label(label) {}
Delta(SetPropertyTag /*unused*/, PropertyId key, const PropertyValue &value, CommitInfo *commit_info,
uint64_t command_id)
: action(Action::SET_PROPERTY), commit_info(commit_info), command_id(command_id), property({key, value}) {}
uint64_t delta_id, uint64_t command_id)
: action(Action::SET_PROPERTY),
id(delta_id),
commit_info(commit_info),
command_id(command_id),
property({key, value}) {}
Delta(AddInEdgeTag /*unused*/, EdgeTypeId edge_type, VertexId vertex_id, EdgeRef edge, CommitInfo *commit_info,
uint64_t command_id)
uint64_t delta_id, uint64_t command_id)
: action(Action::ADD_IN_EDGE),
id(delta_id),
commit_info(commit_info),
command_id(command_id),
vertex_edge({edge_type, std::move(vertex_id), edge}) {}
Delta(AddOutEdgeTag /*unused*/, EdgeTypeId edge_type, VertexId vertex_id, EdgeRef edge, CommitInfo *commit_info,
uint64_t command_id)
uint64_t delta_id, uint64_t command_id)
: action(Action::ADD_OUT_EDGE),
id(delta_id),
commit_info(commit_info),
command_id(command_id),
vertex_edge({edge_type, std::move(vertex_id), edge}) {}
Delta(RemoveInEdgeTag /*unused*/, EdgeTypeId edge_type, VertexId vertex_id, EdgeRef edge, CommitInfo *commit_info,
uint64_t command_id)
uint64_t delta_id, uint64_t command_id)
: action(Action::REMOVE_IN_EDGE),
id(delta_id),
commit_info(commit_info),
command_id(command_id),
vertex_edge({edge_type, std::move(vertex_id), edge}) {}
Delta(RemoveOutEdgeTag /*unused*/, EdgeTypeId edge_type, VertexId vertex_id, EdgeRef edge, CommitInfo *commit_info,
uint64_t command_id)
uint64_t delta_id, uint64_t command_id)
: action(Action::REMOVE_OUT_EDGE),
id(delta_id),
commit_info(commit_info),
command_id(command_id),
vertex_edge({edge_type, std::move(vertex_id), edge}) {}
@@ -226,13 +246,16 @@ struct Delta {
}
}
Action action;
friend bool operator==(const Delta &lhs, const Delta &rhs) noexcept { return lhs.id == rhs.id; }
Action action;
uint64_t id;
// TODO: optimize with in-place copy
CommitInfo *commit_info;
uint64_t command_id;
PreviousPtr prev;
Delta *next{nullptr};
uint64_t idempotency_token;
union {
LabelId label;

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -325,7 +325,7 @@ void LabelIndex::RemoveObsoleteEntries(const uint64_t clean_up_before_timestamp)
}
}
LabelIndex::Iterable::Iterator::Iterator(Iterable *self, LabelIndexContainer::iterator index_iterator)
LabelIndex::Iterable::Iterator::Iterator(Iterable *self, IndexContainer::iterator index_iterator)
: self_(self),
index_iterator_(index_iterator),
current_vertex_accessor_(nullptr, nullptr, nullptr, self_->config_, *self_->vertex_validator_),
@@ -353,7 +353,7 @@ void LabelIndex::Iterable::Iterator::AdvanceUntilValid() {
}
}
LabelIndex::Iterable::Iterable(LabelIndexContainer &index_container, LabelId label, View view, Transaction *transaction,
LabelIndex::Iterable::Iterable(IndexContainer &index_container, LabelId label, View view, Transaction *transaction,
Indices *indices, Config::Items config, const VertexValidator &vertex_validator)
: index_container_(&index_container),
label_(label),
@@ -465,7 +465,7 @@ void LabelPropertyIndex::RemoveObsoleteEntries(const uint64_t clean_up_before_ti
}
}
LabelPropertyIndex::Iterable::Iterator::Iterator(Iterable *self, LabelPropertyIndexContainer::iterator index_iterator)
LabelPropertyIndex::Iterable::Iterator::Iterator(Iterable *self, IndexContainer::iterator index_iterator)
: self_(self),
index_iterator_(index_iterator),
current_vertex_accessor_(nullptr, nullptr, nullptr, self_->config_, *self_->vertex_validator_),
@@ -526,7 +526,7 @@ const PropertyValue kSmallestMap = PropertyValue(std::map<std::string, PropertyV
const PropertyValue kSmallestTemporalData =
PropertyValue(TemporalData{static_cast<TemporalType>(0), std::numeric_limits<int64_t>::min()});
LabelPropertyIndex::Iterable::Iterable(LabelPropertyIndexContainer &index_container, LabelId label, PropertyId property,
LabelPropertyIndex::Iterable::Iterable(IndexContainer &index_container, LabelId label, PropertyId property,
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view,
Transaction *transaction, Indices *indices, Config::Items config,

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -18,6 +18,8 @@
#include <utility>
#include "storage/v3/config.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/key_store.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/vertex_accessor.hpp"
@@ -40,12 +42,18 @@ class LabelIndex {
bool operator==(const Entry &rhs) const { return vertex == rhs.vertex && timestamp == rhs.timestamp; }
};
using IndexType = LabelId;
public:
using LabelIndexContainer = std::set<Entry>;
using IndexContainer = std::set<Entry>;
LabelIndex(Indices *indices, Config::Items config, const VertexValidator &vertex_validator)
: indices_(indices), config_(config), vertex_validator_{&vertex_validator} {}
LabelIndex(Indices *indices, Config::Items config, const VertexValidator &vertex_validator,
std::map<LabelId, IndexContainer> &data)
: index_{std::move(data)}, indices_(indices), config_(config), vertex_validator_{&vertex_validator} {}
/// @throw std::bad_alloc
void UpdateOnAddLabel(LabelId label, Vertex *vertex, const Transaction &tx);
@@ -63,12 +71,12 @@ class LabelIndex {
class Iterable {
public:
Iterable(LabelIndexContainer &index_container, LabelId label, View view, Transaction *transaction, Indices *indices,
Iterable(IndexContainer &index_container, LabelId label, View view, Transaction *transaction, Indices *indices,
Config::Items config, const VertexValidator &vertex_validator);
class Iterator {
public:
Iterator(Iterable *self, LabelIndexContainer::iterator index_iterator);
Iterator(Iterable *self, IndexContainer::iterator index_iterator);
VertexAccessor operator*() const { return current_vertex_accessor_; }
@@ -81,7 +89,7 @@ class LabelIndex {
void AdvanceUntilValid();
Iterable *self_;
LabelIndexContainer::iterator index_iterator_;
IndexContainer::iterator index_iterator_;
VertexAccessor current_vertex_accessor_;
Vertex *current_vertex_;
};
@@ -90,7 +98,7 @@ class LabelIndex {
Iterator end() { return {this, index_container_->end()}; }
private:
LabelIndexContainer *index_container_;
IndexContainer *index_container_;
LabelId label_;
View view_;
Transaction *transaction_;
@@ -114,8 +122,35 @@ class LabelIndex {
void Clear() { index_.clear(); }
std::map<IndexType, IndexContainer> SplitIndexEntries(const PrimaryKey &split_key) {
if (index_.empty()) {
return {};
}
// Cloned index entries will contain new index entry iterators, but old
// vertices address which need to be adjusted after extracting vertices
std::map<IndexType, IndexContainer> cloned_indices;
for (auto &[index_type_val, index] : index_) {
auto entry_it = index.begin();
auto &cloned_indices_container = cloned_indices[index_type_val];
while (entry_it != index.end()) {
// We need to save the next pointer since the current one will be
// invalidated after extract
auto next_entry_it = std::next(entry_it);
if (entry_it->vertex->first > split_key) {
[[maybe_unused]] const auto &[inserted_entry_it, inserted, node] =
cloned_indices_container.insert(index.extract(entry_it));
MG_ASSERT(inserted, "Failed to extract index entry!");
}
entry_it = next_entry_it;
}
}
return cloned_indices;
}
private:
std::map<LabelId, LabelIndexContainer> index_;
std::map<LabelId, IndexContainer> index_;
Indices *indices_;
Config::Items config_;
const VertexValidator *vertex_validator_;
@@ -133,9 +168,10 @@ class LabelPropertyIndex {
bool operator<(const PropertyValue &rhs) const;
bool operator==(const PropertyValue &rhs) const;
};
using IndexType = std::pair<LabelId, PropertyId>;
public:
using LabelPropertyIndexContainer = std::set<Entry>;
using IndexContainer = std::set<Entry>;
LabelPropertyIndex(Indices *indices, Config::Items config, const VertexValidator &vertex_validator)
: indices_(indices), config_(config), vertex_validator_{&vertex_validator} {}
@@ -159,14 +195,14 @@ class LabelPropertyIndex {
class Iterable {
public:
Iterable(LabelPropertyIndexContainer &index_container, LabelId label, PropertyId property,
Iterable(IndexContainer &index_container, LabelId label, PropertyId property,
const std::optional<utils::Bound<PropertyValue>> &lower_bound,
const std::optional<utils::Bound<PropertyValue>> &upper_bound, View view, Transaction *transaction,
Indices *indices, Config::Items config, const VertexValidator &vertex_validator);
class Iterator {
public:
Iterator(Iterable *self, LabelPropertyIndexContainer::iterator index_iterator);
Iterator(Iterable *self, IndexContainer::iterator index_iterator);
VertexAccessor operator*() const { return current_vertex_accessor_; }
@@ -179,7 +215,7 @@ class LabelPropertyIndex {
void AdvanceUntilValid();
Iterable *self_;
LabelPropertyIndexContainer::iterator index_iterator_;
IndexContainer::iterator index_iterator_;
VertexAccessor current_vertex_accessor_;
Vertex *current_vertex_;
};
@@ -188,7 +224,7 @@ class LabelPropertyIndex {
Iterator end();
private:
LabelPropertyIndexContainer *index_container_;
IndexContainer *index_container_;
LabelId label_;
PropertyId property_;
std::optional<utils::Bound<PropertyValue>> lower_bound_;
@@ -229,8 +265,35 @@ class LabelPropertyIndex {
void Clear() { index_.clear(); }
std::map<IndexType, IndexContainer> SplitIndexEntries(const PrimaryKey &split_key) {
if (index_.empty()) {
return {};
}
// Cloned index entries will contain new index entry iterators, but old
// vertices address which need to be adjusted after extracting vertices
std::map<IndexType, IndexContainer> cloned_indices;
for (auto &[index_type_val, index] : index_) {
auto entry_it = index.begin();
auto &cloned_index_container = cloned_indices[index_type_val];
while (entry_it != index.end()) {
// We need to save the next pointer since the current one will be
// invalidated after extract
auto next_entry_it = std::next(entry_it);
if (entry_it->vertex->first > split_key) {
[[maybe_unused]] const auto &[inserted_entry_it, inserted, node] =
cloned_index_container.insert(index.extract(entry_it));
MG_ASSERT(inserted, "Failed to extract index entry!");
}
entry_it = next_entry_it;
}
}
return cloned_indices;
}
private:
std::map<std::pair<LabelId, PropertyId>, LabelPropertyIndexContainer> index_;
std::map<std::pair<LabelId, PropertyId>, IndexContainer> index_;
Indices *indices_;
Config::Items config_;
const VertexValidator *vertex_validator_;

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -107,9 +107,9 @@ inline bool PrepareForWrite(Transaction *transaction, TObj *object) {
/// and is primarily used to create the first delta for an object (that must be
/// a `DELETE_OBJECT` delta).
/// @throw std::bad_alloc
inline Delta *CreateDeleteObjectDelta(Transaction *transaction) {
return &transaction->deltas.emplace_back(Delta::DeleteObjectTag(), transaction->commit_info.get(),
transaction->command_id);
inline Delta *CreateDeleteObjectDelta(Transaction *transaction, uint64_t idempotency_token) {
return &transaction->deltas.emplace_back(Delta::DeleteObjectTag(), transaction->commit_info.get(), GetNextDeltaId(),
transaction->command_id, idempotency_token);
}
/// This function creates a delta in the transaction for the object and links
@@ -119,7 +119,7 @@ template <typename TObj, class... Args>
requires utils::SameAsAnyOf<TObj, Edge, Vertex>
inline void CreateAndLinkDelta(Transaction *transaction, TObj *object, Args &&...args) {
auto delta = &transaction->deltas.emplace_back(std::forward<Args>(args)..., transaction->commit_info.get(),
transaction->command_id);
GetNextDeltaId(), transaction->command_id);
auto *delta_holder = GetDeltaHolder(object);
// The operations are written in such order so that both `next` and `prev`

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -18,14 +18,14 @@
#include <memory>
#include <mutex>
#include <optional>
#include <variant>
#include <bits/ranges_algo.h>
#include <gflags/gflags.h>
#include <spdlog/spdlog.h>
#include "io/network/endpoint.hpp"
#include "io/time.hpp"
#include "storage/v3/delta.hpp"
#include "storage/v3/edge.hpp"
#include "storage/v3/edge_accessor.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/indices.hpp"
@@ -36,6 +36,7 @@
#include "storage/v3/result.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/value_conversions.hpp"
#include "storage/v3/vertex.hpp"
#include "storage/v3/vertex_accessor.hpp"
#include "storage/v3/view.hpp"
@@ -323,30 +324,90 @@ bool VerticesIterable::Iterator::operator==(const Iterator &other) const {
}
Shard::Shard(const LabelId primary_label, const PrimaryKey min_primary_key,
const std::optional<PrimaryKey> max_primary_key, std::vector<SchemaProperty> schema, Config config,
std::unordered_map<uint64_t, std::string> id_to_name)
const std::optional<PrimaryKey> max_primary_key, std::vector<SchemaProperty> schema, Hlc shard_version,
Config config, std::unordered_map<uint64_t, std::string> id_to_name)
: primary_label_{primary_label},
min_primary_key_{min_primary_key},
max_primary_key_{max_primary_key},
shard_version_{shard_version},
schema_validator_{schemas_, name_id_mapper_},
vertex_validator_{schema_validator_, primary_label},
indices_{config.items, vertex_validator_},
isolation_level_{config.transaction.isolation_level},
config_{config},
uuid_{utils::GenerateUUID()},
epoch_id_{utils::GenerateUUID()},
global_locker_{file_retainer_.AddLocker()} {
shard_splitter_(primary_label, vertices_, edges_, start_logical_id_to_transaction_, indices_, config_, schema,
name_id_mapper_) {
spdlog::trace("Shard constructed with low key {} (path 1)", min_primary_key_.back());
CreateSchema(primary_label_, schema);
StoreMapping(std::move(id_to_name));
}
Shard::Shard(LabelId primary_label, PrimaryKey min_primary_key, std::optional<PrimaryKey> max_primary_key,
std::vector<SchemaProperty> schema, VertexContainer &&vertices, EdgeContainer &&edges,
std::map<uint64_t, std::unique_ptr<Transaction>> &&start_logical_id_to_transaction, const Config &config,
const std::unordered_map<uint64_t, std::string> &id_to_name, const Hlc shard_version)
: primary_label_{primary_label},
min_primary_key_{min_primary_key},
max_primary_key_{max_primary_key},
vertices_(std::move(vertices)),
edges_(std::move(edges)),
shard_version_(shard_version),
schema_validator_{schemas_, name_id_mapper_},
vertex_validator_{schema_validator_, primary_label},
indices_{config.items, vertex_validator_},
isolation_level_{config.transaction.isolation_level},
config_{config},
start_logical_id_to_transaction_(std::move(start_logical_id_to_transaction)),
shard_splitter_(primary_label, vertices_, edges_, start_logical_id_to_transaction_, indices_, config_, schema,
name_id_mapper_) {
spdlog::trace("Shard constructed with low key {} (path 2)", min_primary_key_.back());
CreateSchema(primary_label_, schema);
StoreMapping(id_to_name);
}
Shard::Shard(LabelId primary_label, PrimaryKey min_primary_key, std::optional<PrimaryKey> max_primary_key,
std::vector<SchemaProperty> schema, VertexContainer &&vertices,
std::map<uint64_t, std::unique_ptr<Transaction>> &&start_logical_id_to_transaction, const Config &config,
const std::unordered_map<uint64_t, std::string> &id_to_name, const Hlc shard_version)
: primary_label_{primary_label},
min_primary_key_{min_primary_key},
max_primary_key_{max_primary_key},
vertices_(std::move(vertices)),
shard_version_(shard_version),
schema_validator_{schemas_, name_id_mapper_},
vertex_validator_{schema_validator_, primary_label},
indices_{config.items, vertex_validator_},
isolation_level_{config.transaction.isolation_level},
config_{config},
start_logical_id_to_transaction_(std::move(start_logical_id_to_transaction)),
shard_splitter_(primary_label, vertices_, edges_, start_logical_id_to_transaction_, indices_, config_, schema,
name_id_mapper_) {
spdlog::trace("Shard constructed with low key {} (path 3)", min_primary_key_.back());
CreateSchema(primary_label_, schema);
StoreMapping(id_to_name);
}
Shard::~Shard() {}
std::unique_ptr<Shard> Shard::FromSplitData(SplitData &&split_data) {
if (split_data.config.items.properties_on_edges) [[likely]] {
return std::make_unique<Shard>(split_data.primary_label, split_data.min_primary_key, split_data.max_primary_key,
split_data.schema, std::move(split_data.vertices), std::move(*split_data.edges),
std::move(split_data.transactions), split_data.config, split_data.id_to_name,
split_data.shard_version);
}
const auto low_key_int = split_data.min_primary_key.back();
spdlog::trace("Shard constructed FromSplitData with low key {}", low_key_int);
return std::make_unique<Shard>(split_data.primary_label, split_data.min_primary_key, split_data.max_primary_key,
split_data.schema, std::move(split_data.vertices), std::move(split_data.transactions),
split_data.config, split_data.id_to_name, split_data.shard_version);
}
Shard::Accessor::Accessor(Shard &shard, Transaction &transaction)
: shard_(&shard), transaction_(&transaction), config_(shard_->config_.items) {}
ShardResult<VertexAccessor> Shard::Accessor::CreateVertexAndValidate(
const std::vector<LabelId> &labels, const PrimaryKey &primary_properties,
std::optional<ShardError> Shard::Accessor::CreateVertexAndValidate(
const uint64_t idempotency_token, const std::vector<LabelId> &labels, const PrimaryKey &primary_properties,
const std::vector<std::pair<PropertyId, PropertyValue>> &properties) {
OOMExceptionEnabler oom_exception;
const auto schema = shard_->GetSchema(shard_->primary_label_)->second;
@@ -357,14 +418,19 @@ ShardResult<VertexAccessor> Shard::Accessor::CreateVertexAndValidate(
return {std::move(maybe_schema_violation.GetError())};
}
auto *delta = CreateDeleteObjectDelta(transaction_);
auto *delta = CreateDeleteObjectDelta(transaction_, idempotency_token);
auto [it, inserted] = shard_->vertices_.emplace(primary_properties, VertexData{delta});
delta->prev.Set(&*it);
VertexAccessor vertex_acc{&*it, transaction_, &shard_->indices_, config_, shard_->vertex_validator_};
if (!inserted) {
if (it->second.delta->idempotency_token == idempotency_token) {
// this is a benign race condition due to request retries - signal success
return std::nullopt;
}
return SHARD_ERROR(ErrorCode::VERTEX_ALREADY_INSERTED);
}
delta->prev.Set(&*it);
VertexAccessor vertex_acc{&*it, transaction_, &shard_->indices_, config_, shard_->vertex_validator_};
MG_ASSERT(it != shard_->vertices_.end(), "Invalid Vertex accessor!");
// TODO(jbajic) Improve, maybe delay index update
@@ -381,7 +447,8 @@ ShardResult<VertexAccessor> Shard::Accessor::CreateVertexAndValidate(
return {err.GetError()};
}
}
return vertex_acc;
return std::nullopt;
}
std::optional<VertexAccessor> Shard::Accessor::FindVertex(std::vector<PropertyValue> primary_key, View view) {
@@ -436,7 +503,7 @@ ShardResult<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>>
}
std::vector<EdgeAccessor> deleted_edges;
const VertexId vertex_id{shard_->primary_label_, *vertex->PrimaryKey(View::OLD)}; // TODO Replace
const VertexId vertex_id{shard_->primary_label_, *vertex->PrimaryKey(View::OLD)};
for (const auto &item : in_edges) {
auto [edge_type, from_vertex, edge] = item;
EdgeAccessor e(edge, edge_type, from_vertex, vertex_id, transaction_, &shard_->indices_, config_);
@@ -481,7 +548,8 @@ ShardResult<std::optional<std::pair<VertexAccessor, std::vector<EdgeAccessor>>>>
}
ShardResult<EdgeAccessor> Shard::Accessor::CreateEdge(VertexId from_vertex_id, VertexId to_vertex_id,
const EdgeTypeId edge_type, const Gid gid) {
const EdgeTypeId edge_type, const Gid gid,
const uint64_t idempotency_token) {
OOMExceptionEnabler oom_exception;
Vertex *from_vertex{nullptr};
Vertex *to_vertex{nullptr};
@@ -515,7 +583,7 @@ ShardResult<EdgeAccessor> Shard::Accessor::CreateEdge(VertexId from_vertex_id, V
EdgeRef edge(gid);
if (config_.properties_on_edges) {
auto *delta = CreateDeleteObjectDelta(transaction_);
auto *delta = CreateDeleteObjectDelta(transaction_, idempotency_token);
auto [it, inserted] = shard_->edges_.emplace(gid, Edge{gid, delta});
MG_ASSERT(inserted, "The edge must be inserted here!");
MG_ASSERT(it != shard_->edges_.end(), "Invalid Edge accessor!");
@@ -664,9 +732,16 @@ void Shard::Accessor::Commit(coordinator::Hlc commit_timestamp) {
MG_ASSERT(!transaction_->must_abort, "The transaction can't be committed!");
MG_ASSERT(transaction_->start_timestamp.logical_id < commit_timestamp.logical_id,
"Commit timestamp must be older than start timestamp!");
MG_ASSERT(!transaction_->commit_info->is_locally_committed, "The transaction is already committed!");
transaction_->commit_info->start_or_commit_timestamp = commit_timestamp;
transaction_->commit_info->is_locally_committed = true;
if (transaction_->commit_info->is_locally_committed) {
MG_ASSERT(transaction_->commit_info->start_or_commit_timestamp == commit_timestamp);
spdlog::debug(
"Shard::Accessor::Commit called for already-committed transaction, probably due to a retry request that timed "
"out for the RequestRouter");
} else {
MG_ASSERT(transaction_->commit_info->start_or_commit_timestamp != commit_timestamp);
transaction_->commit_info->start_or_commit_timestamp = commit_timestamp;
transaction_->commit_info->is_locally_committed = true;
}
}
void Shard::Accessor::Abort() {
@@ -897,6 +972,8 @@ StorageInfo Shard::GetInfo() const {
return {vertex_count, edge_count_, average_degree, utils::GetMemoryUsage()};
}
Hlc Shard::Version() const { return shard_version_; }
VerticesIterable Shard::Accessor::Vertices(LabelId label, View view) {
return VerticesIterable(shard_->indices_.label_index.Vertices(label, view, transaction_));
}
@@ -1048,6 +1125,47 @@ void Shard::StoreMapping(std::unordered_map<uint64_t, std::string> id_to_name) {
name_id_mapper_.StoreMapping(std::move(id_to_name));
}
std::optional<ShardSuggestedSplitInfo> Shard::ShouldSplit() const noexcept {
if (vertices_.size() >= config_.split.max_shard_vertex_size) {
spdlog::debug("Shard::ShouldSplit is signalling that the split process may begin");
auto mid_elem = vertices_.begin();
// TODO(tyler) the first time we calculate the split point, we should store it so that we don't have to
// iterate over half of the entire index each time Cron is run until the split succeeds.
std::ranges::advance(mid_elem, static_cast<VertexContainer::difference_type>(vertices_.size() / 2));
spdlog::debug("Shard::ShouldSplit returning split key with end Value {} for shard version {}",
mid_elem->first.back(), shard_version_.logical_id);
return ShardSuggestedSplitInfo{
.label_id = PrimaryLabel(),
.splitting_shard_low_key = min_primary_key_,
.split_key = mid_elem->first,
.shard_version = shard_version_,
};
}
spdlog::trace("Shard not splitting because we have {} vertices, lower than split threshold of {}", vertices_.size(),
config_.split.max_shard_vertex_size);
return std::nullopt;
}
std::optional<SplitData> Shard::PerformSplit(const PrimaryKey &split_key, const Hlc old_shard_version,
const Hlc new_lhs_shard_version, const Hlc new_rhs_shard_version) {
if (old_shard_version != shard_version_) {
spdlog::debug("Shard::PerformSplit - Curent shard version {} does not match given {}", shard_version_,
old_shard_version);
return std::nullopt;
}
spdlog::debug("Shard::PerformSplit - splitting from shard version {} to versions {} and {}", old_shard_version,
new_lhs_shard_version, new_rhs_shard_version);
shard_version_ = new_lhs_shard_version;
const auto old_max_key = max_primary_key_;
max_primary_key_ = split_key;
return shard_splitter_.SplitShard(split_key, old_max_key, new_rhs_shard_version);
}
bool Shard::IsVertexBelongToShard(const VertexId &vertex_id) const {
return vertex_id.primary_label == primary_label_ && vertex_id.primary_key >= min_primary_key_ &&
(!max_primary_key_.has_value() || vertex_id.primary_key < *max_primary_key_);

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -14,12 +14,15 @@
#include <cstdint>
#include <filesystem>
#include <map>
#include <memory>
#include <numeric>
#include <optional>
#include <shared_mutex>
#include <variant>
#include <vector>
#include <boost/uuid/uuid.hpp>
#include "coordinator/hybrid_logical_clock.hpp"
#include "io/network/endpoint.hpp"
#include "io/time.hpp"
@@ -37,11 +40,13 @@
#include "storage/v3/result.hpp"
#include "storage/v3/schema_validator.hpp"
#include "storage/v3/schemas.hpp"
#include "storage/v3/splitter.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/vertex.hpp"
#include "storage/v3/vertex_accessor.hpp"
#include "storage/v3/vertex_id.hpp"
#include "storage/v3/view.hpp"
#include "utils/concrete_msg_sender.hpp"
#include "utils/exceptions.hpp"
#include "utils/file_locker.hpp"
#include "utils/on_scope_exit.hpp"
@@ -53,6 +58,8 @@
namespace memgraph::storage::v3 {
using coordinator::Hlc;
// The storage is based on this paper:
// https://db.in.tum.de/~muehlbau/papers/mvcc.pdf
// The paper implements a fully serializable storage, in our implementation we
@@ -182,13 +189,31 @@ struct StorageInfo {
uint64_t memory_usage;
};
struct ShardSuggestedSplitInfo {
boost::uuids::uuid shard_to_split_uuid;
LabelId label_id;
PrimaryKey splitting_shard_low_key;
PrimaryKey split_key;
Hlc shard_version;
};
class Shard final {
public:
/// @throw std::system_error
/// @throw std::bad_alloc
explicit Shard(LabelId primary_label, PrimaryKey min_primary_key, std::optional<PrimaryKey> max_primary_key,
std::vector<SchemaProperty> schema, Config config = Config(),
std::unordered_map<uint64_t, std::string> id_to_name = {});
Shard(LabelId primary_label, PrimaryKey min_primary_key, std::optional<PrimaryKey> max_primary_key,
std::vector<SchemaProperty> schema, Hlc shard_version, Config config = Config(),
std::unordered_map<uint64_t, std::string> id_to_name = {});
Shard(LabelId primary_label, PrimaryKey min_primary_key, std::optional<PrimaryKey> max_primary_key,
std::vector<SchemaProperty> schema, VertexContainer &&vertices, EdgeContainer &&edges,
std::map<uint64_t, std::unique_ptr<Transaction>> &&start_logical_id_to_transaction, const Config &config,
const std::unordered_map<uint64_t, std::string> &id_to_name, Hlc shard_version);
Shard(LabelId primary_label, PrimaryKey min_primary_key, std::optional<PrimaryKey> max_primary_key,
std::vector<SchemaProperty> schema, VertexContainer &&vertices,
std::map<uint64_t, std::unique_ptr<Transaction>> &&start_logical_id_to_transaction, const Config &config,
const std::unordered_map<uint64_t, std::string> &id_to_name, Hlc shard_version);
Shard(const Shard &) = delete;
Shard(Shard &&) noexcept = delete;
@@ -196,6 +221,8 @@ class Shard final {
Shard operator=(Shard &&) noexcept = delete;
~Shard();
static std::unique_ptr<Shard> FromSplitData(SplitData &&split_data);
class Accessor final {
private:
friend class Shard;
@@ -204,8 +231,9 @@ class Shard final {
public:
/// @throw std::bad_alloc
ShardResult<VertexAccessor> CreateVertexAndValidate(
const std::vector<LabelId> &labels, const std::vector<PropertyValue> &primary_properties,
std::optional<ShardError> CreateVertexAndValidate(
const uint64_t idempotency_token, const std::vector<LabelId> &labels,
const std::vector<PropertyValue> &primary_properties,
const std::vector<std::pair<PropertyId, PropertyValue>> &properties);
std::optional<VertexAccessor> FindVertex(std::vector<PropertyValue> primary_key, View view);
@@ -266,7 +294,8 @@ class Shard final {
VertexAccessor *vertex);
/// @throw std::bad_alloc
ShardResult<EdgeAccessor> CreateEdge(VertexId from_vertex_id, VertexId to_vertex_id, EdgeTypeId edge_type, Gid gid);
ShardResult<EdgeAccessor> CreateEdge(VertexId from_vertex_id, VertexId to_vertex_id, EdgeTypeId edge_type, Gid gid,
const uint64_t idempotency_token);
/// Accessor to the deleted edge if a deletion took place, std::nullopt otherwise
/// @throw std::bad_alloc
@@ -329,6 +358,10 @@ class Shard final {
LabelId PrimaryLabel() const;
Hlc Version() const;
PrimaryKey LowKey() const { return min_primary_key_; }
[[nodiscard]] bool IsVertexBelongToShard(const VertexId &vertex_id) const;
/// @throw std::bad_alloc
@@ -360,6 +393,11 @@ class Shard final {
void StoreMapping(std::unordered_map<uint64_t, std::string> id_to_name);
std::optional<ShardSuggestedSplitInfo> ShouldSplit() const noexcept;
std::optional<SplitData> PerformSplit(const PrimaryKey &split_key, Hlc old_shard_version,
const Hlc new_lhs_shard_version, const Hlc new_rhs_shard_version);
private:
Transaction &GetTransaction(coordinator::Hlc start_timestamp, IsolationLevel isolation_level);
@@ -377,6 +415,7 @@ class Shard final {
// list is used only when properties are enabled for edges. Because of that we
// keep a separate count of edges that is always updated.
uint64_t edge_count_{0};
Hlc shard_version_{};
SchemaValidator schema_validator_;
VertexValidator vertex_validator_;
@@ -396,41 +435,10 @@ class Shard final {
// storage.
std::list<Gid> deleted_edges_;
// UUID used to distinguish snapshots and to link snapshots to WALs
std::string uuid_;
// Sequence number used to keep track of the chain of WALs.
uint64_t wal_seq_num_{0};
// UUID to distinguish different main instance runs for replication process
// on SAME storage.
// Multiple instances can have same storage UUID and be MAIN at the same time.
// We cannot compare commit timestamps of those instances if one of them
// becomes the replica of the other so we use epoch_id_ as additional
// discriminating property.
// Example of this:
// We have 2 instances of the same storage, S1 and S2.
// S1 and S2 are MAIN and accept their own commits and write them to the WAL.
// At the moment when S1 commited a transaction with timestamp 20, and S2
// a different transaction with timestamp 15, we change S2's role to REPLICA
// and register it on S1.
// Without using the epoch_id, we don't know that S1 and S2 have completely
// different transactions, we think that the S2 is behind only by 5 commits.
std::string epoch_id_;
// History of the previous epoch ids.
// Each value consists of the epoch id along the last commit belonging to that
// epoch.
std::deque<std::pair<std::string, uint64_t>> epoch_history_;
uint64_t wal_unsynced_transactions_{0};
utils::FileRetainer file_retainer_;
// Global locker that is used for clients file locking
utils::FileRetainer::FileLocker global_locker_;
// Holds all of the (in progress, committed and aborted) transactions that are read or write to this shard, but
// haven't been cleaned up yet
std::map<uint64_t, std::unique_ptr<Transaction>> start_logical_id_to_transaction_{};
Splitter shard_splitter_;
bool has_any_transaction_aborted_since_last_gc{false};
};

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -12,8 +12,9 @@
#pragma once
#include <queue>
#include <set>
#include <unordered_map>
#include <variant>
#include <vector>
#include <boost/functional/hash.hpp>
#include <boost/uuid/uuid.hpp>
@@ -31,6 +32,8 @@
#include "storage/v3/shard.hpp"
#include "storage/v3/shard_rsm.hpp"
#include "storage/v3/shard_worker.hpp"
#include "storage/v3/value_conversions.hpp"
#include "utils/variant_helpers.hpp"
namespace memgraph::storage::v3 {
@@ -42,7 +45,6 @@ using coordinator::HeartbeatRequest;
using coordinator::HeartbeatResponse;
using io::Address;
using io::Duration;
using io::Message;
using io::RequestId;
using io::ResponseFuture;
using io::Time;
@@ -74,14 +76,17 @@ static_assert(kMinimumCronInterval < kMaximumCronInterval,
/// * reconciling the storage engine's local configuration with the Coordinator's
/// intentions for how it should participate in multiple raft clusters
/// * replying to heartbeat requests to the Coordinator
/// * routing incoming messages to the appropriate sRSM
/// * routing incoming messages to the appropriate RSM
///
/// Every storage engine has exactly one RsmEngine.
template <typename IoImpl>
class ShardManager {
public:
ShardManager(io::Io<IoImpl> io, size_t shard_worker_threads, Address coordinator_leader)
: io_(io), coordinator_leader_(coordinator_leader) {
ShardManager(io::Io<IoImpl> io, size_t shard_worker_threads, Address coordinator_leader,
bool serialize_shard_splits_for_determinisim = false)
: io_(io),
coordinator_leader_(coordinator_leader),
serialize_shard_splits_for_determinisim_(serialize_shard_splits_for_determinisim) {
MG_ASSERT(shard_worker_threads >= 1);
for (int i = 0; i < shard_worker_threads; i++) {
@@ -149,7 +154,7 @@ class ShardManager {
/// Periodic protocol maintenance. Returns the time that Cron should be called again
/// in the future.
Time Cron() {
spdlog::info("running ShardManager::Cron, address {}", io_.GetAddress().ToString());
spdlog::trace("ShardManager running Cron, address {}", io_.GetAddress().ToString());
Time now = io_.Now();
if (now >= next_reconciliation_) {
@@ -174,7 +179,39 @@ class ShardManager {
/// Returns the Address for our underlying Io implementation
Address GetAddress() { return io_.GetAddress(); }
void Receive(ShardManagerMessages &&smm, RequestId request_id, Address from) {}
void InitializeSplitShard(msgs::InitializeSplitShard &&init_split_shard) {
spdlog::trace("ShardManager received InitializeSplitShard message");
for (const auto &[from_uuid, new_uuid] : init_split_shard.uuid_mapping) {
bool has_source = rsm_worker_mapping_.contains(from_uuid);
if (has_source) {
const auto low_key = init_split_shard.shard->LowKey();
coordinator::ShardId new_shard_id = std::make_pair(init_split_shard.shard->PrimaryLabel(), low_key);
spdlog::debug("ShardManager initialized split shard {} with uuid {} and low key {}",
init_split_shard.shard->Version().logical_id, new_uuid, low_key.back());
msgs::InitializeSplitShardByUUID msg{.shard = std::move(init_split_shard.shard), .shard_uuid = new_uuid};
SendToWorkerByUuid(new_uuid, std::move(msg));
initialized_but_not_confirmed_rsm_.emplace(new_uuid, new_shard_id);
break;
}
}
}
void Receive(ShardManagerMessages &&smm, RequestId request_id, Address from) {
std::visit(utils::Overloaded{[this](msgs::SuggestedSplitInfo &&split_info) {
spdlog::debug(
"ShardManager adding new suggested split info to the pending_splits_ structure");
pending_splits_.emplace(std::move(split_info));
},
[this](msgs::InitializeSplitShard &&init_split_shard) {
// TODO(jbajic) remove pending split for this completed split
// TODO(jbajic) Add new shard to initialized but not confirmed rsm
spdlog::debug("ShardManager received a new split shard that it will now initialize");
InitializeSplitShard(std::move(init_split_shard));
}},
std::move(smm));
}
void Route(ShardMessages &&sm, RequestId request_id, Address to, Address from) {
Address address = io_.GetAddress();
@@ -201,14 +238,13 @@ class ShardManager {
std::vector<shard_worker::Queue> workers_;
std::vector<std::jthread> worker_handles_;
std::vector<size_t> worker_rsm_counts_;
std::set<msgs::SuggestedSplitInfo> pending_splits_;
std::unordered_map<uuid, size_t, boost::hash<boost::uuids::uuid>> rsm_worker_mapping_;
Time next_reconciliation_ = Time::min();
Address coordinator_leader_;
std::optional<ResponseFuture<WriteResponse<CoordinatorWriteResponses>>> heartbeat_res_;
// TODO(tyler) over time remove items from initialized_but_not_confirmed_rsm_
// after the Coordinator is clearly aware of them
std::set<boost::uuids::uuid> initialized_but_not_confirmed_rsm_;
std::map<boost::uuids::uuid, coordinator::ShardId> initialized_but_not_confirmed_rsm_;
bool serialize_shard_splits_for_determinisim_;
void Reconciliation() {
if (heartbeat_res_.has_value()) {
@@ -218,18 +254,18 @@ class ShardManager {
heartbeat_res_.reset();
if (response_result.HasError()) {
spdlog::error("SM timed out while trying to reach C");
spdlog::info("ShardManager timed out while trying to reach Coordinator");
} else {
auto response_envelope = response_result.GetValue();
WriteResponse<CoordinatorWriteResponses> wr = response_envelope.message;
if (wr.retry_leader.has_value()) {
spdlog::info("SM redirected to new C leader");
spdlog::info("ShardManager redirected to new Coordinator leader");
coordinator_leader_ = wr.retry_leader.value();
} else if (wr.success) {
CoordinatorWriteResponses cwr = wr.write_return;
HeartbeatResponse hr = std::get<HeartbeatResponse>(cwr);
spdlog::info("SM received heartbeat response from C");
spdlog::info("ShardManager received heartbeat response from Coordinator");
EnsureShardsInitialized(hr);
}
@@ -242,28 +278,35 @@ class ShardManager {
HeartbeatRequest req{
.from_storage_manager = GetAddress(),
.initialized_rsms = initialized_but_not_confirmed_rsm_,
.suggested_splits = std::move(pending_splits_),
};
CoordinatorWriteRequests cwr = req;
WriteRequest<CoordinatorWriteRequests> ww;
ww.operation = cwr;
spdlog::info("SM sending heartbeat to coordinator {}", coordinator_leader_.ToString());
spdlog::info("ShardManager sending heartbeat to coordinator {} with {} initialized rsms",
coordinator_leader_.ToString(), initialized_but_not_confirmed_rsm_.size());
heartbeat_res_.emplace(std::move(
io_.template Request<WriteRequest<CoordinatorWriteRequests>, WriteResponse<CoordinatorWriteResponses>>(
coordinator_leader_, ww)));
spdlog::info("SM sent heartbeat");
spdlog::info("ShardManager sent heartbeat");
}
void EnsureShardsInitialized(HeartbeatResponse hr) {
for (const auto &acknowledged_rsm : hr.acknowledged_initialized_rsms) {
initialized_but_not_confirmed_rsm_.erase(acknowledged_rsm);
}
for (const auto &to_init : hr.shards_to_initialize) {
initialized_but_not_confirmed_rsm_.emplace(to_init.uuid);
coordinator::ShardId new_shard_id = std::make_pair(to_init.label_id, to_init.min_key);
spdlog::trace("ShardManager has been told to initialize shard rsm with uuid {}", to_init.uuid);
initialized_but_not_confirmed_rsm_.emplace(to_init.uuid, new_shard_id);
if (rsm_worker_mapping_.contains(to_init.uuid)) {
// it's not a bug for the coordinator to send us UUIDs that we have
// already created, because there may have been lag that caused
// the coordinator not to hear back from us.
return;
spdlog::info(
"ShardManager forwarding shard intialization request to worker despite a mapping already existing. This "
"can happen due to benign race conditions.");
}
size_t worker_index = UuidToWorkerIndex(to_init.uuid);
@@ -272,6 +315,54 @@ class ShardManager {
rsm_worker_mapping_.emplace(to_init.uuid, worker_index);
}
for (const auto &to_split : hr.shards_to_split) {
for (const auto &[source, destination] : to_split.uuid_mapping) {
if (rsm_worker_mapping_.contains(source)) {
// Create the proper layered request providing a Raft write
// request to the local shard rsm, under the guess that it is
// the current leader. Most of the time this will be an incorrect
// guess, but it will eventually succeed when the right ShardManager
// happens to send the message to the leader shard that is local.
// This is done to avoid blocking on an RsmClient or maintaining
// complex async request logic. It's fine to fire-and-forget because
// it is rare and will eventually succeed.
msgs::WriteRequests split_request_1 =
msgs::SplitRequest{.split_key = conversions::ConvertValueVector(to_split.split_key),
.old_shard_version = to_split.old_shard_version,
.new_lhs_shard_version = to_split.new_lhs_shard_version,
.new_rhs_shard_version = to_split.new_rhs_shard_version,
.uuid_mapping = to_split.uuid_mapping};
WriteRequest<msgs::WriteRequests> split_request_2;
split_request_2.operation = split_request_1;
ShardMessages split_request_3 = split_request_2;
const Address our_address = io_.GetAddress();
Address shard_address = our_address;
shard_address.unique_id = source;
shard_worker::RouteMessage shard_worker_message = {
.message = split_request_3,
.request_id = 0,
.to = shard_address,
.from = our_address,
};
SendToWorkerByUuid(source, shard_worker_message);
if (serialize_shard_splits_for_determinisim_) {
// This is only serialized during simulation for determinism
// purposes, and has been tested for correctness without the
// imposed determinism.
size_t worker_index = UuidToWorkerIndex(source);
workers_[worker_index].BlockOnQuiescence();
}
} else {
MG_ASSERT(false, "bad split source: {}", source);
}
}
}
}
};

View File

@@ -17,7 +17,6 @@
#include <optional>
#include <unordered_set>
#include <utility>
#include <variant>
#include "common/errors.hpp"
#include "parser/opencypher/parser.hpp"
@@ -61,6 +60,8 @@ using conversions::FromMap;
using conversions::FromPropertyValueToValue;
using conversions::ToMsgsVertexId;
using conversions::ToPropertyValue;
using io::messages::ShardManagerMessages;
using msgs::SplitResponse;
auto CreateErrorResponse(const ShardError &shard_error, const auto transaction_id, const std::string_view action) {
msgs::ShardError message_shard_error{shard_error.code, shard_error.message};
@@ -70,6 +71,16 @@ auto CreateErrorResponse(const ShardError &shard_error, const auto transaction_i
}
msgs::WriteResponses ShardRsm::ApplyWrite(msgs::CreateVerticesRequest &&req) {
if (req.shard_map_version < shard_->Version()) {
spdlog::debug("ShardRsm Rejecting client request with stale ShardMap version, so that they retry");
return msgs::CreateVerticesResponse{
.error = msgs::ShardError{
.code = common::ErrorCode::STALE_SHARD_MAP,
.message = "Shard has a higher version than the requestor's ShardMap due to a Shard split or merge, so the "
"requestor must refresh their ShardMap and try again",
}};
}
auto acc = shard_->Access(req.transaction_id);
std::optional<msgs::ShardError> shard_error;
@@ -90,10 +101,12 @@ msgs::WriteResponses ShardRsm::ApplyWrite(msgs::CreateVerticesRequest &&req) {
PrimaryKey transformed_pk;
std::transform(new_vertex.primary_key.begin(), new_vertex.primary_key.end(), std::back_inserter(transformed_pk),
[](msgs::Value &val) { return ToPropertyValue(std::move(val)); });
auto result_schema = acc.CreateVertexAndValidate(converted_label_ids, transformed_pk, converted_property_map);
if (result_schema.HasError()) {
shard_error.emplace(CreateErrorResponse(result_schema.GetError(), req.transaction_id, "creating vertices"));
auto error_opt = acc.CreateVertexAndValidate(new_vertex.idempotency_token, converted_label_ids, transformed_pk,
converted_property_map);
if (error_opt) {
shard_error.emplace(CreateErrorResponse(error_opt.value(), req.transaction_id, "creating vertices"));
break;
}
}
@@ -199,7 +212,8 @@ msgs::WriteResponses ShardRsm::ApplyWrite(msgs::CreateExpandRequest &&req) {
break;
}
auto edge_acc = acc.CreateEdge(from_vertex_id, to_vertex_id, new_expand.type.id, Gid::FromUint(new_expand.id.gid));
auto edge_acc = acc.CreateEdge(from_vertex_id, to_vertex_id, new_expand.type.id, Gid::FromUint(new_expand.id.gid),
new_expand.idempotency_token);
if (edge_acc.HasValue()) {
auto edge = edge_acc.GetValue();
if (!new_expand.properties.empty()) {
@@ -315,7 +329,32 @@ msgs::WriteResponses ShardRsm::ApplyWrite(msgs::UpdateEdgesRequest &&req) {
return msgs::UpdateEdgesResponse{std::move(shard_error)};
}
msgs::WriteResponses ShardRsm::ApplyWrite(msgs::SplitRequest &&req) {
auto converted_primary_key = conversions::ConvertPropertyVector(req.split_key);
auto new_shard_split_data = shard_->PerformSplit(converted_primary_key, req.old_shard_version,
req.new_lhs_shard_version, req.new_rhs_shard_version);
if (new_shard_split_data) {
spdlog::debug("ShardRsm performed split from version {} to versions {} and {}", req.old_shard_version,
req.new_lhs_shard_version, req.new_rhs_shard_version);
msgs::InitializeSplitShard msg{.shard = Shard::FromSplitData(std::move(*new_shard_split_data)),
.uuid_mapping = req.uuid_mapping};
shard_manager_sender_.Send(std::move(msg));
}
return SplitResponse{};
}
msgs::ReadResponses ShardRsm::HandleRead(msgs::ScanVerticesRequest &&req) {
if (req.shard_map_version < shard_->Version()) {
spdlog::debug("ShardRsm Rejecting client request with stale ShardMap version, so that they retry");
return msgs::ScanVerticesResponse{
.error = msgs::ShardError{
.code = common::ErrorCode::STALE_SHARD_MAP,
.message = "Shard has a higher version than the requestor's ShardMap due to a Shard split or merge, so the "
"requestor must refresh their ShardMap and try again",
}};
}
auto acc = shard_->Access(req.transaction_id);
std::optional<msgs::ShardError> shard_error;
@@ -409,6 +448,9 @@ msgs::ReadResponses ShardRsm::HandleRead(msgs::ScanVerticesRequest &&req) {
resp.results = std::move(results);
}
spdlog::trace("Shard version {} returning {} results for ScanVertices", shard_->Version().logical_id,
resp.results.size());
return resp;
}

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -12,17 +12,20 @@
#pragma once
#include <memory>
#include <variant>
#include <optional>
#include <openssl/ec.h>
#include "io/messages.hpp"
#include "query/v2/requests.hpp"
#include "storage/v3/shard.hpp"
#include "storage/v3/value_conversions.hpp"
#include "storage/v3/vertex_accessor.hpp"
namespace memgraph::storage::v3 {
class ShardRsm {
std::unique_ptr<Shard> shard_;
std::shared_ptr<Shard> shard_;
utils::Sender<msgs::InitializeSplitShard> shard_manager_sender_;
msgs::ReadResponses HandleRead(msgs::ExpandOneRequest &&req);
msgs::ReadResponses HandleRead(msgs::GetPropertiesRequest &&req);
@@ -36,10 +39,26 @@ class ShardRsm {
msgs::WriteResponses ApplyWrite(msgs::DeleteEdgesRequest &&req);
msgs::WriteResponses ApplyWrite(msgs::UpdateEdgesRequest &&req);
msgs::WriteResponses ApplyWrite(msgs::SplitRequest &&req);
msgs::WriteResponses ApplyWrite(msgs::CommitRequest &&req);
public:
explicit ShardRsm(std::unique_ptr<Shard> &&shard) : shard_(std::move(shard)){};
ShardRsm(std::shared_ptr<Shard> &&shard, utils::Sender<msgs::InitializeSplitShard> shard_manager_sender)
: shard_(std::move(shard)), shard_manager_sender_{std::move(shard_manager_sender)} {};
std::optional<msgs::SuggestedSplitInfo> ShouldSplit() const noexcept {
auto split_info = shard_->ShouldSplit();
if (split_info) {
return msgs::SuggestedSplitInfo{
.shard_to_split_uuid = split_info->shard_to_split_uuid,
.label_id = split_info->label_id,
.splitting_shard_low_key = conversions::ConvertValueVector(split_info->splitting_shard_low_key),
.split_key = conversions::ConvertValueVector(split_info->split_key),
.shard_version = split_info->shard_version};
}
return std::nullopt;
}
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
msgs::ReadResponses Read(msgs::ReadRequests requests) {

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -17,9 +17,11 @@
#include <queue>
#include <variant>
#include <boost/core/demangle.hpp>
#include <boost/uuid/uuid.hpp>
#include "coordinator/coordinator.hpp"
#include "coordinator/hybrid_logical_clock.hpp"
#include "coordinator/shard_map.hpp"
#include "io/address.hpp"
#include "io/future.hpp"
@@ -46,6 +48,7 @@ using io::RequestId;
using io::Time;
using io::messages::ShardMessages;
using io::rsm::Raft;
using msgs::InitializeSplitShardByUUID;
using msgs::ReadRequests;
using msgs::ReadResponses;
using msgs::WriteRequests;
@@ -66,7 +69,7 @@ struct RouteMessage {
Address from;
};
using Message = std::variant<ShutDown, Cron, ShardToInitialize, RouteMessage>;
using Message = std::variant<ShutDown, Cron, ShardToInitialize, RouteMessage, InitializeSplitShardByUUID>;
struct QueueInner {
std::mutex mu{};
@@ -155,9 +158,54 @@ class ShardWorker {
}
bool Process(RouteMessage &&route_message) {
auto &rsm = rsm_map_.at(route_message.to.unique_id);
if (rsm_map_.contains(route_message.to.unique_id)) {
spdlog::info("ShardWorker routing message to rsm {}", route_message.to.unique_id);
auto &rsm = rsm_map_.at(route_message.to.unique_id);
rsm.Handle(std::move(route_message.message), route_message.request_id, route_message.from);
rsm.Handle(std::move(route_message.message), route_message.request_id, route_message.from);
} else {
auto type_info = std::visit([&](const auto &msg) { return io::TypeInfoFor(msg); }, route_message.message);
std::string demangled_name = boost::core::demangle(type_info.get().name());
spdlog::debug(
"ShardWorker received {} message for rsm {} which does not exist on our system (possibly due to a split not "
"having been applied locally yet)",
demangled_name, route_message.to.unique_id);
}
return true;
}
bool Process(InitializeSplitShardByUUID &&initialize_split_shard) {
if (rsm_map_.contains(initialize_split_shard.shard_uuid)) {
// it's not a bug for the coordinator to send us UUIDs that we have
// already created, because there may have been lag that caused
// the coordinator not to hear back from us.
return true;
}
auto rsm_io = io_.ForkLocal(initialize_split_shard.shard_uuid);
// TODO(tyler) get peers from Coordinator in HeartbeatResponse
std::vector<Address> rsm_peers = {};
Address local_shard_manager_address = io_.GetAddress().ForkLocalShardManager();
utils::Sender<msgs::InitializeSplitShard> local_shard_manager_sender =
io_.template GetSender<msgs::InitializeSplitShard>(local_shard_manager_address);
// TODO(tyler) pass this local_shard_manager_sender to the Shard so that it can communicate back to the local
// manager from split code
ShardRsm rsm_state{std::move(initialize_split_shard.shard), std::move(local_shard_manager_sender)};
ShardRaft<IoImpl> rsm{std::move(rsm_io), rsm_peers, std::move(rsm_state)};
spdlog::info("ShardWorker created a new shard with UUID {}", initialize_split_shard.shard_uuid);
// perform an initial Cron call for the new RSM
Time next_cron = rsm.Cron();
cron_schedule_.push(std::make_pair(next_cron, initialize_split_shard.shard_uuid));
rsm_map_.emplace(initialize_split_shard.shard_uuid, std::move(rsm));
return true;
}
@@ -173,6 +221,13 @@ class ShardWorker {
auto &rsm = rsm_map_.at(uuid);
Time next_for_uuid = rsm.Cron();
// Check if shard should split
if (auto split_info = rsm.ShouldSplit(); split_info) {
split_info->shard_to_split_uuid = uuid;
const auto shard_manager_addr = io_.GetAddress().ForkLocalShardManager();
io_.Send(shard_manager_addr, 0, *split_info);
}
cron_schedule_.pop();
cron_schedule_.push(std::make_pair(next_for_uuid, uuid));
} else {
@@ -188,6 +243,7 @@ class ShardWorker {
// it's not a bug for the coordinator to send us UUIDs that we have
// already created, because there may have been lag that caused
// the coordinator not to hear back from us.
spdlog::debug("ShardWorker told to initialize already-existing shard with UUID {} - skipping", to_init.uuid);
return;
}
@@ -196,14 +252,22 @@ class ShardWorker {
// TODO(tyler) get peers from Coordinator in HeartbeatResponse
std::vector<Address> rsm_peers = {};
std::unique_ptr<Shard> shard = std::make_unique<Shard>(to_init.label_id, to_init.min_key, to_init.max_key,
to_init.schema, to_init.config, to_init.id_to_names);
Address local_shard_manager_address = io_.GetAddress().ForkLocalShardManager();
utils::Sender<msgs::InitializeSplitShard> local_shard_manager_sender =
io_.template GetSender<msgs::InitializeSplitShard>(local_shard_manager_address);
ShardRsm rsm_state{std::move(shard)};
// TODO(tyler) pass this local_shard_manager_sender to the Shard so that it can communicate back to the local
// manager from split code
std::unique_ptr<Shard> shard =
std::make_unique<Shard>(to_init.label_id, to_init.min_key, to_init.max_key, to_init.schema,
to_init.new_shard_version, to_init.config, to_init.id_to_names);
ShardRsm rsm_state{std::move(shard), std::move(local_shard_manager_sender)};
ShardRaft<IoImpl> rsm{std::move(rsm_io), rsm_peers, std::move(rsm_state)};
spdlog::info("SM created a new shard with UUID {}", to_init.uuid);
spdlog::debug("ShardWorker created a new shard with UUID {}", to_init.uuid);
// perform an initial Cron call for the new RSM
Time next_cron = rsm.Cron();

351
src/storage/v3/splitter.cpp Normal file
View File

@@ -0,0 +1,351 @@
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#include "storage/v3/splitter.hpp"
#include <algorithm>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include "storage/v3/config.hpp"
#include "storage/v3/delta.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/indices.hpp"
#include "storage/v3/key_store.hpp"
#include "storage/v3/name_id_mapper.hpp"
#include "storage/v3/schemas.hpp"
#include "storage/v3/shard.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/vertex.hpp"
#include "utils/logging.hpp"
namespace memgraph::storage::v3 {
Splitter::Splitter(const LabelId primary_label, VertexContainer &vertices, EdgeContainer &edges,
std::map<uint64_t, std::unique_ptr<Transaction>> &start_logical_id_to_transaction, Indices &indices,
const Config &config, const std::vector<SchemaProperty> &schema, const NameIdMapper &name_id_mapper)
: primary_label_(primary_label),
vertices_(vertices),
edges_(edges),
start_logical_id_to_transaction_(start_logical_id_to_transaction),
indices_(indices),
config_(config),
schema_(schema),
name_id_mapper_(name_id_mapper) {}
SplitData Splitter::SplitShard(const PrimaryKey &split_key, const std::optional<PrimaryKey> &max_primary_key,
const Hlc shard_version) {
SplitData data{.primary_label = primary_label_,
.min_primary_key = split_key,
.max_primary_key = max_primary_key,
.schema = schema_,
.config = config_,
.id_to_name = name_id_mapper_.GetIdToNameMap(),
.shard_version = shard_version};
std::set<uint64_t> collected_transactions_;
data.vertices = CollectVertices(data, collected_transactions_, split_key);
data.edges = CollectEdges(collected_transactions_, data.vertices, split_key);
data.transactions = CollectTransactions(collected_transactions_, data.vertices, *data.edges, split_key);
return data;
}
void Splitter::ScanDeltas(std::set<uint64_t> &collected_transactions_, Delta *delta) {
while (delta != nullptr) {
collected_transactions_.insert(delta->commit_info->start_or_commit_timestamp.logical_id);
delta = delta->next;
}
}
VertexContainer Splitter::CollectVertices(SplitData &data, std::set<uint64_t> &collected_transactions_,
const PrimaryKey &split_key) {
data.label_indices = indices_.label_index.SplitIndexEntries(split_key);
data.label_property_indices = indices_.label_property_index.SplitIndexEntries(split_key);
VertexContainer splitted_data;
auto split_key_it = vertices_.find(split_key);
while (split_key_it != vertices_.end()) {
// Go through deltas and pick up transactions start_id/commit_id
ScanDeltas(collected_transactions_, split_key_it->second.delta);
auto next_it = std::next(split_key_it);
const auto &[splitted_vertex_it, inserted, node] = splitted_data.insert(vertices_.extract(split_key_it->first));
MG_ASSERT(inserted, "Failed to extract vertex!");
split_key_it = next_it;
}
return splitted_data;
}
std::optional<EdgeContainer> Splitter::CollectEdges(std::set<uint64_t> &collected_transactions_,
const VertexContainer &split_vertices,
const PrimaryKey &split_key) {
if (!config_.items.properties_on_edges) {
return std::nullopt;
}
EdgeContainer splitted_edges;
const auto split_vertex_edges = [&](const auto &edges_ref) {
// This is safe since if properties_on_edges is true, the this must be a ptr
for (const auto &edge_ref : edges_ref) {
auto *edge = std::get<2>(edge_ref).ptr;
const auto &other_vtx = std::get<1>(edge_ref);
ScanDeltas(collected_transactions_, edge->delta);
// Check if src and dest edge are both on splitted shard so we know if we
// should remove orphan edge, or make a clone
if (other_vtx.primary_key >= split_key) {
// Remove edge from shard
splitted_edges.insert(edges_.extract(edge->gid));
} else {
splitted_edges.insert({edge->gid, Edge{edge->gid, edge->delta}});
}
}
};
for (const auto &vertex : split_vertices) {
split_vertex_edges(vertex.second.in_edges);
split_vertex_edges(vertex.second.out_edges);
}
return splitted_edges;
}
std::map<uint64_t, std::unique_ptr<Transaction>> Splitter::CollectTransactions(
const std::set<uint64_t> &collected_transactions_, VertexContainer &cloned_vertices, EdgeContainer &cloned_edges,
const PrimaryKey &split_key) {
std::map<uint64_t, std::unique_ptr<Transaction>> transactions;
for (const auto &[commit_start, transaction] : start_logical_id_to_transaction_) {
// We need all transaction whose deltas need to be resolved for any of the
// entities
if (collected_transactions_.contains(transaction->commit_info->start_or_commit_timestamp.logical_id)) {
transactions.insert({commit_start, start_logical_id_to_transaction_[commit_start]->Clone()});
}
}
// It is necessary to clone all the transactions first so we have new addresses
// for deltas, before doing alignment of deltas and prev_ptr
AdjustClonedTransactions(transactions, cloned_vertices, cloned_edges, split_key);
return transactions;
}
void PruneDeltas(Transaction &cloned_transaction, std::map<uint64_t, std::unique_ptr<Transaction>> &cloned_transactions,
const PrimaryKey &split_key) {
// Remove delta chains that don't point to objects on splitted shard
auto cloned_delta_it = cloned_transaction.deltas.begin();
while (cloned_delta_it != cloned_transaction.deltas.end()) {
const auto prev = cloned_delta_it->prev.Get();
switch (prev.type) {
case PreviousPtr::Type::DELTA:
case PreviousPtr::Type::NULLPTR:
++cloned_delta_it;
break;
case PreviousPtr::Type::VERTEX: {
if (prev.vertex->first < split_key) {
// We can remove this delta chain
auto *current_next_delta = cloned_delta_it->next;
cloned_delta_it = cloned_transaction.deltas.erase(cloned_delta_it);
while (current_next_delta != nullptr) {
auto *next_delta = current_next_delta->next;
// Find next delta transaction delta list
auto current_transaction_it = std::ranges::find_if(
cloned_transactions,
[&start_or_commit_timestamp =
current_next_delta->commit_info->start_or_commit_timestamp](const auto &transaction) {
return transaction.second->start_timestamp == start_or_commit_timestamp ||
transaction.second->commit_info->start_or_commit_timestamp == start_or_commit_timestamp;
});
MG_ASSERT(current_transaction_it != cloned_transactions.end(), "Error when pruning deltas!");
// Remove it
current_transaction_it->second->deltas.remove_if(
[&current_next_delta = *current_next_delta](const auto &delta) { return delta == current_next_delta; });
current_next_delta = next_delta;
}
} else {
++cloned_delta_it;
}
break;
}
case PreviousPtr::Type::EDGE:
++cloned_delta_it;
break;
}
}
}
void Splitter::AdjustClonedTransactions(std::map<uint64_t, std::unique_ptr<Transaction>> &cloned_transactions,
VertexContainer &cloned_vertices, EdgeContainer &cloned_edges,
const PrimaryKey &split_key) {
for (auto &[commit_start, cloned_transaction] : cloned_transactions) {
AdjustClonedTransaction(*cloned_transaction, *start_logical_id_to_transaction_[commit_start], cloned_transactions,
cloned_vertices, cloned_edges, split_key);
}
// Prune deltas whose delta chain points to vertex/edge that should not belong on that shard
// Prune must be after ajdust, since next, and prev are not set and we cannot follow the chain
for (auto &[commit_start, cloned_transaction] : cloned_transactions) {
PruneDeltas(*cloned_transaction, cloned_transactions, split_key);
}
}
inline bool IsDeltaHeadOfChain(const PreviousPtr::Type &delta_type) {
return delta_type == PreviousPtr::Type::VERTEX || delta_type == PreviousPtr::Type::EDGE;
}
bool DoesPrevPtrPointsToSplittedData(const PreviousPtr::Pointer &prev_ptr, const PrimaryKey &split_key) {
return prev_ptr.type == PreviousPtr::Type::VERTEX && prev_ptr.vertex->first < split_key;
}
void Splitter::AdjustClonedTransaction(Transaction &cloned_transaction, const Transaction &transaction,
std::map<uint64_t, std::unique_ptr<Transaction>> &cloned_transactions,
VertexContainer &cloned_vertices, EdgeContainer &cloned_edges,
const PrimaryKey & /*split_key*/) {
auto delta_it = transaction.deltas.begin();
auto cloned_delta_it = cloned_transaction.deltas.begin();
while (delta_it != transaction.deltas.end()) {
// We can safely ignore deltas which are not head of delta chain
// Dont' adjust delta chain that points to irrelevant data vertices/edges
if (const auto delta_prev = delta_it->prev.Get(); !IsDeltaHeadOfChain(delta_prev.type)) {
++delta_it;
++cloned_delta_it;
continue;
}
const auto *delta = &*delta_it;
auto *cloned_delta = &*cloned_delta_it;
Delta *cloned_delta_prev_ptr = cloned_delta;
while (delta->next != nullptr) {
AdjustEdgeRef(*cloned_delta, cloned_edges);
// Align next ptr
AdjustDeltaNext(*delta, *cloned_delta, cloned_transactions);
// Align prev ptr
if (cloned_delta_prev_ptr != nullptr) {
AdjustDeltaPrevPtr(*delta, *cloned_delta_prev_ptr, cloned_transactions, cloned_vertices, cloned_edges);
}
// TODO Next delta might not belong to the cloned transaction and thats
// why we skip this delta of the delta chain
if (cloned_delta->next != nullptr) {
cloned_delta = cloned_delta->next;
cloned_delta_prev_ptr = cloned_delta;
} else {
cloned_delta_prev_ptr = nullptr;
}
delta = delta->next;
}
// Align prev ptr
if (cloned_delta_prev_ptr != nullptr) {
AdjustDeltaPrevPtr(*delta, *cloned_delta_prev_ptr, cloned_transactions, cloned_vertices, cloned_edges);
}
++delta_it;
++cloned_delta_it;
}
MG_ASSERT(delta_it == transaction.deltas.end() && cloned_delta_it == cloned_transaction.deltas.end(),
"Both iterators must be exhausted!");
}
void Splitter::AdjustEdgeRef(Delta &cloned_delta, EdgeContainer &cloned_edges) const {
switch (cloned_delta.action) {
case Delta::Action::ADD_IN_EDGE:
case Delta::Action::ADD_OUT_EDGE:
case Delta::Action::REMOVE_IN_EDGE:
case Delta::Action::REMOVE_OUT_EDGE: {
// Find edge
if (config_.items.properties_on_edges) {
// Only case when not finding is when the edge is not on splitted shard
// TODO Do this after prune an move condition into assert
if (const auto cloned_edge_it =
std::ranges::find_if(cloned_edges, [edge_ptr = cloned_delta.vertex_edge.edge.ptr](
const auto &elem) { return elem.second.gid == edge_ptr->gid; });
cloned_edge_it != cloned_edges.end()) {
cloned_delta.vertex_edge.edge = EdgeRef{&cloned_edge_it->second};
}
}
break;
}
case Delta::Action::DELETE_OBJECT:
case Delta::Action::RECREATE_OBJECT:
case Delta::Action::SET_PROPERTY:
case Delta::Action::ADD_LABEL:
case Delta::Action::REMOVE_LABEL: {
// noop
break;
}
}
}
void Splitter::AdjustDeltaNext(const Delta &original, Delta &cloned,
std::map<uint64_t, std::unique_ptr<Transaction>> &cloned_transactions) {
// Get cloned_delta->next transaction, using delta->next original transaction
auto cloned_transaction_it = std::ranges::find_if(cloned_transactions, [&original](const auto &elem) {
return elem.second->start_timestamp == original.next->commit_info->start_or_commit_timestamp ||
elem.second->commit_info->start_or_commit_timestamp == original.next->commit_info->start_or_commit_timestamp;
});
// TODO(jbajic) What if next in delta chain does not belong to cloned transaction?
// MG_ASSERT(cloned_transaction_it != cloned_transactions.end(), "Cloned transaction not found");
if (cloned_transaction_it == cloned_transactions.end()) return;
// Find cloned delta in delta list of cloned transaction
auto found_cloned_delta_it = std::ranges::find_if(
cloned_transaction_it->second->deltas, [&original](const auto &elem) { return elem.id == original.next->id; });
MG_ASSERT(found_cloned_delta_it != cloned_transaction_it->second->deltas.end(), "Delta with given uuid must exist!");
cloned.next = &*found_cloned_delta_it;
}
void Splitter::AdjustDeltaPrevPtr(const Delta &original, Delta &cloned,
std::map<uint64_t, std::unique_ptr<Transaction>> &cloned_transactions,
VertexContainer & /*cloned_vertices*/, EdgeContainer &cloned_edges) {
auto ptr = original.prev.Get();
switch (ptr.type) {
case PreviousPtr::Type::NULLPTR: {
// noop
break;
}
case PreviousPtr::Type::DELTA: {
// Same as for deltas except don't align next but prev
auto cloned_transaction_it = std::ranges::find_if(cloned_transactions, [&ptr](const auto &elem) {
return elem.second->start_timestamp == ptr.delta->commit_info->start_or_commit_timestamp ||
elem.second->commit_info->start_or_commit_timestamp == ptr.delta->commit_info->start_or_commit_timestamp;
});
MG_ASSERT(cloned_transaction_it != cloned_transactions.end(), "Cloned transaction not found");
// Find cloned delta in delta list of cloned transaction
auto found_cloned_delta_it =
std::ranges::find_if(cloned_transaction_it->second->deltas,
[delta = ptr.delta](const auto &elem) { return elem.id == delta->id; });
MG_ASSERT(found_cloned_delta_it != cloned_transaction_it->second->deltas.end(),
"Delta with given id must exist!");
cloned.prev.Set(&*found_cloned_delta_it);
break;
}
case PreviousPtr::Type::VERTEX: {
// The vertex was extracted and it is safe to reuse address
cloned.prev.Set(ptr.vertex);
break;
}
case PreviousPtr::Type::EDGE: {
// We can never be here if we have properties on edge disabled
auto *cloned_edge = &*cloned_edges.find(ptr.edge->gid);
cloned.prev.Set(&cloned_edge->second);
break;
}
};
}
} // namespace memgraph::storage::v3

108
src/storage/v3/splitter.hpp Normal file
View File

@@ -0,0 +1,108 @@
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#pragma once
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include "coordinator/hybrid_logical_clock.hpp"
#include "storage/v3/config.hpp"
#include "storage/v3/delta.hpp"
#include "storage/v3/edge.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/indices.hpp"
#include "storage/v3/key_store.hpp"
#include "storage/v3/name_id_mapper.hpp"
#include "storage/v3/schemas.hpp"
#include "storage/v3/transaction.hpp"
#include "storage/v3/vertex.hpp"
#include "utils/concepts.hpp"
namespace memgraph::storage::v3 {
// If edge properties-on-edges is false then we don't need to send edges but
// only vertices, since they will contain those edges
struct SplitData {
LabelId primary_label;
PrimaryKey min_primary_key;
std::optional<PrimaryKey> max_primary_key;
std::vector<SchemaProperty> schema;
Config config;
std::unordered_map<uint64_t, std::string> id_to_name;
coordinator::Hlc shard_version;
VertexContainer vertices;
std::optional<EdgeContainer> edges;
std::map<uint64_t, std::unique_ptr<Transaction>> transactions;
std::map<LabelId, LabelIndex::IndexContainer> label_indices;
std::map<std::pair<LabelId, PropertyId>, LabelPropertyIndex::IndexContainer> label_property_indices;
};
class Splitter final {
public:
Splitter(LabelId primary_label, VertexContainer &vertices, EdgeContainer &edges,
std::map<uint64_t, std::unique_ptr<Transaction>> &start_logical_id_to_transaction, Indices &indices,
const Config &config, const std::vector<SchemaProperty> &schema, const NameIdMapper &name_id_mapper_);
Splitter(const Splitter &) = delete;
Splitter(Splitter &&) noexcept = delete;
Splitter &operator=(const Splitter &) = delete;
Splitter operator=(Splitter &&) noexcept = delete;
~Splitter() = default;
SplitData SplitShard(const PrimaryKey &split_key, const std::optional<PrimaryKey> &max_primary_key,
coordinator::Hlc shard_version);
private:
VertexContainer CollectVertices(SplitData &data, std::set<uint64_t> &collected_transactions_start_id,
const PrimaryKey &split_key);
std::optional<EdgeContainer> CollectEdges(std::set<uint64_t> &collected_transactions_start_id,
const VertexContainer &split_vertices, const PrimaryKey &split_key);
std::map<uint64_t, std::unique_ptr<Transaction>> CollectTransactions(
const std::set<uint64_t> &collected_transactions_start_id, VertexContainer &cloned_vertices,
EdgeContainer &cloned_edges, const PrimaryKey &split_key);
static void ScanDeltas(std::set<uint64_t> &collected_transactions_start_id, Delta *delta);
void AdjustClonedTransaction(Transaction &cloned_transaction, const Transaction &transaction,
std::map<uint64_t, std::unique_ptr<Transaction>> &cloned_transactions,
VertexContainer &cloned_vertices, EdgeContainer &cloned_edges,
const PrimaryKey &split_key);
void AdjustClonedTransactions(std::map<uint64_t, std::unique_ptr<Transaction>> &cloned_transactions,
VertexContainer &cloned_vertices, EdgeContainer &cloned_edges,
const PrimaryKey &split_key);
void AdjustEdgeRef(Delta &cloned_delta, EdgeContainer &cloned_edges) const;
static void AdjustDeltaNext(const Delta &original, Delta &cloned,
std::map<uint64_t, std::unique_ptr<Transaction>> &cloned_transactions);
static void AdjustDeltaPrevPtr(const Delta &original, Delta &cloned,
std::map<uint64_t, std::unique_ptr<Transaction>> &cloned_transactions,
VertexContainer &cloned_vertices, EdgeContainer &cloned_edges);
const LabelId primary_label_;
VertexContainer &vertices_;
EdgeContainer &edges_;
std::map<uint64_t, std::unique_ptr<Transaction>> &start_logical_id_to_transaction_;
Indices &indices_;
const Config &config_;
const std::vector<SchemaProperty> schema_;
const NameIdMapper &name_id_mapper_;
};
} // namespace memgraph::storage::v3

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -31,6 +31,16 @@ struct CommitInfo {
};
struct Transaction {
Transaction(coordinator::Hlc start_timestamp, CommitInfo new_commit_info, std::list<Delta> deltas,
uint64_t command_id, bool must_abort, bool is_aborted, IsolationLevel isolation_level)
: start_timestamp{start_timestamp},
commit_info{std::make_unique<CommitInfo>(new_commit_info)},
command_id(command_id),
deltas(std::move(deltas)),
must_abort(must_abort),
is_aborted(is_aborted),
isolation_level(isolation_level){};
Transaction(coordinator::Hlc start_timestamp, IsolationLevel isolation_level)
: start_timestamp(start_timestamp),
commit_info(std::make_unique<CommitInfo>(CommitInfo{false, {start_timestamp}})),
@@ -54,6 +64,55 @@ struct Transaction {
~Transaction() {}
std::list<Delta> CopyDeltas(CommitInfo *commit_info) const {
std::list<Delta> copied_deltas;
for (const auto &delta : deltas) {
switch (delta.action) {
case Delta::Action::DELETE_OBJECT:
copied_deltas.emplace_back(Delta::DeleteObjectTag{}, commit_info, delta.id, command_id,
delta.idempotency_token);
break;
case Delta::Action::RECREATE_OBJECT:
copied_deltas.emplace_back(Delta::RecreateObjectTag{}, commit_info, delta.id, command_id);
break;
case Delta::Action::ADD_LABEL:
copied_deltas.emplace_back(Delta::AddLabelTag{}, delta.label, commit_info, delta.id, command_id);
break;
case Delta::Action::REMOVE_LABEL:
copied_deltas.emplace_back(Delta::RemoveLabelTag{}, delta.label, commit_info, delta.id, command_id);
break;
case Delta::Action::ADD_IN_EDGE:
copied_deltas.emplace_back(Delta::AddInEdgeTag{}, delta.vertex_edge.edge_type, delta.vertex_edge.vertex_id,
delta.vertex_edge.edge, commit_info, delta.id, command_id);
break;
case Delta::Action::ADD_OUT_EDGE:
copied_deltas.emplace_back(Delta::AddOutEdgeTag{}, delta.vertex_edge.edge_type, delta.vertex_edge.vertex_id,
delta.vertex_edge.edge, commit_info, delta.id, command_id);
break;
case Delta::Action::REMOVE_IN_EDGE:
copied_deltas.emplace_back(Delta::RemoveInEdgeTag{}, delta.vertex_edge.edge_type, delta.vertex_edge.vertex_id,
delta.vertex_edge.edge, commit_info, delta.id, command_id);
break;
case Delta::Action::REMOVE_OUT_EDGE:
copied_deltas.emplace_back(Delta::RemoveOutEdgeTag{}, delta.vertex_edge.edge_type,
delta.vertex_edge.vertex_id, delta.vertex_edge.edge, commit_info, delta.id,
command_id);
break;
case Delta::Action::SET_PROPERTY:
copied_deltas.emplace_back(Delta::SetPropertyTag{}, delta.property.key, delta.property.value, commit_info,
delta.id, command_id);
break;
}
}
return copied_deltas;
}
// This does not solve the whole problem of copying deltas
std::unique_ptr<Transaction> Clone() const {
return std::make_unique<Transaction>(start_timestamp, *commit_info, CopyDeltas(commit_info.get()), command_id,
must_abort, is_aborted, isolation_level);
}
coordinator::Hlc start_timestamp;
std::unique_ptr<CommitInfo> commit_info;
uint64_t command_id;

View File

@@ -0,0 +1,42 @@
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#pragma once
#include <concepts>
#include <functional>
#include <type_traits>
namespace memgraph::utils {
// TODO(tyler) ensure that Message continues to represent
// reasonable constraints around message types over time,
// as we adapt things to use Thrift-generated message types.
template <typename T>
concept Message = std::movable<T> && std::copyable<T>;
/// This is a concrete type that allows one message type to be
/// sent to a single address. Initially intended to be used by the
/// Shard to send messages to the local ShardManager.
template <Message M>
class Sender {
public:
Sender() = default;
explicit Sender(std::function<void(M)> sender) : sender_(sender) {}
void Send(M &&message) { sender_(std::forward<M>(message)); }
private:
std::function<void(M)> sender_;
};
} // namespace memgraph::utils

View File

@@ -79,3 +79,6 @@ target_link_libraries(${test_prefix}data_structures_contains mg-utils mg-storage
add_benchmark(data_structures_remove.cpp)
target_link_libraries(${test_prefix}data_structures_remove mg-utils mg-storage-v3)
add_benchmark(storage_v3_split.cpp)
target_link_libraries(${test_prefix}storage_v3_split mg-storage-v3 mg-query-v2)

View File

@@ -0,0 +1,194 @@
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#include <cstdint>
#include <optional>
#include <vector>
#include <benchmark/benchmark.h>
#include <gflags/gflags.h>
#include "storage/v3/id_types.hpp"
#include "storage/v3/key_store.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/shard.hpp"
#include "storage/v3/vertex.hpp"
#include "storage/v3/vertex_id.hpp"
namespace memgraph::benchmark {
class ShardSplitBenchmark : public ::benchmark::Fixture {
protected:
using PrimaryKey = storage::v3::PrimaryKey;
using PropertyId = storage::v3::PropertyId;
using PropertyValue = storage::v3::PropertyValue;
using LabelId = storage::v3::LabelId;
using EdgeTypeId = storage::v3::EdgeTypeId;
using Shard = storage::v3::Shard;
using VertexId = storage::v3::VertexId;
using Gid = storage::v3::Gid;
void SetUp(const ::benchmark::State &state) override {
storage.emplace(primary_label, min_pk, std::nullopt, schema_property_vector, last_hlc);
storage->StoreMapping(
{{1, "label"}, {2, "property"}, {3, "edge_property"}, {4, "secondary_label"}, {5, "secondary_prop"}});
}
void TearDown(const ::benchmark::State &) override { storage = std::nullopt; }
const PropertyId primary_property{PropertyId::FromUint(2)};
const PropertyId secondary_property{PropertyId::FromUint(5)};
std::vector<storage::v3::SchemaProperty> schema_property_vector = {
storage::v3::SchemaProperty{primary_property, common::SchemaType::INT}};
const std::vector<PropertyValue> min_pk{PropertyValue{0}};
const LabelId primary_label{LabelId::FromUint(1)};
const LabelId secondary_label{LabelId::FromUint(4)};
const EdgeTypeId edge_type_id{EdgeTypeId::FromUint(3)};
std::optional<Shard> storage;
coordinator::Hlc last_hlc{0, io::Time{}};
coordinator::Hlc GetNextHlc() {
++last_hlc.logical_id;
last_hlc.coordinator_wall_clock += std::chrono::seconds(1);
return last_hlc;
}
};
BENCHMARK_DEFINE_F(ShardSplitBenchmark, BigDataSplit)(::benchmark::State &state) {
const auto number_of_vertices{state.range(0)};
std::random_device r;
std::default_random_engine e1(r());
std::uniform_int_distribution<int> uniform_dist(0, number_of_vertices);
for (int64_t i{0}; i < number_of_vertices; ++i) {
auto acc = storage->Access(GetNextHlc());
MG_ASSERT(acc.CreateVertexAndValidate({secondary_label}, PrimaryKey{PropertyValue(i)},
{{secondary_property, PropertyValue(i)}})
.HasValue(),
"Failed creating with pk {}", i);
if (i > 1) {
const auto vtx1 = uniform_dist(e1) % i;
const auto vtx2 = uniform_dist(e1) % i;
MG_ASSERT(acc.CreateEdge(VertexId{primary_label, {PropertyValue(vtx1)}},
VertexId{primary_label, {PropertyValue(vtx2)}}, edge_type_id, Gid::FromUint(i))
.HasValue(),
"Failed on {} and {}", vtx1, vtx2);
}
acc.Commit(GetNextHlc());
}
for (auto _ : state) {
auto data = storage->PerformSplit(PrimaryKey{PropertyValue{number_of_vertices / 2}}, last_hlc, GetNextHlc());
}
}
BENCHMARK_DEFINE_F(ShardSplitBenchmark, BigDataSplitWithGc)(::benchmark::State &state) {
const auto number_of_vertices{state.range(0)};
std::random_device r;
std::default_random_engine e1(r());
std::uniform_int_distribution<int> uniform_dist(0, number_of_vertices);
for (int64_t i{0}; i < number_of_vertices; ++i) {
auto acc = storage->Access(GetNextHlc());
MG_ASSERT(acc.CreateVertexAndValidate({secondary_label}, PrimaryKey{PropertyValue(i)},
{{secondary_property, PropertyValue(i)}})
.HasValue(),
"Failed creating with pk {}", i);
if (i > 1) {
const auto vtx1 = uniform_dist(e1) % i;
const auto vtx2 = uniform_dist(e1) % i;
MG_ASSERT(acc.CreateEdge(VertexId{primary_label, {PropertyValue(vtx1)}},
VertexId{primary_label, {PropertyValue(vtx2)}}, edge_type_id, Gid::FromUint(i))
.HasValue(),
"Failed on {} and {}", vtx1, vtx2);
}
acc.Commit(GetNextHlc());
}
storage->CollectGarbage(GetNextHlc().coordinator_wall_clock);
for (auto _ : state) {
auto data = storage->PerformSplit(PrimaryKey{PropertyValue{number_of_vertices / 2}}, last_hlc, GetNextHlc());
}
}
BENCHMARK_DEFINE_F(ShardSplitBenchmark, BigDataSplitWithFewTransactions)(::benchmark::State &state) {
const auto number_of_vertices = state.range(0);
const auto number_of_edges = state.range(1);
const auto number_of_transactions = state.range(2);
std::random_device r;
std::default_random_engine e1(r());
std::uniform_int_distribution<int> uniform_dist(0, number_of_vertices);
const auto max_transactions_needed = std::max(number_of_vertices, number_of_edges);
for (int64_t vertex_counter{number_of_vertices}, edge_counter{number_of_edges}, i{0};
vertex_counter > 0 || edge_counter > 0; --vertex_counter, --edge_counter, ++i) {
auto acc = storage->Access(GetNextHlc());
if (vertex_counter > 0) {
MG_ASSERT(acc.CreateVertexAndValidate({secondary_label}, PrimaryKey{PropertyValue(i)},
{{secondary_property, PropertyValue(i)}})
.HasValue(),
"Failed creating with pk {}", i);
}
if (edge_counter > 0 && i > 1) {
const auto vtx1 = uniform_dist(e1) % std::min(i, number_of_vertices);
const auto vtx2 = uniform_dist(e1) % std::min(i, number_of_vertices);
MG_ASSERT(acc.CreateEdge(VertexId{primary_label, {PropertyValue(vtx1)}},
VertexId{primary_label, {PropertyValue(vtx2)}}, edge_type_id, Gid::FromUint(i))
.HasValue(),
"Failed on {} and {}", vtx1, vtx2);
}
acc.Commit(GetNextHlc());
if (i == max_transactions_needed - number_of_transactions) {
storage->CollectGarbage(GetNextHlc().coordinator_wall_clock);
}
}
for (auto _ : state) {
// Don't create shard since shard deallocation can take some time as well
auto data = storage->PerformSplit(PrimaryKey{PropertyValue{number_of_vertices / 2}}, last_hlc, GetNextHlc());
}
}
// Range:
// Number of vertices
// This run is pessimistic, number of vertices corresponds with number if transactions
BENCHMARK_REGISTER_F(ShardSplitBenchmark, BigDataSplit)
->RangeMultiplier(10)
->Range(100'000, 100'000)
->Unit(::benchmark::kMillisecond);
// Range:
// Number of vertices
// This run is optimistic, in this run there are no transactions
BENCHMARK_REGISTER_F(ShardSplitBenchmark, BigDataSplitWithGc)
->RangeMultiplier(10)
->Range(100'000, 1'000'000)
->Unit(::benchmark::kMillisecond);
// Args:
// Number of vertices
// Number of edges
// Number of transaction
BENCHMARK_REGISTER_F(ShardSplitBenchmark, BigDataSplitWithFewTransactions)
->Args({100'000, 100'000, 1'000})
->Args({100'000, 100'000, 10'000})
->Args({1'000'000, 100'000, 1'000})
->Args({1'000'000, 100'000, 10'000})
->Args({100'000, 1'000'000, 1'000})
->Args({1'000'000, 1'00'000, 10'000})
->Unit(::benchmark::kMillisecond);
} // namespace memgraph::benchmark
BENCHMARK_MAIN();

View File

@@ -31,6 +31,7 @@ add_simulation_test(raft.cpp)
add_simulation_test(trial_query_storage/query_storage_test.cpp)
add_simulation_test(sharded_map.cpp)
add_simulation_test(shard_rsm.cpp)
add_simulation_test(cluster_property_test.cpp)
add_simulation_test(cluster_property_test_cypher_queries.cpp)
add_simulation_test(bulk_load.cpp)
add_simulation_test(random_workload.cpp)
add_simulation_test(request_router.cpp)
add_simulation_test(property_test_demo.cpp)

View File

@@ -33,24 +33,41 @@ using io::Time;
using io::simulator::SimulatorConfig;
using storage::v3::kMaximumCronInterval;
RC_GTEST_PROP(RandomClusterConfig, HappyPath, (ClusterConfig cluster_config, NonEmptyOpVec ops, uint64_t rng_seed)) {
RC_GTEST_PROP(RandomClusterConfig, BulkLoadAndSplit,
(ClusterConfig cluster_config, uint8_t inserts, uint64_t rng_seed)) {
spdlog::cfg::load_env_levels();
// This is a static workload that just inserts vertices and reads them back, which implicitly triggers concurrent
// splits
std::vector<Op> ops{};
for (int key = 0; key < inserts; key++) {
Op op1 = {.inner = AssertShardsSplit{}};
ops.emplace_back(std::move(op1));
Op op2 = {.inner = CreateVertex{.first = 0, .second = key}};
ops.emplace_back(std::move(op2));
}
Op op1 = {.inner = AssertShardsSplit{}};
ops.emplace_back(std::move(op1));
ops.emplace_back(Op{.inner = ScanAll{}});
SimulatorConfig sim_config{
.drop_percent = 0,
.perform_timeouts = false,
.perform_timeouts = true,
.scramble_messages = true,
.rng_seed = rng_seed,
.start_time = Time::min(),
// TODO(tyler) set abort_time to something more restrictive than Time::max()
.abort_time = Time::max(),
};
std::vector<std::string> queries = {"CREATE (n:test_label{property_1: 0, property_2: 0});", "MATCH (n) RETURN n;"};
auto [sim_stats_1, latency_stats_1] = RunClusterSimulation(sim_config, cluster_config, ops);
auto [sim_stats_2, latency_stats_2] = RunClusterSimulation(sim_config, cluster_config, ops);
auto [sim_stats_1, latency_stats_1] = RunClusterSimulationWithQueries(sim_config, cluster_config, queries);
auto [sim_stats_2, latency_stats_2] = RunClusterSimulationWithQueries(sim_config, cluster_config, queries);
if (latency_stats_1 != latency_stats_2) {
if (latency_stats_1 != latency_stats_2 || sim_stats_1 != sim_stats_2) {
spdlog::error("simulator stats diverged across runs");
spdlog::error("run 1 simulator stats: {}", sim_stats_1);
spdlog::error("run 2 simulator stats: {}", sim_stats_2);
@@ -59,6 +76,8 @@ RC_GTEST_PROP(RandomClusterConfig, HappyPath, (ClusterConfig cluster_config, Non
RC_ASSERT(latency_stats_1 == latency_stats_2);
RC_ASSERT(sim_stats_1 == sim_stats_2);
}
spdlog::trace("passed stats comparison - all good!");
}
} // namespace memgraph::tests::simulation

View File

@@ -20,11 +20,11 @@ namespace memgraph::tests::simulation {
struct ClusterConfig {
int servers;
int replication_factor;
int shards;
int split_threshold;
friend std::ostream &operator<<(std::ostream &in, const ClusterConfig &cluster) {
in << "ClusterConfig { servers: " << cluster.servers << ", replication_factor: " << cluster.replication_factor
<< ", shards: " << cluster.shards << " }";
<< " }";
return in;
}
};
@@ -44,7 +44,7 @@ struct Arbitrary<ClusterConfig> {
gen::set(&ClusterConfig::servers, gen::inRange(kMinimumServers, kMaximumServers)),
gen::set(&ClusterConfig::replication_factor,
gen::inRange(kMinimumReplicationFactor, kMaximumReplicationFactor)),
gen::set(&ClusterConfig::shards, gen::inRange(kMinimumShards, kMaximumShards)));
gen::set(&ClusterConfig::split_threshold, gen::inRange(kMinimumSplitThreshold, kMaximumSplitThreshold)));
}
};

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -130,6 +130,7 @@ class MockedShardRsm {
msgs::DeleteEdgesResponse ApplyImpl(msgs::DeleteEdgesRequest rqst) { return {}; }
msgs::UpdateEdgesResponse ApplyImpl(msgs::UpdateEdgesRequest rqst) { return {}; }
msgs::CommitResponse ApplyImpl(msgs::CommitRequest rqst) { return {}; }
msgs::CommitResponse ApplyImpl(msgs::SplitRequest rqst) { return {}; }
WriteResponses Apply(WriteRequests write_requests) {
return {std::visit([this]<typename T>(T &&request) { return WriteResponses{ApplyImpl(std::forward<T>(request))}; },

View File

@@ -35,13 +35,20 @@ struct CreateVertex {
};
struct ScanAll {
friend std::ostream &operator<<(std::ostream &in, const ScanAll &get) {
friend std::ostream &operator<<(std::ostream &in, const ScanAll & /* unused */) {
in << "ScanAll {}";
return in;
}
};
using OpVariant = std::variant<CreateVertex, ScanAll>;
struct AssertShardsSplit {
friend std::ostream &operator<<(std::ostream &in, const AssertShardsSplit & /* unused */) {
in << "AssertShardsSplit {}";
return in;
}
};
using OpVariant = std::variant<CreateVertex, ScanAll, AssertShardsSplit>;
struct Op {
OpVariant inner;
@@ -81,8 +88,8 @@ using namespace memgraph::tests::simulation;
template <>
struct Arbitrary<CreateVertex> {
static Gen<CreateVertex> arbitrary() {
return gen::build<CreateVertex>(gen::set(&CreateVertex::first, gen::inRange(0, kMaximumShards + 1)),
gen::set(&CreateVertex::second, gen::inRange(0, kMaximumShards + 1)));
return gen::build<CreateVertex>(gen::set(&CreateVertex::first, gen::inRange(0, kMaximumKey)),
gen::set(&CreateVertex::second, gen::inRange(0, kMaximumKey)));
}
};
@@ -91,15 +98,23 @@ struct Arbitrary<ScanAll> {
static Gen<ScanAll> arbitrary() { return gen::just(ScanAll{}); }
};
template <>
struct Arbitrary<AssertShardsSplit> {
static Gen<AssertShardsSplit> arbitrary() { return gen::just(AssertShardsSplit{}); }
};
OpVariant opHoist(ScanAll op) { return op; }
OpVariant opHoist(CreateVertex op) { return op; }
OpVariant opHoist(AssertShardsSplit op) { return op; }
template <>
struct ::rc::Arbitrary<Op> {
static Gen<Op> arbitrary() {
return gen::build<Op>(gen::set(
&Op::inner, gen::oneOf(gen::map(gen::arbitrary<CreateVertex>(), [](CreateVertex op) { return opHoist(op); }),
gen::map(gen::arbitrary<ScanAll>(), [](ScanAll op) { return opHoist(op); }))));
&Op::inner,
gen::oneOf(gen::map(gen::arbitrary<CreateVertex>(), [](CreateVertex op) { return opHoist(op); }),
gen::map(gen::arbitrary<ScanAll>(), [](ScanAll op) { return opHoist(op); }),
gen::map(gen::arbitrary<AssertShardsSplit>(), [](AssertShardsSplit op) { return opHoist(op); }))));
}
};

View File

@@ -0,0 +1,37 @@
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
// This test serves as an example of a property-based model test.
// It generates a cluster configuration and a set of operations to
// apply against both the real system and a greatly simplified model.
#include <chrono>
#include <gtest/gtest.h>
#include <rapidcheck.h>
#include <rapidcheck/gtest.h>
#include <spdlog/cfg/env.h>
#include "generated_operations.hpp"
namespace memgraph::tests::simulation {
void my_code(NonEmptyOpVec input) { RC_ASSERT(input.ops.size() < 3); }
RC_GTEST_PROP(PropertyTestDemo, Demo1, (NonEmptyOpVec ops, uint64_t rng_seed)) {
spdlog::cfg::load_env_levels();
my_code(ops);
spdlog::trace("passed stats comparison - all good!");
}
} // namespace memgraph::tests::simulation

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -33,12 +33,13 @@ using io::Time;
using io::simulator::SimulatorConfig;
using storage::v3::kMaximumCronInterval;
RC_GTEST_PROP(RandomClusterConfig, HappyPath, (ClusterConfig cluster_config, NonEmptyOpVec ops, uint64_t rng_seed)) {
RC_GTEST_PROP(RandomClusterConfig, RandomWorkload,
(ClusterConfig cluster_config, NonEmptyOpVec ops, uint64_t rng_seed)) {
spdlog::cfg::load_env_levels();
SimulatorConfig sim_config{
.drop_percent = 0,
.perform_timeouts = false,
.perform_timeouts = true,
.scramble_messages = true,
.rng_seed = rng_seed,
.start_time = Time::min(),
@@ -49,7 +50,7 @@ RC_GTEST_PROP(RandomClusterConfig, HappyPath, (ClusterConfig cluster_config, Non
auto [sim_stats_1, latency_stats_1] = RunClusterSimulation(sim_config, cluster_config, ops.ops);
auto [sim_stats_2, latency_stats_2] = RunClusterSimulation(sim_config, cluster_config, ops.ops);
if (latency_stats_1 != latency_stats_2) {
if (latency_stats_1 != latency_stats_2 || sim_stats_1 != sim_stats_2) {
spdlog::error("simulator stats diverged across runs");
spdlog::error("run 1 simulator stats: {}", sim_stats_1);
spdlog::error("run 2 simulator stats: {}", sim_stats_2);
@@ -58,6 +59,8 @@ RC_GTEST_PROP(RandomClusterConfig, HappyPath, (ClusterConfig cluster_config, Non
RC_ASSERT(latency_stats_1 == latency_stats_2);
RC_ASSERT(sim_stats_1 == sim_stats_2);
}
spdlog::trace("passed stats comparison - all good!");
}
} // namespace memgraph::tests::simulation

View File

@@ -24,6 +24,7 @@
#include "common/types.hpp"
#include "coordinator/coordinator_client.hpp"
#include "coordinator/coordinator_rsm.hpp"
#include "coordinator/shard_map.hpp"
#include "io/address.hpp"
#include "io/errors.hpp"
#include "io/rsm/raft.hpp"
@@ -39,7 +40,7 @@
#include "utils/result.hpp"
namespace memgraph::query::v2::tests {
using coordinator::AddressAndStatus;
using coordinator::PeerMetadata;
using CompoundKey = coordinator::PrimaryKey;
using coordinator::Coordinator;
using coordinator::CoordinatorClient;
@@ -100,7 +101,8 @@ ShardMap CreateDummyShardmap(coordinator::Address a_io_1, coordinator::Address a
SchemaProperty{.property_id = property_id_2, .type = type_2},
};
auto label_success = sm.InitializeNewLabel(label_name, schema, 1, sm.shard_map_version);
const auto split_threshold = 4;
auto label_success = sm.InitializeNewLabel(label_name, schema, 1, split_threshold, sm.shard_map_version);
MG_ASSERT(label_success);
const LabelId label_id = sm.labels.at(label_name);
@@ -109,11 +111,11 @@ ShardMap CreateDummyShardmap(coordinator::Address a_io_1, coordinator::Address a
shards_for_label.clear();
// add first shard at [0, 0]
AddressAndStatus aas1_1{.address = a_io_1, .status = Status::CONSENSUS_PARTICIPANT};
AddressAndStatus aas1_2{.address = a_io_2, .status = Status::CONSENSUS_PARTICIPANT};
AddressAndStatus aas1_3{.address = a_io_3, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas1_1{.address = a_io_1, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas1_2{.address = a_io_2, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas1_3{.address = a_io_3, .status = Status::CONSENSUS_PARTICIPANT};
ShardMetadata shard1 = ShardMetadata{.peers = {aas1_1, aas1_2, aas1_3}, .version = 1};
ShardMetadata shard1 = ShardMetadata{.peers = {aas1_1, aas1_2, aas1_3}, .version = {1}};
auto key1 = storage::v3::PropertyValue(0);
auto key2 = storage::v3::PropertyValue(0);
@@ -121,11 +123,11 @@ ShardMap CreateDummyShardmap(coordinator::Address a_io_1, coordinator::Address a
shards_for_label[compound_key_1] = shard1;
// add second shard at [12, 13]
AddressAndStatus aas2_1{.address = b_io_1, .status = Status::CONSENSUS_PARTICIPANT};
AddressAndStatus aas2_2{.address = b_io_2, .status = Status::CONSENSUS_PARTICIPANT};
AddressAndStatus aas2_3{.address = b_io_3, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas2_1{.address = b_io_1, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas2_2{.address = b_io_2, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas2_3{.address = b_io_3, .status = Status::CONSENSUS_PARTICIPANT};
ShardMetadata shard2 = ShardMetadata{.peers = {aas2_1, aas2_2, aas2_3}, .version = 1};
ShardMetadata shard2 = ShardMetadata{.peers = {aas2_1, aas2_2, aas2_3}, .version = {1}};
auto key3 = storage::v3::PropertyValue(12);
auto key4 = storage::v3::PropertyValue(13);

View File

@@ -1496,9 +1496,13 @@ int TestMessages() {
std::vector<SchemaProperty> schema_prop = {get_schema_property()};
auto shard_ptr1 = std::make_unique<Shard>(get_primary_label(), min_prim_key, max_prim_key, schema_prop);
auto shard_ptr2 = std::make_unique<Shard>(get_primary_label(), min_prim_key, max_prim_key, schema_prop);
auto shard_ptr3 = std::make_unique<Shard>(get_primary_label(), min_prim_key, max_prim_key, schema_prop);
const coordinator::Hlc shard_version{0};
auto shard_ptr1 =
std::make_unique<Shard>(get_primary_label(), min_prim_key, max_prim_key, schema_prop, shard_version);
auto shard_ptr2 =
std::make_unique<Shard>(get_primary_label(), min_prim_key, max_prim_key, schema_prop, shard_version);
auto shard_ptr3 =
std::make_unique<Shard>(get_primary_label(), min_prim_key, max_prim_key, schema_prop, shard_version);
shard_ptr1->StoreMapping(
{{1, "label"}, {2, "prop1"}, {3, "label1"}, {4, "prop2"}, {5, "prop3"}, {6, "prop4"}, {7, "e_prop"}});
@@ -1511,9 +1515,13 @@ int TestMessages() {
std::vector<Address> address_for_2{shard_server_1_address, shard_server_3_address};
std::vector<Address> address_for_3{shard_server_1_address, shard_server_2_address};
ConcreteShardRsm shard_server1(std::move(shard_server_io_1), address_for_1, ShardRsm(std::move(shard_ptr1)));
ConcreteShardRsm shard_server2(std::move(shard_server_io_2), address_for_2, ShardRsm(std::move(shard_ptr2)));
ConcreteShardRsm shard_server3(std::move(shard_server_io_3), address_for_3, ShardRsm(std::move(shard_ptr3)));
utils::Sender<msgs::InitializeSplitShard> local_shard_manager_sender{[](const auto &elem) {}};
ConcreteShardRsm shard_server1(std::move(shard_server_io_1), address_for_1,
ShardRsm(std::move(shard_ptr1), local_shard_manager_sender));
ConcreteShardRsm shard_server2(std::move(shard_server_io_2), address_for_2,
ShardRsm(std::move(shard_ptr2), local_shard_manager_sender));
ConcreteShardRsm shard_server3(std::move(shard_server_io_3), address_for_3,
ShardRsm(std::move(shard_ptr3), local_shard_manager_sender));
auto server_thread1 = std::jthread([&shard_server1]() { shard_server1.Run(); });
simulator.IncrementServerCountAndWaitForQuiescentState(shard_server_1_address);

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -33,12 +33,12 @@
#include "utils/result.hpp"
using memgraph::common::SchemaType;
using memgraph::coordinator::AddressAndStatus;
using memgraph::coordinator::Coordinator;
using memgraph::coordinator::CoordinatorClient;
using memgraph::coordinator::CoordinatorRsm;
using memgraph::coordinator::HlcRequest;
using memgraph::coordinator::HlcResponse;
using memgraph::coordinator::PeerMetadata;
using memgraph::coordinator::PrimaryKey;
using memgraph::coordinator::ShardMap;
using memgraph::coordinator::ShardMetadata;
@@ -94,9 +94,10 @@ ShardMap CreateDummyShardmap(Address a_io_1, Address a_io_2, Address a_io_3, Add
SchemaProperty{.property_id = property_id_1, .type = type_1},
SchemaProperty{.property_id = property_id_2, .type = type_2},
};
size_t replication_factor = 3;
const size_t replication_factor = 3;
const auto split_threshold = 999;
std::optional<LabelId> label_id_opt =
sm.InitializeNewLabel(label_name, schema, replication_factor, sm.shard_map_version);
sm.InitializeNewLabel(label_name, schema, replication_factor, split_threshold, sm.shard_map_version);
MG_ASSERT(label_id_opt.has_value());
const LabelId label_id = label_id_opt.value();
@@ -105,9 +106,9 @@ ShardMap CreateDummyShardmap(Address a_io_1, Address a_io_2, Address a_io_3, Add
shards_for_label.clear();
// add first shard at [0, 0]
AddressAndStatus aas1_1{.address = a_io_1, .status = Status::CONSENSUS_PARTICIPANT};
AddressAndStatus aas1_2{.address = a_io_2, .status = Status::CONSENSUS_PARTICIPANT};
AddressAndStatus aas1_3{.address = a_io_3, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas1_1{.address = a_io_1, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas1_2{.address = a_io_2, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas1_3{.address = a_io_3, .status = Status::CONSENSUS_PARTICIPANT};
ShardMetadata shard1 = ShardMetadata{.peers = {aas1_1, aas1_2, aas1_3}, .version = 1};
@@ -117,9 +118,9 @@ ShardMap CreateDummyShardmap(Address a_io_1, Address a_io_2, Address a_io_3, Add
shards_for_label.emplace(compound_key_1, shard1);
// add second shard at [12, 13]
AddressAndStatus aas2_1{.address = b_io_1, .status = Status::CONSENSUS_PARTICIPANT};
AddressAndStatus aas2_2{.address = b_io_2, .status = Status::CONSENSUS_PARTICIPANT};
AddressAndStatus aas2_3{.address = b_io_3, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas2_1{.address = b_io_1, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas2_2{.address = b_io_2, .status = Status::CONSENSUS_PARTICIPANT};
PeerMetadata aas2_3{.address = b_io_3, .status = Status::CONSENSUS_PARTICIPANT};
ShardMetadata shard2 = ShardMetadata{.peers = {aas2_1, aas2_2, aas2_3}, .version = 1};

View File

@@ -27,9 +27,18 @@
namespace memgraph::io::simulator {
class SimulatedInterpreter {
using ResultStream = query::v2::DiscardValueResultStream;
class FlatStream {
std::vector<query::v2::TypedValue> all_results_;
public:
void Result(const std::vector<query::v2::TypedValue> &new_values) {
all_results_.insert(all_results_.end(), new_values.begin(), new_values.end());
}
std::vector<query::v2::TypedValue> &&TakeResults() { return std::move(all_results_); }
};
class SimulatedInterpreter {
public:
explicit SimulatedInterpreter(std::unique_ptr<query::v2::InterpreterContext> interpreter_context)
: interpreter_context_(std::move(interpreter_context)) {
@@ -42,23 +51,8 @@ class SimulatedInterpreter {
SimulatedInterpreter &operator=(SimulatedInterpreter &&) = delete;
~SimulatedInterpreter() = default;
void InstallSimulatorTicker(Simulator &simulator) {
interpreter_->InstallSimulatorTicker(simulator.GetSimulatorTickClosure());
}
std::vector<ResultStream> RunQueries(const std::vector<std::string> &queries) {
std::vector<ResultStream> results;
results.reserve(queries.size());
for (const auto &query : queries) {
results.emplace_back(RunQuery(query));
}
return results;
}
private:
ResultStream RunQuery(const std::string &query) {
ResultStream stream;
std::vector<query::v2::TypedValue> RunQuery(const std::string &query) {
FlatStream stream;
std::map<std::string, memgraph::storage::v3::PropertyValue> params;
const std::string *username = nullptr;
@@ -66,9 +60,10 @@ class SimulatedInterpreter {
interpreter_->Prepare(query, params, username);
interpreter_->PullAll(&stream);
return stream;
return stream.TakeResults();
}
private:
std::unique_ptr<query::v2::InterpreterContext> interpreter_context_;
std::unique_ptr<query::v2::Interpreter> interpreter_;
};

View File

@@ -36,6 +36,7 @@
#include "utils/print_helpers.hpp"
#include "utils/variant_helpers.hpp"
#include "generated_operations.hpp"
#include "simulation_interpreter.hpp"
namespace memgraph::tests::simulation {
@@ -71,6 +72,13 @@ using storage::v3::SchemaProperty;
using CompoundKey = std::pair<int, int>;
using ShardClient = RsmClient<SimulatorTransport, WriteRequests, WriteResponses, ReadRequests, ReadResponses>;
struct SimClientContext {
CoordinatorClient<SimulatorTransport> coordinator_client;
const ClusterConfig &cluster_config;
io::simulator::SimulatedInterpreter interpreter;
std::set<CompoundKey> correctness_model;
};
MachineManager<SimulatorTransport> MkMm(Simulator &simulator, std::vector<Address> coordinator_addresses, Address addr,
ShardMap shard_map) {
MachineConfig config{
@@ -116,9 +124,8 @@ void WaitForShardsToInitialize(CoordinatorClient<SimulatorTransport> &coordinato
}
}
ShardMap TestShardMap(int n_splits, int replication_factor) {
ShardMap TestShardMap(const ClusterConfig &cluster_config) {
ShardMap sm{};
const std::string label_name = std::string("test_label");
// register new properties
@@ -135,65 +142,100 @@ ShardMap TestShardMap(int n_splits, int replication_factor) {
SchemaProperty{.property_id = property_id_2, .type = type_2},
};
std::optional<LabelId> label_id = sm.InitializeNewLabel(label_name, schema, replication_factor, sm.shard_map_version);
std::optional<LabelId> label_id = sm.InitializeNewLabel(label_name, schema, cluster_config.replication_factor,
cluster_config.split_threshold, sm.shard_map_version);
RC_ASSERT(label_id.has_value());
// split the shard at N split points
for (int64_t i = 1; i < n_splits; ++i) {
const auto key1 = memgraph::storage::v3::PropertyValue(i);
const auto key2 = memgraph::storage::v3::PropertyValue(0);
const auto split_point = {key1, key2};
const bool split_success = sm.SplitShard(sm.shard_map_version, label_id.value(), split_point);
RC_ASSERT(split_success);
}
return sm;
}
void ExecuteOp(query::v2::RequestRouter<SimulatorTransport> &request_router, std::set<CompoundKey> &correctness_model,
CreateVertex create_vertex) {
void ExecuteOp(SimClientContext &context, CreateVertex create_vertex) {
const auto key1 = memgraph::storage::v3::PropertyValue(create_vertex.first);
const auto key2 = memgraph::storage::v3::PropertyValue(create_vertex.second);
std::vector<msgs::Value> primary_key = {msgs::Value(int64_t(create_vertex.first)),
msgs::Value(int64_t(create_vertex.second))};
if (correctness_model.contains(std::make_pair(create_vertex.first, create_vertex.second))) {
if (context.correctness_model.contains(std::make_pair(create_vertex.first, create_vertex.second))) {
// TODO(tyler) remove this early-return when we have properly handled setting non-unique vertexes
return;
}
auto label_id = request_router.NameToLabel("test_label");
std::string query = fmt::format("CREATE (n:test_label{{property_1: {}, property_2: {}}});", create_vertex.first,
create_vertex.second);
msgs::NewVertex nv{.primary_key = primary_key};
nv.label_ids.push_back({label_id});
auto result_stream = context.interpreter.RunQuery(query);
std::vector<msgs::NewVertex> new_vertices;
new_vertices.push_back(std::move(nv));
// TODO(tyler) is there a better way to assert the actual success of the operation?
RC_ASSERT(result_stream.size() == 0);
auto result = request_router.CreateVertices(std::move(new_vertices));
// RC_ASSERT(!result_stream[0].error.has_value());
RC_ASSERT(result.size() == 1);
RC_ASSERT(!result[0].error.has_value());
correctness_model.emplace(std::make_pair(create_vertex.first, create_vertex.second));
context.correctness_model.emplace(std::make_pair(create_vertex.first, create_vertex.second));
}
void ExecuteOp(query::v2::RequestRouter<SimulatorTransport> &request_router, std::set<CompoundKey> &correctness_model,
ScanAll scan_all) {
auto results = request_router.ScanVertices("test_label");
void ExecuteOp(SimClientContext &context, ScanAll scan_all) {
auto results = context.interpreter.RunQuery("MATCH (n) RETURN n;");
RC_ASSERT(results.size() == correctness_model.size());
auto result_set = std::set<CompoundKey>{};
for (const auto &vertex_accessor : results) {
const auto properties = vertex_accessor.Properties();
const auto primary_key = vertex_accessor.Id().second;
const CompoundKey model_key = std::make_pair(primary_key[0].int_v, primary_key[1].int_v);
RC_ASSERT(correctness_model.contains(model_key));
for (const auto &typed_value : results) {
auto &vertex = typed_value.ValueVertex();
auto &pk = vertex.Properties();
auto &[l1, v1] = pk[0];
auto &[l2, v2] = pk[1];
auto compound_key = std::make_pair(v1.int_v, v2.int_v);
result_set.emplace(compound_key);
}
// TODO(tyler) we are getting more results back than expected, due to
// race conditions during shard splits causing multiple shards to be
// queried by the client. The std::set above papers-over this, but
// it's an issue that needs to be addressed eventually.
RC_ASSERT(result_set == context.correctness_model);
}
void ExecuteOp(SimClientContext &context, AssertShardsSplit assert_shards_split) {
// 1 -> 1
// 2 -> 1
// 3 -> 1
// 4 -> 2
// 5 -> 2
// 6 -> 2
// 7 -> 3
const int max_shard_size = context.cluster_config.split_threshold - 1;
const int min_shards = (std::max((size_t)1, context.correctness_model.size()) - 1) / max_shard_size;
const int minimum_expected_shards = min_shards + 1;
// TODO(tyler) make this a higher number of retries
const int maximum_attempts = 100;
size_t initialized_shards;
for (int i = 0; i < maximum_attempts; i++) {
GetShardMapRequest req{};
CoordinatorReadRequests read_req = req;
auto read_res = context.coordinator_client.SendReadRequest(read_req);
if (read_res.HasError()) {
// timed out
continue;
}
auto response_result = read_res.GetValue();
auto response = std::get<GetShardMapResponse>(response_result);
auto shard_map = response.shard_map;
initialized_shards = shard_map.InitializedShards();
if (initialized_shards >= minimum_expected_shards) {
spdlog::info(
"AssertShardsSplit returning after we see {} initialized shards ({} minimum expected with model size {} and "
"split threshold {})",
initialized_shards, minimum_expected_shards, context.correctness_model.size(),
context.cluster_config.split_threshold);
return;
}
}
spdlog::error("expected {} shards, but we only have {}", minimum_expected_shards, initialized_shards);
MG_ASSERT(false, "exceeded maximum attempts for waiting for the expected number of shard splits to occur");
}
/// This struct exists as a way of detaching
@@ -222,15 +264,17 @@ std::pair<SimulatorStats, LatencyHistogramSummaries> RunClusterSimulation(const
auto machine_1_addr = Address::TestAddress(1);
auto cli_addr = Address::TestAddress(2);
auto cli_addr_2 = Address::TestAddress(3);
auto cli_addr_3 = Address::TestAddress(4);
Io<SimulatorTransport> cli_io = simulator.Register(cli_addr);
Io<SimulatorTransport> cli_io_2 = simulator.Register(cli_addr_2);
Io<SimulatorTransport> cli_io_3 = simulator.Register(cli_addr_3);
auto coordinator_addresses = std::vector{
machine_1_addr,
};
ShardMap initialization_sm = TestShardMap(cluster_config.shards - 1, cluster_config.replication_factor);
ShardMap initialization_sm = TestShardMap(cluster_config);
auto mm_1 = MkMm(simulator, coordinator_addresses, machine_1_addr, initialization_sm);
Address coordinator_address = mm_1.CoordinatorAddress();
@@ -242,19 +286,19 @@ std::pair<SimulatorStats, LatencyHistogramSummaries> RunClusterSimulation(const
// TODO(tyler) clarify addresses of coordinator etc... as it's a mess
CoordinatorClient<SimulatorTransport> coordinator_client(cli_io, coordinator_address, {coordinator_address});
WaitForShardsToInitialize(coordinator_client);
query::v2::RequestRouter<SimulatorTransport> request_router(std::move(coordinator_client), std::move(cli_io));
std::function<bool()> tick_simulator = simulator.GetSimulatorTickClosure();
request_router.InstallSimulatorTicker(tick_simulator);
request_router.StartTransaction();
auto correctness_model = std::set<CompoundKey>{};
SimClientContext context{
.coordinator_client = CoordinatorClient<SimulatorTransport>(cli_io, coordinator_address, {coordinator_address}),
.cluster_config = cluster_config,
.interpreter = io::simulator::SetUpInterpreter(coordinator_address, simulator),
.correctness_model = correctness_model,
};
WaitForShardsToInitialize(context.coordinator_client);
for (const Op &op : ops) {
std::visit([&](auto &o) { ExecuteOp(request_router, correctness_model, o); }, op.inner);
std::visit([&](auto &o) { ExecuteOp(context, o); }, op.inner);
}
// We have now completed our workload without failing any assertions, so we can
@@ -281,65 +325,4 @@ std::pair<SimulatorStats, LatencyHistogramSummaries> RunClusterSimulation(const
return std::make_pair(stats, histo);
}
std::pair<SimulatorStats, LatencyHistogramSummaries> RunClusterSimulationWithQueries(
const SimulatorConfig &sim_config, const ClusterConfig &cluster_config, const std::vector<std::string> &queries) {
spdlog::info("========================== NEW SIMULATION ==========================");
auto simulator = Simulator(sim_config);
auto machine_1_addr = Address::TestAddress(1);
auto cli_addr = Address::TestAddress(2);
auto cli_addr_2 = Address::TestAddress(3);
Io<SimulatorTransport> cli_io = simulator.Register(cli_addr);
Io<SimulatorTransport> cli_io_2 = simulator.Register(cli_addr_2);
auto coordinator_addresses = std::vector{
machine_1_addr,
};
ShardMap initialization_sm = TestShardMap(cluster_config.shards - 1, cluster_config.replication_factor);
auto mm_1 = MkMm(simulator, coordinator_addresses, machine_1_addr, initialization_sm);
Address coordinator_address = mm_1.CoordinatorAddress();
auto mm_thread_1 = std::jthread(RunMachine, std::move(mm_1));
simulator.IncrementServerCountAndWaitForQuiescentState(machine_1_addr);
auto detach_on_error = DetachIfDropped{.handle = mm_thread_1};
// TODO(tyler) clarify addresses of coordinator etc... as it's a mess
CoordinatorClient<SimulatorTransport> coordinator_client(cli_io, coordinator_address, {coordinator_address});
WaitForShardsToInitialize(coordinator_client);
auto simulated_interpreter = io::simulator::SetUpInterpreter(coordinator_address, simulator);
simulated_interpreter.InstallSimulatorTicker(simulator);
auto query_results = simulated_interpreter.RunQueries(queries);
// We have now completed our workload without failing any assertions, so we can
// disable detaching the worker thread, which will cause the mm_thread_1 jthread
// to be joined when this function returns.
detach_on_error.detach = false;
simulator.ShutDown();
mm_thread_1.join();
SimulatorStats stats = simulator.Stats();
spdlog::info("total messages: {}", stats.total_messages);
spdlog::info("dropped messages: {}", stats.dropped_messages);
spdlog::info("timed out requests: {}", stats.timed_out_requests);
spdlog::info("total requests: {}", stats.total_requests);
spdlog::info("total responses: {}", stats.total_responses);
spdlog::info("simulator ticks: {}", stats.simulator_ticks);
auto histo = cli_io_2.ResponseLatencies();
spdlog::info("========================== SUCCESS :) ==========================");
return std::make_pair(stats, histo);
}
} // namespace memgraph::tests::simulation

View File

@@ -13,9 +13,7 @@
namespace memgraph::tests::simulation {
// TODO(tyler) increase this when we start standing up multiple machines in cluster tests
static constexpr auto kMinimumShards = 1;
static constexpr auto kMaximumShards = kMinimumShards + 10;
static constexpr auto kMaximumKey = 5;
// TODO(tyler) increase this when we start standing up multiple machines in cluster tests
static constexpr auto kMinimumServers = 1;
@@ -25,4 +23,7 @@ static constexpr auto kMaximumServers = kMinimumServers + 1;
static constexpr auto kMinimumReplicationFactor = 1;
static constexpr auto kMaximumReplicationFactor = kMinimumReplicationFactor + 1;
static constexpr auto kMinimumSplitThreshold = 4;
static constexpr auto kMaximumSplitThreshold = 5;
} // namespace memgraph::tests::simulation

View File

@@ -294,6 +294,9 @@ target_link_libraries(${test_prefix}storage_v3_expr mg-storage-v3 mg-expr)
add_unit_test(storage_v3_schema.cpp)
target_link_libraries(${test_prefix}storage_v3_schema mg-storage-v3)
add_unit_test(storage_v3_shard_split.cpp)
target_link_libraries(${test_prefix}storage_v3_shard_split mg-storage-v3 mg-query-v2)
# Test mg-query-v2
# These are commented out because of the new TypedValue in the query engine
# add_unit_test(query_v2_interpreter.cpp ${CMAKE_SOURCE_DIR}/src/glue/v2/communication.cpp)

View File

@@ -124,7 +124,7 @@ void WaitForShardsToInitialize(CoordinatorClient<LocalTransport> &coordinator_cl
}
}
ShardMap TestShardMap(int shards, int replication_factor, int gap_between_shards) {
ShardMap TestShardMap(int shards, int replication_factor, int split_threshold) {
ShardMap sm{};
const auto label_name = std::string("test_label");
@@ -143,21 +143,10 @@ ShardMap TestShardMap(int shards, int replication_factor, int gap_between_shards
SchemaProperty{.property_id = property_id_2, .type = type_2},
};
std::optional<LabelId> label_id = sm.InitializeNewLabel(label_name, schema, replication_factor, sm.shard_map_version);
std::optional<LabelId> label_id =
sm.InitializeNewLabel(label_name, schema, replication_factor, split_threshold, sm.shard_map_version);
MG_ASSERT(label_id.has_value());
// split the shard at N split points
for (int64_t i = 1; i < shards; ++i) {
const auto key1 = memgraph::storage::v3::PropertyValue(i * gap_between_shards);
const auto key2 = memgraph::storage::v3::PropertyValue(0);
const auto split_point = {key1, key2};
const bool split_success = sm.SplitShard(sm.shard_map_version, label_id.value(), split_point);
MG_ASSERT(split_success);
}
return sm;
}
@@ -205,15 +194,15 @@ void ExecuteOp(query::v2::RequestRouter<LocalTransport> &request_router, std::se
}
}
void RunWorkload(int shards, int replication_factor, int create_ops, int scan_ops, int shard_worker_threads,
int gap_between_shards) {
void RunWorkload(int shards, int replication_factor, int split_threshold, int create_ops, int scan_ops,
int shard_worker_threads, int gap_between_shards) {
spdlog::info("======================== NEW TEST ========================");
spdlog::info("shards: ", shards);
spdlog::info("replication factor: ", replication_factor);
spdlog::info("create ops: ", create_ops);
spdlog::info("scan all ops: ", scan_ops);
spdlog::info("shard worker threads: ", shard_worker_threads);
spdlog::info("gap between shards: ", gap_between_shards);
spdlog::info("split threshold: ", split_threshold);
LocalSystem local_system;
@@ -228,7 +217,7 @@ void RunWorkload(int shards, int replication_factor, int create_ops, int scan_op
};
auto time_before_shard_map_creation = cli_io_2.Now();
ShardMap initialization_sm = TestShardMap(shards, replication_factor, gap_between_shards);
ShardMap initialization_sm = TestShardMap(shards, replication_factor, split_threshold);
auto time_after_shard_map_creation = cli_io_2.Now();
auto mm_1 = MkMm(local_system, coordinator_addresses, machine_1_addr, initialization_sm, shard_worker_threads);
@@ -281,19 +270,20 @@ void RunWorkload(int shards, int replication_factor, int create_ops, int scan_op
}
TEST(MachineManager, ManyShards) {
auto shards_attempts = {1, 64};
auto shard_worker_thread_attempts = {1, 32};
auto replication_factor = 1;
auto create_ops = 128;
auto scan_ops = 1;
const auto shards_attempts = {1, 64};
const auto shard_worker_thread_attempts = {1, 32};
const auto replication_factor = 1;
const auto create_ops = 128;
const auto scan_ops = 1;
const auto shards = 1;
const auto split_threshold = create_ops / shards;
std::cout << "splits threads scan_all_microseconds\n";
for (const auto shards : shards_attempts) {
auto gap_between_shards = create_ops / shards;
for (const auto shard_worker_threads : shard_worker_thread_attempts) {
RunWorkload(shards, replication_factor, create_ops, scan_ops, shard_worker_threads, gap_between_shards);
RunWorkload(shards, replication_factor, split_threshold, create_ops, scan_ops, shard_worker_threads,
split_threshold);
}
}
}

View File

@@ -82,29 +82,13 @@ ShardMap TestShardMap() {
};
const size_t replication_factor = 1;
const size_t split_threshold = 4;
const auto label_id = sm.InitializeNewLabel(kLabelName, schema, replication_factor, sm.shard_map_version);
const auto label_id =
sm.InitializeNewLabel(kLabelName, schema, replication_factor, split_threshold, sm.shard_map_version);
EXPECT_TRUE(label_id.has_value());
sm.AllocateEdgeTypeIds(std::vector<std::string>{"edge_type"});
// split the shard at N split points
// NB: this is the logic that should be provided by the "split file"
// TODO(tyler) split points should account for signedness
const size_t n_splits = 16;
const auto split_interval = std::numeric_limits<int64_t>::max() / n_splits;
for (int64_t i = 0; i < n_splits; ++i) {
const int64_t value = i * split_interval;
const auto key1 = memgraph::storage::v3::PropertyValue(value);
const auto key2 = memgraph::storage::v3::PropertyValue(0);
const CompoundKey split_point = {key1, key2};
const auto split_success = sm.SplitShard(sm.shard_map_version, label_id.value(), split_point);
EXPECT_TRUE(split_success);
}
return sm;
}

View File

@@ -188,7 +188,8 @@ ShardMap CreateDummyShardmap() {
SchemaProperty{.property_id = property_id_7, .type = type_1},
};
auto label_success = sm.InitializeNewLabel(label_name, schema, 1, sm.shard_map_version);
const auto split_threshold = 4;
auto label_success = sm.InitializeNewLabel(label_name, schema, 1, split_threshold, sm.shard_map_version);
MG_ASSERT(label_success);
const LabelId label_id = sm.labels.at(label_name);

View File

@@ -78,9 +78,9 @@ class StorageV3 : public ::testing::TestWithParam<bool> {
const PropertyId primary_property{PropertyId::FromUint(2)};
std::vector<storage::v3::SchemaProperty> schema_property_vector = {
storage::v3::SchemaProperty{primary_property, common::SchemaType::INT}};
Shard store{primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector,
Config{.gc = {.reclamation_interval = reclamation_interval}}};
coordinator::Hlc last_hlc{0, io::Time{}};
Shard store{primary_label, min_pk, std::nullopt /*max_primary_key*/,
schema_property_vector, last_hlc, Config{.gc = {.reclamation_interval = reclamation_interval}}};
};
INSTANTIATE_TEST_SUITE_P(WithGc, StorageV3, ::testing::Values(true));
INSTANTIATE_TEST_SUITE_P(WithoutGc, StorageV3, ::testing::Values(false));
@@ -2650,7 +2650,7 @@ TEST_P(StorageV3, TestCreateVertexAndValidate) {
(std::map<PropertyId, PropertyValue>{{prop1, PropertyValue(111)}}));
}
{
Shard store(primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector);
Shard store(primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector, last_hlc);
auto acc = store.Access(GetNextHlc());
auto vertex1 = acc.CreateVertexAndValidate({}, {PropertyValue{0}}, {});
auto vertex2 = acc.CreateVertexAndValidate({}, {PropertyValue{0}}, {});

View File

@@ -54,10 +54,9 @@ class StorageEdgeTest : public ::testing::TestWithParam<bool> {
const PropertyId primary_property{PropertyId::FromUint(2)};
std::vector<storage::v3::SchemaProperty> schema_property_vector = {
storage::v3::SchemaProperty{primary_property, common::SchemaType::INT}};
Shard store{primary_label, min_pk, max_pk, schema_property_vector,
Config{.items = {.properties_on_edges = GetParam()}}};
coordinator::Hlc last_hlc{0, io::Time{}};
Shard store{primary_label, min_pk, max_pk,
schema_property_vector, last_hlc, Config{.items = {.properties_on_edges = GetParam()}}};
};
INSTANTIATE_TEST_SUITE_P(EdgesWithProperties, StorageEdgeTest, ::testing::Values(true));

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -136,7 +136,8 @@ class ExpressionEvaluatorTest : public ::testing::Test {
std::vector<storage::v3::SchemaProperty> schema_property_vector = {
storage::v3::SchemaProperty{primary_property, common::SchemaType::INT}};
Shard db{primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector};
coordinator::Hlc last_hlc{0, io::Time{}};
Shard db{primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector, last_hlc};
Shard::Accessor storage_dba{db.Access(GetNextHlc())};
DbAccessor dba{&storage_dba};
@@ -149,8 +150,6 @@ class ExpressionEvaluatorTest : public ::testing::Test {
Frame frame{128};
ExpressionEvaluator eval{&frame, symbol_table, ctx, &dba, View::OLD};
coordinator::Hlc last_hlc{0, io::Time{}};
void SetUp() override { db.StoreMapping({{1, "label"}, {2, "property"}}); }
std::vector<PropertyId> NamesToProperties(const std::vector<std::string> &property_names) {

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -44,7 +44,8 @@ class IndexTest : public testing::Test {
storage::v3::SchemaProperty{primary_property, common::SchemaType::INT}};
const std::vector<PropertyValue> min_pk{PropertyValue{0}};
const LabelId primary_label{LabelId::FromUint(1)};
Shard storage{primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector};
coordinator::Hlc last_hlc{0, io::Time{}};
Shard storage{primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector, last_hlc};
const PropertyId prop_id{PropertyId::FromUint(5)};
const PropertyId prop_val{PropertyId::FromUint(6)};
@@ -55,7 +56,6 @@ class IndexTest : public testing::Test {
static constexpr io::Duration one_time_unit{1};
int primary_key_id{0};
int vertex_id{0};
coordinator::Hlc last_hlc{0, io::Time{}};
LabelId NameToLabelId(std::string_view label_name) { return storage.NameToLabel(label_name); }

View File

@@ -82,8 +82,9 @@ TEST_P(StorageIsolationLevelTest, Visibility) {
for (auto override_isolation_level_index{0U}; override_isolation_level_index < isolation_levels.size();
++override_isolation_level_index) {
Shard store{primary_label, min_pk, max_pk, schema_property_vector,
Config{.transaction = {.isolation_level = default_isolation_level}}};
coordinator::Hlc last_hlc{0, io::Time{}};
Shard store{primary_label, min_pk, max_pk,
schema_property_vector, last_hlc, Config{.transaction = {.isolation_level = default_isolation_level}}};
const auto override_isolation_level = isolation_levels[override_isolation_level_index];
auto creator = store.Access(GetNextHlc());
auto default_isolation_level_reader = store.Access(GetNextHlc());

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -18,7 +18,6 @@
#include <memory>
#include <optional>
#include <string>
#include <variant>
#include <vector>
#include "common/types.hpp"
@@ -58,15 +57,20 @@ class ShardRSMTest : public testing::Test {
PropertyValue max_pk(static_cast<int64_t>(10000000));
std::vector<PropertyValue> max_prim_key = {max_pk};
auto shard_ptr1 = std::make_unique<Shard>(primary_label, min_prim_key, max_prim_key, std::vector{schema_prop});
coordinator::Hlc shard_version{GetTransactionId()};
auto shard_ptr1 =
std::make_unique<Shard>(primary_label, min_prim_key, max_prim_key, std::vector{schema_prop}, shard_version);
shard_ptr1->StoreMapping({{1, "primary_label"},
{2, "primary_label2"},
{3, "label"},
{4, "primary_prop1"},
{5, "primary_prop2"},
{6, "prop"}});
coordinator::Address local_shard_manager_address = coordinator::Address();
utils::Sender<msgs::InitializeSplitShard> local_shard_manager_sender{[](const auto &elem) {}};
shard_ptr1->CreateSchema(primary_label2, {{primary_property2, SchemaType::INT}});
shard_rsm = std::make_unique<ShardRsm>(std::move(shard_ptr1));
shard_rsm = std::make_unique<ShardRsm>(std::move(shard_ptr1), std::move(local_shard_manager_sender));
}
LabelId NameToLabel(const std::string &name) { return LabelId::FromUint(id_mapper_.NameToId(name)); }

View File

@@ -0,0 +1,500 @@
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
// License, and you may not use this file except in compliance with the Business Source License.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
#include <cstdint>
#include <memory>
#include <gmock/gmock-matchers.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "coordinator/hybrid_logical_clock.hpp"
#include "query/v2/requests.hpp"
#include "storage/v3/delta.hpp"
#include "storage/v3/id_types.hpp"
#include "storage/v3/key_store.hpp"
#include "storage/v3/mvcc.hpp"
#include "storage/v3/property_value.hpp"
#include "storage/v3/shard.hpp"
#include "storage/v3/vertex.hpp"
#include "storage/v3/vertex_id.hpp"
using testing::Pair;
using testing::UnorderedElementsAre;
namespace memgraph::storage::v3::tests {
class ShardSplitTest : public testing::Test {
protected:
void SetUp() override {
storage.StoreMapping(
{{1, "label"}, {2, "property"}, {3, "edge_property"}, {4, "secondary_label"}, {5, "secondary_prop"}});
}
const PropertyId primary_property{PropertyId::FromUint(2)};
const PropertyId secondary_property{PropertyId::FromUint(5)};
std::vector<storage::v3::SchemaProperty> schema_property_vector = {
storage::v3::SchemaProperty{primary_property, common::SchemaType::INT}};
const std::vector<PropertyValue> min_pk{PropertyValue{0}};
const LabelId primary_label{LabelId::FromUint(1)};
const LabelId secondary_label{LabelId::FromUint(4)};
const EdgeTypeId edge_type_id{EdgeTypeId::FromUint(3)};
coordinator::Hlc last_hlc{0, io::Time{}};
Shard storage{primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector, last_hlc};
coordinator::Hlc GetNextHlc() {
++last_hlc.logical_id;
last_hlc.coordinator_wall_clock += std::chrono::seconds(1);
return last_hlc;
}
void AssertShardState(auto &shard, const int split_min, const int split_max) {
auto acc = shard.Access(GetNextHlc());
for (int i{0}; i < split_min; ++i) {
EXPECT_FALSE(acc.FindVertex(PrimaryKey{{PropertyValue(i)}}, View::OLD).has_value());
}
for (int i{split_min}; i < split_max; ++i) {
const auto vtx = acc.FindVertex(PrimaryKey{{PropertyValue(i)}}, View::OLD);
ASSERT_TRUE(vtx.has_value());
EXPECT_TRUE(vtx->InEdges(View::OLD)->size() == 1 || vtx->OutEdges(View::OLD)->size() == 1);
}
}
};
void AssertEqVertexContainer(const VertexContainer &actual, const VertexContainer &expected) {
ASSERT_EQ(actual.size(), expected.size());
auto expected_it = expected.begin();
auto actual_it = actual.begin();
while (expected_it != expected.end()) {
EXPECT_EQ(actual_it->first, expected_it->first);
EXPECT_EQ(actual_it->second.deleted, expected_it->second.deleted);
EXPECT_EQ(actual_it->second.labels, expected_it->second.labels);
auto *expected_delta = expected_it->second.delta;
auto *actual_delta = actual_it->second.delta;
// This asserts delta chain
while (expected_delta != nullptr) {
EXPECT_EQ(actual_delta->action, expected_delta->action);
EXPECT_EQ(actual_delta->id, expected_delta->id);
switch (expected_delta->action) {
case Delta::Action::ADD_LABEL:
case Delta::Action::REMOVE_LABEL: {
EXPECT_EQ(actual_delta->label, expected_delta->label);
break;
}
case Delta::Action::SET_PROPERTY: {
EXPECT_EQ(actual_delta->property.key, expected_delta->property.key);
EXPECT_EQ(actual_delta->property.value, expected_delta->property.value);
break;
}
case Delta::Action::ADD_IN_EDGE:
case Delta::Action::ADD_OUT_EDGE:
case Delta::Action::REMOVE_IN_EDGE:
case Delta::Action::RECREATE_OBJECT:
case Delta::Action::DELETE_OBJECT:
case Delta::Action::REMOVE_OUT_EDGE: {
break;
}
}
const auto expected_prev = expected_delta->prev.Get();
const auto actual_prev = actual_delta->prev.Get();
switch (expected_prev.type) {
case PreviousPtr::Type::NULLPTR: {
ASSERT_EQ(actual_prev.type, PreviousPtr::Type::NULLPTR) << "Expected type is nullptr!";
break;
}
case PreviousPtr::Type::DELTA: {
ASSERT_EQ(actual_prev.type, PreviousPtr::Type::DELTA) << "Expected type is delta!";
EXPECT_EQ(actual_prev.delta->action, expected_prev.delta->action);
EXPECT_EQ(actual_prev.delta->id, expected_prev.delta->id);
break;
}
case v3::PreviousPtr::Type::EDGE: {
ASSERT_EQ(actual_prev.type, PreviousPtr::Type::EDGE) << "Expected type is edge!";
EXPECT_EQ(actual_prev.edge->gid, expected_prev.edge->gid);
break;
}
case v3::PreviousPtr::Type::VERTEX: {
ASSERT_EQ(actual_prev.type, PreviousPtr::Type::VERTEX) << "Expected type is vertex!";
EXPECT_EQ(actual_prev.vertex->first, expected_prev.vertex->first);
break;
}
}
expected_delta = expected_delta->next;
actual_delta = actual_delta->next;
}
EXPECT_EQ(expected_delta, nullptr);
EXPECT_EQ(actual_delta, nullptr);
++expected_it;
++actual_it;
}
}
void AssertEqDeltaLists(const std::list<Delta> &actual, const std::list<Delta> &expected) {
EXPECT_EQ(actual.size(), expected.size());
auto actual_it = actual.begin();
auto expected_it = expected.begin();
while (actual_it != actual.end()) {
EXPECT_EQ(actual_it->id, expected_it->id);
EXPECT_EQ(actual_it->action, expected_it->action);
++actual_it;
++expected_it;
}
}
void AddDeltaToDeltaChain(Vertex *object, Delta *new_delta) {
auto *delta_holder = GetDeltaHolder(object);
new_delta->next = delta_holder->delta;
new_delta->prev.Set(object);
if (delta_holder->delta) {
delta_holder->delta->prev.Set(new_delta);
}
delta_holder->delta = new_delta;
}
TEST_F(ShardSplitTest, TestBasicSplitWithVertices) {
auto acc = storage.Access(GetNextHlc());
EXPECT_FALSE(acc.CreateVertexAndValidate({secondary_label}, {PropertyValue(1)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(2)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(3)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(4)}, {}).HasError());
EXPECT_FALSE(
acc.CreateVertexAndValidate({secondary_label}, {PropertyValue(5)}, {{secondary_property, PropertyValue(121)}})
.HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(6)}, {}).HasError());
auto current_hlc = GetNextHlc();
acc.Commit(current_hlc);
auto splitted_data = *storage.PerformSplit({PropertyValue(4)}, last_hlc, GetNextHlc());
EXPECT_EQ(splitted_data.vertices.size(), 3);
EXPECT_EQ(splitted_data.edges->size(), 0);
EXPECT_EQ(splitted_data.transactions.size(), 1);
EXPECT_EQ(splitted_data.label_indices.size(), 0);
EXPECT_EQ(splitted_data.label_property_indices.size(), 0);
CommitInfo commit_info{.start_or_commit_timestamp = current_hlc};
Delta delta_delete1{Delta::DeleteObjectTag{}, &commit_info, 4, 1};
Delta delta_delete2{Delta::DeleteObjectTag{}, &commit_info, 5, 2};
Delta delta_remove_label{Delta::RemoveLabelTag{}, secondary_label, &commit_info, 7, 4};
Delta delta_set_property{Delta::SetPropertyTag{}, secondary_property, PropertyValue(), &commit_info, 6, 4};
Delta delta_delete3{Delta::DeleteObjectTag{}, &commit_info, 8, 3};
VertexContainer expected_vertices;
auto [it_4, inserted1] = expected_vertices.emplace(PrimaryKey{PropertyValue{4}}, VertexData(&delta_delete1));
delta_delete1.prev.Set(&*it_4);
auto [it_5, inserted2] = expected_vertices.emplace(PrimaryKey{PropertyValue{5}}, VertexData(&delta_delete2));
delta_delete2.prev.Set(&*it_5);
auto [it_6, inserted3] = expected_vertices.emplace(PrimaryKey{PropertyValue{6}}, VertexData(&delta_delete3));
delta_delete3.prev.Set(&*it_6);
it_5->second.labels.push_back(secondary_label);
AddDeltaToDeltaChain(&*it_5, &delta_set_property);
AddDeltaToDeltaChain(&*it_5, &delta_remove_label);
AssertEqVertexContainer(splitted_data.vertices, expected_vertices);
// This is to ensure that the transaction that we have don't point to invalid
// object on the other shard
std::list<Delta> expected_deltas;
expected_deltas.emplace_back(Delta::DeleteObjectTag{}, &commit_info, 4, 1);
expected_deltas.emplace_back(Delta::DeleteObjectTag{}, &commit_info, 5, 2);
expected_deltas.emplace_back(Delta::SetPropertyTag{}, secondary_property, PropertyValue(), &commit_info, 6, 4);
expected_deltas.emplace_back(Delta::RemoveLabelTag{}, secondary_label, &commit_info, 7, 4);
expected_deltas.emplace_back(Delta::DeleteObjectTag{}, &commit_info, 8, 3);
AssertEqDeltaLists(splitted_data.transactions.begin()->second->deltas, expected_deltas);
}
TEST_F(ShardSplitTest, TestBasicSplitVerticesAndEdges) {
auto acc = storage.Access(GetNextHlc());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(1)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(2)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(3)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(4)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(5)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(6)}, {}).HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(1)}},
VertexId{primary_label, PrimaryKey{PropertyValue(5)}}, edge_type_id, Gid::FromUint(1))
.HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(4)}},
VertexId{primary_label, PrimaryKey{PropertyValue(6)}}, edge_type_id, Gid::FromUint(2))
.HasError());
auto current_hlc = GetNextHlc();
acc.Commit(current_hlc);
auto splitted_data = *storage.PerformSplit({PropertyValue(4)}, last_hlc, GetNextHlc());
EXPECT_EQ(splitted_data.vertices.size(), 3);
EXPECT_EQ(splitted_data.edges->size(), 2);
EXPECT_EQ(splitted_data.transactions.size(), 1);
EXPECT_EQ(splitted_data.label_indices.size(), 0);
EXPECT_EQ(splitted_data.label_property_indices.size(), 0);
CommitInfo commit_info{.start_or_commit_timestamp = current_hlc};
Delta delta_delete1{Delta::DeleteObjectTag{}, &commit_info, 12, 1};
Delta delta_delete2{Delta::DeleteObjectTag{}, &commit_info, 13, 1};
Delta delta_delete3{Delta::DeleteObjectTag{}, &commit_info, 14, 1};
Delta delta_add_in_edge1{Delta::RemoveInEdgeTag{},
edge_type_id,
VertexId{primary_label, {PropertyValue(1)}},
EdgeRef{Gid::FromUint(1)},
&commit_info,
17,
1};
Delta delta_add_out_edge2{Delta::RemoveOutEdgeTag{},
edge_type_id,
VertexId{primary_label, {PropertyValue(6)}},
EdgeRef{Gid::FromUint(2)},
&commit_info,
19,
1};
Delta delta_add_in_edge2{Delta::RemoveInEdgeTag{},
edge_type_id,
VertexId{primary_label, {PropertyValue(4)}},
EdgeRef{Gid::FromUint(2)},
&commit_info,
20,
1};
VertexContainer expected_vertices;
auto [vtx4, inserted4] = expected_vertices.emplace(PrimaryKey{PropertyValue{4}}, VertexData(&delta_delete1));
auto [vtx5, inserted5] = expected_vertices.emplace(PrimaryKey{PropertyValue{5}}, VertexData(&delta_delete2));
auto [vtx6, inserted6] = expected_vertices.emplace(PrimaryKey{PropertyValue{6}}, VertexData(&delta_delete3));
AddDeltaToDeltaChain(&*vtx4, &delta_add_out_edge2);
AddDeltaToDeltaChain(&*vtx5, &delta_add_in_edge1);
AddDeltaToDeltaChain(&*vtx6, &delta_add_in_edge2);
AssertEqVertexContainer(splitted_data.vertices, expected_vertices);
}
TEST_F(ShardSplitTest, TestBasicSplitBeforeCommit) {
auto acc = storage.Access(GetNextHlc());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(1)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(2)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(3)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(4)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(5)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(6)}, {}).HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(1)}},
VertexId{primary_label, PrimaryKey{PropertyValue(2)}}, edge_type_id, Gid::FromUint(0))
.HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(1)}},
VertexId{primary_label, PrimaryKey{PropertyValue(5)}}, edge_type_id, Gid::FromUint(1))
.HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(4)}},
VertexId{primary_label, PrimaryKey{PropertyValue(6)}}, edge_type_id, Gid::FromUint(2))
.HasError());
auto splitted_data = *storage.PerformSplit({PropertyValue(4)}, last_hlc, GetNextHlc());
EXPECT_EQ(splitted_data.vertices.size(), 3);
EXPECT_EQ(splitted_data.edges->size(), 2);
EXPECT_EQ(splitted_data.transactions.size(), 1);
EXPECT_EQ(splitted_data.label_indices.size(), 0);
EXPECT_EQ(splitted_data.label_property_indices.size(), 0);
}
TEST_F(ShardSplitTest, TestBasicSplitWithCommitedAndOngoingTransactions) {
{
auto acc = storage.Access(GetNextHlc());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(1)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(2)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(3)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(4)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(5)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(6)}, {}).HasError());
acc.Commit(GetNextHlc());
}
auto acc = storage.Access(GetNextHlc());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(1)}},
VertexId{primary_label, PrimaryKey{PropertyValue(2)}}, edge_type_id, Gid::FromUint(0))
.HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(3)}},
VertexId{primary_label, PrimaryKey{PropertyValue(5)}}, edge_type_id, Gid::FromUint(1))
.HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(4)}},
VertexId{primary_label, PrimaryKey{PropertyValue(6)}}, edge_type_id, Gid::FromUint(2))
.HasError());
auto splitted_data = *storage.PerformSplit({PropertyValue(4)}, last_hlc, GetNextHlc());
EXPECT_EQ(splitted_data.vertices.size(), 3);
EXPECT_EQ(splitted_data.edges->size(), 2);
EXPECT_EQ(splitted_data.transactions.size(), 2);
EXPECT_EQ(splitted_data.label_indices.size(), 0);
EXPECT_EQ(splitted_data.label_property_indices.size(), 0);
}
TEST_F(ShardSplitTest, TestBasicSplitWithLabelIndex) {
auto acc = storage.Access(GetNextHlc());
EXPECT_FALSE(acc.CreateVertexAndValidate({secondary_label}, {PropertyValue(1)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(2)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(3)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(4)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({secondary_label}, {PropertyValue(5)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({secondary_label}, {PropertyValue(6)}, {}).HasError());
acc.Commit(GetNextHlc());
storage.CreateIndex(secondary_label);
auto splitted_data = *storage.PerformSplit({PropertyValue(4)}, last_hlc, GetNextHlc());
EXPECT_EQ(splitted_data.vertices.size(), 3);
EXPECT_EQ(splitted_data.edges->size(), 0);
EXPECT_EQ(splitted_data.transactions.size(), 1);
EXPECT_EQ(splitted_data.label_indices.size(), 1);
EXPECT_EQ(splitted_data.label_property_indices.size(), 0);
}
TEST_F(ShardSplitTest, TestBasicSplitWithLabelPropertyIndex) {
auto acc = storage.Access(GetNextHlc());
EXPECT_FALSE(
acc.CreateVertexAndValidate({secondary_label}, {PropertyValue(1)}, {{secondary_property, PropertyValue(1)}})
.HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(2)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(3)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(4)}, {}).HasError());
EXPECT_FALSE(
acc.CreateVertexAndValidate({secondary_label}, {PropertyValue(5)}, {{secondary_property, PropertyValue(21)}})
.HasError());
EXPECT_FALSE(
acc.CreateVertexAndValidate({secondary_label}, {PropertyValue(6)}, {{secondary_property, PropertyValue(22)}})
.HasError());
acc.Commit(GetNextHlc());
storage.CreateIndex(secondary_label, secondary_property);
auto splitted_data = *storage.PerformSplit({PropertyValue(4)}, last_hlc, GetNextHlc());
EXPECT_EQ(splitted_data.vertices.size(), 3);
EXPECT_EQ(splitted_data.edges->size(), 0);
EXPECT_EQ(splitted_data.transactions.size(), 1);
EXPECT_EQ(splitted_data.label_indices.size(), 0);
EXPECT_EQ(splitted_data.label_property_indices.size(), 1);
}
TEST_F(ShardSplitTest, TestSplittingShardsWithGcDestroyOriginalShard) {
const auto split_value{4};
PrimaryKey splitted_value{{PropertyValue(4)}};
std::unique_ptr<Shard> splitted_shard;
{
Shard storage2{primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector, GetNextHlc()};
auto acc = storage2.Access(GetNextHlc());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(1)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(2)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(3)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(4)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(5)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(6)}, {}).HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(1)}},
VertexId{primary_label, PrimaryKey{PropertyValue(2)}}, edge_type_id, Gid::FromUint(0))
.HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(3)}},
VertexId{primary_label, PrimaryKey{PropertyValue(5)}}, edge_type_id, Gid::FromUint(1))
.HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(4)}},
VertexId{primary_label, PrimaryKey{PropertyValue(6)}}, edge_type_id, Gid::FromUint(2))
.HasError());
acc.Commit(GetNextHlc());
auto splitted_data = storage2.PerformSplit({PropertyValue(split_value)}, last_hlc, GetNextHlc());
EXPECT_EQ(splitted_data->vertices.size(), 3);
EXPECT_EQ(splitted_data->edges->size(), 2);
EXPECT_EQ(splitted_data->transactions.size(), 1);
EXPECT_EQ(splitted_data->label_indices.size(), 0);
EXPECT_EQ(splitted_data->label_property_indices.size(), 0);
// Create a new shard
splitted_shard = Shard::FromSplitData(std::move(*splitted_data));
// Call gc on old shard
storage2.CollectGarbage(GetNextHlc().coordinator_wall_clock);
// Destroy original
}
splitted_shard->CollectGarbage(GetNextHlc().coordinator_wall_clock);
AssertShardState(*splitted_shard, 4, 6);
}
TEST_F(ShardSplitTest, TestSplittingShardsWithGcDestroySplittedShard) {
PrimaryKey splitted_value{{PropertyValue(4)}};
auto acc = storage.Access(GetNextHlc());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(1)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(2)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(3)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(4)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(5)}, {}).HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(6)}, {}).HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(1)}},
VertexId{primary_label, PrimaryKey{PropertyValue(2)}}, edge_type_id, Gid::FromUint(0))
.HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(3)}},
VertexId{primary_label, PrimaryKey{PropertyValue(5)}}, edge_type_id, Gid::FromUint(1))
.HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(4)}},
VertexId{primary_label, PrimaryKey{PropertyValue(6)}}, edge_type_id, Gid::FromUint(2))
.HasError());
acc.Commit(GetNextHlc());
auto splitted_data = storage.PerformSplit({PropertyValue(4)}, last_hlc, GetNextHlc());
EXPECT_EQ(splitted_data->vertices.size(), 3);
EXPECT_EQ(splitted_data->edges->size(), 2);
EXPECT_EQ(splitted_data->transactions.size(), 1);
EXPECT_EQ(splitted_data->label_indices.size(), 0);
EXPECT_EQ(splitted_data->label_property_indices.size(), 0);
{
// Create a new shard
auto splitted_shard = Shard::FromSplitData(std::move(*splitted_data));
// Call gc on new shard
splitted_shard->CollectGarbage(GetNextHlc().coordinator_wall_clock);
// Destroy splitted shard
}
storage.CollectGarbage(GetNextHlc().coordinator_wall_clock);
AssertShardState(storage, 1, 3);
}
TEST_F(ShardSplitTest, TestBigSplit) {
int pk{0};
for (int64_t i{0}; i < 10'000; ++i) {
auto acc = storage.Access(GetNextHlc());
EXPECT_FALSE(
acc.CreateVertexAndValidate({secondary_label}, {PropertyValue(pk++)}, {{secondary_property, PropertyValue(i)}})
.HasError());
EXPECT_FALSE(acc.CreateVertexAndValidate({}, {PropertyValue(pk++)}, {}).HasError());
EXPECT_FALSE(acc.CreateEdge(VertexId{primary_label, PrimaryKey{PropertyValue(pk - 2)}},
VertexId{primary_label, PrimaryKey{PropertyValue(pk - 1)}}, edge_type_id,
Gid::FromUint(pk))
.HasError());
acc.Commit(GetNextHlc());
}
storage.CreateIndex(secondary_label, secondary_property);
const auto split_value = pk / 2;
auto splitted_data = *storage.PerformSplit({PropertyValue(split_value)}, last_hlc, GetNextHlc());
EXPECT_EQ(splitted_data.vertices.size(), 10000);
EXPECT_EQ(splitted_data.edges->size(), 5000);
EXPECT_EQ(splitted_data.transactions.size(), 5000);
EXPECT_EQ(splitted_data.label_indices.size(), 0);
EXPECT_EQ(splitted_data.label_property_indices.size(), 1);
auto shard = Shard::FromSplitData(std::move(splitted_data));
AssertShardState(*shard, split_value, split_value * 2);
}
} // namespace memgraph::storage::v3::tests

View File

@@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// Copyright 2023 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@@ -57,9 +57,8 @@ class StorageV3Accessor : public ::testing::Test {
const PropertyId primary_property{PropertyId::FromUint(2)};
std::vector<storage::v3::SchemaProperty> schema_property_vector = {
storage::v3::SchemaProperty{primary_property, common::SchemaType::INT}};
Shard storage{primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector};
coordinator::Hlc last_hlc{0, io::Time{}};
Shard storage{primary_label, min_pk, std::nullopt /*max_primary_key*/, schema_property_vector, last_hlc};
};
TEST_F(StorageV3Accessor, TestPrimaryLabel) {