diff --git a/.gitignore b/.gitignore index fec77aad6..9a0b1e06a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ build/ tests/geo/eckit_geo_cache _build *.ccls-cache +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 39bf58c04..a773d4c57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,26 @@ if( NOT TARGET OpenMP::OpenMP_CXX ) set( eckit_HAVE_OMP 0 ) endif() +find_package(LibUUID QUIET) +find_package(LibOPENFAM QUIET) + +### OpenFAM Support + +ecbuild_add_option( FEATURE OPENFAM + DEFAULT ON + CONDITION LibUUID_FOUND AND LibOPENFAM_FOUND + DESCRIPTION "Enables OpenFAM support" ) + +ecbuild_add_option( FEATURE OPENFAM_MOCK + DEFAULT ON + CONDITION LibUUID_FOUND AND NOT LibOPENFAM_FOUND + DESCRIPTION "Enables OpenFAM Mock for testing and development." ) + +if( eckit_HAVE_OPENFAM_MOCK ) + add_subdirectory( src/eckit/io/fam/openfam_mock ) + set( eckit_HAVE_OPENFAM 1 ) +endif() + ### RADOS ecbuild_add_option( FEATURE RADOS diff --git a/cmake/FindLibOPENFAM.cmake b/cmake/FindLibOPENFAM.cmake new file mode 100644 index 000000000..e03dcb0cc --- /dev/null +++ b/cmake/FindLibOPENFAM.cmake @@ -0,0 +1,104 @@ +# Copyright 2024- European Centre for Medium-Range Weather Forecasts (ECMWF) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation nor +# does it submit to any jurisdiction. +# +# Requires: +# FindPackageHandleStandardArgs (CMake standard module) +# + +#[=======================================================================[.rst: +FindLibOPENFAM +-------------- + +This module finds the libopenfam (Fabric-Attached Memory) library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``LibOPENFAM`` + The libopenfam library + +Result variables +^^^^^^^^^^^^^^^^ + +This module will set the following variables in your project: + +``LIB_OPENFAM_FOUND`` + True if the libopenfam library is found. +``LIB_OPENFAM_INCLUDE_DIRS`` + Include directories needed to use libopenfam. +``LIB_OPENFAM_LIBRARIES`` + Libraries needed to link to libopenfam. + +Cache variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set to help find libopenfam library: + +``LIB_OPENFAM_INCLUDE_DIR`` + where to find the libopenfam headers. +``LIB_OPENFAM_LIBRARY`` + where to find the libopenfam library. + +Hints +^^^^^ + +The environment variables ``OPENFAM_DIR`` and ``OPENFAM_PATH`` +may also be set to help find libopenfam library. + +#]=======================================================================] + +find_path(LIB_OPENFAM_INCLUDE_DIR fam/fam.h + HINTS + ${OPENFAM_DIR} + ${OPENFAM_PATH} + ENV OPENFAM_DIR + ENV OPENFAM_PATH + PATH_SUFFIXES include ../include +) + +find_library(LIB_OPENFAM_LIBRARY + NAMES openfam + HINTS + ${OPENFAM_DIR} + ${OPENFAM_PATH} + ENV OPENFAM_DIR + ENV OPENFAM_PATH + PATH_SUFFIXES lib lib64 build/lib build/lib64 +) + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args(LibOPENFAM REQUIRED_VARS + LIB_OPENFAM_LIBRARY + LIB_OPENFAM_INCLUDE_DIR) + +if (LibOPENFAM_FOUND) + set(LIB_OPENFAM_LIBRARIES ${LIB_OPENFAM_LIBRARY}) + set(LIB_OPENFAM_INCLUDE_DIRS ${LIB_OPENFAM_INCLUDE_DIR}) + if(NOT TARGET LibOPENFAM) + add_library(LibOPENFAM UNKNOWN IMPORTED) + set_target_properties(LibOPENFAM PROPERTIES + IMPORTED_LOCATION "${LIB_OPENFAM_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${LIB_OPENFAM_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "${LIB_OPENFAM_LIBRARY}") + endif() +endif() + +mark_as_advanced(LIB_OPENFAM_INCLUDE_DIR LIB_OPENFAM_LIBRARY) diff --git a/cmake/FindLibUUID.cmake b/cmake/FindLibUUID.cmake new file mode 100644 index 000000000..b2c83cf6c --- /dev/null +++ b/cmake/FindLibUUID.cmake @@ -0,0 +1,108 @@ +# Copyright 2024- European Centre for Medium-Range Weather Forecasts (ECMWF) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation nor +# does it submit to any jurisdiction. +# +# Requires: +# FindPackageHandleStandardArgs (CMake standard module) +# + +#[=======================================================================[.rst: +FindLibUUID +-------- + +This module finds the libuuid (from util-linux) library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``LibUUID`` + The libuuid library + +Result variables +^^^^^^^^^^^^^^^^ + +This module will set the following variables in your project: + +``LIB_UUID_FOUND`` + True if the libuuid library is found. +``LIB_UUID_INCLUDE_DIRS`` + Include directories needed to use libuuid. +``LIB_UUID_LIBRARIES`` + Libraries needed to link to libuuid. + +Cache variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set to help find libuuid library: + +``LIB_UUID_INCLUDE_DIR`` + where to find the libuuid headers. +``LIB_UUID_LIBRARY`` + where to find the libuuid library. + +Hints +^^^^^ + +The environment variables ``LIB_UUID_ROOT``, ``LIB_UUID_DIR``, and ``LIB_UUID_PATH`` +may also be set to help find libuuid library. + +#]=======================================================================] + +find_path(LIB_UUID_INCLUDE_DIR uuid.h + HINTS + ${LIB_UUID_ROOT} + ${LIB_UUID_DIR} + ${LIB_UUID_PATH} + ENV LIB_UUID_ROOT + ENV LIB_UUID_DIR + ENV LIB_UUID_PATH + PATH_SUFFIXES uuid +) + +find_library(LIB_UUID_LIBRARY + NAMES uuid + HINTS + ${LIB_UUID_ROOT} + ${LIB_UUID_DIR} + ${LIB_UUID_PATH} + ENV LIB_UUID_ROOT + ENV LIB_UUID_DIR + ENV LIB_UUID_PATH + PATH_SUFFIXES lib lib64 +) + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args(LibUUID REQUIRED_VARS + LIB_UUID_LIBRARY + LIB_UUID_INCLUDE_DIR) + +if (LibUUID_FOUND) + set(LIB_UUID_LIBRARIES ${LIB_UUID_LIBRARY}) + set(LIB_UUID_INCLUDE_DIRS ${LIB_UUID_INCLUDE_DIR}) + if(NOT TARGET LibUUID) + add_library(LibUUID UNKNOWN IMPORTED) + set_target_properties(LibUUID PROPERTIES + IMPORTED_LOCATION "${LIB_UUID_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${LIB_UUID_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "${LIB_UUID_LIBRARY}") + endif() +endif() + +mark_as_advanced(LIB_UUID_INCLUDE_DIR LIB_UUID_LIBRARY) diff --git a/src/eckit/CMakeLists.txt b/src/eckit/CMakeLists.txt index 6a0766eeb..93580c305 100644 --- a/src/eckit/CMakeLists.txt +++ b/src/eckit/CMakeLists.txt @@ -268,6 +268,48 @@ list(APPEND eckit_message_srcs message/Splitter.h ) +# FAM support + +list( APPEND eckit_io_srcs + io/fam/FamTypes.h +) + +if( eckit_HAVE_OPENFAM ) + list( APPEND eckit_io_srcs + io/fam/FamHandle.cc + io/fam/FamHandle.h + io/fam/FamList.cc + io/fam/FamList.h + io/fam/FamListIterator.cc + io/fam/FamListIterator.h + io/fam/FamMap.cc + io/fam/FamMap.h + io/fam/FamMapEntry.h + io/fam/FamMapIterator.cc + io/fam/FamMapIterator.h + io/fam/FamName.cc + io/fam/FamName.h + io/fam/FamObject.cc + io/fam/FamObject.h + io/fam/FamObjectName.cc + io/fam/FamObjectName.h + io/fam/FamPath.cc + io/fam/FamPath.h + io/fam/FamProperty.cc + io/fam/FamProperty.h + io/fam/FamRegion.cc + io/fam/FamRegion.h + io/fam/FamRegionName.cc + io/fam/FamRegionName.h + io/fam/FamSession.cc + io/fam/FamSession.h + io/fam/FamSessionManager.cc + io/fam/FamSessionManager.h + io/fam/FamURIManager.cc + io/fam/FamURIManager.h + ) +endif( eckit_HAVE_OPENFAM ) + if(HAVE_RADOS) list( APPEND eckit_io_srcs io/rados/RadosHandle.cc @@ -830,6 +872,7 @@ list( APPEND eckit_transaction_srcs list( APPEND eckit_testing_srcs testing/Filesystem.cc testing/Filesystem.h + testing/ProcessFork.h testing/Test.h testing/Filesystem.cc testing/Filesystem.h @@ -928,6 +971,7 @@ ecbuild_add_library( "${RADOS_INCLUDE_DIRS}" "${OPENSSL_INCLUDE_DIR}" "${AIO_INCLUDE_DIRS}" + "${LIB_OPENFAM_INCLUDE_DIRS}" PRIVATE_LIBS "${SNAPPY_LIBRARIES}" @@ -937,6 +981,8 @@ ecbuild_add_library( "${CURL_LIBRARIES}" "${AIO_LIBRARIES}" "${RADOS_LIBRARIES}" + $<$:LibUUID> + $<$:LibOPENFAM> $<${HAVE_AEC}:libaec::aec> PUBLIC_LIBS diff --git a/src/eckit/exception/Exceptions.cc b/src/eckit/exception/Exceptions.cc index 91a005c6f..d3e77dbc0 100644 --- a/src/eckit/exception/Exceptions.cc +++ b/src/eckit/exception/Exceptions.cc @@ -8,13 +8,14 @@ * does it submit to any jurisdiction. */ +#include "eckit/exception/Exceptions.h" + #include #include #include #include "eckit/config/LibEcKit.h" -#include "eckit/exception/Exceptions.h" #include "eckit/log/Log.h" #include "eckit/os/BackTrace.h" #include "eckit/thread/ThreadSingleton.h" @@ -155,6 +156,15 @@ Cancel::Cancel(const std::string& w, const CodeLocation& loc) : Exception("Cance Retry::Retry(const std::string& w, const CodeLocation& loc) : Exception("Retry: " + w, loc) {} +PermissionDenied::PermissionDenied(const std::string& w, const CodeLocation& loc) : + Exception("Permission denied: " + w, loc) {} + +NotFound::NotFound(const std::string& w, const CodeLocation& loc) : Exception("Not found: " + w, loc) {} + +AlreadyExists::AlreadyExists(const std::string& w, const CodeLocation& loc) : Exception("Already exists: " + w, loc) {} + +OutOfStorage::OutOfStorage(const std::string& w, const CodeLocation& loc) : Exception("Out of storage: " + w, loc) {} + UserError::UserError(const std::string& w, const CodeLocation& loc) : Exception("UserError: " + w, loc) {} UserError::UserError(const std::string& user, const std::string& w, const CodeLocation& loc) : diff --git a/src/eckit/exception/Exceptions.h b/src/eckit/exception/Exceptions.h index a06067912..10d264e2d 100644 --- a/src/eckit/exception/Exceptions.h +++ b/src/eckit/exception/Exceptions.h @@ -187,6 +187,30 @@ class Retry : public Exception { explicit Retry(const std::string&, const CodeLocation& = {}); }; +class PermissionDenied : public Exception { +public: + + explicit PermissionDenied(const std::string& /* w */, const CodeLocation& = {}); +}; + +class NotFound : public Exception { +public: + + explicit NotFound(const std::string& /* w */, const CodeLocation& = {}); +}; + +class AlreadyExists : public Exception { +public: + + explicit AlreadyExists(const std::string& /* w */, const CodeLocation& = {}); +}; + +class OutOfStorage : public Exception { +public: + + explicit OutOfStorage(const std::string& /* w */, const CodeLocation& = {}); +}; + class UserError : public Exception { public: diff --git a/src/eckit/filesystem/URI.cc b/src/eckit/filesystem/URI.cc index 76c998ff5..814aa0353 100644 --- a/src/eckit/filesystem/URI.cc +++ b/src/eckit/filesystem/URI.cc @@ -67,6 +67,9 @@ URI::URI(const std::string& scheme, const URI& uri, const std::string& hostname, fragment_(uri.fragment_), queryValues_(uri.queryValues_) {} +URI::URI(std::string scheme, const net::Endpoint& endpoint, std::string name) noexcept : + name_(std::move(name)), scheme_(std::move(scheme)), host_(endpoint.host()), port_(endpoint.port()) {} + URI::URI(Stream& s) { s >> scheme_; s >> user_; diff --git a/src/eckit/filesystem/URI.h b/src/eckit/filesystem/URI.h index 220194b0c..3d5b32953 100644 --- a/src/eckit/filesystem/URI.h +++ b/src/eckit/filesystem/URI.h @@ -24,6 +24,7 @@ #include "eckit/io/Offset.h" #include "eckit/net/Endpoint.h" +#include namespace eckit { @@ -49,6 +50,7 @@ class URI { URI(const std::string& scheme, const URI& uri); URI(const std::string& scheme, const std::string& hostname, int port); URI(const std::string& scheme, const URI& uri, const std::string& hostname, int port); + URI(std::string scheme, const net::Endpoint& endpoint, std::string name) noexcept; URI(Stream& s); // Destructor @@ -63,7 +65,9 @@ class URI { DataHandle* newReadHandle(const OffsetList&, const LengthList&) const; DataHandle* newReadHandle() const; - void endpoint(const eckit::net::Endpoint& endpoint) { + net::Endpoint endpoint() const { return {host_, port_}; } + + void endpoint(const net::Endpoint& endpoint) { host_ = endpoint.host(); port_ = endpoint.port(); } diff --git a/src/eckit/io/Buffer.h b/src/eckit/io/Buffer.h index 708a0a6e5..04c055e0e 100644 --- a/src/eckit/io/Buffer.h +++ b/src/eckit/io/Buffer.h @@ -17,6 +17,7 @@ #include #include +#include namespace eckit { @@ -54,6 +55,8 @@ class Buffer { ~Buffer(); + std::string_view view() const noexcept { return {buffer_, size_}; } + operator char*() { return buffer_; } operator const char*() const { return buffer_; } diff --git a/src/eckit/io/fam/FamConfig.h b/src/eckit/io/fam/FamConfig.h new file mode 100644 index 000000000..f11a498b7 --- /dev/null +++ b/src/eckit/io/fam/FamConfig.h @@ -0,0 +1,70 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamConfig.h +/// @author Metin Cakircali +/// @date May 2024 + +#pragma once + +#include +#include + +#include "eckit/config/Resource.h" +#include "eckit/net/Endpoint.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +/// Configuration for a FAM session. +/// +/// Session name resolution order: +/// 1. Explicit sessionName passed at construction +/// 2. Resource "famSessionName" / env $FAM_SESSION_NAME +/// 3. Derived from endpoint: "FamSession::" +struct FamConfig { + + /// Resolves the session name for a given endpoint. + /// Checks Resource "famSessionName" / $FAM_SESSION_NAME first; + /// falls back to "FamSession::" for per-endpoint uniqueness. + static std::string resolveSessionName(const net::Endpoint& endpoint) { + std::string name = Resource{"famSessionName;$FAM_SESSION_NAME", ""}; + if (!name.empty()) { + return name; + } + return "FamSession:" + endpoint.host() + ":" + std::to_string(endpoint.port()); + } + + net::Endpoint endpoint{"127.0.0.1", -1}; + std::string sessionName{resolveSessionName(endpoint)}; + + explicit FamConfig(const net::Endpoint& ep) : endpoint{ep}, sessionName{resolveSessionName(endpoint)} {} + + explicit FamConfig(const net::Endpoint& ep, std::string name) : endpoint{ep}, sessionName{std::move(name)} {} + + bool operator==(const FamConfig& other) const { + return (endpoint == other.endpoint && sessionName == other.sessionName); + } + + friend std::ostream& operator<<(std::ostream& out, const FamConfig& config) { + out << "endpoint=" << config.endpoint << ", sessionName=" << config.sessionName; + return out; + } +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamHandle.cc b/src/eckit/io/fam/FamHandle.cc new file mode 100644 index 000000000..0826f5592 --- /dev/null +++ b/src/eckit/io/fam/FamHandle.cc @@ -0,0 +1,156 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamHandle.h" + +#include +#include +#include + +#include "eckit/config/LibEcKit.h" +#include "eckit/exception/Exceptions.h" +#include "eckit/io/Length.h" +#include "eckit/io/Offset.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamObjectName.h" +#include "eckit/log/Log.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +FamHandle::FamHandle(const FamObjectName& name, const Offset& position, const Length& length, const bool overwrite) : + name_{name}, overwrite_{overwrite}, pos_{position}, len_{length} {} + +FamHandle::FamHandle(const FamObjectName& name, const bool overwrite) : FamHandle(name, 0, 0, overwrite) {} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamHandle::open(const Mode mode) { + ASSERT(!object_ && mode_ == Mode::CLOSED); + pos_ = 0; + mode_ = mode; +} + +void FamHandle::close() { + pos_ = 0; + mode_ = Mode::CLOSED; + object_.reset(); +} + +void FamHandle::flush() { + Log::debug() << "FamHandle::flush() ?! noop\n"; +} + +Offset FamHandle::seek(const Offset& offset) { + pos_ = offset; + + ASSERT(0 <= pos_ && size() >= pos_); + + return pos_; +} + +Length FamHandle::size() { + return object_ ? Length(object_->size()) : estimate(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +Length FamHandle::openForRead() { + open(Mode::READ); + object_ = name_.lookup(); + len_ = size(); + return len_; +} + +void FamHandle::openForWrite(const Length& length) { + open(Mode::WRITE); + + try { + object_ = name_.lookup(); + if (overwrite_ && length > 0) { + ASSERT(size() >= length); + } + } + catch (const NotFound& e) { + Log::debug() << "FamHandle::openForWrite() " << e.what() << '\n'; + ASSERT(length > 0); + object_ = name_.allocate(length); + } + + len_ = size(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +long FamHandle::read(void* buffer, long length) { + ASSERT(mode_ == Mode::READ); + ASSERT(0 <= pos_); + + if (size() <= pos_) { + return 0; + } + + // Adjust length to read only up to the end of the object + length = std::min(len_ - pos_, length); + + object_->get(buffer, pos_, length); + + pos_ += length; + + return length; +} + +long FamHandle::write(const void* buffer, const long length) { + ASSERT(mode_ == Mode::WRITE); + ASSERT(0 <= pos_); + ASSERT(length >= 0); + + const auto end = static_cast(pos_) + static_cast(length); + if (end > object_->size()) { + std::ostringstream msg; + msg << "FamHandle::write: out of range for object '" << name_ << "': pos=" << pos_ << ", length=" << length + << ", object size=" << object_->size(); + throw OutOfRange(msg.str(), Here()); + } + + object_->put(buffer, pos_, length); + + pos_ += length; + + return length; +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamHandle::print(std::ostream& out) const { + out << "FamHandle[name=" << name_ << ", position=" << pos_ << ", mode="; + switch (mode_) { + case Mode::CLOSED: + out << "closed"; + break; + case Mode::READ: + out << "read"; + break; + case Mode::WRITE: + out << "write"; + break; + } + out << "]"; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamHandle.h b/src/eckit/io/fam/FamHandle.h new file mode 100644 index 000000000..34d38153e --- /dev/null +++ b/src/eckit/io/fam/FamHandle.h @@ -0,0 +1,94 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamHandle.h +/// @author Metin Cakircali +/// @date May 2024 + +#pragma once + +#include +#include +#include + +#include "eckit/io/DataHandle.h" +#include "eckit/io/Length.h" +#include "eckit/io/Offset.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamObjectName.h" + +namespace eckit { + + +//---------------------------------------------------------------------------------------------------------------------- + +class FamHandle : public DataHandle { +public: // methods + + enum class Mode : std::uint8_t { + CLOSED, + READ, + WRITE + }; + + FamHandle(const FamObjectName& name, const Offset& position, const Length& length, bool overwrite); + + FamHandle(const FamObjectName& name, bool overwrite = false); + + Length openForRead() override; + + void openForWrite(const Length& length) override; + + long read(void* buffer, long length) override; + + long write(const void* buffer, long length) override; + + void flush() override; + + void close() override; + + Offset seek(const Offset& offset) override; + + bool canSeek() const override { return true; } + + Offset position() override { return pos_; } + + Length estimate() override { return len_; } + + Length size() override; + +private: // methods + + void open(Mode mode); + + void print(std::ostream& out) const override; + +private: // members + + const FamObjectName name_; + + const bool overwrite_{false}; + + Offset pos_{0}; + Length len_{0}; + + Mode mode_{Mode::CLOSED}; + + std::optional object_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamList.cc b/src/eckit/io/fam/FamList.cc new file mode 100644 index 000000000..f44cdb1dc --- /dev/null +++ b/src/eckit/io/fam/FamList.cc @@ -0,0 +1,364 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamList.h" + +#include +#include +#include +#include + +#include "eckit/io/fam/detail/FamBackoff.h" + +#include "eckit/exception/Exceptions.h" +#include "eckit/io/fam/FamListIterator.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamRegion.h" +#include "eckit/io/fam/FamRegionName.h" +#include "eckit/io/fam/detail/FamListNode.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +FamList::FamList(FamRegion region, const Descriptor& desc) : + region_{std::move(region)}, + head_{region_.proxyObject(desc.head)}, + tail_{region_.proxyObject(desc.tail)}, + size_{region_.proxyObject(desc.size)} { + ASSERT(region_.index() == desc.region); +} + +FamList::FamList(FamRegion region, const std::string& list_name) : + region_{std::move(region)}, + head_{region_.ensureObject(sizeof(FamListNode), list_name + "h")}, + tail_{region_.ensureObject(sizeof(FamListNode), list_name + "t")}, + size_{region_.ensureObject(sizeof(size_type), list_name + "s")} { + // set head's next to tail's prev (idempotent) + if (FamListNode::getNextOffset(head_) == 0) { + head_.put(tail_.descriptor(), FamListNode::nextOff()); + } + // set tail's prev to head's next (idempotent) + if (FamListNode::getPrevOffset(tail_) == 0) { + tail_.put(head_.descriptor(), offsetof(FamListNode, prev)); + } +} + +auto FamList::descriptor() const -> Descriptor { + return {region_.index(), head_.offset(), tail_.offset(), size_.offset()}; +} + +//---------------------------------------------------------------------------------------------------------------------- +// iterators + +auto FamList::begin() const -> iterator { + return region_.proxyObject(FamListNode::getNextOffset(head_)); +} + +auto FamList::cbegin() const -> const_iterator { + return region_.proxyObject(FamListNode::getNextOffset(head_)); +} + +auto FamList::end() const -> iterator { + return region_.proxyObject(tail_.offset()); +} + +auto FamList::cend() const -> const_iterator { + return region_.proxyObject(tail_.offset()); +} + +//---------------------------------------------------------------------------------------------------------------------- +// accessors + +auto FamList::front() const -> value_type { + ASSERT(!empty()); + return std::move(*begin()); +} + +auto FamList::back() const -> value_type { + ASSERT(!empty()); + return std::move(*--end()); +} + +//---------------------------------------------------------------------------------------------------------------------- +// Lock-Free Insertion + +void FamList::pushFront(const void* data, const size_type length) { + // 1. Allocate new node with data + auto new_object = region_.allocateObject(sizeof(FamListNode) + length); + new_object.put(length, offsetof(FamListNode, length)); + new_object.put(data, sizeof(FamListNode), length); + + // 2. Link into list: use CAS-loop (Compare-And-Swap) to atomically update head.next + // This ensures the new node becomes visible to other readers + fam::detail::CasBackoff backoff; + while (true) { + // Get current first node (what head.next points to) + const auto first_offset = FamListNode::getNextOffset(head_); + auto first_object = region_.proxyObject(first_offset); + + // Point new node backward to head + new_object.put(head_.descriptor(), offsetof(FamListNode, prev)); + + // Point new node forward to current first node + new_object.put(first_object.descriptor(), FamListNode::nextOff()); + + // Atomically update head.next to new node. + // On success, we become the new first node. + const auto old_offset = + head_.compareSwap(FamListNode::nextOffsetOff(), first_offset, new_object.offset()); + if (old_offset == first_offset) { + // Success! Update old first node's prev to point to us. + // Use CAS instead of plain put to avoid overwriting a concurrent + // pushBack's CAS on tail.prev (when first_object is the tail sentinel). + first_object.compareSwap(FamListNode::prevOffsetOff(), head_.offset(), new_object.offset()); + + // Atomically increment size + size_.add(0, size_type{1}); + + return; + } + // CAS failed, another thread modified head.next. Back off before retry. + backoff(); + } +} + +void FamList::pushBack(const void* data, const size_type length) { + // 1. Allocate new node with data + auto new_object = region_.allocateObject(sizeof(FamListNode) + length); + new_object.put(length, offsetof(FamListNode, length)); + new_object.put(data, sizeof(FamListNode), length); + + // 2. Link into list: use CAS-loop to atomically update tail.prev + // This ensures new node becomes visible to other readers + fam::detail::CasBackoff backoff; + while (true) { + // Get current last node (what tail.prev points to) + const auto last_offset = FamListNode::getPrevOffset(tail_); + auto last_object = region_.proxyObject(last_offset); + + // Point new node forward to tail + new_object.put(tail_.descriptor(), FamListNode::nextOff()); + + // Point new node backward to current last node + new_object.put(last_object.descriptor(), offsetof(FamListNode, prev)); + + // Atomically update tail.prev to new node. + // On success, we become the new last node. + const auto old_offset = tail_.compareSwap(FamListNode::prevOffsetOff(), last_offset, new_object.offset()); + if (old_offset == last_offset) { + // Success! Now link new_object into the forward chain. + // Use CAS-loop: walk forward from last_object to find the node whose + // next is tail, then CAS its next to new_object. + // This prevents the plain-put race with concurrent pushFront on head.next. + fam::detail::CasBackoff inner_backoff; + auto current = std::move(last_object); + while (true) { + const auto cur_next = FamListNode::getNextOffset(current); + if (cur_next == tail_.offset()) { + const auto old = + current.compareSwap(FamListNode::nextOffsetOff(), tail_.offset(), new_object.offset()); + if (old == tail_.offset()) { + break; // Successfully linked into forward chain + } + // CAS failed — another node was inserted. Follow the new link. + current.replaceWith({region_.index(), old}); + inner_backoff(); + } + else { + // Follow forward chain to find the node just before tail + current.replaceWith({region_.index(), cur_next}); + } + } + + // Atomically increment size + size_.add(0, size_type{1}); + + return; + } + // CAS failed, another thread modified tail.prev. Back off before retry. + backoff(); + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// Wait-Free Deletion (Logical + Physical) + +void FamList::popFront() { + ASSERT(!empty()); + + fam::detail::CasBackoff backoff; + while (true) { + // Get the first node to delete + const auto first_offset = FamListNode::getNextOffset(head_); + auto first_object = region_.proxyObject(first_offset); + + // Safety check: don't delete the tail sentinel + if (first_offset == tail_.offset()) { + return; // Already empty + } + + // 1. Logically mark the node as deleted (wait-free flag) + FamListNode::mark(first_object); + + // 2. Get the next node after the one we're deleting + const auto next_offset = FamListNode::getNextOffset(first_object); + + // 3. Atomically update head.next to skip over the marked node + const auto old_offset = head_.compareSwap(FamListNode::nextOffsetOff(), first_offset, next_offset); + if (old_offset == first_offset) { + // Success! We've removed the node from the list. + // Update the next node's prev pointer to point to head + auto next_object = region_.proxyObject(next_offset); + next_object.put(head_.descriptor(), offsetof(FamListNode, prev)); + + // Decrement size + size_.subtract(0, size_type{1}); + + // Node is marked and unlinked but NOT deallocated. + // Concurrent iterators may still hold the node's offset and + // follow its next/prev pointers, which remain valid. + // Physical reclamation happens on region wipe or clear(). + return; + } + // CAS failed, another thread modified head.next. Back off before retry. + backoff(); + } +} + +void FamList::popBack() { + ASSERT(!empty()); + + fam::detail::CasBackoff backoff; + while (true) { + // Get the last node to delete + const auto last_offset = FamListNode::getPrevOffset(tail_); + auto last_object = region_.proxyObject(last_offset); + + // Safety check: don't delete the head sentinel + if (last_offset == head_.offset()) { + return; // Already empty + } + + // 1. Logically mark the node as deleted (wait-free flag) + FamListNode::mark(last_object); + + // 2. Get the previous node + const auto prev_offset = FamListNode::getPrevOffset(last_object); + + // 3. Atomically update tail.prev to point before the marked node + const auto old_offset = tail_.compareSwap(FamListNode::prevOffsetOff(), last_offset, prev_offset); + if (old_offset == last_offset) { + // Success! We've removed the node from the list. + // Update the previous node's next pointer to point to tail + auto prev_object = region_.proxyObject(prev_offset); + prev_object.put(tail_.descriptor(), FamListNode::nextOff()); + + // Decrement size + size_.subtract(0, size_type{1}); + + // Node is marked and unlinked but NOT deallocated. + // See popFront() for rationale. + return; + } + // CAS failed, another thread modified tail.prev. Back off before retry. + backoff(); + } +} + +auto FamList::erase(iterator pos) -> iterator { + const auto& object = pos.object(); + ASSERT(object.offset() != tail_.offset()); + + fam::detail::CasBackoff backoff; + while (true) { + // 1. Mark the node for deletion + FamListNode::mark(object); + + // 2. Get next and prev pointers + const auto next_offset = FamListNode::getNextOffset(object); + const auto prev_offset = FamListNode::getPrevOffset(object); + + auto next_object = region_.proxyObject(next_offset); + auto prev_object = region_.proxyObject(prev_offset); + + // 3. Atomically update prev.next to skip over marked node + const auto old_next = prev_object.compareSwap(FamListNode::nextOffsetOff(), object.offset(), next_offset); + if (old_next == object.offset()) { + // Success! Update next.prev as well + next_object.put(prev_object.descriptor(), offsetof(FamListNode, prev)); + + // Update size + size_.subtract(0, size_type{1}); + + // Node is marked and unlinked but NOT deallocated. + // See popFront() for rationale. + + return region_.proxyObject(next_offset); + } + // CAS failed, back off before retry. + backoff(); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamList::clear() { + while (true) { + const auto first_offset = FamListNode::getNextOffset(head_); + if (first_offset == tail_.offset()) { + break; // empty + } + + auto first_object = region_.proxyObject(first_offset); + const auto next_offset = FamListNode::getNextOffset(first_object); + auto next_object = region_.proxyObject(next_offset); + + // this is single-threaded, no CAS / mark needed + head_.put(next_object.descriptor(), FamListNode::nextOff()); + next_object.put(head_.descriptor(), offsetof(FamListNode, prev)); + + first_object.deallocate(); + size_.subtract(0, size_type{1}); + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// capacity + +auto FamList::size() const -> size_type { + return size_.get(); +} + +bool FamList::empty() const { + // A node is the first real element if it's the next of head and not tail + const auto first_offset = FamListNode::getNextOffset(head_); + return first_offset == tail_.offset(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamList::print(std::ostream& out) const { + out << "FamList[size=" << size() << ", region=" << region_ << ", head=" << head_ << ", tail=" << tail_ << "]"; +} + +std::ostream& operator<<(std::ostream& out, const FamList& list) { + list.print(out); + return out; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamList.h b/src/eckit/io/fam/FamList.h new file mode 100644 index 000000000..70b03dc5f --- /dev/null +++ b/src/eckit/io/fam/FamList.h @@ -0,0 +1,197 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamList.h +/// @author Metin Cakircali +/// @date Mar 2024 +/// +/// @brief Concurrent-safe FAM-resident doubly-linked list. +/// +/// ## Thread Safety +/// +/// FamList provides **multiple-reader, multiple-writer (MRMW)** safety: +/// +/// - **Concurrent insertions** (`pushFront`, `pushBack`): Lock-free using atomic CAS loops. +/// Two threads can insert simultaneously without serialization. +/// +/// - **Concurrent deletions** (`popFront`, `popBack`, `erase`): Wait-free logical deletion. +/// Nodes are *marked* for deletion before physical deallocation. Readers skip marked nodes. +/// +/// - **Concurrent iteration**: Safe during concurrent insertions/deletions via node versioning. +/// Iterators validate version stamps to detect stale node descriptors (ABA problem). +/// +/// - **Size tracking**: Updated atomically with pointer modifications via careful CAS loops. +/// +/// ## Lock-Free Algorithm Details +/// +/// ### Insertion (pushBack example): +/// ``` +/// 1. Allocate new node with data +/// 2. CAS-loop: old_last.next = new_node (atomic version-aware) +/// 3. CAS-loop: tail.prev = new_node (atomic version-aware) +/// 4. Atomic add to size +/// ``` +/// +/// ### Deletion (popFront example, logical): +/// ``` +/// 1. CAS-loop: mark first node as deleted +/// 2. CAS-loop: update head.next (skip marked node) +/// 3. Atomic subtract from size +/// 4. Deallocate node immediately (safe due to logical marking) +/// ``` +/// +/// ## ABA Problem Handling +/// +/// - Each node has a `version` field (incremented on reuse). +/// - Node descriptors are `{offset, version}` pairs. +/// - CAS operations compare both offset and version. +/// - Prevents use-after-free when freed nodes are reallocated at same address. +/// +/// ## Marked Node Convention +/// +/// - Deleted nodes are **logically marked** (bit flag) before physical deallocation. +/// - Readers check the mark bit; they skip marked nodes transparently. +/// - This delays physical freeing and avoids race windows. + +#pragma once + +#include +#include +#include + +#include "eckit/io/fam/FamListIterator.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamRegion.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +/// @brief Concurrent-safe, FAM-resident doubly-linked list. +/// +/// Supports multiple readers and writers operating concurrently without locks. +/// Implements wait-free insertion and logical deletion, with version-based ABA detection. +class FamList { +public: // types + + using size_type = fam::size_t; + using iterator = FamListIterator; + using const_iterator = FamListConstIterator; + using value_type = FamListIterator::data_type; + + /// List descriptor: encodes region ID and FAM object locations. + struct Descriptor { + fam::index_t region{0}; // region ID + fam::index_t head{0}; // offset of head sentinel + fam::index_t tail{0}; // offset of tail sentinel + fam::index_t size{0}; // offset of atomic size counter + }; + +public: // methods + + /// Construct from descriptor (for reopening existing list). + FamList(FamRegion region, const Descriptor& desc); + + /// Construct with new list in FAM (idempotent: reopens if exists). + FamList(FamRegion region, const std::string& list_name); + + /// Return descriptor for persistence/serialization. + Descriptor descriptor() const; + + // ---- capacity ---- + + /// Return number of elements (atomic read). + size_type size() const; + + /// Check if list is empty (lock-free wait-free). + [[nodiscard]] + bool empty() const; + + // ---- iterators ---- + + /// Return iterator to first element (or end() if empty). + /// Safe during concurrent modifications; validates version stamps. + iterator begin() const; + + const_iterator cbegin() const; + + /// Return iterator to sentinel tail (one-past-end). + iterator end() const; + + const_iterator cend() const; + + // ---- accessors ---- + + /// Return copy of first element's data. + /// Precondition: !empty() + value_type front() const; + + /// Return copy of last element's data. + /// Precondition: !empty() + value_type back() const; + + // ---- modifiers (lock-free) ---- + + /// Insert data at front. Lock-free, multiple-writer safe. + void pushFront(const void* data, size_type length); + + void pushFront(std::string_view data) { pushFront(data.data(), data.size()); } + + void pushFront(const Buffer& data) { pushFront(data.view()); } + + /// Insert data at back. Lock-free, multiple-writer safe. + void pushBack(const void* data, size_type length); + + void pushBack(std::string_view data) { pushBack(data.data(), data.size()); } + + void pushBack(const Buffer& data) { pushBack(data.view()); } + + /// Remove first element. Lock-free (logical mark + unlink). + /// The node is NOT deallocated; concurrent iterators may still + /// reference it. Physical reclamation happens on region wipe or clear(). + /// Precondition: !empty() + void popFront(); + + /// Remove last element. Lock-free (logical mark + unlink). + /// The node is NOT deallocated; see popFront() for rationale. + /// Precondition: !empty() + void popBack(); + + /// Remove element at position. Returns iterator to following element. + /// The node is NOT deallocated; see popFront() for rationale. + iterator erase(iterator pos); + + /// Reclaims the FAM memory immediately. Deallocate all data nodes and reset the list to empty. + /// Precondition: caller guarantees no concurrent operations on this list. + /// @pre No concurrent readers or writers. + void clear(); + +private: // methods + + void print(std::ostream& out) const; + + friend std::ostream& operator<<(std::ostream& out, const FamList& list); + +private: // members + + FamRegion region_; + FamObject head_; + FamObject tail_; + FamObject size_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamListIterator.cc b/src/eckit/io/fam/FamListIterator.cc new file mode 100644 index 000000000..a1b2ee424 --- /dev/null +++ b/src/eckit/io/fam/FamListIterator.cc @@ -0,0 +1,87 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamListIterator.h" + +#include + +#include "eckit/io/fam/detail/FamListNode.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- +// ITERATOR + +FamListIterator::FamListIterator(value_type object) : object_{std::move(object)} {} + +/// Advance to next node, skipping logically deleted (marked) nodes. +/// This ensures iterators remain consistent even during concurrent deletions. +FamListIterator& FamListIterator::operator++() { + if (const auto next = FamListNode::getNext(object_); next.offset > 0) { + object_.replaceWith(next); + + // Skip over marked (deleted) nodes - iterate until we find an unmarked node + while (FamListNode::isMarked(object_)) { + if (const auto next_next = FamListNode::getNext(object_); next_next.offset > 0) { + object_.replaceWith(next_next); + } + else { + break; // No more nodes + } + } + buffer_.reset(); + } + return *this; +} + +/// Retreat to previous node, skipping logically deleted (marked) nodes. +FamListIterator& FamListIterator::operator--() { + if (const auto prev = FamListNode::getPrev(object_); prev.offset > 0) { + object_.replaceWith(prev); + + // Skip over marked (deleted) nodes - iterate until we find an unmarked node + while (FamListNode::isMarked(object_)) { + if (const auto prev_prev = FamListNode::getPrev(object_); prev_prev.offset > 0) { + object_.replaceWith(prev_prev); + } + else { + break; // No more nodes + } + } + buffer_.reset(); + } + return *this; +} + +bool FamListIterator::operator==(const FamListIterator& other) const { + return other.object_ == object_; +} + +auto FamListIterator::operator->() -> pointer { + return &object_; +} + +/// Dereference operator: returns data buffer. +/// Safely skips marked nodes when dereferencing. +auto FamListIterator::operator*() const -> data_type& { + if (!buffer_) { + FamListNode::getData(object_, buffer_.emplace()); + } + return *buffer_; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamListIterator.h b/src/eckit/io/fam/FamListIterator.h new file mode 100644 index 000000000..954270cea --- /dev/null +++ b/src/eckit/io/fam/FamListIterator.h @@ -0,0 +1,117 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamListIterator.h +/// @author Metin Cakircali +/// @date Mar 2024 +/// +/// @brief Concurrent-safe bidirectional iterator for FamList. +/// +/// ## Thread Safety +/// +/// Iterators are safe to use during concurrent insertions and deletions: +/// +/// - **Marked Node Skipping**: When a node is logically deleted (marked), iterators +/// transparently skip over it on `++` and `--` operations. +/// - **Lock-Free Traversal**: Iteration does not require any synchronization. +/// - **Consistency**: Each iterator snapshot sees a consistent view of the list +/// at the moment the iterator was created, with logical deletions becoming visible +/// as nodes are skipped. + +#pragma once + +#include +#include + +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamObject.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- +// ITERATOR + +/// @brief Concurrent-safe forward and bidirectional iterator for FamList. +/// +/// Advances through the list, automatically skipping marked (deleted) nodes. +/// Safe to use while other threads insert/delete elements. +class FamListIterator { +public: // types + + using iterator_category = std::bidirectional_iterator_tag; + + using value_type = FamObject; + using pointer = value_type*; + using reference = value_type&; + using data_type = Buffer; + +public: // methods + + /// Construct iterator wrapping a FAM object. + FamListIterator(value_type object); // NOLINT(google-explicit-constructor) + + /// Advance to next node (skip marked nodes). + FamListIterator& operator++(); + + /// Retreat to previous node (skip marked nodes). + FamListIterator& operator--(); + + /// Compare iterators for equality. + bool operator==(const FamListIterator& other) const; + + /// Compare iterators for inequality. + bool operator!=(const FamListIterator& other) const { return !operator==(other); } + + /// Access the underlying FAM object (to read node metadata). + pointer operator->(); + + /// Dereference to return data payload as Buffer. + data_type& operator*() const; + + /// Access underlying FAM object. + value_type& object() { return object_; } + + /// Access underlying FAM object (const). + const value_type& object() const { return object_; } + +private: // members + + value_type object_; + + mutable std::optional buffer_; +}; + +//---------------------------------------------------------------------------------------------------------------------- +// CONST ITERATOR + +/// @brief Const variant of FamListIterator. +class FamListConstIterator : public FamListIterator { +public: // types + + using pointer = const value_type*; + using reference = const data_type&; + +public: // methods + + using FamListIterator::FamListIterator; + + pointer operator->() { return FamListIterator::operator->(); } + + reference operator*() { return FamListIterator::operator*(); } +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamMap.cc b/src/eckit/io/fam/FamMap.cc new file mode 100644 index 000000000..dbfb8e049 --- /dev/null +++ b/src/eckit/io/fam/FamMap.cc @@ -0,0 +1,413 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamMap.h" + +#include +#include +#include +#include +#include +#include + +#include "eckit/exception/Exceptions.h" +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamList.h" +#include "eckit/io/fam/FamMapIterator.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamRegion.h" +#include "eckit/log/Log.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- +// Helpers + +namespace { + +/// Byte offset of the bucket descriptor at given index in the table. +constexpr fam::size_t bucketOffset(std::size_t index) { + return static_cast(index * sizeof(FamList::Descriptor)); +} + +/// Offset of the head field within a FamList::Descriptor. +constexpr fam::size_t bucketHeadOffset(std::size_t index) { + return bucketOffset(index) + offsetof(FamList::Descriptor, head); +} + +/// Current wall-clock time as seconds since epoch (uint64). +inline fam::size_t nowSeconds() { + return static_cast( + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()); +} + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- + +template +FamMap::FamMap(std::string name, FamRegion region) : + name_{std::move(name)}, + region_{std::move(region)}, + table_{region_.ensureObject(bucket_count * sizeof(FamList::Descriptor), name_ + table_suffix)}, + count_{region_.ensureObject(sizeof(size_type), name_ + count_suffix)}, + lock_{region_.ensureObject(sizeof(size_type), name_ + lock_suffix)} {} + +//---------------------------------------------------------------------------------------------------------------------- +// Bucket management + +template +FamListIterator FamMap::findInBucket(const FamList& bucket, const key_type& key) { + for (auto iter = bucket.begin(); iter != bucket.end(); ++iter) { + const auto& buffer = *iter; + if (buffer.size() >= key_size && entry_type::decodeKey(buffer) == key) { + return iter; + } + } + return bucket.end(); +} + +template +fam::size_t FamMap::getBucketHead(const std::size_t index) const { + return table_.get(bucketHeadOffset(index)); +} + +template +FamList::Descriptor FamMap::getBucketDescriptor(const std::size_t index) const { + return table_.get(bucketOffset(index)); +} + +template +std::optional FamMap::getBucket(const std::size_t index) const { + if (const auto head = getBucketHead(index); head == 0 || head == creating) { + return {}; + } + return FamList{region_, getBucketDescriptor(index)}; +} + +template +FamList FamMap::getOrCreateBucket(const std::size_t index) { + // bucket already exists + if (auto bucket = getBucket(index)) { + return std::move(*bucket); + } + + // Try to claim the bucket via CAS: 0 -> CREATING + const auto old_head = table_.compareSwap(bucketHeadOffset(index), fam::size_t{0}, creating); + + if (old_head == 0) { + // We claimed the bucket. Create a new FamList bucket. + // Use short name to stay within OpenFAM dataitem name limits (40 chars). + // Format: "{map_name}.{index}" + const auto bucket_name = name_ + "." + std::to_string(index); + auto bucket = FamList{region_, bucket_name}; + auto desc = bucket.descriptor(); + + // Write remaining descriptor fields FIRST (tail, size) + const auto offset = bucketOffset(index); + table_.put(desc.region, offset + offsetof(FamList::Descriptor, region)); + table_.put(desc.tail, offset + offsetof(FamList::Descriptor, tail)); + table_.put(desc.size, offset + offsetof(FamList::Descriptor, size)); + + // Write head LAST to "publish" the bucket (transitions from CREATING → real offset) + table_.put(desc.head, bucketHeadOffset(index)); + + return bucket; + } + + // Another proc/thread is creating this bucket. Spin until head is valid. + // Limit retries to detect a crashed creator (avoid infinite spin). + static constexpr int max_spin = 100000; + auto head = old_head; + for (int spin = 0; head == 0 || head == creating; ++spin) { + ASSERT_MSG(spin < max_spin, "FamMap::getOrCreateBucket: bucket creation stalled (creator may have crashed)"); + std::this_thread::yield(); + head = table_.get(bucketHeadOffset(index)); + } + + return {region_, getBucketDescriptor(index)}; +} + +//---------------------------------------------------------------------------------------------------------------------- +// Iterators + +template +auto FamMap::begin() const -> iterator { + return {*this, 0, true}; +} + +template +auto FamMap::cbegin() const -> const_iterator { + return {*this, 0, true}; +} + +template +auto FamMap::end() const -> iterator { + return {*this, bucket_count, false}; +} + +template +auto FamMap::cend() const -> const_iterator { + return {*this, bucket_count, false}; +} + +//---------------------------------------------------------------------------------------------------------------------- +// Lookup + +template +auto FamMap::find(const key_type& key) const -> iterator { + const auto index = bucketIndex(key); + + auto bucket = getBucket(index); + if (!bucket) { + return end(); + } + + auto iter = findInBucket(*bucket, key); + if (iter == bucket->end()) { + return end(); + } + + return {*this, index, std::move(iter), std::move(*bucket)}; +} + +template +bool FamMap::contains(const key_type& key) const { + const auto bucket = getBucket(bucketIndex(key)); + if (bucket) { + return findInBucket(*bucket, key) != bucket->end(); + } + return false; +} + +template +auto FamMap::count(const key_type& key) const -> size_type { + if (const auto bucket = getBucket(bucketIndex(key))) { + size_type result = 0; + for (const auto& buffer : *bucket) { + if (buffer.size() >= key_size && entry_type::decodeKey(buffer) == key) { + ++result; + } + } + return result; + } + return 0; +} + +//---------------------------------------------------------------------------------------------------------------------- +// Modifiers + +template +auto FamMap::insert(const key_type& key, const void* data, const size_type length) -> std::pair { + const auto index = bucketIndex(key); + auto bucket = getOrCreateBucket(index); + + // Check if key already exists. + // NOTE: This check-then-insert sequence is not atomic. + // Concurrent inserts of the same key may both succeed, resulting in duplicates. + // A per-bucket CAS lock would be needed for full MRMW uniqueness guarantees. + auto iter = findInBucket(bucket, key); + if (iter != bucket.end()) { + return {iterator{*this, index, std::move(iter), std::move(bucket)}, false}; + } + + // Encode and insert into bucket list (lock-free via FamList::pushFront). + // pushFront ensures the new entry is found first by findInBucket (head→tail scan), + // consistent with forceInsert() and insertOrAssign(). + auto payload = entry_type::encode(key, data, length); + bucket.pushFront(payload); + + // Atomically increment total count + count_.add(0, size_type{1}); + + // Re-find the entry to return a valid iterator + auto new_it = findInBucket(bucket, key); + return {iterator{*this, index, std::move(new_it), std::move(bucket)}, true}; +} + +template +auto FamMap::insertOrAssign(const key_type& key, const void* data, const size_type length) + -> std::pair { + const auto index = bucketIndex(key); + auto bucket = getOrCreateBucket(index); + + // 1. Insert new entry at the FRONT of the bucket + // so, concurrent find() (which iterate head→tail) sees it + auto payload = entry_type::encode(key, data, length); + bucket.pushFront(payload); + count_.add(0, size_type{1}); + + // 2. Scan for old entry with the same key (the second occurrence) and erase it. + // findInBucket returns the first match, which is the entry we just inserted. + // We need to find and erase the next match, if any. + bool found_first = false; + bool replaced = false; + for (auto iter = bucket.begin(); iter != bucket.end(); ++iter) { + const auto& buffer = *iter; + if (buffer.size() >= key_size && entry_type::decodeKey(buffer) == key) { + if (!found_first) { + found_first = true; // skip the newly inserted entry (first match) + continue; + } + // This is the old entry — erase it and adjust count. + bucket.erase(std::move(iter)); + count_.subtract(0, size_type{1}); + replaced = true; + break; + } + } + + // 3. Return iterator to the new entry (first match from head). + // Second element: true if inserted (no prior entry), false if replaced. + auto new_it = findInBucket(bucket, key); + return {iterator{*this, index, std::move(new_it), std::move(bucket)}, !replaced}; +} + +template +auto FamMap::forceInsert(const key_type& key, const void* data, const size_type length) -> iterator { + const auto index = bucketIndex(key); + auto bucket = getOrCreateBucket(index); + + auto payload = entry_type::encode(key, data, length); + bucket.pushFront(payload); + + count_.add(0, size_type{1}); + + auto new_it = findInBucket(bucket, key); + return {*this, index, std::move(new_it), std::move(bucket)}; +} + +template +auto FamMap::erase(const key_type& key) -> size_type { + const auto index = bucketIndex(key); + auto bucket = getBucket(index); + if (!bucket) { + return 0; + } + + size_type removed = 0; + for (auto iter = bucket->begin(); iter != bucket->end();) { + const auto& buffer = *iter; + if (buffer.size() >= key_size && entry_type::decodeKey(buffer) == key) { + iter = bucket->erase(std::move(iter)); + ++removed; + } + else { + ++iter; + } + } + + if (removed > 0) { + count_.subtract(0, removed); + } + return removed; +} + +template +void FamMap::clear() { + /// @note insert/erase paths are lock-free + lock(); + try { + for (std::size_t i = 0; i < bucket_count; ++i) { + if (auto bucket = getBucket(i)) { + bucket->clear(); + } + } + count_.set(0, size_type{0}); + } + catch (...) { + unlock(); + throw; + } + unlock(); +} + +template +void FamMap::merge(const FamMap& other) { + for (const auto& [key, value] : other) { + insert(key, value); + } +} + +//---------------------------------------------------------------------------------------------------------------------- +// Capacity + +template +auto FamMap::size() const -> size_type { + return count_.get(); +} + +template +bool FamMap::empty() const { + return size() == 0; +} + +template +float FamMap::loadFactor() const { + return static_cast(size()) / static_cast(bucket_count); +} + +//---------------------------------------------------------------------------------------------------------------------- +// Output + +template +void FamMap::print(std::ostream& out) const { + out << "FamMap[name=" << name_ << ",key_size=" << key_size << ",size=" << size() << ",region=" << region_ << ']'; +} + +//---------------------------------------------------------------------------------------------------------------------- +// Locking (lease-based with TTL) + +template +void FamMap::lock() { + for (;;) { + const auto now = nowSeconds(); + + // Fast path: lock is free (0) + if (lock_.compareSwap(0, 0, now) == 0) { + return; + } + + // Locked: check for a stale lease. + const auto held = lock_.fetch(0); + if (held != 0 && (now - held) > static_cast(lock_ttl.count())) { + // Lease expired — attempt to steal. + if (lock_.compareSwap(0, held, now) == held) { + Log::warning() << "FamMap::lock(): stale lock detected (held for " << (now - held) << "s > TTL " + << lock_ttl.count() << "s) — stolen\n"; + return; + } + // Another process acquired it; retry. + } + + std::this_thread::yield(); + } +} + +template +void FamMap::unlock() { + lock_.set(0, 0); +} + +//---------------------------------------------------------------------------------------------------------------------- + +// Explicit instantiations +template class FamMap>; +template class FamMap>; +template class FamMap>; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamMap.h b/src/eckit/io/fam/FamMap.h new file mode 100644 index 000000000..7f91403f5 --- /dev/null +++ b/src/eckit/io/fam/FamMap.h @@ -0,0 +1,302 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamMap.h +/// @author Metin Cakircali +/// @date Jul 2024 + +/// @brief Concurrent-safe, FAM-resident unordered associative container. +/// +/// ## Overview +/// +/// FamMap is a hash-based key-value store residing entirely in Fabric-Attached Memory (FAM). +/// It provides an `std::unordered_map`-like interface with fixed types, using `FixedString` keys and +/// variable-length `Buffer` values. +/// +/// ## Architecture +/// +/// - **Hash table**: A flat FAM object holding `bucket_count` (1024) bucket slots. Each slot stores a +/// `FamList::Descriptor` (32 bytes). A zero `head` field means the bucket is empty. +/// - **Buckets**: Each non-empty bucket is a `FamList` whose nodes store key-value entries as: +/// `[key (32/64/128 bytes)] [value data (variable length)]` +/// - **Size counter**: An atomic FAM counter tracking total number of entries across all buckets. +/// +/// ## Concurrency +/// +/// Provides **multiple-reader, multiple-writer (MRMW)** safety: +/// +/// - **Bucket creation**: When the first element hashes to an empty bucket, a new `FamList` is +/// created using a CAS-based protocol on the bucket slot's `head` field: +/// - CAS `head` from 0 → `CREATING` sentinel to claim the slot. +/// - Create `FamList`, write remaining descriptor fields, then write the real `head` value. +/// - Competing writers spin-wait on the sentinel until the bucket is ready. +/// +/// - **Per-bucket operations**: Insertions, deletions, and lookups within a bucket delegate to +/// `FamList`, which provides lock-free insert and wait-free logical deletion. +/// +/// - **Size tracking**: Updated atomically after each insert/erase. +/// +/// ## Entry Layout in FamList Nodes +/// +/// Each FamList node's data payload holds: +/// ``` +/// | key (32/64/128 bytes, FixedString<32/64/128>) | value_data (node.length - key_size bytes) | +/// ``` +/// +/// ## Iterator +/// +/// The iterator walks buckets 0..bucket_count-1, and within each non-empty bucket walks +/// the FamList elements. Dereferencing returns a `FamMapEntry{key, value}` by value. +/// Iterators are safe during concurrent modifications via FamList's marked-node skipping. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamList.h" +#include "eckit/io/fam/FamMapEntry.h" +#include "eckit/io/fam/FamMapIterator.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamRegion.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +/// @brief Hash functor for FamMap keys. +template +struct FamHash { + std::size_t operator()(const T& key) const noexcept { + return std::hash{}(std::string_view{key.data(), T::static_size()}); + } +}; + +//---------------------------------------------------------------------------------------------------------------------- + +/// @brief Concurrent-safe, FAM-resident unordered associative container. +/// +/// Hash table with FamList buckets. Fixed key type `FixedString`, variable-length +/// `Buffer` values. Supports concurrent insert, find, erase, and iteration. +/// Iterators are forward-only and safe during concurrent modifications. +/// @tparam T Must be `FamMapEntry`, which defines key and value types and encoding. +template +class FamMap { + static_assert(IsFamMapEntry::value, "FamMap only supports T = FamMapEntry<...>"); + +public: // types + + using entry_type = T; + using key_type = typename entry_type::key_type; + using value_type = typename entry_type::value_type; + using hash_type = FamHash; + using size_type = fam::size_t; + + using iterator = FamMapIterator; + using const_iterator = FamMapConstIterator; + +public: // constants + + /// Key size in bytes (e.g. 32 for FixedString<32>). + static constexpr auto key_size = entry_type::key_size; + + /// Number of buckets in the hash table. Chosen as a power of two for efficient modulo. + static constexpr std::size_t bucket_count = 1024; + + /// needed for preventing concurrent double-init of buckets, as sentinel value in bucket head during creation + static constexpr fam::size_t creating = ~fam::size_t{0}; + + static constexpr auto table_suffix = ".t"; + static constexpr auto count_suffix = ".c"; + static constexpr auto lock_suffix = ".l"; + + /// Lock lease time-to-live. If a lock holder crashes, waiters can steal + /// the lock after this many seconds. + static constexpr std::chrono::seconds lock_ttl{30}; + +public: // methods + + /// Construct or open a FamMap in the given region with the given name. + FamMap(std::string name, FamRegion region); + + /// rules + FamMap(const FamMap&) = delete; + FamMap& operator=(const FamMap&) = delete; + FamMap(FamMap&&) = default; + FamMap& operator=(FamMap&&) = default; + + ~FamMap() = default; + + // ---- capacity ---- + + /// Return total number of entries across all buckets (atomic read). + [[nodiscard]] + size_type size() const; + + /// Check if the map has no entries. + [[nodiscard]] + bool empty() const; + + /// Return the average number of entries per bucket (size / bucket_count). + [[nodiscard]] + float loadFactor() const; + + // ---- iterators ---- + + /// Return iterator to the first entry (across all buckets). + iterator begin() const; + + const_iterator cbegin() const; + + /// Return past-the-end iterator. + iterator end() const; + + const_iterator cend() const; + + // ---- lookup ---- + + /// Find entry by key. Returns end() if not found. + [[nodiscard]] + iterator find(const key_type& key) const; + + /// Check if an entry with the given key exists. + [[nodiscard]] + bool contains(const key_type& key) const; + + /// Return the number of entries matching key. + /// For unique-key usage this is 0 or 1; may be >1 after forceInsert() creates duplicates. + [[nodiscard]] + size_type count(const key_type& key) const; + + // ---- modifiers (concurrent-safe) ---- + + /// Insert a key-value pair. If the key already exists, no insertion is performed. + /// Returns {iterator, true} on success, {iterator_to_existing, false} if key exists. + /// @important: This check-then-insert sequence is not atomic. + /// concurrent inserts of the same key may both insert resulting duplicates. + std::pair insert(const key_type& key, const void* data, size_type length); + + /// Insert with string_view value. + std::pair insert(const key_type& key, std::string_view data) { + return insert(key, data.data(), data.size()); + } + + /// Insert with Buffer value. + std::pair insert(const key_type& key, const Buffer& data) { return insert(key, data.view()); } + + /// Insert or replace a key-value pair. + /// First insert (via pushFront), then erase any previous entry. This ordering guarantees that + /// concurrent readers always see either the old or the new value — never an empty slot. + /// Returns {iterator, true} if no prior entry existed, {iterator, false} if an existing entry was replaced. + std::pair insertOrAssign(const key_type& key, const void* data, size_type length); + + /// insertOrAssign with string_view value. + std::pair insertOrAssign(const key_type& key, std::string_view data) { + return insertOrAssign(key, data.data(), data.size()); + } + + /// insertOrAssign with Buffer value. + std::pair insertOrAssign(const key_type& key, const Buffer& data) { + return insertOrAssign(key, data.view()); + } + + /// Insert a key-value pair unconditionally (no dedup check). Supports multi-valued keys. + /// The new entry is prepended (pushFront) so the latest entry for a key is found first. + /// Use insert() for unique-key semantics. + /// Returns an iterator to the newly inserted entry. + iterator forceInsert(const key_type& key, const void* data, size_type length); + + /// forceInsert with string_view value. + iterator forceInsert(const key_type& key, std::string_view data) { + return forceInsert(key, data.data(), data.size()); + } + + /// forceInsert with Buffer value. + iterator forceInsert(const key_type& key, const Buffer& data) { return forceInsert(key, data.view()); } + + /// Erase all entries with the given key. Returns the number of entries removed. + size_type erase(const key_type& key); + + /// Remove all entries from all buckets. + /// @pre No concurrent modifications. Not thread-safe. + void clear(); + + /// Insert all entries from @p other into this map. + /// Entries that already exist (by key) are skipped (insert semantics). + /// Data is copied across regions; FAM nodes are not spliced. + /// @pre No concurrent modifications to @p other during merge. + void merge(const FamMap& other); + + /// Acquire the map-wide FAM spinlock (lease-based). Pair with unlock(). + /// Stores a wall-clock timestamp in the FAM lock object. If the holder + /// crashes, waiters take the lock after @c lock_ttl seconds. + void lock(); + + /// Release the map-wide FAM spinlock. Pair with lock(). + void unlock(); + +private: // methods + + friend class FamMapIterator; + + /// Return the bucket index for a given key. + static std::size_t bucketIndex(const key_type& key) { return hash_type{}(key) % bucket_count; } + + /// Find the list iterator pointing to the entry with given key in a bucket. + /// Returns the bucket's end() if not found. + static FamListIterator findInBucket(const FamList& bucket, const key_type& key); + + /// Get the head offset of the bucket at the given index. + fam::size_t getBucketHead(std::size_t index) const; + + /// Read bucket descriptor at given index from the table. + FamList::Descriptor getBucketDescriptor(std::size_t index) const; + + /// Get or create the FamList for a given bucket index. Thread-safe via CAS. + FamList getOrCreateBucket(std::size_t index); + + /// Get existing bucket as FamList, or return nullopt if bucket is empty. + std::optional getBucket(std::size_t index) const; + + void print(std::ostream& out) const; + + friend std::ostream& operator<<(std::ostream& out, const FamMap& map) { + map.print(out); + return out; + } + +private: // members + + std::string name_; ///< Map name (used for bucket list naming) + FamRegion region_; ///< FAM region holding all map objects + FamObject table_; ///< Flat array of FamList::Descriptor, one per bucket + FamObject count_; ///< Atomic uint64 tracking total element count + FamObject lock_; ///< Lease-based FAM lock (uint64: 0=free, timestamp=held) +}; + +//---------------------------------------------------------------------------------------------------------------------- + +using FamMap32 = FamMap>; +using FamMap64 = FamMap>; +using FamMap128 = FamMap>; + +} // namespace eckit diff --git a/src/eckit/io/fam/FamMapEntry.h b/src/eckit/io/fam/FamMapEntry.h new file mode 100644 index 000000000..d78e4c1a2 --- /dev/null +++ b/src/eckit/io/fam/FamMapEntry.h @@ -0,0 +1,103 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamMapEntry.h +/// @author Metin Cakircali +/// @date Mar 2026 + +#pragma once + +#include +#include + +#include "eckit/exception/Exceptions.h" +#include "eckit/io/Buffer.h" +#include "eckit/types/FixedString.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +/// @brief Type returned by FamMap iterator dereference. +/// +/// Encapsulates the key and value of a map entry. +/// The buffer layout is: [key (KeySize bytes)] [value data (variable length)]. +/// This design allows the FamMap to store entries in FamList nodes without needing a separate structure in FAM for the +/// entry, and keeps all data in FAM for efficient access by multiple processes/threads. +template +struct FamMapEntry { + + using key_type = FixedString; + using value_type = Buffer; + using size_type = std::size_t; + + static constexpr auto key_size = static_cast(KeySize); + + /// Encode a key-value pair into a flat buffer for storage in FamList nodes. + /// Layout: [key (KeySize bytes)] [value data (length bytes)] + static Buffer encode(const key_type& key, const void* data, size_type length) { + Buffer payload(key_size + length); + std::memcpy(payload.data(), key.data(), key_size); + if (length > 0) { + void* value_ptr = static_cast(payload.data()) + key_size; + if (data) { + std::memcpy(value_ptr, data, length); + } + else { + std::memset(value_ptr, 0, length); + } + } + return payload; + } + + static key_type decodeKey(const Buffer& buffer) { + if (buffer.size() < key_size) { + throw eckit::SeriousBug("FamMapEntry: buffer too small to decode key!", Here()); + } + key_type key; + std::memcpy(key.data(), buffer.data(), key_size); + return key; + } + + static value_type decodeValue(const Buffer& buffer) { + if (buffer.size() < key_size) { + throw eckit::SeriousBug("FamMapEntry: buffer too small to decode value!", Here()); + } + const auto value_size = buffer.size() - key_size; + value_type value(value_size); + if (value_size > 0) { + std::memcpy(value.data(), static_cast(buffer.data()) + key_size, value_size); + } + return value; + } + + /// Decode a buffer into key + value + explicit FamMapEntry(const Buffer& buffer) : key{decodeKey(buffer)}, value{decodeValue(buffer)} {} + + key_type key; + value_type value; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +template +struct IsFamMapEntry : std::false_type {}; + +template +struct IsFamMapEntry> : std::true_type {}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamMapIterator.cc b/src/eckit/io/fam/FamMapIterator.cc new file mode 100644 index 000000000..f6807f6f0 --- /dev/null +++ b/src/eckit/io/fam/FamMapIterator.cc @@ -0,0 +1,117 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamMapIterator.h" + +#include + +#include "eckit/exception/Exceptions.h" +#include "eckit/io/fam/FamMap.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +template +FamMapIterator::FamMapIterator(const FamMap& map, const std::size_t bucket, const bool advance) : + map_{&map}, bucket_{bucket} { + if (advance) { + advanceToNextBucket(); + } +} + +template +FamMapIterator::FamMapIterator(const FamMap& map, const std::size_t bucket, FamListIterator iter, FamList list) : + map_{&map}, bucket_{bucket}, list_{std::move(list)}, iter_{std::move(iter)} {} + +//---------------------------------------------------------------------------------------------------------------------- + +template +bool FamMapIterator::hasMoreBuckets() const { + return bucket_ < FamMap::bucket_count; +} + +template +bool FamMapIterator::loadBucket() { + auto bucket = map_->getBucket(bucket_); + if (!bucket || bucket->empty()) { + list_.reset(); + iter_.reset(); + return false; + } + list_ = std::move(*bucket); + iter_ = list_->begin(); + return iter_ != list_->end(); +} + +template +void FamMapIterator::advanceToNextBucket() { + while (hasMoreBuckets()) { + if (loadBucket()) { + return; // found a non-empty bucket with entries + } + ++bucket_; + } + list_.reset(); + iter_.reset(); +} + +template +FamMapIterator& FamMapIterator::operator++() { + ASSERT(hasMoreBuckets()); + ASSERT(iter_.has_value()); + + ++(*iter_); + + if (*iter_ != list_->end()) { + return *this; + } + + ++bucket_; + advanceToNextBucket(); + + return *this; +} + +template +bool FamMapIterator::operator==(const FamMapIterator& other) const { + if (bucket_ != other.bucket_) { + return false; + } + if (!iter_.has_value() && !other.iter_.has_value()) { + return true; + } + if (iter_.has_value() && other.iter_.has_value()) { + return iter_->object() == other.iter_->object(); + } + return false; +} + +template +T FamMapIterator::operator*() const { + ASSERT(iter_.has_value()); + return T{**iter_}; +} + +//---------------------------------------------------------------------------------------------------------------------- + +// Explicit instantiations +template class FamMapIterator>; +template class FamMapIterator>; +template class FamMapIterator>; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamMapIterator.h b/src/eckit/io/fam/FamMapIterator.h new file mode 100644 index 000000000..d5604fbb4 --- /dev/null +++ b/src/eckit/io/fam/FamMapIterator.h @@ -0,0 +1,124 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamMapIterator.h +/// @author Metin Cakircali +/// @date Jul 2024 + +/// @brief Forward iterator for FamMap that walks all entries across all hash buckets. +/// +/// ## Iteration Order +/// +/// The iterator visits entries in bucket order (0 → bucket_count-1). Within each bucket, +/// entries are visited in FamList insertion order. Empty buckets are skipped. +/// +/// ## Thread Safety +/// +/// Safe during concurrent insertions and deletions. Relies on FamList iterator's +/// marked-node skipping to handle concurrent deletions transparently. + +#pragma once + +#include +#include +#include + +#include "eckit/io/fam/FamList.h" +#include "eckit/io/fam/FamMapEntry.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +template +class FamMap; + +/// @brief Forward iterator for FamMap. +/// +/// Walks bucket 0..N and within each bucket walks the FamList entries. +/// Dereference returns value_type (=FamMapEntry{key, value}) by value (data lives in FAM). +template +class FamMapIterator { + + static_assert(IsFamMapEntry::value, "FamMapIterator only supports T = FamMapEntry<...>"); + +public: // types + + /// single-pass forward traversal only + using iterator_category = std::forward_iterator_tag; + /// conventional default for single-pass iterators + using difference_type = std::ptrdiff_t; + /// value type returned by dereference operator (by value, not reference) + using value_type = T; + +public: // methods + + /// Construct begin/end iterator for the map. + /// @param map The owning FamMap. + /// @param bucket Bucket index (bucket_count for end). + /// @param advance If true, advances to the first non-empty entry. + FamMapIterator(const FamMap& map, std::size_t bucket, bool advance); + + /// Construct iterator pointing to a specific entry in a specific bucket. + FamMapIterator(const FamMap& map, std::size_t bucket, FamListIterator iter, FamList list); + + FamMapIterator(const FamMapIterator&) = delete; + FamMapIterator& operator=(const FamMapIterator&) = delete; + FamMapIterator(FamMapIterator&&) = default; + FamMapIterator& operator=(FamMapIterator&&) = default; + + ~FamMapIterator() = default; + + /// Advance to next entry. Crosses bucket boundaries. + FamMapIterator& operator++(); + + /// Compare iterators. + bool operator==(const FamMapIterator& other) const; + bool operator!=(const FamMapIterator& other) const { return !operator==(other); } + + /// Dereference: returns FamMapEntry{key, value} by value. + /// (pointer and reference are intentionally omitted here) + value_type operator*() const; + +private: // methods + + /// Advance bucket_ to the next non-empty bucket and load it. + void advanceToNextBucket(); + + /// Load the bucket FamList at the bucket_ and position iter_ to its begin(). + /// Returns false if bucket is empty. + bool loadBucket(); + + /// Check if there's more buckets. + bool hasMoreBuckets() const; + +private: // members + + const FamMap* map_; + std::size_t bucket_; + std::optional list_; + std::optional iter_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +/// @brief Const variant of FamMapIterator, for use in const contexts and range-based for loops. +/// Otherwise identical to FamMapIterator, as it already returns entries by value. +template +using FamMapConstIterator = FamMapIterator; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamName.cc b/src/eckit/io/fam/FamName.cc new file mode 100644 index 000000000..830892d16 --- /dev/null +++ b/src/eckit/io/fam/FamName.cc @@ -0,0 +1,76 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamName.h" + +#include +#include +#include +#include + +#include "eckit/filesystem/URI.h" +#include "eckit/io/fam/FamSessionManager.h" +#include "eckit/io/fam/FamTypes.h" +#include "eckit/net/Endpoint.h" +#include "eckit/serialisation/Stream.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +FamName::FamName(const net::Endpoint& endpoint, FamPath path) : endpoint_{endpoint}, path_{std::move(path)} {} + +FamName::FamName(const URI& uri) : FamName(uri.endpoint(), FamPath(uri)) {} + +FamName::FamName(Stream& stream) : endpoint_{stream}, path_{stream} {} + +FamName::~FamName() = default; + +//---------------------------------------------------------------------------------------------------------------------- + +FamSessionManager::Session FamName::session() const { + return FamSessionManager::instance().session(endpoint_); +} + +std::string FamName::asString() const { + std::ostringstream oss; + oss << fam::scheme << "://" << endpoint_ << path_; + return oss.str(); +} + +URI FamName::uri() const { + return {fam::scheme, endpoint_, path_.asString()}; +} + +void FamName::print(std::ostream& out) const { + out << "endpoint=" << endpoint_ << ", path=" << path_; +} + +//---------------------------------------------------------------------------------------------------------------------- + +std::ostream& operator<<(std::ostream& out, const FamName& name) { + name.print(out); + return out; +} + +Stream& operator<<(Stream& out, const FamName& name) { + out << name.endpoint_; + out << name.path_; + return out; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamName.h b/src/eckit/io/fam/FamName.h new file mode 100644 index 000000000..9c058761d --- /dev/null +++ b/src/eckit/io/fam/FamName.h @@ -0,0 +1,85 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamName.h +/// @author Metin Cakircali +/// @date May 2024 + +#pragma once + +#include +#include + +#include "eckit/io/fam/FamPath.h" +#include "eckit/io/fam/FamSessionManager.h" +#include "eckit/net/Endpoint.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +class FamName { +public: // methods + + FamName(const net::Endpoint& endpoint, FamPath path); + FamName(const net::Endpoint& endpoint, const std::string& path) : FamName(endpoint, FamPath(path)) {} + + explicit FamName(const URI& uri); + + explicit FamName(Stream& stream); + + // rules + FamName(const FamName&) = default; + FamName& operator=(const FamName&) = default; + FamName(FamName&&) = default; + FamName& operator=(FamName&&) = default; + + virtual ~FamName(); + + virtual bool exists() const = 0; + + /// @todo implement + // virtual FamItem lookup() const = 0; + + std::string asString() const; + + URI uri() const; + + const net::Endpoint& endpoint() const { return endpoint_; } + + const FamPath& path() const { return path_; } + +protected: // methods + + FamSessionManager::Session session() const; + + FamPath& path() { return path_; } + + virtual void print(std::ostream& out) const; + + friend std::ostream& operator<<(std::ostream& out, const FamName& name); + + friend Stream& operator<<(Stream& out, const FamName& name); + +private: // members + + net::Endpoint endpoint_; + + FamPath path_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamObject.cc b/src/eckit/io/fam/FamObject.cc new file mode 100644 index 000000000..8bcba7f08 --- /dev/null +++ b/src/eckit/io/fam/FamObject.cc @@ -0,0 +1,207 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamObject.h" + +#include +#include +#include +#include + +#include "fam/fam.h" + +#include "eckit/exception/Exceptions.h" +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamProperty.h" +#include "eckit/io/fam/FamSession.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +FamObject::FamObject(FamSession& session, FamObjectDescriptor* object) : + session_{session.shared_from_this()}, object_{object} { + ASSERT(session_); + ASSERT(object_); +} + +FamObject::FamObject(FamSession& session, const std::uint64_t region, const std::uint64_t offset) : + session_{session.shared_from_this()}, + object_{std::make_shared(Fam_Global_Descriptor{region, offset})} { + ASSERT(session_); + ASSERT(object_); +} + +bool FamObject::operator==(const FamObject& other) const { + const auto desc = object_->get_global_descriptor(); + const auto o_desc = other.object_->get_global_descriptor(); + return (desc.regionId == o_desc.regionId && desc.offset == o_desc.offset); +} + +//---------------------------------------------------------------------------------------------------------------------- +// OPERATIONS + +void FamObject::replaceWith(const FamDescriptor& object) { + object_ = std::make_shared(Fam_Global_Descriptor{object.region, object.offset}); +} + +void FamObject::deallocate() const { + session_->deallocateObject(*object_); +} + +bool FamObject::exists() const { + return (object_->get_desc_status() != openfam::Fam_Descriptor_Status::DESC_INVALID); +} + +//---------------------------------------------------------------------------------------------------------------------- +// PROPERTIES + +fam::index_t FamObject::regionId() const { + return object_->get_global_descriptor().regionId; +} + +fam::index_t FamObject::offset() const { + return object_->get_global_descriptor().offset; +} + +fam::size_t FamObject::size() const { + return object_->get_size(); +} + +fam::perm_t FamObject::permissions() const { + return object_->get_perm(); +} + +std::string FamObject::name() const { + return object_->get_name() ? object_->get_name() : ""; +} + +FamProperty FamObject::property() const { + return {size(), permissions(), name(), object_->get_uid(), object_->get_gid()}; +} + +//---------------------------------------------------------------------------------------------------------------------- +// DATA + +void FamObject::put(const void* buffer, const fam::size_t offset, const fam::size_t length) const { + session_->put(*object_, buffer, offset, length); +} + +void FamObject::get(void* buffer, const fam::size_t offset, const fam::size_t length) const { + session_->get(*object_, buffer, offset, length); +} + +auto FamObject::data(const fam::size_t offset) const -> value_type { + ASSERT(offset <= this->size()); + const auto size = this->size() - offset; + Buffer buffer(size); + if (size > 0) { + get(buffer.data(), offset, size); + } + return buffer; +} + +//---------------------------------------------------------------------------------------------------------------------- +// ATOMIC + +template +T FamObject::fetch(const fam::size_t offset) const { + return session_->fetch(*object_, offset); +} + +template +void FamObject::set(const fam::size_t offset, const T value) const { + session_->set(*object_, offset, value); +} + +template +void FamObject::add(const fam::size_t offset, const T value) const { + session_->add(*object_, offset, value); +} + +template +void FamObject::subtract(const fam::size_t offset, const T value) const { + session_->subtract(*object_, offset, value); +} + +template +T FamObject::swap(const fam::size_t offset, const T value) const { // NOLINT + return session_->swap(*object_, offset, value); +} + +template +T FamObject::compareSwap(const fam::size_t offset, const T old_value, const T new_value) const { + return session_->compareSwap(*object_, offset, old_value, new_value); +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamObject::print(std::ostream& out) const { + out << "FamObject[" << property() << ", region=" << regionId() << ", offset=" << offset() << "]"; +} + +std::ostream& operator<<(std::ostream& out, const FamObject& object) { + object.print(out); + return out; +} + +//---------------------------------------------------------------------------------------------------------------------- +// forward instantiations + +template int32_t FamObject::fetch(const fam::size_t) const; +template int64_t FamObject::fetch(const fam::size_t) const; +template openfam::int128_t FamObject::fetch(const fam::size_t) const; +template uint32_t FamObject::fetch(const fam::size_t) const; +template uint64_t FamObject::fetch(const fam::size_t) const; +template float FamObject::fetch(const fam::size_t) const; +template double FamObject::fetch(const fam::size_t) const; + +template void FamObject::set(const fam::size_t, const int32_t) const; +template void FamObject::set(const fam::size_t, const int64_t) const; +template void FamObject::set(const fam::size_t, const openfam::int128_t) const; +template void FamObject::set(const fam::size_t, const uint32_t) const; +template void FamObject::set(const fam::size_t, const uint64_t) const; +template void FamObject::set(const fam::size_t, const float) const; +template void FamObject::set(const fam::size_t, const double) const; + +template void FamObject::add(const fam::size_t, const int32_t) const; +template void FamObject::add(const fam::size_t, const int64_t) const; +template void FamObject::add(const fam::size_t, const uint32_t) const; +template void FamObject::add(const fam::size_t, const uint64_t) const; +template void FamObject::add(const fam::size_t, const float) const; +template void FamObject::add(const fam::size_t, const double) const; + +template void FamObject::subtract(const fam::size_t, const int32_t) const; +template void FamObject::subtract(const fam::size_t, const int64_t) const; +template void FamObject::subtract(const fam::size_t, const uint32_t) const; +template void FamObject::subtract(const fam::size_t, const uint64_t) const; +template void FamObject::subtract(const fam::size_t, const float) const; +template void FamObject::subtract(const fam::size_t, const double) const; + +template int32_t FamObject::swap(const fam::size_t, const int32_t) const; +template int64_t FamObject::swap(const fam::size_t, const int64_t) const; +template uint32_t FamObject::swap(const fam::size_t, const uint32_t) const; +template uint64_t FamObject::swap(const fam::size_t, const uint64_t) const; +template float FamObject::swap(const fam::size_t, const float) const; +template double FamObject::swap(const fam::size_t, const double) const; + +template int32_t FamObject::compareSwap(const fam::size_t, const int32_t, const int32_t) const; +template int64_t FamObject::compareSwap(const fam::size_t, const int64_t, const int64_t) const; +template uint32_t FamObject::compareSwap(const fam::size_t, const uint32_t, const uint32_t) const; +template uint64_t FamObject::compareSwap(const fam::size_t, const uint64_t, const uint64_t) const; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamObject.h b/src/eckit/io/fam/FamObject.h new file mode 100644 index 000000000..60b96e52a --- /dev/null +++ b/src/eckit/io/fam/FamObject.h @@ -0,0 +1,137 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamObject.h +/// @author Metin Cakircali +/// @date Mar 2024 + +#pragma once + +#include +#include +#include +#include + +#include "eckit/io/fam/FamProperty.h" + +namespace eckit { + +class Buffer; +class FamSession; + +//---------------------------------------------------------------------------------------------------------------------- + +class FamObject { +public: // types + + using value_type = Buffer; + +public: // methods + + FamObject(FamSession& session, FamObjectDescriptor* object); + + FamObject(FamSession& session, std::uint64_t region, std::uint64_t offset); + + ~FamObject() = default; + + // rules: move-only + FamObject(const FamObject&) = delete; + FamObject& operator=(const FamObject&) = delete; + FamObject(FamObject&&) = default; + FamObject& operator=(FamObject&&) = default; + + // operators + + bool operator==(const FamObject& other) const; + + bool operator!=(const FamObject& other) const { return !operator==(other); } + + void replaceWith(const FamDescriptor& object); + + void deallocate() const; + + bool exists() const; + + // properties + + fam::index_t regionId() const; + + fam::index_t offset() const; + + FamDescriptor descriptor() const { return {regionId(), offset()}; } + + fam::size_t size() const; + + fam::perm_t permissions() const; + + std::string name() const; + + FamProperty property() const; + + // data access + + void put(const void* buffer, fam::size_t offset, fam::size_t length) const; + + void get(void* buffer, fam::size_t offset, fam::size_t length) const; + + template + T get(const fam::size_t offset = 0) const { + auto buffer = T{0}; + get(&buffer, offset, sizeof(T)); + return buffer; + } + + template + void put(const T& buffer, const fam::size_t offset) const { + put(&buffer, offset, sizeof(T)); + } + + value_type data(fam::size_t offset = 0) const; + + // atomic operations + + template + void set(fam::size_t offset, T value) const; + + template + T fetch(fam::size_t offset) const; + + template + void add(fam::size_t offset, T value) const; + + template + void subtract(fam::size_t offset, T value) const; + + template + T swap(fam::size_t offset, T value) const; + + template + T compareSwap(fam::size_t offset, T old_value, T new_value) const; + +private: // methods + + void print(std::ostream& out) const; + + friend std::ostream& operator<<(std::ostream& out, const FamObject& object); + +private: // members + + std::shared_ptr session_; + std::shared_ptr object_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamObjectName.cc b/src/eckit/io/fam/FamObjectName.cc new file mode 100644 index 000000000..9e7dfc8c2 --- /dev/null +++ b/src/eckit/io/fam/FamObjectName.cc @@ -0,0 +1,77 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamObjectName.h" + +#include +#include + +#include "eckit/config/LibEcKit.h" +#include "eckit/exception/Exceptions.h" +#include "eckit/io/Length.h" +#include "eckit/io/Offset.h" +#include "eckit/io/fam/FamHandle.h" +#include "eckit/io/fam/FamSession.h" +#include "eckit/log/Log.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +FamObjectName& FamObjectName::withObject(const std::string& object_name) { + path().objectName(object_name); + return *this; +} + +FamObjectName& FamObjectName::withUUID() { + return withObject(path().generateUUID()); +} + +FamObject FamObjectName::lookup() const { + return session()->lookupObject(path().regionName(), path().objectName()); +} + +FamObject FamObjectName::allocate(const fam::size_t object_size, const bool overwrite) const { + return session()->lookupRegion(path().regionName()).allocateObject(object_size, path().objectName(), overwrite); +} + +bool FamObjectName::exists() const { + try { + return lookup().exists(); + } + catch (const NotFound& not_found) { + Log::debug() << not_found << '\n'; + } + catch (const PermissionDenied& permission_denied) { + Log::debug() << permission_denied << '\n'; + } + return false; +} + +//---------------------------------------------------------------------------------------------------------------------- + +DataHandle* FamObjectName::dataHandle(const bool overwrite) const { + return new FamHandle(*this, overwrite); +} + +DataHandle* FamObjectName::partHandle(const OffsetList& offsets, const LengthList& lengths) const { + ASSERT(!offsets.empty() && !lengths.empty()); + // FAM objects are single contiguous allocations — only the first part is supported. + return new FamHandle(*this, offsets[0], lengths[0], true); +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamObjectName.h b/src/eckit/io/fam/FamObjectName.h new file mode 100644 index 000000000..0ab4a2517 --- /dev/null +++ b/src/eckit/io/fam/FamObjectName.h @@ -0,0 +1,63 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamObjectName.h +/// @author Metin Cakircali +/// @date May 2024 + +#pragma once + +#include + +#include "eckit/io/Length.h" +#include "eckit/io/Offset.h" +#include "eckit/io/fam/FamName.h" +#include "eckit/io/fam/FamProperty.h" + +namespace eckit { + +class DataHandle; +class FamObject; + +//---------------------------------------------------------------------------------------------------------------------- + +class FamObjectName : public FamName { +public: // methods + + using FamName::FamName; + + FamObjectName& withObject(const std::string& object_name); + + /// @brief Replaces [objectName] with UUID (e.g., 34bd2214-2a97-5a8a-802f-76ebefd84816) + FamObjectName& withUUID(); + + FamObject lookup() const; + + FamObject allocate(fam::size_t object_size, bool overwrite = false) const; + + bool exists() const override; + + // data handles + + [[nodiscard]] + DataHandle* dataHandle(bool overwrite = false) const; + + [[nodiscard]] + DataHandle* partHandle(const OffsetList& offsets, const LengthList& lengths) const; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamPath.cc b/src/eckit/io/fam/FamPath.cc new file mode 100644 index 000000000..5c1901a7b --- /dev/null +++ b/src/eckit/io/fam/FamPath.cc @@ -0,0 +1,117 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamPath.h" + +#include + +#include +#include +#include +#include + +#include "eckit/exception/Exceptions.h" +#include "eckit/filesystem/URI.h" +#include "eckit/io/fam/FamTypes.h" +#include "eckit/serialisation/Stream.h" +#include "eckit/utils/Tokenizer.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +namespace { + +std::tuple parsePath(const std::string& path) { + const auto names = Tokenizer("/").tokenize(path); + const auto count = names.size(); + if (count > 2) { + throw UserError("Invalid FAM path: " + path, Here()); + } + switch (count) { + case 1: + return {names[0], ""}; + case 2: + return {names[0], names[1]}; + default: + return {}; + } +} + +/* ISO Object Identifier Namespace */ +const uuid_t ns_oid = {0x6b, 0xa7, 0xb8, 0x12, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8}; + +std::string generateUuid(const std::string& name) { + std::string result = "00000000-0000-0000-0000-000000000000"; + + uuid_t oid; + uuid_generate_sha1(&oid[0], &ns_oid[0], name.c_str(), name.length()); + uuid_unparse(&oid[0], result.data()); + + return result; +} + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- + +FamPath::FamPath(std::string region, std::string object) : + regionName_{std::move(region)}, objectName_{std::move(object)} {} + +FamPath::FamPath(const std::string& path) { + std::tie(regionName_, objectName_) = parsePath(path); +} + +FamPath::FamPath(const char* path) : FamPath(std::string(path)) {} + +FamPath::FamPath(const URI& uri) : FamPath(uri.name()) { + ASSERT(uri.scheme() == fam::scheme); +} + +FamPath::FamPath(Stream& stream) { + stream >> regionName_; + stream >> objectName_; +} + +bool FamPath::operator==(const FamPath& other) const { + return (regionName_ == other.regionName_ && objectName_ == other.objectName_); +} + +std::string FamPath::generateUUID() const { + return generateUuid(asString()); +} + +void FamPath::encode(Stream& stream) const { + stream << regionName_; + stream << objectName_; +} + +std::string FamPath::asString() const { + return objectName_.empty() ? '/' + regionName_ : '/' + regionName_ + '/' + objectName_; +} + +std::ostream& operator<<(std::ostream& out, const FamPath& path) { + out << path.asString(); + return out; +} + +Stream& operator<<(Stream& stream, const FamPath& name) { + name.encode(stream); + return stream; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamPath.h b/src/eckit/io/fam/FamPath.h new file mode 100644 index 000000000..a10977138 --- /dev/null +++ b/src/eckit/io/fam/FamPath.h @@ -0,0 +1,75 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamPath.h +/// @author Metin Cakircali +/// @date Jun 2024 + +#pragma once + +#include +#include + +namespace eckit { + +class URI; +class Stream; + +//---------------------------------------------------------------------------------------------------------------------- + +class FamPath { +public: + + FamPath() = default; + + FamPath(std::string region, std::string object); + + explicit FamPath(const std::string& path); + + explicit FamPath(const char* path); + + explicit FamPath(const URI& uri); + + explicit FamPath(Stream& stream); + + bool operator==(const FamPath& other) const; + + std::string generateUUID() const; + + void encode(Stream& stream) const; + + std::string asString() const; + + auto regionName() const -> const std::string& { return regionName_; } + auto objectName() const -> const std::string& { return objectName_; } + + void regionName(std::string name) { regionName_ = std::move(name); } + void objectName(std::string name) { objectName_ = std::move(name); } + +private: + + friend std::ostream& operator<<(std::ostream& out, const FamPath& path); + + friend Stream& operator<<(Stream& stream, const FamPath& name); + +private: // members + + std::string regionName_; + std::string objectName_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamProperty.cc b/src/eckit/io/fam/FamProperty.cc new file mode 100644 index 000000000..3fa1a787a --- /dev/null +++ b/src/eckit/io/fam/FamProperty.cc @@ -0,0 +1,80 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamProperty.h" + +#include "eckit/exception/Exceptions.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +namespace { + +fam::perm_t stringToPerm(const std::string& perm) { + std::size_t pos = 0; + const auto value = std::stoul(perm, &pos, 8); + + // reject partial parse (e.g. "0644abc") and values beyond valid permissions + ASSERT_MSG(pos == perm.size(), "invalid permission string: " + perm); + ASSERT_MSG(value <= 07777, "permission value out of range: " + perm); + + return static_cast(value); +} + +std::string permToString(fam::perm_t perm) { + std::ostringstream oss; + oss << std::oct << perm; + return oss.str(); +} + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- + +FamProperty::FamProperty(fam::size_t size, fam::perm_t perm, std::string name, std::uint32_t uid, std::uint32_t gid) : + size{size}, perm{perm}, name{std::move(name)}, uid{uid}, gid{gid} {} + +FamProperty::FamProperty(fam::size_t size, fam::perm_t perm, const std::string& name) : + FamProperty(size, perm, name, getuid(), getgid()) {} + +FamProperty::FamProperty(fam::size_t size, fam::perm_t perm) : FamProperty(size, perm, "") {} + +FamProperty::FamProperty(fam::size_t size, const std::string& perm) : FamProperty(size, stringToPerm(perm)) {} + +void FamProperty::print(std::ostream& out) const { + out << "Property[size=" << size << ", perm=" << perm << "(" << permToString(perm) << ")" << ",name=" << name + << ",uid=" << uid << ",gid=" << gid << "]"; +} + +//---------------------------------------------------------------------------------------------------------------------- + +std::ostream& operator<<(std::ostream& out, const FamProperty& prop) { + prop.print(out); + return out; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamProperty.h b/src/eckit/io/fam/FamProperty.h new file mode 100644 index 000000000..3ea55b950 --- /dev/null +++ b/src/eckit/io/fam/FamProperty.h @@ -0,0 +1,62 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamProperty.h +/// @author Metin Cakircali +/// @date Mar 2024 + +#pragma once + +#include +#include + +#include "eckit/io/fam/FamTypes.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +struct FamProperty { + + static constexpr fam::perm_t default_perm = 0640; + + FamProperty() = default; + + FamProperty(fam::size_t size, fam::perm_t perm, std::string name, std::uint32_t uid, std::uint32_t gid); + + FamProperty(fam::size_t size, fam::perm_t perm, const std::string& name); + + FamProperty(fam::size_t size, fam::perm_t perm); + + FamProperty(fam::size_t size, const std::string& perm); + + void print(std::ostream& out) const; + + bool operator==(const FamProperty& other) const { + return (size == other.size && perm == other.perm && name == other.name && uid == other.uid && gid == other.gid); + } + + friend std::ostream& operator<<(std::ostream& out, const FamProperty& prop); + + fam::size_t size{0}; + fam::perm_t perm{default_perm}; + std::string name; + std::uint32_t uid{0}; + std::uint32_t gid{0}; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamRegion.cc b/src/eckit/io/fam/FamRegion.cc new file mode 100644 index 000000000..c3b8b58fe --- /dev/null +++ b/src/eckit/io/fam/FamRegion.cc @@ -0,0 +1,166 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamRegion.h" + +#include +#include +#include +#include + +#include "fam/fam.h" + +#include "eckit/exception/Exceptions.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamProperty.h" +#include "eckit/io/fam/FamSession.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +FamRegion::FamRegion(FamSession& session, FamRegionDescriptor* region) : + session_{session.shared_from_this()}, region_{region} { + ASSERT(region_); +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamRegion::destroy() const { + session_->destroyRegion(*region_); +} + +bool FamRegion::exists() const { + return (region_->get_desc_status() != openfam::Fam_Descriptor_Status::DESC_INVALID); +} + +//---------------------------------------------------------------------------------------------------------------------- +// PROPERTIES + +fam::index_t FamRegion::index() const { + return region_->get_global_descriptor().regionId; +} + +fam::size_t FamRegion::size() const { + return region_->get_size(); +} + +fam::perm_t FamRegion::permissions() const { + return region_->get_perm(); +} + +std::string FamRegion::name() const { + return region_->get_name() ? region_->get_name() : ""; +} + +FamProperty FamRegion::property() const { + return {size(), permissions(), name()}; +} + +void FamRegion::setRegionLevelPermissions() const { + region_->set_permissionLevel(Fam_Permission_Level::REGION); +} + +void FamRegion::setObjectLevelPermissions() const { + region_->set_permissionLevel(Fam_Permission_Level::DATAITEM); +} + +//---------------------------------------------------------------------------------------------------------------------- +// OBJECT factory methods + +// Creates a FamObject wrapper around an existing object identified by {regionId, offset} +FamObject FamRegion::proxyObject(const fam::index_t offset) const { + return session_->proxyObject(index(), offset); +} + +FamObject FamRegion::lookupObject(const std::string& object_name) const { + return session_->lookupObject(name(), object_name); +} + +FamObject FamRegion::allocateObject(const fam::size_t object_size, const fam::perm_t object_perm, + const std::string& object_name, const bool overwrite) const { + if (overwrite) { + return session_->ensureAllocateObject(*region_, object_size, object_perm, object_name); + } + return session_->allocateObject(*region_, object_size, object_perm, object_name); +} + +void FamRegion::deallocateObject(const std::string& object_name) const { + session_->deallocateObject(region_->get_name(), object_name); +} + +FamObject FamRegion::ensureObject(const fam::size_t object_size, const std::string& object_name) const { + try { + return allocateObject(object_size, object_name); + } + catch (const AlreadyExists&) { + auto object = lookupObject(object_name); + if (object.size() != object_size) { + std::ostringstream msg; + msg << "FamRegion::ensureObject: size mismatch for object '" << object_name << "' in region '" + << region_->get_name() << "': existing=" << object.size() << ", requested=" << object_size; + throw SeriousBug(msg.str(), Here()); + } + return object; + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamRegion::print(std::ostream& out) const { + out << "FamRegion[" << property() << ",status="; + switch (region_->get_desc_status()) { + case openfam::Fam_Descriptor_Status::DESC_INVALID: + out << "invalid"; + break; + case openfam::Fam_Descriptor_Status::DESC_INIT_DONE: + out << "initialized"; + break; + case openfam::Fam_Descriptor_Status::DESC_INIT_DONE_BUT_KEY_NOT_VALID: + out << "initialized_invalidkey"; + break; + case openfam::Fam_Descriptor_Status::DESC_UNINITIALIZED: + out << "uninitialized"; + break; + default: + out << "unknown"; + break; + } + out << ",perm. Level="; + switch (region_->get_permissionLevel()) { + case Fam_Permission_Level::REGION: + out << "region"; + break; + case Fam_Permission_Level::DATAITEM: + out << "dataitem"; + break; + case Fam_Permission_Level::PERMISSION_LEVEL_DEFAULT: + out << "default"; + break; + default: + out << "unknown"; + break; + } + out << "]"; +} + +std::ostream& operator<<(std::ostream& out, const FamRegion& region) { + region.print(out); + return out; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamRegion.h b/src/eckit/io/fam/FamRegion.h new file mode 100644 index 000000000..302bc1698 --- /dev/null +++ b/src/eckit/io/fam/FamRegion.h @@ -0,0 +1,93 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamRegion.h +/// @author Metin Cakircali +/// @date Mar 2024 + +#pragma once + +#include +#include +#include + +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamProperty.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +class FamRegion { + +public: // methods + + FamRegion(FamSession& session, FamRegionDescriptor* region); + + void destroy() const; + + bool exists() const; + + // properties + + fam::index_t index() const; + + fam::size_t size() const; + + fam::perm_t permissions() const; + + std::string name() const; + + FamProperty property() const; + + void setObjectLevelPermissions() const; + + void setRegionLevelPermissions() const; + + // object methods + + /// @note this avoids invoking fam, as in lookupObject. + FamObject proxyObject(fam::index_t offset) const; + + FamObject lookupObject(const std::string& object_name) const; + + FamObject allocateObject(fam::size_t object_size, fam::perm_t object_perm, const std::string& object_name = "", + bool overwrite = false) const; + + FamObject allocateObject(fam::size_t object_size, const std::string& object_name = "", + bool overwrite = false) const { + return allocateObject(object_size, permissions(), object_name, overwrite); + } + + /// Allocate a named object, or look it up if it already exists (idempotent). + FamObject ensureObject(fam::size_t object_size, const std::string& object_name) const; + + void deallocateObject(const std::string& object_name) const; + +private: // methods + + void print(std::ostream& out) const; + + friend std::ostream& operator<<(std::ostream& out, const FamRegion& region); + +private: // members + + std::shared_ptr session_; + std::shared_ptr region_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamRegionName.cc b/src/eckit/io/fam/FamRegionName.cc new file mode 100644 index 000000000..dc6adec6d --- /dev/null +++ b/src/eckit/io/fam/FamRegionName.cc @@ -0,0 +1,74 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamRegionName.h" + +#include + +#include "eckit/config/LibEcKit.h" +#include "eckit/exception/Exceptions.h" +#include "eckit/filesystem/URI.h" +#include "eckit/io/fam/FamObjectName.h" +#include "eckit/io/fam/FamRegion.h" +#include "eckit/io/fam/FamSession.h" +#include "eckit/io/fam/FamTypes.h" +#include "eckit/log/Log.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +FamRegionName& FamRegionName::withRegion(const std::string& region_name) { + path().regionName(region_name); + return *this; +} + +FamObjectName FamRegionName::object(const std::string& object_name) const { + return {endpoint(), {path().regionName(), object_name}}; +} + +FamRegion FamRegionName::lookup() const { + return session()->lookupRegion(path().regionName()); +} + +FamRegion FamRegionName::create(const fam::size_t region_size, const fam::perm_t region_perm, + const bool overwrite) const { + if (overwrite) { + return session()->ensureCreateRegion(region_size, region_perm, path().regionName()); + } + return session()->createRegion(region_size, region_perm, path().regionName()); +} + +bool FamRegionName::exists() const { + try { + return lookup().exists(); + } + catch (const NotFound& not_found) { + Log::debug() << not_found << '\n'; + } + catch (const PermissionDenied& permission_denied) { + Log::debug() << permission_denied << '\n'; + } + return false; +} + +bool FamRegionName::uriBelongs(const URI& uri) const { + return uri.scheme() == fam::scheme && uri.endpoint() == endpoint() && + FamPath(uri).regionName() == path().regionName(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamRegionName.h b/src/eckit/io/fam/FamRegionName.h new file mode 100644 index 000000000..8ec25840f --- /dev/null +++ b/src/eckit/io/fam/FamRegionName.h @@ -0,0 +1,55 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamRegionName.h +/// @author Metin Cakircali +/// @date May 2024 + +#pragma once + +#include + +#include "eckit/io/fam/FamName.h" +#include "eckit/io/fam/FamObjectName.h" +#include "eckit/io/fam/FamProperty.h" + +namespace eckit { + +class URI; +class FamRegion; + +//---------------------------------------------------------------------------------------------------------------------- + +class FamRegionName : public FamName { +public: // methods + + using FamName::FamName; + + FamRegionName& withRegion(const std::string& region_name); + + FamObjectName object(const std::string& object_name) const; + + FamRegion lookup() const; + + FamRegion create(fam::size_t region_size, fam::perm_t region_perm, bool overwrite = false) const; + + bool exists() const override; + + bool uriBelongs(const URI& uri) const; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamSession.cc b/src/eckit/io/fam/FamSession.cc new file mode 100644 index 000000000..9ed242813 --- /dev/null +++ b/src/eckit/io/fam/FamSession.cc @@ -0,0 +1,428 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "FamSession.h" + +#include // mode_t + +#include +#include // isspace isprint +#include +#include +#include +#include +#include +#include + +#include "fam/fam.h" +#include "fam/fam_exception.h" + +#include "eckit/config/LibEcKit.h" +#include "eckit/exception/Exceptions.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamProperty.h" +#include "eckit/io/fam/FamRegion.h" +#include "eckit/log/CodeLocation.h" +#include "eckit/log/Log.h" +#include "eckit/net/Endpoint.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- +// HELPERS + +namespace { + +std::unique_ptr initializeFamSession(const std::string& name, const net::Endpoint& endpoint) { + Log::debug() << "Initializing FAM session: " << name << " with endpoint " << endpoint << '\n'; + + auto fam = std::make_unique(); + + try { + // pins + auto runtime = std::string{"NONE"}; + auto host = endpoint.host(); + auto port = std::to_string(endpoint.port()); + + Fam_Options options; + ::memset(static_cast(&options), 0, sizeof(Fam_Options)); + options.runtime = runtime.data(); + options.cisServer = host.data(); + options.grpcPort = port.data(); + + fam->fam_initialize(name.c_str(), &options); + } + catch (openfam::Fam_Exception& e) { + fam->fam_abort(-1); + throw Exception(e.fam_error_msg(), Here()); + } + + return fam; +} + +bool isValidName(std::string_view str) { + if (str.empty()) { + return false; + } + return std::all_of(str.begin(), str.end(), [](char chr) { + const auto uchr = static_cast(chr); + return std::isprint(uchr) != 0 && std::isspace(uchr) == 0; + }); +} + + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- +// SESSION + +FamSession::FamSession(std::string name, const net::Endpoint& endpoint) : name_{std::move(name)}, endpoint_{endpoint} { + ASSERT(isValidName(name_)); +} + +FamSession::~FamSession() { + if (fam_) { + try { + fam_->fam_finalize(name_.c_str()); + } + catch (openfam::Fam_Exception& e) { + Log::error() << "Failed to finalize session: " << name_ << ", msg=" << e.fam_error_msg() << '\n'; + fam_->fam_abort(-1); + } + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamSession::print(std::ostream& out) const { + out << "FamSession[name=" << name_ << "]"; +} + +std::ostream& operator<<(std::ostream& out, const FamSession& session) { + session.print(out); + return out; +} + +template +auto FamSession::invokeFam(Func&& fn_ptr, Args&&... args) { + if (!fam_) { + fam_ = initializeFamSession(name_, endpoint_); + } + + try { + return (fam_.get()->*std::forward(fn_ptr))(std::forward(args)...); + } + catch (openfam::Fam_Exception& e) { + const auto code = e.fam_error(); + if (code == openfam::Fam_Error::FAM_ERR_NOTFOUND) { + throw NotFound(e.fam_error_msg()); + } + if (code == openfam::Fam_Error::FAM_ERR_ALREADYEXIST) { + throw AlreadyExists(e.fam_error_msg()); + } + if (code == openfam::Fam_Error::FAM_ERR_NOPERM) { + throw PermissionDenied(e.fam_error_msg()); + } + if (code == openfam::Fam_Error::FAM_ERR_INVALID) { + throw BadValue(e.fam_error_msg()); + } + if (code == openfam::Fam_Error::FAM_ERR_NO_SPACE) { + throw OutOfStorage(e.fam_error_msg()); + } + if (code == openfam::Fam_Error::FAM_ERR_OUTOFRANGE) { + throw OutOfRange(e.fam_error_msg(), Here()); + } + if (code == openfam::Fam_Error::FAM_ERR_METADATA) { + throw NotFound(e.fam_error_msg()); + } + if (code == openfam::Fam_Error::FAM_ERR_RPC) { + std::string option_name = "CIS_SERVER"; + const auto* server_cstr = static_cast(fam_->fam_get_option(option_name.data())); + const std::string server_name = server_cstr ? server_cstr : ""; + throw RemoteException(e.fam_error_msg(), server_name); + } + throw SeriousBug("Code=" + std::to_string(code) + ' ' + e.fam_error_msg()); + } +} + + +//---------------------------------------------------------------------------------------------------------------------- +// REGION + +FamRegion FamSession::lookupRegion(const std::string& region_name) { + ASSERT(isValidName(region_name)); + + auto* region = invokeFam(&openfam::fam::fam_lookup_region, region_name.c_str()); + + return {*this, region}; +} + +FamRegion FamSession::createRegion(const fam::size_t region_size, const fam::perm_t region_perm, + const std::string& region_name) { + ASSERT(region_size > 0); + ASSERT(isValidName(region_name)); + + auto* region = invokeFam(&openfam::fam::fam_create_region, region_name.c_str(), region_size, region_perm, nullptr); + + return {*this, region}; +} + +void FamSession::resizeRegion(FamRegionDescriptor& region, const fam::size_t size) { + ASSERT(size > 0); + + invokeFam(&openfam::fam::fam_resize_region, ®ion, size); +} + +void FamSession::destroyRegion(FamRegionDescriptor& region) { + invokeFam(&openfam::fam::fam_destroy_region, ®ion); +} + +void FamSession::destroyRegion(const std::string& region_name) { + lookupRegion(region_name).destroy(); +} + +FamRegion FamSession::ensureCreateRegion(const fam::size_t region_size, const fam::perm_t region_perm, + const std::string& region_name) { + // Retry loop guards against TOCTOU + constexpr int max_retries = 3; + for (int attempt = 0; attempt <= max_retries; ++attempt) { + try { + return createRegion(region_size, region_perm, region_name); + } + catch (const AlreadyExists&) { + try { + destroyRegion(region_name); + } + catch (const NotFound&) { + LOG_DEBUG_LIB(LibEcKit) << "Region '" << region_name + << "' already existed but was concurrently destroyed by another " + "process/thread; retrying create (attempt " + << (attempt + 1) << " of " << max_retries << ")\n"; + } + } + } + throw SeriousBug("ensureCreateRegion: failed after " + std::to_string(max_retries) + " retries for region '" + + region_name + "'"); +} + +FamProperty FamSession::stat(FamRegionDescriptor& region) { + Fam_Stat info; + + auto fn_ptr = static_cast(&openfam::fam::fam_stat); + invokeFam(fn_ptr, ®ion, &info); + + return {info.size, info.perm, info.name, info.uid, info.gid}; +} + +//---------------------------------------------------------------------------------------------------------------------- +// OBJECT + +FamObject FamSession::proxyObject(const std::uint64_t region, const std::uint64_t offset) { + return {*this, region, offset}; +} + +FamObject FamSession::lookupObject(const std::string& region_name, const std::string& object_name) { + ASSERT(isValidName(region_name)); + ASSERT(isValidName(object_name)); + + auto* object = invokeFam(&openfam::fam::fam_lookup, object_name.c_str(), region_name.c_str()); + + return {*this, object}; +} + +FamObject FamSession::allocateObject(FamRegionDescriptor& region, const fam::size_t object_size, + const fam::perm_t object_perm, const std::string& object_name) { + ASSERT(object_size > 0); + + auto allocate = + static_cast( + &openfam::fam::fam_allocate); + + auto* object = invokeFam(allocate, object_name.c_str(), object_size, object_perm, ®ion); + + return {*this, object}; +} + +void FamSession::deallocateObject(FamObjectDescriptor& object) { + invokeFam(&openfam::fam::fam_deallocate, &object); +} + +void FamSession::deallocateObject(const std::string& region_name, const std::string& object_name) { + lookupObject(region_name, object_name).deallocate(); +} + +FamObject FamSession::ensureAllocateObject(FamRegionDescriptor& region, const fam::size_t object_size, + const fam::perm_t object_perm, const std::string& object_name) { + // Retry loop guards against TOCTOU + constexpr int max_retries = 3; + for (int attempt = 0; attempt <= max_retries; ++attempt) { + try { + return allocateObject(region, object_size, object_perm, object_name); + } + catch (const AlreadyExists&) { + try { + deallocateObject(region.get_name(), object_name); + } + catch (const NotFound&) { + LOG_DEBUG_LIB(LibEcKit) << "Object '" << object_name + << "' already existed but was concurrently destroyed by another " + "process/thread; retrying allocate (attempt " + << (attempt + 1) << " of " << max_retries << ")\n"; + } + } + } + throw SeriousBug("ensureAllocateObject: failed after " + std::to_string(max_retries) + " retries for object '" + + object_name + "'"); +} + +FamProperty FamSession::stat(FamObjectDescriptor& object) { + Fam_Stat info; + + auto fn_ptr = static_cast(&openfam::fam::fam_stat); + invokeFam(fn_ptr, &object, &info); + + return {info.size, info.perm, info.name, info.uid, info.gid}; +} + +void FamSession::put(FamObjectDescriptor& object, const void* buffer, const fam::size_t offset, + const fam::size_t length) { + ASSERT(buffer); + ASSERT(length > 0); + + /// @note we have to remove "const" qualifier from buffer + invokeFam(&openfam::fam::fam_put_blocking, const_cast(buffer), &object, offset, length); +} + +void FamSession::get(FamObjectDescriptor& object, void* buffer, const fam::size_t offset, const fam::size_t length) { + ASSERT(buffer); + ASSERT(length > 0); + + invokeFam(&openfam::fam::fam_get_blocking, buffer, &object, offset, length); +} + +//---------------------------------------------------------------------------------------------------------------------- +// OBJECT - ATOMIC + +template +T FamSession::fetch(FamObjectDescriptor& /* object */, const fam::size_t /* offset */) { + throw SeriousBug("This type is not specialized!", Here()); +} + +template <> +int32_t FamSession::fetch(FamObjectDescriptor& object, const fam::size_t offset) { + return invokeFam(&openfam::fam::fam_fetch_int32, &object, offset); +} + +template <> +int64_t FamSession::fetch(FamObjectDescriptor& object, const fam::size_t offset) { + return invokeFam(&openfam::fam::fam_fetch_int64, &object, offset); +} + +template <> +openfam::int128_t FamSession::fetch(FamObjectDescriptor& object, const fam::size_t offset) { + return invokeFam(&openfam::fam::fam_fetch_int128, &object, offset); +} + +template <> +uint32_t FamSession::fetch(FamObjectDescriptor& object, const fam::size_t offset) { + return invokeFam(&openfam::fam::fam_fetch_uint32, &object, offset); +} + +template <> +uint64_t FamSession::fetch(FamObjectDescriptor& object, const fam::size_t offset) { + return invokeFam(&openfam::fam::fam_fetch_uint64, &object, offset); +} + +template <> +float FamSession::fetch(FamObjectDescriptor& object, const fam::size_t offset) { + return invokeFam(&openfam::fam::fam_fetch_float, &object, offset); +} + +template <> +double FamSession::fetch(FamObjectDescriptor& object, const fam::size_t offset) { + return invokeFam(&openfam::fam::fam_fetch_double, &object, offset); +} + +template +void FamSession::set(FamObjectDescriptor& object, const fam::size_t offset, const T value) { + auto fptr = static_cast(&openfam::fam::fam_set); + invokeFam(fptr, &object, offset, value); +} + +template +void FamSession::add(FamObjectDescriptor& object, const fam::size_t offset, const T value) { + auto fptr = static_cast(&openfam::fam::fam_add); + invokeFam(fptr, &object, offset, value); +} + +template +void FamSession::subtract(FamObjectDescriptor& object, const fam::size_t offset, const T value) { + auto fptr = static_cast(&openfam::fam::fam_subtract); + invokeFam(fptr, &object, offset, value); +} + +template +T FamSession::swap(FamObjectDescriptor& object, const fam::size_t offset, const T value) { // NOLINT + auto fptr = static_cast(&openfam::fam::fam_swap); + return invokeFam(fptr, &object, offset, value); +} + +template +T FamSession::compareSwap(FamObjectDescriptor& object, const fam::size_t offset, const T old_value, const T new_value) { + auto fptr = + static_cast(&openfam::fam::fam_compare_swap); + return invokeFam(fptr, &object, offset, old_value, new_value); +} + +//---------------------------------------------------------------------------------------------------------------------- +// forward instantiations + +template void FamSession::set(FamObjectDescriptor&, const fam::size_t, const int32_t); +template void FamSession::set(FamObjectDescriptor&, const fam::size_t, const int64_t); +template void FamSession::set(FamObjectDescriptor&, const fam::size_t, const openfam::int128_t); +template void FamSession::set(FamObjectDescriptor&, const fam::size_t, const uint32_t); +template void FamSession::set(FamObjectDescriptor&, const fam::size_t, const uint64_t); +template void FamSession::set(FamObjectDescriptor&, const fam::size_t, const float); +template void FamSession::set(FamObjectDescriptor&, const fam::size_t, const double); + +template void FamSession::add(FamObjectDescriptor&, const fam::size_t, const int32_t); +template void FamSession::add(FamObjectDescriptor&, const fam::size_t, const int64_t); +template void FamSession::add(FamObjectDescriptor&, const fam::size_t, const uint32_t); +template void FamSession::add(FamObjectDescriptor&, const fam::size_t, const uint64_t); +template void FamSession::add(FamObjectDescriptor&, const fam::size_t, const float); +template void FamSession::add(FamObjectDescriptor&, const fam::size_t, const double); + +template void FamSession::subtract(FamObjectDescriptor&, const fam::size_t, const int32_t); +template void FamSession::subtract(FamObjectDescriptor&, const fam::size_t, const int64_t); +template void FamSession::subtract(FamObjectDescriptor&, const fam::size_t, const uint32_t); +template void FamSession::subtract(FamObjectDescriptor&, const fam::size_t, const uint64_t); +template void FamSession::subtract(FamObjectDescriptor&, const fam::size_t, const float); +template void FamSession::subtract(FamObjectDescriptor&, const fam::size_t, const double); + +template int32_t FamSession::swap(FamObjectDescriptor&, const fam::size_t, const int32_t); +template int64_t FamSession::swap(FamObjectDescriptor&, const fam::size_t, const int64_t); +template uint32_t FamSession::swap(FamObjectDescriptor&, const fam::size_t, const uint32_t); +template uint64_t FamSession::swap(FamObjectDescriptor&, const fam::size_t, const uint64_t); +template float FamSession::swap(FamObjectDescriptor&, const fam::size_t, const float); +template double FamSession::swap(FamObjectDescriptor&, const fam::size_t, const double); + +template int32_t FamSession::compareSwap(FamObjectDescriptor&, const fam::size_t, const int32_t, const int32_t); +template int64_t FamSession::compareSwap(FamObjectDescriptor&, const fam::size_t, const int64_t, const int64_t); +template uint32_t FamSession::compareSwap(FamObjectDescriptor&, const fam::size_t, const uint32_t, const uint32_t); +template uint64_t FamSession::compareSwap(FamObjectDescriptor&, const fam::size_t, const uint64_t, const uint64_t); + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamSession.h b/src/eckit/io/fam/FamSession.h new file mode 100644 index 000000000..7e569fdc7 --- /dev/null +++ b/src/eckit/io/fam/FamSession.h @@ -0,0 +1,171 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamSession.h +/// @author Metin Cakircali +/// @date Mar 2024 + +#pragma once + +#include +#include +#include +#include +#include + +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamProperty.h" +#include "eckit/io/fam/FamRegion.h" +#include "eckit/net/Endpoint.h" + +namespace openfam { +class fam; +} // namespace openfam + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- +// SESSION + +class FamSession : public std::enable_shared_from_this { +public: // types + + using TimePoint = std::chrono::system_clock::time_point; + +public: // methods + + FamSession(std::string name, const net::Endpoint& endpoint); + + // rule of five + FamSession(const FamSession&) = delete; + FamSession& operator=(const FamSession&) = delete; + FamSession(FamSession&&) = delete; + FamSession& operator=(FamSession&&) = delete; + + ~FamSession(); + + // Operations + + bool operator==(const FamSession& other) const { return name_ == other.name_ && endpoint_ == other.endpoint_; } + + bool operator!=(const FamSession& other) const { return !(*this == other); } + + // Accessors + + const std::string& name() const { return name_; } + + const TimePoint& lastAccess() const { return lastAccess_; } + + const net::Endpoint& endpoint() const { return endpoint_; } + + // Modifiers + + void updateLastAccess(const TimePoint& when = std::chrono::system_clock::now()) { lastAccess_ = when; } + + //------------------------------------------------------------------------------------------------------------------ + // REGION + + FamRegion lookupRegion(const std::string& region_name); + + FamRegion createRegion(fam::size_t region_size, fam::perm_t region_perm, const std::string& region_name); + + FamRegion createRegion(const FamProperty& property) { + return createRegion(property.size, property.perm, property.name); + } + + void resizeRegion(FamRegionDescriptor& region, fam::size_t size); + + void destroyRegion(FamRegionDescriptor& region); + + void destroyRegion(const std::string& region_name); + + FamRegion ensureCreateRegion(fam::size_t region_size, fam::perm_t region_perm, const std::string& region_name); + + FamProperty stat(FamRegionDescriptor& region); + + //------------------------------------------------------------------------------------------------------------------ + // OBJECT + + FamObject proxyObject(std::uint64_t region, std::uint64_t offset); + + FamObject lookupObject(const std::string& region_name, const std::string& object_name); + + FamObject allocateObject(FamRegionDescriptor& region, fam::size_t object_size, fam::perm_t object_perm, + const std::string& object_name = ""); + + FamObject allocateObject(FamRegionDescriptor& region, const FamProperty& property) { + return allocateObject(region, property.size, property.perm, property.name); + } + + void deallocateObject(FamObjectDescriptor& object); + + void deallocateObject(const std::string& region_name, const std::string& object_name); + + /// IMPORTANT: This method will deallocate any existing object with the same name + FamObject ensureAllocateObject(FamRegionDescriptor& region, fam::size_t object_size, fam::perm_t object_perm, + const std::string& object_name); + + FamProperty stat(FamObjectDescriptor& object); + + void put(FamObjectDescriptor& object, const void* buffer, fam::size_t offset, fam::size_t length); + + void get(FamObjectDescriptor& object, void* buffer, fam::size_t offset, fam::size_t length); + + //------------------------------------------------------------------------------------------------------------------ + // OBJECT - ATOMIC + + template + T fetch(FamObjectDescriptor& object, fam::size_t offset); + + template + void set(FamObjectDescriptor& object, fam::size_t offset, T value); + + template + void add(FamObjectDescriptor& object, fam::size_t offset, T value); + + template + void subtract(FamObjectDescriptor& object, fam::size_t offset, T value); + + template + T swap(FamObjectDescriptor& object, fam::size_t offset, T value); + + template + T compareSwap(FamObjectDescriptor& object, fam::size_t offset, T old_value, T new_value); + + //------------------------------------------------------------------------------------------------------------------ + +private: // methods + + friend std::ostream& operator<<(std::ostream& out, const FamSession& session); + + void print(std::ostream& out) const; + + template + auto invokeFam(Func&& fn_ptr, Args&&... args); + +private: // members + + std::string name_; + + net::Endpoint endpoint_; + + TimePoint lastAccess_{std::chrono::system_clock::now()}; + + std::unique_ptr fam_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamSessionManager.cc b/src/eckit/io/fam/FamSessionManager.cc new file mode 100644 index 000000000..63b4084b4 --- /dev/null +++ b/src/eckit/io/fam/FamSessionManager.cc @@ -0,0 +1,82 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamSessionManager.h" + +#include +#include +#include + +#include "eckit/io/fam/FamConfig.h" +#include "eckit/io/fam/FamSession.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +FamSessionManager& FamSessionManager::instance() { + static FamSessionManager instance; + return instance; +} + +//---------------------------------------------------------------------------------------------------------------------- + + +void FamSessionManager::remove(const std::string& name) { + std::lock_guard lock(mutex_); + sessions_.remove_if([&name](const auto& session) { return session && session->name() == name; }); +} + +void FamSessionManager::cleanup() { + // Assumes mutex is held by caller + const auto expire_time = std::chrono::system_clock::now() - std::chrono::minutes(30); + // Remove null sessions or last accessed more than 30 minutes ago + sessions_.remove_if([expire_time](const auto& session) { return !session || session->lastAccess() < expire_time; }); +} + +//---------------------------------------------------------------------------------------------------------------------- + +auto FamSessionManager::find(const std::string& name) -> Session { + for (auto& session : sessions_) { + if (session && session->name() == name) { + return session; + } + } + return {}; +} + +auto FamSessionManager::session(const net::Endpoint& endpoint) -> Session { + std::lock_guard lock(mutex_); + + const auto name = FamConfig::resolveSessionName(endpoint); + + auto session = find(name); + + if (session) { + session->updateLastAccess(); + } + else { + session = std::make_shared(name, endpoint); + sessions_.emplace_back(session); + } + + cleanup(); + + return session; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamSessionManager.h b/src/eckit/io/fam/FamSessionManager.h new file mode 100644 index 000000000..c948e7161 --- /dev/null +++ b/src/eckit/io/fam/FamSessionManager.h @@ -0,0 +1,78 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamSessionManager.h +/// @author Metin Cakircali +/// @date Mar 2024 + +#pragma once + +#include +#include +#include +#include + +namespace eckit { + +class FamSession; + +namespace net { +class Endpoint; +} + +//---------------------------------------------------------------------------------------------------------------------- + +class FamSessionManager { +public: // types + + class TestAccessor; + using Session = std::shared_ptr; + +public: // methods + + FamSessionManager(const FamSessionManager&) = delete; + FamSessionManager& operator=(const FamSessionManager&) = delete; + FamSessionManager(FamSessionManager&&) = delete; + FamSessionManager& operator=(FamSessionManager&&) = delete; + + static FamSessionManager& instance(); + + Session session(const net::Endpoint& endpoint); + + void remove(const std::string& name); + +private: // methods + + FamSessionManager() = default; + + ~FamSessionManager() = default; + + Session find(const std::string& name); + + // Removes null sessions or older than 30 minutes + void cleanup(); + +private: // members + + friend class TestAccessor; + + mutable std::recursive_mutex mutex_; + + std::list sessions_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamTypes.h b/src/eckit/io/fam/FamTypes.h new file mode 100644 index 000000000..8cc3b34e8 --- /dev/null +++ b/src/eckit/io/fam/FamTypes.h @@ -0,0 +1,62 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamTypes.h +/// @author Metin Cakircali +/// @date Mar 2026 + +#pragma once + +#include // mode_t + +#include // uint64_t +#include + +//---------------------------------------------------------------------------------------------------------------------- + +namespace openfam { +class Fam_Descriptor; +class Fam_Region_Descriptor; +} // namespace openfam + +//---------------------------------------------------------------------------------------------------------------------- + +namespace eckit { + +using FamObjectDescriptor = openfam::Fam_Descriptor; +using FamRegionDescriptor = openfam::Fam_Region_Descriptor; + +namespace fam { + +constexpr const char* scheme = "fam"; + +using size_t = std::uint64_t; +using perm_t = mode_t; +using index_t = std::uint64_t; + +} // namespace fam + +struct FamDescriptor { + fam::index_t region{0}; + fam::index_t offset{0}; +}; + +static_assert(std::is_standard_layout_v, "FamDescriptor must be standard-layout for offsetof()"); +static_assert(std::is_trivially_copyable_v, "FamDescriptor must be trivially copyable for FAM put/get"); +static_assert(sizeof(FamDescriptor) == 16, "FamDescriptor layout changed (FAM on-wire format depends on this)"); + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamURIManager.cc b/src/eckit/io/fam/FamURIManager.cc new file mode 100644 index 000000000..e6ada527c --- /dev/null +++ b/src/eckit/io/fam/FamURIManager.cc @@ -0,0 +1,70 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#include "eckit/io/fam/FamURIManager.h" + +#include + +#include "eckit/filesystem/URI.h" +#include "eckit/filesystem/URIManager.h" +#include "eckit/io/Length.h" +#include "eckit/io/Offset.h" +#include "eckit/io/fam/FamObjectName.h" +#include "eckit/io/fam/FamTypes.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +FamURIManager::FamURIManager(const std::string& name) : URIManager(name) {} + +FamURIManager::~FamURIManager() = default; + +bool FamURIManager::exists(const URI& uri) { + return FamObjectName(uri).exists(); +} + +DataHandle* FamURIManager::newWriteHandle(const URI& uri) { + return FamObjectName(uri).dataHandle(); +} + +DataHandle* FamURIManager::newReadHandle(const URI& uri) { + return FamObjectName(uri).dataHandle(); +} + +DataHandle* FamURIManager::newReadHandle(const URI& uri, const OffsetList& offsets, const LengthList& lengths) { + return FamObjectName(uri).partHandle(offsets, lengths); +} + +std::string FamURIManager::asString(const URI& uri) const { + std::string query = uri.query(); + if (!query.empty()) { + query = "?" + query; + } + + std::string fragment = uri.fragment(); + if (!fragment.empty()) { + fragment = "#" + fragment; + } + + /// @todo consider return FamObjectName(uri).asString() + query + fragment; + return uri.scheme() + ":" + uri.name() + query + fragment; +} + +static FamURIManager manager(fam::scheme); + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/FamURIManager.h b/src/eckit/io/fam/FamURIManager.h new file mode 100644 index 000000000..ed859abb4 --- /dev/null +++ b/src/eckit/io/fam/FamURIManager.h @@ -0,0 +1,54 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamURIManager.h +/// @author Metin Cakircali +/// @date May 2024 + +#pragma once + +#include + +#include "eckit/filesystem/URIManager.h" +#include "eckit/io/Length.h" +#include "eckit/io/Offset.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +class FamURIManager : public URIManager { +public: // methods + + FamURIManager(const std::string& name); + + ~FamURIManager() override; + + bool authority() override { return true; } + +private: // methods + + bool exists(const URI& /*uri*/) override; + + DataHandle* newWriteHandle(const URI& uri) override; + DataHandle* newReadHandle(const URI& uri) override; + DataHandle* newReadHandle(const URI& uri, const OffsetList& offsets, const LengthList& lengths) override; + + std::string asString(const URI& uri) const override; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/detail/FamBackoff.h b/src/eckit/io/fam/detail/FamBackoff.h new file mode 100644 index 000000000..9ca4bfabf --- /dev/null +++ b/src/eckit/io/fam/detail/FamBackoff.h @@ -0,0 +1,52 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +#pragma once + +#include +#include +#include +#include + +namespace eckit::fam::detail { + +//---------------------------------------------------------------------------------------------------------------------- + +/// @brief Exponential backoff helper for CAS Copy-And-Swap retry loops. +/// Used in FamMap bucket creation to reduce contention under high concurrency. +/// +/// With OpenFAM/libfabric backend, multiple processes doing RDMA CAS create contention +/// that causes lock for retry loops. + +struct CasBackoff { + std::uint32_t delay = 1; + std::uint32_t cap = 1024; // ~1 ms cap + + void operator()() { + std::this_thread::yield(); + + if (delay > 1) { + thread_local std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution dist(0, delay); + std::this_thread::sleep_for(std::chrono::microseconds(dist(rng))); + } + + delay = std::min(delay * 2, cap); + } +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::fam::detail diff --git a/src/eckit/io/fam/detail/FamListNode.h b/src/eckit/io/fam/detail/FamListNode.h new file mode 100644 index 000000000..30b0bd7fd --- /dev/null +++ b/src/eckit/io/fam/detail/FamListNode.h @@ -0,0 +1,126 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamListNode.h +/// @author Metin Cakircali +/// @date Mar 2024 + +#pragma once + +#include +#include +#include + +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/detail/FamNode.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +/// @brief Concurrent-safe list node for FAM-resident doubly-linked list. +/// +/// Layout (56+ bytes): +/// - version: u8 - node version (incremented on reuse, for ABA detection) +/// - next: FamDescriptor - next node pointer +/// - prev: FamDescriptor - previous node pointer +/// - length: u64 - data payload length in bytes +/// - marked: u8 - logical deletion marker (for wait-free deletion) +/// - padding: u7 - alignment +/// - [data]: next node allocation starts here +/// +/// Thread safety: +/// - Use version field to detect ABA problems when reusing freed nodes +/// - The 'marked' bit enables logical deletion: physical deallocation happens +/// asynchronously to avoid disrupting other readers +/// - Atomic swap() and compareSwap() on next/prev pointers coordinate insertions +/// - Readers can safely traverse marked nodes; they'll eventually be cleaned up +/// +/// @important: DO NOT add any virtual functions in this class. +struct FamListNode { + FamNode header{}; // 24 bytes: { version, next } + FamDescriptor prev; // 16 bytes + fam::size_t length{0}; // 8 bytes + std::uint8_t marked{0}; // 1 byte: 0=active, 1=logically deleted + // 7 bytes padding to align 'data' to 8-byte boundary + + /// Byte offset of `prev.offset` within FamListNode (portable two-step form). + static constexpr std::size_t prevOffsetOff() noexcept { + return offsetof(FamListNode, prev) + offsetof(FamDescriptor, offset); + } + + /// Byte offset of the embedded `next` descriptor (header). + static constexpr std::size_t nextOff() noexcept { return offsetof(FamListNode, header) + offsetof(FamNode, next); } + + /// Byte offset of `next.offset` within FamListNode (portable two-step form). + /// Equivalent to FamNode::nextOffsetOff() because `header` is placed first. + static constexpr std::size_t nextOffsetOff() noexcept { + return offsetof(FamListNode, header) + FamNode::nextOffsetOff(); + } + + /// Forwarders to the embedded FamNode header. + static FamDescriptor getNext(const FamObject& object) { return FamNode::getNext(object); } + static std::uint64_t getNextOffset(const FamObject& object) { return FamNode::getNextOffset(object); } + + /// Increment version stamp to prevent ABA problems on node reuse + static void bumpVersion(const FamObject& object) { + auto ver = object.get(offsetof(FamNode, version)); + object.put(static_cast(ver + 1), offsetof(FamNode, version)); + } + + /// Fetch previous node descriptor (includes version for ABA detection) + static FamDescriptor getPrev(const FamObject& object) { + return object.get(offsetof(FamListNode, prev)); + } + + /// Fetch previous node offset only + static std::uint64_t getPrevOffset(const FamObject& object) { return object.get(prevOffsetOff()); } + + /// Fetch data payload length + static fam::size_t getLength(const FamObject& object) { + return object.get(offsetof(FamListNode, length)); + } + + /// Check if node is logically deleted (marked for removal) + static bool isMarked(const FamObject& object) { + return object.get(offsetof(FamListNode, marked)) != 0; + } + + /// Mark node as logically deleted (wait-free deletion) + static void mark(const FamObject& object) { + object.put(static_cast(1), offsetof(FamListNode, marked)); + } + + /// Copy node data to buffer + static void getData(const FamObject& object, Buffer& buffer) { + if (const auto length = getLength(object); length > 0) { + buffer.resize(length); + object.get(buffer.data(), sizeof(FamListNode), length); + } + } +}; + +static_assert(std::is_standard_layout_v, "FamListNode must be standard-layout for offsetof()"); +static_assert(std::is_trivially_copyable_v, "FamListNode must be trivially copyable for FAM put/get"); +static_assert(sizeof(FamListNode) == 56, "FamListNode layout changed (FAM on-wire format depends on this)"); +static_assert(offsetof(FamListNode, header) == 0, "FamListNode::header must be at offset 0 (wire layout)"); +static_assert(offsetof(FamListNode, prev) == 24, "FamListNode::prev offset mismatch"); +static_assert(offsetof(FamListNode, length) == 40, "FamListNode::length offset mismatch"); +static_assert(offsetof(FamListNode, marked) == 48, "FamListNode::marked offset mismatch"); + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/detail/FamMapNode.h b/src/eckit/io/fam/detail/FamMapNode.h new file mode 100644 index 000000000..6487a92b6 --- /dev/null +++ b/src/eckit/io/fam/detail/FamMapNode.h @@ -0,0 +1,56 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamMapNode.h +/// @author Metin Cakircali +/// @date Jul 2024 + +#pragma once + +#include "FamNode.h" + +#include + +#include "eckit/io/fam/FamList.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +struct FamMapNode { + FamNode header{}; // 24 bytes: { version, next } + FamList::Descriptor desc; // bucket FamList descriptor + + //------------------------------------------------------------------------------------------------------------------ + // HELPERS (DO NOT add any virtual function here) + + static FamList::Descriptor getDescriptor(const FamObject& object) { + return object.get(offsetof(FamMapNode, desc)); + } + + static std::unique_ptr getList(const FamRegion& region, const FamObject& object) { + return std::make_unique(region, FamMapNode::getDescriptor(object)); + } +}; + +static_assert(std::is_standard_layout_v, "FamMapNode must be standard-layout for offsetof()"); +static_assert(std::is_trivially_copyable_v, "FamMapNode must be trivially copyable for FAM put/get"); +static_assert(sizeof(FamMapNode) == 56, "FamMapNode layout changed (FAM on-wire format depends on this)"); +static_assert(offsetof(FamMapNode, header) == 0, "FamMapNode::header must be the first member (wire layout)"); +static_assert(offsetof(FamMapNode, desc) == 24, "FamMapNode::desc offset mismatch"); + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/detail/FamNode.h b/src/eckit/io/fam/detail/FamNode.h new file mode 100644 index 000000000..f90bf4857 --- /dev/null +++ b/src/eckit/io/fam/detail/FamNode.h @@ -0,0 +1,53 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamNode.h +/// @author Metin Cakircali +/// @date Mar 2024 + +#pragma once + +#include +#include // uint8_t +#include + +#include "eckit/io/fam/FamObject.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +/// @important: DO NOT add any virtual functions in this class. +struct FamNode { + std::uint8_t version{1}; // 1 byte + FamDescriptor next; + + /// Byte offset of `next.offset` within FamNode. + static constexpr std::size_t nextOffsetOff() noexcept { + return offsetof(FamNode, next) + offsetof(FamDescriptor, offset); + } + + static FamDescriptor getNext(const FamObject& object) { return object.get(offsetof(FamNode, next)); } + + static std::uint64_t getNextOffset(const FamObject& object) { return object.get(nextOffsetOff()); } +}; + +static_assert(std::is_standard_layout_v, "FamNode must be standard-layout for offsetof()"); +static_assert(std::is_trivially_copyable_v, "FamNode must be trivially copyable for FAM put/get"); +static_assert(sizeof(FamNode) == 24, "FamNode layout changed (FAM on-wire format depends on this)"); + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit diff --git a/src/eckit/io/fam/openfam_mock/CMakeLists.txt b/src/eckit/io/fam/openfam_mock/CMakeLists.txt new file mode 100644 index 000000000..98e1832ee --- /dev/null +++ b/src/eckit/io/fam/openfam_mock/CMakeLists.txt @@ -0,0 +1,30 @@ +# OpenFAM Mock — standalone shared library +# +# Provides the same fam/fam.h API as the real OpenFAM library. +# creates a LibOPENFAM alias target so eckit and downstream projects link uniformly. + +ecbuild_add_library( + TARGET eckit_openfam_mock + TYPE SHARED + INSTALL_HEADERS LISTED + HEADER_DESTINATION ${INSTALL_INCLUDE_DIR} + + SOURCES + fam/fam.cc + fam/fam.h + fam/fam_exception.h + FamMockSession.cc + + PRIVATE_INCLUDES + $ + + PRIVATE_LIBS + ${RT_LIBRARIES} + ${THREADS_LIBRARIES} +) + +# Same target name as FindLibOPENFAM would create for the real library. +add_library( LibOPENFAM ALIAS eckit_openfam_mock ) + +# Expose the mock's include directory (so eckit sources find fam/fam.h) +set( LIB_OPENFAM_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} PARENT_SCOPE ) diff --git a/src/eckit/io/fam/openfam_mock/FamMockSession.cc b/src/eckit/io/fam/openfam_mock/FamMockSession.cc new file mode 100644 index 000000000..d209b7d05 --- /dev/null +++ b/src/eckit/io/fam/openfam_mock/FamMockSession.cc @@ -0,0 +1,416 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamMockSession.cc +/// @author Metin Cakircali +/// @date Mar 2026 + +#include "FamMockSession.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fam/fam.h" +#include "fam/fam_exception.h" + +namespace openfam::mock { + +//---------------------------------------------------------------------------------------------------------------------- +// HELPERS + +namespace { + +/// Magic value written to `State::initialized` once the creator finishes setup. +constexpr std::uint32_t k_init_magic = 0xFA00CAFE; + +/// Round @p n up to the next multiple of 8. +template +constexpr T alignTo8(T n) { + return (n + T{7}) & ~T{7}; +} + +template +void debugLog(Args&&... args) { + static const bool enabled = std::getenv("DEBUG") != nullptr; + if (enabled) { + ((std::cerr << "[openfam mock] ") << ... << std::forward(args)) << '\n'; + } +} + +/// Byte offset of the data area from the start of the shared memory segment. +constexpr std::size_t k_data_offset = alignTo8(sizeof(State)); + +using SessionMap = std::map>; + +// Process-local mock session cache +std::pair& sessionCache() { + static std::pair cache; + return cache; +} + +std::string getShmName(std::string name) { + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char code) { return std::isalnum(code) ? static_cast(code) : '_'; }); + // Limit length to be safe on all platforms (POSIX requires NAME_MAX support). + if (name.size() > 200) { + name = name.substr(0, 64) + "_" + std::to_string(std::hash{}(name)); + } + return "/eckit_fam_mock_" + (name.empty() ? "default" : name); +} + +/// Gets shm size from $ECKIT_FAM_MOCK_SHM_SIZE (bytes) or defaults to g_default_shm_size. +std::size_t getShmSize() { + const char* env = std::getenv("ECKIT_FAM_MOCK_SHM_SIZE"); + auto size = env ? static_cast(std::stol(env)) : g_default_shm_size; + if (size <= k_data_offset) { + throw std::runtime_error("ECKIT_FAM_MOCK_SHM_SIZE too small to hold State metadata"); + } + return size; +} + +//---------------------------------------------------------------------------------------------------------------------- +// SHM lifecycle helper + +/// Opens (or creates) the POSIX shm segment, truncates if creator, and maps it. +/// Populates @p handle with the fd and mapping. Returns true if this call created the segment. +bool openOrCreateShm(FamMockSession::ShmHandle& handle) { + bool creator = false; + auto shm_fd = ::shm_open(handle.name.c_str(), O_CREAT | O_EXCL | O_RDWR, 0600); + if (shm_fd >= 0) { + creator = true; + debugLog("Created new shared memory segment."); + } + else if (errno == EEXIST) { + shm_fd = ::shm_open(handle.name.c_str(), O_RDWR, 0600); + debugLog("Opened existing shared memory segment."); + } + + if (shm_fd < 0) { + throw std::system_error(errno, std::system_category(), "shm_open(" + handle.name + ")"); + } + + if (creator && ::ftruncate(shm_fd, static_cast(handle.size)) != 0) { + ::close(shm_fd); + handle.unlink(); + throw std::system_error(errno, std::system_category(), "ftruncate"); + } + + auto* mapping = ::mmap(nullptr, handle.size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); + if (mapping == MAP_FAILED) { + ::close(shm_fd); + if (creator) { + handle.unlink(); + } + throw std::system_error(errno, std::system_category(), "mmap"); + } + + handle.fd = shm_fd; + handle.mapping = mapping; + return creator; +} + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- + +void FamMockSession::ShmHandle::close() { + if (mapping && mapping != MAP_FAILED) { + ::munmap(mapping, size); + mapping = nullptr; + } + if (fd >= 0) { + ::close(fd); + fd = -1; + } +} + +void FamMockSession::ShmHandle::unlink() const { + ::shm_unlink(name.c_str()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +FamMockSession& FamMockSession::instance(const std::string& name) { + auto& [mutex, cache] = sessionCache(); + std::lock_guard lock(mutex); + + if (auto iter = cache.find(name); iter != cache.end()) { + return *iter->second; + } + + auto session = std::unique_ptr(new FamMockSession(name)); + auto& ref = *session; + cache[name] = std::move(session); + return ref; +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamMockSession::mapFields() { + state_ = static_cast(handle_.mapping); + data_ = static_cast(handle_.mapping) + k_data_offset; +} + +FamMockSession::FamMockSession(const std::string& name) : handle_{getShmName(name), getShmSize()} { + debugLog("Opening shared memory: ", handle_.name, " (", (handle_.size / (1024 * 1024)), " MiB)"); + + const auto data_capacity = handle_.size - k_data_offset; + + bool creator = openOrCreateShm(handle_); + mapFields(); + + // Stale/uninitialized segment (e.g., after crash or forced kill) — tear down and recreate. + if (!creator && state_->initialized != k_init_magic) { + debugLog("Detected stale/uninitialized segment. recreating..."); + handle_.close(); + handle_.unlink(); + + creator = openOrCreateShm(handle_); + mapFields(); + } + + if (creator) { + debugLog("Zero-initializing shared memory and mutex."); + std::memset(handle_.mapping, 0, handle_.size); + + // Initialize process-shared robust mutex. + pthread_mutexattr_t attr; + ::pthread_mutexattr_init(&attr); + ::pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED); + ::pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST); + const int mrc = ::pthread_mutex_init(&state_->mutex, &attr); + ::pthread_mutexattr_destroy(&attr); + if (mrc != 0) { + throw std::system_error(mrc, std::system_category(), "pthread_mutex_init"); + } + debugLog("pthread_mutex_init returned ", mrc); + + state_->nextRegion = 1; + state_->dataCapacity = data_capacity; + // Release fence to ensure preceding writes (mutex, nextRegion) are visible. + __atomic_store_n(&state_->initialized, k_init_magic, __ATOMIC_RELEASE); + debugLog("Shared memory initialization complete."); + } + else { + // Wait for the creator to finish initialization. + debugLog("Waiting for creator to finish initialization..."); + // Spin with microsecond sleeps — acceptable for test infrastructure. + while (__atomic_load_n(&state_->initialized, __ATOMIC_ACQUIRE) != k_init_magic) { + ::usleep(100); + } + debugLog("Shared memory initialization detected, proceeding."); + } +} + +FamMockSession::~FamMockSession() { + handle_.close(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamMockSession::lock() { + debugLog("Attempting to lock mutex..."); + const auto code = ::pthread_mutex_lock(&state_->mutex); + debugLog("pthread_mutex_lock returned ", code); + if (code == EOWNERDEAD) { + // The previous owner died holding the mutex. + // MUST NOT call lock()/LockGuard here — the mutex is already ours. + debugLog("EOWNERDEAD detected, calling pthread_mutex_consistent (data preserved)."); + ::pthread_mutex_consistent(&state_->mutex); + // Note: we intentionally do NOT reset data here. The previous owner's + // half-finished operation may have left a partially-written object, but + // a full reset would destroy all regions/objects — catastrophic for + // multi-process workloads where another process simply _exit()'d while + // holding the lock. + } + else if (code != 0) { + throw std::system_error(code, std::system_category(), "pthread_mutex_lock"); + } +} + +void FamMockSession::unlock() { + debugLog("Unlocking mutex."); + const int code = ::pthread_mutex_unlock(&state_->mutex); + if (code != 0) { + throw std::system_error(code, std::system_category(), "pthread_mutex_unlock"); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +void FamMockSession::resetUnlocked() { + // Must be called while the mutex is already held (or during constructor setup). + state_->nextRegion = 1; + state_->dataUsed = 0; + for (auto& region : state_->regions) { + region = Region{}; + } + std::memset(data_, 0, state_->dataCapacity); +} + +void FamMockSession::reset() { + std::lock_guard guard(*this); + resetUnlocked(); +} + +//---------------------------------------------------------------------------------------------------------------------- +// Region helpers + +Region* FamMockSession::allocateRegionSlot() { + for (auto& region : state_->regions) { + if (!region.active) { + return ®ion; + } + } + return nullptr; +} + +Region* FamMockSession::findRegionByName(const char* name) { + for (auto& region : state_->regions) { + if (region.active && std::string_view{region.name} == name) { + return ®ion; + } + } + return nullptr; +} + +Region* FamMockSession::findRegionById(std::uint64_t regionId) { + for (auto& region : state_->regions) { + if (region.active && region.id == regionId) { + return ®ion; + } + } + return nullptr; +} + +Region& FamMockSession::findRegion(Fam_Region_Descriptor* desc) { + if (auto* region = findRegionById(desc->get_global_descriptor().regionId)) { + return *region; + } + throw Fam_Exception("Region not found", FAM_ERR_NOTFOUND); +} + +void FamMockSession::freeRegion(Region& region) { + region = Region{}; + reclaimDataArea(); +} + +//---------------------------------------------------------------------------------------------------------------------- +// Object helpers + +Object* FamMockSession::findObjectByOffset(Region& region, std::uint64_t offset) { + for (auto& obj : region.objects) { + if (obj.active && obj.offset == offset) { + return &obj; + } + } + return nullptr; +} + +Object* FamMockSession::findObjectByName(Region& region, const char* name) { + for (auto& obj : region.objects) { + if (obj.active && obj.name[0] != '\0' && std::strcmp(obj.name, name) == 0) { + return &obj; + } + } + return nullptr; +} + +Object* FamMockSession::allocateObjectSlot(Region& region) { + for (auto& obj : region.objects) { + if (!obj.active) { + return &obj; + } + } + return nullptr; +} + +Object& FamMockSession::findObject(Fam_Descriptor* desc) { + const auto [regionId, offset] = desc->get_global_descriptor(); + + auto* region = findRegionById(regionId); + if (!region) { + throw Fam_Exception("Region not found", FAM_ERR_NOTFOUND); + } + + auto* obj = findObjectByOffset(*region, offset); + if (!obj) { + throw Fam_Exception("Object not found", FAM_ERR_NOTFOUND); + } + return *obj; +} + +void FamMockSession::freeObject(Object& obj) { + obj = Object{}; + reclaimDataArea(); +} + +//---------------------------------------------------------------------------------------------------------------------- +// Data area + +std::uint64_t FamMockSession::allocateData(std::uint64_t size) { + const auto aligned = alignTo8(size); + + if (state_->dataUsed + aligned > state_->dataCapacity) { + throw Fam_Exception("Mock FAM data area exhausted", FAM_ERR_NO_SPACE); + } + + const auto offset = state_->dataUsed; + state_->dataUsed += aligned; + return offset; +} + +void FamMockSession::reclaimDataArea() { + std::uint64_t high_water = 0; + for (const auto& region : state_->regions) { + if (!region.active) { + continue; + } + for (const auto& obj : region.objects) { + if (!obj.active) { + continue; + } + const auto end = obj.dataOffset + alignTo8(obj.size); + high_water = std::max(end, high_water); + } + } + state_->dataUsed = high_water; +} + +std::uint8_t* FamMockSession::objectData(const Object& obj) { + return data_ + obj.dataOffset; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace openfam::mock diff --git a/src/eckit/io/fam/openfam_mock/FamMockSession.h b/src/eckit/io/fam/openfam_mock/FamMockSession.h new file mode 100644 index 000000000..3007dfebd --- /dev/null +++ b/src/eckit/io/fam/openfam_mock/FamMockSession.h @@ -0,0 +1,226 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file FamMockSession.h +/// @author Metin Cakircali +/// @date Mar 2026 + +/// @brief Implements multi-process FAM mock. +/// +/// Uses POSIX shared-memory to simulate FAM (Fabric-Attached Memory). +/// +/// All mock states live in POSIX shared memory. +// Layout: +/// [State] (mutex, counters, region table) +/// └ Region[g_max_regions] +/// └ Object[g_max_objs_per_region] +/// [Data] — raw bytes + +#pragma once + +#include +#include // mode_t + +#include +#include +#include +#include + +//---------------------------------------------------------------------------------------------------------------------- + +namespace openfam { + +class Fam_Descriptor; +class Fam_Region_Descriptor; + +namespace mock { + +//---------------------------------------------------------------------------------------------------------------------- +/// Capacity +/// default: 64 MiB total shared memory +/// use "export ECKIT_FAM_MOCK_SHM_SIZE=536870912" for 512 MiB, etc. Must be > sizeof(State) (currently ~3 MiB). + +constexpr std::size_t g_max_name_len = 40; // OpenFAM real dataitem name limit +constexpr std::size_t g_max_regions = 64; // Max number of regions +constexpr std::size_t g_max_objs_per_region = 4096; // Max objects per region +constexpr std::size_t g_default_shm_size = 64 * 1024 * 1024; // 64 MiB default + +//---------------------------------------------------------------------------------------------------------------------- + +/// @note POD no virtual, no std::string +struct Object { + bool active{false}; + + char name[g_max_name_len]{}; + + std::uint64_t offset{0}; + + std::uint64_t size{0}; + mode_t perm{0}; + std::uint32_t uid{0}; + std::uint32_t gid{0}; + + std::uint64_t dataOffset{0}; +}; + +/// @note POD no virtual, no std::string +struct Region { + bool active{false}; + + char name[g_max_name_len]{}; + + std::uint64_t id{0}; + std::uint64_t size{0}; + mode_t perm{0}; + + /// Starts at 8 as offset 0 is used as a null value + std::uint64_t nextOffset{8}; + + Object objects[g_max_objs_per_region]; +}; + +/// Lives in POSIX shared memory and is accessed by multiple processes. +/// @note POD no virtual, no std::string +struct State { + /// We use pthread_mutex_t (with PTHREAD_PROCESS_SHARED) instead of std::mutex, + /// since it is not guaranteed to be copyable, and does not support inter-process locking. + pthread_mutex_t mutex; + + /// marker for initialization completion (set to `k_init_magic`) + std::uint32_t initialized; + + /// next region ID (0 is invalid) + std::uint64_t nextRegion; + + /// bytes used in data area (starts at 0) + std::uint64_t dataUsed; + + /// runtime data area capacity (set by creator, read by joiners) + std::uint64_t dataCapacity; + + Region regions[g_max_regions]; +}; + +// Ensure State is 8-byte aligned so that the data area is properly aligned for any type. +static_assert(sizeof(State) % 8 == 0, "State size must be a multiple of 8 for proper data alignment"); + +// Build time check that State fits into the default shared-memory segment. +static_assert(sizeof(State) < g_default_shm_size, + "State overflows g_default_shm_size! reduce g_max_objs_per_region or g_max_regions"); + +//---------------------------------------------------------------------------------------------------------------------- + +/// Manages a shared-memory that stores all mock FAM States. +/// Thread-safe and process-safe via shared mutex (satisfies Lockable for std::lock_guard). +class FamMockSession { +public: + + struct ShmHandle { + std::string name; + std::size_t size{g_default_shm_size}; + int fd{-1}; + void* mapping{nullptr}; + + /// Unmaps the shared memory and closes the file descriptor. + void close(); + + /// Removes the shared-memory segment from the filesystem. + void unlink() const; + }; + + /// Obtain (or create) the shared-memory session + static FamMockSession& instance(const std::string& name = ""); + + ~FamMockSession(); + + // rules + FamMockSession(const FamMockSession&) = delete; + FamMockSession& operator=(const FamMockSession&) = delete; + FamMockSession(FamMockSession&&) = delete; + FamMockSession& operator=(FamMockSession&&) = delete; + + //------------------------------------------------------------------------------------------------------------------ + + /// Wipes all mock states and resets the session to the initial state. + void reset(); + + //------------------------------------------------------------------------------------------------------------------ + + void lock(); + void unlock(); + + //------------------------------------------------------------------------------------------------------------------ + // Region + + Region* findRegionByName(const char* name); + Region* findRegionById(std::uint64_t regionId); + Region* allocateRegionSlot(); + + /// throws `FAM_ERR_NOTFOUND` + Region& findRegion(Fam_Region_Descriptor* desc); + + /// Frees all objects in the region, marks it inactive, and reclaims data. + void freeRegion(Region& region); + + //------------------------------------------------------------------------------------------------------------------ + // Object + + static Object* findObjectByOffset(Region& region, std::uint64_t offset); + static Object* findObjectByName(Region& region, const char* name); + static Object* allocateObjectSlot(Region& region); + + /// Finds an object by descriptor or throws `FAM_ERR_NOTFOUND`. + Object& findObject(Fam_Descriptor* desc); + + void freeObject(Object& obj); + + //------------------------------------------------------------------------------------------------------------------ + // Data + + /// Allocate @p size bytes in the data area. + /// Returns the offset from data-area start. + /// Throws `FAM_ERR_NO_SPACE` if the data area is exhausted. + std::uint64_t allocateData(std::uint64_t size); + + /// Returns a raw pointer to the payload bytes of an object. + std::uint8_t* objectData(const Object& obj); + + //------------------------------------------------------------------------------------------------------------------ + // Accessors + + std::uint64_t nextRegion() { return state_->nextRegion++; } + +private: + + explicit FamMockSession(const std::string& name); + + void reclaimDataArea(); + + void mapFields(); + + /// Same as reset() but must be called while the mutex is held (e.g., during initialization). + void resetUnlocked(); + + ShmHandle handle_; + + State* state_{nullptr}; + std::uint8_t* data_{nullptr}; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace mock + +} // namespace openfam diff --git a/src/eckit/io/fam/openfam_mock/fam/.clang-tidy b/src/eckit/io/fam/openfam_mock/fam/.clang-tidy new file mode 100644 index 000000000..64efd8163 --- /dev/null +++ b/src/eckit/io/fam/openfam_mock/fam/.clang-tidy @@ -0,0 +1,19 @@ +--- +# Narrow override for OpenFAM-compatible mock headers. +InheritParentConfig: true +Checks: > + -cppcoreguidelines-avoid-c-arrays, + -bugprone-easily-swappable-parameters, + -readability-magic-numbers, + -cppcoreguidelines-avoid-magic-numbers + +CheckOptions: + - { key: readability-identifier-naming.ClassCase, value: aNy_CasE } + - { key: readability-identifier-naming.StructCase, value: aNy_CasE } + - { key: readability-identifier-naming.FunctionCase, value: aNy_CasE } + - { key: readability-identifier-naming.ClassMethodCase, value: aNy_CasE } + - { key: readability-identifier-naming.VariableCase, value: aNy_CasE } + - { key: readability-identifier-naming.ParameterCase, value: aNy_CasE } + - { key: readability-identifier-naming.PrivateMemberCase, value: aNy_CasE } + - { key: readability-identifier-naming.ProtectedMemberCase, value: aNy_CasE } + - { key: readability-identifier-naming.PublicMemberCase, value: aNy_CasE } diff --git a/src/eckit/io/fam/openfam_mock/fam/fam.cc b/src/eckit/io/fam/openfam_mock/fam/fam.cc new file mode 100644 index 000000000..fee2b4a4d --- /dev/null +++ b/src/eckit/io/fam/openfam_mock/fam/fam.cc @@ -0,0 +1,488 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file openfam_mock/fam/fam.cc +/// @author Metin Cakircali +/// @date Mar 2026 + +#include "fam/fam.h" + +#include // getuid, getgid + +#include +#include // std::abort +#include +#include +#include +#include + +#include "fam/fam_exception.h" + +#include "FamMockSession.h" + +//---------------------------------------------------------------------------------------------------------------------- + +namespace openfam { + +namespace { + +//---------------------------------------------------------------------------------------------------------------------- +// HELPERS + +template +auto typed_fetch(std::uint8_t* base, std::uint64_t objSize, std::uint64_t offset) -> T { + if (offset > objSize || sizeof(T) > (objSize - offset)) { + throw Fam_Exception("Offset out of range", FAM_ERR_OUTOFRANGE); + } + T value{}; + std::memcpy(&value, base + offset, sizeof(T)); + return value; +} + +template +void typed_store(std::uint8_t* base, std::uint64_t objSize, std::uint64_t offset, T value) { + if (offset > objSize || sizeof(T) > (objSize - offset)) { + throw Fam_Exception("Offset out of range", FAM_ERR_OUTOFRANGE); + } + std::memcpy(base + offset, &value, sizeof(T)); +} + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- + +fam::fam() = default; +fam::~fam() = default; + +mock::FamMockSession& fam::getSession() { + assert(session_ != nullptr && "fam_initialize() must be called before any other FAM operation"); + return *session_; +} + +void fam::fam_initialize(const char* /*name*/, Fam_Options* options) { + if (options && options->cisServer) { + serverName_ = options->cisServer; + } + session_ = &mock::FamMockSession::instance(serverName_); +} + +void fam::fam_finalize(const char* /*name*/) { + session_ = nullptr; +} + +void fam::fam_abort(int /*code*/) { + std::abort(); +} + +const void* fam::fam_get_option(char* option_name) { + if (option_name && std::string_view(option_name) == "CIS_SERVER") { + return static_cast(serverName_.data()); + } + return nullptr; +} + +//---------------------------------------------------------------------------------------------------------------------- +// REGION OPERATIONS + +Fam_Region_Descriptor* fam::fam_create_region(const char* name, std::uint64_t size, mode_t perm, + Fam_Region_Attributes* /*region_attributes*/) { + if (!name || !*name) { + throw Fam_Exception("Invalid region name", FAM_ERR_INVALID); + } + + auto& session = getSession(); + std::lock_guard lock(session); + + if (session.findRegionByName(name) != nullptr) { + throw Fam_Exception(std::string("Region already exists: ") + name, FAM_ERR_ALREADYEXIST); + } + + auto* slot = session.allocateRegionSlot(); + if (!slot) { + throw Fam_Exception("Maximum number of regions reached", FAM_ERR_NO_SPACE); + } + + const auto regionId = session.nextRegion(); + + *slot = mock::Region{}; + slot->active = true; + slot->id = regionId; + slot->size = size; + slot->perm = perm; + std::strncpy(slot->name, name, mock::g_max_name_len - 1); + slot->name[mock::g_max_name_len - 1] = '\0'; + + return new Fam_Region_Descriptor(regionId, size, perm, name); +} + +Fam_Region_Descriptor* fam::fam_lookup_region(const char* name) { + if (!name || !*name) { + throw Fam_Exception("Invalid region name", FAM_ERR_INVALID); + } + + auto& session = getSession(); + std::lock_guard lock(session); + + const auto* region = session.findRegionByName(name); + if (!region) { + throw Fam_Exception(std::string("Region not found: ") + name, FAM_ERR_NOTFOUND); + } + + return new Fam_Region_Descriptor(region->id, region->size, region->perm, name); +} + +void fam::fam_destroy_region(Fam_Region_Descriptor* region_desc) { + if (!region_desc) { + return; + } + + auto& session = getSession(); + std::lock_guard lock(session); + + const auto regionId = region_desc->get_global_descriptor().regionId; + auto* region = session.findRegionById(regionId); + if (!region) { + // Keep region destruction idempotent during test teardown. + region_desc->mock_invalidate(); + return; + } + + session.freeRegion(*region); + region_desc->mock_invalidate(); +} + +void fam::fam_resize_region(Fam_Region_Descriptor* region_desc, std::uint64_t size) { + if (!region_desc) { + throw Fam_Exception("Null region descriptor", FAM_ERR_INVALID); + } + + auto& session = getSession(); + std::lock_guard lock(session); + + auto& region = session.findRegion(region_desc); + region.size = size; + region_desc->mock_setSize(size); +} + +void fam::fam_stat(Fam_Region_Descriptor* region_desc, Fam_Stat* info) { + if (!region_desc) { + throw Fam_Exception("Null region descriptor", FAM_ERR_INVALID); + } + if (!info) { + return; + } + + auto& session = getSession(); + std::lock_guard lock(session); + + const auto& region = session.findRegion(region_desc); + info->size = region.size; + info->perm = region.perm; + std::strncpy(info->name, region.name, sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; + info->uid = 0; + info->gid = 0; +} + +//---------------------------------------------------------------------------------------------------------------------- +// OBJECT OPERATIONS + +Fam_Descriptor* fam::fam_allocate(const char* name, std::uint64_t size, mode_t perm, + Fam_Region_Descriptor* region_desc) { + if (!region_desc) { + throw Fam_Exception("Null region descriptor", FAM_ERR_INVALID); + } + if (size == 0) { + throw Fam_Exception("Object size must be > 0", FAM_ERR_INVALID); + } + + auto& session = getSession(); + std::lock_guard lock(session); + + auto& region = session.findRegion(region_desc); + + // Check for duplicate named object. + if (name && *name && session.findObjectByName(region, name) != nullptr) { + throw Fam_Exception(std::string("Object already exists: ") + name, FAM_ERR_ALREADYEXIST); + } + + // Match test expectations: reject only objects larger than region size. + if (size > region.size) { + throw Fam_Exception("Object exceeds region size", FAM_ERR_NO_SPACE); + } + + auto* slot = session.allocateObjectSlot(region); + if (!slot) { + throw Fam_Exception("Maximum number of objects per region reached", FAM_ERR_NO_SPACE); + } + + const std::uint64_t offset = region.nextOffset; + const std::uint64_t aligned = ((offset + size) + std::uint64_t{7}) & ~std::uint64_t{7}; + region.nextOffset = aligned; + + // Allocate backing storage in the shared data area. + const std::uint64_t dataOff = session.allocateData(size); + + *slot = mock::Object{}; + slot->active = true; + slot->offset = offset; + slot->size = size; + slot->perm = perm; + slot->uid = ::getuid(); + slot->gid = ::getgid(); + slot->dataOffset = dataOff; + + if (name && *name) { + std::strncpy(slot->name, name, mock::g_max_name_len - 1); + slot->name[mock::g_max_name_len - 1] = '\0'; + } + + // Zero-init the data area for this object. + std::memset(session.objectData(*slot), 0, size); + + return new Fam_Descriptor(region.id, offset, size, perm, name, slot->uid, slot->gid); +} + +Fam_Descriptor* fam::fam_lookup(const char* object_name, const char* region_name) { + if (!object_name || !*object_name || !region_name || !*region_name) { + throw Fam_Exception("Invalid name parameter", FAM_ERR_INVALID); + } + + auto& session = getSession(); + std::lock_guard lock(session); + + auto* region = session.findRegionByName(region_name); + if (!region) { + throw Fam_Exception(std::string("Region not found: ") + region_name, FAM_ERR_NOTFOUND); + } + + auto* obj = session.findObjectByName(*region, object_name); + if (!obj) { + throw Fam_Exception(std::string("Object not found: ") + object_name, FAM_ERR_NOTFOUND); + } + + return new Fam_Descriptor(region->id, obj->offset, obj->size, obj->perm, object_name, obj->uid, obj->gid); +} + +void fam::fam_deallocate(Fam_Descriptor* object) { + if (!object) { + return; + } + + auto& session = getSession(); + std::lock_guard lock(session); + + const auto regionId = object->get_global_descriptor().regionId; + const auto objectOffset = object->get_global_descriptor().offset; + + auto* region = session.findRegionById(regionId); + if (!region) { + // Keep object deallocation idempotent during teardown. + object->mock_invalidate(); + return; + } + + auto* obj = session.findObjectByOffset(*region, objectOffset); + if (!obj) { + object->mock_invalidate(); + return; + } + + const auto nextExpectedOffset = objectOffset + obj->size; + const auto nextExpectedAligned = (nextExpectedOffset + std::uint64_t{7}) & ~std::uint64_t{7}; + + session.freeObject(*obj); + + // If this was the last allocated object, reclaim its region offset space. + if (region->nextOffset == nextExpectedAligned) { + region->nextOffset = objectOffset; + } + + object->mock_invalidate(); +} + +void fam::fam_stat(Fam_Descriptor* object, Fam_Stat* info) { + if (!object) { + throw Fam_Exception("Null object descriptor", FAM_ERR_INVALID); + } + if (!info) { + return; + } + + auto& session = getSession(); + std::lock_guard lock(session); + + const auto& obj = session.findObject(object); + info->size = obj.size; + info->perm = obj.perm; + std::strncpy(info->name, obj.name, sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; + info->uid = obj.uid; + info->gid = obj.gid; +} + +//---------------------------------------------------------------------------------------------------------------------- +// DATA I/O + +void fam::fam_put_blocking(void* buffer, Fam_Descriptor* obj, std::uint64_t offset, std::uint64_t length) { + if (!buffer || !obj || length == 0) { + throw Fam_Exception("Invalid parameters to fam_put_blocking", FAM_ERR_INVALID); + } + + auto& session = getSession(); + std::lock_guard lock(session); + + auto& sobj = session.findObject(obj); + if (offset > sobj.size || length > (sobj.size - offset)) { + throw Fam_Exception("Write range out of bounds", FAM_ERR_OUTOFRANGE); + } + std::memcpy(session.objectData(sobj) + offset, buffer, length); +} + +void fam::fam_get_blocking(void* buffer, Fam_Descriptor* obj, std::uint64_t offset, std::uint64_t length) { + if (!buffer || !obj || length == 0) { + throw Fam_Exception("Invalid parameters to fam_get_blocking", FAM_ERR_INVALID); + } + + auto& session = getSession(); + std::lock_guard lock(session); + + const auto& sobj = session.findObject(obj); + if (offset > sobj.size || length > (sobj.size - offset)) { + throw Fam_Exception("Read range out of bounds", FAM_ERR_OUTOFRANGE); + } + std::memcpy(buffer, session.objectData(sobj) + offset, length); +} + +//---------------------------------------------------------------------------------------------------------------------- + +#define OPENFAM_MOCK_DEFINE_FETCH(TYPE, suffix) \ + TYPE fam::fam_fetch_##suffix(Fam_Descriptor* obj, std::uint64_t offset) { \ + auto& session = getSession(); \ + std::lock_guard lock(session); \ + auto& sobj = session.findObject(obj); \ + return typed_fetch(session.objectData(sobj), sobj.size, offset); \ + } + +OPENFAM_MOCK_DEFINE_FETCH(std::int32_t, int32) +OPENFAM_MOCK_DEFINE_FETCH(std::int64_t, int64) +OPENFAM_MOCK_DEFINE_FETCH(int128_t, int128) +OPENFAM_MOCK_DEFINE_FETCH(std::uint32_t, uint32) +OPENFAM_MOCK_DEFINE_FETCH(std::uint64_t, uint64) +OPENFAM_MOCK_DEFINE_FETCH(float, float) +OPENFAM_MOCK_DEFINE_FETCH(double, double) + +#undef OPENFAM_MOCK_DEFINE_FETCH + +#define OPENFAM_MOCK_DEFINE_SET(TYPE) \ + void fam::fam_set(Fam_Descriptor* obj, std::uint64_t offset, TYPE value) { \ + auto& session = getSession(); \ + std::lock_guard lock(session); \ + auto& sobj = session.findObject(obj); \ + typed_store(session.objectData(sobj), sobj.size, offset, value); \ + } + +OPENFAM_MOCK_DEFINE_SET(std::int32_t) +OPENFAM_MOCK_DEFINE_SET(std::int64_t) +OPENFAM_MOCK_DEFINE_SET(int128_t) +OPENFAM_MOCK_DEFINE_SET(std::uint32_t) +OPENFAM_MOCK_DEFINE_SET(std::uint64_t) +OPENFAM_MOCK_DEFINE_SET(float) +OPENFAM_MOCK_DEFINE_SET(double) + +#undef OPENFAM_MOCK_DEFINE_SET + +#define OPENFAM_MOCK_DEFINE_ADD(TYPE) \ + void fam::fam_add(Fam_Descriptor* obj, std::uint64_t offset, TYPE value) { \ + auto& session = getSession(); \ + std::lock_guard lock(session); \ + auto& sobj = session.findObject(obj); \ + auto* data = session.objectData(sobj); \ + auto current = typed_fetch(data, sobj.size, offset); \ + typed_store(data, sobj.size, offset, static_cast(current + value)); \ + } + +OPENFAM_MOCK_DEFINE_ADD(std::int32_t) +OPENFAM_MOCK_DEFINE_ADD(std::int64_t) +OPENFAM_MOCK_DEFINE_ADD(std::uint32_t) +OPENFAM_MOCK_DEFINE_ADD(std::uint64_t) +OPENFAM_MOCK_DEFINE_ADD(float) +OPENFAM_MOCK_DEFINE_ADD(double) + +#undef OPENFAM_MOCK_DEFINE_ADD + +#define OPENFAM_MOCK_DEFINE_SUB(TYPE) \ + void fam::fam_subtract(Fam_Descriptor* obj, std::uint64_t offset, TYPE value) { \ + auto& session = getSession(); \ + std::lock_guard lock(session); \ + auto& sobj = session.findObject(obj); \ + auto* data = session.objectData(sobj); \ + auto current = typed_fetch(data, sobj.size, offset); \ + typed_store(data, sobj.size, offset, static_cast(current - value)); \ + } + +OPENFAM_MOCK_DEFINE_SUB(std::int32_t) +OPENFAM_MOCK_DEFINE_SUB(std::int64_t) +OPENFAM_MOCK_DEFINE_SUB(std::uint32_t) +OPENFAM_MOCK_DEFINE_SUB(std::uint64_t) +OPENFAM_MOCK_DEFINE_SUB(float) +OPENFAM_MOCK_DEFINE_SUB(double) + +#undef OPENFAM_MOCK_DEFINE_SUB + +#define OPENFAM_MOCK_DEFINE_SWAP(TYPE) \ + TYPE fam::fam_swap(Fam_Descriptor* obj, std::uint64_t offset, TYPE value) { \ + auto& session = getSession(); \ + std::lock_guard lock(session); \ + auto& sobj = session.findObject(obj); \ + auto* data = session.objectData(sobj); \ + auto old = typed_fetch(data, sobj.size, offset); \ + typed_store(data, sobj.size, offset, value); \ + return old; \ + } + +OPENFAM_MOCK_DEFINE_SWAP(std::int32_t) +OPENFAM_MOCK_DEFINE_SWAP(std::int64_t) +OPENFAM_MOCK_DEFINE_SWAP(std::uint32_t) +OPENFAM_MOCK_DEFINE_SWAP(std::uint64_t) +OPENFAM_MOCK_DEFINE_SWAP(float) +OPENFAM_MOCK_DEFINE_SWAP(double) + +#undef OPENFAM_MOCK_DEFINE_SWAP + +#define OPENFAM_MOCK_DEFINE_CAS(TYPE) \ + TYPE fam::fam_compare_swap(Fam_Descriptor* obj, std::uint64_t offset, TYPE old_val, TYPE new_val) { \ + auto& session = getSession(); \ + std::lock_guard lock(session); \ + auto& sobj = session.findObject(obj); \ + auto* data = session.objectData(sobj); \ + auto current = typed_fetch(data, sobj.size, offset); \ + if (current == old_val) { \ + typed_store(data, sobj.size, offset, new_val); \ + } \ + return current; \ + } + +OPENFAM_MOCK_DEFINE_CAS(std::int32_t) +OPENFAM_MOCK_DEFINE_CAS(std::int64_t) +OPENFAM_MOCK_DEFINE_CAS(std::uint32_t) +OPENFAM_MOCK_DEFINE_CAS(std::uint64_t) +OPENFAM_MOCK_DEFINE_CAS(int128_t) + +#undef OPENFAM_MOCK_DEFINE_CAS + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace openfam diff --git a/src/eckit/io/fam/openfam_mock/fam/fam.h b/src/eckit/io/fam/openfam_mock/fam/fam.h new file mode 100644 index 000000000..a157536d5 --- /dev/null +++ b/src/eckit/io/fam/openfam_mock/fam/fam.h @@ -0,0 +1,312 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file openfam_mock/fam/fam.h +/// @author Metin Cakircali +/// @date Mar 2026 + +/// @brief Mock of OpenFAM. +// Mirrors the OpenFAM fam.h: types, descriptors, and the openfam::fam class. + +#pragma once + +#include // mode_t + +#include +#include +#include + +//---------------------------------------------------------------------------------------------------------------------- + +struct Fam_Global_Descriptor { + std::uint64_t regionId{0}; + std::uint64_t offset{0}; +}; + +struct Fam_Stat { + std::uint64_t size{0}; + mode_t perm{0}; + char name[256]{}; + std::uint32_t uid{0}; + std::uint32_t gid{0}; +}; + +enum Fam_Permission_Level { + PERMISSION_LEVEL_DEFAULT = 0, + REGION, + DATAITEM, +}; + +struct Fam_Region_Attributes { + int redundancyLevel{0}; + int memoryType{0}; + int interleaveEnable{0}; + Fam_Permission_Level permissionLevel{PERMISSION_LEVEL_DEFAULT}; +}; + +struct Fam_Options { + char* runtime{nullptr}; + char* cisServer{nullptr}; + char* grpcPort{nullptr}; + char* openFamModel{nullptr}; + char* famThreadModel{nullptr}; + char* numConsumer{nullptr}; + char* allocator{nullptr}; + char* rpcOn{nullptr}; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +namespace openfam { + +using int128_t = __int128; + +enum class Fam_Descriptor_Status { + DESC_INVALID, + DESC_INIT_DONE, + DESC_INIT_DONE_BUT_KEY_NOT_VALID, + DESC_UNINITIALIZED, +}; + +//---------------------------------------------------------------------------------------------------------------------- +// Region descriptor + +class Fam_Region_Descriptor { +public: + + Fam_Region_Descriptor() = default; + + Fam_Region_Descriptor(std::uint64_t regionId, std::uint64_t size, mode_t perm, const char* name, + std::uint32_t uid = 0, std::uint32_t gid = 0) : + desc_{regionId, 0}, + size_(size), + perm_(perm), + name_(name ? name : ""), + uid_(uid), + gid_(gid), + status_(Fam_Descriptor_Status::DESC_INIT_DONE) {} + + Fam_Global_Descriptor get_global_descriptor() const noexcept { return desc_; } + Fam_Descriptor_Status get_desc_status() const noexcept { return status_; } + std::uint64_t get_size() const noexcept { return size_; } + mode_t get_perm() const noexcept { return perm_; } + + /// Returns a pointer to the region name string, or nullptr when empty. + const char* get_name() const noexcept { return name_.empty() ? nullptr : name_.c_str(); } + + std::uint32_t get_uid() const noexcept { return uid_; } + std::uint32_t get_gid() const noexcept { return gid_; } + + void set_permissionLevel(Fam_Permission_Level level) noexcept { permLevel_ = level; } + Fam_Permission_Level get_permissionLevel() const noexcept { return permLevel_; } + + /// Internal helpers used by the mock storage implementation. + void mock_invalidate() noexcept { status_ = Fam_Descriptor_Status::DESC_INVALID; } + void mock_setSize(std::uint64_t size) noexcept { size_ = size; } + +private: + + Fam_Global_Descriptor desc_; + std::uint64_t size_{0}; + mode_t perm_{0}; + std::string name_; + std::uint32_t uid_{0}; + std::uint32_t gid_{0}; + Fam_Descriptor_Status status_{Fam_Descriptor_Status::DESC_UNINITIALIZED}; + Fam_Permission_Level permLevel_{PERMISSION_LEVEL_DEFAULT}; +}; + +//---------------------------------------------------------------------------------------------------------------------- +// Object (data item) descriptor + +class Fam_Descriptor { +public: + + /// Construct a proxy descriptor from a raw global address. + /// Used by FamObject(session, region, offset) and replaceWith(). + explicit Fam_Descriptor(Fam_Global_Descriptor desc) : desc_(desc), status_(Fam_Descriptor_Status::DESC_INIT_DONE) {} + + /// Construct a fully-populated descriptor (from allocate / lookup). + Fam_Descriptor(std::uint64_t regionId, std::uint64_t offset, std::uint64_t size, mode_t perm, const char* name, + std::uint32_t uid = 0, std::uint32_t gid = 0) : + desc_{regionId, offset}, + size_(size), + perm_(perm), + name_(name ? name : ""), + uid_(uid), + gid_(gid), + status_(Fam_Descriptor_Status::DESC_INIT_DONE) {} + + Fam_Global_Descriptor get_global_descriptor() const noexcept { return desc_; } + Fam_Descriptor_Status get_desc_status() const noexcept { return status_; } + std::uint64_t get_size() const noexcept { return size_; } + mode_t get_perm() const noexcept { return perm_; } + + /// Returns a pointer to the object name string, or nullptr when empty. + const char* get_name() const noexcept { return name_.empty() ? nullptr : name_.c_str(); } + + std::uint32_t get_uid() const noexcept { return uid_; } + std::uint32_t get_gid() const noexcept { return gid_; } + + /// Internal helpers used by the mock storage implementation. + void mock_invalidate() noexcept { status_ = Fam_Descriptor_Status::DESC_INVALID; } + void mock_setSize(std::uint64_t size) noexcept { size_ = size; } + +private: + + Fam_Global_Descriptor desc_{}; + std::uint64_t size_{0}; + mode_t perm_{0}; + std::string name_; + std::uint32_t uid_{0}; + std::uint32_t gid_{0}; + Fam_Descriptor_Status status_{Fam_Descriptor_Status::DESC_UNINITIALIZED}; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +namespace mock { +class FamMockSession; +} // namespace mock + +class fam { +public: + + fam(); + ~fam(); + + // non-copyable, non-movable (same as real openfam::fam) + fam(const fam&) = delete; + fam& operator=(const fam&) = delete; + fam(fam&&) = delete; + fam& operator=(fam&&) = delete; + + //------------------------------------------------------------------ + // Session lifecycle + + void fam_initialize(const char* name, Fam_Options* options); + void fam_finalize(const char* name); + void fam_abort(int code); + const void* fam_get_option(char* option_name); + + //------------------------------------------------------------------ + // Region operations + + Fam_Region_Descriptor* fam_create_region(const char* name, std::uint64_t size, mode_t perm, + Fam_Region_Attributes* region_attributes); + + Fam_Region_Descriptor* fam_lookup_region(const char* name); + + void fam_destroy_region(Fam_Region_Descriptor* region); + void fam_resize_region(Fam_Region_Descriptor* region, std::uint64_t size); + + void fam_stat(Fam_Region_Descriptor* region, Fam_Stat* info); + + //------------------------------------------------------------------ + // Object operations + + Fam_Descriptor* fam_allocate(const char* name, std::uint64_t size, mode_t perm, Fam_Region_Descriptor* region); + + Fam_Descriptor* fam_lookup(const char* object_name, const char* region_name); + + void fam_deallocate(Fam_Descriptor* object); + void fam_stat(Fam_Descriptor* object, Fam_Stat* info); + + //------------------------------------------------------------------ + // Blocking data I/O + + void fam_put_blocking(void* buffer, Fam_Descriptor* obj, std::uint64_t offset, std::uint64_t length); + + void fam_get_blocking(void* buffer, Fam_Descriptor* obj, std::uint64_t offset, std::uint64_t length); + + //------------------------------------------------------------------ + // Atomic fetch + + std::int32_t fam_fetch_int32(Fam_Descriptor* obj, std::uint64_t offset); + std::int64_t fam_fetch_int64(Fam_Descriptor* obj, std::uint64_t offset); + int128_t fam_fetch_int128(Fam_Descriptor* obj, std::uint64_t offset); + std::uint32_t fam_fetch_uint32(Fam_Descriptor* obj, std::uint64_t offset); + std::uint64_t fam_fetch_uint64(Fam_Descriptor* obj, std::uint64_t offset); + float fam_fetch_float(Fam_Descriptor* obj, std::uint64_t offset); + double fam_fetch_double(Fam_Descriptor* obj, std::uint64_t offset); + + //------------------------------------------------------------------ + // Atomic set (overloaded on value type) + + void fam_set(Fam_Descriptor* obj, std::uint64_t offset, std::int32_t value); + void fam_set(Fam_Descriptor* obj, std::uint64_t offset, std::int64_t value); + void fam_set(Fam_Descriptor* obj, std::uint64_t offset, int128_t value); + void fam_set(Fam_Descriptor* obj, std::uint64_t offset, std::uint32_t value); + void fam_set(Fam_Descriptor* obj, std::uint64_t offset, std::uint64_t value); + void fam_set(Fam_Descriptor* obj, std::uint64_t offset, float value); + void fam_set(Fam_Descriptor* obj, std::uint64_t offset, double value); + + //------------------------------------------------------------------ + // Atomic add + + void fam_add(Fam_Descriptor* obj, std::uint64_t offset, std::int32_t value); + void fam_add(Fam_Descriptor* obj, std::uint64_t offset, std::int64_t value); + void fam_add(Fam_Descriptor* obj, std::uint64_t offset, std::uint32_t value); + void fam_add(Fam_Descriptor* obj, std::uint64_t offset, std::uint64_t value); + void fam_add(Fam_Descriptor* obj, std::uint64_t offset, float value); + void fam_add(Fam_Descriptor* obj, std::uint64_t offset, double value); + + //------------------------------------------------------------------ + // Atomic subtract + + void fam_subtract(Fam_Descriptor* obj, std::uint64_t offset, std::int32_t value); + void fam_subtract(Fam_Descriptor* obj, std::uint64_t offset, std::int64_t value); + void fam_subtract(Fam_Descriptor* obj, std::uint64_t offset, std::uint32_t value); + void fam_subtract(Fam_Descriptor* obj, std::uint64_t offset, std::uint64_t value); + void fam_subtract(Fam_Descriptor* obj, std::uint64_t offset, float value); + void fam_subtract(Fam_Descriptor* obj, std::uint64_t offset, double value); + + //------------------------------------------------------------------ + // Atomic swap (returns old value) + + std::int32_t fam_swap(Fam_Descriptor* obj, std::uint64_t offset, std::int32_t value); + std::int64_t fam_swap(Fam_Descriptor* obj, std::uint64_t offset, std::int64_t value); + std::uint32_t fam_swap(Fam_Descriptor* obj, std::uint64_t offset, std::uint32_t value); + std::uint64_t fam_swap(Fam_Descriptor* obj, std::uint64_t offset, std::uint64_t value); + float fam_swap(Fam_Descriptor* obj, std::uint64_t offset, float value); + double fam_swap(Fam_Descriptor* obj, std::uint64_t offset, double value); + + //------------------------------------------------------------------ + // Atomic compare-and-swap (returns old value; writes new_val only when old matches) + + std::int32_t fam_compare_swap(Fam_Descriptor* obj, std::uint64_t offset, std::int32_t old_val, + std::int32_t new_val); + std::int64_t fam_compare_swap(Fam_Descriptor* obj, std::uint64_t offset, std::int64_t old_val, + std::int64_t new_val); + std::uint32_t fam_compare_swap(Fam_Descriptor* obj, std::uint64_t offset, std::uint32_t old_val, + std::uint32_t new_val); + std::uint64_t fam_compare_swap(Fam_Descriptor* obj, std::uint64_t offset, std::uint64_t old_val, + std::uint64_t new_val); + int128_t fam_compare_swap(Fam_Descriptor* obj, std::uint64_t offset, int128_t old_val, int128_t new_val); + +private: + + mock::FamMockSession& getSession(); + +private: + + std::string serverName_; + + mock::FamMockSession* session_{nullptr}; // local cache, initialized in fam_initialize() +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace openfam diff --git a/src/eckit/io/fam/openfam_mock/fam/fam_exception.h b/src/eckit/io/fam/openfam_mock/fam/fam_exception.h new file mode 100644 index 000000000..49129d8fe --- /dev/null +++ b/src/eckit/io/fam/openfam_mock/fam/fam_exception.h @@ -0,0 +1,76 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file fam_exception.h +/// @author Metin Cakircali +/// @date Mar 2026 + +/// @brief Mock of OpenFAM's fam_exception.h + +#pragma once + +#include +#include +#include + +namespace openfam { + +//---------------------------------------------------------------------------------------------------------------------- + +/// Mirror of OpenFAM error codes +enum Fam_Error { + FAM_SUCCESS = 0, + FAM_ERR_UNKNOWN = 1, + FAM_ERR_NOTFOUND = 2, + FAM_ERR_ALREADYEXIST = 3, + FAM_ERR_NOPERM = 4, + FAM_ERR_INVALID = 5, + FAM_ERR_NO_SPACE = 6, + FAM_ERR_OUTOFRANGE = 7, + FAM_ERR_METADATA = 8, + FAM_ERR_RPC = 9, + FAM_ERR_TIMEOUT = 10, + FAM_ERR_RESOURCE = 11, + FAM_ERR_LIBFABRIC = 12, + FAM_ERR_SHM = 13, + FAM_ERR_GRPC = 14, + FAM_ERR_PMI = 15, + FAM_ERR_UNIMPL = 16, +}; + +//---------------------------------------------------------------------------------------------------------------------- + +// Mirror of OpenFAM's Fam_Exception. Adds fam_error() method to retrieve the error code. +class Fam_Exception : public std::exception { +public: + + explicit Fam_Exception(const char* msg, Fam_Error err = FAM_ERR_UNKNOWN) : + message_(msg ? msg : "OpenFAM mock exception"), error_(err) {} + + explicit Fam_Exception(std::string msg, Fam_Error err = FAM_ERR_UNKNOWN) : message_(std::move(msg)), error_(err) {} + + const char* what() const noexcept override { return message_.c_str(); } + const char* fam_error_msg() const noexcept { return message_.c_str(); } + Fam_Error fam_error() const noexcept { return error_; } + +private: + + std::string message_; + Fam_Error error_; +}; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace openfam diff --git a/src/eckit/net/Endpoint.cc b/src/eckit/net/Endpoint.cc index 343152192..ab3f5543c 100644 --- a/src/eckit/net/Endpoint.cc +++ b/src/eckit/net/Endpoint.cc @@ -10,17 +10,22 @@ #include "eckit/net/Endpoint.h" -#include - #include "eckit/exception/Exceptions.h" +#include "eckit/filesystem/URI.h" #include "eckit/serialisation/Stream.h" #include "eckit/utils/Tokenizer.h" #include "eckit/utils/Translator.h" +#include + namespace eckit::net { //---------------------------------------------------------------------------------------------------------------------- +Endpoint::Endpoint(const URI& uri) : host_(uri.host()), port_(uri.port()) { + validate(); +} + Endpoint::Endpoint(const std::string& s) { Tokenizer tokenize(":"); std::vector tokens; diff --git a/src/eckit/net/Endpoint.h b/src/eckit/net/Endpoint.h index 7df492d5b..dcda4c7cd 100644 --- a/src/eckit/net/Endpoint.h +++ b/src/eckit/net/Endpoint.h @@ -21,6 +21,7 @@ namespace eckit { class Stream; +class URI; namespace net { @@ -30,6 +31,7 @@ class Endpoint { public: // methods + Endpoint(const URI& uri); // gets hostname:port from uri Endpoint(const std::string&); // parses the std::string formated as hostname:port Endpoint(const std::string& host, int port); Endpoint(Stream& s); diff --git a/src/eckit/testing/ProcessFork.h b/src/eckit/testing/ProcessFork.h new file mode 100644 index 000000000..f20c12777 --- /dev/null +++ b/src/eckit/testing/ProcessFork.h @@ -0,0 +1,142 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/// @file ProcessFork.h +/// @author Metin Cakircali +/// @date Apr 2025 + +#pragma once + +#include +#include + +#include +#include +#include + +namespace eckit::testing { + +//---------------------------------------------------------------------------------------------------------------------- + +/// Fork @p n children, each re-exec-ing the current binary as a fresh process. +/// +/// This avoids inheriting in-process state (gRPC channels, libfabric contexts, etc.) +/// that is not fork-safe. Each child gets a completely clean address space. +/// +/// @p args Extra arguments passed to each child on the command line. +/// "--worker-id=<0..n-1>" is prepended automatically. +/// +/// The binary's main() should call parse_worker_args() to detect whether it was +/// launched as a child worker and retrieve the argument map. +/// +/// Returns true if every child exited with status 0. +inline bool fork_and_exec(int n, const std::vector& args = {}) { + // Resolve path to current executable + char exe[4096]; + const ssize_t len = ::readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if (len <= 0) { + return false; + } + exe[len] = '\0'; + + std::vector pids; + pids.reserve(n); + + for (int i = 0; i < n; ++i) { + const pid_t pid = ::fork(); + if (pid < 0) { + for (auto p : pids) { + ::kill(p, SIGTERM); + ::waitpid(p, nullptr, 0); + } + return false; + } + if (pid == 0) { + // Build argv: exe --worker-id=i [extra args...] + std::vector storage; + storage.reserve(2 + args.size()); + storage.push_back(exe); + storage.push_back("--worker-id=" + std::to_string(i)); + for (const auto& a : args) { + storage.push_back(a); + } + + std::vector argv; + argv.reserve(storage.size() + 1); + for (auto& s : storage) { + argv.push_back(s.data()); + } + argv.push_back(nullptr); + + ::execv(exe, argv.data()); + ::_exit(127); // exec failed + } + pids.push_back(pid); + } + + bool all_ok = true; + for (auto pid : pids) { + int status = 0; + if (::waitpid(pid, &status, 0) < 0) { + all_ok = false; + } + else if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + all_ok = false; + } + } + return all_ok; +} + +/// Parse argc/argv for child worker arguments set by fork_and_exec. +/// Returns a map of "--key=value" arguments if "--worker-id=N" is present, +/// or an empty map if this process was not launched as a child worker. +/// Keys are stored without the "--" prefix. +inline std::vector> parse_worker_args(int argc, char** argv) { + std::vector> result; + bool is_worker = false; + + for (int i = 1; i < argc; ++i) { + std::string arg(argv[i]); + if (arg.substr(0, 2) != "--") { + continue; + } + auto eq = arg.find('=', 2); + if (eq == std::string::npos) { + continue; + } + auto key = arg.substr(2, eq - 2); + auto value = arg.substr(eq + 1); + if (key == "worker-id") { + is_worker = true; + } + result.emplace_back(std::move(key), std::move(value)); + } + + if (!is_worker) { + result.clear(); + } + return result; +} + +/// Convenience: extract a named argument from the child worker args map. +inline const std::string& get_worker_arg(const std::vector>& args, + const std::string& key) { + for (const auto& kv : args) { + if (kv.first == key) { + return kv.second; + } + } + static const std::string empty; + return empty; +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::testing diff --git a/tests/io/CMakeLists.txt b/tests/io/CMakeLists.txt index 22a678979..52821a9c7 100644 --- a/tests/io/CMakeLists.txt +++ b/tests/io/CMakeLists.txt @@ -54,3 +54,5 @@ ecbuild_add_test( TARGET eckit_rados-performance INCLUDES ${RADOS_INCLUDE_DIRS} TEST_DEPENDS get_eckit_io_test_data LIBS eckit ) + +add_subdirectory( fam ) diff --git a/tests/io/fam/CMakeLists.txt b/tests/io/fam/CMakeLists.txt new file mode 100644 index 000000000..c7478492a --- /dev/null +++ b/tests/io/fam/CMakeLists.txt @@ -0,0 +1,21 @@ +if( eckit_HAVE_OPENFAM_MOCK ) + # Mock takes priority: per-process shm isolation via PID-mangled endpoints. + list( APPEND _fam_environment + "ECKIT_FAM_MOCK_SHM_SIZE=67108864" + ) +elseif( eckit_HAVE_OPENFAM ) + # Real OpenFAM servers running in docker environment. + list( APPEND _fam_environment ${_test_environment} + "OPENFAM_ROOT=/workspace/cis-rpc_meta-direct_mem-rpc" + "ECKIT_FAM_TEST_ENDPOINT=172.26.0.2:8880" + ) +endif() + +foreach( _test fam_handle fam_list fam_map fam_map_64 fam_map_concurrent fam_name fam_object fam_path fam_property fam_region fam_session_manager fam_uri_manager ) + ecbuild_add_test( TARGET eckit_test_${_test} + SOURCES test_${_test}.cc + CONDITION eckit_HAVE_OPENFAM + LABELS fam + ENVIRONMENT "${_fam_environment}" + LIBS eckit ) +endforeach() diff --git a/tests/io/fam/test_fam_common.h b/tests/io/fam/test_fam_common.h new file mode 100644 index 000000000..9ffcfb27c --- /dev/null +++ b/tests/io/fam/test_fam_common.h @@ -0,0 +1,137 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_common.h +/// @author Metin Cakircali +/// @date Jun 2024 + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "eckit/io/fam/FamRegion.h" +#include "eckit/io/fam/FamRegionName.h" +#include "eckit/io/fam/FamTypes.h" +#include "eckit/testing/ProcessFork.h" + +namespace eckit::test { + +using namespace std::string_literals; + +//---------------------------------------------------------------------------------------------------------------------- +// HELPERS + +namespace fam { + +// This returns a random number as string, unique per process. +inline auto random_number() -> std::string { + struct timeval tv{}; + ::gettimeofday(&tv, nullptr); + ::srandom(static_cast(tv.tv_sec + tv.tv_usec + ::getpid())); + return std::to_string(::random()); +} + +/// Derives the POSIX shm name from an endpoint using the same algorithm as FamMockSession. +/// The mock uses only the host part (cisServer) of the endpoint for the shm name. +inline std::string shm_name_from_endpoint(const std::string& endpoint) { + auto colon = endpoint.rfind(':'); + auto host = (colon != std::string::npos) ? endpoint.substr(0, colon) : endpoint; + std::transform(host.begin(), host.end(), host.begin(), + [](unsigned char ch) { return std::isalnum(ch) ? static_cast(ch) : '_'; }); + return "/eckit_fam_mock_" + (host.empty() ? "default" : host); +} + +/// Per-process unique endpoint so parallel test binaries each get their own shm segment. +/// When ECKIT_FAM_TEST_ENDPOINT is set (real OpenFAM CIS), use it verbatim — the PID +/// mangling is only needed for the mock backend, where the endpoint string becomes a POSIX +/// shm key. With real OpenFAM, the endpoint must be a valid host:port for DNS resolution. +inline const std::string test_endpoint = []() -> std::string { + const char* ep = std::getenv("ECKIT_FAM_TEST_ENDPOINT"); + + // Real OpenFAM: use the endpoint as-is. + if (ep && *ep) { + return ep; + } + + // Mock OpenFAM: append "_" to produce a unique POSIX shm path per process. + auto base = std::string("localhost:8880"); + auto colon = base.rfind(':'); + std::string endpoint; + if (colon == std::string::npos) { + endpoint = base + "_" + std::to_string(::getpid()) + ":0"; + } + else { + auto host = base.substr(0, colon); + auto port = base.substr(colon); // includes ':' + endpoint = host + "_" + std::to_string(::getpid()) + port; + } + static std::string shm_name = shm_name_from_endpoint(endpoint); + std::atexit([] { ::shm_unlink(shm_name.c_str()); }); + // Export the computed endpoint so fork_and_exec'd children use the same shm segment. + ::setenv("ECKIT_FAM_TEST_ENDPOINT", endpoint.c_str(), 1); + fprintf(stderr, "[test_fam_common] PID %d: mock endpoint=%s shm=%s\n", ::getpid(), endpoint.c_str(), + shm_name.c_str()); + return endpoint; +}(); + +class TestFam { +public: + + TestFam() = default; + + ~TestFam() = default; + + // rules + TestFam(const TestFam&) = delete; + TestFam(TestFam&&) = delete; + TestFam& operator=(const TestFam&) = delete; + TestFam& operator=(TestFam&&) = delete; + + static auto makeRandomText(const std::string& text = "") -> std::string { + return "ECKIT_TEST_FAM_" + text + '_' + random_number(); + } + + auto makeRandomRegion(const eckit::fam::size_t size) -> FamRegion { + auto region = name_.withRegion(makeRandomText("REGION")).create(size, 0640, true); + return regions_.emplace_back(region); + } + +private: + + FamRegionName name_{test_endpoint, ""}; + + std::vector regions_; +}; + +} // namespace fam + +//---------------------------------------------------------------------------------------------------------------------- + +using eckit::testing::fork_and_exec; +using eckit::testing::get_worker_arg; +using eckit::testing::parse_worker_args; + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test diff --git a/tests/io/fam/test_fam_handle.cc b/tests/io/fam/test_fam_handle.cc new file mode 100644 index 000000000..8f41dd213 --- /dev/null +++ b/tests/io/fam/test_fam_handle.cc @@ -0,0 +1,372 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_handle.cc +/// @author Metin Cakircali +/// @date Mar 2026 + +#include "test_fam_common.h" + +#include +#include +#include +#include + +#include "eckit/io/Buffer.h" +#include "eckit/io/DataHandle.h" +#include "eckit/io/fam/FamHandle.h" +#include "eckit/io/fam/FamObjectName.h" +#include "eckit/io/fam/FamRegionName.h" +#include "eckit/testing/Test.h" + +using namespace eckit; + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- +// HELPERS + +namespace { + +fam::TestFam tester; + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamHandle: write and read back data") { + constexpr eckit::fam::size_t region_size = 4096; + constexpr eckit::fam::size_t object_size = 256; + + auto region = tester.makeRandomRegion(region_size); + auto region_name = region.name(); + auto object_name = fam::TestFam::makeRandomText("HANDLE_OBJ"); + + const FamObjectName objName(fam::test_endpoint, FamPath{region_name, object_name}); + + const std::string test_data = "Hello, FamHandle world!"; + + // write via FamHandle + { + std::unique_ptr handle(objName.dataHandle(true)); + handle->openForWrite(object_size); + AutoClose closer(*handle); + + EXPECT(handle->estimate() == Length(object_size)); + + const auto written = handle->write(test_data.data(), static_cast(test_data.size())); + EXPECT_EQUAL(written, static_cast(test_data.size())); + + EXPECT(handle->position() == Offset(static_cast(test_data.size()))); + } + + // read back via FamHandle + { + std::unique_ptr handle(objName.dataHandle()); + const auto len = handle->openForRead(); + AutoClose closer(*handle); + + EXPECT(len == Length(object_size)); + + Buffer buf(object_size); + buf.zero(); + const auto bytes_read = handle->read(buf.data(), static_cast(test_data.size())); + EXPECT_EQUAL(bytes_read, static_cast(test_data.size())); + + EXPECT(std::string(static_cast(buf.data()), test_data.size()) == test_data); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamHandle: seek and canSeek") { + constexpr eckit::fam::size_t region_size = 4096; + constexpr eckit::fam::size_t object_size = 128; + + auto region = tester.makeRandomRegion(region_size); + auto region_name = region.name(); + auto object_name = fam::TestFam::makeRandomText("HANDLE_SEEK"); + + const FamObjectName objName(fam::test_endpoint, FamPath{region_name, object_name}); + + const std::string part1 = "AAAA"; + const std::string part2 = "BBBB"; + + // write data in two chunks + { + std::unique_ptr handle(objName.dataHandle(true)); + handle->openForWrite(object_size); + AutoClose closer(*handle); + + handle->write(part1.data(), static_cast(part1.size())); + handle->write(part2.data(), static_cast(part2.size())); + } + + // seek to read second chunk + { + std::unique_ptr handle(objName.dataHandle()); + handle->openForRead(); + AutoClose closer(*handle); + + EXPECT(handle->canSeek()); + + const auto seeked = handle->seek(Offset(static_cast(part1.size()))); + EXPECT(seeked == Offset(static_cast(part1.size()))); + + Buffer buf(part2.size()); + buf.zero(); + const auto bytes = handle->read(buf.data(), static_cast(part2.size())); + EXPECT_EQUAL(bytes, static_cast(part2.size())); + EXPECT(std::string(static_cast(buf.data()), part2.size()) == part2); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamHandle: read returns 0 at end of data") { + constexpr eckit::fam::size_t region_size = 4096; + constexpr eckit::fam::size_t object_size = 32; + + auto region = tester.makeRandomRegion(region_size); + auto region_name = region.name(); + auto object_name = fam::TestFam::makeRandomText("HANDLE_EOF"); + + const FamObjectName objName(fam::test_endpoint, FamPath{region_name, object_name}); + + const std::string data = "short"; + + { + std::unique_ptr handle(objName.dataHandle(true)); + handle->openForWrite(object_size); + AutoClose closer(*handle); + handle->write(data.data(), static_cast(data.size())); + } + + { + std::unique_ptr handle(objName.dataHandle()); + handle->openForRead(); + AutoClose closer(*handle); + + // read entire object + Buffer buf(object_size); + buf.zero(); + handle->read(buf.data(), static_cast(object_size)); + + // at EOF — read returns 0 + char extra = 0; + const auto result = handle->read(&extra, 1); + EXPECT_EQUAL(result, 0); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamHandle: openForWrite on non-existing object allocates") { + constexpr eckit::fam::size_t region_size = 4096; + constexpr eckit::fam::size_t object_size = 64; + + auto region = tester.makeRandomRegion(region_size); + auto region_name = region.name(); + auto object_name = fam::TestFam::makeRandomText("HANDLE_NEW"); + + // object does not exist yet + const FamObjectName obj_name(fam::test_endpoint, FamPath{region_name, object_name}); + EXPECT_NOT(obj_name.exists()); + + { + std::unique_ptr handle(obj_name.dataHandle(true)); + handle->openForWrite(object_size); + AutoClose closer(*handle); + + EXPECT(handle->size() == Length(object_size)); + + const std::string data = "fresh"; + handle->write(data.data(), static_cast(data.size())); + } + + EXPECT(obj_name.exists()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamHandle: openForWrite on existing object with overwrite") { + constexpr eckit::fam::size_t region_size = 4096; + constexpr eckit::fam::size_t object_size = 64; + + auto region = tester.makeRandomRegion(region_size); + auto region_name = region.name(); + auto object_name = fam::TestFam::makeRandomText("HANDLE_OVR"); + + const FamObjectName obj_name(fam::test_endpoint, FamPath{region_name, object_name}); + + // create object first + obj_name.allocate(object_size, true); + EXPECT(obj_name.exists()); + + // open for write with overwrite — should succeed with smaller length + { + std::unique_ptr handle(obj_name.dataHandle(true)); + handle->openForWrite(32); + AutoClose closer(*handle); + + const std::string data = "overwritten"; + handle->write(data.data(), static_cast(data.size())); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamHandle: print") { + constexpr eckit::fam::size_t region_size = 4096; + + auto region = tester.makeRandomRegion(region_size); + auto region_name = region.name(); + auto object_name = fam::TestFam::makeRandomText("HANDLE_PRINT"); + + const FamObjectName obj_name(fam::test_endpoint, FamPath{region_name, object_name}); + + // closed mode + { + FamHandle handle(obj_name); + std::ostringstream oss; + oss << handle; + const auto str = oss.str(); + EXPECT(str.find("FamHandle") != std::string::npos); + EXPECT(str.find("closed") != std::string::npos); + } + + // read mode + { + obj_name.allocate(64, true); + FamHandle handle(obj_name); + handle.openForRead(); + AutoClose closer(handle); + std::ostringstream oss; + oss << handle; + EXPECT(oss.str().find("read") != std::string::npos); + } + + // write mode + { + FamHandle handle(obj_name, true); + handle.openForWrite(64); + AutoClose closer(handle); + std::ostringstream oss; + oss << handle; + EXPECT(oss.str().find("write") != std::string::npos); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamHandle: partHandle") { + constexpr eckit::fam::size_t region_size = 4096; + constexpr eckit::fam::size_t object_size = 128; + + auto region = tester.makeRandomRegion(region_size); + auto region_name = region.name(); + auto object_name = fam::TestFam::makeRandomText("HANDLE_PART"); + + const FamObjectName obj_name(fam::test_endpoint, FamPath{region_name, object_name}); + + const std::string data = "0123456789ABCDEF0123456789ABCDEF"; + + // write full data + { + std::unique_ptr handle(obj_name.dataHandle(true)); + handle->openForWrite(object_size); + AutoClose closer(*handle); + handle->write(data.data(), static_cast(data.size())); + } + + // partHandle creates a FamHandle then the we seek + read + { + OffsetList offsets; + offsets.emplace_back(4); + LengthList lengths; + lengths.emplace_back(8); + + std::unique_ptr handle(obj_name.partHandle(offsets, lengths)); + handle->openForRead(); + AutoClose closer(*handle); + + // seek to the desired offset manually + handle->seek(Offset(4)); + + Buffer buf(8); + buf.zero(); + const auto bytes = handle->read(buf.data(), 8); + EXPECT_EQUAL(bytes, 8); + EXPECT(std::string(static_cast(buf.data()), 8) == "456789AB"); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamHandle: flush (is a no-op but) does not crash") { + constexpr eckit::fam::size_t region_size = 4096; + + auto region = tester.makeRandomRegion(region_size); + auto region_name = region.name(); + auto object_name = fam::TestFam::makeRandomText("HANDLE_FLUSH"); + + const FamObjectName obj_name(fam::test_endpoint, FamPath{region_name, object_name}); + + FamHandle handle(obj_name, true); + handle.openForWrite(64); + AutoClose closer(handle); + + EXPECT_NO_THROW(handle.flush()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamHandle: size before and after open") { + constexpr eckit::fam::size_t region_size = 4096; + constexpr eckit::fam::size_t object_size = 96; + + auto region = tester.makeRandomRegion(region_size); + auto region_name = region.name(); + auto object_name = fam::TestFam::makeRandomText("HANDLE_SIZE"); + + const FamObjectName obj_name(fam::test_endpoint, FamPath{region_name, object_name}); + + { + // size before open returns estimate (0 by default) + FamHandle handle(obj_name); + EXPECT(handle.estimate() == Length(0)); + EXPECT(handle.size() == Length(0)); + } + + // allocate and write + obj_name.allocate(object_size, true); + + { + // size after openForRead returns object size + FamHandle handle(obj_name); + const auto len = handle.openForRead(); + AutoClose closer(handle); + EXPECT(len == Length(object_size)); + EXPECT(handle.size() == Length(object_size)); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test + +int main(int argc, char** argv) { + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_list.cc b/tests/io/fam/test_fam_list.cc new file mode 100644 index 000000000..f157b4f76 --- /dev/null +++ b/tests/io/fam/test_fam_list.cc @@ -0,0 +1,311 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_famlist.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include "test_fam_common.h" + +#include +#include +#include + +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamList.h" +#include "eckit/io/fam/FamRegionName.h" +#include "eckit/runtime/Main.h" +#include "eckit/testing/Test.h" + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- +// HELPERS + +namespace { + +using fam::TestFam; + +TestFam tester; + +const auto list_name = "L" + fam::random_number(); + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamList: create an empty list and validate size, empty, front, back") { + + constexpr const eckit::fam::size_t region_size = 1024; + + auto list_region = tester.makeRandomRegion(region_size); + const auto list = FamList(list_region, list_name); + + // empty list should have size 0 + EXPECT(list.empty()); + EXPECT(list.size() == 0); + + // front/back should throw as it's undefined behavior + EXPECT_THROWS({ auto front = list.front(); }); + EXPECT_THROWS({ auto back = list.back(); }); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamList: pop on empty list throws") { + + constexpr const eckit::fam::size_t region_size = 1024; + + auto region = + FamRegionName(fam::test_endpoint, "").withRegion("RP" + fam::random_number()).create(region_size, 0640, true); + auto list = FamList(region, "LP" + fam::random_number()); + + EXPECT(list.empty()); + EXPECT_EQUAL(list.size(), 0); + + EXPECT_THROWS({ list.popFront(); }); + EXPECT_THROWS({ list.popBack(); }); + + region.destroy(); +} + +CASE("FamList: populate a list and validate size, !empty, front, back") { + + constexpr const eckit::fam::size_t region_size = 1024; + + auto region = + FamRegionName(fam::test_endpoint, "").withRegion("LR" + fam::random_number()).create(region_size, 0640, true); + auto list = FamList(region, list_name); + + // empty list should have size 0 + EXPECT(list.empty()); + EXPECT_EQUAL(list.size(), 0); + + std::string front = "Front FAM List!"; + EXPECT_NO_THROW(list.pushBack(front)); + EXPECT_EQUAL(list.size(), 1); + EXPECT_EQUAL(list.front().view(), front); + EXPECT_EQUAL(list.back().view(), front); + + + std::string back = "Back FAM List!"; + EXPECT_NO_THROW(list.pushBack(back)); + EXPECT_EQUAL(list.size(), 2); + EXPECT_EQUAL(list.front().view(), front); + EXPECT_EQUAL(list.back().view(), back); + + region.destroy(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamList: pop front/back updates size and values") { + + constexpr const eckit::fam::size_t region_size = 1024; + + auto region = + FamRegionName(fam::test_endpoint, "").withRegion("RP" + fam::random_number()).create(region_size, 0640, true); + auto list = FamList(region, "LP" + fam::random_number()); + + std::string first = "first"; + std::string second = "second"; + std::string third = "third"; + + EXPECT_NO_THROW(list.pushBack(first)); + EXPECT_NO_THROW(list.pushBack(second)); + EXPECT_NO_THROW(list.pushBack(third)); + + EXPECT_EQUAL(list.size(), 3); + EXPECT_EQUAL(list.front().view(), first); + EXPECT_EQUAL(list.back().view(), third); + + EXPECT_NO_THROW(list.popFront()); + EXPECT_EQUAL(list.size(), 2); + EXPECT_EQUAL(list.front().view(), second); + EXPECT_EQUAL(list.back().view(), third); + + EXPECT_NO_THROW(list.popBack()); + EXPECT_EQUAL(list.size(), 1); + EXPECT_EQUAL(list.front().view(), second); + EXPECT_EQUAL(list.back().view(), second); + + EXPECT_NO_THROW(list.popBack()); + EXPECT(list.empty()); + EXPECT_EQUAL(list.size(), 0); + + region.destroy(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamList: concurrent pushBack from 4 processes") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + constexpr int num_procs = 4; + constexpr int items_per_proc = 50; + + auto region = tester.makeRandomRegion(region_size); + auto name = "MPL" + fam::random_number(); + + { FamList list(region, name); } + + bool ok = fork_and_exec(num_procs, { + "--fn=pushBack", + "--region=" + region.name(), + "--list=" + name, + "--count=" + std::to_string(items_per_proc), + }); + + EXPECT(ok); + + FamList list(region, name); + EXPECT_EQUAL(list.size(), num_procs * items_per_proc); + + std::set found; + for (const auto& item : list) { + found.insert(std::string(item.view())); + } + EXPECT_EQUAL(found.size(), static_cast(num_procs * items_per_proc)); + + for (int c = 0; c < num_procs; ++c) { + for (int i = 0; i < items_per_proc; ++i) { + EXPECT(found.count("c" + std::to_string(c) + "-i" + std::to_string(i)) == 1); + } + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamList: one writer process, parent reads") { + constexpr eckit::fam::size_t region_size = 2 * 1024 * 1024; + constexpr int count = 20; + + auto region = tester.makeRandomRegion(region_size); + auto name = "MPWR" + fam::random_number(); + + { FamList list(region, name); } + + bool ok = fork_and_exec(1, { + "--fn=write", + "--region=" + region.name(), + "--list=" + name, + "--count=" + std::to_string(count), + }); + + EXPECT(ok); + + FamList list(region, name); + EXPECT_EQUAL(list.size(), count); + + std::set found; + for (const auto& item : list) { + found.insert(std::string(item.view())); + } + for (int i = 0; i < count; ++i) { + EXPECT(found.count("item-" + std::to_string(i)) == 1); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamList: concurrent pushFront and pushBack from 4 processes") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + constexpr int num_procs = 4; + constexpr int items_per_proc = 50; + + auto region = tester.makeRandomRegion(region_size); + auto name = "MPFB" + fam::random_number(); + + { FamList list(region, name); } + + bool ok = fork_and_exec(num_procs, { + "--fn=pushFrontBack", + "--region=" + region.name(), + "--list=" + name, + "--count=" + std::to_string(items_per_proc), + }); + + EXPECT(ok); + + FamList list(region, name); + EXPECT_EQUAL(list.size(), num_procs * items_per_proc); + + std::set found; + for (const auto& item : list) { + found.insert(std::string(item.view())); + } + EXPECT_EQUAL(found.size(), static_cast(num_procs * items_per_proc)); +} + +} // namespace eckit::test + +//---------------------------------------------------------------------------------------------------------------------- + +namespace { + +/// Look up a pre-created region by name from the test FAM endpoint. +eckit::FamRegion lookupRegion(const std::string& region_name) { + return eckit::FamRegionName(eckit::test::fam::test_endpoint, "").withRegion(region_name).lookup(); +} + +int child_worker_main(int argc, char** argv) { + auto args = eckit::testing::parse_worker_args(argc, argv); + int child_id = std::stoi(eckit::testing::get_worker_arg(args, "worker-id")); + auto fn = eckit::testing::get_worker_arg(args, "fn"); + auto region = lookupRegion(eckit::testing::get_worker_arg(args, "region")); + auto list_name = eckit::testing::get_worker_arg(args, "list"); + int count = std::stoi(eckit::testing::get_worker_arg(args, "count")); + + if (fn == "pushBack") { + eckit::FamList list(region, list_name); + for (int i = 0; i < count; ++i) { + auto data = "c" + std::to_string(child_id) + "-i" + std::to_string(i); + list.pushBack(data.data(), data.size()); + } + return 0; + } + if (fn == "write") { + eckit::FamList list(region, list_name); + for (int i = 0; i < count; ++i) { + auto data = "item-" + std::to_string(i); + list.pushBack(data.data(), data.size()); + } + return 0; + } + if (fn == "pushFrontBack") { + eckit::FamList list(region, list_name); + for (int i = 0; i < count; ++i) { + auto data = "c" + std::to_string(child_id) + "-i" + std::to_string(i); + if (child_id % 2 == 0) { + list.pushFront(data.data(), data.size()); + } + else { + list.pushBack(data.data(), data.size()); + } + } + return 0; + } + return 1; +} + +} // namespace + +int main(int argc, char** argv) { + auto args = eckit::testing::parse_worker_args(argc, argv); + if (!args.empty()) { + eckit::Main::initialise(argc, argv); + return child_worker_main(argc, argv); + } + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_map.cc b/tests/io/fam/test_fam_map.cc new file mode 100644 index 000000000..c68b5a4a2 --- /dev/null +++ b/tests/io/fam/test_fam_map.cc @@ -0,0 +1,610 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_map.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include "test_fam_common.h" + +#include +#include +#include + +#include "eckit/io/fam/FamMap.h" +#include "eckit/testing/Test.h" + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- +// HELPERS + +namespace { + +fam::TestFam tester; + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +// +// KeySize = 32 +// +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: create empty map and validate size/empty") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("M" + fam::random_number(), region); + + EXPECT(map.empty()); + EXPECT_EQUAL(map.size(), 0); + EXPECT(map.begin() == map.end()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: insert single entry and find it") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MS" + fam::random_number(), region); + + FamMap32::key_type key("hello"); + std::string value = "world"; + + auto [iter, inserted] = map.insert(key, value); + + EXPECT(inserted); + EXPECT_EQUAL(map.size(), 1); + EXPECT_NOT(map.empty()); + + EXPECT(map.contains(key)); + auto found = map.find(key); + EXPECT(found != map.end()); + + auto entry = *found; + EXPECT(entry.key == key); + EXPECT_EQUAL(entry.value.view(), value); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: insert duplicate key returns false") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MD" + fam::random_number(), region); + + FamMap32::key_type key("duplicate"); + std::string val1 = "first"; + std::string val2 = "second"; + + auto [it1, success1] = map.insert(key, val1); + EXPECT(success1); + EXPECT_EQUAL(map.size(), 1); + + auto [it2, success2] = map.insert(key, val2); + EXPECT_NOT(success2); + EXPECT_EQUAL(map.size(), 1); + + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.view(), val1); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: find non-existent key returns end") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MM" + fam::random_number(), region); + + FamMap32::key_type key("ghost"); + + EXPECT_NOT(map.contains(key)); + EXPECT(map.find(key) == map.end()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: insert multiple entries and iterate") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MI" + fam::random_number(), region); + + constexpr std::size_t count = 20; + std::set expected_keys; + + for (std::size_t i = 0; i < count; ++i) { + auto key_str = "key-" + std::to_string(i); + auto val_str = "val-" + std::to_string(i); + FamMap32::key_type key(key_str); + + auto [iter, success] = map.insert(key, val_str); + EXPECT(success); + expected_keys.insert(key_str); + } + + EXPECT_EQUAL(map.size(), count); + + std::set found_keys; + for (auto entry : map) { + found_keys.insert(entry.key.asString()); + auto key_str = entry.key.asString(); + auto expected_val = "val-" + key_str.substr(4); + EXPECT_EQUAL(entry.value.view(), expected_val); + } + + EXPECT_EQUAL(found_keys.size(), count); + EXPECT(found_keys == expected_keys); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: erase existing key") { + constexpr eckit::fam::size_t region_size = 2 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("ME" + fam::random_number(), region); + + FamMap32::key_type key1("alpha"); + FamMap32::key_type key2("beta"); + FamMap32::key_type key3("gamma"); + + map.insert(key1, "a"); + map.insert(key2, "b"); + map.insert(key3, "c"); + EXPECT_EQUAL(map.size(), 3); + + auto erased = map.erase(key2); + EXPECT_EQUAL(erased, 1); + EXPECT_EQUAL(map.size(), 2); + EXPECT_NOT(map.contains(key2)); + + EXPECT(map.contains(key1)); + EXPECT(map.contains(key3)); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: erase non-existent key returns 0") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MX" + fam::random_number(), region); + + FamMap32::key_type key("phantom"); + EXPECT_EQUAL(map.erase(key), 0); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: clear removes all entries") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MC" + fam::random_number(), region); + + for (std::size_t i = 0; i < 10; ++i) { + FamMap32::key_type key("clr-" + std::to_string(i)); + map.insert(key, "data"); + } + EXPECT_EQUAL(map.size(), 10); + + map.clear(); + EXPECT(map.empty()); + EXPECT_EQUAL(map.size(), 0); + EXPECT(map.begin() == map.end()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: idempotent reopen preserves data") { + constexpr eckit::fam::size_t region_size = 2 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto name = "MR" + fam::random_number(); + + FamMap32::key_type key("persist"); + std::string value = "survive"; + + { + auto map = FamMap32(name, region); + map.insert(key, value); + EXPECT_EQUAL(map.size(), 1); + } + + { + auto map = FamMap32(name, region); + EXPECT_EQUAL(map.size(), 1); + EXPECT(map.contains(key)); + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.view(), value); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: insert with empty value") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MV" + fam::random_number(), region); + + FamMap32::key_type key("no-value"); + auto [iter, success] = map.insert(key, nullptr, 0); + EXPECT(success); + EXPECT_EQUAL(map.size(), 1); + + auto entry = *map.find(key); + EXPECT(entry.key == key); + EXPECT_EQUAL(entry.value.size(), 0); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: insertOrAssign inserts new entry") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MO" + fam::random_number(), region); + + FamMap32::key_type key("assign-key"); + std::string val = "first"; + + auto [iter, inserted] = map.insertOrAssign(key, val); + EXPECT(inserted); + EXPECT_EQUAL(map.size(), 1); + + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.view(), val); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: insertOrAssign replaces existing entry") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MP" + fam::random_number(), region); + + FamMap32::key_type key("replace-me"); + std::string val1 = "original"; + std::string val2 = "replaced"; + + auto [it1, ins1] = map.insertOrAssign(key, val1); + EXPECT(ins1); + EXPECT_EQUAL(map.size(), 1); + + auto [it2, ins2] = map.insertOrAssign(key, val2); + EXPECT_NOT(ins2); // replaced, not inserted + EXPECT_EQUAL(map.size(), 1); + + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.view(), val2); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: forceInsert allows duplicate keys") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MF" + fam::random_number(), region); + + FamMap32::key_type key("dup"); + std::string val1 = "first"; + std::string val2 = "second"; + + map.forceInsert(key, val1); + map.forceInsert(key, val2); + + EXPECT_EQUAL(map.size(), 2); + EXPECT(map.contains(key)); + + // find returns the most recently inserted (pushFront) + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.view(), val2); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: count returns correct number of entries per key") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MN" + fam::random_number(), region); + + FamMap32::key_type key("counted"); + FamMap32::key_type other("other"); + + EXPECT_EQUAL(map.count(key), 0); + + map.forceInsert(key, "a"); + EXPECT_EQUAL(map.count(key), 1); + + map.forceInsert(key, "b"); + EXPECT_EQUAL(map.count(key), 2); + + map.forceInsert(other, "c"); + EXPECT_EQUAL(map.count(key), 2); + EXPECT_EQUAL(map.count(other), 1); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: loadFactor reflects entry count") { + constexpr eckit::fam::size_t region_size = 2 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("ML" + fam::random_number(), region); + + EXPECT(map.loadFactor() == 0.0f); + + for (std::size_t i = 0; i < 10; ++i) { + FamMap32::key_type key("lf-" + std::to_string(i)); + map.insert(key, "v"); + } + + float expected = 10.0f / static_cast(FamMap32::bucket_count); + EXPECT(map.loadFactor() == expected); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: merge copies entries from another map") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map1 = FamMap32("MG1" + fam::random_number(), region); + auto map2 = FamMap32("MG2" + fam::random_number(), region); + + FamMap32::key_type k1("alpha"); + FamMap32::key_type k2("beta"); + FamMap32::key_type k3("gamma"); + + map1.insert(k1, "a1"); + map2.insert(k2, "b2"); + map2.insert(k3, "c3"); + + map1.merge(map2); + + EXPECT_EQUAL(map1.size(), 3); + EXPECT(map1.contains(k1)); + EXPECT(map1.contains(k2)); + EXPECT(map1.contains(k3)); + + EXPECT_EQUAL((*map1.find(k2)).value.view(), "b2"); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: merge skips existing keys") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map1 = FamMap32("MH1" + fam::random_number(), region); + auto map2 = FamMap32("MH2" + fam::random_number(), region); + + FamMap32::key_type key("shared"); + + map1.insert(key, "from-map1"); + map2.insert(key, "from-map2"); + + map1.merge(map2); + + EXPECT_EQUAL(map1.size(), 1); + EXPECT_EQUAL((*map1.find(key)).value.view(), "from-map1"); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: erase after forceInsert removes all duplicates") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MQ" + fam::random_number(), region); + + FamMap32::key_type key("multi"); + + map.forceInsert(key, "a"); + map.forceInsert(key, "b"); + map.forceInsert(key, "c"); + EXPECT_EQUAL(map.size(), 3); + EXPECT_EQUAL(map.count(key), 3); + + auto erased = map.erase(key); + EXPECT_EQUAL(erased, 3); + EXPECT_EQUAL(map.size(), 0); + EXPECT_NOT(map.contains(key)); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: forceInsert entries are all visible during iteration") { + constexpr eckit::fam::size_t region_size = 2 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MJ" + fam::random_number(), region); + + FamMap32::key_type key("multi"); + + map.forceInsert(key, "val-a"); + map.forceInsert(key, "val-b"); + map.forceInsert(key, "val-c"); + EXPECT_EQUAL(map.size(), 3); + + // Iterate all entries — all three duplicates must appear. + std::set values; + for (const auto& [k, v] : map) { + EXPECT(k == key); + values.insert(std::string{v.view()}); + } + EXPECT_EQUAL(values.size(), 3); + EXPECT(values.count("val-a")); + EXPECT(values.count("val-b")); + EXPECT(values.count("val-c")); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: insertOrAssign repeated replacement keeps size 1") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MW" + fam::random_number(), region); + + FamMap32::key_type key("hot-key"); + + for (int i = 0; i < 5; ++i) { + auto val = "v" + std::to_string(i); + auto [iter, inserted] = map.insertOrAssign(key, val); + // First iteration inserts, the rest replace. + if (i == 0) { + EXPECT(inserted); + } + else { + EXPECT_NOT(inserted); + } + EXPECT_EQUAL(map.size(), 1); + } + + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.view(), "v4"); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: clear then re-insert works") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MY" + fam::random_number(), region); + + for (std::size_t i = 0; i < 5; ++i) { + FamMap32::key_type key("phase1-" + std::to_string(i)); + map.insert(key, "data"); + } + EXPECT_EQUAL(map.size(), 5); + + map.clear(); + EXPECT(map.empty()); + EXPECT(map.begin() == map.end()); + + // Re-insert different keys. + for (std::size_t i = 0; i < 3; ++i) { + FamMap32::key_type key("phase2-" + std::to_string(i)); + auto [iter, success] = map.insert(key, "new"); + EXPECT(success); + } + EXPECT_EQUAL(map.size(), 3); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: move construction transfers ownership") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map1 = FamMap32("MZ" + fam::random_number(), region); + + FamMap32::key_type key("movable"); + map1.insert(key, "data"); + EXPECT_EQUAL(map1.size(), 1); + + auto map2 = std::move(map1); + EXPECT_EQUAL(map2.size(), 1); + EXPECT(map2.contains(key)); + EXPECT_EQUAL((*map2.find(key)).value.view(), "data"); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: operator<< prints summary") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MA" + fam::random_number(), region); + + map.insert(FamMap32::key_type("k1"), "v1"); + map.insert(FamMap32::key_type("k2"), "v2"); + + std::ostringstream oss; + oss << map; + + auto str = oss.str(); + // Verify the output contains expected substrings. + EXPECT(str.find("FamMap") != std::string::npos); + EXPECT(str.find("key_size=32") != std::string::npos); + EXPECT(str.find("size=2") != std::string::npos); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: large binary values") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MB" + fam::random_number(), region); + + // Simulate FDB-like payload: a 512-byte binary blob. + constexpr std::size_t payload_size = 512; + std::vector blob(payload_size); + for (std::size_t i = 0; i < payload_size; ++i) { + blob[i] = static_cast(i % 256); + } + + FamMap32::key_type key("big-val"); + auto [iter, success] = map.insert(key, blob.data(), payload_size); + EXPECT(success); + + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.size(), payload_size); + EXPECT(std::memcmp(entry.value.data(), blob.data(), payload_size) == 0); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: cbegin/cend const iteration") { + constexpr eckit::fam::size_t region_size = 2 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap32("MU" + fam::random_number(), region); + + map.insert(FamMap32::key_type("a"), "1"); + map.insert(FamMap32::key_type("b"), "2"); + + std::size_t count = 0; + for (auto it = map.cbegin(); it != map.cend(); ++it) { + auto entry = *it; + EXPECT(entry.value.size() > 0); + ++count; + } + EXPECT_EQUAL(count, 2); +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test + +//---------------------------------------------------------------------------------------------------------------------- + +int main(int argc, char** argv) { + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_map_64.cc b/tests/io/fam/test_fam_map_64.cc new file mode 100644 index 000000000..f1c3ce4e8 --- /dev/null +++ b/tests/io/fam/test_fam_map_64.cc @@ -0,0 +1,251 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_map_64.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include "test_fam_common.h" + +#include +#include +#include + +#include "eckit/io/fam/FamMap.h" +#include "eckit/testing/Test.h" + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- +// HELPERS + +namespace { + +fam::TestFam tester; + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<64>: create empty map and validate size/empty") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap64("N" + fam::random_number(), region); + + EXPECT(map.empty()); + EXPECT_EQUAL(map.size(), 0); + EXPECT_EQUAL(map.key_size, 64); + EXPECT(map.begin() == map.end()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<64>: insert single entry and find it") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap64("NS" + fam::random_number(), region); + + FamMap64::key_type key("long-key-for-64-byte-test"); + std::string value = "data-64"; + + auto [iter, inserted] = map.insert(key, value); + + EXPECT(inserted); + EXPECT_EQUAL(map.size(), 1); + EXPECT_NOT(map.empty()); + + EXPECT(map.contains(key)); + auto found = map.find(key); + EXPECT(found != map.end()); + + auto entry = *found; + EXPECT(entry.key == key); + EXPECT_EQUAL(entry.value.view(), value); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<64>: insert duplicate key returns false") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap64("ND" + fam::random_number(), region); + + FamMap64::key_type key("dup-64-byte-key"); + std::string val1 = "first-64"; + std::string val2 = "second-64"; + + auto [it1, success1] = map.insert(key, val1); + EXPECT(success1); + EXPECT_EQUAL(map.size(), 1); + + auto [it2, success2] = map.insert(key, val2); + EXPECT_NOT(success2); + EXPECT_EQUAL(map.size(), 1); + + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.view(), val1); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<64>: find non-existent key returns end") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap64("NM" + fam::random_number(), region); + + FamMap64::key_type key("ghost-64"); + + EXPECT_NOT(map.contains(key)); + EXPECT(map.find(key) == map.end()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<64>: insert multiple entries and iterate") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap64("NI" + fam::random_number(), region); + + constexpr std::size_t count = 20; + std::set expected_keys; + + for (std::size_t i = 0; i < count; ++i) { + auto key_str = "key64-" + std::to_string(i); + auto val_str = "val64-" + std::to_string(i); + FamMap64::key_type key(key_str); + + auto [iter, success] = map.insert(key, val_str); + EXPECT(success); + expected_keys.insert(key_str); + } + + EXPECT_EQUAL(map.size(), count); + + std::set found_keys; + for (auto entry : map) { + found_keys.insert(entry.key.asString()); + auto key_str = entry.key.asString(); + auto expected_val = "val64-" + key_str.substr(6); + EXPECT_EQUAL(entry.value.view(), expected_val); + } + + EXPECT_EQUAL(found_keys.size(), count); + EXPECT(found_keys == expected_keys); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<64>: erase existing key") { + constexpr eckit::fam::size_t region_size = 2 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap64("NE" + fam::random_number(), region); + + FamMap64::key_type key1("alpha-64"); + FamMap64::key_type key2("beta-64"); + FamMap64::key_type key3("gamma-64"); + + map.insert(key1, "a"); + map.insert(key2, "b"); + map.insert(key3, "c"); + EXPECT_EQUAL(map.size(), 3); + + auto erased = map.erase(key2); + EXPECT_EQUAL(erased, 1); + EXPECT_EQUAL(map.size(), 2); + EXPECT_NOT(map.contains(key2)); + + EXPECT(map.contains(key1)); + EXPECT(map.contains(key3)); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<64>: clear removes all entries") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap64("NC" + fam::random_number(), region); + + for (std::size_t i = 0; i < 10; ++i) { + FamMap64::key_type key("clr64-" + std::to_string(i)); + map.insert(key, "data64"); + } + EXPECT_EQUAL(map.size(), 10); + + map.clear(); + EXPECT(map.empty()); + EXPECT_EQUAL(map.size(), 0); + EXPECT(map.begin() == map.end()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<64>: idempotent reopen preserves data") { + constexpr eckit::fam::size_t region_size = 2 * 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto name = "NR" + fam::random_number(); + + FamMap64::key_type key("persist-64"); + std::string value = "survive-64"; + + { + auto map = FamMap64(name, region); + map.insert(key, value); + EXPECT_EQUAL(map.size(), 1); + } + + { + auto map = FamMap64(name, region); + EXPECT_EQUAL(map.size(), 1); + EXPECT(map.contains(key)); + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.view(), value); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<64>: insert with empty value") { + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + auto region = tester.makeRandomRegion(region_size); + auto map = FamMap64("NV" + fam::random_number(), region); + + FamMap64::key_type key("no-value-64"); + auto [iter, success] = map.insert(key, nullptr, 0); + EXPECT(success); + EXPECT_EQUAL(map.size(), 1); + + auto entry = *map.find(key); + EXPECT(entry.key == key); + EXPECT_EQUAL(entry.value.size(), 0); +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test + +//---------------------------------------------------------------------------------------------------------------------- + +int main(int argc, char** argv) { + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_map_concurrent.cc b/tests/io/fam/test_fam_map_concurrent.cc new file mode 100644 index 000000000..e4e0e3587 --- /dev/null +++ b/tests/io/fam/test_fam_map_concurrent.cc @@ -0,0 +1,247 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_map_concurrent.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include "test_fam_common.h" + +#include +#include +#include +#include + +#include "eckit/io/fam/FamMap.h" +#include "eckit/io/fam/FamRegionName.h" +#include "eckit/log/Log.h" +#include "eckit/runtime/Main.h" +#include "eckit/testing/Test.h" + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- +// HELPERS + +namespace { + +fam::TestFam tester; + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- +// CHILD WORKER — runs in a clean exec'd process (no inherited gRPC state) + +namespace { + +/// Look up a pre-created region by name from the test FAM endpoint. +FamRegion lookupRegion(const std::string& region_name) { + return FamRegionName(fam::test_endpoint, "").withRegion(region_name).lookup(); +} + +/// Worker entry point for "concurrent insert" test. +int worker_insert(int child_id, const std::vector>& args) { + auto region = lookupRegion(get_worker_arg(args, "region")); + auto map_name = get_worker_arg(args, "map"); + int count = std::stoi(get_worker_arg(args, "count")); + + FamMap32 map(map_name, region); + for (int i = 0; i < count; ++i) { + auto key_str = "p" + std::to_string(child_id) + "-k" + std::to_string(i); + auto val_str = "v" + std::to_string(child_id) + "-" + std::to_string(i); + FamMap32::key_type key(key_str); + auto [iter, success] = map.insert(key, val_str); + if (!success) { + return 2; + } + } + return 0; +} + +/// Worker entry point for "one writer" test. +int worker_write(int /*child_id*/, const std::vector>& args) { + auto region = lookupRegion(get_worker_arg(args, "region")); + auto map_name = get_worker_arg(args, "map"); + int count = std::stoi(get_worker_arg(args, "count")); + + FamMap32 map(map_name, region); + for (int i = 0; i < count; ++i) { + auto key_str = "wk-" + std::to_string(i); + auto val_str = "wv-" + std::to_string(i); + FamMap32::key_type key(key_str); + map.insert(key, val_str); + } + return 0; +} + +/// Worker entry point for "lock/unlock" test. +int worker_lock(int /*child_id*/, const std::vector>& args) { + auto region = lookupRegion(get_worker_arg(args, "region")); + auto map_name = get_worker_arg(args, "map"); + int count = std::stoi(get_worker_arg(args, "count")); + + FamMap32 map(map_name, region); + FamMap32::key_type key("counter"); + for (int i = 0; i < count; ++i) { + map.lock(); + auto entry = *map.find(key); + int val = std::stoi(std::string(entry.value.view())); + map.insertOrAssign(key, std::to_string(val + 1)); + map.unlock(); + } + return 0; +} + +/// Dispatch table for child workers. +int child_worker_main(int argc, char** argv) { + auto args = parse_worker_args(argc, argv); + int child_id = std::stoi(get_worker_arg(args, "worker-id")); + auto fn = get_worker_arg(args, "fn"); + + if (fn == "insert") { + return worker_insert(child_id, args); + } + if (fn == "write") { + return worker_write(child_id, args); + } + if (fn == "lock") { + return worker_lock(child_id, args); + } + + return 1; // unknown worker +} + +} // namespace + +//---------------------------------------------------------------------------------------------------------------------- + + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: concurrent insert from 4 processes") { + constexpr eckit::fam::size_t region_size = 16 * 1024 * 1024; + constexpr int num_procs = 4; + constexpr int items_per_proc = 50; + + auto region = tester.makeRandomRegion(region_size); + auto map_name = "MPM" + fam::random_number(); + + { FamMap32 map(map_name, region); } + + bool ok = fork_and_exec(num_procs, { + "--fn=insert", + "--region=" + region.name(), + "--map=" + map_name, + "--count=" + std::to_string(items_per_proc), + }); + + EXPECT(ok); + + FamMap32 map(map_name, region); + EXPECT_EQUAL(map.size(), static_cast(num_procs * items_per_proc)); + + for (int c = 0; c < num_procs; ++c) { + for (int i = 0; i < items_per_proc; ++i) { + auto key_str = "p" + std::to_string(c) + "-k" + std::to_string(i); + FamMap32::key_type key(key_str); + EXPECT(map.contains(key)); + + auto entry = *map.find(key); + auto expected_val = "v" + std::to_string(c) + "-" + std::to_string(i); + EXPECT_EQUAL(entry.value.view(), expected_val); + } + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: one writer process, parent reads") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + constexpr int count = 30; + + auto region = tester.makeRandomRegion(region_size); + auto map_name = "MPKV" + fam::random_number(); + + { FamMap32 map(map_name, region); } + + bool ok = fork_and_exec(1, { + "--fn=write", + "--region=" + region.name(), + "--map=" + map_name, + "--count=" + std::to_string(count), + }); + + EXPECT(ok); + + FamMap32 map(map_name, region); + EXPECT_EQUAL(map.size(), static_cast(count)); + + for (int i = 0; i < count; ++i) { + auto key_str = "wk-" + std::to_string(i); + FamMap32::key_type key(key_str); + EXPECT(map.contains(key)); + + auto entry = *map.find(key); + EXPECT_EQUAL(entry.value.view(), "wv-" + std::to_string(i)); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamMap<32>: lock/unlock serialises concurrent read-modify-write") { + constexpr eckit::fam::size_t region_size = 4 * 1024 * 1024; + constexpr int num_writers = 4; + constexpr int increments_per_proc = 50; + + auto region = tester.makeRandomRegion(region_size); + auto map_name = "MPLK" + fam::random_number(); + + // Pre-create map and seed the counter key with "0". + { + FamMap32 map(map_name, region); + FamMap32::key_type key("counter"); + map.insert(key, "0"); + } + + bool ok = fork_and_exec(num_writers, { + "--fn=lock", + "--region=" + region.name(), + "--map=" + map_name, + "--count=" + std::to_string(increments_per_proc), + }); + + EXPECT(ok); + + FamMap32 map(map_name, region); + FamMap32::key_type key("counter"); + auto entry = *map.find(key); + int final_val = std::stoi(std::string(entry.value.view())); + int expected_val = num_writers * increments_per_proc; + eckit::Log::info() << "lock/unlock counter: " << final_val << "/" << expected_val << std::endl; + EXPECT_EQUAL(final_val, expected_val); +} + +} // namespace eckit::test + +//---------------------------------------------------------------------------------------------------------------------- + +int main(int argc, char** argv) { + auto args = eckit::testing::parse_worker_args(argc, argv); + if (!args.empty()) { + eckit::Main::initialise(argc, argv); + return eckit::test::child_worker_main(argc, argv); + } + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_name.cc b/tests/io/fam/test_fam_name.cc new file mode 100644 index 000000000..d5d82ba10 --- /dev/null +++ b/tests/io/fam/test_fam_name.cc @@ -0,0 +1,74 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_name.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include "test_fam_common.h" + +#include + +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamName.h" +#include "eckit/io/fam/FamRegionName.h" +#include "eckit/serialisation/ResizableMemoryStream.h" +#include "eckit/testing/Test.h" + +using namespace eckit; + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamName: stream round-trip via FamRegionName") { + const FamRegionName original(fam::test_endpoint, "streamTestRegion"); + + // Serialize + Buffer buffer(1024); + { + ResizableMemoryStream stream(buffer); + stream << original; + } + + // Deserialize — use endpoint + path from stream + { + ResizableMemoryStream stream(buffer); + const FamRegionName decoded(stream); + EXPECT_EQUAL(decoded.path().regionName(), "streamTestRegion"); + EXPECT_EQUAL(decoded.endpoint().host(), original.endpoint().host()); + } +} + +CASE("FamName: print and asString") { + const FamRegionName name(fam::test_endpoint, "printRegion"); + + std::ostringstream oss; + oss << name; + EXPECT(oss.str().find("printRegion") != std::string::npos); + EXPECT(oss.str().find("endpoint") != std::string::npos); + + const auto str = name.asString(); + EXPECT(str.find("fam://") != std::string::npos); + EXPECT(str.find("printRegion") != std::string::npos); +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test + +int main(int argc, char** argv) { + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_object.cc b/tests/io/fam/test_fam_object.cc new file mode 100644 index 000000000..4a68f945d --- /dev/null +++ b/tests/io/fam/test_fam_object.cc @@ -0,0 +1,263 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_object.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include "test_fam_common.h" + +#include +#include + +#include "eckit/exception/Exceptions.h" +#include "eckit/filesystem/URI.h" +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamObjectName.h" +#include "eckit/io/fam/FamPath.h" +#include "eckit/io/fam/FamProperty.h" +#include "eckit/io/fam/FamRegionName.h" +#include "eckit/testing/Test.h" + +using namespace eckit; +using namespace std::string_literals; + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamObjectName: ctor, lookup, and allocate") { + FamPath path{fam::TestFam::makeRandomText("REGION"), fam::TestFam::makeRandomText("OBJECT")}; + + // create region + EXPECT_NO_THROW(FamRegionName(fam::test_endpoint, "").withRegion(path.regionName()).create(1024, 0640)); + + const FamObjectName object(fam::test_endpoint, path); + + EXPECT_EQUAL(object.uri().scheme(), eckit::fam::scheme); + EXPECT_EQUAL(object.uri().hostport(), fam::test_endpoint); + EXPECT_EQUAL(object.uri().name(), path.asString()); + EXPECT_EQUAL(object.uri(), URI("fam", fam::test_endpoint, path.asString())); + EXPECT_EQUAL(object.asString(), "fam://" + fam::test_endpoint + path.asString()); + EXPECT_EQUAL(object.path(), path); + + EXPECT_THROWS_AS(object.lookup(), NotFound); + + EXPECT_NOT(object.exists()); + + EXPECT_THROWS_AS(object.allocate(1025), OutOfStorage); + + EXPECT_NO_THROW(object.allocate(512)); + + EXPECT(object.exists()); + + EXPECT_NO_THROW(object.lookup().deallocate()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamObject: lookup, create, and destroy") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + const auto region_size = 64; + const auto region_perm = static_cast(0640); + + const auto object_name = fam::TestFam::makeRandomText("OBJECT"); + const auto object_size = 24; + + const auto path = '/' + region_name + '/' + object_name; + + { + auto name = FamRegionName(fam::test_endpoint, path); + + EXPECT_NO_THROW(name.create(region_size, region_perm)); + + // object inherits permissions from region + EXPECT_NO_THROW(FamObjectName(fam::test_endpoint, path).allocate(object_size)); + + auto object = name.object(object_name).lookup(); + + const FamProperty prop{object_size, region_perm, object_name}; + EXPECT_EQUAL(object.property(), prop); + EXPECT_NO_THROW(name.lookup().deallocateObject(object_name)); + } + + // empty region name + auto name = FamRegionName(fam::test_endpoint, ""); + // set region name and lookup + auto region = name.withRegion(region_name).lookup(); + + EXPECT_THROWS_AS(region.lookupObject(object_name), NotFound); + + /// @note object permissions are broken in OpenFAM API + region.setObjectLevelPermissions(); + const auto size = 12; + // const auto objectPerm = static_cast(0400); + EXPECT_NO_THROW(region.allocateObject(size, object_name)); + EXPECT_NO_THROW(region.lookupObject(object_name)); + + { + auto object = region.lookupObject(object_name); + EXPECT_EQUAL(object.size(), size); + EXPECT_EQUAL(object.permissions(), region_perm); + EXPECT_EQUAL(object.name(), object_name); + } + + // overwrite: allocate with different size + EXPECT_NO_THROW(region.allocateObject(object_size, object_name, true)); + + { + auto object = region.lookupObject(object_name); + const FamProperty prop{object_size, region_perm, object_name}; + EXPECT_EQUAL(object.property(), prop); + + EXPECT_NO_THROW(object.deallocate()); + } + + EXPECT_THROWS_AS(region.lookupObject(object_name), NotFound); + + EXPECT_NO_THROW(region.destroy()); + + EXPECT_THROWS_AS(name.lookup(), NotFound); +} + +CASE("FamObject: large data small object") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + const auto region_size = 64; + const auto region_perm = static_cast(0640); + + const auto object_name = fam::TestFam::makeRandomText("OBJECT"); + const auto object_size = 32; + + const FamPath path{region_name, object_name}; + + { + auto region = FamRegionName(fam::test_endpoint, path).create(region_size, region_perm, true); + + // object bigger than region + EXPECT_THROWS_AS(region.allocateObject(region_size + 1, object_name), OutOfStorage); + EXPECT_THROWS_AS(region.lookupObject(object_name), NotFound); + + EXPECT(region_size >= object_size); + + // object fits + EXPECT_NO_THROW(region.allocateObject(object_size, object_name)); + EXPECT_NO_THROW(region.lookupObject(object_name)); + EXPECT_NO_THROW(region.deallocateObject(object_name)); + EXPECT_THROWS_AS(region.lookupObject(object_name), NotFound); + } + + // data ops + + const auto test_data = "ECKIT_TEST_FAM_DATA_2048413561EC"s; // size=32 + + { // write + auto object = FamObjectName(fam::test_endpoint, path).allocate(object_size, true); + EXPECT_NO_THROW(object.put(test_data.data(), 0, test_data.size())); + } + + { // read + auto object = FamObjectName(fam::test_endpoint, path).lookup(); + + Buffer test_buffer(object.size()); + test_buffer.zero(); + + EXPECT_NO_THROW(object.get(test_buffer.data(), 0, test_buffer.size())); + + EXPECT(test_data == test_buffer.view()); + } + + { // cleanup + auto region = FamRegionName(fam::test_endpoint, path); + + EXPECT_NO_THROW(region.lookup().destroy()); + + EXPECT_THROWS_AS(region.lookup(), NotFound); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamObjectName: withObject replaces the object name") { + FamObjectName name(fam::test_endpoint, FamPath{"region", "original"}); + EXPECT_EQUAL(static_cast(name).path().objectName(), "original"); + + name.withObject("replaced"); + EXPECT_EQUAL(static_cast(name).path().objectName(), "replaced"); +} + +CASE("FamObjectName: withUUID replaces objectName with UUID") { + FamObjectName name(fam::test_endpoint, FamPath{"region", "placeholder"}); + name.withUUID(); + + // UUID format: 8-4-4-4-12 hex chars + const auto& obj = static_cast(name).path().objectName(); + EXPECT_EQUAL(obj.size(), 36); + EXPECT_EQUAL(obj[8], '-'); + EXPECT_EQUAL(obj[13], '-'); + EXPECT_EQUAL(obj[18], '-'); + EXPECT_EQUAL(obj[23], '-'); +} + +CASE("FamObjectName: exists returns false for non-existent object") { + FamObjectName name(fam::test_endpoint, FamPath{"nonExistentRegion", "nonExistentObject"}); + EXPECT_NOT(name.exists()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamObject: print and operator<<") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + const auto object_name = fam::TestFam::makeRandomText("OBJECT"); + + FamRegionName rname(fam::test_endpoint, region_name); + auto region = rname.create(1024, 0640); + + auto object = region.allocateObject(64, object_name); + + std::ostringstream oss; + oss << object; + EXPECT(oss.str().find("FamObject") != std::string::npos); + + object.deallocate(); + region.destroy(); +} + +CASE("FamObject: data() retrieves full contents") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + const auto object_name = fam::TestFam::makeRandomText("OBJECT"); + + FamRegionName rname(fam::test_endpoint, region_name); + auto region = rname.create(1024, 0640); + + const std::string test_str = "FamObject::data() test!"; + auto object = region.allocateObject(test_str.size(), object_name); + object.put(test_str.data(), 0, test_str.size()); + + auto buf = object.data(); + EXPECT_EQUAL(buf.size(), test_str.size()); + EXPECT(buf.view() == test_str); + + object.deallocate(); + region.destroy(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test + +int main(int argc, char** argv) { + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_path.cc b/tests/io/fam/test_fam_path.cc new file mode 100644 index 000000000..13cc54135 --- /dev/null +++ b/tests/io/fam/test_fam_path.cc @@ -0,0 +1,121 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_path.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include "test_fam_common.h" + +#include + +#include "eckit/exception/Exceptions.h" +#include "eckit/filesystem/URI.h" +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamPath.h" +#include "eckit/serialisation/ResizableMemoryStream.h" +#include "eckit/testing/Test.h" + +using namespace eckit; + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamPath: ctor and uuid generation") { + { + // uuid of "/region/object" + constexpr const auto* const uuid = "7b07021d-f3ce-5717-8124-c78b5613fe79"; + + const FamPath path{"region", "object"}; + EXPECT_EQUAL(path.generateUUID(), uuid); + + EXPECT_EQUAL(FamPath("/region/object").generateUUID(), uuid); + } + + { // assert uri.scheme + const auto uri = URI("/regionName/objectName"); + EXPECT_THROWS_AS(FamPath{uri}, eckit::Exception); + } + + { + const auto uri = URI("fam://" + fam::test_endpoint + "/regionName/objectName"); + EXPECT_NO_THROW(FamPath{uri}); + } + + { + const auto uri = URI("fam", fam::test_endpoint, "/regionName/objectName"); + EXPECT_EQUAL(uri.scheme(), eckit::fam::scheme); + EXPECT_EQUAL(uri.hostport(), fam::test_endpoint); + EXPECT_EQUAL(uri.name(), "/regionName/objectName"); + EXPECT_NO_THROW(const auto path = FamPath(uri)); + } + + { + const auto uri = URI("fam://" + fam::test_endpoint + "/regionName/objectName"); + + EXPECT_EQUAL(uri.scheme(), eckit::fam::scheme); + EXPECT_EQUAL(uri.hostport(), fam::test_endpoint); + EXPECT_EQUAL(uri.name(), "/regionName/objectName"); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamPath: stream round-trip") { + const FamPath original{"myRegion", "myObject"}; + + // Serialize + Buffer buffer(1024); + { + ResizableMemoryStream stream(buffer); + stream << original; + } + + // Deserialize + { + ResizableMemoryStream stream(buffer); + FamPath decoded(stream); + EXPECT(decoded == original); + EXPECT_EQUAL(decoded.regionName(), "myRegion"); + EXPECT_EQUAL(decoded.objectName(), "myObject"); + } +} + +CASE("FamPath: from char* and from string give same result") { + const FamPath from_string(std::string("/region/object")); + const FamPath from_cstr("/region/object"); + EXPECT(from_string == from_cstr); + EXPECT_EQUAL(from_string.regionName(), "region"); + EXPECT_EQUAL(from_string.objectName(), "object"); +} + +CASE("FamPath: invalid path with too many segments throws") { + EXPECT_THROWS(FamPath("/a/b/c")); +} + +CASE("FamPath: single segment path has empty objectName") { + const FamPath path("/regionOnly"); + EXPECT_EQUAL(path.regionName(), "regionOnly"); + EXPECT(path.objectName().empty()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test + +int main(int argc, char** argv) { + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_property.cc b/tests/io/fam/test_fam_property.cc new file mode 100644 index 000000000..b780c76dd --- /dev/null +++ b/tests/io/fam/test_fam_property.cc @@ -0,0 +1,93 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_property.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include + +#include "eckit/io/fam/FamProperty.h" +#include "eckit/testing/Test.h" + +using namespace eckit; + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamProperty: construction and perm conversion") { + // default construction + { + const FamProperty prop; + EXPECT_EQUAL(prop.size, 0U); + EXPECT_EQUAL(prop.perm, FamProperty::default_perm); + EXPECT(prop.name.empty()); + } + + // size + perm + { + const FamProperty prop{1024, 0755}; + EXPECT_EQUAL(prop.size, 1024U); + EXPECT_EQUAL(prop.perm, 0755); + } + + // size + string perm (exercises stringToPerm) + { + const FamProperty prop{2048, "0640"}; + EXPECT_EQUAL(prop.perm, 0640); + } + + // string perm with setuid bits (exercises the former buffer-overflow case) + { + const FamProperty prop{4096, "4755"}; + EXPECT_EQUAL(prop.perm, 04755); + } + + // equality + { + const FamProperty a{512, 0644, "test", 1000, 1000}; + const FamProperty b{512, 0644, "test", 1000, 1000}; + EXPECT(a == b); + + const FamProperty c{512, 0600, "test", 1000, 1000}; + EXPECT(!(a == c)); + } + + // print output contains octal perm + { + const FamProperty prop{1024, 0755, "myobj"}; + std::ostringstream oss; + oss << prop; + const auto str = oss.str(); + EXPECT(str.find("755") != std::string::npos); + EXPECT(str.find("myobj") != std::string::npos); + } + + // invalid perm strings must throw + { + EXPECT_THROWS(FamProperty(1024, "")); // empty + EXPECT_THROWS(FamProperty(1024, "0644abc")); // trailing garbage + EXPECT_THROWS(FamProperty(1024, "99999")); // out of range (> 07777) + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test + +int main(int argc, char** argv) { + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_region.cc b/tests/io/fam/test_fam_region.cc new file mode 100644 index 000000000..cadecaea7 --- /dev/null +++ b/tests/io/fam/test_fam_region.cc @@ -0,0 +1,258 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_region.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include "test_fam_common.h" + +#include +#include +#include + +#include "eckit/exception/Exceptions.h" +#include "eckit/filesystem/URI.h" +#include "eckit/io/Buffer.h" +#include "eckit/io/fam/FamObject.h" +#include "eckit/io/fam/FamProperty.h" +#include "eckit/io/fam/FamRegion.h" +#include "eckit/io/fam/FamRegionName.h" +#include "eckit/io/fam/FamSession.h" +#include "eckit/io/fam/FamSessionManager.h" +#include "eckit/runtime/Main.h" +#include "eckit/testing/Test.h" + +using namespace eckit; + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamRegionName: ctor, lookup, and allocate") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + + const FamRegionName region(fam::test_endpoint, region_name); + + EXPECT_EQUAL(region.uri().scheme(), eckit::fam::scheme); + EXPECT_EQUAL(region.uri().hostport(), fam::test_endpoint); + EXPECT_EQUAL(region.uri().name(), '/' + region_name); + EXPECT_EQUAL(region.uri(), URI("fam://" + fam::test_endpoint + '/' + region_name)); + EXPECT_EQUAL(region.asString(), "fam://" + fam::test_endpoint + '/' + region_name); + EXPECT_EQUAL(region.path().regionName(), region_name); + + EXPECT_THROWS_AS(region.lookup(), NotFound); + + EXPECT_NOT(region.exists()); + + EXPECT_NO_THROW(region.create(1024, 0640)); + + EXPECT(region.exists()); + + EXPECT_NO_THROW(region.lookup()); + + { + auto name = FamRegionName(fam::test_endpoint, ""); + EXPECT_NO_THROW(name.withRegion(region_name).lookup().destroy()); + } +} + +CASE("FamRegionName: invalid names are rejected") { + // empty name + EXPECT_THROWS(FamRegionName(fam::test_endpoint, "").create(1024, 0640)); + + // name with spaces + EXPECT_THROWS(FamRegionName(fam::test_endpoint, "has space").create(1024, 0640)); + + // name with tab + EXPECT_THROWS(FamRegionName(fam::test_endpoint, "has\ttab").create(1024, 0640)); + + // name with non-printable control character + EXPECT_THROWS(FamRegionName(fam::test_endpoint, std::string("ctrl\x01char")).create(1024, 0640)); + + // name with high UTF-8 byte (previously caused UB with signed char) + EXPECT_THROWS(FamRegionName(fam::test_endpoint, std::string("caf\xC3\xA9")).create(1024, 0640)); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamRegion: lookup, create, validate properties, and destroy") { + + const auto region_name = fam::TestFam::makeRandomText("REGION"); + const auto region_size = 1024; + const auto region_perm = static_cast(0640); + + const FamRegionName name{fam::test_endpoint, region_name}; + { + EXPECT_THROWS_AS(name.lookup(), NotFound); + EXPECT_NO_THROW(name.create(region_size, region_perm)); + EXPECT_NO_THROW(name.lookup()); + } + + auto region = name.lookup(); + + EXPECT_EQUAL(region.size(), region_size); + EXPECT_EQUAL(region.permissions(), region_perm); + EXPECT_EQUAL(region.name(), region_name); + + const FamProperty prop{region_size, region_perm, region_name}; + EXPECT_EQUAL(region.property(), prop); + + EXPECT_NO_THROW(region.destroy()); + + EXPECT_THROWS_AS(FamRegionName(fam::test_endpoint, region_name).lookup(), NotFound); +} + +CASE("FamRegion: print produces meaningful output") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + FamRegionName name{fam::test_endpoint, region_name}; + + name.create(1024, 0640); + auto region = name.lookup(); + + std::ostringstream oss; + oss << region; + const auto str = oss.str(); + EXPECT(str.find("FamRegion") != std::string::npos); + EXPECT(str.find(region_name) != std::string::npos); + + region.destroy(); +} + +CASE("FamRegion: proxyObject creates a valid proxy") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + const auto object_name = fam::TestFam::makeRandomText("OBJECT"); + FamRegionName name{fam::test_endpoint, region_name}; + + name.create(1024, 0640); + auto region = name.lookup(); + + const std::string data = "proxy_test_data"; + auto object = region.allocateObject(data.size(), object_name); + object.put(data.data(), 0, data.size()); + const auto offset = object.offset(); + + // proxyObject wraps an existing object by {regionId, offset} + auto proxy = region.proxyObject(offset); + + // proxy doesn't carry metadata, but can perform data ops + Buffer buf(data.size()); + buf.zero(); + proxy.get(buf.data(), 0, data.size()); + EXPECT(buf.view() == data); + + object.deallocate(); + region.destroy(); +} + +CASE("FamRegion: set and query permission level") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + FamRegionName name{fam::test_endpoint, region_name}; + + name.create(1024, 0640); + auto region = name.lookup(); + + // default is REGION-level + EXPECT_NO_THROW(region.setRegionLevelPermissions()); + + // switch to OBJECT-level + EXPECT_NO_THROW(region.setObjectLevelPermissions()); + + // Switch back to region-level + EXPECT_NO_THROW(region.setRegionLevelPermissions()); + + region.destroy(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamSession: destroyRegion by name") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + FamRegionName rname{fam::test_endpoint, region_name}; + + rname.create(1024, 0640); + EXPECT(rname.exists()); + + // destroyRegion(name) looks up and destroys internally + auto session = FamSessionManager::instance().session(fam::test_endpoint); + EXPECT_NO_THROW(session->destroyRegion(region_name)); + + EXPECT_NOT(rname.exists()); +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamRegion: idempotent creation from 4 processes") { + auto region_name = fam::TestFam::makeRandomText("REGION"); + constexpr eckit::fam::size_t region_size = 1024 * 1024; + + bool ok = fork_and_exec(4, { + "--fn=idempotent_create", + "--region=" + region_name, + "--region-size=" + std::to_string(region_size), + }); + + EXPECT(ok); + + auto name = FamRegionName(fam::test_endpoint, "").withRegion(region_name); + auto region = name.lookup(); + EXPECT(region.size() > 0); +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test + +namespace { + +int child_worker_main(int argc, char** argv) { + auto args = eckit::testing::parse_worker_args(argc, argv); + auto fn = eckit::testing::get_worker_arg(args, "fn"); + if (fn == "idempotent_create") { + auto region_name = eckit::testing::get_worker_arg(args, "region"); + auto region_size = static_cast(std::stol(eckit::testing::get_worker_arg(args, "region-size"))); + auto name = eckit::FamRegionName(eckit::test::fam::test_endpoint, "").withRegion(region_name); + try { + name.create(region_size, 0640); + } + catch (const eckit::AlreadyExists&) { + // Region created by another child — retry lookup (CIS may not have + // committed metadata yet when we race). + for (int attempt = 0; attempt < 20; ++attempt) { + try { + name.lookup(); + return 0; + } + catch (const eckit::NotFound&) { + ::usleep(50000); // 50 ms + } + } + return 1; // lookup never succeeded + } + return 0; + } + return 1; +} + +} // namespace + +int main(int argc, char** argv) { + auto args = eckit::testing::parse_worker_args(argc, argv); + if (!args.empty()) { + eckit::Main::initialise(argc, argv); + return child_worker_main(argc, argv); + } + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_session_manager.cc b/tests/io/fam/test_fam_session_manager.cc new file mode 100644 index 000000000..37df1d155 --- /dev/null +++ b/tests/io/fam/test_fam_session_manager.cc @@ -0,0 +1,108 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_session_manager.cc +/// @author Metin Cakircali +/// @date Mar 2026 + +#include "test_fam_common.h" + +#include + +#include "eckit/io/fam/FamSession.h" +#include "eckit/io/fam/FamSessionManager.h" +#include "eckit/testing/Test.h" + +namespace eckit { + +//---------------------------------------------------------------------------------------------------------------------- + +class FamSessionManager::TestAccessor { +public: + + static void set_last_access(FamSessionManager& manager, const std::string& name, + const std::chrono::system_clock::time_point& when) { + std::lock_guard lock(manager.mutex_); + if (auto session = manager.find(name)) { + session->updateLastAccess(when); + } + } + + static std::chrono::system_clock::time_point get_last_access(FamSessionManager& manager, const std::string& name) { + std::lock_guard lock(manager.mutex_); + if (auto session = manager.find(name)) { + return session->lastAccess(); + } + return {}; + } + + static void insert_null_entry(FamSessionManager& manager) { + std::lock_guard lock(manager.mutex_); + manager.sessions_.push_back(nullptr); + } + + static size_t size(FamSessionManager& manager) { + std::lock_guard lock(manager.mutex_); + return manager.sessions_.size(); + } + + static FamSessionManager::Session find_session(FamSessionManager& manager, const std::string& name) { + std::lock_guard lock(manager.mutex_); + return manager.find(name); + } +}; + +} // namespace eckit + +//---------------------------------------------------------------------------------------------------------------------- + +namespace eckit::test { + +CASE("FamSessionManager: cleanup and access time update") { + auto& manager = FamSessionManager::instance(); + + const std::string name{"FamSession:" + fam::test_endpoint}; + + const auto session = manager.session(fam::test_endpoint); + EXPECT(session); + + const auto expired = std::chrono::system_clock::now() - std::chrono::minutes(31); + FamSessionManager::TestAccessor::set_last_access(manager, name, expired); + + EXPECT(FamSessionManager::TestAccessor::find_session(manager, name)); + EXPECT_EQUAL(FamSessionManager::TestAccessor::size(manager), 1); + + FamSessionManager::TestAccessor::insert_null_entry(manager); + EXPECT_EQUAL(FamSessionManager::TestAccessor::size(manager), 2); + + const auto session2 = manager.session(fam::test_endpoint); + EXPECT(session2); + EXPECT_EQUAL(FamSessionManager::TestAccessor::size(manager), 1); + + const auto atime = std::chrono::system_clock::now() - std::chrono::minutes(1); + FamSessionManager::TestAccessor::set_last_access(manager, name, atime); + + const auto session3 = FamSessionManager::TestAccessor::find_session(manager, name); + EXPECT(session3); + EXPECT(session3->lastAccess() == atime); +} + +} // namespace eckit::test + +//---------------------------------------------------------------------------------------------------------------------- + +int main(int argc, char** argv) { + return eckit::testing::run_tests(argc, argv); +} diff --git a/tests/io/fam/test_fam_uri_manager.cc b/tests/io/fam/test_fam_uri_manager.cc new file mode 100644 index 000000000..45a8247c3 --- /dev/null +++ b/tests/io/fam/test_fam_uri_manager.cc @@ -0,0 +1,136 @@ +/* + * (C) Copyright 1996- ECMWF. + * + * This software is licensed under the terms of the Apache Licence Version 2.0 + * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. + * In applying this licence, ECMWF does not waive the privileges and immunities + * granted to it by virtue of its status as an intergovernmental organisation nor + * does it submit to any jurisdiction. + */ + +/* + * This software was developed as part of the Horizon Europe programme funded project OpenCUBE + * (Grant agreement: 101092984) horizon-opencube.eu + */ + +/// @file test_fam_uri_manager.cc +/// @author Metin Cakircali +/// @date May 2024 + +#include "test_fam_common.h" + +#include +#include + +#include "eckit/filesystem/URI.h" +#include "eckit/io/Buffer.h" +#include "eckit/io/DataHandle.h" +#include "eckit/io/fam/FamRegionName.h" +#include "eckit/testing/Test.h" + +using namespace eckit; + +namespace eckit::test { + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamURIManager: asString produces scheme:path and appends query/fragment") { + const std::string region = "myregion"; + const std::string object = "myobject"; + const std::string path = "/" + region + "/" + object; + + // FamURIManager::asString returns "scheme:" + uri.name(), where uri.name() + // holds only the path portion ("/region/object") because the authority + // (host:port) is parsed into the separate host_/port_ fields of URI. + // The full canonical form — including "//host:port" — is produced by + // FamName::asString(); see also the @todo in FamURIManager.cc. + const std::string expected_base = std::string(eckit::fam::scheme) + ":" + path; + + // Baseline: no query, no fragment. + { + const auto uri = URI(eckit::fam::scheme, fam::test_endpoint, path); + EXPECT_EQUAL(uri.asString(), expected_base); + } + + // With a single query parameter — must appear as "?key=value". + { + auto uri = URI(eckit::fam::scheme, fam::test_endpoint, path); + uri.query("offset", "42"); + EXPECT_EQUAL(uri.asString(), expected_base + "?offset=42"); + } + + // With a fragment — must appear as "#section". + { + auto uri = URI(eckit::fam::scheme, fam::test_endpoint, path); + uri.fragment("section"); + EXPECT_EQUAL(uri.asString(), expected_base + "#section"); + } + + // With both query and fragment — query precedes fragment. + { + auto uri = URI(eckit::fam::scheme, fam::test_endpoint, path); + uri.query("offset", "0"); + uri.fragment("end"); + EXPECT_EQUAL(uri.asString(), expected_base + "?offset=0#end"); + } + + // URI round-trip: URI constructed from path should parse back to same path + { + const auto uri = URI(eckit::fam::scheme, fam::test_endpoint, path); + const auto reparsed = URI(uri.asString()); + EXPECT_EQUAL(reparsed.scheme(), std::string(eckit::fam::scheme)); + EXPECT_EQUAL(reparsed.name(), path); + } +} + +//---------------------------------------------------------------------------------------------------------------------- + +CASE("FamURIManager: exists returns false for non-existent URI") { + const auto uri = URI("fam://" + fam::test_endpoint + "/noRegion/noObject"); + EXPECT_NOT(uri.exists()); +} + +CASE("FamURIManager: newWriteHandle and newReadHandle create valid handles") { + const auto region_name = fam::TestFam::makeRandomText("REGION"); + const auto object_name = fam::TestFam::makeRandomText("OBJECT"); + + FamRegionName rname(fam::test_endpoint, region_name); + rname.create(4096, 0640); + + const auto uri = URI("fam", fam::test_endpoint, "/" + region_name + "/" + object_name); + + const std::string data = "URI handle data"; + + // write via URI-created handle + { + std::unique_ptr handle(uri.newWriteHandle()); + handle->openForWrite(64); + handle->write(data.data(), static_cast(data.size())); + handle->close(); + } + + // URI should exist now + EXPECT(uri.exists()); + + // read via URI-created handle + { + std::unique_ptr handle(uri.newReadHandle()); + handle->openForRead(); + Buffer buf(64); + buf.zero(); + const auto bytes = handle->read(buf.data(), static_cast(data.size())); + EXPECT_EQUAL(bytes, static_cast(data.size())); + EXPECT(std::string(static_cast(buf.data()), data.size()) == data); + handle->close(); + } + + rname.lookup().destroy(); +} + +//---------------------------------------------------------------------------------------------------------------------- + +} // namespace eckit::test + +int main(int argc, char** argv) { + return eckit::testing::run_tests(argc, argv); +}