071df2f439
* Split queries into system and data queries * System queries are sequentially executed and generate separate transaction deltas * System transaction try locks for 100ms * last_commited_system_ts saved to DBMS durability * Replicating CREATE/DROP DATABASE * Sending a system snapshot if REPLICA behind * Passing a copy of the gatekeeper::access as std::any to all functions that could call an async execution * Removed delete_on_drop flag (we now always delete on drop) * Using UUID as the directory name for databases * DBMS durability update (added versioning and salient information) * Automatic migration from previous version * Interpreter can run some queries without a target database * SHOW REPLICA returns the status of the currently active DB * Returning UUID instead of db name in the RPC responses * Using UUIDs for database specification in RPC (not name) * FrequentCheck forces update on reconnect * TimestampRpc will detect if a replica is behind, and will update client's state * Safer SLK reads * Split SHOW DATABASES in two SHOW DATABASES (list of current databases) and SHOW DATABASE a single string naming the current database --------- Co-authored-by: Gareth Lloyd <gareth.lloyd@memgraph.io>
379 lines
15 KiB
C++
379 lines
15 KiB
C++
// Copyright 2024 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 <algorithm>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <thread>
|
|
|
|
#include "communication/bolt/v1/value.hpp"
|
|
#include "communication/result_stream_faker.hpp"
|
|
#include "csv/parsing.hpp"
|
|
#include "dbms/dbms_handler.hpp"
|
|
#include "disk_test_utils.hpp"
|
|
#include "flags/run_time_configurable.hpp"
|
|
#include "glue/communication.hpp"
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
#include "interpreter_faker.hpp"
|
|
#include "license/license.hpp"
|
|
#include "query/auth_checker.hpp"
|
|
#include "query/config.hpp"
|
|
#include "query/exceptions.hpp"
|
|
#include "query/interpreter.hpp"
|
|
#include "query/interpreter_context.hpp"
|
|
#include "query/metadata.hpp"
|
|
#include "query/stream.hpp"
|
|
#include "query/typed_value.hpp"
|
|
#include "query_common.hpp"
|
|
#include "replication/state.hpp"
|
|
#include "storage/v2/inmemory/storage.hpp"
|
|
#include "storage/v2/isolation_level.hpp"
|
|
#include "storage/v2/property_value.hpp"
|
|
#include "storage/v2/storage_mode.hpp"
|
|
#include "utils/logging.hpp"
|
|
#include "utils/lru_cache.hpp"
|
|
#include "utils/synchronized.hpp"
|
|
|
|
namespace {
|
|
std::set<std::string> GetDirs(auto path) {
|
|
std::set<std::string> dirs;
|
|
// Clean the unused directories
|
|
for (const auto &entry : std::filesystem::directory_iterator(path)) {
|
|
const auto &name = entry.path().filename().string();
|
|
if (entry.is_directory() && !name.empty() && name.front() != '.') {
|
|
dirs.emplace(name);
|
|
}
|
|
}
|
|
return dirs;
|
|
}
|
|
|
|
auto RunMtQuery(auto &interpreter, const std::string &query, std::string_view res) {
|
|
auto [stream, qid] = interpreter.Prepare(query);
|
|
ASSERT_EQ(stream.GetHeader().size(), 1U);
|
|
EXPECT_EQ(stream.GetHeader()[0], "STATUS");
|
|
interpreter.Pull(&stream, 1);
|
|
ASSERT_EQ(stream.GetSummary().count("has_more"), 1);
|
|
ASSERT_FALSE(stream.GetSummary().at("has_more").ValueBool());
|
|
ASSERT_EQ(stream.GetResults()[0].size(), 1U);
|
|
ASSERT_EQ(stream.GetResults()[0][0].ValueString(), res);
|
|
}
|
|
|
|
auto RunQuery(auto &interpreter, const std::string &query) {
|
|
auto [stream, qid] = interpreter.Prepare(query);
|
|
interpreter.Pull(&stream, 1);
|
|
return stream.GetResults();
|
|
}
|
|
|
|
void UseDatabase(auto &interpreter, const std::string &name, std::string_view res) {
|
|
RunMtQuery(interpreter, "USE DATABASE " + name, res);
|
|
}
|
|
|
|
void DropDatabase(auto &interpreter, const std::string &name, std::string_view res) {
|
|
RunMtQuery(interpreter, "DROP DATABASE " + name, res);
|
|
}
|
|
} // namespace
|
|
|
|
class MultiTenantTest : public ::testing::Test {
|
|
public:
|
|
std::filesystem::path data_directory = std::filesystem::temp_directory_path() / "MG_tests_unit_multi_tenancy";
|
|
|
|
MultiTenantTest() = default;
|
|
|
|
memgraph::storage::Config config{
|
|
[&]() {
|
|
memgraph::storage::Config config{};
|
|
UpdatePaths(config, data_directory);
|
|
return config;
|
|
}() // iile
|
|
};
|
|
|
|
struct MinMemgraph {
|
|
explicit MinMemgraph(const memgraph::storage::Config &conf)
|
|
: dbms{conf,
|
|
reinterpret_cast<
|
|
memgraph::utils::Synchronized<memgraph::auth::Auth, memgraph::utils::WritePrioritizedRWLock> *>(0),
|
|
true},
|
|
interpreter_context{{}, &dbms, &dbms.ReplicationState()} {
|
|
memgraph::utils::global_settings.Initialize(conf.durability.storage_directory / "settings");
|
|
memgraph::license::RegisterLicenseSettings(memgraph::license::global_license_checker,
|
|
memgraph::utils::global_settings);
|
|
memgraph::flags::run_time::Initialize();
|
|
memgraph::license::global_license_checker.CheckEnvLicense();
|
|
}
|
|
|
|
~MinMemgraph() { memgraph::utils::global_settings.Finalize(); }
|
|
|
|
auto NewInterpreter() { return InterpreterFaker{&interpreter_context, dbms.Get()}; }
|
|
|
|
memgraph::dbms::DbmsHandler dbms;
|
|
memgraph::query::InterpreterContext interpreter_context;
|
|
};
|
|
|
|
void SetUp() override {
|
|
TearDown();
|
|
min_mg.emplace(config);
|
|
}
|
|
|
|
void TearDown() override {
|
|
min_mg.reset();
|
|
if (std::filesystem::exists(data_directory)) std::filesystem::remove_all(data_directory);
|
|
}
|
|
|
|
auto NewInterpreter() { return min_mg->NewInterpreter(); }
|
|
|
|
auto &DBMS() { return min_mg->dbms; }
|
|
|
|
std::optional<MinMemgraph> min_mg;
|
|
};
|
|
|
|
TEST_F(MultiTenantTest, SimpleCreateDrop) {
|
|
// 1) Create multiple interpreters with the default db
|
|
// 2) Create multiple databases using both
|
|
// 3) Drop databases while the other is using
|
|
|
|
// 1
|
|
auto interpreter1 = this->NewInterpreter();
|
|
auto interpreter2 = this->NewInterpreter();
|
|
|
|
// 2
|
|
auto create = [&](auto &interpreter, const std::string &name, bool success) {
|
|
RunMtQuery(interpreter, "CREATE DATABASE " + name,
|
|
success ? ("Successfully created database " + name) : (name + " already exists."));
|
|
};
|
|
|
|
create(interpreter1, "db1", true);
|
|
create(interpreter1, "db1", false);
|
|
create(interpreter2, "db1", false);
|
|
create(interpreter2, "db2", true);
|
|
create(interpreter1, "db2", false);
|
|
create(interpreter2, "db3", true);
|
|
create(interpreter2, "db4", true);
|
|
|
|
// 3
|
|
UseDatabase(interpreter1, "db2", "Using db2");
|
|
UseDatabase(interpreter1, "db2", "Already using db2");
|
|
UseDatabase(interpreter2, "db2", "Using db2");
|
|
UseDatabase(interpreter1, "db4", "Using db4");
|
|
|
|
ASSERT_THROW(DropDatabase(interpreter1, memgraph::dbms::kDefaultDB.data(), ""),
|
|
memgraph::query::QueryRuntimeException); // default db
|
|
|
|
DropDatabase(interpreter1, "db1", "Successfully deleted db1");
|
|
ASSERT_THROW(DropDatabase(interpreter2, "db1", ""), memgraph::query::QueryRuntimeException); // No db1
|
|
ASSERT_THROW(DropDatabase(interpreter1, "db1", ""), memgraph::query::QueryRuntimeException); // No db1
|
|
|
|
ASSERT_THROW(DropDatabase(interpreter1, "db2", ""), memgraph::query::QueryRuntimeException); // i2 using db2
|
|
ASSERT_THROW(DropDatabase(interpreter1, "db4", ""), memgraph::query::QueryRuntimeException); // i1 using db4
|
|
}
|
|
|
|
TEST_F(MultiTenantTest, DbmsNewTryDelete) {
|
|
// 1) Create multiple interpreters with the default db
|
|
// 2) Create multiple databases using dbms
|
|
// 3) Try delete databases while the interpreters are using them
|
|
|
|
// 1
|
|
auto interpreter1 = this->NewInterpreter();
|
|
auto interpreter2 = this->NewInterpreter();
|
|
|
|
// 2
|
|
auto &dbms = DBMS();
|
|
ASSERT_FALSE(dbms.New("db1").HasError());
|
|
ASSERT_FALSE(dbms.New("db2").HasError());
|
|
ASSERT_FALSE(dbms.New("db3").HasError());
|
|
ASSERT_FALSE(dbms.New("db4").HasError());
|
|
|
|
// 3
|
|
UseDatabase(interpreter2, "db2", "Using db2");
|
|
UseDatabase(interpreter1, "db4", "Using db4");
|
|
|
|
ASSERT_FALSE(dbms.TryDelete("db1").HasError());
|
|
ASSERT_TRUE(dbms.TryDelete("db2").HasError());
|
|
ASSERT_FALSE(dbms.TryDelete("db3").HasError());
|
|
ASSERT_TRUE(dbms.TryDelete("db4").HasError());
|
|
}
|
|
|
|
TEST_F(MultiTenantTest, DbmsUpdate) {
|
|
// 1) Create multiple interpreters with the default db
|
|
// 2) Create multiple databases using dbms
|
|
// 3) Try to update databases
|
|
|
|
auto &dbms = DBMS();
|
|
auto interpreter1 = this->NewInterpreter();
|
|
|
|
// Update clean default db
|
|
auto default_db = dbms.Get();
|
|
const auto old_uuid = default_db->config().salient.uuid;
|
|
const memgraph::utils::UUID new_uuid{/* random */};
|
|
const memgraph::storage::SalientConfig &config{.name = "memgraph", .uuid = new_uuid};
|
|
auto new_default = dbms.Update(config);
|
|
ASSERT_TRUE(new_default.HasValue());
|
|
ASSERT_NE(new_uuid, old_uuid);
|
|
ASSERT_EQ(default_db->storage(), new_default.GetValue()->storage());
|
|
|
|
// Add node to default
|
|
RunQuery(interpreter1, "CREATE (:Node)");
|
|
|
|
// Fail to update dirty default db
|
|
const memgraph::storage::SalientConfig &failing_config{.name = "memgraph", .uuid = {}};
|
|
auto failed_update = dbms.Update(failing_config);
|
|
ASSERT_TRUE(failed_update.HasError());
|
|
|
|
// Succeed when updating with the same config
|
|
auto same_update = dbms.Update(config);
|
|
ASSERT_TRUE(same_update.HasValue());
|
|
ASSERT_EQ(new_default.GetValue()->storage(), same_update.GetValue()->storage());
|
|
|
|
// Create new db
|
|
auto db1 = dbms.New("db1");
|
|
ASSERT_FALSE(db1.HasError());
|
|
RunMtQuery(interpreter1, "USE DATABASE db1", "Using db1");
|
|
RunQuery(interpreter1, "CREATE (:NewNode)");
|
|
RunQuery(interpreter1, "CREATE (:NewNode)");
|
|
const auto db1_config_old = db1.GetValue()->config();
|
|
|
|
// Begin a transaction on db1
|
|
auto interpreter2 = this->NewInterpreter();
|
|
RunMtQuery(interpreter2, "USE DATABASE db1", "Using db1");
|
|
ASSERT_EQ(RunQuery(interpreter2, "SHOW DATABASE")[0][0].ValueString(), "db1");
|
|
RunQuery(interpreter2, "BEGIN");
|
|
|
|
// Update and check the new db in clean
|
|
auto interpreter3 = this->NewInterpreter();
|
|
const memgraph::storage::SalientConfig &db1_config_new{.name = "db1", .uuid = {}};
|
|
auto new_db1 = dbms.Update(db1_config_new);
|
|
ASSERT_TRUE(new_db1.HasValue());
|
|
ASSERT_NE(db1_config_new.uuid, db1_config_old.salient.uuid);
|
|
RunMtQuery(interpreter3, "USE DATABASE db1", "Using db1");
|
|
ASSERT_EQ(RunQuery(interpreter3, "MATCH(n) RETURN count(*)")[0][0].ValueInt(), 0);
|
|
|
|
// Check that the interpreter1 is still valid, but lacking a db
|
|
ASSERT_THROW(RunQuery(interpreter1, "CREATE (:Node)"), memgraph::query::DatabaseContextRequiredException);
|
|
|
|
// Check that the interpreter2 is still valid and pointing to the old db1 (until commit)
|
|
RunQuery(interpreter2, "CREATE (:NewNode)");
|
|
ASSERT_EQ(RunQuery(interpreter2, "MATCH(n) RETURN count(*)")[0][0].ValueInt(), 3);
|
|
RunQuery(interpreter2, "COMMIT");
|
|
ASSERT_THROW(RunQuery(interpreter2, "MATCH(n) RETURN n"), memgraph::query::DatabaseContextRequiredException);
|
|
}
|
|
|
|
TEST_F(MultiTenantTest, DbmsNewDelete) {
|
|
// 1) Create multiple interpreters with the default db
|
|
// 2) Create multiple databases using dbms
|
|
// 3) Defer delete databases while the interpreters are using them
|
|
// 4) Database should be a zombie until the using interpreter retries to query it
|
|
// 5) Check it is deleted from disk
|
|
|
|
// 1
|
|
auto interpreter1 = this->NewInterpreter();
|
|
auto interpreter2 = this->NewInterpreter();
|
|
|
|
// 2
|
|
auto &dbms = DBMS();
|
|
ASSERT_FALSE(dbms.New("db1").HasError());
|
|
ASSERT_FALSE(dbms.New("db2").HasError());
|
|
ASSERT_FALSE(dbms.New("db3").HasError());
|
|
ASSERT_FALSE(dbms.New("db4").HasError());
|
|
|
|
// 3
|
|
UseDatabase(interpreter2, "db2", "Using db2");
|
|
UseDatabase(interpreter1, "db4", "Using db4");
|
|
|
|
RunQuery(interpreter1, "CREATE (:Node{on:\"db4\"})");
|
|
RunQuery(interpreter1, "CREATE (:Node{on:\"db4\"})");
|
|
RunQuery(interpreter1, "CREATE (:Node{on:\"db4\"})");
|
|
RunQuery(interpreter1, "CREATE (:Node{on:\"db4\"})");
|
|
RunQuery(interpreter2, "CREATE (:Node{on:\"db2\"})");
|
|
RunQuery(interpreter2, "CREATE (:Node{on:\"db2\"})");
|
|
|
|
ASSERT_FALSE(dbms.Delete("db1").HasError());
|
|
ASSERT_FALSE(dbms.Delete("db2").HasError());
|
|
ASSERT_FALSE(dbms.Delete("db3").HasError());
|
|
ASSERT_FALSE(dbms.Delete("db4").HasError());
|
|
|
|
// 4
|
|
ASSERT_EQ(dbms.All().size(), 1);
|
|
ASSERT_EQ(GetDirs(data_directory / "databases").size(), 3); // All used databases remain on disk, but unusable
|
|
ASSERT_THROW(RunQuery(interpreter1, "MATCH(:Node{on:db4}) RETURN count(*)"),
|
|
memgraph::query::DatabaseContextRequiredException);
|
|
ASSERT_THROW(RunQuery(interpreter2, "MATCH(:Node{on:db2}) RETURN count(*)"),
|
|
memgraph::query::DatabaseContextRequiredException);
|
|
|
|
// 5
|
|
using namespace std::chrono_literals;
|
|
std::this_thread::sleep_for(100ms); // Wait for the filesystem to be updated
|
|
ASSERT_EQ(GetDirs(data_directory / "databases").size(), 1); // Databases deleted from disk
|
|
ASSERT_THROW(RunQuery(interpreter1, "MATCH(n) RETURN n"), memgraph::query::DatabaseContextRequiredException);
|
|
ASSERT_THROW(RunQuery(interpreter2, "MATCH(n) RETURN n"), memgraph::query::DatabaseContextRequiredException);
|
|
}
|
|
|
|
TEST_F(MultiTenantTest, DbmsNewDeleteWTx) {
|
|
// 1) Create multiple interpreters with the default db
|
|
// 2) Create multiple databases using dbms
|
|
// 3) Defer delete databases while the interpreters are using them
|
|
// 4) Interpreters that had an open transaction before should still be working
|
|
// 5) New transactions on deleted databases should throw
|
|
// 6) Switching databases should still be possible
|
|
|
|
// 1
|
|
auto interpreter1 = this->NewInterpreter();
|
|
auto interpreter2 = this->NewInterpreter();
|
|
|
|
// 2
|
|
auto &dbms = DBMS();
|
|
ASSERT_FALSE(dbms.New("db1").HasError());
|
|
ASSERT_FALSE(dbms.New("db2").HasError());
|
|
ASSERT_FALSE(dbms.New("db3").HasError());
|
|
ASSERT_FALSE(dbms.New("db4").HasError());
|
|
|
|
// 3
|
|
UseDatabase(interpreter2, "db2", "Using db2");
|
|
UseDatabase(interpreter1, "db4", "Using db4");
|
|
|
|
RunQuery(interpreter1, "CREATE (:Node{on:\"db4\"})");
|
|
RunQuery(interpreter1, "CREATE (:Node{on:\"db4\"})");
|
|
RunQuery(interpreter1, "CREATE (:Node{on:\"db4\"})");
|
|
RunQuery(interpreter1, "CREATE (:Node{on:\"db4\"})");
|
|
RunQuery(interpreter2, "CREATE (:Node{on:\"db2\"})");
|
|
RunQuery(interpreter2, "CREATE (:Node{on:\"db2\"})");
|
|
|
|
RunQuery(interpreter1, "BEGIN");
|
|
RunQuery(interpreter2, "BEGIN");
|
|
|
|
ASSERT_FALSE(dbms.Delete("db1").HasError());
|
|
ASSERT_FALSE(dbms.Delete("db2").HasError());
|
|
ASSERT_FALSE(dbms.Delete("db3").HasError());
|
|
ASSERT_FALSE(dbms.Delete("db4").HasError());
|
|
|
|
// 4
|
|
ASSERT_EQ(dbms.All().size(), 1);
|
|
ASSERT_EQ(GetDirs(data_directory / "databases").size(), 3); // All used databases remain on disk, and usable
|
|
ASSERT_EQ(RunQuery(interpreter1, "MATCH(:Node{on:\"db4\"}) RETURN count(*)")[0][0].ValueInt(), 4);
|
|
ASSERT_EQ(RunQuery(interpreter2, "MATCH(:Node{on:\"db2\"}) RETURN count(*)")[0][0].ValueInt(), 2);
|
|
RunQuery(interpreter1, "MATCH(n:Node{on:\"db4\"}) DELETE n");
|
|
RunQuery(interpreter2, "CREATE(:Node{on:\"db2\"})");
|
|
ASSERT_EQ(RunQuery(interpreter1, "MATCH(:Node{on:\"db4\"}) RETURN count(*)")[0][0].ValueInt(), 0);
|
|
ASSERT_EQ(RunQuery(interpreter2, "MATCH(:Node{on:\"db2\"}) RETURN count(*)")[0][0].ValueInt(), 3);
|
|
RunQuery(interpreter1, "COMMIT");
|
|
RunQuery(interpreter2, "COMMIT");
|
|
|
|
// 5
|
|
using namespace std::chrono_literals;
|
|
std::this_thread::sleep_for(100ms); // Wait for the filesystem to be updated
|
|
ASSERT_EQ(GetDirs(data_directory / "databases").size(), 1); // Only the active databases remain
|
|
ASSERT_THROW(RunQuery(interpreter1, "MATCH(n) RETURN n"), memgraph::query::DatabaseContextRequiredException);
|
|
ASSERT_THROW(RunQuery(interpreter2, "MATCH(n) RETURN n"), memgraph::query::DatabaseContextRequiredException);
|
|
|
|
// 6
|
|
UseDatabase(interpreter2, memgraph::dbms::kDefaultDB.data(), "Using memgraph");
|
|
UseDatabase(interpreter1, memgraph::dbms::kDefaultDB.data(), "Using memgraph");
|
|
}
|