Compare commits

..

123 Commits

Author SHA1 Message Date
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
János Benjamin Antal
8b392ecc97 Merge pull request #752 from memgraph/T1226-MG-Implement-scanbyprimarykey-with-multiframe
Implement ScanByPrimaryKey with multiframe
2023-02-06 08:13:43 +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
gvolfing
8e315875f2 Update src/query/v2/plan/operator.cpp
Co-authored-by: János Benjamin Antal <antaljanosbenjamin@users.noreply.github.com>
2023-02-02 07:44:47 +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
gvolfing
bf93b53e7d Fix compile error due to wrong aggregate field name 2023-02-01 12:36:27 +01:00
gvolfing
4be4a86d0a Apply suggestions from code review
Co-authored-by: János Benjamin Antal <antaljanosbenjamin@users.noreply.github.com>
2023-02-01 11:39:48 +01:00
gvolfing
7d63236f87 Set the default pull-mechanism back to single-pull 2023-01-31 17:36:52 +01:00
gvolfing
4f8c241934 Merge branch 'project-pineapples' into T1226-MG-Implement-scanbyprimarykey-with-multiframe 2023-01-31 17:33:12 +01:00
gvolfing
60b71cc2c1 Rework ScanByPrimaryKey operator - multiframe 2023-01-31 17:30:31 +01:00
jbajic
97cab6650b Remove redundant index update 2023-01-31 15:21:39 +01:00
János Benjamin Antal
303362d41c Merge pull request #751 from memgraph/T1229-MG-implement-unwind-with-multiframe
Add implementation of `UNWIND` with `MultiFrame`
2023-01-31 15:06:46 +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
gvolfing
272e710510 Merge branch 'T1229-MG-implement-unwind-with-multiframe' into T1226-MG-Implement-scanbyprimarykey-with-multiframe 2023-01-31 08:58:27 +01:00
gvolfing
0e3229756f Merge branch 'project-pineapples' into T1226-MG-Implement-scanbyprimarykey-with-multiframe 2023-01-31 08:49:22 +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
János Benjamin Antal
e96eed81b1 Merge branch 'project-pineapples' into T1229-MG-implement-unwind-with-multiframe 2023-01-30 15:56:44 +01:00
János Benjamin Antal
7fa7586940 Merge pull request #729 from memgraph/T1216-MG-implement-aggregate
Implement Aggregate with `MultiFrame`
2023-01-30 15:56:30 +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
gvolfing
436e41f71f Init POC of ScanByPrimaryKey multiframe 2023-01-30 13:06:05 +01:00
jbajic
c2ec41cb37 Rename ptr to iterator 2023-01-30 12:39:27 +01: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
János Benjamin Antal
fd047e7303 Unify even more logic 2023-01-27 17:10:21 +01:00
János Benjamin Antal
9214c715e2 Address review comment 2023-01-27 17:05:03 +01:00
János Benjamin Antal
44fc2d01c7 Unify logic between multi and single frame pull 2023-01-27 17:04:40 +01:00
János Benjamin Antal
d36c0cc424 Refactor AggregateCursor::ProcessAll 2023-01-27 16:52:46 +01:00
János Benjamin Antal
2ecf580ae7 Eliminate warnings 2023-01-27 16:50:58 +01:00
János Benjamin Antal
c12a5a9019 Make multi-create queries work 2023-01-27 16:50:16 +01:00
jbajic
ea35f93db2 Convert pk before high check 2023-01-27 14:27:28 +01:00
János Benjamin Antal
883922dba5 Eliminate warning about deprecated macro 2023-01-27 13:11:55 +01:00
jbajic
f6785c0405 Add partial split logic to shard worker and coordinator 2023-01-27 12:38:43 +01:00
János Benjamin Antal
c7591887a8 Merge branch 'project-pineapples' into T1216-MG-implement-aggregate 2023-01-27 09:07:14 +01:00
János Benjamin Antal
33454c7d8e Add implementation 2023-01-27 08:43:10 +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
jeremy
c38a80ccd7 Merge branch 'T1190-MG-Implement-ScanAll-and-ScanAllByLabel-with-MultiFrame_2' into T1216-MG-implement-aggregate 2023-01-03 14:58:48 +01:00
jeremy
9589dd97b6 Impl and correct aggregate 2022-12-30 16:21:41 +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
57 changed files with 2875 additions and 662 deletions

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,206 @@
// 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");
// 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, split_key);
if (shard.pending_split.has_value() || shard.version != suggested_split_info.shard_version) {
spdlog::info("skipping split, already splitting: {}, shard.version: {}, suggested shard_version: {}",
shard.pending_split.has_value(), shard.version, suggested_split_info.shard_version);
continue;
}
// begin the split process for this shard
spdlog::info("Coordinator beginning new split process after receiving a pending split");
// bump current shard version and store pending split info
shard.version = shard_map_.GetHlc();
shard.pending_split = suggested_split_info;
splitting_shards_.insert(splitting_shard_id);
initiated_split = true;
// copy this shard and store it in the ShardMap
ShardMetadata duplicated_shard{shard};
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::info("Coordinator allocating new rsm uuid: {}", new_uuid);
// 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) {
spdlog::info("looking at rsm {}", initialized_rsm);
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);
splitting_shards_.erase(split_shard_id);
rsm_split_from_.erase(initialized_rsm);
MG_ASSERT(false,
"TODO(tyler) remove this. but until it works once, this is removing rsm from split from before "
"actually used");
}
size_t initialized_count = 0;
for (auto &peer : shard.peers) {
if (peer.address.unique_id == initialized_rsm) {
spdlog::info("Coordinator marking rsm {} as initialized", initialized_rsm);
peer.status = Status::CONSENSUS_PARTICIPANT;
}
if (peer.status == Status::CONSENSUS_PARTICIPANT) {
initialized_count++;
}
}
if (initialized_count >= label_space.replication_factor) {
spdlog::info("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);
// 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) {
// 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::info("assigning shard manager to shard");
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
spdlog::info("0");
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 &shard = label_space.shards.at(low_key);
spdlog::info("1");
for (const auto &peer : shard.peers) {
spdlog::info("2");
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::info("not splitting peer: status is PENDING_SPLIT: {}, peer address: {} storage manager address: {}",
peer.status == Status::PENDING_SPLIT, peer.address, heartbeat_request.from_storage_manager);
// not splitting or not us
continue;
}
spdlog::info("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 : shard.peers) {
uuid_mapping.emplace(peer_metadata2.split_from, peer_metadata2.address.unique_id);
}
ret.shards_to_split.push_back(ShardToSplit{
.split_key = low_key,
// .old_shard_version = shard.previous_version,
.new_shard_version = 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 +222,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 +247,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 +272,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};
@@ -159,33 +168,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 +232,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,21 +241,37 @@ 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{};
boost::uuids::uuid ShardMap::NewShardUuid() {
uint64_t shard_id = GetHlc().logical_id;
return Uint64ToUuid(shard_id);
}
Hlc ShardMap::GetHlc() noexcept { return ++shard_map_version; }
HeartbeatResponse ShardMap::AssignShards(Address storage_manager, std::set<boost::uuids::uuid> initialized,
std::set<msgs::SuggestedSplitInfo> pending_splits) {
HeartbeatResponse ret{};
bool mutated = false;
std::map<std::pair<boost::uuids::uuid, Hlc>, msgs::PrimaryKey> mapped_pending_splits;
for (const auto &pending_split : pending_splits) {
MG_ASSERT(pending_split.shard_to_split_uuid != Uint64ToUuid(0),
"a shard split for an impossible UUID is being requested");
spdlog::info("Coordinator adding pending split for shard version: {}, uuid: {} to attempt to initiate",
pending_split.shard_version, pending_split.shard_to_split_uuid);
mapped_pending_splits.insert(
{{pending_split.shard_to_split_uuid, pending_split.shard_version}, pending_split.split_key});
}
for (auto &[label_id, label_space] : label_spaces) {
for (auto it = label_space.shards.begin(); it != label_space.shards.end(); it++) {
@@ -281,63 +281,159 @@ std::vector<ShardToInitialize> ShardMap::AssignShards(Address storage_manager,
high_key = next_it->first;
}
// TODO(tyler) avoid these triple-nested loops by having the heartbeat include better info
bool machine_contains_shard = false;
bool shard_assigned_to_machine = false;
for (auto &aas : shard.peers) {
if (initialized.contains(aas.address.unique_id)) {
machine_contains_shard = true;
if (aas.status != Status::CONSENSUS_PARTICIPANT) {
for (auto &peer_metadata : shard.peers) {
const bool same_machine = peer_metadata.address.last_known_ip == storage_manager.last_known_ip &&
peer_metadata.address.last_known_port == storage_manager.last_known_port;
spdlog::info("Coordinator split debug - shard version: {}", shard.version);
if (initialized.contains(peer_metadata.address.unique_id)) {
spdlog::info("1");
shard_assigned_to_machine = true;
if (!same_machine) {
// set the last known ip and port to the storage manager that has heartbeated it just now
mutated = true;
spdlog::info("marking shard as full consensus participant: {}", aas.address.unique_id);
aas.status = Status::CONSENSUS_PARTICIPANT;
peer_metadata.address.last_known_ip = storage_manager.last_known_ip;
peer_metadata.address.last_known_port = storage_manager.last_known_port;
}
} 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 (peer_metadata.status != Status::CONSENSUS_PARTICIPANT) {
mutated = true;
spdlog::info("marking shard as full consensus participant: {}", peer_metadata.address.unique_id);
peer_metadata.status = Status::CONSENSUS_PARTICIPANT;
}
} else if (same_machine && peer_metadata.status == Status::INITIALIZING) {
spdlog::info("2");
// we are expecting this shard to be initialized (rather than split) on this machine,
// so send it an initialization request
shard_assigned_to_machine = true;
spdlog::info("reminding shard manager that they should begin participating in shard");
ret.shards_to_initialize.push_back(ShardToInitialize{
.new_shard_version = shard.version,
.uuid = peer_metadata.address.unique_id,
.label_id = label_id,
.min_key = low_key,
.max_key = high_key,
.schema = schemas[label_id],
.config = Config{.split =
Config::Split{
.max_shard_vertex_size = label_space.split_threshold,
}},
.id_to_names = IdToNames(),
});
} else if (same_machine && peer_metadata.status == Status::PENDING_SPLIT) {
// we are expecting this shard to be split, so send it a split request
spdlog::info(
"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 : shard.peers) {
uuid_mapping.emplace(peer_metadata2.split_from, peer_metadata2.address.unique_id);
}
MG_ASSERT(false, "adding a shard to split :)");
ret.shards_to_split.push_back(ShardToSplit{
.split_key = low_key,
// .old_shard_version = shard.previous_version,
.new_shard_version = shard.version,
.uuid_mapping = uuid_mapping,
});
} else {
spdlog::info("5");
MG_ASSERT(
!same_machine,
"failed to properly handle a new Status type in the heartbeat management and shard assignment code");
}
if (const auto pending_split = mapped_pending_splits.find({peer_metadata.address.unique_id, shard.version});
pending_split != mapped_pending_splits.end()) {
// Now we handle shard split
if (shard.pending_split) {
spdlog::info("Coordinator received split request while split is happening!");
continue;
}
MG_ASSERT(false, "beginning split from coordinator perspective :)");
spdlog::info("Coordinator beginning new split process after receiving a pending split");
shard.pending_split = msgs::SuggestedSplitInfo{.shard_to_split_uuid = pending_split->first.first,
.split_key = pending_split->second,
.shard_version = pending_split->first.second};
shard.version = GetHlc();
std::map<boost::uuids::uuid, boost::uuids::uuid> split_mapping = {};
ShardMetadata duplicated_shard{shard};
for (auto &peer_metadata3 : duplicated_shard.peers) {
peer_metadata3.status = Status::PENDING_SPLIT;
peer_metadata3.split_from = peer_metadata3.address.unique_id;
const auto new_uuid = NewShardUuid();
// store new uuid for the right side of each shard
split_mapping.emplace(peer_metadata3.address.unique_id, new_uuid);
peer_metadata3.address.unique_id = new_uuid;
}
const auto converted_pk = storage::conversions::ConvertPropertyVector(pending_split->second);
if (high_key) {
MG_ASSERT(converted_pk < *high_key, "Split point is beyond low key of the next shard");
}
label_space.shards.insert({converted_pk, duplicated_shard});
} else {
spdlog::info("Coordinator not attempting to split this shard: there is no pending split for {}:{}",
peer_metadata.address.unique_id, shard.version);
}
}
if (!machine_contains_shard && shard.peers.size() < label_space.replication_factor) {
// increment version for each new uuid for deterministic creation
IncrementShardMapVersion();
if (shard.pending_split.has_value()) {
// if the split shard has any peers that are CONSENSUS_PARTICIPANT, we can clear our
// pending split because even if a single peer has split, it means that the split has
// reached a majority of the replicas.
bool split_applied_anywhere = false;
const auto split_key = storage::conversions::ConvertPropertyVector(shard.pending_split->split_key);
for (const auto &split_peer : label_space.shards.at(split_key).peers) {
split_applied_anywhere |= split_peer.status == Status::CONSENSUS_PARTICIPANT;
}
if (split_applied_anywhere) {
spdlog::info("now that the shard in label space {} is done splitting, we can clear its pending_split data",
label_id);
shard.pending_split.reset();
}
}
if (!shard_assigned_to_machine && shard.peers.size() < label_space.replication_factor) {
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);
address.unique_id = NewShardUuid();
spdlog::info("assigning shard manager to shard");
ret.push_back(ShardToInitialize{
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 = schemas[label_id],
.config = Config{},
.config = Config{.split =
Config::Split{
.max_shard_vertex_size = label_space.split_threshold,
}},
.id_to_names = IdToNames(),
});
AddressAndStatus aas = {
PeerMetadata peer_metadata = {
.address = address,
.status = Status::INITIALIZING,
};
shard.peers.emplace_back(aas);
shard.peers.emplace_back(peer_metadata);
}
}
}
@@ -345,36 +441,15 @@ std::vector<ShardToInitialize> ShardMap::AssignShards(Address storage_manager,
if (mutated) {
IncrementShardMapVersion();
}
return ret;
}
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;
}
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 +468,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);
@@ -561,8 +638,8 @@ bool ShardMap::ClusterInitialized() const {
return false;
}
for (const auto &aas : shard.peers) {
if (aas.status != Status::CONSENSUS_PARTICIPANT) {
for (const auto &peer_metadata : shard.peers) {
if (peer_metadata.status != Status::CONSENSUS_PARTICIPANT) {
spdlog::info("shard member not yet a CONSENSUS_PARTICIPANT");
return false;
}
@@ -573,4 +650,26 @@ 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;
}
for (const auto &peer_metadata : shard.peers) {
if (peer_metadata.status != Status::CONSENSUS_PARTICIPANT) {
continue;
}
}
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,25 @@ struct ShardToInitialize {
std::unordered_map<uint64_t, std::string> id_to_names;
};
struct ShardToSplit {
PrimaryKey split_key;
Hlc old_shard_version;
Hlc new_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 +172,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 +208,19 @@ 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);
HeartbeatResponse AssignShards(Address storage_manager, std::set<boost::uuids::uuid> initialized,
std::set<msgs::SuggestedSplitInfo> pending_splits);
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 +246,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

@@ -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

@@ -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,7 +59,7 @@ 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;
@@ -105,7 +106,7 @@ 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) {
@@ -155,7 +156,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_);
@@ -193,7 +194,7 @@ class SimulatorHandle {
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);
auto type_info = TypeInfoFor(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
@@ -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>
template <utils::Message M>
using ResponseFuture = Future<ResponseResult<M>>;
template <Message 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,22 +57,9 @@ struct RequestEnvelope {
Address from_address;
};
template <Message... Ms>
template <utils::Message... Ms>
using RequestResult = BasicResult<TimedOut, RequestEnvelope<Ms...>>;
/// 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 {
std::function<void(M)> sender_;
public:
Sender(std::function<void(M)> sender) : sender_(sender) {}
void Send(M message) { sender_(message); }
};
template <typename I>
class Io {
I implementation_;
@@ -95,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;
@@ -105,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_;
@@ -115,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_;
@@ -126,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_;
@@ -137,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);
@@ -153,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));
@@ -174,6 +156,7 @@ class Io {
}
Address GetAddress() { return address_; }
void SetAddress(Address address) { address_ = address; }
Io<I> ForkLocal(boost::uuids::uuid uuid) {
@@ -187,15 +170,15 @@ class Io {
LatencyHistogramSummaries ResponseLatencies() { return implementation_.ResponseLatencies(); }
template <Message M>
Sender<M> GetSender(Address address) {
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, message);
std::function<void(M)> sender = [address, io_copy](M &&message) mutable {
io_copy.template Send<M>(address, 0, std::forward<M>(message));
};
return Sender{sender};
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
@@ -127,7 +127,8 @@ 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());
@@ -135,8 +136,8 @@ class MachineManager {
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
@@ -175,7 +176,8 @@ class MachineManager {
spdlog::info("smm: {}", shard_manager_.GetAddress().ToString());
if (to_sm) {
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");

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

@@ -202,7 +202,7 @@ class DistributedCreateNodeCursor : public Cursor {
request_router->CreateVertices(NodeCreationInfoToRequests(context, multi_frame));
}
PlaceNodesOnTheMultiFrame(multi_frame, context);
return false;
return true;
}
void Shutdown() override { input_cursor_->Shutdown(); }
@@ -603,8 +603,6 @@ class DistributedScanByPrimaryKeyCursor : public Cursor {
filter_expressions_(filter_expressions),
primary_key_(primary_key) {}
enum class State : int8_t { INITIALIZING, COMPLETED };
using VertexAccessor = accessors::VertexAccessor;
std::optional<VertexAccessor> MakeRequestSingleFrame(Frame &frame, RequestRouterInterface &request_router,
@@ -637,6 +635,43 @@ class DistributedScanByPrimaryKeyCursor : public Cursor {
return VertexAccessor(vertex, properties, &request_router);
}
void MakeRequestMultiFrame(MultiFrame &multi_frame, RequestRouterInterface &request_router,
ExecutionContext &context) {
msgs::GetPropertiesRequest req;
const msgs::Label label = {.id = msgs::LabelId::FromUint(label_.AsUint())};
std::unordered_set<msgs::VertexId> used_vertex_ids;
for (auto &frame : multi_frame.GetValidFramesModifier()) {
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.request_router,
storage::v3::View::NEW);
std::vector<msgs::Value> pk;
for (auto *primary_property : primary_key_) {
pk.push_back(TypedValueToValue(primary_property->Accept(evaluator)));
}
auto vertex_id = std::make_pair(label, std::move(pk));
auto [it, inserted] = used_vertex_ids.emplace(std::move(vertex_id));
if (inserted) {
req.vertex_ids.emplace_back(*it);
}
}
auto get_prop_result = std::invoke([&context, &request_router, &req]() mutable {
SCOPED_REQUEST_WAIT_PROFILE;
return request_router.GetProperties(req);
});
for (auto &result : get_prop_result) {
// TODO (gvolfing) figure out labels when relevant.
msgs::Vertex vertex = {.id = result.vertex, .labels = {}};
id_to_accessor_mapping_.emplace(result.vertex,
VertexAccessor(std::move(vertex), std::move(result.props), &request_router));
}
}
bool Pull(Frame &frame, ExecutionContext &context) override {
SCOPED_PROFILE_OP(op_name_);
@@ -655,17 +690,108 @@ class DistributedScanByPrimaryKeyCursor : public Cursor {
return false;
}
void EnsureOwnMultiFrameIsGood(MultiFrame &output_multi_frame) {
if (!own_multi_frame_.has_value()) {
own_multi_frame_.emplace(MultiFrame(output_multi_frame.GetFirstFrame().elems().size(),
kNumberOfFramesInMultiframe, output_multi_frame.GetMemoryResource()));
own_frames_consumer_.emplace(own_multi_frame_->GetValidFramesConsumer());
own_frames_it_ = own_frames_consumer_->begin();
}
MG_ASSERT(output_multi_frame.GetFirstFrame().elems().size() == own_multi_frame_->GetFirstFrame().elems().size());
}
bool PullMultiple(MultiFrame &output_multi_frame, ExecutionContext &context) override {
SCOPED_PROFILE_OP(op_name_);
EnsureOwnMultiFrameIsGood(output_multi_frame);
auto output_frames_populator = output_multi_frame.GetInvalidFramesPopulator();
auto populated_any = false;
while (true) {
switch (state_) {
case State::PullInput: {
id_to_accessor_mapping_.clear();
if (!input_cursor_->PullMultiple(*own_multi_frame_, context)) {
state_ = State::Exhausted;
return populated_any;
}
own_frames_consumer_.emplace(own_multi_frame_->GetValidFramesConsumer());
own_frames_it_ = own_frames_consumer_->begin();
if (own_frames_it_ == own_frames_consumer_->end()) {
continue;
}
MakeRequestMultiFrame(*own_multi_frame_, *context.request_router, context);
state_ = State::PopulateOutput;
break;
}
case State::PopulateOutput: {
if (!output_multi_frame.HasInvalidFrame()) {
if (own_frames_it_ == own_frames_consumer_->end()) {
id_to_accessor_mapping_.clear();
}
return populated_any;
}
if (own_frames_it_ == own_frames_consumer_->end()) {
state_ = State::PullInput;
continue;
}
for (auto output_frame_it = output_frames_populator.begin();
output_frame_it != output_frames_populator.end() && own_frames_it_ != own_frames_consumer_->end();
++own_frames_it_) {
auto &output_frame = *output_frame_it;
ExpressionEvaluator evaluator(&*own_frames_it_, context.symbol_table, context.evaluation_context,
context.request_router, storage::v3::View::NEW);
std::vector<msgs::Value> pk;
for (auto *primary_property : primary_key_) {
pk.push_back(TypedValueToValue(primary_property->Accept(evaluator)));
}
const msgs::Label label = {.id = msgs::LabelId::FromUint(label_.AsUint())};
auto vertex_id = std::make_pair(label, std::move(pk));
if (const auto it = id_to_accessor_mapping_.find(vertex_id); it != id_to_accessor_mapping_.end()) {
output_frame = *own_frames_it_;
output_frame[output_symbol_] = TypedValue(it->second);
populated_any = true;
++output_frame_it;
}
own_frames_it_->MakeInvalid();
}
break;
}
case State::Exhausted: {
return populated_any;
}
}
}
return populated_any;
};
void Reset() override { input_cursor_->Reset(); }
void Shutdown() override { input_cursor_->Shutdown(); }
private:
enum class State { PullInput, PopulateOutput, Exhausted };
State state_{State::PullInput};
const Symbol output_symbol_;
const UniqueCursorPtr input_cursor_;
const char *op_name_;
storage::v3::LabelId label_;
std::optional<std::vector<Expression *>> filter_expressions_;
std::vector<Expression *> primary_key_;
std::optional<MultiFrame> own_multi_frame_;
std::optional<ValidFramesConsumer> own_frames_consumer_;
ValidFramesConsumer::Iterator own_frames_it_;
std::unordered_map<msgs::VertexId, VertexAccessor> id_to_accessor_mapping_;
};
ScanAll::ScanAll(const std::shared_ptr<LogicalOperator> &input, Symbol output_symbol, storage::v3::View view)
@@ -1320,6 +1446,55 @@ class AggregateCursor : public Cursor {
auto remember_values_it = aggregation_it_->second.remember_.begin();
for (const Symbol &remember_sym : self_.remember_) frame[remember_sym] = *remember_values_it++;
++aggregation_it_;
return true;
}
bool PullMultiple(MultiFrame &multi_frame, ExecutionContext &context) override {
SCOPED_PROFILE_OP("AggregateMF");
if (!pulled_all_input_) {
ProcessAll(multi_frame, &context);
pulled_all_input_ = true;
MG_ASSERT(!multi_frame.HasValidFrame(), "ProcessAll didn't consumed all input frames!");
aggregation_it_ = aggregation_.begin();
// in case there is no input and no group_bys we need to return true
// just this once
if (aggregation_.empty() && self_.group_by_.empty()) {
auto frame = multi_frame.GetFirstFrame();
frame.MakeValid();
auto *pull_memory = context.evaluation_context.memory;
// place default aggregation values on the frame
for (const auto &elem : self_.aggregations_) {
frame[elem.output_sym] = DefaultAggregationOpValue(elem, pull_memory);
}
// place null as remember values on the frame
for (const Symbol &remember_sym : self_.remember_) {
frame[remember_sym] = TypedValue(pull_memory);
}
return true;
}
}
if (aggregation_it_ == aggregation_.end()) {
return false;
}
// place aggregation values on the frame
auto &frame = multi_frame.GetFirstFrame();
frame.MakeValid();
auto aggregation_values_it = aggregation_it_->second.values_.begin();
for (const auto &aggregation_elem : self_.aggregations_) {
frame[aggregation_elem.output_sym] = *aggregation_values_it++;
}
// place remember values on the frame
auto remember_values_it = aggregation_it_->second.remember_.begin();
for (const Symbol &remember_sym : self_.remember_) {
frame[remember_sym] = *remember_values_it++;
}
aggregation_it_++;
return true;
}
@@ -1388,18 +1563,22 @@ class AggregateCursor : public Cursor {
ProcessOne(*frame, &evaluator);
}
// calculate AVG aggregations (so far they have only been summed)
for (size_t pos = 0; pos < self_.aggregations_.size(); ++pos) {
if (self_.aggregations_[pos].op != Aggregation::Op::AVG) continue;
for (auto &kv : aggregation_) {
AggregationValue &agg_value = kv.second;
auto count = agg_value.counts_[pos];
auto *pull_memory = context->evaluation_context.memory;
if (count > 0) {
agg_value.values_[pos] = agg_value.values_[pos] / TypedValue(static_cast<double>(count), pull_memory);
}
CalculateAverages(*context);
}
void ProcessAll(MultiFrame &multi_frame, ExecutionContext *context) {
while (input_cursor_->PullMultiple(multi_frame, *context)) {
auto valid_frames_modifier =
multi_frame.GetValidFramesConsumer(); // consumer is needed i.o. reader because of the evaluator
for (auto &frame : valid_frames_modifier) {
ExpressionEvaluator evaluator(&frame, context->symbol_table, context->evaluation_context,
context->request_router, storage::v3::View::NEW);
ProcessOne(frame, &evaluator);
}
}
CalculateAverages(*context);
}
/**
@@ -1417,6 +1596,20 @@ class AggregateCursor : public Cursor {
Update(evaluator, &agg_value);
}
void CalculateAverages(ExecutionContext &context) {
for (size_t pos = 0; pos < self_.aggregations_.size(); ++pos) {
if (self_.aggregations_[pos].op != Aggregation::Op::AVG) continue;
for (auto &kv : aggregation_) {
AggregationValue &agg_value = kv.second;
auto count = agg_value.counts_[pos];
auto *pull_memory = context.evaluation_context.memory;
if (count > 0) {
agg_value.values_[pos] = agg_value.values_[pos] / TypedValue(static_cast<double>(count), pull_memory);
}
}
}
}
/** Ensures the new AggregationValue has been initialized. This means
* that the value vectors are filled with an appropriate number of Nulls,
* counts are set to 0 and remember values are remembered.
@@ -1450,7 +1643,7 @@ class AggregateCursor : public Cursor {
for (; count_it < agg_value->counts_.end(); count_it++, value_it++, agg_elem_it++) {
// COUNT(*) is the only case where input expression is optional
// handle it here
auto input_expr_ptr = agg_elem_it->value;
auto *input_expr_ptr = agg_elem_it->value;
if (!input_expr_ptr) {
*count_it += 1;
*value_it = *count_it;
@@ -1541,7 +1734,7 @@ class AggregateCursor : public Cursor {
/** Checks if the given TypedValue is legal in MIN and MAX. If not
* an appropriate exception is thrown. */
void EnsureOkForMinMax(const TypedValue &value) const {
static void EnsureOkForMinMax(const TypedValue &value) {
switch (value.type()) {
case TypedValue::Type::Bool:
case TypedValue::Type::Int:
@@ -1557,7 +1750,7 @@ class AggregateCursor : public Cursor {
/** Checks if the given TypedValue is legal in AVG and SUM. If not
* an appropriate exception is thrown. */
void EnsureOkForAvgSum(const TypedValue &value) const {
static void EnsureOkForAvgSum(const TypedValue &value) {
switch (value.type()) {
case TypedValue::Type::Int:
case TypedValue::Type::Double:
@@ -1972,14 +2165,7 @@ class UnwindCursor : public Cursor {
if (!input_cursor_->Pull(frame, context)) return false;
// successful pull from input, initialize value and iterator
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.request_router,
storage::v3::View::OLD);
TypedValue input_value = self_.input_expression_->Accept(evaluator);
if (input_value.type() != TypedValue::Type::List)
throw QueryRuntimeException("Argument of UNWIND must be a list, but '{}' was provided.", input_value.type());
// Copy the evaluted input_value_list to our vector.
input_value_ = input_value.ValueList();
input_value_it_ = input_value_.begin();
SetInputValue(frame, context);
}
// if we reached the end of our list of values goto back to top
@@ -1990,6 +2176,70 @@ class UnwindCursor : public Cursor {
}
}
bool PullMultiple(MultiFrame &output_multi_frame, ExecutionContext &context) override {
SCOPED_PROFILE_OP("UnwindMF");
if (!own_multi_frame_.has_value()) {
own_multi_frame_.emplace(MultiFrame(output_multi_frame.GetFirstFrame().elems().size(),
kNumberOfFramesInMultiframe, output_multi_frame.GetMemoryResource()));
own_frames_consumer_.emplace(own_multi_frame_->GetValidFramesConsumer());
own_frames_it_ = own_frames_consumer_->begin();
}
auto output_frames_populator = output_multi_frame.GetInvalidFramesPopulator();
auto populated_any = false;
while (true) {
switch (state_) {
case State::PullInput: {
if (!input_cursor_->PullMultiple(*own_multi_frame_, context)) {
state_ = State::Exhausted;
return populated_any;
}
own_frames_consumer_.emplace(own_multi_frame_->GetValidFramesConsumer());
own_frames_it_ = own_frames_consumer_->begin();
state_ = State::InitializeInputValue;
break;
}
case State::InitializeInputValue: {
if (own_frames_it_ == own_frames_consumer_->end()) {
state_ = State::PullInput;
continue;
}
SetInputValue(*own_frames_it_, context);
state_ = State::PopulateOutput;
break;
}
case State::PopulateOutput: {
if (!output_multi_frame.HasInvalidFrame()) {
return populated_any;
}
if (input_value_it_ == input_value_.end()) {
own_frames_it_->MakeInvalid();
++own_frames_it_;
state_ = State::InitializeInputValue;
continue;
}
for (auto output_frame_it = output_frames_populator.begin();
output_frame_it != output_frames_populator.end() && input_value_it_ != input_value_.end();
++output_frame_it) {
auto &output_frame = *output_frame_it;
output_frame = *own_frames_it_;
output_frame[self_.output_symbol_] = std::move(*input_value_it_);
input_value_it_++;
populated_any = true;
}
break;
}
case State::Exhausted: {
return populated_any;
}
}
}
return populated_any;
}
void Shutdown() override { input_cursor_->Shutdown(); }
void Reset() override {
@@ -1998,13 +2248,36 @@ class UnwindCursor : public Cursor {
input_value_it_ = input_value_.end();
}
void SetInputValue(Frame &frame, ExecutionContext &context) {
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.request_router,
storage::v3::View::OLD);
TypedValue input_value = self_.input_expression_->Accept(evaluator);
if (input_value.type() != TypedValue::Type::List) {
throw QueryRuntimeException("Argument of UNWIND must be a list, but '{}' was provided.", input_value.type());
}
// It would be nice if we could move it, however it can be tricky to make it work because of allocators and
// different memory resources, be careful.
input_value_ = std::move(input_value.ValueList());
input_value_it_ = input_value_.begin();
}
private:
using InputVector = utils::pmr::vector<TypedValue>;
using InputIterator = InputVector::iterator;
const Unwind &self_;
const UniqueCursorPtr input_cursor_;
// typed values we are unwinding and yielding
utils::pmr::vector<TypedValue> input_value_;
InputVector input_value_;
// current position in input_value_
decltype(input_value_)::iterator input_value_it_ = input_value_.end();
InputIterator input_value_it_ = input_value_.end();
enum class State { PullInput, InitializeInputValue, PopulateOutput, Exhausted };
State state_{State::PullInput};
std::optional<MultiFrame> own_multi_frame_;
std::optional<ValidFramesConsumer> own_frames_consumer_;
ValidFramesConsumer::Iterator own_frames_it_;
};
UniqueCursorPtr Unwind::MakeCursor(utils::MemoryResource *mem) const {

View File

@@ -420,7 +420,7 @@ 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));
@@ -463,7 +463,7 @@ class RequestRouter : public RequestRouterInterface {
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);
}
@@ -800,15 +800,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 {
@@ -571,13 +575,64 @@ 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_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

@@ -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,54 @@ 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)
: action(Action::DELETE_OBJECT), id(delta_id), commit_info(commit_info), command_id(command_id) {}
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,8 +241,10 @@ 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;

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
@@ -108,7 +108,7 @@ inline bool PrepareForWrite(Transaction *transaction, TObj *object) {
/// a `DELETE_OBJECT` delta).
/// @throw std::bad_alloc
inline Delta *CreateDeleteObjectDelta(Transaction *transaction) {
return &transaction->deltas.emplace_back(Delta::DeleteObjectTag(), transaction->commit_info.get(),
return &transaction->deltas.emplace_back(Delta::DeleteObjectTag(), transaction->commit_info.get(), GetNextDeltaId(),
transaction->command_id);
}
@@ -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,25 +324,80 @@ 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_) {
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_) {
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_) {
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);
}
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) {}
@@ -436,7 +492,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_);
@@ -1048,6 +1104,42 @@ 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::info("ShouldSplit is attempting to begin the split process");
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));
return ShardSuggestedSplitInfo{
.label_id = PrimaryLabel(),
.splitting_shard_low_key = min_primary_key_,
.split_key = mid_elem->first,
.shard_version = shard_version_,
};
}
spdlog::trace("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_shard_version) {
if (old_shard_version < shard_version_) {
MG_ASSERT(false, "considering splitting 1");
spdlog::warn("Curent shard version {} is bigger than given {}", shard_version_, old_shard_version);
return std::nullopt;
}
MG_ASSERT(false, "actually splitting 2");
shard_version_ = new_shard_version;
const auto old_max_key = max_primary_key_;
max_primary_key_ = split_key;
return shard_splitter_.SplitShard(split_key, old_max_key, 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;
@@ -329,6 +356,8 @@ class Shard final {
LabelId PrimaryLabel() const;
PrimaryKey LowKey() const { return min_primary_key_; }
[[nodiscard]] bool IsVertexBelongToShard(const VertexId &vertex_id) const;
/// @throw std::bad_alloc
@@ -360,6 +389,10 @@ 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, Hlc new_shard_version);
private:
Transaction &GetTransaction(coordinator::Hlc start_timestamp, IsolationLevel isolation_level);
@@ -377,6 +410,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 +430,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;
@@ -174,7 +176,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::warn("ShardManager received InitializeSplitShard message");
MG_ASSERT(false, "ShardManager::InitializeSplitShard called :)");
for (const auto &[from_uuid, new_uuid] : init_split_shard.uuid_mapping) {
bool has_source = rsm_worker_mapping_.contains(from_uuid);
if (has_source) {
coordinator::ShardId new_shard_id =
std::make_pair(init_split_shard.shard->PrimaryLabel(), init_split_shard.shard->LowKey());
msgs::InitializeSplitShardByUUID msg{.shard = std::move(init_split_shard.shard), .shard_uuid = new_uuid};
SendToWorkerByUuid(new_uuid, std::move(msg));
spdlog::warn("ShardManager initialized split shard with uuid: {}", new_uuid);
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::info(
"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::info("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 +235,12 @@ 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_;
void Reconciliation() {
if (heartbeat_res_.has_value()) {
@@ -242,6 +274,7 @@ class ShardManager {
HeartbeatRequest req{
.from_storage_manager = GetAddress(),
.initialized_rsms = initialized_but_not_confirmed_rsm_,
.suggested_splits = std::move(pending_splits_),
};
CoordinatorWriteRequests cwr = req;
@@ -256,14 +289,20 @@ class ShardManager {
}
void EnsureShardsInitialized(HeartbeatResponse hr) {
for (const auto &acknowledged_rsm : hr.acknowledged_initialized_rsms) {
MG_ASSERT(false, "coordinator properly acking 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::info("ShardManager has been told to initialize shard {}", 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 +311,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(destination)) {
// 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.
// TODO(tyler) make this idempotent before it hits raft
// break;
}
if (rsm_worker_mapping_.contains(source)) {
MG_ASSERT(false, "sending split request from SM to shard rsm :)");
// 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_shard_version = to_split.new_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);
} else {
MG_ASSERT(false, "bad split source: {}", source);
}
}
}
}
};

View File

@@ -61,6 +61,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};
@@ -315,6 +317,22 @@ msgs::WriteResponses ShardRsm::ApplyWrite(msgs::UpdateEdgesRequest &&req) {
return msgs::UpdateEdgesResponse{std::move(shard_error)};
}
msgs::WriteResponses ShardRsm::ApplyWrite(msgs::SplitRequest &&req) {
MG_ASSERT(false, "ShardRsm::ApplyWrite 0");
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_shard_version);
if (new_shard_split_data) {
msgs::InitializeSplitShard msg{.shard = Shard::FromSplitData(std::move(*new_shard_split_data)),
.uuid_mapping = req.uuid_mapping};
ShardManagerMessages msg_to_send{std::move(msg)};
shard_manager_sender_.Send(std::move(msg_to_send));
}
return SplitResponse{};
}
msgs::ReadResponses ShardRsm::HandleRead(msgs::ScanVerticesRequest &&req) {
auto acc = shard_->Access(req.transaction_id);
std::optional<msgs::ShardError> shard_error;

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,21 @@
#pragma once
#include <memory>
#include <optional>
#include <variant>
#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<io::messages::ShardManagerMessages> shard_manager_sender_;
msgs::ReadResponses HandleRead(msgs::ExpandOneRequest &&req);
msgs::ReadResponses HandleRead(msgs::GetPropertiesRequest &&req);
@@ -36,10 +40,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<io::messages::ShardManagerMessages> 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,55 @@ 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::warn(
"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) {
MG_ASSERT(false, "trying to initialize new 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<io::messages::ShardManagerMessages> local_shard_manager_sender =
io_.template GetSender<io::messages::ShardManagerMessages>(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("SM 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 +222,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 {
@@ -197,16 +253,17 @@ class ShardWorker {
std::vector<Address> rsm_peers = {};
Address local_shard_manager_address = io_.GetAddress().ForkLocalShardManager();
io::Sender<io::messages::ShardManagerMessages> local_shard_manager_sender =
utils::Sender<io::messages::ShardManagerMessages> local_shard_manager_sender =
io_.template GetSender<io::messages::ShardManagerMessages>(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
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);
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)};
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)};

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,54 @@ 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);
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

@@ -32,5 +32,4 @@ 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(request_router.cpp)

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,64 +0,0 @@
// 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"
#include "io/simulator/simulator_config.hpp"
#include "io/time.hpp"
#include "storage/v3/shard_manager.hpp"
#include "test_cluster.hpp"
namespace memgraph::tests::simulation {
using io::Duration;
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)) {
spdlog::cfg::load_env_levels();
SimulatorConfig sim_config{
.drop_percent = 0,
.perform_timeouts = false,
.scramble_messages = true,
.rng_seed = rng_seed,
.start_time = Time::min(),
.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] = 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) {
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);
spdlog::error("run 1 latency:\n{}", latency_stats_1.SummaryTable());
spdlog::error("run 2 latency:\n{}", latency_stats_2.SummaryTable());
RC_ASSERT(latency_stats_1 == latency_stats_2);
RC_ASSERT(sim_stats_1 == sim_stats_2);
}
}
} // 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
@@ -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

@@ -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;
@@ -109,11 +110,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 +122,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<io::messages::ShardManagerMessages> 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;
@@ -105,9 +105,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 +117,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,67 +142,83 @@ 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());
RC_ASSERT(results.size() == context.correctness_model.size());
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) {
// TODO(tyler) assert on actual values returned
// 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(context.correctness_model.contains(model_key));
}
}
void ExecuteOp(SimClientContext &context, AssertShardsSplit assert_shards_split) {
const int minimum_expected_shards = (context.correctness_model.size() / context.cluster_config.split_threshold) + 1;
// TODO(tyler) make this a higher number of retries
const int maximum_attempts = 10'000;
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) {
MG_ASSERT(initialized_shards < 3, "just kidding, this is great, we now have {} initialized shards",
initialized_shards);
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
/// a thread if something causes an uncaught
/// exception - because that thread would not
@@ -222,15 +245,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 +267,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 +306,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 = 50;
// 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 = 3;
static constexpr auto kMaximumSplitThreshold = 10;
} // 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

@@ -86,7 +86,7 @@ class TestPlanner : public ::testing::Test {};
using PlannerTypes = ::testing::Types<Planner>;
TYPED_TEST_CASE(TestPlanner, PlannerTypes);
TYPED_TEST_SUITE(TestPlanner, PlannerTypes);
TYPED_TEST(TestPlanner, MatchFilterPropIsNotNull) {
const char *prim_label_name = "prim_label_one";

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
@@ -58,15 +58,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<io::messages::ShardManagerMessages> 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) {