Fix concurrent query module race condition (#1158)
Concurrent access to the same query module had a race condition on the pointer that was used to handle the custom memory management. With this commit, a mapping has been added to keep information about what thread used the pointer to handle the memory resources. This should be fine since the respected query executions are running on a dedicated thread. Access to the mapping itself is threadsafe. A simple RAII wrapper for the mapping container has also been added for simpler client-side use.
This commit is contained in:
parent
97183fb9da
commit
476968e2c8
379
include/mgp.hpp
379
include/mgp.hpp
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2022 Memgraph Ltd.
|
// Copyright 2023 Memgraph Ltd.
|
||||||
//
|
//
|
||||||
// Use of this software is governed by the Business Source License
|
// 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
|
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||||
@ -18,7 +18,11 @@ void ProcImpl(std::vector<mgp::Value> arguments, mgp::Graph graph, mgp::RecordFa
|
|||||||
|
|
||||||
void SampleReadProc(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
|
void SampleReadProc(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
|
||||||
try {
|
try {
|
||||||
mgp::memory = memory;
|
// The outcommented way of assigning the memory pointer is still
|
||||||
|
// working, but it is deprecated because of certain concurrency
|
||||||
|
// issues. Please use the guard instead.
|
||||||
|
// mgp::memory = memory;
|
||||||
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
|
|
||||||
std::vector<mgp::Value> arguments;
|
std::vector<mgp::Value> arguments;
|
||||||
for (size_t i = 0; i < mgp::list_size(args); i++) {
|
for (size_t i = 0; i < mgp::list_size(args); i++) {
|
||||||
@ -34,7 +38,11 @@ void SampleReadProc(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *resul
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AddXNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
|
void AddXNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
|
||||||
mgp::memory = memory;
|
// The outcommented way of assigning the memory pointer is still
|
||||||
|
// working, but it is deprecated because of certain concurrency
|
||||||
|
// issues. Please use the guard instead.
|
||||||
|
// mgp::memory = memory;
|
||||||
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
auto graph = mgp::Graph(memgraph_graph);
|
auto graph = mgp::Graph(memgraph_graph);
|
||||||
|
|
||||||
std::vector<mgp::Value> arguments;
|
std::vector<mgp::Value> arguments;
|
||||||
@ -49,7 +57,11 @@ void AddXNodes(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mg
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Multiply(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory) {
|
void Multiply(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_memory *memory) {
|
||||||
mgp::memory = memory;
|
// The outcommented way of assigning the memory pointer is still
|
||||||
|
// working, but it is deprecated because of certain concurrency
|
||||||
|
// issues. Please use the guard instead.
|
||||||
|
// mgp::memory = memory;
|
||||||
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
|
|
||||||
std::vector<mgp::Value> arguments;
|
std::vector<mgp::Value> arguments;
|
||||||
for (size_t i = 0; i < mgp::list_size(args); i++) {
|
for (size_t i = 0; i < mgp::list_size(args); i++) {
|
||||||
@ -67,7 +79,11 @@ void Multiply(mgp_list *args, mgp_func_context *ctx, mgp_func_result *res, mgp_m
|
|||||||
|
|
||||||
extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) {
|
extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) {
|
||||||
try {
|
try {
|
||||||
mgp::memory = memory;
|
// The outcommented way of assigning the memory pointer is still
|
||||||
|
// working, but it is deprecated because of certain concurrency
|
||||||
|
// issues. Please use the guard instead.
|
||||||
|
// mgp::memory = memory;
|
||||||
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
|
|
||||||
AddProcedure(SampleReadProc, "return_true", mgp::ProcedureType::Read,
|
AddProcedure(SampleReadProc, "return_true", mgp::ProcedureType::Read,
|
||||||
{mgp::Parameter("param_1", mgp::Type::Int), mgp::Parameter("param_2", mgp::Type::Double, 2.3)},
|
{mgp::Parameter("param_1", mgp::Type::Int), mgp::Parameter("param_2", mgp::Type::Double, 2.3)},
|
||||||
@ -77,7 +93,11 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *mem
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mgp::memory = memory;
|
// The outcommented way of assigning the memory pointer is still
|
||||||
|
// working, but it is deprecated because of certain concurrency
|
||||||
|
// issues. Please use the guard instead.
|
||||||
|
// mgp::memory = memory;
|
||||||
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
|
|
||||||
mgp::AddProcedure(AddXNodes, "add_x_nodes", mgp::ProcedureType::Write, {mgp::Parameter("param_1", mgp::Type::Int)},
|
mgp::AddProcedure(AddXNodes, "add_x_nodes", mgp::ProcedureType::Write, {mgp::Parameter("param_1", mgp::Type::Int)},
|
||||||
{}, module, memory);
|
{}, module, memory);
|
||||||
@ -87,7 +107,11 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *mem
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mgp::memory = memory;
|
// The outcommented way of assigning the memory pointer is still
|
||||||
|
// working, but it is deprecated because of certain concurrency
|
||||||
|
// issues. Please use the guard instead.
|
||||||
|
// mgp::memory = memory;
|
||||||
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
|
|
||||||
mgp::AddFunction(Multiply, "multiply",
|
mgp::AddFunction(Multiply, "multiply",
|
||||||
{mgp::Parameter("int", mgp::Type::Int), mgp::Parameter("int", mgp::Type::Int, (int64_t)3)}, module,
|
{mgp::Parameter("int", mgp::Type::Int), mgp::Parameter("int", mgp::Type::Int, (int64_t)3)}, module,
|
||||||
|
@ -61,6 +61,7 @@ add_subdirectory(load_csv)
|
|||||||
add_subdirectory(init_file_flags)
|
add_subdirectory(init_file_flags)
|
||||||
add_subdirectory(analytical_mode)
|
add_subdirectory(analytical_mode)
|
||||||
add_subdirectory(batched_procedures)
|
add_subdirectory(batched_procedures)
|
||||||
|
add_subdirectory(concurrent_query_modules)
|
||||||
|
|
||||||
copy_e2e_python_files(pytest_runner pytest_runner.sh "")
|
copy_e2e_python_files(pytest_runner pytest_runner.sh "")
|
||||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.crt DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.crt DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
@ -30,7 +30,7 @@ static int returned_strings{0};
|
|||||||
const char *kReturnOutput = "output";
|
const char *kReturnOutput = "output";
|
||||||
|
|
||||||
void NumsBatchInit(struct mgp_list *args, mgp_graph *graph, struct mgp_memory *memory) {
|
void NumsBatchInit(struct mgp_list *args, mgp_graph *graph, struct mgp_memory *memory) {
|
||||||
mgp::memory = memory;
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
const auto arguments = mgp::List(args);
|
const auto arguments = mgp::List(args);
|
||||||
if (arguments.Empty()) {
|
if (arguments.Empty()) {
|
||||||
throw std::runtime_error("Expected to recieve argument");
|
throw std::runtime_error("Expected to recieve argument");
|
||||||
@ -43,7 +43,7 @@ void NumsBatchInit(struct mgp_list *args, mgp_graph *graph, struct mgp_memory *m
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NumsBatch(struct mgp_list *args, mgp_graph *graph, mgp_result *result, struct mgp_memory *memory) {
|
void NumsBatch(struct mgp_list *args, mgp_graph *graph, mgp_result *result, struct mgp_memory *memory) {
|
||||||
mgp::memory = memory;
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
const auto arguments = mgp::List(args);
|
const auto arguments = mgp::List(args);
|
||||||
const auto record_factory = mgp::RecordFactory(result);
|
const auto record_factory = mgp::RecordFactory(result);
|
||||||
if (returned_ints < num_ints) {
|
if (returned_ints < num_ints) {
|
||||||
@ -58,7 +58,7 @@ void NumsBatchCleanup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StringsBatchInit(struct mgp_list *args, mgp_graph *graph, struct mgp_memory *memory) {
|
void StringsBatchInit(struct mgp_list *args, mgp_graph *graph, struct mgp_memory *memory) {
|
||||||
mgp::memory = memory;
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
const auto arguments = mgp::List(args);
|
const auto arguments = mgp::List(args);
|
||||||
if (arguments.Empty()) {
|
if (arguments.Empty()) {
|
||||||
throw std::runtime_error("Expected to recieve argument");
|
throw std::runtime_error("Expected to recieve argument");
|
||||||
@ -71,7 +71,7 @@ void StringsBatchInit(struct mgp_list *args, mgp_graph *graph, struct mgp_memory
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StringsBatch(struct mgp_list *args, mgp_graph *graph, mgp_result *result, struct mgp_memory *memory) {
|
void StringsBatch(struct mgp_list *args, mgp_graph *graph, mgp_result *result, struct mgp_memory *memory) {
|
||||||
mgp::memory = memory;
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
const auto arguments = mgp::List(args);
|
const auto arguments = mgp::List(args);
|
||||||
const auto record_factory = mgp::RecordFactory(result);
|
const auto record_factory = mgp::RecordFactory(result);
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *mem
|
|||||||
|
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
mgp::memory = memory;
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
mgp::AddBatchProcedure(StringsBatch, StringsBatchInit, StringsBatchCleanup, "batch_strings",
|
mgp::AddBatchProcedure(StringsBatch, StringsBatchInit, StringsBatchCleanup, "batch_strings",
|
||||||
mgp::ProcedureType::Read, {mgp::Parameter("num_strings", mgp::Type::Int)},
|
mgp::ProcedureType::Read, {mgp::Parameter("num_strings", mgp::Type::Int)},
|
||||||
{mgp::Return("output", mgp::Type::String)}, module, memory);
|
{mgp::Return("output", mgp::Type::String)}, module, memory);
|
||||||
|
8
tests/e2e/concurrent_query_modules/CMakeLists.txt
Normal file
8
tests/e2e/concurrent_query_modules/CMakeLists.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
function(copy_concurrent_query_modules_e2e_python_files FILE_NAME)
|
||||||
|
copy_e2e_python_files(concurrent_query_modules ${FILE_NAME})
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
copy_concurrent_query_modules_e2e_python_files(client.py)
|
||||||
|
copy_concurrent_query_modules_e2e_python_files(con_query_modules.py)
|
||||||
|
|
||||||
|
add_subdirectory(test_query_modules)
|
33
tests/e2e/concurrent_query_modules/client.py
Normal file
33
tests/e2e/concurrent_query_modules/client.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
import mgclient
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def inner(query, number_of_executions):
|
||||||
|
connection = mgclient.connect(host="127.0.0.1", port=7687)
|
||||||
|
connection.autocommit = True
|
||||||
|
cursor = connection.cursor()
|
||||||
|
for _ in range(number_of_executions):
|
||||||
|
cursor.execute(query)
|
||||||
|
cursor.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
class MemgraphClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.query_list = []
|
||||||
|
|
||||||
|
def initialize_to_execute(self, query: str, number_of_executions):
|
||||||
|
self.query_list.append((query, number_of_executions))
|
||||||
|
|
||||||
|
def execute_queries(self):
|
||||||
|
num_processes = len(self.query_list)
|
||||||
|
with multiprocessing.Pool(processes=num_processes) as pool:
|
||||||
|
pool.starmap(inner, self.query_list)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client() -> MemgraphClient:
|
||||||
|
return MemgraphClient()
|
19
tests/e2e/concurrent_query_modules/con_query_modules.py
Normal file
19
tests/e2e/concurrent_query_modules/con_query_modules.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from client import *
|
||||||
|
|
||||||
|
query = "MATCH (my_person:Person)-[:FOLLOW]->(follow_person:Person) MATCH (follow_person)-[: LIKE]->(post:Post) WHERE post.indexedAt IS NOT NULL AND NOT exists((post)-[:ROOT]->(:Post)) WITH localDateTime() - post.indexedAt as duration, post, follow_person WHERE duration.day < 5 WITH (duration.day * 24) + duration.hour as hour_age, post, follow_person ORDER BY post.indexedAt DESC LIMIT 500 MATCH(: Person) - [l: LIKE] -> (post) WITH count(l) as likes, hour_age, post, follow_person CALL libmodule_test.hacker_news(likes, 123, 4.1) YIELD score RETURN ID(post), post.uri, hour_age, likes, score, follow_person ORDER BY score DESC, hour_age ASC, post.indexedAt DESC LIMIT 100;"
|
||||||
|
|
||||||
|
|
||||||
|
def test_concurrent_module_access(client):
|
||||||
|
client.initialize_to_execute(query, 200)
|
||||||
|
client.initialize_to_execute(query, 200)
|
||||||
|
client.initialize_to_execute(query, 200)
|
||||||
|
|
||||||
|
success = client.execute_queries()
|
||||||
|
assert success
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(pytest.main([__file__, "-rA"]))
|
@ -0,0 +1,3 @@
|
|||||||
|
project(TestSharedObjects)
|
||||||
|
add_library(module_test SHARED module_test.cpp)
|
||||||
|
target_include_directories(module_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../../../include)
|
@ -0,0 +1,59 @@
|
|||||||
|
// 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 <mgp.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <list>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
constexpr char *kProcedureHackerNews = "hacker_news";
|
||||||
|
constexpr char *kArgumentHackerNewsVotes = "votes";
|
||||||
|
constexpr char *kArgumentHackerNewsItemHourAge = "item_hour_age";
|
||||||
|
constexpr char *kArgumentHackerNewsGravity = "gravity";
|
||||||
|
constexpr char *kReturnHackerNewsScore = "score";
|
||||||
|
|
||||||
|
void HackerNews(mgp_list *args, mgp_graph *memgraph_graph, mgp_result *result, mgp_memory *memory) {
|
||||||
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
const auto &arguments = mgp::List(args);
|
||||||
|
const auto record_factory = mgp::RecordFactory(result);
|
||||||
|
try {
|
||||||
|
const auto votes = arguments[0].ValueInt();
|
||||||
|
const auto item_hour_age = arguments[1].ValueInt();
|
||||||
|
const auto gravity = arguments[2].ValueDouble();
|
||||||
|
const auto score = 1000000.0 * (votes / pow((item_hour_age + 2), gravity));
|
||||||
|
auto record = record_factory.NewRecord();
|
||||||
|
record.Insert(kReturnHackerNewsScore, score);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
record_factory.SetErrorMessage(e.what());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int mgp_init_module(struct mgp_module *module, struct mgp_memory *memory) {
|
||||||
|
try {
|
||||||
|
mgp::MemoryDispatcherGuard guard(memory);
|
||||||
|
std::vector<mgp::Parameter> params = {
|
||||||
|
mgp::Parameter(kArgumentHackerNewsVotes, mgp::Type::Int),
|
||||||
|
mgp::Parameter(kArgumentHackerNewsItemHourAge, mgp::Type::Int),
|
||||||
|
mgp::Parameter(kArgumentHackerNewsGravity, mgp::Type::Double),
|
||||||
|
};
|
||||||
|
std::vector<mgp::Return> returns = {mgp::Return(kReturnHackerNewsScore, mgp::Type::Double)};
|
||||||
|
AddProcedure(HackerNews, kProcedureHackerNews, mgp::ProcedureType::Read, params, returns, module, memory);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int mgp_shutdown_module() { return 0; }
|
33
tests/e2e/concurrent_query_modules/workloads.yaml
Normal file
33
tests/e2e/concurrent_query_modules/workloads.yaml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
args: &args
|
||||||
|
- "--bolt-port"
|
||||||
|
- "7687"
|
||||||
|
- "--log-level"
|
||||||
|
- "TRACE"
|
||||||
|
|
||||||
|
in_memory_cluster: &in_memory_cluster
|
||||||
|
cluster:
|
||||||
|
main:
|
||||||
|
args: *args
|
||||||
|
log_file: "concurrent-query-modules-e2e.log"
|
||||||
|
setup_queries: []
|
||||||
|
validation_queries: []
|
||||||
|
|
||||||
|
disk_cluster: &disk_cluster
|
||||||
|
cluster:
|
||||||
|
main:
|
||||||
|
args: *args
|
||||||
|
log_file: "concurrent-query-modules-e2e.log"
|
||||||
|
setup_queries: ["STORAGE MODE ON_DISK_TRANSACTIONAL"]
|
||||||
|
validation_queries: []
|
||||||
|
|
||||||
|
workloads:
|
||||||
|
- name: "Concurrent query modules"
|
||||||
|
binary: "tests/e2e/pytest_runner.sh"
|
||||||
|
proc: "tests/e2e/concurrent_query_modules/test_query_modules/"
|
||||||
|
args: ["concurrent_query_modules/con_query_modules.py"]
|
||||||
|
<<: *in_memory_cluster
|
||||||
|
- name: "Disk concurrent query modules"
|
||||||
|
binary: "tests/e2e/pytest_runner.sh"
|
||||||
|
proc: "tests/e2e/concurrent_query_modules/test_query_modules/"
|
||||||
|
args: ["concurrent_query_modules/con_query_modules.py"]
|
||||||
|
<<: *disk_cluster
|
@ -27,12 +27,13 @@
|
|||||||
template <typename StorageType>
|
template <typename StorageType>
|
||||||
struct CppApiTestFixture : public ::testing::Test {
|
struct CppApiTestFixture : public ::testing::Test {
|
||||||
protected:
|
protected:
|
||||||
virtual void SetUp() override { mgp::memory = &memory; }
|
virtual void SetUp() override { mgp::mrd.Register(&memory); }
|
||||||
|
|
||||||
void TearDown() override {
|
void TearDown() override {
|
||||||
if (std::is_same<StorageType, memgraph::storage::DiskStorage>::value) {
|
if (std::is_same<StorageType, memgraph::storage::DiskStorage>::value) {
|
||||||
disk_test_utils::RemoveRocksDbDirs(testSuite);
|
disk_test_utils::RemoveRocksDbDirs(testSuite);
|
||||||
}
|
}
|
||||||
|
mgp::mrd.UnRegister();
|
||||||
}
|
}
|
||||||
|
|
||||||
mgp_graph CreateGraph(const memgraph::storage::View view = memgraph::storage::View::NEW) {
|
mgp_graph CreateGraph(const memgraph::storage::View view = memgraph::storage::View::NEW) {
|
||||||
|
Loading…
Reference in New Issue
Block a user