diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 61fd6b2..2dfcda3 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -44,6 +44,7 @@ jobs: -DENABLE_EXAMPLES=ON \ -DENABLE_BEDROCK=ON \ -DENABLE_MPI=ON \ + -DENABLE_PYTHON=ON \ -DCMAKE_BUILD_TYPE=Debug make make test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8886278..9490430 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,6 +52,7 @@ jobs: -DENABLE_EXAMPLES=ON \ -DENABLE_BEDROCK=ON \ -DENABLE_MPI=ON \ + -DENABLE_PYTHON=ON \ -DCMAKE_BUILD_TYPE=RelWithDebInfo make make test diff --git a/CMakeLists.txt b/CMakeLists.txt index 60bd3fd..a07ff11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,8 @@ # See COPYRIGHT in top-level directory. cmake_minimum_required (VERSION 3.8) project (flock C CXX) +set (CMAKE_CXX_STANDARD 17) +set (CMAKE_CXX_STANDARD_REQUIRED ON) enable_testing () add_definitions (-Wextra -Wall -Wpedantic) @@ -13,6 +15,7 @@ option (ENABLE_EXAMPLES "Build examples" OFF) option (ENABLE_BEDROCK "Build bedrock module" OFF) option (ENABLE_COVERAGE "Build with coverage" OFF) option (ENABLE_MPI "Build with MPI support" OFF) +option (ENABLE_PYTHON "Build with Python support" OFF) # library version set here (e.g. for shared libs). set (FLOCK_VERSION_MAJOR 0) @@ -62,11 +65,21 @@ endif () pkg_check_modules (margo REQUIRED IMPORTED_TARGET margo) # search for json-c pkg_check_modules (json-c REQUIRED IMPORTED_TARGET json-c) +# search for thallium +find_package (thallium REQUIRED) if (ENABLE_MPI) find_package (MPI REQUIRED) endif () +# search for Python +if (ENABLE_PYTHON) + set (Python3_FIND_STRATEGY LOCATION) + find_package (Python3 COMPONENTS Interpreter Development REQUIRED) + find_package (pybind11 REQUIRED) + add_subdirectory (python) +endif () + add_subdirectory (src) if (${ENABLE_TESTS}) enable_testing () diff --git a/build.sh b/build.sh index 92573ba..93061d6 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,4 @@ #!/bin/bash SCRIPT_DIR=$(dirname "$0") -cmake .. -DENABLE_TESTS=ON -DENABLE_BEDROCK=ON -DENABLE_EXAMPLES=ON -DENABLE_MPI=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +cmake .. -DENABLE_TESTS=ON -DENABLE_BEDROCK=ON -DENABLE_EXAMPLES=ON -DENABLE_MPI=ON -DENABLE_PYTHON=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=ON make diff --git a/include/flock/cxx/client.hpp b/include/flock/cxx/client.hpp new file mode 100644 index 0000000..91ad41c --- /dev/null +++ b/include/flock/cxx/client.hpp @@ -0,0 +1,91 @@ +/* + * (C) 2024 The University of Chicago + * + * See COPYRIGHT in top-level directory. + */ +#ifndef __FLOCK_CLIENT_HPP +#define __FLOCK_CLIENT_HPP + +#include +#include +#include +#include +#include + +namespace flock { + +class Client { + + public: + + Client() = default; + + Client(margo_instance_id mid, ABT_pool pool = ABT_POOL_NULL) + : m_engine{mid} { + auto err = flock_client_init(mid, pool, &m_client); + FLOCK_CONVERT_AND_THROW(err); + } + + Client(const thallium::engine& engine, thallium::pool pool = thallium::pool()) + : m_engine{engine} { + auto err = flock_client_init(engine.get_margo_instance(), pool.native_handle(), &m_client); + FLOCK_CONVERT_AND_THROW(err); + } + + ~Client() { + if(m_client != FLOCK_CLIENT_NULL) { + flock_client_finalize(m_client); + } + } + + Client(Client&& other) + : m_client(other.m_client) { + other.m_client = FLOCK_CLIENT_NULL; + } + + Client(const Client&) = delete; + + Client& operator=(const Client&) = delete; + + Client& operator=(Client&& other) { + if(this == &other) return *this; + if(m_client != FLOCK_CLIENT_NULL) { + flock_client_finalize(m_client); + } + m_client = other.m_client; + other.m_client = FLOCK_CLIENT_NULL; + return *this; + } + + GroupHandle makeGroupHandle( + hg_addr_t addr, + uint16_t provider_id, + uint32_t mode = 0) const { + flock_group_handle_t gh; + auto err = flock_group_handle_create( + m_client, addr, provider_id, mode, &gh); + FLOCK_CONVERT_AND_THROW(err); + return GroupHandle(gh, false); + } + + auto handle() const { + return m_client; + } + + operator flock_client_t() const { + return m_client; + } + + auto engine() const { + return m_engine; + } + + private: + + thallium::engine m_engine; + flock_client_t m_client = FLOCK_CLIENT_NULL; +}; + +} + +#endif diff --git a/include/flock/cxx/exception.hpp b/include/flock/cxx/exception.hpp new file mode 100644 index 0000000..f80973e --- /dev/null +++ b/include/flock/cxx/exception.hpp @@ -0,0 +1,46 @@ +/* + * (C) 2024 The University of Chicago + * + * See COPYRIGHT in top-level directory. + */ +#ifndef __FLOCK_EXCEPTION_HPP +#define __FLOCK_EXCEPTION_HPP + +#include +#include + +namespace flock { + +class Exception : public std::exception { + + public: + + Exception(flock_return_t code) + : m_code(code) {} + + const char* what() const noexcept override { + #define X(__err__, __msg__) case __err__: return __msg__; + switch(m_code) { + FLOCK_RETURN_VALUES + } + #undef X + return "Unknown error"; + } + + auto code() const { + return m_code; + } + + private: + + flock_return_t m_code; +}; + +#define FLOCK_CONVERT_AND_THROW(__err__) do { \ + if((__err__) != FLOCK_SUCCESS) { \ + throw ::flock::Exception((__err__)); \ + } \ +} while(0) + +} +#endif diff --git a/include/flock/cxx/group-view.hpp b/include/flock/cxx/group-view.hpp new file mode 100644 index 0000000..44a9c8d --- /dev/null +++ b/include/flock/cxx/group-view.hpp @@ -0,0 +1,201 @@ +/* + * (C) 2024 The University of Chicago + * + * See COPYRIGHT in top-level directory. + */ +#ifndef __FLOCK_GROUP_VIEW_HPP +#define __FLOCK_GROUP_VIEW_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace flock { + +class GroupHandle; +class Provider; + +class GroupView { + + friend class GroupHandle; + friend class Provider; + + public: + + struct Member { + std::string address; + uint16_t provider_id; + }; + + struct Metadata { + std::string key; + std::string value; + }; + + struct MembersProxy { + + private: + + friend class GroupView; + + GroupView& m_owner; + + MembersProxy(GroupView& gv) + : m_owner(gv) {} + + public: + + MembersProxy(MembersProxy&&) = default; + + void add(const char* address, uint16_t provider_id) { + flock_group_view_add_member(&m_owner.m_view, address, provider_id); + } + + void remove(size_t index) { + auto member = flock_group_view_member_at(&m_owner.m_view, index); + if(!flock_group_view_remove_member(&m_owner.m_view, member)) + throw Exception{FLOCK_ERR_NO_MEMBER}; + } + + void remove(const char* address, uint16_t provider_id) { + auto member = flock_group_view_find_member(&m_owner.m_view, address, provider_id); + if(!flock_group_view_remove_member(&m_owner.m_view, member)) + throw Exception{FLOCK_ERR_NO_MEMBER}; + } + + bool exists(const char* address, uint16_t provider_id) const { + return static_cast(flock_group_view_find_member( + const_cast(&m_owner.m_view), address, provider_id)); + } + + size_t count() const { + return flock_group_view_member_count(&m_owner.m_view); + } + + Member operator[](size_t i) const { + if(i >= count()) throw std::out_of_range{"Invalid member index"}; + auto member = flock_group_view_member_at(&m_owner.m_view, i); + return Member{member->address, member->provider_id}; + } + }; + + struct MetadataProxy { + + private: + + friend class GroupView; + + GroupView& m_owner; + + MetadataProxy(GroupView& gv) + : m_owner(gv) {} + + public: + + MetadataProxy(MetadataProxy&&) = default; + + void add(const char* key, const char* value) { + flock_group_view_add_metadata(&m_owner.m_view, key, value); + } + + void remove(const char* key) { + flock_group_view_remove_metadata(&m_owner.m_view, key); + } + + size_t count() const { + return flock_group_view_metadata_count(&m_owner.m_view); + } + + Metadata operator[](size_t i) const { + if(i >= count()) throw std::out_of_range{"Invalid metadata index"}; + auto metadata = flock_group_view_metadata_at(&m_owner.m_view, i); + return Metadata{metadata->key, metadata->value}; + } + + const char* operator[](const char* key) const { + return flock_group_view_find_metadata(&m_owner.m_view, key); + } + }; + + GroupView() = default; + + GroupView(flock_group_view_t view) { + FLOCK_GROUP_VIEW_MOVE(&view, &m_view); + } + + ~GroupView() { + clear(); + } + + GroupView(GroupView&& other) { + FLOCK_GROUP_VIEW_MOVE(&other.m_view, &m_view); + } + + GroupView& operator=(GroupView&& other) { + if(this == &other) return *this; + clear(); + FLOCK_GROUP_VIEW_MOVE(&other.m_view, &m_view); + return *this; + } + + auto digest() const { + return m_view.digest; + } + + void lock() { + FLOCK_GROUP_VIEW_LOCK(&m_view); + } + + void unlock() { + FLOCK_GROUP_VIEW_UNLOCK(&m_view); + } + + void clear() { + flock_group_view_clear(&m_view); + } + + auto members() { + return MembersProxy{*this}; + } + + auto metadata() { + return MetadataProxy{*this}; + } + + auto copy() const { + auto result = GroupView{}; + auto members = const_cast(this)->members(); + for(size_t i = 0; i < members.count(); ++i) { + result.members().add(members[i].address.c_str(), members[i].provider_id); + } + auto metadata = const_cast(this)->metadata(); + for(size_t i = 0; i < metadata.count(); ++i) { + result.metadata().add(metadata[i].key.c_str(), metadata[i].value.c_str()); + } + return result; + } + + operator std::string() const { + std::string result; + flock_return_t ret = flock_group_view_serialize( + &m_view, [](void* ctx, const char* content, size_t size) { + auto str = static_cast(ctx); + str->assign(content, size); + }, static_cast(&result)); + if(ret != FLOCK_SUCCESS) throw Exception{ret}; + return result; + } + + private: + + flock_group_view_t m_view = FLOCK_GROUP_VIEW_INITIALIZER; +}; + +} + +#endif diff --git a/include/flock/cxx/group.hpp b/include/flock/cxx/group.hpp new file mode 100644 index 0000000..8182599 --- /dev/null +++ b/include/flock/cxx/group.hpp @@ -0,0 +1,142 @@ +/* + * (C) 2024 The University of Chicago + * + * See COPYRIGHT in top-level directory. + */ +#ifndef __FLOCK_GROUP_HPP +#define __FLOCK_GROUP_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace flock { + +class Client; + +class GroupHandle { + + friend class Client; + + public: + + GroupHandle() = default; + + GroupHandle(flock_group_handle_t gh, bool copy=true) + : m_gh(gh) { + if(copy && (m_gh != FLOCK_GROUP_HANDLE_NULL)) { + auto err = flock_group_handle_ref_incr(m_gh); + FLOCK_CONVERT_AND_THROW(err); + } + } + + GroupHandle(flock_client_t client, + hg_addr_t addr, + uint16_t provider_id, + bool check = true) + { + auto err = flock_group_handle_create(client, + addr, provider_id, check, &m_gh); + FLOCK_CONVERT_AND_THROW(err); + } + + GroupHandle(const GroupHandle& other) + : m_gh(other.m_gh) { + if(m_gh != FLOCK_GROUP_HANDLE_NULL) { + auto err = flock_group_handle_ref_incr(m_gh); + FLOCK_CONVERT_AND_THROW(err); + } + } + + GroupHandle(GroupHandle&& other) + : m_gh(other.m_gh) { + other.m_gh = FLOCK_GROUP_HANDLE_NULL; + } + + GroupHandle& operator=(const GroupHandle& other) { + if(m_gh == other.m_gh || &other == this) + return *this; + if(m_gh != FLOCK_GROUP_HANDLE_NULL) { + auto err = flock_group_handle_release(m_gh); + FLOCK_CONVERT_AND_THROW(err); + } + m_gh = other.m_gh; + if(m_gh != FLOCK_GROUP_HANDLE_NULL) { + auto err = flock_group_handle_ref_incr(m_gh); + FLOCK_CONVERT_AND_THROW(err); + } + return *this; + } + + GroupHandle& operator=(GroupHandle&& other) { + if(m_gh == other.m_gh || &other == this) + return *this; + if(m_gh != FLOCK_GROUP_HANDLE_NULL) { + auto err = flock_group_handle_release(m_gh); + FLOCK_CONVERT_AND_THROW(err); + } + m_gh = other.m_gh; + other.m_gh = FLOCK_GROUP_HANDLE_NULL; + return *this; + } + + ~GroupHandle() { + if(m_gh != FLOCK_GROUP_HANDLE_NULL) { + flock_group_handle_release(m_gh); + } + } + + static GroupHandle FromFile( + flock_client_t client, + const char* filename, + uint32_t mode = 0) { + flock_group_handle_t gh; + auto err = flock_group_handle_create_from_file(client, filename, mode, &gh); + FLOCK_CONVERT_AND_THROW(err); + return GroupHandle{gh}; + } + + static GroupHandle FromSerialized( + flock_client_t client, + std::string_view serialized_view, + uint32_t mode = 0) { + flock_group_handle_t gh; + auto err = flock_group_handle_create_from_serialized( + client, serialized_view.data(), serialized_view.size(), mode, &gh); + FLOCK_CONVERT_AND_THROW(err); + return GroupHandle{gh}; + } + + GroupView view() const { + GroupView v; + auto err = flock_group_get_view(m_gh, &v.m_view); + FLOCK_CONVERT_AND_THROW(err); + return v; + } + + void update() const { + auto err = flock_group_update_view(m_gh, NULL); + FLOCK_CONVERT_AND_THROW(err); + } + + auto handle() const { + return m_gh; + } + + operator flock_group_handle_t() const { + return m_gh; + } + + private: + + flock_group_handle_t m_gh = FLOCK_GROUP_HANDLE_NULL; + +}; + +} + +#endif diff --git a/include/flock/cxx/server.hpp b/include/flock/cxx/server.hpp new file mode 100644 index 0000000..5dd9559 --- /dev/null +++ b/include/flock/cxx/server.hpp @@ -0,0 +1,153 @@ +/* + * (C) 2024 The University of Chicago + * + * See COPYRIGHT in top-level directory. + */ +#ifndef __FLOCK_CLIENT_HPP +#define __FLOCK_CLIENT_HPP + +#include +#include +#include +#include +#include + +namespace flock { + +class Observer { + + public: + + Observer(const Observer&) = delete; + Observer(Observer&&) = delete; + + virtual ~Observer() = default; + + virtual void onMembershipUpdate( + flock_update_t update, + const char* address, + uint16_t provider_id) = 0; + + virtual void onMetadataUpdate( + const char* key, + const char* value) = 0; + +}; + +class Provider { + + public: + + Provider() = default; + + Provider(margo_instance_id mid, + uint16_t provider_id, + const char* config, + GroupView& initial_view, + ABT_pool pool = ABT_POOL_NULL) { + m_mid = mid; + flock_provider_args args; + args.backend = nullptr; + args.initial_view = &initial_view.m_view; + args.pool = pool; + auto err = flock_provider_register(mid, provider_id, config, &args, &m_provider); + FLOCK_CONVERT_AND_THROW(err); + margo_provider_push_finalize_callback( + mid, this, [](void* ctx) { + auto self = static_cast(ctx); + self->m_provider = FLOCK_PROVIDER_NULL; + }, this); + } + + Provider(const thallium::engine& engine, + uint16_t provider_id, + const char* config, + GroupView& initial_view, + const thallium::pool& pool = thallium::pool()) + : Provider(engine.get_margo_instance(), + provider_id, + config, + initial_view, + pool.native_handle()) {} + + ~Provider() { + if(m_provider != FLOCK_PROVIDER_NULL) { + margo_provider_pop_finalize_callback(m_mid, this); + flock_provider_destroy(m_provider); + } + } + + Provider(Provider&& other) + : m_provider(other.m_provider) { + margo_provider_pop_finalize_callback(other.m_mid, &other); + other.m_provider = FLOCK_PROVIDER_NULL; + margo_provider_push_finalize_callback( + m_mid, this, [](void* ctx) { + auto self = static_cast(ctx); + self->m_provider = FLOCK_PROVIDER_NULL; + }, this); + } + + Provider(const Provider&) = delete; + + Provider& operator=(const Provider&) = delete; + + Provider& operator=(Provider&& other) { + if(this == &other) return *this; + this->~Provider(); + m_provider = other.m_provider; + other.m_provider = FLOCK_PROVIDER_NULL; + margo_provider_pop_finalize_callback(other.m_mid, &other); + other.m_provider = FLOCK_PROVIDER_NULL; + margo_provider_push_finalize_callback( + m_mid, this, [](void* ctx) { + auto self = static_cast(ctx); + self->m_provider = FLOCK_PROVIDER_NULL; + }, this); + return *this; + } + + void addObserver(Observer* observer) { + auto membership_update_fn = + [](void* ctx, flock_update_t update, const char* address, uint16_t provider_id) { + static_cast(ctx)->onMembershipUpdate(update, address, provider_id); + }; + auto metadata_update_fn = + [](void* ctx, const char* key, const char* value) { + static_cast(ctx)->onMetadataUpdate(key, value); + }; + auto err = flock_provider_add_update_callbacks( + m_provider, membership_update_fn, metadata_update_fn, observer); + FLOCK_CONVERT_AND_THROW(err); + } + + void removeObserver(Observer* observer) { + auto err = flock_provider_remove_update_callbacks(m_provider, observer); + FLOCK_CONVERT_AND_THROW(err); + } + + std::string config() const { + auto cfg = flock_provider_get_config(m_provider); + if(!cfg) throw Exception{FLOCK_ERR_OTHER}; + auto result = std::string{cfg}; + free(cfg); + return result; + } + + auto handle() const { + return m_provider; + } + + operator flock_provider_t() const { + return handle(); + } + + private: + + margo_instance_id m_mid = MARGO_INSTANCE_NULL; + flock_provider_t m_provider = FLOCK_PROVIDER_NULL; +}; + +} + +#endif diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt new file mode 100644 index 0000000..90505ef --- /dev/null +++ b/python/CMakeLists.txt @@ -0,0 +1,24 @@ +add_library (pyflock_client MODULE src/py-flock-client.cpp) +target_link_libraries (pyflock_client PRIVATE pybind11::module flock-cxx-headers PRIVATE coverage_config) +pybind11_extension (pyflock_client) +pybind11_strip (pyflock_client) + +add_library (pyflock_server MODULE src/py-flock-server.cpp) +target_link_libraries (pyflock_server PRIVATE pybind11::module flock-cxx-headers PRIVATE coverage_config) +pybind11_extension (pyflock_server) +pybind11_strip (pyflock_server) + +add_library (pyflock_common MODULE src/py-flock-common.cpp) +target_link_libraries (pyflock_common PRIVATE pybind11::module flock-cxx-headers PRIVATE coverage_config) +pybind11_extension (pyflock_common) +pybind11_strip (pyflock_common) + +set (PY_VERSION ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}) + +install (TARGETS pyflock_client pyflock_server pyflock_common + EXPORT flock-targets + ARCHIVE DESTINATION lib/python${PY_VERSION}/site-packages + LIBRARY DESTINATION lib/python${PY_VERSION}/site-packages) + +install (DIRECTORY mochi/bedrock + DESTINATION lib/python${PY_VERSION}/site-packages/mochi) diff --git a/python/mochi/flock/__init__.py b/python/mochi/flock/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/mochi/flock/client.py b/python/mochi/flock/client.py new file mode 100644 index 0000000..05170a5 --- /dev/null +++ b/python/mochi/flock/client.py @@ -0,0 +1,82 @@ +# (C) 2024 The University of Chicago +# See COPYRIGHT in top-level directory. + + +""" +.. module:: client + :synopsis: This package provides access to the Flock C++ wrapper + +.. moduleauthor:: Matthieu Dorier + + +""" + + +import pyflock_common +import pyflock_client +import pymargo.core +import pymargo + + +class GroupHandle: + + def __init__(self, internal, client): + self._internal = internal + self._client = client + + @property + def client(self): + return self._client + + def update(self): + self._internal.update() + + @property + def view(self): + return self._internal.view + + +class Client: + + def __init__(self, arg): + if isinstance(arg, pymargo.core.Engine): + self._engine = arg + self._owns_engine = False + elif isinstance(arg, str): + self._engine = pymargo.core.Engine(arg, pymargo.client) + self._owns_engine = True + else: + raise TypeError(f'Invalid argument type {type(arg)}') + self._internal = pyflock_client.Client(self._engine.get_internal_mid()) + + def __del__(self): + del self._internal + if self._owns_engine: + self._engine.finalize() + del self._engine + + @property + def mid(self): + return self._internal.margo_instance_id + + @property + def engine(self): + return self._engine + + def make_group_handle(self, address: str|pymargo.core.Address, provider_id: int = 0): + if isinstance(address, pymargo.core.Address): + address = str(address) + return GroupHandle( + self._internal.make_group_handle(address=address, provider_id=provider_id), + self) + + def make_group_handle_from_file(self, filename: str): + return GroupHandle( + self._internal.make_group_handle_from_file(filename=filename), + self) + + def make_group_handle_from_serialized(self, serialized: str): + return GroupHandle( + self._internal.make_group_handle_from_serialized(serialized=serialized), + self) + diff --git a/python/mochi/flock/common.py b/python/mochi/flock/common.py new file mode 100644 index 0000000..5563936 --- /dev/null +++ b/python/mochi/flock/common.py @@ -0,0 +1,18 @@ +# (C) 2024 The University of Chicago +# See COPYRIGHT in top-level directory. + + +""" +.. module:: view + :synopsis: This package provides access to the Flock C++ wrapper + +.. moduleauthor:: Matthieu Dorier + + +""" + + +import pyflock_common + + +FlockException = pyflock_common.Exception diff --git a/python/mochi/flock/server.py b/python/mochi/flock/server.py new file mode 100644 index 0000000..0d60f39 --- /dev/null +++ b/python/mochi/flock/server.py @@ -0,0 +1,26 @@ +# (C) 2024 The University of Chicago +# See COPYRIGHT in top-level directory. + + +""" +.. module:: server + :synopsis: This package provides access to the Flock C++ wrapper + +.. moduleauthor:: Matthieu Dorier + + +""" + + +import pyflock_common +from .view import GroupView +import pyflock_server +import pymargo.core +import pymargo + + +class Provider: + + def __init__(self, engine: pymargo.core.Engine, provider_id: int, config: str, initial_view: GroupView): + self._internal = pyflock_server.Provider( + engine.get_internal_mid(), provider_id, config, initial_view) diff --git a/python/mochi/flock/test_client.py b/python/mochi/flock/test_client.py new file mode 100644 index 0000000..6f316cf --- /dev/null +++ b/python/mochi/flock/test_client.py @@ -0,0 +1,18 @@ +import unittest +import mochi.flock.client as mfc +from pymargo.core import Engine + + +class TestClient(unittest.TestCase): + + def test_init_client_from_address(self): + client = mfc.Client("na+sm") + + def test_init_client_from_engine(self): + with Engine("na+sm") as engine: + client = mfc.Client(engine) + del client + + +if __name__ == '__main__': + unittest.main() diff --git a/python/mochi/flock/test_group_handle.py b/python/mochi/flock/test_group_handle.py new file mode 100644 index 0000000..d10850c --- /dev/null +++ b/python/mochi/flock/test_group_handle.py @@ -0,0 +1,53 @@ +import unittest +import json +import mochi.flock.client as mfc +import mochi.flock.server as mfs +from mochi.flock.view import GroupView +import pymargo.core + +class TestClient(unittest.TestCase): + + def setUp(self): + self.engine = pymargo.core.Engine("na+sm", pymargo.core.server) + self.address = str(self.engine.address) + config = { + "group": { + "type": "static", + "config": {} + } + } + self.initial_view = GroupView() + for i in range(0, 5): + self.initial_view.members.add(self.address, i) + self.initial_view.metadata.add("mykey", "myvalue") + self.providers = [] + for i in range(0, 5): + self.providers.append( + mfs.Provider(self.engine, i, json.dumps(config), self.initial_view.copy())) + self.client = mfc.Client(self.engine) + + def tearDown(self): + del self.client + del self.providers + self.engine.finalize() + del self.engine + + def test_view(self): + gh = self.client.make_group_handle(self.address, 3) + gh.update() + view = gh.view + self.assertIsInstance(view, GroupView) + self.assertEqual(len(view.members), 5) + for i in range(0, 5): + self.assertEqual(view.members[i].address, self.address) + self.assertEqual(view.members[i].provider_id, i) + self.assertEqual(view.metadata["mykey"], "myvalue") + print(view) + + def test_update(self): + gh = self.client.make_group_handle(self.address, 3) + gh.update() + + +if __name__ == '__main__': + unittest.main() diff --git a/python/mochi/flock/test_provider.py b/python/mochi/flock/test_provider.py new file mode 100644 index 0000000..d6316bb --- /dev/null +++ b/python/mochi/flock/test_provider.py @@ -0,0 +1,26 @@ +import unittest +import json +import mochi.flock.server as mfs +import mochi.flock.view as view +import pymargo.core + +class TestClient(unittest.TestCase): + + def test_init_provider(self): + with pymargo.core.Engine("na+sm", pymargo.core.server) as engine: + address = str(engine.address) + config = { + "group": { + "type": "static", + "config": {} + } + } + initial_view = view.GroupView() + initial_view.members.add(address, 42) + provider = mfs.Provider(engine, 42, json.dumps(config), initial_view) + del provider + engine.finalize() + + +if __name__ == '__main__': + unittest.main() diff --git a/python/mochi/flock/test_view.py b/python/mochi/flock/test_view.py new file mode 100644 index 0000000..07af987 --- /dev/null +++ b/python/mochi/flock/test_view.py @@ -0,0 +1,103 @@ +import unittest +import json +from mochi.flock.view import GroupView + + +class TestGroupView(unittest.TestCase): + + def test_init(self): + view = GroupView() + + def test_members(self): + view = GroupView() + # add 5 members + for i in range(0, 5): + view.members.add( + address=f"address{i}", + provider_id=i) + # check that the count is now 5 + self.assertEqual(len(view.members), 5) + self.assertEqual(view.members.count, 5) + # check the members + for i in range(0, 5): + assert view.members.exists(f"address{i}", i) + assert not view.members.exists(f"address{i+1}", i) + assert not view.members.exists(f"address{i}", i+1) + self.assertEqual(view.members[i].address, f"address{i}") + self.assertEqual(view.members[i].provider_id, i) + # erase member 3 via __delitem__ + del view.members[3] + self.assertEqual(view.members.count, 4) + assert not view.members.exists("address3", 3) + # erase member 2 via remove(index) + view.members.remove(2) + self.assertEqual(view.members.count, 3) + assert not view.members.exists("address2", 2) + # erase member 1 via remove(address, provider_id) + view.members.remove("address1", 1) + self.assertEqual(view.members.count, 2) + assert not view.members.exists("address1", 1) + + def test_metadata(self): + view = GroupView() + # add 5 metadata + for i in range(0, 5): + view.metadata.add( + key=f"key{i}", + value=f"value{i}") + # check that the count is now 5 + self.assertEqual(len(view.metadata), 5) + self.assertEqual(view.metadata.count, 5) + # check the metadata + for i in range(0, 5): + self.assertEqual(view.metadata[f"key{i}"], f"value{i}") + self.assertEqual(view.metadata[i].key, f"key{i}") + self.assertEqual(view.metadata[i].value, f"value{i}") + # erase key3 via __delitem__ + del view.metadata["key3"] + self.assertEqual(view.metadata.count, 4) + self.assertIsNone(view.metadata["key3"]) + # erase key2 via remove(key) + view.metadata.remove("key2") + self.assertEqual(view.metadata.count, 3) + self.assertIsNone(view.metadata["key2"]) + + def test_str(self): + view = GroupView() + # add 5 metadata + for i in range(0, 5): + view.metadata.add( + key=f"key{i}", + value=f"value{i}") + view = GroupView() + # add 5 members + for i in range(0, 5): + view.members.add( + address=f"address{i}", + provider_id=i) + v = json.loads(str(view)) + self.assertIn("members", v) + self.assertIn("metadata", v) + self.assertIsInstance(v["members"], list) + self.assertIsInstance(v["metadata"], dict) + for i, member in enumerate(v["members"]): + self.assertEqual(member["address"], f"address{i}") + self.assertEqual(member["provider_id"], i) + i = 0 + for key, val in v["metadata"]: + self.assertEqual(key, f"key{i}") + self.assertEqual(val, f"value{i}") + i = i + 1 + + def test_digest(self): + view = GroupView() + self.assertEqual(view.digest, 0) + view.members.add("address", 1) + self.assertNotEqual(view.digest, 0) + d = view.digest + view.metadata.add("key", "value") + self.assertNotEqual(view.digest, 0) + self.assertNotEqual(view.digest, d) + +if __name__ == '__main__': + unittest.main() diff --git a/python/mochi/flock/view.py b/python/mochi/flock/view.py new file mode 100644 index 0000000..bc261bc --- /dev/null +++ b/python/mochi/flock/view.py @@ -0,0 +1,18 @@ +# (C) 2024 The University of Chicago +# See COPYRIGHT in top-level directory. + + +""" +.. module:: view + :synopsis: This package provides access to the Flock C++ wrapper + +.. moduleauthor:: Matthieu Dorier + + +""" + + +import pyflock_common + + +GroupView = pyflock_common.GroupView diff --git a/python/src/py-flock-client.cpp b/python/src/py-flock-client.cpp new file mode 100644 index 0000000..bffb2cd --- /dev/null +++ b/python/src/py-flock-client.cpp @@ -0,0 +1,61 @@ +/* + * (C) 2018 The University of Chicago + * + * See COPYRIGHT in top-level directory. + */ +#include +#include +#include +#include +#include + +namespace py11 = pybind11; +using namespace pybind11::literals; + +struct margo_instance; +typedef struct margo_instance* margo_instance_id; +typedef py11::capsule pymargo_instance_id; + +#define MID2CAPSULE(__mid) py11::capsule((void*)(__mid), "margo_instance_id") +#define CAPSULE2MID(__caps) (margo_instance_id)(__caps) + +PYBIND11_MODULE(pyflock_client, m) { + m.doc() = "Flock client python extension"; + py11::module_::import("pyflock_common"); + + py11::class_(m, "Client") + .def(py11::init()) + .def_property_readonly("margo_instance_id", + [](const flock::Client& client) { + return MID2CAPSULE(client.engine().get_margo_instance()); + }) + .def("make_group_handle", + [](const flock::Client& client, + const std::string& address, + uint16_t provider_id) { + auto addr = client.engine().lookup(address); + return client.makeGroupHandle(addr.get_addr(), provider_id); + }, + "Create a GroupHandle instance", + "address"_a, + "provider_id"_a=0) + .def("make_group_handle_from_file", + [](const flock::Client& client, + const std::string& filename) { + return flock::GroupHandle::FromFile(client, filename.c_str()); + }, + "Create a GroupHandle instance", + "filename"_a) + .def("make_group_handle_from_serialized", + [](const flock::Client& client, + std::string_view serialized) { + return flock::GroupHandle::FromSerialized(client, serialized); + }, + "Create a GroupHandle instance", + "serialized"_a) + ; + py11::class_(m, "GroupHandle") + .def("update", &flock::GroupHandle::update) + .def_property_readonly("view", &flock::GroupHandle::view) + ; +} diff --git a/python/src/py-flock-common.cpp b/python/src/py-flock-common.cpp new file mode 100644 index 0000000..1b3a8a7 --- /dev/null +++ b/python/src/py-flock-common.cpp @@ -0,0 +1,83 @@ +/* + * (C) 2018 The University of Chicago + * + * See COPYRIGHT in top-level directory. + */ +#include +#include +#include +#include + +namespace py11 = pybind11; +using namespace pybind11::literals; + +struct margo_instance; +typedef struct margo_instance* margo_instance_id; +typedef py11::capsule pymargo_instance_id; + +#define MID2CAPSULE(__mid) py11::capsule((void*)(__mid), "margo_instance_id") +#define CAPSULE2MID(__caps) (margo_instance_id)(__caps) + +PYBIND11_MODULE(pyflock_common, m) { + m.doc() = "Flock common python extension"; + py11::register_exception(m, "Exception", PyExc_RuntimeError); + + py11::class_(m, "Member") + .def_readonly("provider_id", &flock::GroupView::Member::provider_id) + .def_readonly("address", &flock::GroupView::Member::address); + + py11::class_(m, "Metadata") + .def_readonly("key", &flock::GroupView::Metadata::key) + .def_readonly("value", &flock::GroupView::Metadata::value); + + py11::class_(m, "MembersProxy") + .def("__len__", &flock::GroupView::MembersProxy::count) + .def_property_readonly("count", &flock::GroupView::MembersProxy::count) + .def("add", &flock::GroupView::MembersProxy::add, + "address"_a, "provider_id"_a) + .def("remove", [](flock::GroupView::MembersProxy& proxy, size_t i) { + proxy.remove(i); + }, "index"_a) + .def("remove", [](flock::GroupView::MembersProxy& proxy, const char* address, uint16_t provider_id) { + proxy.remove(address, provider_id); + }, "address"_a, "provider_id"_a) + .def("exists", &flock::GroupView::MembersProxy::exists, + "address"_a, "provider_id"_a) + .def("__getitem__", &flock::GroupView::MembersProxy::operator[], + "index"_a) + .def("__delitem__", [](flock::GroupView::MembersProxy& proxy, size_t i) { + proxy.remove(i); + }, "index"_a) + ; + + py11::class_(m, "MetadataProxy") + .def("__len__", &flock::GroupView::MetadataProxy::count) + .def_property_readonly("count", &flock::GroupView::MetadataProxy::count) + .def("add", &flock::GroupView::MetadataProxy::add, + "key"_a, "value"_a) + .def("remove", &flock::GroupView::MetadataProxy::remove, + "key"_a) + .def("__getitem__", [](flock::GroupView::MetadataProxy& proxy, const std::string& key) { + return proxy[key.c_str()]; + }, "key"_a) + .def("__getitem__", [](flock::GroupView::MetadataProxy& proxy, size_t index) { + return proxy[index]; + }, "index"_a) + .def("__delitem__", &flock::GroupView::MetadataProxy::remove, + "key"_a) + ; + + py11::class_(m, "GroupView") + .def(py11::init<>()) + .def_property_readonly("digest", &flock::GroupView::digest) + .def("clear", &flock::GroupView::clear) + .def("lock", &flock::GroupView::lock) + .def("unlock", &flock::GroupView::unlock) + .def_property_readonly("members", &flock::GroupView::members, py11::keep_alive<0, 1>()) + .def_property_readonly("metadata", &flock::GroupView::metadata, py11::keep_alive<0, 1>()) + .def("__str__", [](const flock::GroupView& gv) { + return static_cast(gv); + }) + .def("copy", &flock::GroupView::copy) + ; +} diff --git a/python/src/py-flock-server.cpp b/python/src/py-flock-server.cpp new file mode 100644 index 0000000..d518cb6 --- /dev/null +++ b/python/src/py-flock-server.cpp @@ -0,0 +1,28 @@ +/* + * (C) 2018 The University of Chicago + * + * See COPYRIGHT in top-level directory. + */ +#include +#include +#include + +namespace py11 = pybind11; +using namespace pybind11::literals; + +struct margo_instance; +typedef struct margo_instance* margo_instance_id; +typedef struct hg_addr* hg_addr_t; +typedef py11::capsule pymargo_instance_id; +typedef py11::capsule pyhg_addr_t; + +#define MID2CAPSULE(__mid) py11::capsule((void*)(__mid), "margo_instance_id") +#define CAPSULE2MID(__caps) (margo_instance_id)(__caps) + +PYBIND11_MODULE(pyflock_server, m) { + m.doc() = "Flock server python extension"; + + py11::class_(m, "Provider") + .def(py11::init()) + ; +} diff --git a/spack.yaml b/spack.yaml index 9f4a121..0f9d184 100644 --- a/spack.yaml +++ b/spack.yaml @@ -3,8 +3,11 @@ spack: - cmake - pkgconfig - mochi-margo + - mochi-thallium - json-c - mochi-bedrock + - py-pybind11 + - py-mochi-margo - mpi concretizer: unify: true diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8524c2c..9396032 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -76,6 +76,12 @@ set_target_properties (flock-bedrock-module SOVERSION ${FLOCK_VERSION_MAJOR}) endif () +add_library (flock-cxx-headers INTERFACE) +add_library (flock::headers ALIAS flock-cxx-headers) +target_link_libraries (flock-cxx-headers + INTERFACE PkgConfig::margo thallium flock-client flock-server) +target_include_directories (flock-cxx-headers INTERFACE $) + # installation stuff (packaging and install commands) write_basic_package_version_file ( "flock-config-version.cmake" @@ -101,7 +107,7 @@ configure_file ("flock-client.pc.in" "flock-client.pc" @ONLY) configure_file ("config.h.in" "config.h" @ONLY) # "make install" rules -install (TARGETS flock-server flock-client +install (TARGETS flock-server flock-client flock-cxx-headers EXPORT flock-targets ARCHIVE DESTINATION lib LIBRARY DESTINATION lib) diff --git a/src/provider.c b/src/provider.c index 53e2d04..f9f30c0 100644 --- a/src/provider.c +++ b/src/provider.c @@ -74,7 +74,8 @@ flock_return_t flock_provider_register( .initial_view = FLOCK_GROUP_VIEW_INITIALIZER, .callback_context = NULL, .member_update_callback = dispatch_member_update, - .metadata_update_callback = dispatch_metadata_update + .metadata_update_callback = dispatch_metadata_update, + .join = false }; FLOCK_GROUP_VIEW_MOVE(a.initial_view, &backend_init_args.initial_view); @@ -221,17 +222,17 @@ flock_return_t flock_provider_register( goto finish; // LCOV_EXCL_STOP } - backend_init_args.join = true; bool is_first = false; - for(size_t i = 0; i < backend_init_args.initial_view.members.size; ++i) { - flock_member_t* member = &backend_init_args.initial_view.members.data[i]; - if(member->provider_id != provider_id) continue; - if(strcmp(member->address, self_addr_str) != 0) continue; - backend_init_args.join = false; - is_first = i == 0; - break; + flock_member_t* mem = flock_group_view_find_member( + &backend_init_args.initial_view, self_addr_str, provider_id); + if(mem) { + if(mem == backend_init_args.initial_view.members.data) + is_first = true; + } else { + backend_init_args.join = true; } + /* create the new group's context */ void* context = NULL; ret = a.backend->init_group(&backend_init_args, &context); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d1417a5..8e7a9fa 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,3 +15,28 @@ foreach (test-source ${test-sources}) add_test (NAME ${name} COMMAND ./${name}) endif () endforeach () + +if (ENABLE_PYTHON) + # Set the path to the python directory + set (PYTHON_MODULE_DIR ${CMAKE_SOURCE_DIR}/python) + # Use file(GLOB_RECURSE ...) to find all files matching the test_*.py pattern + file (GLOB_RECURSE PYTHON_TEST_FILES "${PYTHON_MODULE_DIR}/test_*.py") + + foreach (PYTHON_TEST_FILE ${PYTHON_TEST_FILES}) + # Remove the directory part + file (RELATIVE_PATH PYTHON_TEST_FILE_REL ${PYTHON_MODULE_DIR} ${PYTHON_TEST_FILE}) + # Remove the file extension + string (REPLACE ".py" "" PYTHON_TEST_FILE_BASE ${PYTHON_TEST_FILE_REL}) + # Replace slashes with dots + string (REPLACE "/" "." PYTHON_TEST_NAME ${PYTHON_TEST_FILE_BASE}) + # Add the test + if (${ENABLE_COVERAGE}) + message (STATUS "${PYTHON_TEST_NAME} test will run with code coverage") + add_test (NAME ${PYTHON_TEST_NAME} COMMAND coverage run -a -m unittest ${PYTHON_TEST_NAME}) + else () + add_test (NAME ${PYTHON_TEST_NAME} COMMAND python -m unittest ${PYTHON_TEST_NAME}) + endif () + set_property (TEST ${PYTHON_TEST_NAME} PROPERTY ENVIRONMENT + PYTHONPATH=${CMAKE_SOURCE_DIR}/python/:${CMAKE_BINARY_DIR}/python:$ENV{PYTHONPATH}) + endforeach () +endif () diff --git a/tests/spack.yaml b/tests/spack.yaml index f5218e9..3477a46 100644 --- a/tests/spack.yaml +++ b/tests/spack.yaml @@ -3,9 +3,13 @@ spack: - cmake - pkgconfig - mochi-margo ^mercury~boostsys~checksum ^libfabric fabrics=tcp,rxm + - mochi-thallium - json-c - mochi-bedrock - mpich + - py-pybind11 + - py-mochi-margo + - py-coverage concretizer: unify: true reuse: true