// Copyright 2021 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 "query/procedure/module.hpp" #include "utils/memory.hpp" extern "C" { #include } #include #include "fmt/format.h" #include "py/py.hpp" #include "query/procedure/mg_procedure_helpers.hpp" #include "query/procedure/py_module.hpp" #include "utils/file.hpp" #include "utils/logging.hpp" #include "utils/pmr/vector.hpp" #include "utils/string.hpp" namespace query::procedure { ModuleRegistry gModuleRegistry; Module::~Module() {} class BuiltinModule final : public Module { public: BuiltinModule(); ~BuiltinModule() override; BuiltinModule(const BuiltinModule &) = delete; BuiltinModule(BuiltinModule &&) = delete; BuiltinModule &operator=(const BuiltinModule &) = delete; BuiltinModule &operator=(BuiltinModule &&) = delete; bool Close() override; const std::map> *Procedures() const override; const std::map> *Transformations() const override; void AddProcedure(std::string_view name, mgp_proc proc); void AddTransformation(std::string_view name, mgp_trans trans); private: /// Registered procedures std::map> procedures_; std::map> transformations_; }; BuiltinModule::BuiltinModule() {} BuiltinModule::~BuiltinModule() {} bool BuiltinModule::Close() { return true; } const std::map> *BuiltinModule::Procedures() const { return &procedures_; } const std::map> *BuiltinModule::Transformations() const { return &transformations_; } void BuiltinModule::AddProcedure(std::string_view name, mgp_proc proc) { procedures_.emplace(name, std::move(proc)); } void BuiltinModule::AddTransformation(std::string_view name, mgp_trans trans) { transformations_.emplace(name, std::move(trans)); } namespace { void RegisterMgLoad(ModuleRegistry *module_registry, utils::RWLock *lock, BuiltinModule *module) { // Loading relies on the fact that regular procedure invocation through // CallProcedureCursor::Pull takes ModuleRegistry::lock_ with READ access. To // load modules we have to upgrade our READ access to WRITE access, // therefore we release the READ lock and invoke the load function which // takes the WRITE lock. Obviously, some other thread may take a READ or WRITE // lock during our transition when we hold no such lock. In this case it is // fine, because our builtin module cannot be unloaded and we are ok with // using the new state of module_registry when we manage to acquire the lock // we desire. Note, deadlock between threads should not be possible, because a // single thread may only take either a READ or a WRITE lock, it's not // possible for a thread to hold both. If a thread tries to do that, it will // deadlock immediately (no other thread needs to do anything). auto with_unlock_shared = [lock](const auto &load_function) { lock->unlock_shared(); try { load_function(); // There's no finally in C++, but we have to return our original READ lock // state in any possible case. } catch (...) { lock->lock_shared(); throw; } lock->lock_shared(); }; auto load_all_cb = [module_registry, with_unlock_shared](mgp_list * /*args*/, mgp_graph * /*graph*/, mgp_result * /*result*/, mgp_memory * /*memory*/) { with_unlock_shared([&]() { module_registry->UnloadAndLoadModulesFromDirectories(); }); }; mgp_proc load_all("load_all", load_all_cb, utils::NewDeleteResource(), false); module->AddProcedure("load_all", std::move(load_all)); auto load_cb = [module_registry, with_unlock_shared](mgp_list *args, mgp_graph * /*graph*/, mgp_result *result, mgp_memory * /*memory*/) { MG_ASSERT(Call(mgp_list_size, args) == 1U, "Should have been type checked already"); auto *arg = Call(mgp_list_at, args, 0); MG_ASSERT(CallBool(mgp_value_is_string, arg), "Should have been type checked already"); bool succ = false; with_unlock_shared([&]() { const char *arg_as_string{nullptr}; if (const auto err = mgp_value_get_string(arg, &arg_as_string); err != MGP_ERROR_NO_ERROR) { succ = false; } else { succ = module_registry->LoadOrReloadModuleFromName(arg_as_string); } }); if (!succ) { MG_ASSERT(mgp_result_set_error_msg(result, "Failed to (re)load the module.") == MGP_ERROR_NO_ERROR); } }; mgp_proc load("load", load_cb, utils::NewDeleteResource(), false); MG_ASSERT(mgp_proc_add_arg(&load, "module_name", Call(mgp_type_string)) == MGP_ERROR_NO_ERROR); module->AddProcedure("load", std::move(load)); } void RegisterMgProcedures( // We expect modules to be sorted by name. const std::map, std::less<>> *all_modules, BuiltinModule *module) { auto procedures_cb = [all_modules](mgp_list * /*args*/, mgp_graph * /*graph*/, mgp_result *result, mgp_memory *memory) { // Iterating over all_modules assumes that the standard mechanism of custom // procedure invocations takes the ModuleRegistry::lock_ with READ access. // For details on how the invocation is done, take a look at the // CallProcedureCursor::Pull implementation. for (const auto &[module_name, module] : *all_modules) { // Return the results in sorted order by module and by procedure. static_assert( std::is_same_vProcedures()), const std::map> *>, "Expected module procedures to be sorted by name"); for (const auto &[proc_name, proc] : *module->Procedures()) { mgp_result_record *record{nullptr}; if (const auto err = mgp_result_new_record(result, &record); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { static_cast(mgp_result_set_error_msg(result, "Not enough memory!")); return; } else if (err != MGP_ERROR_NO_ERROR) { static_cast(mgp_result_set_error_msg(result, "Unexpected error")); return; } utils::pmr::string full_name(module_name, memory->impl); full_name.append(1, '.'); full_name.append(proc_name); MgpUniquePtr name_value{nullptr, mgp_value_destroy}; if (const auto err = CreateMgpObject(name_value, mgp_value_make_string, full_name.c_str(), memory); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { static_cast(mgp_result_set_error_msg(result, "Not enough memory!")); return; } else if (err != MGP_ERROR_NO_ERROR) { static_cast(mgp_result_set_error_msg(result, "Unexpected error")); return; } std::stringstream ss; ss << module_name << "."; PrintProcSignature(proc, &ss); const auto signature = ss.str(); MgpUniquePtr signature_value{nullptr, mgp_value_destroy}; if (const auto err = CreateMgpObject(signature_value, mgp_value_make_string, signature.c_str(), memory); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { static_cast(mgp_result_set_error_msg(result, "Not enough memory!")); return; } else if (err != MGP_ERROR_NO_ERROR) { static_cast(mgp_result_set_error_msg(result, "Unexpected error")); return; } MgpUniquePtr is_write_value{nullptr, mgp_value_destroy}; if (const auto err = CreateMgpObject(is_write_value, mgp_value_make_bool, proc.is_write_procedure ? 1 : 0, memory); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { static_cast(mgp_result_set_error_msg(result, "Not enough memory!")); return; } else if (err != MGP_ERROR_NO_ERROR) { static_cast(mgp_result_set_error_msg(result, "Unexpected error")); return; } const auto err1 = mgp_result_record_insert(record, "name", name_value.get()); const auto err2 = mgp_result_record_insert(record, "signature", signature_value.get()); const auto err3 = mgp_result_record_insert(record, "is_write", is_write_value.get()); if (err1 != MGP_ERROR_NO_ERROR || err2 != MGP_ERROR_NO_ERROR || err3 != MGP_ERROR_NO_ERROR) { static_cast(mgp_result_set_error_msg(result, "Unable to set the result!")); return; } } } }; mgp_proc procedures("procedures", procedures_cb, utils::NewDeleteResource(), false); MG_ASSERT(mgp_proc_add_result(&procedures, "name", Call(mgp_type_string)) == MGP_ERROR_NO_ERROR); MG_ASSERT(mgp_proc_add_result(&procedures, "signature", Call(mgp_type_string)) == MGP_ERROR_NO_ERROR); MG_ASSERT(mgp_proc_add_result(&procedures, "is_write", Call(mgp_type_bool)) == MGP_ERROR_NO_ERROR); module->AddProcedure("procedures", std::move(procedures)); } void RegisterMgTransformations(const std::map, std::less<>> *all_modules, BuiltinModule *module) { auto transformations_cb = [all_modules](mgp_list * /*unused*/, mgp_graph * /*unused*/, mgp_result *result, mgp_memory *memory) { for (const auto &[module_name, module] : *all_modules) { // Return the results in sorted order by module and by transformation. static_assert( std::is_same_vTransformations()), const std::map> *>, "Expected module transformations to be sorted by name"); for (const auto &[trans_name, proc] : *module->Transformations()) { mgp_result_record *record{nullptr}; if (const auto err = mgp_result_new_record(result, &record); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { static_cast(mgp_result_set_error_msg(result, "Not enough memory!")); return; } else if (err != MGP_ERROR_NO_ERROR) { static_cast(mgp_result_set_error_msg(result, "Unexpected error")); return; } utils::pmr::string full_name(module_name, memory->impl); full_name.append(1, '.'); full_name.append(trans_name); MgpUniquePtr name_value{nullptr, mgp_value_destroy}; if (const auto err = CreateMgpObject(name_value, mgp_value_make_string, full_name.c_str(), memory); err == MGP_ERROR_UNABLE_TO_ALLOCATE) { static_cast(mgp_result_set_error_msg(result, "Not enough memory!")); return; } else if (err != MGP_ERROR_NO_ERROR) { static_cast(mgp_result_set_error_msg(result, "Unexpected error")); return; } if (const auto err = mgp_result_record_insert(record, "name", name_value.get()); err != MGP_ERROR_NO_ERROR) { static_cast(mgp_result_set_error_msg(result, "Unable to set the result!")); return; } } } }; mgp_proc procedures("transformations", transformations_cb, utils::NewDeleteResource(), false); MG_ASSERT(mgp_proc_add_result(&procedures, "name", Call(mgp_type_string)) == MGP_ERROR_NO_ERROR); module->AddProcedure("transformations", std::move(procedures)); } // Run `fun` with `mgp_module *` and `mgp_memory *` arguments. If `fun` returned // a `true` value, store the `mgp_module::procedures` and // `mgp_module::transformations into `proc_map`. The return value of WithModuleRegistration // is the same as that of `fun`. Note, the return value need only be convertible to `bool`, // it does not have to be `bool` itself. template auto WithModuleRegistration(TProcMap *proc_map, TTransMap *trans_map, const TFun &fun) { // We probably don't need more than 256KB for module initialization. constexpr size_t stack_bytes = 256 * 1024; unsigned char stack_memory[stack_bytes]; utils::MonotonicBufferResource monotonic_memory(stack_memory, stack_bytes); mgp_memory memory{&monotonic_memory}; mgp_module module_def{memory.impl}; auto res = fun(&module_def, &memory); if (res) { // Copy procedures into resulting proc_map. for (const auto &proc : module_def.procedures) proc_map->emplace(proc); // Copy transformations into resulting trans_map. for (const auto &trans : module_def.transformations) trans_map->emplace(trans); } return res; } } // namespace class SharedLibraryModule final : public Module { public: SharedLibraryModule(); ~SharedLibraryModule() override; SharedLibraryModule(const SharedLibraryModule &) = delete; SharedLibraryModule(SharedLibraryModule &&) = delete; SharedLibraryModule &operator=(const SharedLibraryModule &) = delete; SharedLibraryModule &operator=(SharedLibraryModule &&) = delete; bool Load(const std::filesystem::path &file_path); bool Close() override; const std::map> *Procedures() const override; const std::map> *Transformations() const override; private: /// Path as requested for loading the module from a library. std::filesystem::path file_path_; /// System handle to shared library. void *handle_; /// Required initialization function called on module load. std::function init_fn_; /// Optional shutdown function called on module unload. std::function shutdown_fn_; /// Registered procedures std::map> procedures_; /// Registered transformations std::map> transformations_; }; SharedLibraryModule::SharedLibraryModule() : handle_(nullptr) {} SharedLibraryModule::~SharedLibraryModule() { if (handle_) Close(); } bool SharedLibraryModule::Load(const std::filesystem::path &file_path) { MG_ASSERT(!handle_, "Attempting to load an already loaded module..."); spdlog::info("Loading module {}...", file_path); file_path_ = file_path; dlerror(); // Clear any existing error. handle_ = dlopen(file_path.c_str(), RTLD_NOW | RTLD_LOCAL); if (!handle_) { spdlog::error("Unable to load module {}; {}", file_path, dlerror()); return false; } // Get required mgp_init_module init_fn_ = reinterpret_cast(dlsym(handle_, "mgp_init_module")); char *dl_errored = dlerror(); if (!init_fn_ || dl_errored) { spdlog::error("Unable to load module {}; {}", file_path, dl_errored); dlclose(handle_); handle_ = nullptr; return false; } auto module_cb = [&](auto *module_def, auto *memory) { // Run mgp_init_module which must succeed. int init_res = init_fn_(module_def, memory); auto with_error = [this](std::string_view error_msg) { spdlog::error(error_msg); dlclose(handle_); handle_ = nullptr; return false; }; if (init_res != 0) { const auto error = fmt::format("Unable to load module {}; mgp_init_module_returned {} ", file_path, init_res); return with_error(error); } for (auto &trans : module_def->transformations) { const bool was_result_added = MgpTransAddFixedResult(&trans.second); if (!was_result_added) { const auto error = fmt::format("Unable to add result to transformation in module {}; add result failed", file_path); return with_error(error); } } return true; }; if (!WithModuleRegistration(&procedures_, &transformations_, module_cb)) { return false; } // Get optional mgp_shutdown_module shutdown_fn_ = reinterpret_cast(dlsym(handle_, "mgp_shutdown_module")); dl_errored = dlerror(); if (dl_errored) spdlog::warn("When loading module {}; {}", file_path, dl_errored); spdlog::info("Loaded module {}", file_path); return true; } bool SharedLibraryModule::Close() { MG_ASSERT(handle_, "Attempting to close a module that has not been loaded..."); spdlog::info("Closing module {}...", file_path_); // non-existent shutdown function is semantically the same as a shutdown // function that does nothing. int shutdown_res = 0; if (shutdown_fn_) shutdown_res = shutdown_fn_(); if (shutdown_res != 0) { spdlog::warn("When closing module {}; mgp_shutdown_module returned {}", file_path_, shutdown_res); } if (dlclose(handle_) != 0) { spdlog::error("Failed to close module {}; {}", file_path_, dlerror()); return false; } spdlog::info("Closed module {}", file_path_); handle_ = nullptr; procedures_.clear(); return true; } const std::map> *SharedLibraryModule::Procedures() const { MG_ASSERT(handle_, "Attempting to access procedures of a module that has not " "been loaded..."); return &procedures_; } const std::map> *SharedLibraryModule::Transformations() const { MG_ASSERT(handle_, "Attempting to access procedures of a module that has not " "been loaded..."); return &transformations_; } class PythonModule final : public Module { public: PythonModule(); ~PythonModule() override; PythonModule(const PythonModule &) = delete; PythonModule(PythonModule &&) = delete; PythonModule &operator=(const PythonModule &) = delete; PythonModule &operator=(PythonModule &&) = delete; bool Load(const std::filesystem::path &file_path); bool Close() override; const std::map> *Procedures() const override; const std::map> *Transformations() const override; private: std::filesystem::path file_path_; py::Object py_module_; std::map> procedures_; std::map> transformations_; }; PythonModule::PythonModule() {} PythonModule::~PythonModule() { if (py_module_) Close(); } bool PythonModule::Load(const std::filesystem::path &file_path) { MG_ASSERT(!py_module_, "Attempting to load an already loaded module..."); spdlog::info("Loading module {}...", file_path); file_path_ = file_path; auto gil = py::EnsureGIL(); auto maybe_exc = py::AppendToSysPath(file_path.parent_path().c_str()); if (maybe_exc) { spdlog::error("Unable to load module {}; {}", file_path, *maybe_exc); return false; } bool succ = true; auto module_cb = [&](auto *module_def, auto * /*memory*/) { auto result = ImportPyModule(file_path.stem().c_str(), module_def); for (auto &trans : module_def->transformations) { succ = MgpTransAddFixedResult(&trans.second) == MGP_ERROR_NO_ERROR; if (!succ) { return result; } }; return result; }; py_module_ = WithModuleRegistration(&procedures_, &transformations_, module_cb); if (py_module_) { spdlog::info("Loaded module {}", file_path); if (!succ) { spdlog::error("Unable to add result to transformation"); return false; } return true; } auto exc_info = py::FetchError().value(); spdlog::error("Unable to load module {}; {}", file_path, exc_info); return false; } bool PythonModule::Close() { MG_ASSERT(py_module_, "Attempting to close a module that has not been loaded..."); spdlog::info("Closing module {}...", file_path_); // The procedures and transformations are closures which hold references to the Python callbacks. // Releasing these references might result in deallocations so we need to take the GIL. auto gil = py::EnsureGIL(); procedures_.clear(); transformations_.clear(); // Delete the module from the `sys.modules` directory so that the module will // be properly imported if imported again. py::Object sys(PyImport_ImportModule("sys")); if (PyDict_DelItemString(sys.GetAttr("modules").Ptr(), file_path_.stem().c_str()) != 0) { spdlog::warn("Failed to remove the module from sys.modules"); py_module_ = py::Object(nullptr); return false; } py_module_ = py::Object(nullptr); spdlog::info("Closed module {}", file_path_); return true; } const std::map> *PythonModule::Procedures() const { MG_ASSERT(py_module_, "Attempting to access procedures of a module that has " "not been loaded..."); return &procedures_; } const std::map> *PythonModule::Transformations() const { MG_ASSERT(py_module_, "Attempting to access procedures of a module that has " "not been loaded..."); return &transformations_; } namespace { std::unique_ptr LoadModuleFromFile(const std::filesystem::path &path) { const auto &ext = path.extension(); if (ext != ".so" && ext != ".py") { spdlog::warn("Unknown query module file {}", path); return nullptr; } std::unique_ptr module; if (path.extension() == ".so") { auto lib_module = std::make_unique(); if (!lib_module->Load(path)) return nullptr; module = std::move(lib_module); } else if (path.extension() == ".py") { auto py_module = std::make_unique(); if (!py_module->Load(path)) return nullptr; module = std::move(py_module); } return module; } } // namespace bool ModuleRegistry::RegisterModule(const std::string_view &name, std::unique_ptr module) { MG_ASSERT(!name.empty(), "Module name cannot be empty"); MG_ASSERT(module, "Tried to register an invalid module"); if (modules_.find(name) != modules_.end()) { spdlog::error("Unable to overwrite an already loaded module {}", name); return false; } modules_.emplace(name, std::move(module)); return true; } void ModuleRegistry::DoUnloadAllModules() { MG_ASSERT(modules_.find("mg") != modules_.end(), "Expected the builtin \"mg\" module to be present."); // This is correct because the destructor will close each module. However, // we don't want to unload the builtin "mg" module. auto module = std::move(modules_["mg"]); modules_.clear(); modules_.emplace("mg", std::move(module)); } ModuleRegistry::ModuleRegistry() { auto module = std::make_unique(); RegisterMgProcedures(&modules_, module.get()); RegisterMgTransformations(&modules_, module.get()); RegisterMgLoad(this, &lock_, module.get()); modules_.emplace("mg", std::move(module)); } void ModuleRegistry::SetModulesDirectory(std::vector modules_dirs) { modules_dirs_ = std::move(modules_dirs); } bool ModuleRegistry::LoadModuleIfFound(const std::filesystem::path &modules_dir, const std::string_view name) { if (!utils::DirExists(modules_dir)) { spdlog::error("Module directory {} doesn't exist", modules_dir); return false; } for (const auto &entry : std::filesystem::directory_iterator(modules_dir)) { const auto &path = entry.path(); if (entry.is_regular_file() && path.stem() == name) { auto module = LoadModuleFromFile(path); if (!module) return false; return RegisterModule(name, std::move(module)); } } return false; } bool ModuleRegistry::LoadOrReloadModuleFromName(const std::string_view name) { if (modules_dirs_.empty()) return false; if (name.empty()) return false; std::unique_lock guard(lock_); auto found_it = modules_.find(name); if (found_it != modules_.end()) { if (!found_it->second->Close()) { spdlog::warn("Failed to close module {}", found_it->first); } modules_.erase(found_it); } for (const auto &module_dir : modules_dirs_) { if (LoadModuleIfFound(module_dir, name)) { return true; } } return false; } void ModuleRegistry::LoadModulesFromDirectory(const std::filesystem::path &modules_dir) { if (modules_dir.empty()) return; if (!utils::DirExists(modules_dir)) { spdlog::error("Module directory {} doesn't exist", modules_dir); return; } for (const auto &entry : std::filesystem::directory_iterator(modules_dir)) { const auto &path = entry.path(); if (entry.is_regular_file()) { std::string name = path.stem(); if (name.empty()) continue; auto module = LoadModuleFromFile(path); if (!module) continue; RegisterModule(name, std::move(module)); } } } void ModuleRegistry::UnloadAndLoadModulesFromDirectories() { std::unique_lock guard(lock_); DoUnloadAllModules(); for (const auto &module_dir : modules_dirs_) { LoadModulesFromDirectory(module_dir); } } ModulePtr ModuleRegistry::GetModuleNamed(const std::string_view &name) const { std::shared_lock guard(lock_); auto found_it = modules_.find(name); if (found_it == modules_.end()) return nullptr; return ModulePtr(found_it->second.get(), std::move(guard)); } void ModuleRegistry::UnloadAllModules() { std::unique_lock guard(lock_); DoUnloadAllModules(); } utils::MemoryResource &ModuleRegistry::GetSharedMemoryResource() noexcept { return *shared_; } namespace { /// This function returns a pair of either // ModuleName | Prop /// 1. /// 2. std::optional> FindModuleNameAndProp( const ModuleRegistry &module_registry, std::string_view fully_qualified_name, utils::MemoryResource *memory) { utils::pmr::vector name_parts(memory); utils::Split(&name_parts, fully_qualified_name, "."); if (name_parts.size() == 1U) return std::nullopt; auto last_dot_pos = fully_qualified_name.find_last_of('.'); MG_ASSERT(last_dot_pos != std::string_view::npos); const auto &module_name = fully_qualified_name.substr(0, last_dot_pos); const auto &name = name_parts.back(); return std::make_pair(module_name, name); } template concept ModuleProperties = utils::SameAsAnyOf; template std::optional> MakePairIfPropFound(const ModuleRegistry &module_registry, std::string_view fully_qualified_name, utils::MemoryResource *memory) { auto prop_fun = [](auto &module) { if constexpr (std::is_same_v) { return module->Procedures(); } else { return module->Transformations(); } }; auto result = FindModuleNameAndProp(module_registry, fully_qualified_name, memory); if (!result) return std::nullopt; auto [module_name, prop_name] = *result; auto module = module_registry.GetModuleNamed(module_name); if (!module) return std::nullopt; auto *prop = prop_fun(module); const auto &prop_it = prop->find(prop_name); if (prop_it == prop->end()) return std::nullopt; return std::make_pair(std::move(module), &prop_it->second); } } // namespace std::optional> FindProcedure( const ModuleRegistry &module_registry, std::string_view fully_qualified_procedure_name, utils::MemoryResource *memory) { return MakePairIfPropFound(module_registry, fully_qualified_procedure_name, memory); } std::optional> FindTransformation( const ModuleRegistry &module_registry, std::string_view fully_qualified_transformation_name, utils::MemoryResource *memory) { return MakePairIfPropFound(module_registry, fully_qualified_transformation_name, memory); } } // namespace query::procedure