From b5cd536130417eb5572ae012df83cd7060a614f0 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Wed, 15 Jul 2026 18:23:10 -0700 Subject: [PATCH 1/8] Add more ignored paths Signed-off-by: Chen Wang --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a259b58a..53ece3a2 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,5 @@ scripts/logs tests/integration/dlio_benchmark/perf_analysis/.ipynb_checkpoints /CLAUDE.md +/deps +/source-me.sh From d9b0509fe3e9e88b9cea8a724a97da46542e32e0 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Thu, 16 Jul 2026 07:18:56 -0700 Subject: [PATCH 2/8] Add node-local cache eviction and origin-backed byte-range fetch Adds two related opt-in features for training-style workloads with a working set larger than any single node's local storage: - A pluggable cache-eviction subsystem (LRU/FIFO) for DYAD-managed directories, so node-local storage doesn't grow unbounded (DYAD_CACHE_CAPACITY/_POLICY/_LOW_WATERMARK/_GRACE_PERIOD). - dyad_consume_range(): fetches a byte range of a file directly into memory (no local file materialization), with an optional PFS/origin fallback (DYAD_PATH_ORIGIN) that lazily fills only the requested span of a managed file on first touch via a new on-disk range cache (range_cache.c) tracking which byte spans are already present. Implemented for the FLUX_RPC and Margo DTL backends. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 2 + docs/_fragments/design_cache.rst | 96 +++++++ docs/design.rst | 2 + docs/runtime_configuration.rst | 13 +- include/dyad/client/dyad_client.h | 53 ++++ include/dyad/common/dyad_cache.h | 90 +++++++ include/dyad/common/dyad_dtl.h | 10 + include/dyad/common/dyad_envs.h | 52 ++++ include/dyad/common/dyad_rc.h | 3 + include/dyad/core/dyad_ctx.h | 68 ++++- include/dyad/stream/dyad_params.hpp | 27 +- pydyad/pydyad/bindings.py | 81 ++++++ pydyad/setup.cfg | 3 + src/dyad/CMakeLists.txt | 1 + src/dyad/cache/CMakeLists.txt | 43 ++++ src/dyad/cache/dyad_cache.c | 212 ++++++++++++++++ src/dyad/cache/dyad_cache_api.c | 64 +++++ src/dyad/cache/dyad_cache_api.h | 128 ++++++++++ src/dyad/cache/dyad_cache_int.h | 115 +++++++++ src/dyad/cache/fifo_cache.c | 52 ++++ src/dyad/cache/fifo_cache.h | 53 ++++ src/dyad/cache/lru_cache.c | 50 ++++ src/dyad/cache/lru_cache.h | 53 ++++ src/dyad/client/CMakeLists.txt | 3 +- src/dyad/client/dyad_client.c | 252 +++++++++++++++++++ src/dyad/client/dyad_client_int.h | 6 + src/dyad/common/dyad_structures_int.h | 8 + src/dyad/core/CMakeLists.txt | 2 +- src/dyad/core/dyad_ctx.c | 153 ++++++++++- src/dyad/dtl/dyad_dtl_api.h | 43 ++++ src/dyad/dtl/flux_dtl.c | 65 +++++ src/dyad/dtl/flux_dtl.h | 51 ++++ src/dyad/dtl/margo_dtl.c | 150 ++++++++++- src/dyad/dtl/margo_dtl.h | 59 +++++ src/dyad/dtl/ucx_dtl.c | 7 + src/dyad/service/dyad_exe.c | 60 +++-- src/dyad/service/flux_module/dyad.c | 279 +++++++++++++++++++- src/dyad/stream/CMakeLists.txt | 3 +- src/dyad/stream/dyad_stream_core.cpp | 7 +- src/dyad/utils/CMakeLists.txt | 6 +- src/dyad/utils/range_cache.c | 307 +++++++++++++++++++++++ src/dyad/utils/range_cache.h | 113 +++++++++ src/dyad/utils/utils.c | 38 +++ src/dyad/utils/utils.h | 27 ++ tests/pydyad_spsc_lazy_range/consumer.py | 89 +++++++ tests/pydyad_spsc_lazy_range/producer.py | 52 ++++ tests/pydyad_spsc_lazy_range/run.sh | 122 +++++++++ tests/pydyad_spsc_range/consumer.py | 56 +++++ tests/pydyad_spsc_range/producer.py | 31 +++ tests/pydyad_spsc_range/run.sh | 93 +++++++ tests/unit/CMakeLists.txt | 3 +- tests/unit/cache/CMakeLists.txt | 1 + tests/unit/cache/cache_functions.cpp | 235 +++++++++++++++++ tests/unit/unit_test.cpp | 1 + 54 files changed, 3534 insertions(+), 59 deletions(-) create mode 100644 docs/_fragments/design_cache.rst create mode 100644 include/dyad/common/dyad_cache.h create mode 100644 src/dyad/cache/CMakeLists.txt create mode 100644 src/dyad/cache/dyad_cache.c create mode 100644 src/dyad/cache/dyad_cache_api.c create mode 100644 src/dyad/cache/dyad_cache_api.h create mode 100644 src/dyad/cache/dyad_cache_int.h create mode 100644 src/dyad/cache/fifo_cache.c create mode 100644 src/dyad/cache/fifo_cache.h create mode 100644 src/dyad/cache/lru_cache.c create mode 100644 src/dyad/cache/lru_cache.h create mode 100644 src/dyad/utils/range_cache.c create mode 100644 src/dyad/utils/range_cache.h create mode 100644 tests/pydyad_spsc_lazy_range/consumer.py create mode 100644 tests/pydyad_spsc_lazy_range/producer.py create mode 100755 tests/pydyad_spsc_lazy_range/run.sh create mode 100644 tests/pydyad_spsc_range/consumer.py create mode 100644 tests/pydyad_spsc_range/producer.py create mode 100644 tests/pydyad_spsc_range/run.sh create mode 100644 tests/unit/cache/CMakeLists.txt create mode 100644 tests/unit/cache/cache_functions.cpp diff --git a/.gitignore b/.gitignore index 53ece3a2..07777df8 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,8 @@ docs/demos/SCA26/cpp_prod tests/integration/dlio_benchmark/logs scripts/checkpoints scripts/logs +tests/pydyad_spsc_range/logs +tests/pydyad_spsc_lazy_range/logs tests/integration/dlio_benchmark/perf_analysis/.ipynb_checkpoints /CLAUDE.md diff --git a/docs/_fragments/design_cache.rst b/docs/_fragments/design_cache.rst new file mode 100644 index 00000000..bcd4fd5a --- /dev/null +++ b/docs/_fragments/design_cache.rst @@ -0,0 +1,96 @@ +.. _dyad_cache_eviction: + +Node-Local Cache Eviction +######################### + +By default, DYAD's producer- and consumer-managed directories (DMDs) grow +without bound: every file produced or consumed accumulates on node-local +storage forever. For workloads that repeatedly touch a shifting working set +larger than any single node's capacity — for example the shuffled-sample-file +training pattern described above, where a `DistributedSampler` shuffles +across all shard files every epoch — this means a node's DMD eventually +approaches the size of the entire dataset rather than just the fraction it +actually needs at any one time. + +DYAD addresses this with an optional, pluggable cache-eviction subsystem, +disabled by default (``DYAD_CACHE_CAPACITY`` unset or ``0``), so existing +deployments see no change in behavior unless they opt in. + +Pluggable policy architecture +============================== + +Cache eviction follows the same struct-of-function-pointers pattern as the +:ref:`Data Transport Layer `: a policy handle selected by a mode +enum (:code:`DYAD_CACHE_NONE`, :code:`DYAD_CACHE_LRU`, :code:`DYAD_CACHE_FIFO`), +with the active policy's implementation reached through a single function +pointer, ``get_recency_key()``, rather than DTL's larger transport-specific +interface. Unlike the UCX/Margo DTL backends, LRU and FIFO have no external +dependencies, so both are always compiled in — no CMake feature flag is +needed to enable them. + +Recency tracking without a persisted index +============================================= + +Rather than maintaining a separate database of access times (which would +itself need cross-process synchronization to stay consistent), each policy +derives its ranking directly from filesystem metadata already maintained by +the kernel: + +- **LRU** ranks by ``st_atime`` — updated on every read, with no DYAD code + changes needed on the hot read path. +- **FIFO** ranks by ``st_mtime`` — files under a DYAD-managed directory are + effectively write-once, so modification time approximates insertion order. + +This works correctly across independent processes sharing one managed +directory (for example, PyTorch ``DataLoader`` workers with ``num_workers > +0``, which run as separate OS processes, each with its own DYAD context) with +no shared memory or IPC: ``stat()`` is a kernel-level, cross-process-consistent +syscall, so every process observes the same recency signal for a given file. + +Locking and eviction procedure +================================= + +The only cross-process hazard is the *scan-and-evict decision* itself — two +processes could otherwise race to decide a file needs evicting and both act +on it. This is serialized with a dedicated per-directory advisory lock file +(``.dyad_cache.lock``), reusing the same ``dyad_excl_flock()``/ +``dyad_release_flock()`` primitives already described in +:ref:`Local file access coordination via locking ` for +per-file coordination — just applied to a directory-level sentinel instead of +a data file. + +``dyad_produce()`` and ``dyad_consume()``/``dyad_consume_w_metadata()`` each +call ``dyad_cache_maybe_evict()`` once they otherwise complete successfully. +That function: + +1. Returns immediately if eviction is disabled (``DYAD_CACHE_CAPACITY`` is + ``0``, the default) — a single branch, no filesystem access at all. +2. Scans the managed directory's current on-disk usage. If already at or + under capacity, returns without taking any lock. +3. Otherwise acquires the directory lock, re-checks usage (another process + may have already evicted enough while this one waited), and if still over + capacity, ranks every candidate file by the active policy's recency key. +4. Walks candidates oldest-first, skipping any file accessed within + ``DYAD_CACHE_GRACE_PERIOD`` seconds or currently locked by another + in-flight ``dyad_produce()``/``dyad_consume()`` call (checked via a + non-blocking ``dyad_try_excl_flock()``, so the evictor never blocks + waiting on a file that's mid-transfer), removing files until usage drops + to ``DYAD_CACHE_LOW_WATERMARK`` (a fraction of capacity, default 0.8, so a + single eviction pass doesn't need to run again on the very next call). +5. Releases the directory lock. + +Eviction failures on individual files are logged and skipped; this mechanism +never turns an otherwise-successful ``dyad_produce()``/``dyad_consume()`` +call into a reported error. + +Known limitation: stale metadata on producer-side eviction +=============================================================== + +Evicting a file from the local DMD does not retract its published Flux KVS +metadata. :ref:`Case 2 ` of the Hierarchical File +Locator already tolerates a *consumer-side* cache miss by falling back to the +global HFL hierarchy — but if the *producer's* origin copy is evicted, a +later consumer resolving that file's metadata could still be pointed at a +producer that no longer has it locally, causing the subsequent fetch to fail. +Addressing this — for example with a metadata retraction/unpublish primitive, +or a staleness check at fetch time — is left as future work. diff --git a/docs/design.rst b/docs/design.rst index 6a62ebf1..e28fcb16 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -56,5 +56,7 @@ representing PyTorch dataloader :ref:`Devarajan et al. 2024 .. include:: _fragments/design_local_access.rst +.. include:: _fragments/design_cache.rst + .. include:: _fragments/design_init.rst diff --git a/docs/runtime_configuration.rst b/docs/runtime_configuration.rst index 9b861222..a980f112 100644 --- a/docs/runtime_configuration.rst +++ b/docs/runtime_configuration.rst @@ -43,7 +43,18 @@ variables is shown below. | | | | | | | | | | | hierarchical KVS within DYAD's namespace. | +------------------------------------+-----------------+--------------+----------+-----------------------------------------------------------------+ - +| :code:`DYAD_CACHE_CAPACITY` | Integer (bytes) | No | 0 | Maximum bytes DYAD may use in a managed directory before | +| | | | | evicting files. 0 (default) disables eviction entirely. | ++------------------------------------+-----------------+--------------+----------+-----------------------------------------------------------------+ +| :code:`DYAD_CACHE_POLICY` | String | No | LRU | Eviction policy: :code:`LRU`, :code:`FIFO`, or :code:`NONE`. | +| | | | | Ignored if :code:`DYAD_CACHE_CAPACITY` is 0. | ++------------------------------------+-----------------+--------------+----------+-----------------------------------------------------------------+ +| :code:`DYAD_CACHE_LOW_WATERMARK` | Float (0..1) | No | 0.8 | Fraction of :code:`DYAD_CACHE_CAPACITY` to evict down to once | +| | | | | eviction triggers. | ++------------------------------------+-----------------+--------------+----------+-----------------------------------------------------------------+ +| :code:`DYAD_CACHE_GRACE_PERIOD` | Integer (sec) | No | 5 | Skip eviction candidates accessed more recently than this many | +| | | | | seconds. | ++------------------------------------+-----------------+--------------+----------+-----------------------------------------------------------------+ .. [#two] For DYAD to do anything, at least one of :code:`DYAD_PATH_PRODUCER` or :code:`DYAD_PATH_CONSUMER` must be provided. Applications will still work if neither are provided, but DYAD will not do anything. diff --git a/include/dyad/client/dyad_client.h b/include/dyad/client/dyad_client.h index e89bd393..f9ffdfcb 100644 --- a/include/dyad/client/dyad_client.h +++ b/include/dyad/client/dyad_client.h @@ -261,6 +261,59 @@ DYAD_PFA_ANNOTATE DYAD_DLL_EXPORTED dyad_rc_t dyad_consume (dyad_ctx_t *ctx, con DYAD_PFA_ANNOTATE DYAD_DLL_EXPORTED dyad_rc_t dyad_consume_w_metadata (dyad_ctx_t *ctx, const char *fname, const dyad_metadata_t *mdata); +/** + * @brief Fetches a byte range of a file without materializing a local copy. + * + * @details + * Unlike @c dyad_consume()/@c dyad_consume_w_metadata(), which always + * materialize a complete local file (required for GOTCHA @c open() + * interception compatibility), this function returns the requested + * @p [offset, offset + length) range of @p fname directly to the caller as + * an in-memory buffer -- no local file is created or written. Intended for + * callers (e.g. the pydyad HDF/flat-cache binding) that only need raw bytes, + * not a file descriptor. + * + * The file must already have been published via @c dyad_produce() by some + * rank (its full contents staged there beforehand -- this function does not + * stage or copy files, only fetches a sub-range of one that's already fully + * present somewhere). Behavior: + * 1. Resolves @p fname to a path relative to the consumer-managed + * directory, same as @c dyad_consume(). + * 2. Calls the existing @c dyad_fetch_metadata() unchanged: if the file is + * already present on this node (KVS lookup returns @c NULL), reads + * @p [offset, length) directly from the local copy via @c pread() -- + * no RPC. Otherwise, issues a byte-range RPC to the owning rank + * returned in the metadata. + * 3. Only implemented for @c DYAD_DTL_FLUX_RPC and @c DYAD_DTL_MARGO -- + * returns @c DYAD_RC_BADDTLMODE immediately under @c DYAD_DTL_UCX. + * + * @param[in] ctx Pointer to the DYAD context. Must not be @c NULL. + * @param[in] fname Path to the file to fetch from. Must not be @c NULL. + * @param[in] offset Starting byte offset of the requested range. + * @param[in] length Number of bytes requested. + * @param[out] data Set to a newly allocated buffer containing the + * requested bytes. The caller must @c free() it. + * @param[out] data_len Set to the number of bytes actually returned in + * @p data (normally equal to @p length). + * + * @return @c dyad_rc_t return code indicating the outcome: + * @retval DYAD_RC_OK The requested range was retrieved successfully. + * @retval DYAD_RC_NOCTX The context @p ctx or its Flux handle is @c NULL. + * @retval DYAD_RC_BADMANAGEDPATH The consumer-managed path in the context is @c NULL. + * @retval DYAD_RC_BADDTLMODE The active DTL mode is @c DYAD_DTL_UCX, which + * does not implement byte-range fetch. + * @retval DYAD_RC_BADFIO A local @c pread() failed. + * @retval DYAD_RC_* Any error code propagated from + * @c dyad_fetch_metadata() or the internal + * RPC/DTL path. + */ +DYAD_PFA_ANNOTATE DYAD_DLL_EXPORTED dyad_rc_t dyad_consume_range (dyad_ctx_t *ctx, + const char *fname, + size_t offset, + size_t length, + void **data, + size_t *data_len); + #ifdef __cplusplus } #endif diff --git a/include/dyad/common/dyad_cache.h b/include/dyad/common/dyad_cache.h new file mode 100644 index 00000000..3e15a6fe --- /dev/null +++ b/include/dyad/common/dyad_cache.h @@ -0,0 +1,90 @@ +/** + * @file dyad_cache.h + * @brief Cache-eviction policy interface definitions for DYAD. + * + * @details + * Defines the enumeration describing the available node-local cache + * eviction policies for DYAD's producer/consumer managed directories + * (DMDs). The eviction subsystem abstracts the choice of *which* cached + * file to remove when a managed directory exceeds its configured + * capacity, so the rest of DYAD does not need to know which policy is + * active. This mirrors the Data Transport Layer (DTL) abstraction in + * @c dyad_dtl.h. + */ + +#ifndef DYAD_COMMON_DYAD_CACHE_H +#define DYAD_COMMON_DYAD_CACHE_H + +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Available cache-eviction policies for DYAD-managed directories. + * + * @details + * Selects how DYAD picks which cached file(s) to remove when a managed + * directory exceeds @c DYAD_CACHE_CAPACITY. Each policy ranks candidate + * files by a "recency key" derived from filesystem metadata (no + * separate persisted index is kept): + * + * - @c DYAD_CACHE_NONE disables eviction entirely (the default). DYAD's + * managed directories grow unboundedly, matching all prior behavior. + * - @c DYAD_CACHE_LRU evicts the least-recently-*accessed* file first, + * ranked by @c st_atime. + * - @c DYAD_CACHE_FIFO evicts the oldest file first, ranked by + * @c st_mtime (files under DYAD's managed directories are effectively + * write-once, so mtime approximates insertion order). + * - @c DYAD_CACHE_DEFAULT aliases @c DYAD_CACHE_LRU — the default + * *policy* once eviction is enabled (i.e. once + * @c DYAD_CACHE_CAPACITY is set to a nonzero value). + * - @c DYAD_CACHE_END is a sentinel marking the end of valid values. + * It is used to size @c dyad_cache_policy_name and for bounds checking. + */ +enum dyad_cache_policy_mode { + DYAD_CACHE_NONE = 0, ///< Eviction disabled (default). + DYAD_CACHE_LRU = 1, ///< Evict least-recently-accessed file first (by st_atime). + DYAD_CACHE_FIFO = 2, ///< Evict oldest file first (by st_mtime). + DYAD_CACHE_DEFAULT = 1, ///< Default policy once enabled (alias for DYAD_CACHE_LRU). + DYAD_CACHE_END = 3 ///< Sentinel — number of valid cache policy modes. +}; +typedef enum dyad_cache_policy_mode dyad_cache_policy_mode_t; + +/** + * @brief Human-readable names for each cache-eviction policy mode. + * + * @details + * Indexed by @c dyad_cache_policy_mode_t. The extra entry at index + * @c DYAD_CACHE_END holds @c "CACHE_UNKNOWN" for out-of-range values. + * Marked @c unused to suppress warnings when included in translation + * units that do not reference it directly. + */ +static const char* dyad_cache_policy_name[DYAD_CACHE_END + 1] + __attribute__ ((unused)) = {"NONE", "LRU", "FIFO", "CACHE_UNKNOWN"}; + +/** + * @brief Opaque cache-eviction policy handle. + * + * @details + * Forward declaration of the policy implementation struct. The full + * definition is provided by each policy (LRU, FIFO) in its own source + * file. Callers interact with the policy exclusively through the + * function pointer embedded in the struct, allowing the policy to be + * swapped at initialization time without changing call sites. + * + * @see dyad_cache_policy_mode_t for available policies. + * @see dyad_ctx_t for the context that owns the cache policy handle. + */ +struct dyad_cache_policy; + +#ifdef __cplusplus +} +#endif + +#endif /* DYAD_COMMON_DYAD_CACHE_H */ diff --git a/include/dyad/common/dyad_dtl.h b/include/dyad/common/dyad_dtl.h index 984a074c..909d5ccf 100644 --- a/include/dyad/common/dyad_dtl.h +++ b/include/dyad/common/dyad_dtl.h @@ -93,6 +93,16 @@ typedef enum dyad_dtl_comm_mode dyad_dtl_comm_mode_t; */ #define DYAD_DTL_RPC_NAME "dyad.fetch" +/** + * @brief Flux RPC topic name for DYAD byte-range fetch requests. + * + * @details + * Like @c DYAD_DTL_RPC_NAME, but for @c dyad_consume_range() requests + * (FLUX_RPC and MARGO DTL modes only). Registered as a separate topic so + * the existing whole-file @c DYAD_DTL_RPC_NAME handler is untouched. + */ +#define DYAD_DTL_RPC_RANGE_NAME "dyad.fetch_range" + /** * @brief Opaque DTL handle. * diff --git a/include/dyad/common/dyad_envs.h b/include/dyad/common/dyad_envs.h index 13eefc20..8be6cfbc 100644 --- a/include/dyad/common/dyad_envs.h +++ b/include/dyad/common/dyad_envs.h @@ -132,4 +132,56 @@ */ #define DYAD_MARGO_PROTO_ENV "DYAD_MARGO_PROTO" +/** + * @brief Maximum bytes DYAD may use in a managed directory before evicting + * cached files. + * + * @details + * @c 0 (the default) disables eviction entirely, preserving prior + * behavior for deployments that don't opt in. + */ +#define DYAD_CACHE_CAPACITY_ENV "DYAD_CACHE_CAPACITY" + +/** + * @brief Cache-eviction policy to use once @c DYAD_CACHE_CAPACITY is set. + * + * @details + * Valid values: @c LRU (default), @c FIFO, @c NONE. Ignored if + * @c DYAD_CACHE_CAPACITY is @c 0. + */ +#define DYAD_CACHE_POLICY_ENV "DYAD_CACHE_POLICY" + +/** + * @brief Fraction (0..1) of @c DYAD_CACHE_CAPACITY to evict down to once + * eviction triggers. + * + * @details + * Evicting to a low-water mark rather than the exact capacity limit + * avoids evicting on every single produce/consume call while usage + * hovers at the boundary. Default: @c 0.8. + */ +#define DYAD_CACHE_LOW_WATERMARK_ENV "DYAD_CACHE_LOW_WATERMARK" + +/** + * @brief Skip eviction candidates accessed more recently than this many + * seconds. + * + * @details + * Guards against evicting a file that may still be mid-access by another + * process. Default: @c 5. + */ +#define DYAD_CACHE_GRACE_PERIOD_ENV "DYAD_CACHE_GRACE_PERIOD" + +/** + * @brief Fallback source path (e.g. on the parallel file system) used by + * @c dyad_range_cache_ensure() to lazily fill missing spans of a + * DYAD-managed file on demand. + * + * @details + * Unset (the default) preserves prior behavior: managed files must already + * be fully present locally (e.g. pre-staged), and byte-range fetch never + * consults an origin. + */ +#define DYAD_PATH_ORIGIN_ENV "DYAD_PATH_ORIGIN" + #endif // DYAD_COMMON_DYAD_ENVS_H diff --git a/include/dyad/common/dyad_rc.h b/include/dyad/common/dyad_rc.h index 7a5b950b..11be1297 100644 --- a/include/dyad/common/dyad_rc.h +++ b/include/dyad/common/dyad_rc.h @@ -66,6 +66,9 @@ enum dyad_core_return_codes { DYAD_RC_BAD_CLI_ARG_DEF = -1008, ///< Trying to define a CLI argument failed DYAD_RC_BAD_CLI_PARSE = -1009, ///< Trying to parse CLI arguments failed DYAD_RC_BADBUF = -1010, ///< Invalid buffer/pointer passed to function + DYAD_RC_BUSY = -1011, ///< Resource (e.g. a file lock) is held by another + ///< process; not a hard failure, caller should skip/retry + DYAD_RC_BADCACHEMODE = -1012, ///< Invalid DYAD cache-eviction policy mode provided // FLUX DYAD_RC_FLUXFAIL = -2000, ///< Some Flux function failed diff --git a/include/dyad/core/dyad_ctx.h b/include/dyad/core/dyad_ctx.h index c59b2c68..65296a55 100644 --- a/include/dyad/core/dyad_ctx.h +++ b/include/dyad/core/dyad_ctx.h @@ -11,6 +11,12 @@ #include #include +#ifdef __cplusplus +#include +#else +#include +#endif + #ifdef __cplusplus extern "C" { #endif @@ -87,9 +93,10 @@ DYAD_DLL_EXPORTED void dyad_ctx_fini (void); * 3. Compute @c node_idx from @c rank / @c service_mux. * 4. Copy the KVS namespace string. * 5. Initialize the DTL via @c dyad_set_and_init_dtl_mode(). - * 6. Set the producer-managed path via @c dyad_set_prod_path(). - * 7. Set the consumer-managed path via @c dyad_set_cons_path(). - * 8. Redirect log output to per-process log files under @c logs/. + * 6. Initialize the cache-eviction policy via @c dyad_set_and_init_cache_policy(). + * 7. Set the producer-managed path via @c dyad_set_prod_path(). + * 8. Set the consumer-managed path via @c dyad_set_cons_path(). + * 9. Redirect log output to per-process log files under @c logs/. * * On any failure after allocation, @c dyad_clear() is called to release * partially initialized resources and the context is reset to @@ -123,6 +130,26 @@ DYAD_DLL_EXPORTED void dyad_ctx_fini (void); * @param[in] flux_handle Optional existing Flux handle. If * @c NULL, a new handle is opened via * @c flux_open(). + * @param[in] cache_capacity_bytes Maximum bytes DYAD may use in a + * managed directory before evicting + * cached files. @c 0 disables eviction. + * @param[in] cache_policy_str Name of the cache-eviction policy to + * use once @p cache_capacity_bytes is + * nonzero (@c "LRU", @c "FIFO", or + * @c "NONE"). Ignored if + * @p cache_capacity_bytes is @c 0. + * @param[in] cache_low_watermark Fraction (0..1) of + * @p cache_capacity_bytes to evict + * down to once eviction triggers. + * @param[in] cache_grace_period_sec Skip eviction candidates accessed + * more recently than this many seconds. + * @param[in] origin_path Optional fallback source path (e.g. + * on the parallel file system) used by + * @c dyad_range_cache_ensure() to + * lazily fill missing spans of a + * managed file on demand. @c NULL + * (default) disables lazy origin-backed + * caching, preserving prior behavior. * * @return @c dyad_rc_t return code indicating the outcome: * @retval DYAD_RC_OK Initialization succeeded, or was already @@ -156,7 +183,12 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_init (bool debug, bool relative_to_managed_path, const char *dtl_mode_str, const dyad_dtl_comm_mode_t dtl_comm_mode, - void *flux_handle); + void *flux_handle, + uint64_t cache_capacity_bytes, + const char *cache_policy_str, + double cache_low_watermark, + unsigned int cache_grace_period_sec, + const char *origin_path); /** * @brief Initializes DYAD by reading configuration from environment variables. @@ -181,6 +213,11 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_init (bool debug, * | @c DYAD_PATH_PRODUCER | @c NULL | Producer-managed directory path | * | @c DYAD_PATH_RELATIVE | @c false | Paths are relative to managed dirs | * | @c DYAD_DTL_MODE | @c DYAD_DTL_DEFAULT | Data transport layer mode | + * | @c DYAD_CACHE_CAPACITY | @c 0 (disabled) | Max bytes DYAD may use in a managed dir | + * | @c DYAD_CACHE_POLICY | @c LRU | Cache-eviction policy: LRU/FIFO/NONE | + * | @c DYAD_CACHE_LOW_WATERMARK | @c 0.8 | Fraction of capacity to evict down to | + * | @c DYAD_CACHE_GRACE_PERIOD | @c 5 | Skip candidates accessed within N seconds | + * | @c DYAD_PATH_ORIGIN | @c NULL (disabled) | Fallback path for lazy origin-backed range caching | * * If @c DYAD_DTL_MODE is not set, defaults to @c DYAD_DTL_DEFAULT and logs * a warning to @c stderr. @@ -225,6 +262,29 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_init_env (const dyad_dtl_comm_mode_t dtl_comm_m DYAD_DLL_EXPORTED dyad_rc_t dyad_set_and_init_dtl_mode (const char *dtl_mode_name, dyad_dtl_comm_mode_t dtl_comm_mode); +/** + * @brief Switches DYAD's cache-eviction policy to a new mode. + * + * @details + * Resolves @p cache_policy_name to a @c dyad_cache_policy_mode_t value, + * finalizes the current policy handle via @c dyad_cache_policy_finalize(), + * and reinitializes it via @c dyad_cache_policy_init(). Mirrors + * @c dyad_set_and_init_dtl_mode() for the cache-eviction subsystem. + * + * @param[in] cache_policy_name Name of the policy to switch to: @c "LRU", + * @c "FIFO", or @c "NONE". Must not be @c NULL. + * @param[in] cache_capacity_bytes Maximum bytes DYAD may use in a managed + * directory. @c 0 disables eviction + * regardless of @p cache_policy_name. + * + * @return @c dyad_rc_t return code indicating the outcome: + * @retval DYAD_RC_OK The policy was successfully switched and initialized. + * @retval DYAD_RC_BADCACHEMODE @p cache_policy_name is @c NULL or does not + * match any supported policy name. + */ +DYAD_DLL_EXPORTED dyad_rc_t dyad_set_and_init_cache_policy (const char *cache_policy_name, + uint64_t cache_capacity_bytes); + /** * @brief Sets the producer-managed directory path in the DYAD context. * diff --git a/include/dyad/stream/dyad_params.hpp b/include/dyad/stream/dyad_params.hpp index 25e8b2d0..a93b4893 100644 --- a/include/dyad/stream/dyad_params.hpp +++ b/include/dyad/stream/dyad_params.hpp @@ -17,7 +17,9 @@ #error "no config" #endif +#include #include +#include #include namespace dyad @@ -81,6 +83,24 @@ struct dyad_params { /// A relative path is relative to a managed path bool m_relative_to_managed_path; + /// Maximum bytes DYAD may use in a managed directory before evicting + /// cached files. 0 (default) disables eviction. + uint64_t m_cache_capacity_bytes; + /// Cache-eviction policy name ("LRU", "FIFO", or "NONE"). Ignored if + /// m_cache_capacity_bytes is 0. + std::string m_cache_policy; + /// Fraction (0..1) of m_cache_capacity_bytes to evict down to once + /// eviction triggers. + double m_cache_low_watermark; + /// Skip eviction candidates accessed more recently than this many seconds. + unsigned int m_cache_grace_period_sec; + + /// Optional fallback source path (e.g. on the parallel file system) used + /// by dyad_range_cache_ensure() to lazily fill missing spans of a + /// managed file on demand. Empty (default) disables lazy origin-backed + /// caching. + std::string m_origin_path; + /** * @brief Constructs a @c dyad_params object with safe default values. * @@ -104,7 +124,12 @@ struct dyad_params { m_kvs_namespace (""), m_cons_managed_path (""), m_prod_managed_path (""), - m_relative_to_managed_path (false) + m_relative_to_managed_path (false), + m_cache_capacity_bytes (0ull), + m_cache_policy (dyad_cache_policy_name[DYAD_CACHE_NONE]), + m_cache_low_watermark (0.8), + m_cache_grace_period_sec (5u), + m_origin_path ("") { } }; diff --git a/pydyad/pydyad/bindings.py b/pydyad/pydyad/bindings.py index 63378b0b..dff8879c 100644 --- a/pydyad/pydyad/bindings.py +++ b/pydyad/pydyad/bindings.py @@ -27,6 +27,10 @@ class DyadDTLHandle(ctypes.Structure): pass +class DyadCachePolicyHandle(ctypes.Structure): + pass + + class DyadCtxWrapper(ctypes.Structure): _fields_ = [ ("h", ctypes.POINTER(FluxHandle)), @@ -61,6 +65,12 @@ class DyadCtxWrapper(ctypes.Structure): ("prod_managed_path", ctypes.c_char_p), ("cons_managed_path", ctypes.c_char_p), ("relative_to_managed_path", ctypes.c_bool), + ("cache_policy", ctypes.POINTER(DyadCachePolicyHandle)), + ("cache_policy_mode", ctypes.c_int), + ("cache_capacity_bytes", ctypes.c_uint64), + ("cache_low_watermark_frac", ctypes.c_double), + ("cache_grace_period_sec", ctypes.c_uint), + ("origin_path", ctypes.c_char_p), ] @@ -109,6 +119,15 @@ class DTLCommMode(enum.IntEnum): DYAD_COMM_END = 3 +class CachePolicyMode(enum.Enum): + DYAD_CACHE_NONE = "NONE" + DYAD_CACHE_LRU = "LRU" + DYAD_CACHE_FIFO = "FIFO" + + def __str__(self): + return self.value + + class Dyad: def __init__(self): self.initialized = False @@ -169,6 +188,11 @@ def __init__(self): ctypes.c_char_p, # dtl_mode ctypes.c_int, # dtl_comm_mode ctypes.c_void_p, # flux_handle + ctypes.c_uint64, # cache_capacity_bytes + ctypes.c_char_p, # cache_policy + ctypes.c_double, # cache_low_watermark + ctypes.c_uint, # cache_grace_period_sec + ctypes.c_char_p, # origin_path ] self.dyad_init.restype = ctypes.c_int @@ -216,6 +240,17 @@ def __init__(self): ] self.dyad_consume_w_metadata.restype = ctypes.c_int + self.dyad_consume_range = self.dyad_client_lib.dyad_consume_range + self.dyad_consume_range.argtypes = [ + ctypes.POINTER(DyadCtxWrapper), + ctypes.c_char_p, + ctypes.c_size_t, # offset + ctypes.c_size_t, # length + ctypes.POINTER(ctypes.c_void_p), # data (out) + ctypes.POINTER(ctypes.c_size_t), # data_len (out) + ] + self.dyad_consume_range.restype = ctypes.c_int + self.dyad_finalize = self.dyad_ctx_lib.dyad_finalize self.dyad_finalize.argtypes = [] self.dyad_finalize.restype = ctypes.c_int @@ -242,6 +277,11 @@ def init( dtl_mode=None, dtl_comm_mode=DTLCommMode.DYAD_COMM_RECV, flux_handle=None, + cache_capacity_bytes=0, + cache_policy=None, + cache_low_watermark=0.8, + cache_grace_period_sec=5, + origin_path=None, ): self.log_inst = dftracer.initialize_log( logfile=None, data_dir=None, process_id=-1 @@ -269,6 +309,11 @@ def init( str(dtl_mode).encode() if dtl_mode is not None else None, ctypes.c_int(dtl_comm_mode), ctypes.c_void_p(flux_handle), + ctypes.c_uint64(cache_capacity_bytes), + str(cache_policy).encode() if cache_policy is not None else None, + ctypes.c_double(cache_low_watermark), + ctypes.c_uint(cache_grace_period_sec), + origin_path.encode() if origin_path is not None else None, ) self.ctx = self.dyad_ctx_get() @@ -400,6 +445,42 @@ def consume_w_metadata(self, fname, metadata_wrapper): if int(res) != 0: raise RuntimeError("Cannot consume data with metadata with DYAD!") + @dft_log.log + def consume_range(self, fname, offset, length): + """Fetch a byte range [offset, offset + length) of fname without + materializing a local copy of the whole file. Only supported under + DYAD_DTL_FLUX_RPC and DYAD_DTL_MARGO. Returns the requested bytes + directly as a Python `bytes` object. + """ + if self.dyad_consume_range is None: + warnings.warn( + "Trying to consume a byte range with DYAD when libdyad_client.so was not found", + RuntimeWarning, + ) + return None + data_ptr = ctypes.c_void_p() + data_len = ctypes.c_size_t() + res = self.dyad_consume_range( + self.ctx, + fname.encode(), + ctypes.c_size_t(offset), + ctypes.c_size_t(length), + ctypes.byref(data_ptr), + ctypes.byref(data_len), + ) + if int(res) != 0: + raise RuntimeError("Cannot consume byte range with DYAD!") + data = ctypes.string_at(data_ptr, data_len.value) + # dyad_consume_range() returns a plain malloc()-family buffer (either + # a direct malloc() for the local-pread path, or the DTL's + # get_buffer() allocation for the remote-RPC path -- both + # free()-compatible for FLUX_RPC/MARGO). ctypes.string_at() already + # copied the bytes into Python-owned memory above, so free the C + # buffer now via libc directly (DYAD has no dedicated free function + # for this, unlike dyad_free_metadata()). + ctypes.CDLL(None).free(data_ptr) + return data + @dft_log.log def finalize(self): if not self.initialized: diff --git a/pydyad/setup.cfg b/pydyad/setup.cfg index 6c77fd34..1e81765e 100644 --- a/pydyad/setup.cfg +++ b/pydyad/setup.cfg @@ -10,4 +10,7 @@ install_requires = numpy h5py typing-extensions + +[options.extras_require] +profiling = dftracer==2.0.2 diff --git a/src/dyad/CMakeLists.txt b/src/dyad/CMakeLists.txt index ef83edf7..0e8ab3bc 100644 --- a/src/dyad/CMakeLists.txt +++ b/src/dyad/CMakeLists.txt @@ -2,6 +2,7 @@ include_directories(${CMAKE_SOURCE_DIR}/src) # usual source code of the current include_directories(${CMAKE_BINARY_DIR}/include) add_subdirectory(utils) add_subdirectory(dtl) +add_subdirectory(cache) add_subdirectory(core) add_subdirectory(client) add_subdirectory(service) diff --git a/src/dyad/cache/CMakeLists.txt b/src/dyad/cache/CMakeLists.txt new file mode 100644 index 00000000..cb592333 --- /dev/null +++ b/src/dyad/cache/CMakeLists.txt @@ -0,0 +1,43 @@ +# Cache-eviction policy interface +set(CACHE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/dyad_cache_api.c + ${CMAKE_CURRENT_SOURCE_DIR}/dyad_cache.c + ${CMAKE_CURRENT_SOURCE_DIR}/lru_cache.c + ${CMAKE_CURRENT_SOURCE_DIR}/fifo_cache.c) +set(CACHE_PRIVATE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/dyad_cache_api.h + ${CMAKE_CURRENT_SOURCE_DIR}/dyad_cache_int.h + ${CMAKE_CURRENT_SOURCE_DIR}/lru_cache.h + ${CMAKE_CURRENT_SOURCE_DIR}/fifo_cache.h + ${CMAKE_CURRENT_SOURCE_DIR}/../common/dyad_structures_int.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/dyad/common/dyad_cache.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/dyad/common/dyad_rc.h) +set(CACHE_PUBLIC_HEADERS) + +# Unlike the DTL backends, LRU and FIFO have no external dependencies, so +# both are always compiled in -- no compile-time feature flags needed. + +add_library(${PROJECT_NAME}_cache SHARED ${CACHE_SRC} ${CACHE_PUBLIC_HEADERS} ${CACHE_PRIVATE_HEADERS}) +target_link_libraries(${PROJECT_NAME}_cache PRIVATE ${PROJECT_NAME}_utils) + +target_compile_definitions(${PROJECT_NAME}_cache PUBLIC DYAD_HAS_CONFIG) +target_include_directories(${PROJECT_NAME}_cache PUBLIC + $ + $) + +dyad_add_werror_if_needed(${PROJECT_NAME}_cache) +if(DYAD_LOGGER STREQUAL "CPP_LOGGER") + target_link_libraries(${PROJECT_NAME}_cache PRIVATE ${cpp-logger_LIBRARIES}) +endif() +if(DYAD_PROFILER STREQUAL "DFTRACER") + target_link_libraries(${PROJECT_NAME}_cache PRIVATE ${DFTRACER_LIBRARIES}) +endif() + +install( + TARGETS ${PROJECT_NAME}_cache + EXPORT ${DYAD_EXPORTED_TARGETS} + LIBRARY DESTINATION ${DYAD_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${DYAD_INSTALL_LIBDIR} + RUNTIME DESTINATION ${DYAD_INSTALL_BINDIR} +) +if(NOT ${CACHE_PUBLIC_HEADERS} STREQUAL "") + dyad_install_headers("${CACHE_PUBLIC_HEADERS}" ${CMAKE_CURRENT_SOURCE_DIR}) +endif() diff --git a/src/dyad/cache/dyad_cache.c b/src/dyad/cache/dyad_cache.c new file mode 100644 index 00000000..85c5af97 --- /dev/null +++ b/src/dyad/cache/dyad_cache.c @@ -0,0 +1,212 @@ +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +// clang-format off +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// clang-format on + +dyad_rc_t dyad_cache_scan_dir (const dyad_ctx_t *ctx, + const char *managed_path, + struct dyad_cache_entry **out_entries, + size_t *out_n_entries, + uint64_t *out_total_size) +{ + dyad_rc_t rc = DYAD_RC_OK; + DIR *dirp = NULL; + struct dirent *dent = NULL; + struct dyad_cache_entry *entries = NULL; + size_t n_entries = 0; + size_t capacity = 0; + uint64_t total_size = 0; + + *out_entries = NULL; + *out_n_entries = 0; + *out_total_size = 0; + + dirp = opendir (managed_path); + if (dirp == NULL) { + DYAD_LOG_ERROR (ctx, "DYAD CACHE: Cannot open managed directory %s for scanning", + managed_path); + return DYAD_RC_BADFIO; + } + + while ((dent = readdir (dirp)) != NULL) { + char path[PATH_MAX + 1]; + struct stat st; + int64_t recency_key = 0; + + if (strcmp (dent->d_name, ".") == 0 || strcmp (dent->d_name, "..") == 0 + || strcmp (dent->d_name, DYAD_CACHE_LOCK_FILENAME) == 0) { + continue; + } + snprintf (path, sizeof (path), "%s/%s", managed_path, dent->d_name); + if (stat (path, &st) != 0 || !S_ISREG (st.st_mode)) { + continue; + } + if (ctx->cache_policy == NULL || ctx->cache_policy->get_recency_key == NULL + || DYAD_IS_ERROR (ctx->cache_policy->get_recency_key (ctx, &st, &recency_key))) { + // No usable policy -- skip this candidate rather than guessing an order. + continue; + } + + if (n_entries == capacity) { + size_t new_capacity = (capacity == 0) ? 16 : capacity * 2; + struct dyad_cache_entry *new_entries = + realloc (entries, new_capacity * sizeof (struct dyad_cache_entry)); + if (new_entries == NULL) { + DYAD_LOG_ERROR (ctx, "DYAD CACHE: Cannot allocate memory while scanning %s", + managed_path); + free (entries); + closedir (dirp); + return DYAD_RC_SYSFAIL; + } + entries = new_entries; + capacity = new_capacity; + } + + strncpy (entries[n_entries].path, path, PATH_MAX); + entries[n_entries].path[PATH_MAX] = '\0'; + entries[n_entries].size = st.st_size; + entries[n_entries].atime = st.st_atime; + entries[n_entries].recency_key = recency_key; + total_size += (uint64_t)st.st_size; + n_entries++; + } + closedir (dirp); + + *out_entries = entries; + *out_n_entries = n_entries; + *out_total_size = total_size; + return rc; +} + +static int dyad_cache_entry_cmp (const void *a, const void *b) +{ + const struct dyad_cache_entry *ea = (const struct dyad_cache_entry *)a; + const struct dyad_cache_entry *eb = (const struct dyad_cache_entry *)b; + if (ea->recency_key < eb->recency_key) return -1; + if (ea->recency_key > eb->recency_key) return 1; + return 0; +} + +dyad_rc_t dyad_cache_maybe_evict (dyad_ctx_t *ctx, const char *managed_path) +{ + struct dyad_cache_entry *entries = NULL; + size_t n_entries = 0; + uint64_t total_size = 0; + uint64_t low_watermark = 0; + char lock_path[PATH_MAX + 1]; + int lock_fd = -1; + struct flock dir_lock; + time_t now = 0; + size_t i = 0; + size_t n_evicted = 0; + uint64_t bytes_freed = 0; + + // Zero-overhead when disabled (the default) -- a single check, no + // filesystem access at all. + if (ctx == NULL || ctx->cache_capacity_bytes == 0 || ctx->cache_policy_mode == DYAD_CACHE_NONE + || managed_path == NULL) { + return DYAD_RC_OK; + } + + if (DYAD_IS_ERROR (dyad_cache_scan_dir (ctx, managed_path, &entries, &n_entries, &total_size))) { + // Best-effort: a scan failure should not fail the caller's produce/consume. + return DYAD_RC_OK; + } + if (total_size <= ctx->cache_capacity_bytes) { + free (entries); + return DYAD_RC_OK; + } + free (entries); + entries = NULL; + + // Over capacity: serialize the scan-and-evict decision across + // processes sharing this managed directory via a dedicated lock file, + // reusing the same dyad_excl_flock() primitive used for per-file + // coordination elsewhere in DYAD. + snprintf (lock_path, sizeof (lock_path), "%s/%s", managed_path, DYAD_CACHE_LOCK_FILENAME); + lock_fd = open (lock_path, O_RDWR | O_CREAT, 0666); + if (lock_fd == -1) { + DYAD_LOG_ERROR (ctx, "DYAD CACHE: Cannot open cache lock file %s", lock_path); + return DYAD_RC_OK; + } + if (DYAD_IS_ERROR (dyad_excl_flock (ctx, lock_fd, &dir_lock))) { + close (lock_fd); + return DYAD_RC_OK; + } + + // Re-check: another process may have already evicted while we waited. + if (DYAD_IS_ERROR (dyad_cache_scan_dir (ctx, managed_path, &entries, &n_entries, &total_size))) { + dyad_release_flock (ctx, lock_fd, &dir_lock); + close (lock_fd); + return DYAD_RC_OK; + } + if (total_size <= ctx->cache_capacity_bytes) { + free (entries); + dyad_release_flock (ctx, lock_fd, &dir_lock); + close (lock_fd); + return DYAD_RC_OK; + } + + low_watermark = (uint64_t)(ctx->cache_low_watermark_frac * (double)ctx->cache_capacity_bytes); + qsort (entries, n_entries, sizeof (struct dyad_cache_entry), dyad_cache_entry_cmp); + now = time (NULL); + + for (i = 0; i < n_entries && total_size > low_watermark; i++) { + int victim_fd = -1; + struct flock victim_lock; + + if ((now - entries[i].atime) < (time_t)ctx->cache_grace_period_sec) { + // Recently accessed -- may still be mid-access. Skip. + continue; + } + + victim_fd = open (entries[i].path, O_RDWR); + if (victim_fd == -1) { + // Already gone (e.g. raced with another evictor or the owner). + continue; + } + if (dyad_try_excl_flock (ctx, victim_fd, &victim_lock) != DYAD_RC_OK) { + // Locked by an in-flight dyad_produce()/dyad_consume() elsewhere. Skip. + close (victim_fd); + continue; + } + if (unlink (entries[i].path) == 0) { + total_size -= (uint64_t)entries[i].size; + bytes_freed += (uint64_t)entries[i].size; + n_evicted++; + } + dyad_release_flock (ctx, victim_fd, &victim_lock); + close (victim_fd); + } + + free (entries); + dyad_release_flock (ctx, lock_fd, &dir_lock); + close (lock_fd); + + DYAD_LOG_INFO (ctx, + "DYAD CACHE: [node %u rank %u pid %d] Evicted %zu file(s), freed %llu bytes " + "from %s", + ctx->node_idx, + ctx->rank, + ctx->pid, + n_evicted, + (unsigned long long)bytes_freed, + managed_path); + + return DYAD_RC_OK; +} diff --git a/src/dyad/cache/dyad_cache_api.c b/src/dyad/cache/dyad_cache_api.c new file mode 100644 index 00000000..b7984e87 --- /dev/null +++ b/src/dyad/cache/dyad_cache_api.c @@ -0,0 +1,64 @@ +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +#include + +#include +#include +#include +#include +#include + +dyad_rc_t dyad_cache_policy_init (dyad_ctx_t *ctx, dyad_cache_policy_mode_t mode) +{ + DYAD_C_FUNCTION_START (); + dyad_rc_t rc = DYAD_RC_OK; + ctx->cache_policy = malloc (sizeof (struct dyad_cache_policy)); + if (ctx->cache_policy == NULL) { + rc = DYAD_RC_SYSFAIL; + DYAD_LOG_ERROR (ctx, "Cannot allocate memory for cache policy handle"); + goto cache_policy_init_done; + } + ctx->cache_policy->mode = mode; + ctx->cache_policy->get_recency_key = NULL; + // No compile-time gating here (unlike the DTL backends): LRU and FIFO + // have no external dependencies and are always available. + if (mode == DYAD_CACHE_LRU) { + rc = dyad_cache_lru_init (ctx); + } else if (mode == DYAD_CACHE_FIFO) { + rc = dyad_cache_fifo_init (ctx); + } else if (mode == DYAD_CACHE_NONE) { + rc = DYAD_RC_OK; + } else { + rc = DYAD_RC_BADCACHEMODE; + } + +cache_policy_init_done:; + DYAD_C_FUNCTION_END (); + return rc; +} + +dyad_rc_t dyad_cache_policy_finalize (dyad_ctx_t *ctx) +{ + DYAD_C_FUNCTION_START (); + dyad_rc_t rc = DYAD_RC_OK; + if (ctx->cache_policy == NULL) { + rc = DYAD_RC_OK; + goto cache_policy_finalize_done; + } + if (ctx->cache_policy->mode == DYAD_CACHE_LRU) { + dyad_cache_lru_finalize (ctx); + } else if (ctx->cache_policy->mode == DYAD_CACHE_FIFO) { + dyad_cache_fifo_finalize (ctx); + } + rc = DYAD_RC_OK; + +cache_policy_finalize_done:; + free (ctx->cache_policy); + ctx->cache_policy = NULL; + DYAD_C_FUNCTION_END (); + return rc; +} diff --git a/src/dyad/cache/dyad_cache_api.h b/src/dyad/cache/dyad_cache_api.h new file mode 100644 index 00000000..fa212fa7 --- /dev/null +++ b/src/dyad/cache/dyad_cache_api.h @@ -0,0 +1,128 @@ +#ifndef DYAD_CACHE_DYAD_CACHE_API_H +#define DYAD_CACHE_DYAD_CACHE_API_H + +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +// clang-format off +#include +#include +#include +#include +// clang-format on + +#ifdef __cplusplus +#include +#else +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Internal state for the LRU cache-eviction policy. + * @see dyad_cache_private_t + */ +struct dyad_cache_lru; + +/** + * @brief Internal state for the FIFO cache-eviction policy. + * @see dyad_cache_private_t + */ +struct dyad_cache_fifo; + +/** + * @brief Union holding a pointer to the internal state of the active + * cache-eviction policy. + * + * @details + * Only one member is valid at a time, selected by the @c mode field of + * the owning @c dyad_cache_policy struct. LRU and FIFO carry no state + * beyond their mode (both are derived purely from filesystem metadata + * at scan time), so these structs are currently empty placeholders — + * the union exists so a future, richer policy (e.g. one that needs a + * persisted access-count for LFU) can add real state without another + * interface change. + */ +union dyad_cache_private { + struct dyad_cache_lru *lru_handle; + struct dyad_cache_fifo *fifo_handle; +} __attribute__ ((aligned (16))); +typedef union dyad_cache_private dyad_cache_private_t; + +/** + * @brief Cache-eviction policy handle. + * + * @details + * Provides a uniform interface for all eviction policies through a + * single function pointer. The active policy is selected at + * initialization time by @c dyad_cache_policy_init() and stored in + * @c private_cache. Mirrors the @c dyad_dtl struct-of-function-pointers + * pattern used for the Data Transport Layer (@c dyad_dtl_api.h), scoped + * down to what LRU/FIFO actually need. + */ +struct dyad_cache_policy { + dyad_cache_private_t private_cache; ///< Opaque pointer to the active policy's state. + dyad_cache_policy_mode_t mode; ///< Active policy. @see dyad_cache_policy_mode_t. + + /** + * @brief Computes the recency key used to rank an eviction candidate. + * + * @details + * Smaller values are evicted first. LRU returns @c st->st_atime; + * FIFO returns @c st->st_mtime. + * + * @param[in] ctx DYAD context. + * @param[in] st @c stat() result for the candidate file. + * @param[out] out Recency key; smaller is evicted first. + * @return @c DYAD_RC_OK on success, or an error code on failure. + */ + dyad_rc_t (*get_recency_key) (const dyad_ctx_t *ctx, const struct stat *st, int64_t *out); + +} __attribute__ ((aligned (256))); +typedef struct dyad_cache_policy dyad_cache_policy_t; + +/** + * @brief Initializes the cache-eviction policy for a DYAD context. + * + * @details + * Allocates the @c cache_policy handle inside @p ctx and wires its + * @c get_recency_key function pointer based on @p mode. Unlike the DTL + * backends, LRU and FIFO have no external dependencies and thus no + * compile-time gating — both are always available. + * + * @param[in,out] ctx DYAD context. On success, @c ctx->cache_policy is + * allocated and initialized. + * @param[in] mode Eviction policy to use. @c DYAD_CACHE_NONE is a + * valid mode: initialization still succeeds, but + * @c dyad_cache_maybe_evict() will no-op. + * + * @return @c dyad_rc_t return code: + * @retval DYAD_RC_OK Initialization succeeded. + * @retval DYAD_RC_SYSFAIL Failed to allocate the @c dyad_cache_policy handle. + */ +dyad_rc_t dyad_cache_policy_init (dyad_ctx_t *ctx, dyad_cache_policy_mode_t mode); + +/** + * @brief Finalizes and frees the cache-eviction policy for a DYAD context. + * + * @details + * Frees @c ctx->cache_policy and sets it to @c NULL. If already @c NULL, + * this is a no-op that returns @c DYAD_RC_OK. + * + * @param[in,out] ctx DYAD context. On return, @c ctx->cache_policy is @c NULL. + * @return @c DYAD_RC_OK always (finalization errors are non-recoverable + * and should not block teardown). + */ +dyad_rc_t dyad_cache_policy_finalize (dyad_ctx_t *ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* DYAD_CACHE_DYAD_CACHE_API_H */ diff --git a/src/dyad/cache/dyad_cache_int.h b/src/dyad/cache/dyad_cache_int.h new file mode 100644 index 00000000..942f498d --- /dev/null +++ b/src/dyad/cache/dyad_cache_int.h @@ -0,0 +1,115 @@ +#ifndef DYAD_CACHE_DYAD_CACHE_INT_H +#define DYAD_CACHE_DYAD_CACHE_INT_H + +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +// clang-format off +#include +#include +#include +#include +#include +// clang-format on + +#ifdef __cplusplus +#include +#include +#else +#include +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Name of the per-directory advisory lock file used to serialize + * the scan-and-evict step across independent processes sharing + * one DYAD-managed directory (e.g. DataLoader worker processes). + */ +#define DYAD_CACHE_LOCK_FILENAME ".dyad_cache.lock" + +/** + * @struct dyad_cache_entry + * @brief One eviction candidate discovered by @c dyad_cache_scan_dir(). + */ +struct dyad_cache_entry { + char path[PATH_MAX + 1]; ///< Full path to the candidate file. + off_t size; ///< Size in bytes, from stat(). + time_t atime; ///< Last-access time, from stat() -- used for the grace-period + ///< check regardless of which policy is active. + int64_t recency_key; ///< Ranking key from the active policy; smaller = evict first. +}; + +/** + * @brief Scans a managed directory for eviction candidates. + * + * @details + * Lists every regular file in @p managed_path except the cache lock file + * itself (@c DYAD_CACHE_LOCK_FILENAME), @c stat()s each one, and + * populates @p out_entries (caller-owned, must be released with + * @c free()) with one @c dyad_cache_entry per file, ranked via + * @c ctx->cache_policy->get_recency_key(). Also reports the total size + * of all discovered entries via @p out_total_size. + * + * @param[in] ctx DYAD context. @c ctx->cache_policy must be initialized. + * @param[in] managed_path Directory to scan. + * @param[out] out_entries Set to a malloc'd array of entries (caller frees). + * Set to @c NULL if the directory contains no + * eligible entries. + * @param[out] out_n_entries Number of entries in @p out_entries. + * @param[out] out_total_size Sum of @c size across all entries. + * @return @c DYAD_RC_OK on success, or an error code on failure. + */ +dyad_rc_t dyad_cache_scan_dir (const dyad_ctx_t *ctx, + const char *managed_path, + struct dyad_cache_entry **out_entries, + size_t *out_n_entries, + uint64_t *out_total_size); + +/** + * @brief Evicts cached files from a managed directory if it exceeds capacity. + * + * @details + * No-op if @c ctx->cache_capacity_bytes is 0 (eviction disabled, the + * default) or @c ctx->cache_policy_mode is @c DYAD_CACHE_NONE -- a single + * branch-and-return, so calling this unconditionally from + * @c dyad_produce()/@c dyad_consume() carries no overhead for existing + * deployments that don't opt in. Otherwise: + * + * 1. Scans @p managed_path via @c dyad_cache_scan_dir(). If total usage + * is already at or below capacity, returns immediately without + * taking any lock. + * 2. Acquires an exclusive lock on + * @c managed_path/DYAD_CACHE_LOCK_FILENAME (reusing @c dyad_excl_flock()), + * then re-scans and re-checks usage in case another process already + * evicted enough while this one was waiting for the lock. + * 3. Sorts candidates by recency key ascending (oldest/least-recently + * used first per the active policy) and @c unlink()s them, skipping + * any candidate that is currently locked by another process (via + * @c dyad_try_excl_flock()) or was accessed within + * @c ctx->cache_grace_period_sec, until usage drops to + * @c ctx->cache_low_watermark_frac * ctx->cache_capacity_bytes. + * 4. Releases the directory lock. + * + * Eviction failures on individual files are logged and skipped -- this + * function never turns a successful @c dyad_produce()/@c dyad_consume() + * call into a reported error. + * + * @param[in] ctx DYAD context. + * @param[in] managed_path Managed directory to enforce the capacity of + * (@c ctx->prod_managed_path or @c ctx->cons_managed_path). + * @return @c DYAD_RC_OK always (best-effort; see above). + */ +dyad_rc_t dyad_cache_maybe_evict (dyad_ctx_t *ctx, const char *managed_path); + +#ifdef __cplusplus +} +#endif + +#endif /* DYAD_CACHE_DYAD_CACHE_INT_H */ diff --git a/src/dyad/cache/fifo_cache.c b/src/dyad/cache/fifo_cache.c new file mode 100644 index 00000000..92409d9a --- /dev/null +++ b/src/dyad/cache/fifo_cache.c @@ -0,0 +1,52 @@ +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +#include + +#include +#include +#include + +static dyad_rc_t dyad_cache_fifo_get_recency_key (const dyad_ctx_t *ctx, + const struct stat *st, + int64_t *out) +{ + (void)ctx; + if (st == NULL || out == NULL) { + return DYAD_RC_BADBUF; + } + // Files under a DYAD-managed directory are effectively write-once, so + // mtime approximates insertion order for a FIFO policy. + *out = (int64_t)st->st_mtime; + return DYAD_RC_OK; +} + +dyad_rc_t dyad_cache_fifo_init (dyad_ctx_t *ctx) +{ + DYAD_C_FUNCTION_START (); + dyad_rc_t rc = DYAD_RC_OK; + ctx->cache_policy->private_cache.fifo_handle = malloc (sizeof (struct dyad_cache_fifo)); + if (ctx->cache_policy->private_cache.fifo_handle == NULL) { + DYAD_LOG_ERROR (ctx, "Cannot allocate memory for FIFO cache policy"); + rc = DYAD_RC_SYSFAIL; + goto fifo_init_done; + } + ctx->cache_policy->get_recency_key = dyad_cache_fifo_get_recency_key; +fifo_init_done:; + DYAD_C_FUNCTION_END (); + return rc; +} + +dyad_rc_t dyad_cache_fifo_finalize (dyad_ctx_t *ctx) +{ + DYAD_C_FUNCTION_START (); + if (ctx->cache_policy != NULL) { + free (ctx->cache_policy->private_cache.fifo_handle); + ctx->cache_policy->private_cache.fifo_handle = NULL; + } + DYAD_C_FUNCTION_END (); + return DYAD_RC_OK; +} diff --git a/src/dyad/cache/fifo_cache.h b/src/dyad/cache/fifo_cache.h new file mode 100644 index 00000000..1e797627 --- /dev/null +++ b/src/dyad/cache/fifo_cache.h @@ -0,0 +1,53 @@ +#ifndef DYAD_CACHE_FIFO_CACHE_H +#define DYAD_CACHE_FIFO_CACHE_H + +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Internal state for the FIFO cache-eviction policy. + * + * @details + * Carries no fields: FIFO's recency key (@c st_mtime) is read directly + * from each candidate's @c stat() result at scan time, so there is + * nothing to persist between calls. The struct exists only to give + * @c dyad_cache_private_t a distinct, named member per policy. + */ +struct dyad_cache_fifo { + int _unused; +}; + +/** + * @brief Initializes the FIFO cache-eviction policy. + * + * @details + * Allocates @c ctx->cache_policy->private_cache.fifo_handle and wires + * @c ctx->cache_policy->get_recency_key to @c dyad_cache_fifo_get_recency_key(). + * + * @param[in,out] ctx DYAD context. @c ctx->cache_policy must already be + * allocated by @c dyad_cache_policy_init(). + * @return @c DYAD_RC_OK on success, or @c DYAD_RC_SYSFAIL on allocation failure. + */ +dyad_rc_t dyad_cache_fifo_init (dyad_ctx_t *ctx); + +/** + * @brief Finalizes the FIFO cache-eviction policy. + * @param[in,out] ctx DYAD context. + * @return @c DYAD_RC_OK always. + */ +dyad_rc_t dyad_cache_fifo_finalize (dyad_ctx_t *ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* DYAD_CACHE_FIFO_CACHE_H */ diff --git a/src/dyad/cache/lru_cache.c b/src/dyad/cache/lru_cache.c new file mode 100644 index 00000000..fd449317 --- /dev/null +++ b/src/dyad/cache/lru_cache.c @@ -0,0 +1,50 @@ +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +#include + +#include +#include +#include + +static dyad_rc_t dyad_cache_lru_get_recency_key (const dyad_ctx_t *ctx, + const struct stat *st, + int64_t *out) +{ + (void)ctx; + if (st == NULL || out == NULL) { + return DYAD_RC_BADBUF; + } + *out = (int64_t)st->st_atime; + return DYAD_RC_OK; +} + +dyad_rc_t dyad_cache_lru_init (dyad_ctx_t *ctx) +{ + DYAD_C_FUNCTION_START (); + dyad_rc_t rc = DYAD_RC_OK; + ctx->cache_policy->private_cache.lru_handle = malloc (sizeof (struct dyad_cache_lru)); + if (ctx->cache_policy->private_cache.lru_handle == NULL) { + DYAD_LOG_ERROR (ctx, "Cannot allocate memory for LRU cache policy"); + rc = DYAD_RC_SYSFAIL; + goto lru_init_done; + } + ctx->cache_policy->get_recency_key = dyad_cache_lru_get_recency_key; +lru_init_done:; + DYAD_C_FUNCTION_END (); + return rc; +} + +dyad_rc_t dyad_cache_lru_finalize (dyad_ctx_t *ctx) +{ + DYAD_C_FUNCTION_START (); + if (ctx->cache_policy != NULL) { + free (ctx->cache_policy->private_cache.lru_handle); + ctx->cache_policy->private_cache.lru_handle = NULL; + } + DYAD_C_FUNCTION_END (); + return DYAD_RC_OK; +} diff --git a/src/dyad/cache/lru_cache.h b/src/dyad/cache/lru_cache.h new file mode 100644 index 00000000..19d62a59 --- /dev/null +++ b/src/dyad/cache/lru_cache.h @@ -0,0 +1,53 @@ +#ifndef DYAD_CACHE_LRU_CACHE_H +#define DYAD_CACHE_LRU_CACHE_H + +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Internal state for the LRU cache-eviction policy. + * + * @details + * Carries no fields: LRU's recency key (@c st_atime) is read directly + * from each candidate's @c stat() result at scan time, so there is + * nothing to persist between calls. The struct exists only to give + * @c dyad_cache_private_t a distinct, named member per policy. + */ +struct dyad_cache_lru { + int _unused; +}; + +/** + * @brief Initializes the LRU cache-eviction policy. + * + * @details + * Allocates @c ctx->cache_policy->private_cache.lru_handle and wires + * @c ctx->cache_policy->get_recency_key to @c dyad_cache_lru_get_recency_key(). + * + * @param[in,out] ctx DYAD context. @c ctx->cache_policy must already be + * allocated by @c dyad_cache_policy_init(). + * @return @c DYAD_RC_OK on success, or @c DYAD_RC_SYSFAIL on allocation failure. + */ +dyad_rc_t dyad_cache_lru_init (dyad_ctx_t *ctx); + +/** + * @brief Finalizes the LRU cache-eviction policy. + * @param[in,out] ctx DYAD context. + * @return @c DYAD_RC_OK always. + */ +dyad_rc_t dyad_cache_lru_finalize (dyad_ctx_t *ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* DYAD_CACHE_LRU_CACHE_H */ diff --git a/src/dyad/client/CMakeLists.txt b/src/dyad/client/CMakeLists.txt index 470f1c10..5b8dffd2 100644 --- a/src/dyad/client/CMakeLists.txt +++ b/src/dyad/client/CMakeLists.txt @@ -3,6 +3,7 @@ set(DYAD_CLIENT_PRIVATE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/../common/dyad_loggi ${CMAKE_CURRENT_SOURCE_DIR}/../common/dyad_profiler.h ${CMAKE_CURRENT_SOURCE_DIR}/../common/dyad_structures_int.h ${CMAKE_CURRENT_SOURCE_DIR}/../dtl/dyad_dtl_api.h + ${CMAKE_CURRENT_SOURCE_DIR}/../cache/dyad_cache_int.h ${CMAKE_CURRENT_SOURCE_DIR}/../utils/utils.h ${CMAKE_CURRENT_SOURCE_DIR}/../utils/murmur3.h ${CMAKE_CURRENT_SOURCE_DIR}/dyad_client_int.h) @@ -18,7 +19,7 @@ set_target_properties(${PROJECT_NAME}_client PROPERTIES CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${DYAD_LIBDIR}") target_link_libraries(${PROJECT_NAME}_client PRIVATE Jansson::Jansson flux::core) target_link_libraries(${PROJECT_NAME}_client PRIVATE ${PROJECT_NAME}_utils - ${PROJECT_NAME}_murmur3 ${PROJECT_NAME}_dtl) + ${PROJECT_NAME}_murmur3 ${PROJECT_NAME}_dtl ${PROJECT_NAME}_cache) target_link_libraries(${PROJECT_NAME}_client PUBLIC ${PROJECT_NAME}_ctx) target_compile_definitions(${PROJECT_NAME}_client PRIVATE BUILDING_DYAD=1) diff --git a/src/dyad/client/dyad_client.c b/src/dyad/client/dyad_client.c index 290acc4a..37d97393 100644 --- a/src/dyad/client/dyad_client.c +++ b/src/dyad/client/dyad_client.c @@ -9,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -797,6 +799,115 @@ get_done:; return rc; } +/** + * @brief Retrieves a byte range of file data from a remote producer's Flux + * broker via RPC. + * + * @details + * Parallel to @c dyad_get_data(), for @c dyad_consume_range() requests + * (FLUX_RPC and MARGO DTL modes only, checked by the caller). Differs only + * in: packs @p offset/@p length via @c rpc_pack_range() instead of just + * @p mdata->fpath, and targets the @c DYAD_DTL_RPC_RANGE_NAME topic instead + * of @c DYAD_DTL_RPC_NAME so it is routed to @c dyad_fetch_range_request_cb() + * on the producer's broker. + * + * @param[in] ctx Pointer to the DYAD context. Must not be @c NULL. + * @param[in] mdata Metadata for the file to retrieve. Must not be + * @c NULL. @c mdata->fpath and @c mdata->owner_rank + * identify the file and the producer broker to + * contact. + * @param[in] offset Starting byte offset of the requested range. + * @param[in] length Number of bytes requested. + * @param[out] file_data Address of a pointer to be set to the buffer + * containing the retrieved range. Allocated by the + * DTL layer; the caller releases it via + * @c ctx->dtl_handle->return_buffer(). + * @param[out] file_len Address of a @c size_t to be set to the number of + * bytes in @p file_data. + * + * @return @c dyad_rc_t return code, same conventions as @c dyad_get_data(). + */ +DYAD_DLL_EXPORTED dyad_rc_t dyad_get_data_range (const dyad_ctx_t *restrict ctx, + const dyad_metadata_t *restrict mdata, + size_t offset, + size_t length, + char **restrict file_data, + size_t *restrict file_len) +{ + DYAD_C_FUNCTION_START (); + dyad_rc_t rc = DYAD_RC_OK; + flux_future_t *f = NULL; + json_t *rpc_payload = NULL; + DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Packing ranged payload for RPC to DYAD module"); + DYAD_C_FUNCTION_UPDATE_INT ("owner_rank", mdata->owner_rank); + DYAD_C_FUNCTION_UPDATE_STR ("fpath", mdata->fpath); + rc = ctx->dtl_handle->rpc_pack_range (ctx, mdata->fpath, mdata->owner_rank, offset, length, + &rpc_payload); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, + "Cannot create JSON payload for ranged Flux RPC to " + "DYAD module\n"); + goto get_range_done; + } + DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Sending ranged payload for RPC to DYAD module"); + f = flux_rpc_pack ((flux_t *)ctx->h, + DYAD_DTL_RPC_RANGE_NAME, + mdata->owner_rank, + FLUX_RPC_STREAMING, + "o", + rpc_payload); + if (f == NULL) { + DYAD_LOG_ERROR (ctx, "Cannot send ranged RPC to producer module."); + rc = DYAD_RC_BADRPC; + goto get_range_done; + } + DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Receive RPC response from DYAD module"); + rc = ctx->dtl_handle->rpc_recv_response (ctx, f); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, "Cannot receive and/or parse the RPC response."); + goto get_range_done; + } + DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Establish DTL connection with DYAD module"); + rc = ctx->dtl_handle->establish_connection (ctx); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, + "Cannot establish connection with DYAD module on broker " + "%u.", + mdata->owner_rank); + goto get_range_done; + } + DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Receive ranged file data via DTL"); + rc = ctx->dtl_handle->recv (ctx, (void **)file_data, file_len); + DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Close DTL connection with DYAD module"); + ctx->dtl_handle->close_connection (ctx); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, "Cannot receive ranged data from producer module."); + goto get_range_done; + } + DYAD_C_FUNCTION_UPDATE_INT ("file_len", *file_len); + + rc = DYAD_RC_OK; + +get_range_done:; + // Same end-of-stream handling as dyad_get_data() -- see its comment. + if (rc != DYAD_RC_RPC_FINISHED && rc != DYAD_RC_BADRPC) { + if (!(flux_rpc_get (f, NULL) < 0 && errno == ENODATA)) { + DYAD_LOG_ERROR (ctx, + "An error occured at end of getting ranged data! Either the " + "module sent too many responses, or the module " + "failed with a bad error (errno = %d).", + errno); + rc = DYAD_RC_BADRPC; + } + } + DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Read %zd bytes of range from %s file", *file_len, + mdata->fpath); + DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Destroy the Flux future for the RPC."); + flux_future_destroy (f); + DYAD_C_FUNCTION_END (); + return rc; +} + /** * @brief Writes file data retrieved from a producer to the consumer-managed directory. * @@ -946,6 +1057,11 @@ dyad_rc_t dyad_produce (dyad_ctx_t *restrict ctx, const char *restrict fname) // If the context is valid, call dyad_commit to perform // the producer operation rc = dyad_commit (ctx, fname); + if (!DYAD_IS_ERROR (rc)) { + // Best-effort: eviction failures must never turn a successful + // produce into a reported error. No-ops if eviction is disabled. + dyad_cache_maybe_evict (ctx, ctx->prod_managed_path); + } produce_done:; DYAD_C_FUNCTION_END (); return rc; @@ -1216,6 +1332,12 @@ dyad_rc_t dyad_consume (dyad_ctx_t *restrict ctx, const char *restrict fname) }; } dyad_release_flock (ctx, lock_fd, &exclusive_lock); + // Best-effort: eviction failures must never turn a successful + // consume into a reported error. No-ops if eviction is disabled. + // Reached whether the file was already local or freshly fetched; + // dyad_cache_maybe_evict()'s own capacity check makes the + // already-under-budget case a cheap no-op either way. + dyad_cache_maybe_evict (ctx, ctx->cons_managed_path); } DYAD_C_FUNCTION_UPDATE_INT ("file_size", file_size); consume_done:; @@ -1321,6 +1443,12 @@ dyad_rc_t dyad_consume_w_metadata (dyad_ctx_t *restrict ctx, }; } dyad_release_flock (ctx, lock_fd, &exclusive_lock); + // Best-effort: eviction failures must never turn a successful consume + // into a reported error. No-ops if eviction is disabled. Reached + // whether the file was already local or freshly fetched; + // dyad_cache_maybe_evict()'s own capacity check makes the + // already-under-budget case a cheap no-op either way. + dyad_cache_maybe_evict (ctx, ctx->cons_managed_path); DYAD_C_FUNCTION_UPDATE_INT ("file_size", file_size); if (close (lock_fd) != 0) { @@ -1339,6 +1467,130 @@ consume_close:; return rc; } +dyad_rc_t dyad_consume_range (dyad_ctx_t *restrict ctx, + const char *restrict fname, + size_t offset, + size_t length, + void **restrict data, + size_t *restrict data_len) +{ + DYAD_C_FUNCTION_START (); + DYAD_C_FUNCTION_UPDATE_STR ("fname", fname); + dyad_rc_t rc = DYAD_RC_OK; + dyad_metadata_t *mdata = NULL; + char upath[PATH_MAX + 1] = {'\0'}; + char fullpath[PATH_MAX + 1] = {'\0'}; + int fd = -1; + ssize_t nread = 0; + struct flock shared_lock; + + *data = NULL; + *data_len = 0ul; + + if (!ctx || !ctx->h) { + rc = DYAD_RC_NOCTX; + goto consume_range_close; + } + if (ctx->cons_managed_path == NULL) { + rc = DYAD_RC_BADMANAGEDPATH; + goto consume_range_close; + } + if (ctx->dtl_handle == NULL || ctx->dtl_handle->mode == DYAD_DTL_UCX) { + DYAD_LOG_ERROR (ctx, "dyad_consume_range is not supported for the active DTL mode (UCX)"); + rc = DYAD_RC_BADDTLMODE; + goto consume_range_close; + } + + if (ctx->relative_to_managed_path && (strlen (fname) > 0ul) + && (strncmp (fname, DYAD_PATH_DELIM, ctx->delim_len) != 0)) { + memcpy (upath, fname, strlen (fname)); + } else if (!cmp_canonical_path_prefix (ctx, false, fname, upath, PATH_MAX)) { + DYAD_LOG_ERROR (ctx, + "dyad_consume_range: '%s' is not under the consumer-managed path", + fname); + rc = DYAD_RC_BADMANAGEDPATH; + goto consume_range_close; + } + ctx->reenter = false; + + rc = dyad_fetch_metadata (ctx, fname, upath, &mdata); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, "dyad_consume_range: dyad_fetch_metadata failed!"); + goto consume_range_done; + } + + if (mdata == NULL) { + // File is already local to this node (per dyad_fetch_metadata's + // existing HFL/DMD check) -- pread the local copy directly, no RPC. + strncpy (fullpath, ctx->cons_managed_path, PATH_MAX - 1); + concat_str (fullpath, upath, "/", PATH_MAX); + if (ctx->origin_path != NULL) { + char origin_fullpath[PATH_MAX + 1] = {'\0'}; + strncpy (origin_fullpath, ctx->origin_path, PATH_MAX - 1); + concat_str (origin_fullpath, upath, "/", PATH_MAX); + rc = dyad_range_cache_ensure (ctx, fullpath, origin_fullpath, offset, length); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, + "dyad_consume_range: dyad_range_cache_ensure failed for local " + "file '%s'", + fullpath); + goto consume_range_done; + } + } + fd = open (fullpath, O_RDONLY); + if (fd == -1) { + DYAD_LOG_ERROR (ctx, "dyad_consume_range: cannot open local file '%s'", fullpath); + rc = DYAD_RC_BADFIO; + goto consume_range_done; + } + rc = dyad_shared_flock (ctx, fd, &shared_lock); + if (DYAD_IS_ERROR (rc)) { + close (fd); + goto consume_range_done; + } + *data = malloc (length); + if (*data == NULL) { + rc = DYAD_RC_SYSFAIL; + dyad_release_flock (ctx, fd, &shared_lock); + close (fd); + goto consume_range_done; + } + nread = pread (fd, *data, length, (off_t)offset); + dyad_release_flock (ctx, fd, &shared_lock); + close (fd); + if (nread < 0 || (size_t)nread != length) { + DYAD_LOG_ERROR (ctx, + "dyad_consume_range: pread of local file '%s' failed (got %zd of " + "%zu)", + fullpath, + nread, + length); + free (*data); + *data = NULL; + rc = DYAD_RC_BADFIO; + goto consume_range_done; + } + *data_len = length; + } else { + // File owned by a remote broker -- fetch just the requested range. + rc = dyad_get_data_range (ctx, mdata, offset, length, (char **)data, data_len); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, "dyad_consume_range: dyad_get_data_range failed!"); + goto consume_range_done; + } + } + rc = DYAD_RC_OK; + +consume_range_done:; + if (mdata != NULL) { + dyad_free_metadata (&mdata); + } +consume_range_close:; + ctx->reenter = true; + DYAD_C_FUNCTION_END (); + return rc; +} + #if DYAD_SYNC_DIR /** * @brief Synchronizes the parent directory of a file to ensure its entry is diff --git a/src/dyad/client/dyad_client_int.h b/src/dyad/client/dyad_client_int.h index 48952f27..b993db2b 100644 --- a/src/dyad/client/dyad_client_int.h +++ b/src/dyad/client/dyad_client_int.h @@ -52,6 +52,12 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_get_data (const dyad_ctx_t *ctx, const dyad_metadata_t *mdata, char **file_data, size_t *file_len); +DYAD_DLL_EXPORTED dyad_rc_t dyad_get_data_range (const dyad_ctx_t *ctx, + const dyad_metadata_t *mdata, + size_t offset, + size_t length, + char **file_data, + size_t *file_len); DYAD_DLL_EXPORTED dyad_rc_t dyad_commit (dyad_ctx_t *ctx, const char *fname); DYAD_DLL_EXPORTED dyad_rc_t dyad_kvs_read (const dyad_ctx_t *ctx, diff --git a/src/dyad/common/dyad_structures_int.h b/src/dyad/common/dyad_structures_int.h index 6871270a..15015888 100644 --- a/src/dyad/common/dyad_structures_int.h +++ b/src/dyad/common/dyad_structures_int.h @@ -7,6 +7,7 @@ #error "no config" #endif +#include #include #ifdef __cplusplus @@ -58,6 +59,13 @@ struct dyad_ctx { char *prod_managed_path; ///< producer path managed by DYAD char *cons_managed_path; ///< consumer path managed by DYAD bool relative_to_managed_path; ///< relative path is relative to the managed path + struct dyad_cache_policy *cache_policy; ///< Opaque handle to the active cache-eviction policy + dyad_cache_policy_mode_t cache_policy_mode; ///< DYAD_CACHE_NONE (default), LRU, or FIFO + uint64_t cache_capacity_bytes; ///< 0 (default) disables eviction; else max bytes in DMD + double cache_low_watermark_frac; ///< Fraction of capacity to evict down to (default 0.8) + unsigned int cache_grace_period_sec; ///< Skip candidates accessed within this many seconds + char *origin_path; ///< Optional PFS/origin fallback path for dyad_range_cache_ensure(); + ///< NULL (default) disables lazy origin-backed caching }; typedef void *ucx_ep_cache_h; diff --git a/src/dyad/core/CMakeLists.txt b/src/dyad/core/CMakeLists.txt index 27571506..e0d9ff8e 100644 --- a/src/dyad/core/CMakeLists.txt +++ b/src/dyad/core/CMakeLists.txt @@ -16,7 +16,7 @@ target_include_directories(${PROJECT_NAME}_ctx PUBLIC $) target_include_directories(${PROJECT_NAME}_ctx SYSTEM PRIVATE ${JANSSON_INCLUDE_DIRS}) target_include_directories(${PROJECT_NAME}_ctx SYSTEM PRIVATE ${FluxCore_INCLUDE_DIRS}) -target_link_libraries(${PROJECT_NAME}_ctx PRIVATE ${PROJECT_NAME}_dtl ${PROJECT_NAME}_utils) +target_link_libraries(${PROJECT_NAME}_ctx PRIVATE ${PROJECT_NAME}_dtl ${PROJECT_NAME}_cache ${PROJECT_NAME}_utils) dyad_add_werror_if_needed(${PROJECT_NAME}_ctx) diff --git a/src/dyad/core/dyad_ctx.c b/src/dyad/core/dyad_ctx.c index aa639c55..a9e5cd68 100644 --- a/src/dyad/core/dyad_ctx.c +++ b/src/dyad/core/dyad_ctx.c @@ -4,6 +4,8 @@ #error "no config" #endif +#include +#include #include #include #include @@ -66,7 +68,13 @@ const struct dyad_ctx dyad_ctx_default = { NULL, ///< kvs_namespace NULL, ///< prod_managed_path NULL, ///< cons_managed_path - false ///< relative_to_managed_path + false, ///< relative_to_managed_path + NULL, ///< cache_policy + DYAD_CACHE_NONE, ///< cache_policy_mode + 0ull, ///< cache_capacity_bytes + 0.8, ///< cache_low_watermark_frac + 5u, ///< cache_grace_period_sec + NULL ///< origin_path }; DYAD_DLL_EXPORTED dyad_ctx_t *dyad_ctx_get (void) @@ -128,7 +136,12 @@ dyad_rc_t dyad_init (bool debug, bool relative_to_managed_path, const char *dtl_mode_str, const dyad_dtl_comm_mode_t dtl_comm_mode, - void *flux_handle) + void *flux_handle, + uint64_t cache_capacity_bytes, + const char *cache_policy_str, + double cache_low_watermark, + unsigned int cache_grace_period_sec, + const char *origin_path) { DYAD_LOGGER_INIT (); unsigned my_rank = 0u; @@ -198,6 +211,9 @@ dyad_rc_t dyad_init (bool debug, ctx->fsync_write = fsync_write; ctx->key_depth = key_depth; ctx->key_bins = key_bins; + ctx->cache_capacity_bytes = cache_capacity_bytes; + ctx->cache_low_watermark_frac = cache_low_watermark; + ctx->cache_grace_period_sec = cache_grace_period_sec; // Open a Flux handle and store it in the dyad_ctx_t // object. If the open operation failed, return DYAD_FLUXFAIL @@ -261,6 +277,20 @@ dyad_rc_t dyad_init (bool debug, DYAD_LOG_DEBUG (ctx, "DYAD_CORE: kvs_namespace %s", ctx->kvs_namespace); } + // Origin path is a plain fallback-read prefix, not part of the HFL key + // scheme -- unlike prod/cons managed paths, no hashing or realpath() + // resolution is needed, just a copy. + ctx->origin_path = NULL; + if (origin_path != NULL && strlen (origin_path) > 0ul) { + size_t origin_path_len = strlen (origin_path); + ctx->origin_path = (char *)calloc (origin_path_len + 1, sizeof (char)); + if (ctx->origin_path == NULL) { + DYAD_LOG_ERROR (ctx, "Could not allocate buffer for origin path!\n"); + goto init_region_failed; + } + memcpy (ctx->origin_path, origin_path, origin_path_len + 1); + } + // Initialize the DTL based on the value of dtl_mode // If an error occurs, log it and return an error DYAD_LOG_DEBUG (ctx, "DYAD_CORE: inintializing DYAD DTL %s", dtl_mode_str); @@ -270,6 +300,20 @@ dyad_rc_t dyad_init (bool debug, goto init_region_failed; } + // Initialize the cache-eviction policy. A NULL/unset cache_policy_str + // is treated as DYAD_CACHE_NONE (eviction disabled) rather than an + // error, since eviction is opt-in. + rc = dyad_set_and_init_cache_policy ( + (cache_capacity_bytes == 0ull) + ? dyad_cache_policy_name[DYAD_CACHE_NONE] + : ((cache_policy_str != NULL) ? cache_policy_str + : dyad_cache_policy_name[DYAD_CACHE_DEFAULT]), + cache_capacity_bytes); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, "Cannot initialize the cache-eviction policy %s", cache_policy_str); + goto init_region_failed; + } + // If the producer-managed path is provided, copy it into the dyad_ctx_t // object if (dyad_set_prod_path (prod_managed_path) != DYAD_RC_OK) { @@ -341,6 +385,11 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_init_env (const dyad_dtl_comm_mode_t dtl_comm_m const char *prod_managed_path = NULL; const char *cons_managed_path = NULL; const char *dtl_mode = NULL; + uint64_t cache_capacity_bytes = 0ull; + const char *cache_policy_str = NULL; + double cache_low_watermark = 0.8; + unsigned int cache_grace_period_sec = 5u; + const char *origin_path = NULL; if ((e = getenv (DYAD_SYNC_DEBUG_ENV))) { debug = true; @@ -427,6 +476,36 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_init_env (const dyad_dtl_comm_mode_t dtl_comm_m DYAD_LOG_STDERR ("%s is not set. Defaulting to %s\n", DYAD_DTL_MODE_ENV, dtl_mode); } + if ((e = getenv (DYAD_CACHE_CAPACITY_ENV))) { + cache_capacity_bytes = strtoull (e, NULL, 10); + } else { + cache_capacity_bytes = 0ull; // disabled by default + } + + if ((e = getenv (DYAD_CACHE_POLICY_ENV))) { + cache_policy_str = e; + } else { + cache_policy_str = dyad_cache_policy_name[DYAD_CACHE_DEFAULT]; + } + + if ((e = getenv (DYAD_CACHE_LOW_WATERMARK_ENV))) { + cache_low_watermark = atof (e); + } else { + cache_low_watermark = 0.8; + } + + if ((e = getenv (DYAD_CACHE_GRACE_PERIOD_ENV))) { + cache_grace_period_sec = atoi (e); + } else { + cache_grace_period_sec = 5u; + } + + if ((e = getenv (DYAD_PATH_ORIGIN_ENV))) { + origin_path = e; + } else { + origin_path = NULL; + } + dyad_rc_t rc = dyad_init (debug, check, shared_storage, @@ -442,7 +521,12 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_init_env (const dyad_dtl_comm_mode_t dtl_comm_m relative_to_managed_path, dtl_mode, dtl_comm_mode, - flux_handle); + flux_handle, + cache_capacity_bytes, + cache_policy_str, + cache_low_watermark, + cache_grace_period_sec, + origin_path); return rc; } @@ -489,6 +573,64 @@ set_and_init_dtl_mode_region_finish:; return rc; } +DYAD_DLL_EXPORTED dyad_rc_t dyad_set_and_init_cache_policy (const char *cache_policy_name, + uint64_t cache_capacity_bytes) +{ + int rc = DYAD_RC_OK; + size_t cache_policy_name_len = 0ul; + dyad_cache_policy_mode_t cache_policy_mode = DYAD_CACHE_NONE; + + DYAD_C_FUNCTION_START (); + + if (!cache_policy_name) { + DYAD_C_FUNCTION_END (); + return DYAD_RC_BADCACHEMODE; + } + + cache_policy_name_len = strlen (cache_policy_name); + if (strncmp (cache_policy_name, dyad_cache_policy_name[DYAD_CACHE_NONE], cache_policy_name_len) + == 0) { + cache_policy_mode = DYAD_CACHE_NONE; + } else if (strncmp (cache_policy_name, + dyad_cache_policy_name[DYAD_CACHE_LRU], + cache_policy_name_len) + == 0) { + cache_policy_mode = DYAD_CACHE_LRU; + } else if (strncmp (cache_policy_name, + dyad_cache_policy_name[DYAD_CACHE_FIFO], + cache_policy_name_len) + == 0) { + cache_policy_mode = DYAD_CACHE_FIFO; + } else { + DYAD_LOG_STDERR ("Invalid env %s = %s.\n", DYAD_CACHE_POLICY_ENV, cache_policy_name); + DYAD_C_FUNCTION_END (); + return DYAD_RC_BADCACHEMODE; + } + // A capacity of 0 always disables eviction, regardless of the + // requested policy name. + if (cache_capacity_bytes == 0ull) { + cache_policy_mode = DYAD_CACHE_NONE; + } + ctx->cache_policy_mode = cache_policy_mode; + + // If ctx->cache_policy is already null, it will just return success + rc = dyad_cache_policy_finalize (ctx); + if (DYAD_IS_ERROR (rc)) { + goto set_and_init_cache_policy_region_finish; + } + + rc = dyad_cache_policy_init (ctx, cache_policy_mode); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, + "Cannot initialize the cache-eviction policy %s\n", + dyad_cache_policy_name[cache_policy_mode]); + } + +set_and_init_cache_policy_region_finish:; + DYAD_C_FUNCTION_END (); + return rc; +} + DYAD_DLL_EXPORTED dyad_rc_t dyad_set_prod_path (const char *prod_managed_path) { dyad_rc_t rc = DYAD_RC_OK; @@ -751,6 +893,7 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_clear (void) goto clear_region_finish; } dyad_dtl_finalize (ctx); + dyad_cache_policy_finalize (ctx); if (ctx->h != NULL) { flux_close (ctx->h); ctx->h = NULL; @@ -775,6 +918,10 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_clear (void) free (ctx->cons_real_path); ctx->cons_real_path = NULL; } + if (ctx->origin_path != NULL) { + free (ctx->origin_path); + ctx->origin_path = NULL; + } rc = DYAD_RC_OK; clear_region_finish:; DYAD_C_FUNCTION_END (); diff --git a/src/dyad/dtl/dyad_dtl_api.h b/src/dyad/dtl/dyad_dtl_api.h index 2114ddc8..3a551539 100644 --- a/src/dyad/dtl/dyad_dtl_api.h +++ b/src/dyad/dtl/dyad_dtl_api.h @@ -124,6 +124,49 @@ struct dyad_dtl { */ dyad_rc_t (*rpc_unpack) (const dyad_ctx_t *ctx, const flux_msg_t *packed_obj, char **upath); + /** + * @brief Packs a byte-range fetch request into a JSON object for an RPC call. + * + * @details + * Only implemented for @c DYAD_DTL_FLUX_RPC and @c DYAD_DTL_MARGO. Left + * @c NULL for @c DYAD_DTL_UCX; callers must check @c ctx->dtl_handle->mode + * before invoking this (see @c dyad_consume_range()). + * + * @param[in] ctx DYAD context. + * @param[in] upath Relative path of the file to fetch from. + * @param[in] producer_rank Flux rank of the producer broker. + * @param[in] offset Starting byte offset of the requested range. + * @param[in] length Number of bytes requested. + * @param[out] packed_obj JSON object containing the packed request. + * @return @c DYAD_RC_OK on success, or an error code on failure. + */ + dyad_rc_t (*rpc_pack_range) (const dyad_ctx_t *ctx, + const char *upath, + uint32_t producer_rank, + size_t offset, + size_t length, + json_t **packed_obj); + + /** + * @brief Unpacks a byte-range fetch request from an incoming Flux RPC message. + * + * @details + * Only implemented for @c DYAD_DTL_FLUX_RPC and @c DYAD_DTL_MARGO. Left + * @c NULL for @c DYAD_DTL_UCX. + * + * @param[in] ctx DYAD context. + * @param[in] packed_obj Incoming Flux RPC message. + * @param[out] upath Relative path of the requested file. + * @param[out] offset Starting byte offset of the requested range. + * @param[out] length Number of bytes requested. + * @return @c DYAD_RC_OK on success, or an error code on failure. + */ + dyad_rc_t (*rpc_unpack_range) (const dyad_ctx_t *ctx, + const flux_msg_t *packed_obj, + char **upath, + size_t *offset, + size_t *length); + /** * @brief Sends the initial RPC acknowledgement from the service to the consumer. * diff --git a/src/dyad/dtl/flux_dtl.c b/src/dyad/dtl/flux_dtl.c index 381348e0..b0152663 100644 --- a/src/dyad/dtl/flux_dtl.c +++ b/src/dyad/dtl/flux_dtl.c @@ -31,6 +31,8 @@ dyad_rc_t dyad_dtl_flux_init (const dyad_ctx_t *ctx, ctx->dtl_handle->rpc_pack = dyad_dtl_flux_rpc_pack; ctx->dtl_handle->rpc_unpack = dyad_dtl_flux_rpc_unpack; + ctx->dtl_handle->rpc_pack_range = dyad_dtl_flux_rpc_pack_range; + ctx->dtl_handle->rpc_unpack_range = dyad_dtl_flux_rpc_unpack_range; ctx->dtl_handle->rpc_respond = dyad_dtl_flux_rpc_respond; ctx->dtl_handle->rpc_recv_response = dyad_dtl_flux_rpc_recv_response; ctx->dtl_handle->get_buffer = dyad_dtl_flux_get_buffer; @@ -85,6 +87,69 @@ dyad_rc_t dyad_dtl_flux_rpc_unpack (const dyad_ctx_t *ctx, const flux_msg_t *msg return dyad_rc; } +dyad_rc_t dyad_dtl_flux_rpc_pack_range (const dyad_ctx_t *ctx, + const char *restrict upath, + uint32_t producer_rank, + size_t offset, + size_t length, + json_t **restrict packed_obj) +{ + DYAD_C_FUNCTION_START (); + DYAD_C_FUNCTION_UPDATE_STR ("upath", upath); + DYAD_C_FUNCTION_UPDATE_INT ("producer_rank", producer_rank); + dyad_rc_t rc = DYAD_RC_OK; + *packed_obj = json_pack ("{s:s, s:I, s:I}", + "upath", + upath, + "offset", + (json_int_t)offset, + "length", + (json_int_t)length); + if (*packed_obj == NULL) { + DYAD_LOG_ERROR (ctx, "Could not pack upath/offset/length for Flux DTL"); + rc = DYAD_RC_BADPACK; + goto dtl_flux_rpc_pack_range; + } +dtl_flux_rpc_pack_range: + DYAD_C_FUNCTION_END (); + return rc; +} + +dyad_rc_t dyad_dtl_flux_rpc_unpack_range (const dyad_ctx_t *ctx, + const flux_msg_t *msg, + char **upath, + size_t *offset, + size_t *length) +{ + DYAD_C_FUNCTION_START (); + int rc = 0; + dyad_rc_t dyad_rc = DYAD_RC_OK; + json_int_t offset_val = 0; + json_int_t length_val = 0; + rc = flux_request_unpack (msg, + NULL, + "{s:s, s:I, s:I}", + "upath", + upath, + "offset", + &offset_val, + "length", + &length_val); + if (FLUX_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, "Could not unpack ranged Flux message from consumer"); + dyad_rc = DYAD_RC_BADUNPACK; + goto dtl_flux_rpc_unpack_range_region_finish; + } + *offset = (size_t)offset_val; + *length = (size_t)length_val; + ctx->dtl_handle->private_dtl.flux_dtl_handle->msg = (flux_msg_t *)msg; + dyad_rc = DYAD_RC_OK; + DYAD_C_FUNCTION_UPDATE_STR ("upath", *upath); +dtl_flux_rpc_unpack_range_region_finish: + DYAD_C_FUNCTION_END (); + return dyad_rc; +} + dyad_rc_t dyad_dtl_flux_rpc_respond (const dyad_ctx_t *ctx, const flux_msg_t *orig_msg) { DYAD_C_FUNCTION_START (); diff --git a/src/dyad/dtl/flux_dtl.h b/src/dyad/dtl/flux_dtl.h index a214a248..b56a6ecf 100644 --- a/src/dyad/dtl/flux_dtl.h +++ b/src/dyad/dtl/flux_dtl.h @@ -133,6 +133,57 @@ dyad_rc_t dyad_dtl_flux_rpc_pack (const dyad_ctx_t *ctx, */ dyad_rc_t dyad_dtl_flux_rpc_unpack (const dyad_ctx_t *ctx, const flux_msg_t *msg, char **upath); +/** + * @brief Packs a byte-range fetch request into a JSON object for a Flux RPC call. + * + * @details + * Same as @c dyad_dtl_flux_rpc_pack() but additionally packs @p offset and + * @p length, producing @c {"upath": "", "offset": , "length": }. + * + * @param[in] ctx DYAD context. + * @param[in] upath Relative path of the file to fetch from. + * @param[in] producer_rank Flux rank of the producer broker. Not embedded + * in the payload for Flux RPC. + * @param[in] offset Starting byte offset of the requested range. + * @param[in] length Number of bytes requested. + * @param[out] packed_obj Set to the allocated JSON object on success. + * + * @return @c dyad_rc_t return code: + * @retval DYAD_RC_OK The JSON object was created successfully. + * @retval DYAD_RC_BADPACK @c json_pack() failed to create the object. + */ +dyad_rc_t dyad_dtl_flux_rpc_pack_range (const dyad_ctx_t *ctx, + const char *restrict upath, + uint32_t producer_rank, + size_t offset, + size_t length, + json_t **restrict packed_obj); + +/** + * @brief Unpacks a byte-range fetch request from an incoming Flux RPC message. + * + * @details + * Same as @c dyad_dtl_flux_rpc_unpack() but additionally extracts @p offset + * and @p length from the JSON payload. + * + * @param[in] ctx DYAD context. + * @param[in] msg Incoming Flux RPC message containing the JSON payload. + * @param[out] upath Relative path of the requested file. Valid for the + * lifetime of @p msg. + * @param[out] offset Starting byte offset of the requested range. + * @param[out] length Number of bytes requested. + * + * @return @c dyad_rc_t return code: + * @retval DYAD_RC_OK Unpacking succeeded. + * @retval DYAD_RC_BADUNPACK @c flux_request_unpack() failed to extract the + * fields from the message. + */ +dyad_rc_t dyad_dtl_flux_rpc_unpack_range (const dyad_ctx_t *ctx, + const flux_msg_t *msg, + char **upath, + size_t *offset, + size_t *length); + /** * @brief Sends the initial RPC acknowledgement from the service to the consumer. * diff --git a/src/dyad/dtl/margo_dtl.c b/src/dyad/dtl/margo_dtl.c index 05106597..b8f7608a 100644 --- a/src/dyad/dtl/margo_dtl.c +++ b/src/dyad/dtl/margo_dtl.c @@ -373,6 +373,8 @@ dyad_rc_t dyad_dtl_margo_init (const dyad_ctx_t *ctx, ctx->dtl_handle->rpc_pack = dyad_dtl_margo_rpc_pack; ctx->dtl_handle->rpc_unpack = dyad_dtl_margo_rpc_unpack; + ctx->dtl_handle->rpc_pack_range = dyad_dtl_margo_rpc_pack_range; + ctx->dtl_handle->rpc_unpack_range = dyad_dtl_margo_rpc_unpack_range; ctx->dtl_handle->rpc_respond = dyad_dtl_margo_rpc_respond; ctx->dtl_handle->rpc_recv_response = dyad_dtl_margo_rpc_recv_response; ctx->dtl_handle->get_buffer = dyad_dtl_margo_get_buffer; @@ -414,7 +416,24 @@ dyad_rc_t dyad_dtl_margo_rpc_pack (const dyad_ctx_t *ctx, // send my address (me as consumer and margo server) char addr_str[128]; size_t addr_str_size = 128; - margo_addr_to_string (margo_handle->mid, addr_str, &addr_str_size, margo_handle->local_addr); + { + hg_return_t addr_str_ret = margo_addr_to_string (margo_handle->mid, + addr_str, + &addr_str_size, + margo_handle->local_addr); + if (addr_str_ret != HG_SUCCESS) { + DYAD_LOG_ERROR (ctx, "[MARGO DTL] margo_addr_to_string failed: %d", (int)addr_str_ret); + rc = DYAD_RC_BADPACK; + goto dtl_margo_rpc_pack_region_finish; + } + } + // HG_Addr_to_string() sets addr_str_size to the string length *including* + // the null terminator; Jansson's "s%" format wants the length *excluding* + // it, so passing addr_str_size unadjusted packs one extra (the null) + // byte as string content. + if (addr_str_size > 0) { + addr_str_size -= 1; + } *packed_obj = json_pack ("{s:s, s:i, s:i, s:s%}", "upath", // s:s @@ -486,6 +505,127 @@ dtl_margo_rpc_unpack_region_finish:; return rc; } +dyad_rc_t dyad_dtl_margo_rpc_pack_range (const dyad_ctx_t *ctx, + const char *upath, + uint32_t producer_rank, + size_t offset, + size_t length, + json_t **packed_obj) +{ + DYAD_C_FUNCTION_START (); + dyad_rc_t rc = DYAD_RC_OK; + + dyad_dtl_margo_t *margo_handle = ctx->dtl_handle->private_dtl.margo_dtl_handle; + + // send my address (me as consumer and margo server) + char addr_str[128]; + size_t addr_str_size = 128; + { + hg_return_t addr_str_ret = margo_addr_to_string (margo_handle->mid, + addr_str, + &addr_str_size, + margo_handle->local_addr); + if (addr_str_ret != HG_SUCCESS) { + DYAD_LOG_ERROR (ctx, "[MARGO DTL] margo_addr_to_string failed: %d", (int)addr_str_ret); + rc = DYAD_RC_BADPACK; + goto dtl_margo_rpc_pack_range_region_finish; + } + } + // HG_Addr_to_string() sets addr_str_size to the string length *including* + // the null terminator; Jansson's "s%" format wants the length *excluding* + // it, so passing addr_str_size unadjusted packs one extra (the null) + // byte as string content. + if (addr_str_size > 0) { + addr_str_size -= 1; + } + + *packed_obj = json_pack ("{s:s, s:i, s:i, s:s%, s:I, s:I}", + "upath", // s:s + upath, + "tag_prod", // s:i + (int)producer_rank, + "pid_cons", // s:s + ctx->pid, + "addr", // s:s% + addr_str, + addr_str_size, + "offset", // s:I + (json_int_t)offset, + "length", // s:I + (json_int_t)length); + + if (*packed_obj == NULL) { + DYAD_LOG_ERROR (ctx, "Could not pack upath/offset/length and Margo address for RPC."); + rc = DYAD_RC_BADPACK; + goto dtl_margo_rpc_pack_range_region_finish; + } + + DYAD_LOG_DEBUG (ctx, + "[MARGO DTL] pack/send ranged margo sever addr: %s, %ld.", + addr_str, + addr_str_size); + +dtl_margo_rpc_pack_range_region_finish:; + DYAD_C_FUNCTION_END (); + return rc; +} + +dyad_rc_t dyad_dtl_margo_rpc_unpack_range (const dyad_ctx_t *ctx, + const flux_msg_t *msg, + char **upath, + size_t *offset, + size_t *length) +{ + DYAD_C_FUNCTION_START (); + dyad_rc_t rc = DYAD_RC_OK; + + uint64_t tag_prod = 0; + uint64_t pid = 0; + char *addr_str = NULL; + size_t addr_str_size = 0; + json_int_t offset_val = 0; + json_int_t length_val = 0; + int errcode; + + dyad_dtl_margo_t *margo_handle = ctx->dtl_handle->private_dtl.margo_dtl_handle; + + // retrive and decode the consumer margo-server address, plus the + // requested byte range + errcode = flux_request_unpack (msg, + NULL, + "{s:s, s:i, s:i, s:s%, s:I, s:I}", + "upath", // s:s + upath, + "tag_prod", // s:i + &tag_prod, + "pid_cons", // s:i + &pid, + "addr", // s:s% + &addr_str, + &addr_str_size, + "offset", // s:I + &offset_val, + "length", // s:I + &length_val); + if (errcode < 0) { + DYAD_LOG_ERROR (ctx, "Could not unpack ranged Flux message from consumer!\n"); + rc = DYAD_RC_BADUNPACK; + goto dtl_margo_rpc_unpack_range_region_finish; + } + *offset = (size_t)offset_val; + *length = (size_t)length_val; + + DYAD_LOG_DEBUG (ctx, + "[MARGO DTL] recv/unpack ranged margo sever addr: %s, %ld.", + addr_str, + addr_str_size); + margo_addr_lookup (margo_handle->mid, addr_str, &margo_handle->remote_addr); + +dtl_margo_rpc_unpack_range_region_finish:; + DYAD_C_FUNCTION_END (); + return rc; +} + dyad_rc_t dyad_dtl_margo_rpc_respond (const dyad_ctx_t *ctx, const flux_msg_t *orig_msg) { DYAD_C_FUNCTION_START (); @@ -514,9 +654,10 @@ dyad_rc_t dyad_dtl_margo_send (const dyad_ctx_t *ctx, void *buf, size_t buflen) dyad_rc_t rc = DYAD_RC_OK; hg_return_t ret = HG_SUCCESS; - DYAD_LOG_DEBUG (ctx, "[MARGO DTL] margo_send is called, buflen: %ld.", buflen); dyad_dtl_margo_t *margo_handle = ctx->dtl_handle->private_dtl.margo_dtl_handle; + DYAD_LOG_DEBUG (ctx, "[MARGO DTL] margo_send is called, buflen: %ld.", buflen); + hg_size_t segment_sizes[1] = {buflen}; void *segment_ptrs[1] = {buf}; hg_bulk_t local_bulk; @@ -541,10 +682,7 @@ dyad_rc_t dyad_dtl_margo_send (const dyad_ctx_t *ctx, void *buf, size_t buflen) args.bulk = local_bulk; // send a message to the consumer, notifying it that my data is ready - ret = margo_create (margo_handle->mid, - margo_handle->remote_addr, - margo_handle->sendrecv_rpc_id, - &mh); + ret = margo_create (margo_handle->mid, margo_handle->remote_addr, margo_handle->sendrecv_rpc_id, &mh); if (ret != HG_SUCCESS) { DYAD_LOG_ERROR (ctx, "margo_create failed: %d", (int)ret); goto margo_error; diff --git a/src/dyad/dtl/margo_dtl.h b/src/dyad/dtl/margo_dtl.h index 6e6b86c2..d74af851 100644 --- a/src/dyad/dtl/margo_dtl.h +++ b/src/dyad/dtl/margo_dtl.h @@ -202,6 +202,65 @@ dyad_rc_t dyad_dtl_margo_rpc_pack (const dyad_ctx_t *ctx, */ dyad_rc_t dyad_dtl_margo_rpc_unpack (const dyad_ctx_t *ctx, const flux_msg_t *msg, char **upath); +/** + * @brief Packs a byte-range fetch request into a JSON object for a Margo RPC call. + * + * @details + * Same as @c dyad_dtl_margo_rpc_pack() but additionally packs @p offset and + * @p length. The Mercury/RDMA data-transfer layer (@c margo_rpc_in_t, + * @c data_ready_rpc(), @c dyad_dtl_margo_send()/@c dyad_dtl_margo_recv()) + * needs no range-awareness at all: @p offset only matters to the + * producer's Flux-side callback, which uses it to decide what to + * @c pread() from the file *before* ever registering a bulk buffer. By + * the time @c dyad_dtl_margo_send() is called, its buffer already holds + * just the requested sub-range, so the existing bulk-create/RDMA-pull + * path moves it identically to a whole-file transfer. + * + * @param[in] ctx DYAD context. + * @param[in] upath Relative path of the file to fetch from. + * @param[in] producer_rank Flux rank of the producer broker. + * @param[in] offset Starting byte offset of the requested range. + * @param[in] length Number of bytes requested. + * @param[out] packed_obj Set to the allocated JSON object on success. + * + * @return @c dyad_rc_t return code: + * @retval DYAD_RC_OK The JSON object was created successfully. + * @retval DYAD_RC_BADPACK @c json_pack() failed to create the object. + */ +dyad_rc_t dyad_dtl_margo_rpc_pack_range (const dyad_ctx_t *ctx, + const char *upath, + uint32_t producer_rank, + size_t offset, + size_t length, + json_t **packed_obj); + +/** + * @brief Unpacks a byte-range fetch request from an incoming Flux RPC message + * and resolves the consumer's Margo address. + * + * @details + * Same as @c dyad_dtl_margo_rpc_unpack() but additionally extracts @p offset + * and @p length from the JSON payload. + * + * @param[in] ctx DYAD context. + * @param[in] msg Incoming Flux RPC message containing the JSON payload + * packed by @c dyad_dtl_margo_rpc_pack_range(). + * @param[out] upath Relative path of the requested file. Valid for the + * lifetime of @p msg. + * @param[out] offset Starting byte offset of the requested range. + * @param[out] length Number of bytes requested. + * + * @return @c dyad_rc_t return code: + * @retval DYAD_RC_OK Unpacking and address resolution succeeded. + * @retval DYAD_RC_BADUNPACK @c flux_request_unpack() failed to extract the + * required fields from the message. + */ +dyad_rc_t dyad_dtl_margo_rpc_unpack_range (const dyad_ctx_t *ctx, + const flux_msg_t *msg, + char **upath, + size_t *offset, + size_t *length); + /** * @brief Sends the initial RPC acknowledgement from the service to the consumer. * diff --git a/src/dyad/dtl/ucx_dtl.c b/src/dyad/dtl/ucx_dtl.c index 0c25d31b..6121b774 100644 --- a/src/dyad/dtl/ucx_dtl.c +++ b/src/dyad/dtl/ucx_dtl.c @@ -815,6 +815,13 @@ dyad_rc_t dyad_dtl_ucx_init (const dyad_ctx_t *ctx, ctx->dtl_handle->rpc_pack = dyad_dtl_ucx_rpc_pack; ctx->dtl_handle->rpc_unpack = dyad_dtl_ucx_rpc_unpack; + // Byte-range fetch (dyad_consume_range()) is only implemented for + // FLUX_RPC and MARGO. Explicitly NULL here (ctx->dtl_handle is malloc'd, + // not zero-initialized) rather than leaving these uninitialized, even + // though dyad_consume_range() gates on ctx->dtl_handle->mode before ever + // dereferencing them. + ctx->dtl_handle->rpc_pack_range = NULL; + ctx->dtl_handle->rpc_unpack_range = NULL; ctx->dtl_handle->rpc_respond = dyad_dtl_ucx_rpc_respond; ctx->dtl_handle->rpc_recv_response = dyad_dtl_ucx_rpc_recv_response; ctx->dtl_handle->get_buffer = dyad_dtl_ucx_get_buffer; diff --git a/src/dyad/service/dyad_exe.c b/src/dyad/service/dyad_exe.c index ffb92117..1c8d81eb 100644 --- a/src/dyad/service/dyad_exe.c +++ b/src/dyad/service/dyad_exe.c @@ -17,12 +17,13 @@ struct dyad_cli_args { char* prod_managed_path; char* dtl_mode; + char* origin_path; bool debug; }; typedef struct dyad_cli_args dyad_cli_args_t; // global variable to store parsed command line arguments -static dyad_cli_args_t cli_args = {NULL, NULL, false}; +static dyad_cli_args_t cli_args = {NULL, NULL, NULL, false}; typedef enum { INVALID_ACTION = -1, ACT_START = 0, ACT_STOP = 1, N_ACT = 2 } action_e; @@ -61,7 +62,12 @@ static void usage (int status) " -e, --error_log: Specify the file into which to redirect\n" " error logging. Does nothing if DYAD was\n" " not configured with '-DDYAD_LOGGER=PRINTF'\n" - " Need a filename as an argument.\n"); + " Need a filename as an argument.\n" + " -o, --origin_path: Fallback source path (e.g. on the parallel\n" + " file system) used to lazily fill missing\n" + " spans of a managed file on demand. Need a\n" + " path as an argument. Omit to require files\n" + " be fully staged upfront (default).\n"); printf ( "\n" "Command options for \"stop\":\n" @@ -82,8 +88,9 @@ static void parse_cmd_arguments (int argc, char** argv) {"producer_managed_path", required_argument, 0, 'p'}, {"info_log", required_argument, 0, 'i'}, {"error_log", required_argument, 0, 'e'}, + {"origin_path", required_argument, 0, 'o'}, {0, 0, 0, 0}}; - static char* short_options = "hdm:i:e:p:"; + static char* short_options = "hdm:i:e:p:o:"; while ((ch = getopt_long (argc, argv, short_options, long_options, &optidx)) >= 0) { switch (ch) { @@ -104,6 +111,9 @@ static void parse_cmd_arguments (int argc, char** argv) break; case 'e': break; + case 'o': + cli_args.origin_path = strdup (optarg); + break; case '?': // getopt_long already printed an error message. break; @@ -140,31 +150,29 @@ int dyad_start_service (dyad_cli_args_t* cli_args) char dyad_module_path[PATH_MAX + 1] = {0}; sprintf (dyad_module_path, "%s/dyad.so", DYAD_INSTALL_LIBDIR); + // Fixed-size buffer sized for the largest possible combination of + // flags below; grow this if another optional flag is added. + char* argv[16]; + int i = 0; + argv[i++] = "flux"; + argv[i++] = "exec"; + argv[i++] = "-r"; + argv[i++] = "all"; + argv[i++] = "flux"; + argv[i++] = "module"; + argv[i++] = "load"; + argv[i++] = dyad_module_path; if (cli_args->dtl_mode != NULL) { - char* const argv[] = {"flux", - "exec", - "-r", - "all", - "flux", - "module", - "load", - dyad_module_path, - "--mode", - cli_args->dtl_mode, - cli_args->prod_managed_path, - NULL}; - return fork_exec_wait (argv); + argv[i++] = "--mode"; + argv[i++] = cli_args->dtl_mode; + } + if (cli_args->origin_path != NULL) { + argv[i++] = "--origin_path"; + argv[i++] = cli_args->origin_path; } - char* const argv[] = {"flux", - "exec", - "-r", - "all", - "flux", - "module", - "load", - dyad_module_path, - cli_args->prod_managed_path, - NULL}; + argv[i++] = cli_args->prod_managed_path; + argv[i++] = NULL; + return fork_exec_wait (argv); } diff --git a/src/dyad/service/flux_module/dyad.c b/src/dyad/service/flux_module/dyad.c index 9b485ade..5d116963 100644 --- a/src/dyad/service/flux_module/dyad.c +++ b/src/dyad/service/flux_module/dyad.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include // clang-format on @@ -94,7 +95,7 @@ typedef struct dyad_mod_ctx { dyad_ctx_t *ctx; ///< DYAD context for this module instance. } dyad_mod_ctx_t; -const struct dyad_mod_ctx dyad_mod_ctx_default = {NULL, NULL}; +const struct dyad_mod_ctx dyad_mod_ctx_default = {0}; static void dyad_mod_fini (void) __attribute__ ((destructor)); @@ -246,8 +247,10 @@ getctx_error:; #if DYAD_PERFFLOW __attribute__ ((annotate ("@critical_path()"))) #endif -static void -dyad_fetch_request_cb (flux_t *h, flux_msg_handler_t *w, const flux_msg_t *msg, void *arg) +static void dyad_fetch_request_cb (flux_t *h, + flux_msg_handler_t *w, + const flux_msg_t *msg, + void *arg) { DYAD_C_FUNCTION_START (); dyad_mod_ctx_t *mod_ctx = get_mod_ctx (h); @@ -426,22 +429,255 @@ end_fetch_cb:; return; } +/** + * @brief Flux message handler callback that serves a byte range of a file + * to a consumer via RPC. + * + * @details + * Registered as the handler for @c DYAD_DTL_RPC_RANGE_NAME requests in + * @c htab. Parallel to @c dyad_fetch_request_cb(), for @c dyad_consume_range() + * requests (FLUX_RPC and MARGO DTL modes only) — the existing whole-file + * handler is untouched. Differs only in: + * - Unpacks @c upath, @c offset, and @c length via @c rpc_unpack_range() + * instead of just @c upath. + * - Reads only @c [offset, offset+length) from the file via @c pread() + * (clamped against the real file size) instead of the whole file via + * @c fstat()-sized sequential @c read(). + * - Allocates/sends a buffer sized to @c length instead of the whole + * file size. No UCX file-size-prefix handling, since UCX does not + * implement @c rpc_unpack_range() (out of scope for byte-range fetch). + * + * @param[in] h Flux handle for the broker. + * @param[in] w Flux message handler (unused directly). + * @param[in] msg Incoming Flux RPC message containing @c upath/@c offset/ + * @c length packed by the consumer. + * @param[in] arg Auxiliary argument (the Flux handle, passed as @c void* + * from @c flux_msg_handler_addvec()). + */ +#if DYAD_PERFFLOW +__attribute__ ((annotate ("@critical_path()"))) +#endif +static void dyad_fetch_range_request_cb (flux_t *h, + flux_msg_handler_t *w, + const flux_msg_t *msg, + void *arg) +{ + DYAD_C_FUNCTION_START (); + dyad_mod_ctx_t *mod_ctx = get_mod_ctx (h); + DYAD_LOG_DEBUG (mod_ctx->ctx, "DYAD_MOD: Launched callback for %s", DYAD_DTL_RPC_RANGE_NAME); + ssize_t inlen = 0l; + char *inbuf = NULL; + int fd = -1; + uint32_t userid = 0u; + char *upath = NULL; + char fullpath[PATH_MAX + 1] = {'\0'}; + int saved_errno = errno; + size_t offset = 0; + size_t length = 0; + ssize_t file_size = 0l; + ssize_t range_len = 0l; + dyad_rc_t rc = 0; + struct flock shared_lock; + if (!flux_msg_is_streaming (msg)) { + errno = EPROTO; + goto fetch_range_error_wo_flock; + } + + if (flux_msg_get_userid (msg, &userid) < 0) + goto fetch_range_error_wo_flock; + + DYAD_LOG_DEBUG (mod_ctx->ctx, "DYAD_MOD: unpacking ranged RPC message"); + + rc = mod_ctx->ctx->dtl_handle->rpc_unpack_range (mod_ctx->ctx, msg, &upath, &offset, &length); + + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: Could not unpack ranged message from client"); + errno = EPROTO; + goto fetch_range_error_wo_flock; + } + DYAD_C_FUNCTION_UPDATE_STR ("upath", upath); + DYAD_LOG_DEBUG (mod_ctx->ctx, + "DYAD_MOD: requested user_path: %s, offset: %zu, length: %zu", + upath, + offset, + length); + DYAD_LOG_DEBUG (mod_ctx->ctx, "DYAD_MOD: sending initial response to consumer"); + + rc = mod_ctx->ctx->dtl_handle->rpc_respond (mod_ctx->ctx, msg); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: Could not send primary RPC response to client"); + goto fetch_range_error_wo_flock; + } + + strncpy (fullpath, mod_ctx->ctx->prod_managed_path, PATH_MAX - 1); + concat_str (fullpath, upath, "/", PATH_MAX); + DYAD_C_FUNCTION_UPDATE_STR ("fullpath", fullpath); + + if (mod_ctx->ctx->origin_path != NULL) { + char origin_fullpath[PATH_MAX + 1] = {'\0'}; + strncpy (origin_fullpath, mod_ctx->ctx->origin_path, PATH_MAX - 1); + concat_str (origin_fullpath, upath, "/", PATH_MAX); + rc = dyad_range_cache_ensure (mod_ctx->ctx, fullpath, origin_fullpath, offset, length); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (mod_ctx->ctx, + "DYAD_MOD: dyad_range_cache_ensure failed for file \"%s\".", + fullpath); + errno = EIO; + goto fetch_range_error_wo_flock; + } + } + + DYAD_LOG_DEBUG (mod_ctx->ctx, "DYAD_MOD: Reading file %s for ranged transfer", fullpath); + fd = open (fullpath, O_RDONLY); + + if (fd < 0) { + DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: Failed to open file \"%s\".", fullpath); + goto fetch_range_error_wo_flock; + } + rc = dyad_shared_flock (mod_ctx->ctx, fd, &shared_lock); + if (DYAD_IS_ERROR (rc)) { + goto fetch_range_error; + } + file_size = get_file_size (fd); + // Clamp against the real file size for safety -- offset/length come + // from the consumer's own index and should already be valid, but a + // stale/corrupt index must not turn into an out-of-bounds pread(). + if ((ssize_t)offset >= file_size) { + range_len = 0; + } else if ((ssize_t)(offset + length) > file_size) { + range_len = file_size - (ssize_t)offset; + } else { + range_len = (ssize_t)length; + } + DYAD_LOG_DEBUG (mod_ctx->ctx, + "DYAD_MOD: file %s has size %zd, serving range [%zu, %zu)", + fullpath, + file_size, + offset, + offset + (size_t)range_len); + rc = mod_ctx->ctx->dtl_handle->get_buffer (mod_ctx->ctx, (size_t)range_len, (void **)&inbuf); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: Could not allocate DTL buffer for ranged fetch"); + goto fetch_range_error; + } + if (range_len > 0l) { + if (range_len < DYAD_POSIX_TRANSFER_GRANULARITY) { + inlen = pread (fd, inbuf, (size_t)range_len, (off_t)offset); + } else { + ssize_t read_data = 0; + int granularity = DYAD_POSIX_TRANSFER_GRANULARITY; + while (read_data < range_len) { + ssize_t read_size = + (range_len - read_data) > granularity ? granularity : (range_len - read_data); + inlen = pread (fd, + inbuf + read_data, + (size_t)read_size, + (off_t)(offset + (size_t)read_data)); + DYAD_LOG_DEBUG (mod_ctx->ctx, + "DYAD_MOD: reading range of file %s with bytes %zd of %zd", + fullpath, + read_size, + inlen); + if (inlen <= 0) { + DYAD_LOG_ERROR (mod_ctx->ctx, + "DYAD_MOD: Failed to load range of file \"%s\" only read %zd " + "of %zd of %zd. with code %d:%s.", + fullpath, + inlen, + read_size, + range_len, + errno, + strerror (errno)); + goto fetch_range_error; + } + read_data += inlen; + } + inlen = read_data; + } + if (inlen != range_len) { + DYAD_LOG_ERROR (mod_ctx->ctx, + "DYAD_MOD: Failed to load range of file \"%s\" only read %zd of %zd. " + "with code %d:%s.", + fullpath, + inlen, + range_len, + errno, + strerror (errno)); + goto fetch_range_error; + } + } + DYAD_C_FUNCTION_UPDATE_INT ("range_len", range_len); + dyad_release_flock (mod_ctx->ctx, fd, &shared_lock); + close (fd); + DYAD_LOG_DEBUG (mod_ctx->ctx, "DYAD_MOD: Establish DTL connection with consumer"); + rc = mod_ctx->ctx->dtl_handle->establish_connection (mod_ctx->ctx); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: Could not establish DTL connection with client"); + errno = ECONNREFUSED; + goto fetch_range_error_wo_flock; + } + DYAD_LOG_DEBUG (mod_ctx->ctx, "DYAD_MOD: Send file range to consumer with DTL"); + rc = mod_ctx->ctx->dtl_handle->send (mod_ctx->ctx, inbuf, (size_t)range_len); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: Could not send range data to client via DTL\n"); + errno = ECOMM; + goto fetch_range_error_wo_flock; + } + DYAD_LOG_DEBUG (mod_ctx->ctx, "DYAD_MOD: Close DTL connection with consumer"); + mod_ctx->ctx->dtl_handle->close_connection (mod_ctx->ctx); + mod_ctx->ctx->dtl_handle->return_buffer (mod_ctx->ctx, (void **)&inbuf); + + DYAD_LOG_DEBUG (mod_ctx->ctx, + "DYAD_MOD: Close RPC message stream with an ENODATA (%d) message", + ENODATA); + if (flux_respond_error (h, msg, ENODATA, NULL) < 0) { + DYAD_LOG_DEBUG (mod_ctx->ctx, + "DYAD_MOD: %s: flux_respond_error with ENODATA failed\n", + __func__); + } + DYAD_LOG_DEBUG (mod_ctx->ctx, + "DYAD_MOD: Finished %s module invocation\n", + DYAD_DTL_RPC_RANGE_NAME); + goto end_fetch_range_cb; + +fetch_range_error:; + dyad_release_flock (mod_ctx->ctx, fd, &shared_lock); + close (fd); + +fetch_range_error_wo_flock:; + DYAD_LOG_ERROR (mod_ctx->ctx, + "DYAD_MOD: Close RPC message stream with an error (errno = %d)\n", + errno); + if (flux_respond_error (h, msg, errno, NULL) < 0) { + DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: %s: flux_respond_error", __func__); + } + errno = saved_errno; + DYAD_C_FUNCTION_END (); + return; + +end_fetch_range_cb:; + errno = saved_errno; + DYAD_C_FUNCTION_END (); + return; +} + /** * @brief Flux message handler table for the DYAD module. * * @details - * Registers @c dyad_fetch_request_cb as the handler for all incoming - * @c FLUX_MSGTYPE_REQUEST messages addressed to @c DYAD_DTL_RPC_NAME. - * This is the single RPC endpoint exposed by the DYAD module — consumers - * send file fetch requests to this name on the producer's broker, and the - * reactor dispatches them to @c dyad_fetch_request_cb. - * @c DYAD_DTL_RPC_NAME is defined as "dyad.fetch" + * Registers @c dyad_fetch_request_cb for whole-file @c DYAD_DTL_RPC_NAME + * ("dyad.fetch") requests and @c dyad_fetch_range_request_cb for byte-range + * @c DYAD_DTL_RPC_RANGE_NAME ("dyad.fetch_range") requests. Consumers send + * fetch requests to whichever topic matches the API they called + * (@c dyad_consume()/@c dyad_consume_w_metadata() vs @c dyad_consume_range()), + * and the reactor dispatches them to the corresponding handler. * * Passed to @c flux_msg_handler_addvec() in @c mod_main() and terminated * by @c FLUX_MSGHANDLER_TABLE_END as required by the Flux API. */ static const struct flux_msg_handler_spec htab[] = {{FLUX_MSGTYPE_REQUEST, DYAD_DTL_RPC_NAME, dyad_fetch_request_cb, 0}, + {FLUX_MSGTYPE_REQUEST, DYAD_DTL_RPC_RANGE_NAME, dyad_fetch_range_request_cb, 0}, FLUX_MSGHANDLER_TABLE_END}; static void show_help (void) @@ -462,6 +698,12 @@ static void show_help (void) " error logging. Does nothing if DYAD was\n" " not configured with '-DDYAD_LOGGER=PRINTF'\n" " Need a filename as an argument.\n"); + DYAD_LOG_STDOUT ( + " -o, --origin_path: Fallback source path (e.g. on the parallel\n" + " file system) used to lazily fill missing\n" + " spans of a managed file on demand. Need a\n" + " path as an argument. Omit to require files\n" + " be fully staged upfront (default).\n"); } /** @@ -470,6 +712,7 @@ static void show_help (void) struct opt_parse_out { const char *prod_managed_path; ///< Producer-managed directory path, or @c NULL. const char *dtl_mode; ///< DTL mode string, or @c NULL for default. + const char *origin_path; ///< Fallback origin path, or @c NULL to disable. bool debug; ///< Whether debug logging is enabled. bool showed_help; ///< Whether @c -h was passed and help was shown. }; @@ -492,6 +735,8 @@ typedef struct opt_parse_out opt_parse_out_t; * - @c -m / @c --mode Sets @c opt->dtl_mode. * - @c -i / @c --info_log Redirects info log output to a per-rank file. * - @c -e / @c --error_log Redirects error log output to a per-rank file. + * - @c -o / @c --origin_path Sets @c opt->origin_path (lazy origin-backed + * range cache fallback source). * * Any remaining non-option argument is treated as the producer-managed * directory path and stored in @c opt->prod_managed_path. @@ -552,10 +797,11 @@ int opt_parse (opt_parse_out_t *restrict opt, {"mode", required_argument, 0, 'm'}, {"info_log", required_argument, 0, 'i'}, {"error_log", required_argument, 0, 'e'}, + {"origin_path", required_argument, 0, 'o'}, {0, 0, 0, 0}}; int c; - while ((c = getopt_long (_argc, _argv, "hdm:i:e:", long_options, NULL)) != -1) { + while ((c = getopt_long (_argc, _argv, "hdm:i:e:o:", long_options, NULL)) != -1) { switch (c) { case 'h': show_help (); @@ -586,6 +832,10 @@ int opt_parse (opt_parse_out_t *restrict opt, sprintf (err_file_name, "%s_%d.err", optarg, broker_rank); #endif // DYAD_LOGGER_NO_LOG break; + case 'o': + DYAD_LOG_STDERR ("DYAD_MOD: 'origin_path' option -o with value `%s'\n", optarg); + opt->origin_path = optarg; + break; case '?': /* getopt_long already printed an error message. */ break; @@ -684,6 +934,13 @@ dyad_rc_t dyad_module_ctx_init (const opt_parse_out_t *opt, flux_t *h) opt->dtl_mode); } + if (opt->origin_path) { + setenv (DYAD_PATH_ORIGIN_ENV, opt->origin_path, 1); + DYAD_LOG_STDOUT ("DYAD_MOD: origin_path option set. Setting env %s=%s\n", + DYAD_PATH_ORIGIN_ENV, + opt->origin_path); + } + char *kvs_namespace = getenv ("DYAD_KVS_NAMESPACE"); if (kvs_namespace != NULL) { DYAD_LOG_STDOUT ("DYAD_MOD: DYAD_KVS_NAMESPACE is set to `%s'\n", kvs_namespace); @@ -793,7 +1050,7 @@ DYAD_DLL_EXPORTED int mod_main (flux_t *h, int argc, char **argv) mod_ctx = get_mod_ctx (h); - opt_parse_out_t opt = {NULL, NULL, false, false}; + opt_parse_out_t opt = {NULL, NULL, NULL, false, false}; if (DYAD_IS_ERROR (opt_parse (&opt, broker_rank, argc, argv))) { DYAD_LOG_STDERR ("DYAD_MOD: Cannot parse command line arguments\n"); diff --git a/src/dyad/stream/CMakeLists.txt b/src/dyad/stream/CMakeLists.txt index 6d702381..cb7ca263 100644 --- a/src/dyad/stream/CMakeLists.txt +++ b/src/dyad/stream/CMakeLists.txt @@ -6,7 +6,8 @@ set(DYAD_FSTREAM_PRIVATE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/dy ${CMAKE_CURRENT_SOURCE_DIR}/../utils/utils.h) set(DYAD_FSTREAM_PUBLIC_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/dyad/stream/dyad_stream_api.hpp ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/dyad/stream/dyad_params.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/dyad/stream/dyad_stream_core.hpp) + ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/dyad/stream/dyad_stream_core.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/dyad/common/dyad_cache.h) add_library(${PROJECT_NAME}_fstream SHARED ${DYAD_FSTREAM_SRC} diff --git a/src/dyad/stream/dyad_stream_core.cpp b/src/dyad/stream/dyad_stream_core.cpp index 172737de..00f5c8fd 100644 --- a/src/dyad/stream/dyad_stream_core.cpp +++ b/src/dyad/stream/dyad_stream_core.cpp @@ -145,7 +145,12 @@ void dyad_stream_core::init (const dyad_params &p) p.m_relative_to_managed_path, dyad_dtl_mode_name[static_cast (p.m_dtl_mode)], DYAD_COMM_RECV, - NULL); + NULL, + p.m_cache_capacity_bytes, + p.m_cache_policy.c_str (), + p.m_cache_low_watermark, + p.m_cache_grace_period_sec, + p.m_origin_path.empty () ? nullptr : p.m_origin_path.c_str ()); DYAD_CPP_FUNCTION (); m_ctx = m_ctx_mutable = dyad_ctx_get (); if (!DYAD_IS_ERROR (rc) && m_ctx != NULL) { diff --git a/src/dyad/utils/CMakeLists.txt b/src/dyad/utils/CMakeLists.txt index 1d8ec2e9..b51b1acc 100644 --- a/src/dyad/utils/CMakeLists.txt +++ b/src/dyad/utils/CMakeLists.txt @@ -1,10 +1,12 @@ add_subdirectory(base64) set(DYAD_UTILS_SRC ${CMAKE_CURRENT_SOURCE_DIR}/utils.c - ${CMAKE_CURRENT_SOURCE_DIR}/read_all.c) + ${CMAKE_CURRENT_SOURCE_DIR}/read_all.c + ${CMAKE_CURRENT_SOURCE_DIR}/range_cache.c) set(DYAD_UTILS_PRIVATE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/utils.h ${CMAKE_CURRENT_SOURCE_DIR}/../common/dyad_structures_int.h - ${CMAKE_CURRENT_SOURCE_DIR}/read_all.h) + ${CMAKE_CURRENT_SOURCE_DIR}/read_all.h + ${CMAKE_CURRENT_SOURCE_DIR}/range_cache.h) set(DYAD_UTILS_PUBLIC_HEADERS) set(DYAD_MURMUR3_SRC ${CMAKE_CURRENT_SOURCE_DIR}/murmur3.c) diff --git a/src/dyad/utils/range_cache.c b/src/dyad/utils/range_cache.c new file mode 100644 index 00000000..3df17fcf --- /dev/null +++ b/src/dyad/utils/range_cache.c @@ -0,0 +1,307 @@ +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif // _GNU_SOURCE + +// clang-format off +#include +#include +#include +#include +// clang-format on + +#if defined(__cplusplus) +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include + +#include +#include +#include + +#define DYAD_RANGE_CACHE_SUFFIX ".dyad_cached" + +static dyad_rc_t range_cache_bitmap_path (const char *local_path, char *out, size_t out_capacity) +{ + if (snprintf (out, out_capacity, "%s%s", local_path, DYAD_RANGE_CACHE_SUFFIX) + >= (int)out_capacity) { + return DYAD_RC_BADBUF; + } + return DYAD_RC_OK; +} + +static inline size_t range_cache_num_blocks (size_t file_size) +{ + return (file_size + DYAD_RANGE_CACHE_BLOCK_SIZE - 1ul) / DYAD_RANGE_CACHE_BLOCK_SIZE; +} + +static inline size_t range_cache_bitmap_bytes (size_t num_blocks) +{ + return (num_blocks + 7ul) / 8ul; +} + +static bool range_cache_blocks_set (const uint8_t *bitmap, size_t block_start, size_t block_end) +{ + size_t b = 0ul; + for (b = block_start; b <= block_end; b++) { + if ((bitmap[b / 8ul] & (uint8_t)(1u << (b % 8ul))) == 0) { + return false; + } + } + return true; +} + +static void range_cache_set_blocks (uint8_t *bitmap, size_t block_start, size_t block_end) +{ + size_t b = 0ul; + for (b = block_start; b <= block_end; b++) { + bitmap[b / 8ul] |= (uint8_t)(1u << (b % 8ul)); + } +} + +// Reads the block-aligned span [block_start, block_end] from origin_path and +// writes it into local_path at the same offset. Called with no bitmap lock +// held -- see the "no lock held" comment in dyad_range_cache_ensure() for +// why concurrent callers racing on an overlapping span is safe. +static dyad_rc_t range_cache_fetch_span (const dyad_ctx_t *ctx, + const char *local_path, + const char *origin_path, + size_t block_start, + size_t block_end, + size_t origin_size) +{ + dyad_rc_t rc = DYAD_RC_OK; + int origin_fd = -1; + int local_fd = -1; + void *span_buf = NULL; + size_t span_off = block_start * DYAD_RANGE_CACHE_BLOCK_SIZE; + size_t span_end = (block_end + 1ul) * DYAD_RANGE_CACHE_BLOCK_SIZE; + size_t span_len = 0ul; + + if (span_end > origin_size) { + span_end = origin_size; + } + if (span_end <= span_off) { + return DYAD_RC_BADFIO; + } + span_len = span_end - span_off; + + span_buf = malloc (span_len); + if (span_buf == NULL) { + return DYAD_RC_SYSFAIL; + } + + origin_fd = open (origin_path, O_RDONLY); + if (origin_fd == -1 + || pread (origin_fd, span_buf, span_len, (off_t)span_off) != (ssize_t)span_len) { + DYAD_LOG_ERROR (ctx, + "DYAD RANGE_CACHE: cannot read span [%zu, %zu) from origin '%s'", + span_off, + span_end, + origin_path); + rc = DYAD_RC_BADFIO; + goto fetch_span_done; + } + + local_fd = open (local_path, O_WRONLY); + if (local_fd == -1 + || pwrite (local_fd, span_buf, span_len, (off_t)span_off) != (ssize_t)span_len) { + DYAD_LOG_ERROR (ctx, + "DYAD RANGE_CACHE: cannot write span [%zu, %zu) into local '%s'", + span_off, + span_end, + local_path); + rc = DYAD_RC_BADFIO; + goto fetch_span_done; + } + rc = DYAD_RC_OK; + +fetch_span_done:; + if (origin_fd != -1) { + close (origin_fd); + } + if (local_fd != -1) { + close (local_fd); + } + free (span_buf); + return rc; +} + +dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, + const char *local_path, + const char *origin_path, + size_t offset, + size_t length) +{ + dyad_rc_t rc = DYAD_RC_OK; + char bitmap_path[PATH_MAX + 1] = {'\0'}; + int bitmap_fd = -1; + int size_fd = -1; + struct flock lock; + struct stat origin_st; + uint8_t *bitmap = NULL; + size_t bitmap_bytes = 0ul; + size_t num_blocks = 0ul; + size_t block_start = 0ul; + size_t block_end = 0ul; + size_t origin_size = 0ul; + bool already_cached = false; + + if (origin_path == NULL || length == 0ul) { + return DYAD_RC_OK; + } + + rc = range_cache_bitmap_path (local_path, bitmap_path, sizeof (bitmap_path)); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, "DYAD RANGE_CACHE: local path '%s' is too long", local_path); + return rc; + } + + block_start = offset / DYAD_RANGE_CACHE_BLOCK_SIZE; + block_end = (offset + length - 1ul) / DYAD_RANGE_CACHE_BLOCK_SIZE; + + bitmap_fd = open (bitmap_path, O_RDWR | O_CREAT, 0644); + if (bitmap_fd == -1) { + DYAD_LOG_ERROR (ctx, "DYAD RANGE_CACHE: cannot open bitmap file '%s'", bitmap_path); + return DYAD_RC_BADFIO; + } + + // Fast path: under a shared lock, check whether the requested span is + // already fully cached. + rc = dyad_shared_flock (ctx, bitmap_fd, &lock); + if (DYAD_IS_ERROR (rc)) { + close (bitmap_fd); + return rc; + } + bitmap_bytes = (size_t)get_file_size (bitmap_fd); + if (bitmap_bytes > block_end / 8ul) { + bitmap = (uint8_t *)malloc (bitmap_bytes); + if (bitmap != NULL && pread (bitmap_fd, bitmap, bitmap_bytes, 0) == (ssize_t)bitmap_bytes + && range_cache_blocks_set (bitmap, block_start, block_end)) { + free (bitmap); + dyad_release_flock (ctx, bitmap_fd, &lock); + close (bitmap_fd); + return DYAD_RC_OK; + } + free (bitmap); + bitmap = NULL; + } + dyad_release_flock (ctx, bitmap_fd, &lock); + + // Miss (or bitmap not yet sized): escalate to an exclusive lock just + // long enough to do the cheap metadata work (stat/ftruncate/re-check), + // then release it *before* doing the slow origin fetch + local write -- + // holding it across that would serialize every concurrent miss on this + // shard (even for disjoint spans) behind whatever process got there + // first, which is exactly what turned occasional slow PFS reads into + // multi-second-to-tens-of-seconds stalls for every other consumer of + // the same shard queued behind the lock. + rc = dyad_excl_flock (ctx, bitmap_fd, &lock); + if (DYAD_IS_ERROR (rc)) { + close (bitmap_fd); + return rc; + } + + if (stat (origin_path, &origin_st) != 0 || origin_st.st_size <= 0) { + DYAD_LOG_ERROR (ctx, "DYAD RANGE_CACHE: cannot stat origin file '%s'", origin_path); + dyad_release_flock (ctx, bitmap_fd, &lock); + close (bitmap_fd); + return DYAD_RC_BADFIO; + } + origin_size = (size_t)origin_st.st_size; + num_blocks = range_cache_num_blocks (origin_size); + bitmap_bytes = range_cache_bitmap_bytes (num_blocks); + + if ((size_t)get_file_size (bitmap_fd) < bitmap_bytes) { + // First touch for this local_path: size both the bitmap and the + // (possibly not-yet-existing) local data file once, up front. + size_fd = open (local_path, O_WRONLY | O_CREAT, 0644); + if (size_fd == -1 || ftruncate (size_fd, (off_t)origin_size) != 0 + || ftruncate (bitmap_fd, (off_t)bitmap_bytes) != 0) { + DYAD_LOG_ERROR (ctx, + "DYAD RANGE_CACHE: cannot size local file/bitmap for '%s'", + local_path); + if (size_fd != -1) { + close (size_fd); + } + dyad_release_flock (ctx, bitmap_fd, &lock); + close (bitmap_fd); + return DYAD_RC_BADFIO; + } + close (size_fd); + } + + bitmap = (uint8_t *)calloc (1ul, bitmap_bytes); + if (bitmap == NULL) { + dyad_release_flock (ctx, bitmap_fd, &lock); + close (bitmap_fd); + return DYAD_RC_SYSFAIL; + } + // A short/zero read here just means no blocks are set yet (e.g. right + // after the ftruncate() above), which range_cache_blocks_set() below + // will correctly treat as a miss since bitmap was calloc'd to all-zero. + pread (bitmap_fd, bitmap, bitmap_bytes, 0); + already_cached = range_cache_blocks_set (bitmap, block_start, block_end); + free (bitmap); + bitmap = NULL; + dyad_release_flock (ctx, bitmap_fd, &lock); + + if (already_cached) { + // Another process filled this span while we waited. + close (bitmap_fd); + return DYAD_RC_OK; + } + + // Fetch from origin_path into local_path with *no* lock held. If + // another process is concurrently missing on an overlapping span (same + // shard), both will redundantly pread the same origin bytes and pwrite + // them to the same local_path offset -- since both write identical data + // to the same range, this races safely (worst case is duplicated I/O, + // never corruption), and is a better trade than serializing unrelated + // misses behind a single slow PFS read. + rc = range_cache_fetch_span (ctx, local_path, origin_path, block_start, block_end, origin_size); + if (DYAD_IS_ERROR (rc)) { + close (bitmap_fd); + return rc; + } + + // Re-acquire the lock just long enough to record that this span is now + // cached, merging with whatever bits any concurrent racers have set. + rc = dyad_excl_flock (ctx, bitmap_fd, &lock); + if (DYAD_IS_ERROR (rc)) { + close (bitmap_fd); + return rc; + } + bitmap = (uint8_t *)calloc (1ul, bitmap_bytes); + if (bitmap == NULL) { + dyad_release_flock (ctx, bitmap_fd, &lock); + close (bitmap_fd); + return DYAD_RC_SYSFAIL; + } + pread (bitmap_fd, bitmap, bitmap_bytes, 0); + range_cache_set_blocks (bitmap, block_start, block_end); + if (pwrite (bitmap_fd, bitmap, bitmap_bytes, 0) != (ssize_t)bitmap_bytes) { + DYAD_LOG_ERROR (ctx, "DYAD RANGE_CACHE: cannot update bitmap file '%s'", bitmap_path); + rc = DYAD_RC_BADFIO; + } + free (bitmap); + dyad_release_flock (ctx, bitmap_fd, &lock); + close (bitmap_fd); + return rc; +} diff --git a/src/dyad/utils/range_cache.h b/src/dyad/utils/range_cache.h new file mode 100644 index 00000000..da90bfe3 --- /dev/null +++ b/src/dyad/utils/range_cache.h @@ -0,0 +1,113 @@ +/************************************************************\ + * Copyright 2021 Lawrence Livermore National Security, LLC + * (c.f. AUTHORS, NOTICE.LLNS, COPYING) + * + * This file is part of the Flux resource manager framework. + * For details, see https://github.com/flux-framework. + * + * SPDX-License-Identifier: LGPL-3.0 +\************************************************************/ + +#ifndef DYAD_UTILS_RANGE_CACHE_H +#define DYAD_UTILS_RANGE_CACHE_H + +#if defined(DYAD_HAS_CONFIG) +#include +#else +#error "no config" +#endif + +#if defined(__cplusplus) +#include +#else +#include +#endif // defined(__cplusplus) + +#include +#include + +#if defined(__cplusplus) +extern "C" { +#endif // defined(__cplusplus) + +/** + * @brief Block size, in bytes, used to track which spans of a locally + * cached file are present. + * + * @details + * Chosen to be a few times larger than the typical byte-range request + * (~50-200KB atom/edge spans in the PECAN workload) so that most requests + * span only 1-3 blocks, keeping block-edge over-fetch small relative to + * request size, while still keeping the tracking bitmap small (a 15GB + * shard is ~234K blocks, i.e. ~29KB of bitmap). + */ +#define DYAD_RANGE_CACHE_BLOCK_SIZE (64UL * 1024UL) + +/** + * @brief Ensures that @c [offset, offset+length) of @p local_path is + * present locally, lazily fetching any missing block-aligned span + * from @p origin_path first. + * + * @details + * Tracks which @c DYAD_RANGE_CACHE_BLOCK_SIZE-aligned blocks of + * @p local_path have already been populated using a bitmap stored in a + * companion file (@c ".dyad_cached"), sized lazily on first + * touch from @c stat(origin_path). Blocks are filled in write-through: on + * a miss, the block-aligned span covering @c [offset, offset+length) is + * read from @p origin_path and written into @p local_path at the same + * offset, and the corresponding bits are set so later requests (from any + * process sharing @p local_path, local or remote) hit the cache instead + * of re-reading the origin. + * + * Coordination across concurrent callers uses @c dyad_shared_flock() / + * @c dyad_excl_flock() on the bitmap file (not on @p local_path itself, + * so large reads/writes to the data file are never serialized by the + * bookkeeping lock): a shared lock is used to check for a full hit, and + * only escalates to an exclusive lock (with a re-check, since another + * process may have filled the span while this one waited) on a miss. + * + * This function does not read data into a caller-supplied buffer itself + * -- once it returns @c DYAD_RC_OK, the caller is expected to read the + * requested range from @p local_path exactly as it would if the file had + * been fully staged upfront (e.g. via @c pread()). + * + * @param[in] ctx DYAD context. Must not be @c NULL. + * @param[in] local_path Path to the locally cached copy of the file. + * Created (and sized to match @p origin_path) on + * first touch if it does not already exist. + * @param[in] origin_path Path to the authoritative source (e.g. on the + * parallel file system) to fetch missing spans + * from. If @c NULL, this function is a no-op that + * returns @c DYAD_RC_OK immediately, preserving + * the "already fully staged" behavior for callers + * that don't opt in to lazy caching. + * @param[in] offset Byte offset of the requested range within the + * file. + * @param[in] length Length, in bytes, of the requested range. If 0, + * this function is a no-op that returns + * @c DYAD_RC_OK immediately. + * + * @return @c dyad_rc_t Return code indicating the outcome: + * @retval DYAD_RC_OK @p origin_path is @c NULL, @p length is 0, or + * @c [offset, offset+length) is now present in + * @p local_path (either because it already was, + * or because it was just fetched from + * @p origin_path). + * @retval DYAD_RC_BADFIO A filesystem operation (open/stat/read/write) + * on @p local_path, its bitmap file, or + * @p origin_path failed. + * @retval DYAD_RC_SYSFAIL A memory allocation failed. + * @retval DYAD_RC_BADBUF @p local_path is too long to build the bitmap + * file's path. + */ +dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, + const char *local_path, + const char *origin_path, + size_t offset, + size_t length); + +#if defined(__cplusplus) +} +#endif // defined(__cplusplus) + +#endif // DYAD_UTILS_RANGE_CACHE_H diff --git a/src/dyad/utils/utils.c b/src/dyad/utils/utils.c index fc333b9c..1cf62c9b 100644 --- a/src/dyad/utils/utils.c +++ b/src/dyad/utils/utils.c @@ -554,6 +554,44 @@ excl_flock_end:; return rc; } +dyad_rc_t dyad_try_excl_flock (const dyad_ctx_t* __restrict__ ctx, + int fd, + struct flock* __restrict__ lock) +{ + dyad_rc_t rc = DYAD_RC_OK; + DYAD_C_FUNCTION_START (); + DYAD_C_FUNCTION_UPDATE_INT ("fd", fd); + DYAD_LOG_DEBUG (ctx, + "DYAD UTIL: [node %u rank %u pid %d] Attempts a non-blocking exclusive lock " + "on fd %d.", + ctx->node_idx, + ctx->rank, + ctx->pid, + fd); + if (!lock) { + rc = DYAD_RC_BADFIO; + goto try_excl_flock_end; + } + lock->l_type = F_WRLCK; + lock->l_whence = SEEK_SET; + lock->l_start = 0; + lock->l_len = 0; + lock->l_pid = ctx->pid; // getpid(); + if (fcntl (fd, F_SETLK, lock) == -1) { // returns immediately instead of waiting + if (errno == EAGAIN || errno == EACCES) { + rc = DYAD_RC_BUSY; + } else { + DYAD_LOG_ERROR (ctx, "DYAD UTIL: Cannot apply exclusive lock on fd %d.", fd); + rc = DYAD_RC_BADFIO; + } + goto try_excl_flock_end; + } + rc = DYAD_RC_OK; +try_excl_flock_end:; + DYAD_C_FUNCTION_END (); + return rc; +} + dyad_rc_t dyad_shared_flock (const dyad_ctx_t* __restrict__ ctx, int fd, struct flock* __restrict__ lock) diff --git a/src/dyad/utils/utils.h b/src/dyad/utils/utils.h index 758cbf49..bd60cec7 100644 --- a/src/dyad/utils/utils.h +++ b/src/dyad/utils/utils.h @@ -458,6 +458,33 @@ ssize_t get_file_size (int fd); dyad_rc_t dyad_excl_flock (const dyad_ctx_t *__restrict__ ctx, int fd, struct flock *__restrict__ lock); +/** + * @brief Attempts to acquire an exclusive (write) lock without blocking. + * + * @details + * Sets a POSIX write lock (@c F_WRLCK) over the entire file using @c fcntl() + * with @c F_SETLK (non-blocking), returning immediately instead of waiting + * if the lock cannot be acquired. Used by the cache evictor to skip a + * candidate file that is currently locked by another in-flight + * @c dyad_produce()/@c dyad_consume() call, rather than blocking on it. + * + * If @p lock is @c NULL, the function returns without taking any action. + * + * @param[in] ctx DYAD context. + * @param[in] fd File descriptor of the open file to lock. + * @param[out] lock Pointer to a @c flock structure populated by this function. + * Must not be @c NULL. The structure is used for subsequent + * unlock calls via @c dyad_release_flock(). + * + * @return @c dyad_rc_t Return code indicating the outcome: + * @retval DYAD_RC_OK The lock was successfully acquired. + * @retval DYAD_RC_BUSY The lock is currently held by another process. + * @retval DYAD_RC_BADFIO The @c fcntl() call failed for a reason other than + * the lock being held (e.g. bad file descriptor). + */ +dyad_rc_t dyad_try_excl_flock (const dyad_ctx_t *__restrict__ ctx, + int fd, + struct flock *__restrict__ lock); /** * @brief Acquires a shared (read) lock on an open file descriptor. * diff --git a/tests/pydyad_spsc_lazy_range/consumer.py b/tests/pydyad_spsc_lazy_range/consumer.py new file mode 100644 index 00000000..58a86249 --- /dev/null +++ b/tests/pydyad_spsc_lazy_range/consumer.py @@ -0,0 +1,89 @@ +from pydyad import Dyad + +import argparse +import os +from pathlib import Path + + +def make_pattern(size): + return bytes([i % 256 for i in range(size)]) + + +def compute_ranges(file_size, num_ranges): + # Deterministic (offset, length) pairs spread across the file, varying + # in size (including a couple of edge cases: offset 0, and a range + # ending exactly at file_size) so the test exercises more than just one + # fixed-size middle chunk, and typically spans multiple + # DYAD_RANGE_CACHE_BLOCK_SIZE-aligned blocks. + step = max(file_size // (num_ranges + 1), 1) + ranges = [] + for i in range(num_ranges): + offset = min(i * step, max(file_size - 1, 0)) + length = min(step + (i * 37) % 101 + 1, file_size - offset) + if length > 0: + ranges.append((offset, length)) + return ranges + + +def fetch_and_check(dyad_io, fname, expected, ranges, label): + for offset, length in ranges: + print("[{}] Trying to consume range (offset={}, length={})".format(label, offset, length), + flush=True) + got = dyad_io.consume_range(str(fname), offset, length) + want = expected[offset:offset + length] + if got != want: + raise RuntimeError( + "[{}] Range mismatch at offset={}, length={}: got {} bytes, expected {} bytes".format( + label, offset, length, len(got) if got is not None else -1, len(want) + ) + ) + print("[{}] Correctly consumed range (offset={}, length={})".format(label, offset, length), + flush=True) + + +def main(): + parser = argparse.ArgumentParser("Consumer side of pydyad lazy origin-backed range cache test") + parser.add_argument("cons_managed_dir", type=Path, + help="DYAD's consumer managed path") + parser.add_argument("origin_dir", type=Path, + help="Shared-FS directory standing in for the PFS origin") + parser.add_argument("file_size", type=int, + help="Size (bytes) of the file the producer wrote") + parser.add_argument("num_ranges", type=int, + help="Number of byte ranges to fetch and verify") + args = parser.parse_args() + cons_dir = args.cons_managed_dir.expanduser().resolve() + origin_dir = args.origin_dir.expanduser().resolve() + fname = cons_dir / "range_test_data.bin" + origin_path = origin_dir / "range_test_data.bin" + + dyad_io = Dyad() + dyad_io.init_env() + + expected = make_pattern(args.file_size) + ranges = compute_ranges(args.file_size, args.num_ranges) + + # Pass 1: origin still exists. Each range is a miss on the producer's + # lazy range cache the first time it's touched, fetched from origin_path + # and write-through cached into the producer's managed directory by + # dyad_range_cache_ensure() (invoked from dyad_fetch_range_request_cb() + # since this file is owned by a different rank than the consumer). + fetch_and_check(dyad_io, fname, expected, ranges, "pass1-cold") + + # Remove the origin entirely. If pass 1 had left any requested byte + # uncached, this pass would now fail outright (dyad_range_cache_ensure() + # would have no origin to fall back to for a real miss) -- so success + # here proves every previously-requested span was truly cached + # write-through, not silently re-read from origin on every request. + if origin_path.exists(): + os.remove(origin_path) + print("Removed origin file {} -- repeating fetches from cache only".format(origin_path), + flush=True) + + fetch_and_check(dyad_io, fname, expected, ranges, "pass2-cached") + + dyad_io.finalize() + + +if __name__ == "__main__": + main() diff --git a/tests/pydyad_spsc_lazy_range/producer.py b/tests/pydyad_spsc_lazy_range/producer.py new file mode 100644 index 00000000..ef31eea1 --- /dev/null +++ b/tests/pydyad_spsc_lazy_range/producer.py @@ -0,0 +1,52 @@ +from pydyad import Dyad + +import argparse +from pathlib import Path + + +def make_pattern(size): + return bytes([i % 256 for i in range(size)]) + + +def main(): + parser = argparse.ArgumentParser("Producer side of pydyad lazy origin-backed range cache test") + parser.add_argument("prod_managed_dir", type=Path, + help="DYAD's producer managed path") + parser.add_argument("origin_dir", type=Path, + help="Shared-FS directory standing in for the PFS origin") + parser.add_argument("file_size", type=int, + help="Size (bytes) of the file to produce") + args = parser.parse_args() + prod_dir = args.prod_managed_dir.expanduser().resolve() + origin_dir = args.origin_dir.expanduser().resolve() + origin_dir.mkdir(parents=True, exist_ok=True) + + # Write the "real" data directly to the origin (PFS-like) location -- + # NOT through DYAD, and NOT into prod_managed_dir. dyad_range_cache_ensure() + # is responsible for lazily copying pieces of this into prod_managed_dir + # on demand; this test intentionally never stages it upfront (unlike + # tests/pydyad_spsc_range, which writes directly into the managed dir). + origin_path = origin_dir / "range_test_data.bin" + with open(origin_path, "wb") as f: + f.write(make_pattern(args.file_size)) + print("Wrote {} bytes directly to origin {}".format(args.file_size, origin_path), flush=True) + + local_path = prod_dir / "range_test_data.bin" + if local_path.exists(): + raise RuntimeError( + "{} already exists -- test requires the local copy to not be pre-staged".format( + local_path + ) + ) + + dyad_io = Dyad() + dyad_io.init_env() + # Announce ownership of the not-yet-materialized local path -- no data + # movement (mirrors pecan/datacopy_flat_dyad.py's simplified announce-only + # flow once upfront `cp` staging is dropped in favor of lazy caching). + dyad_io.produce(str(local_path)) + print("Announced ownership of {} (not yet materialized locally)".format(local_path), flush=True) + + +if __name__ == "__main__": + main() diff --git a/tests/pydyad_spsc_lazy_range/run.sh b/tests/pydyad_spsc_lazy_range/run.sh new file mode 100755 index 00000000..e09fdcdf --- /dev/null +++ b/tests/pydyad_spsc_lazy_range/run.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# FLUX: -N 2 +# FLUX: --output=pydyad_spsc_lazy_range_test.out +# FLUX: --error=pydyad_spsc_lazy_range_test.err + +if [ -z "${DYAD_INSTALL_LIBDIR}" ]; then + echo "DYAD_INSTALL_LIBDIR must be defined" + exit 1 +fi +if [ ! -d "${DYAD_INSTALL_LIBDIR}" ]; then + echo "DYAD_INSTALL_LIBDIR ($DYAD_INSTALL_LIBDIR) does not exist" + exit 1 +fi +if [ ! -f "${DYAD_INSTALL_LIBDIR}/dyad.so" ]; then + echo "Invalid contents in DYAD_INSTALL_LIBDIR ($DYAD_INSTALL_LIBDIR)" + exit 1 +fi +if [ -z "${DYAD_PATH_CONSUMER}" ]; then + if [ -z "${DYAD_PATH}" ]; then + echo "Either DYAD_PATH_CONSUMER or DYAD_PATH must be defined" + exit 1 + else + DYAD_PATH_CONSUMER="${DYAD_PATH}" + fi +fi +if [ -z "${DYAD_PATH_PRODUCER}" ]; then + if [ -z "${DYAD_PATH}" ]; then + echo "Either DYAD_PATH_PRODUCER or DYAD_PATH must be defined" + exit 1 + else + DYAD_PATH_PRODUCER="${DYAD_PATH}" + fi +fi +if [ -z "${DYAD_DTL_MODE}" ]; then + DYAD_DTL_MODE="FLUX_RPC" +fi +if [ -z "${DYAD_KVS_NAMESPACE}" ]; then + DYAD_KVS_NAMESPACE="pydyad_lazy_range_test" +fi +if [ -z "${FILE_SIZE}" ]; then + FILE_SIZE=1048576 # 1 MiB, big enough for several distinct ranges/blocks +fi +if [ -z "${NUM_RANGES}" ]; then + NUM_RANGES=20 +fi +if [ -z "${ORIGIN_DIR}" ]; then + # Must be on a filesystem shared by both the producer and consumer nodes + # (unlike DYAD_PATH_PRODUCER/_CONSUMER, which are typically node-local + # storage) -- this stands in for the parallel file system. + ORIGIN_DIR="$(pwd)/lazy_range_origin" +fi + +export LD_LIBRARY_PATH="${DYAD_INSTALL_LIBDIR}:${LD_LIBRARY_PATH}" + +flux kvs namespace create ${DYAD_KVS_NAMESPACE} + +rm -rf ${ORIGIN_DIR} +mkdir -m 755 -p ${ORIGIN_DIR} + +#### NOTE: consume_range() (and thus dyad_range_cache_ensure()'s lazy origin +#### fallback) is only implemented for DYAD_DTL_FLUX_RPC and DYAD_DTL_MARGO +#### (not UCX) -- see dyad_consume_range() in dyad_client.c. + +cmd_cons="(rm -rf ${DYAD_PATH_CONSUMER}; \ + mkdir -m 755 -p ${DYAD_PATH_CONSUMER}; \ + python3 consumer.py ${DYAD_PATH_CONSUMER} ${ORIGIN_DIR} ${FILE_SIZE} ${NUM_RANGES})" + +flux submit -N 1 --tasks-per-node=1 --exclusive \ + --output="pydyad_lazy_range_cons.out" --error="pydyad_lazy_range_cons.err" \ + --env=DYAD_PATH_CONSUMER=${DYAD_PATH_CONSUMER} \ + --env=DYAD_DTL_MODE=${DYAD_DTL_MODE} \ + --env=DYAD_KVS_NAMESPACE=${DYAD_KVS_NAMESPACE} \ + --flags=waitable \ + bash -c "${cmd_cons}" + +# Note: --origin_path is only set on the producer's flux module -- the +# consumer never touches DYAD_PATH_ORIGIN since this file is always remote +# to it (owned by a different rank), so only the module's +# dyad_fetch_range_request_cb() ever needs the origin fallback here. +cmd_prod="(rm -rf ${DYAD_PATH_PRODUCER}; \ + mkdir -m 755 -p ${DYAD_PATH_PRODUCER}; \ + flux module load ${DYAD_INSTALL_LIBDIR}/dyad.so --mode=${DYAD_DTL_MODE} \ + --origin_path=${ORIGIN_DIR} ${DYAD_PATH_PRODUCER}; \ + flux getattr rank > prod_rank.txt; \ + python3 producer.py ${DYAD_PATH_PRODUCER} ${ORIGIN_DIR} ${FILE_SIZE})" + +flux submit -N 1 --tasks-per-node=1 --exclusive \ + --output="pydyad_lazy_range_prod.out" --error="pydyad_lazy_range_prod.err" \ + --env=DYAD_PATH_PRODUCER=${DYAD_PATH_PRODUCER} \ + --env=DYAD_DTL_MODE=${DYAD_DTL_MODE} \ + --env=DYAD_KVS_NAMESPACE=${DYAD_KVS_NAMESPACE} \ + --flags=waitable \ + bash -c "${cmd_prod}" + +flux job wait --all +job_wait_rc=$? + +if [ -f prod_rank.txt ] ; then + prod_rank=$(cat prod_rank.txt) + # Verify the local cache file and its bitmap were actually populated + # lazily by dyad_range_cache_ensure() while serving the consumer's + # fetches -- i.e. confirm laziness itself happened, not just that the + # bytes returned to the consumer were correct. + echo "Checking that the local cache file/bitmap were populated lazily on producer rank ${prod_rank}..." + if ! flux exec -r ${prod_rank} bash -c \ + "test -s ${DYAD_PATH_PRODUCER}/range_test_data.bin && \ + test -s ${DYAD_PATH_PRODUCER}/range_test_data.bin.dyad_cached"; then + echo "ERROR: local cache file/bitmap missing or empty after lazy fetch" + job_wait_rc=1 + fi + flux exec -r ${prod_rank} flux module remove dyad + rm prod_rank.txt +else + flux exec -r all flux module remove dyad +fi +flux kvs namespace remove ${DYAD_KVS_NAMESPACE} +rm -rf ${ORIGIN_DIR} + +if [ ${job_wait_rc} -ne 0 ]; then + echo "ERROR: a job crashed with an error, or the post-run laziness check failed" + exit 1 +fi diff --git a/tests/pydyad_spsc_range/consumer.py b/tests/pydyad_spsc_range/consumer.py new file mode 100644 index 00000000..b44ed710 --- /dev/null +++ b/tests/pydyad_spsc_range/consumer.py @@ -0,0 +1,56 @@ +from pydyad import Dyad + +import argparse +from pathlib import Path + + +CONS_DIR = None + + +def make_pattern(size): + return bytes([i % 256 for i in range(size)]) + + +def main(): + parser = argparse.ArgumentParser("Consumes byte ranges for pydyad byte-range test") + parser.add_argument("cons_managed_dir", type=Path, + help="DYAD's consumer managed path") + parser.add_argument("file_size", type=int, + help="Size (bytes) of the file the producer wrote") + parser.add_argument("num_ranges", type=int, + help="Number of byte ranges to fetch and verify") + args = parser.parse_args() + global CONS_DIR + CONS_DIR = args.cons_managed_dir.expanduser().resolve() + fname = CONS_DIR / "range_test_data.bin" + + dyad_io = Dyad() + dyad_io.init_env() + + # Deterministic (offset, length) pairs spread across the file, varying + # in size (including a couple of edge cases: offset 0, and a range + # ending exactly at file_size) so the test exercises more than just one + # fixed-size middle chunk. + expected = make_pattern(args.file_size) + step = max(args.file_size // (args.num_ranges + 1), 1) + for i in range(args.num_ranges): + offset = min(i * step, max(args.file_size - 1, 0)) + length = min(step + (i * 37) % 101 + 1, args.file_size - offset) + if length <= 0: + continue + print("Trying to consume range (offset={}, length={})".format(offset, length), flush=True) + got = dyad_io.consume_range(str(fname), offset, length) + want = expected[offset:offset + length] + if got != want: + raise RuntimeError( + "Range mismatch at offset={}, length={}: got {} bytes, expected {} bytes".format( + offset, length, len(got) if got is not None else -1, len(want) + ) + ) + print("Correctly consumed range (offset={}, length={})".format(offset, length), flush=True) + + dyad_io.finalize() + + +if __name__ == "__main__": + main() diff --git a/tests/pydyad_spsc_range/producer.py b/tests/pydyad_spsc_range/producer.py new file mode 100644 index 00000000..50d97cf0 --- /dev/null +++ b/tests/pydyad_spsc_range/producer.py @@ -0,0 +1,31 @@ +from pydyad import dyad_open + +import argparse +from pathlib import Path + + +PROD_DIR = None + + +def make_pattern(size): + return bytes([i % 256 for i in range(size)]) + + +def main(): + parser = argparse.ArgumentParser("Generates data for pydyad byte-range test") + parser.add_argument("prod_managed_dir", type=Path, + help="DYAD's producer managed path") + parser.add_argument("file_size", type=int, + help="Size (bytes) of the file to produce") + args = parser.parse_args() + global PROD_DIR + PROD_DIR = args.prod_managed_dir.expanduser().resolve() + fname = PROD_DIR / "range_test_data.bin" + + with dyad_open(fname, "wb") as f: + f.write(make_pattern(args.file_size)) + print("Successfully produced {} bytes to {}".format(args.file_size, fname), flush=True) + + +if __name__ == "__main__": + main() diff --git a/tests/pydyad_spsc_range/run.sh b/tests/pydyad_spsc_range/run.sh new file mode 100644 index 00000000..6c92ae99 --- /dev/null +++ b/tests/pydyad_spsc_range/run.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# FLUX: -N 2 +# FLUX: --output=pydyad_spsc_range_test.out +# FLUX: --error=pydyad_spsc_range_test.err + +if [ -z "${DYAD_INSTALL_LIBDIR}" ]; then + echo "DYAD_INSTALL_LIBDIR must be defined" + exit 1 +fi +if [ ! -d "${DYAD_INSTALL_LIBDIR}" ]; then + echo "DYAD_INSTALL_LIBDIR ($DYAD_INSTALL_LIBDIR) does not exist" + exit 1 +fi +if [ ! -f "${DYAD_INSTALL_LIBDIR}/dyad.so" ]; then + echo "Invalid contents in DYAD_INSTALL_LIBDIR ($DYAD_INSTALL_LIBDIR)" + exit 1 +fi +if [ -z "${DYAD_PATH_CONSUMER}" ]; then + if [ -z "${DYAD_PATH}" ]; then + echo "Either DYAD_PATH_CONSUMER or DYAD_PATH must be defined" + exit 1 + else + DYAD_PATH_CONSUMER="${DYAD_PATH}" + fi +fi +if [ -z "${DYAD_PATH_PRODUCER}" ]; then + if [ -z "${DYAD_PATH}" ]; then + echo "Either DYAD_PATH_PRODUCER or DYAD_PATH must be defined" + exit 1 + else + DYAD_PATH_PRODUCER="${DYAD_PATH}" + fi +fi +if [ -z "${DYAD_DTL_MODE}" ]; then + DYAD_DTL_MODE="FLUX_RPC" +fi +if [ -z "${DYAD_KVS_NAMESPACE}" ]; then + DYAD_KVS_NAMESPACE="pydyad_range_test" +fi +if [ -z "${FILE_SIZE}" ]; then + FILE_SIZE=1048576 # 1 MiB, big enough for several distinct ranges +fi +if [ -z "${NUM_RANGES}" ]; then + NUM_RANGES=20 +fi + +export LD_LIBRARY_PATH="${DYAD_INSTALL_LIBDIR}:${LD_LIBRARY_PATH}" + +flux kvs namespace create ${DYAD_KVS_NAMESPACE} + +#### NOTE: consume_range() is only implemented for DYAD_DTL_FLUX_RPC and +#### DYAD_DTL_MARGO (not UCX) -- see dyad_consume_range() in dyad_client.c. + +cmd_cons="(rm -rf ${DYAD_PATH_CONSUMER}; \ + mkdir -m 755 -p ${DYAD_PATH_CONSUMER}; \ + python3 consumer.py ${DYAD_PATH_CONSUMER} ${FILE_SIZE} ${NUM_RANGES})" + +flux submit -N 1 --tasks-per-node=1 --exclusive \ + --output="pydyad_range_cons.out" --error="pydyad_range_cons.err" \ + --env=DYAD_PATH_CONSUMER=${DYAD_PATH_CONSUMER} \ + --env=DYAD_DTL_MODE=${DYAD_DTL_MODE} \ + --env=DYAD_KVS_NAMESPACE=${DYAD_KVS_NAMESPACE} \ + --flags=waitable \ + bash -c "${cmd_cons}" + +cmd_prod="(rm -rf ${DYAD_PATH_PRODUCER}; \ + mkdir -m 755 -p ${DYAD_PATH_PRODUCER}; \ + flux module load ${DYAD_INSTALL_LIBDIR}/dyad.so --mode=${DYAD_DTL_MODE} ${DYAD_PATH_PRODUCER}; \ + flux getattr rank > prod_rank.txt; \ + python3 producer.py ${DYAD_PATH_PRODUCER} ${FILE_SIZE})" + +flux submit -N 1 --tasks-per-node=1 --exclusive \ + --output="pydyad_range_prod.out" --error="pydyad_range_prod.err" \ + --env=DYAD_PATH_PRODUCER=${DYAD_PATH_PRODUCER} \ + --env=DYAD_DTL_MODE=${DYAD_DTL_MODE} \ + --env=DYAD_KVS_NAMESPACE=${DYAD_KVS_NAMESPACE} \ + --flags=waitable \ + bash -c "${cmd_prod}" + +flux job wait --all + +if [ -f prod_rank.txt ] ; then + flux exec -r `cat prod_rank.txt` flux module remove dyad + rm prod_rank.txt +else + flux exec -r all flux module remove dyad +fi +flux kvs namespace remove ${DYAD_KVS_NAMESPACE} + +if [ $? -ne 0 ]; then + echo "ERROR: a job crashed with an error" + exit 1 +fi diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 6122eb5a..80b4ef9d 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -23,7 +23,7 @@ include_directories(${MPI_CXX_INCLUDE_DIRS}) include_directories(${DYAD_PROJECT_DIR}/src) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include) include_directories(${CMAKE_BINARY_DIR}/include) -set(TEST_LIBS Catch2::Catch2 -lstdc++fs ${MPI_CXX_LIBRARIES} -rdynamic dyad_client dyad_ctx dyad_utils flux-core ${CPP_LOGGER_LIBRARIES}) +set(TEST_LIBS Catch2::Catch2 -lstdc++fs ${MPI_CXX_LIBRARIES} -rdynamic dyad_client dyad_ctx dyad_cache dyad_utils flux-core ${CPP_LOGGER_LIBRARIES}) set(TEST_SRC ${CMAKE_CURRENT_SOURCE_DIR}/catch_config.h ${CMAKE_CURRENT_SOURCE_DIR}/mpi_console_reporter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/mpi_console_reporter.hpp ${CMAKE_CURRENT_SOURCE_DIR}/test_utils.h) add_executable(unit_test unit_test.cpp ${TEST_SRC}) target_link_libraries(unit_test PRIVATE ${TEST_LIBS}) @@ -38,3 +38,4 @@ add_subdirectory(script) add_subdirectory(data_plane) add_subdirectory(mdm) add_subdirectory(dyad_core) +add_subdirectory(cache) diff --git a/tests/unit/cache/CMakeLists.txt b/tests/unit/cache/CMakeLists.txt new file mode 100644 index 00000000..115bb68c --- /dev/null +++ b/tests/unit/cache/CMakeLists.txt @@ -0,0 +1 @@ +add_test(unit_dyad_cache ${CMAKE_BINARY_DIR}/bin/unit_test --reporter mpi_console "[module=dyad_cache]") diff --git a/tests/unit/cache/cache_functions.cpp b/tests/unit/cache/cache_functions.cpp new file mode 100644 index 00000000..cb9ffc90 --- /dev/null +++ b/tests/unit/cache/cache_functions.cpp @@ -0,0 +1,235 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +// Zero-initialized dyad_ctx_t with just enough set to exercise the cache +// module directly, without needing Flux/MPI (pure-function test tier). +struct dyad_ctx make_test_ctx () +{ + struct dyad_ctx ctx; + memset (&ctx, 0, sizeof (ctx)); + ctx.pid = getpid (); + return ctx; +} + +std::string make_temp_dir () +{ + char tmpl[] = "/tmp/dyad_cache_test_XXXXXX"; + char *dir = mkdtemp (tmpl); + REQUIRE (dir != nullptr); + return std::string (dir); +} + +void write_file (const std::string &path, size_t size_bytes) +{ + FILE *f = fopen (path.c_str (), "wb"); + REQUIRE (f != nullptr); + std::string buf (size_bytes, 'x'); + REQUIRE (fwrite (buf.data (), 1, buf.size (), f) == buf.size ()); + fclose (f); +} + +// utimensat() lets us set deterministic atime/mtime instead of relying on +// sleeps between writes (which would be both slow and flaky under coarse +// filesystem timestamp granularity). +void set_times (const std::string &path, time_t atime, time_t mtime) +{ + struct timespec times[2]; + times[0].tv_sec = atime; + times[0].tv_nsec = 0; + times[1].tv_sec = mtime; + times[1].tv_nsec = 0; + REQUIRE (utimensat (AT_FDCWD, path.c_str (), times, 0) == 0); +} + +void remove_dir_recursive (const std::string &dir) +{ + std::string cmd = "rm -rf '" + dir + "'"; + system (cmd.c_str ()); +} + +} // namespace + +TEST_CASE ("dyad_cache_lru_recency_key", "[module=dyad_cache][method=get_recency_key]") +{ + struct dyad_ctx ctx = make_test_ctx (); + REQUIRE (dyad_cache_policy_init (&ctx, DYAD_CACHE_LRU) == DYAD_RC_OK); + REQUIRE (ctx.cache_policy != nullptr); + REQUIRE (ctx.cache_policy->get_recency_key != nullptr); + + struct stat st_old; + memset (&st_old, 0, sizeof (st_old)); + st_old.st_atime = 100; + struct stat st_new; + memset (&st_new, 0, sizeof (st_new)); + st_new.st_atime = 200; + + int64_t key_old = 0, key_new = 0; + REQUIRE (ctx.cache_policy->get_recency_key (&ctx, &st_old, &key_old) == DYAD_RC_OK); + REQUIRE (ctx.cache_policy->get_recency_key (&ctx, &st_new, &key_new) == DYAD_RC_OK); + REQUIRE (key_old == 100); + REQUIRE (key_new == 200); + REQUIRE (key_old < key_new); + + dyad_cache_policy_finalize (&ctx); +} + +TEST_CASE ("dyad_cache_fifo_recency_key", "[module=dyad_cache][method=get_recency_key]") +{ + struct dyad_ctx ctx = make_test_ctx (); + REQUIRE (dyad_cache_policy_init (&ctx, DYAD_CACHE_FIFO) == DYAD_RC_OK); + REQUIRE (ctx.cache_policy != nullptr); + + struct stat st_old; + memset (&st_old, 0, sizeof (st_old)); + st_old.st_mtime = 50; + struct stat st_new; + memset (&st_new, 0, sizeof (st_new)); + st_new.st_mtime = 150; + + int64_t key_old = 0, key_new = 0; + REQUIRE (ctx.cache_policy->get_recency_key (&ctx, &st_old, &key_old) == DYAD_RC_OK); + REQUIRE (ctx.cache_policy->get_recency_key (&ctx, &st_new, &key_new) == DYAD_RC_OK); + REQUIRE (key_old == 50); + REQUIRE (key_new == 150); + + dyad_cache_policy_finalize (&ctx); +} + +TEST_CASE ("dyad_cache_scan_dir", "[module=dyad_cache][method=scan]") +{ + struct dyad_ctx ctx = make_test_ctx (); + REQUIRE (dyad_cache_policy_init (&ctx, DYAD_CACHE_LRU) == DYAD_RC_OK); + std::string dir = make_temp_dir (); + + write_file (dir + "/a.bin", 100); + write_file (dir + "/b.bin", 200); + // The lock file must be excluded from scan results even though it lives + // in the same managed directory. + write_file (dir + "/" + DYAD_CACHE_LOCK_FILENAME, 999); + + struct dyad_cache_entry *entries = nullptr; + size_t n_entries = 0; + uint64_t total_size = 0; + REQUIRE (dyad_cache_scan_dir (&ctx, dir.c_str (), &entries, &n_entries, &total_size) == DYAD_RC_OK); + REQUIRE (n_entries == 2); + REQUIRE (total_size == 300); + + free (entries); + dyad_cache_policy_finalize (&ctx); + remove_dir_recursive (dir); +} + +TEST_CASE ("dyad_cache_maybe_evict_noop_when_disabled", + "[module=dyad_cache][method=maybe_evict]") +{ + struct dyad_ctx ctx = make_test_ctx (); + ctx.cache_capacity_bytes = 0; // disabled (the default) + ctx.cache_policy_mode = DYAD_CACHE_NONE; + std::string dir = make_temp_dir (); + write_file (dir + "/a.bin", 1024); + + REQUIRE (dyad_cache_maybe_evict (&ctx, dir.c_str ()) == DYAD_RC_OK); + REQUIRE (access ((dir + "/a.bin").c_str (), F_OK) == 0); + // No-op must not even create the lock file. + REQUIRE (access ((dir + "/" + DYAD_CACHE_LOCK_FILENAME).c_str (), F_OK) != 0); + + remove_dir_recursive (dir); +} + +TEST_CASE ("dyad_cache_maybe_evict_selects_oldest_first", + "[module=dyad_cache][method=maybe_evict]") +{ + struct dyad_ctx ctx = make_test_ctx (); + REQUIRE (dyad_cache_policy_init (&ctx, DYAD_CACHE_LRU) == DYAD_RC_OK); + ctx.cache_policy_mode = DYAD_CACHE_LRU; + ctx.cache_capacity_bytes = 2048; // 2 KiB cap + ctx.cache_low_watermark_frac = 0.5; // evict down to 1 KiB + ctx.cache_grace_period_sec = 0; // don't skip anything based on recency + + std::string dir = make_temp_dir (); + write_file (dir + "/oldest.bin", 1024); + write_file (dir + "/middle.bin", 1024); + write_file (dir + "/newest.bin", 1024); + // Deterministic ages: oldest.bin < middle.bin < newest.bin + set_times (dir + "/oldest.bin", 100, 100); + set_times (dir + "/middle.bin", 200, 200); + set_times (dir + "/newest.bin", 300, 300); + + REQUIRE (dyad_cache_maybe_evict (&ctx, dir.c_str ()) == DYAD_RC_OK); + + REQUIRE (access ((dir + "/oldest.bin").c_str (), F_OK) != 0); + REQUIRE (access ((dir + "/newest.bin").c_str (), F_OK) == 0); + + dyad_cache_policy_finalize (&ctx); + remove_dir_recursive (dir); +} + +TEST_CASE ("dyad_cache_maybe_evict_skips_locked_candidate", + "[module=dyad_cache][method=maybe_evict]") +{ + struct dyad_ctx ctx = make_test_ctx (); + REQUIRE (dyad_cache_policy_init (&ctx, DYAD_CACHE_LRU) == DYAD_RC_OK); + ctx.cache_policy_mode = DYAD_CACHE_LRU; + ctx.cache_capacity_bytes = 1024; // 1 KiB cap + ctx.cache_low_watermark_frac = 0.5; + ctx.cache_grace_period_sec = 0; + + std::string dir = make_temp_dir (); + std::string locked_path = dir + "/locked_oldest.bin"; + std::string free_path = dir + "/free_newest.bin"; + write_file (locked_path, 1024); + write_file (free_path, 1024); + set_times (locked_path, 100, 100); // oldest -- would normally be evicted first + set_times (free_path, 200, 200); + + // Hold a real exclusive flock on locked_path from a child process, since + // fcntl locks are associated with the (process, open file description), + // not the fd alone -- a lock from this same process wouldn't conflict. + pid_t child = fork (); + REQUIRE (child >= 0); + if (child == 0) { + int fd = open (locked_path.c_str (), O_RDWR); + if (fd == -1) { + _exit (1); + } + struct flock lock; + memset (&lock, 0, sizeof (lock)); + lock.l_type = F_WRLCK; + lock.l_whence = SEEK_SET; + if (fcntl (fd, F_SETLK, &lock) == -1) { + _exit (1); + } + // Hold the lock until the parent signals it's done checking. + pause (); + _exit (0); + } + // Give the child a moment to acquire the lock. + usleep (200000); + + REQUIRE (dyad_cache_maybe_evict (&ctx, dir.c_str ()) == DYAD_RC_OK); + + // The locked (oldest) file must survive; eviction should have skipped it. + REQUIRE (access (locked_path.c_str (), F_OK) == 0); + + kill (child, SIGTERM); + int status = 0; + waitpid (child, &status, 0); + + dyad_cache_policy_finalize (&ctx); + remove_dir_recursive (dir); +} diff --git a/tests/unit/unit_test.cpp b/tests/unit/unit_test.cpp index cb617c46..917dfb5a 100644 --- a/tests/unit/unit_test.cpp +++ b/tests/unit/unit_test.cpp @@ -165,3 +165,4 @@ int clean_directories() { #include "data_plane/data_plane.cpp" #include "dyad_core/core_functions.cpp" #include "mdm/mdm.cpp" +#include "cache/cache_functions.cpp" From f764772359bd002bf6bd87d7a0155f9627d845ce Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Thu, 16 Jul 2026 07:21:32 -0700 Subject: [PATCH 3/8] Serve fetch RPCs concurrently via a worker-thread pool DYAD's Flux broker module runs a single reactor thread, so every dyad.fetch_range request -- including its blocking file I/O and, for Margo, the RDMA send itself -- was serviced fully synchronously on that one thread. Concurrent requests from different ranks/nodes queued behind whichever one happened to arrive first, capping fetch throughput at 1/(mean per-request time) regardless of how many requests were actually independent. Adds a fixed-size worker-thread pool (DYAD_FETCH_WORKER_THREADS, default 8) that the reactor hands blocking work off to via a new detach/send-detached DTL vtable extension (rpc_detach_request / rpc_send_detached / rpc_abort_detached), implemented for the FLUX_RPC and Margo backends (UCX keeps the original synchronous path via a NULL rpc_detach_request). Completion is handed back to the reactor thread through a self-pipe + fd-watcher for backends that must not touch Flux state off the reactor thread. Two correctness hazards from adding concurrency, both fixed: - fcntl() locks only exclude other *processes*, not threads of the same process -- added a module-local pthread_mutex_t in range_cache.c alongside the existing fcntl locks. - Both backends stored per-request state (a message reference / resolved RDMA address) in a single shared slot, safe only under strictly serial servicing -- replaced with the detach/send-detached handoff so each in-flight request owns independent state. Co-Authored-By: Claude Sonnet 5 --- include/dyad/common/dyad_envs.h | 16 + src/dyad/dtl/dyad_dtl_api.h | 102 ++++ src/dyad/dtl/flux_dtl.c | 64 +++ src/dyad/dtl/flux_dtl.h | 81 ++++ src/dyad/dtl/margo_dtl.c | 105 +++- src/dyad/dtl/margo_dtl.h | 98 ++++ src/dyad/dtl/ucx_dtl.c | 9 + src/dyad/service/flux_module/CMakeLists.txt | 3 + src/dyad/service/flux_module/dyad.c | 511 ++++++++++++++++++++ src/dyad/utils/CMakeLists.txt | 5 +- src/dyad/utils/range_cache.c | 62 ++- 11 files changed, 1035 insertions(+), 21 deletions(-) diff --git a/include/dyad/common/dyad_envs.h b/include/dyad/common/dyad_envs.h index 8be6cfbc..1ef8da55 100644 --- a/include/dyad/common/dyad_envs.h +++ b/include/dyad/common/dyad_envs.h @@ -184,4 +184,20 @@ */ #define DYAD_PATH_ORIGIN_ENV "DYAD_PATH_ORIGIN" +/** + * @brief Number of worker threads the DYAD Flux module uses to service + * @c dyad.fetch_range RPCs (@c dyad_fetch_range_request_cb()). + * + * @details + * The module's single Flux reactor thread hands each byte-range fetch's + * blocking file I/O (and, on the Margo DTL, the RDMA send itself) off to + * this worker pool instead of doing it inline, so concurrent requests from + * different ranks/nodes don't serialize behind whichever one happened to + * arrive first. Read directly via @c getenv() in @c mod_main(), not + * through @c dyad_init_env(), since it configures the module's own + * internal servicing, not context state shared with client processes. + * Default: @c 8. + */ +#define DYAD_FETCH_WORKER_THREADS_ENV "DYAD_FETCH_WORKER_THREADS" + #endif // DYAD_COMMON_DYAD_ENVS_H diff --git a/src/dyad/dtl/dyad_dtl_api.h b/src/dyad/dtl/dyad_dtl_api.h index 3a551539..61ba41eb 100644 --- a/src/dyad/dtl/dyad_dtl_api.h +++ b/src/dyad/dtl/dyad_dtl_api.h @@ -250,6 +250,108 @@ struct dyad_dtl { */ dyad_rc_t (*close_connection) (const dyad_ctx_t *ctx); + /** + * @brief Detaches the current request's DTL state from the shared, + * single-slot @c private_dtl fields into an independently-owned + * blob, so the request can be finished (@c rpc_send_detached()) + * later/concurrently without racing a subsequent request's + * @c rpc_unpack_range() call, which would otherwise overwrite the + * same shared field (e.g. Flux RPC's @c msg, Margo's + * @c remote_addr). + * + * @details + * Optional: @c NULL for backends that do not support detached/threaded + * servicing (currently UCX). Callers must check for @c NULL before use + * and fall back to the fully-synchronous @c establish_connection()/ + * @c send()/@c close_connection() sequence on the shared fields instead. + * + * Must be called on the same thread as -- and immediately after -- the + * @c rpc_unpack_range() call whose state it is detaching, while the + * shared field is still valid for this request. + * + * @param[in] ctx DYAD context. + * @param[out] req_state Set to a newly allocated, backend-specific blob + * owning this request's DTL state. Must be passed + * to @c rpc_send_detached() exactly once to + * complete the request and free it. + * @return @c DYAD_RC_OK on success, or an error code on failure. + */ + dyad_rc_t (*rpc_detach_request) (const dyad_ctx_t *ctx, void **req_state); + + /** + * @brief Sends file data to the consumer using a previously detached + * request's state, instead of the shared @c private_dtl fields. + * + * @details + * Optional: @c NULL for backends that do not support detached/threaded + * servicing (currently UCX). Equivalent to + * @c establish_connection()+@c send()+@c close_connection(), but reads + * per-request state from @p req_state instead of the shared fields, and + * frees @p req_state before returning (success or failure). + * + * Whether this may be called from a thread other than the reactor + * thread that received the request is backend-specific -- + * @see send_detached_is_thread_safe. + * + * @param[in] ctx DYAD context. + * @param[in] req_state Request state from a prior @c rpc_detach_request() + * call. Freed by this call; must not be reused + * afterward. + * @param[in] buf Buffer containing the data to send. + * @param[in] buflen Number of bytes to send. + * @return @c DYAD_RC_OK on success, or an error code on failure. + */ + dyad_rc_t (*rpc_send_detached) (const dyad_ctx_t *ctx, + void *req_state, + void *buf, + size_t buflen); + + /** + * @brief Frees a detached request's state without sending data, for + * use on an I/O-error path where the fetch never reached + * @c rpc_send_detached(). + * + * @details + * Optional: @c NULL for backends that do not support detached/threaded + * servicing (currently UCX; never called when @c rpc_detach_request is + * @c NULL). Frees whatever @p req_state owns (e.g. Flux RPC's message + * reference, Margo's resolved address) without attempting a send. Safe + * to call from the same threads as @c rpc_send_detached() -- see + * @c send_detached_is_thread_safe. + * + * @param[in] ctx DYAD context. + * @param[in] req_state Request state from a prior + * @c rpc_detach_request() call. Freed by this + * call; must not be reused afterward. + * @return @c DYAD_RC_OK on success, or an error code on failure. + */ + dyad_rc_t (*rpc_abort_detached) (const dyad_ctx_t *ctx, void *req_state); + + /** + * @brief Whether @c rpc_send_detached() is safe to call from a worker + * thread other than the reactor thread that received the + * request. + * + * @details + * @c false for both backends currently implemented. Flux RPC's + * @c send() calls @c flux_respond_raw(), which touches the module's + * @c flux_t handle and must only be touched from the reactor thread + * that owns it. Margo's @c send() touches no Flux state but is + * Argobots/Mercury state, and this DTL's producer-side @c margo_init() + * creates no dedicated Argobots execution stream, so its @c mid is + * only a valid execution context on the thread that called + * @c margo_init() (the reactor thread) -- a plain worker-pool + * @c pthread is not an Argobots ULT/execution-stream context, and + * calling @c margo_forward() from one hangs. Callers must hand the + * completed request back to the reactor thread (e.g. via a self-pipe + + * reactor fd-watcher) before calling @c rpc_send_detached() for either + * backend. This field exists for a future backend where it may + * legitimately be @c true (e.g. one whose send touches neither Flux + * nor an Argobots-only execution context). Meaningless (left @c false) + * when @c rpc_send_detached is @c NULL. + */ + bool send_detached_is_thread_safe; + } __attribute__ ((aligned (256))); typedef struct dyad_dtl dyad_dtl_t; diff --git a/src/dyad/dtl/flux_dtl.c b/src/dyad/dtl/flux_dtl.c index b0152663..27dbd98a 100644 --- a/src/dyad/dtl/flux_dtl.c +++ b/src/dyad/dtl/flux_dtl.c @@ -41,6 +41,12 @@ dyad_rc_t dyad_dtl_flux_init (const dyad_ctx_t *ctx, ctx->dtl_handle->send = dyad_dtl_flux_send; ctx->dtl_handle->recv = dyad_dtl_flux_recv; ctx->dtl_handle->close_connection = dyad_dtl_flux_close_connection; + ctx->dtl_handle->rpc_detach_request = dyad_dtl_flux_detach_request; + ctx->dtl_handle->rpc_send_detached = dyad_dtl_flux_send_detached; + ctx->dtl_handle->rpc_abort_detached = dyad_dtl_flux_abort_detached; + // flux_respond_raw() touches the module's flux_t handle, which must + // only be touched from the reactor thread that owns it. + ctx->dtl_handle->send_detached_is_thread_safe = false; dtl_flux_init_region_finish: DYAD_C_FUNCTION_END (); @@ -280,6 +286,64 @@ dyad_rc_t dyad_dtl_flux_recv (const dyad_ctx_t *ctx, void **buf, size_t *buflen) return dyad_rc; } +dyad_rc_t dyad_dtl_flux_detach_request (const dyad_ctx_t *ctx, void **req_state) +{ + DYAD_C_FUNCTION_START (); + struct dyad_dtl_flux_req_state *state = NULL; + dyad_dtl_flux_t *dtl_handle = ctx->dtl_handle->private_dtl.flux_dtl_handle; + + state = (struct dyad_dtl_flux_req_state *)malloc (sizeof (struct dyad_dtl_flux_req_state)); + if (state == NULL) { + DYAD_C_FUNCTION_END (); + return DYAD_RC_SYSFAIL; + } + state->h = dtl_handle->h; + // Standard Flux idiom for retaining a message past the synchronous + // callback scope that received it -- see flux_msg_incref()/decref() + // docs. dtl_handle->msg is cleared afterward so a later request's + // rpc_unpack_range() cannot be mistaken for this one. + state->msg = (flux_msg_t *)flux_msg_incref (dtl_handle->msg); + dtl_handle->msg = NULL; + *req_state = state; + + DYAD_C_FUNCTION_END (); + return DYAD_RC_OK; +} + +dyad_rc_t dyad_dtl_flux_send_detached (const dyad_ctx_t *ctx, + void *req_state, + void *buf, + size_t buflen) +{ + DYAD_C_FUNCTION_START (); + dyad_rc_t dyad_rc = DYAD_RC_OK; + int rc = 0; + struct dyad_dtl_flux_req_state *state = (struct dyad_dtl_flux_req_state *)req_state; + + DYAD_LOG_INFO (ctx, "Send data to consumer using a Flux RPC response (detached)"); + rc = flux_respond_raw (state->h, state->msg, buf, buflen); + if (FLUX_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, + "Could not send Flux RPC response containing file " + "contents (detached)"); + dyad_rc = DYAD_RC_FLUXFAIL; + } + flux_msg_decref (state->msg); + free (state); + + DYAD_C_FUNCTION_UPDATE_INT ("buflen", buflen); + DYAD_C_FUNCTION_END (); + return dyad_rc; +} + +dyad_rc_t dyad_dtl_flux_abort_detached (const dyad_ctx_t *ctx, void *req_state) +{ + struct dyad_dtl_flux_req_state *state = (struct dyad_dtl_flux_req_state *)req_state; + flux_msg_decref (state->msg); + free (state); + return DYAD_RC_OK; +} + dyad_rc_t dyad_dtl_flux_close_connection (const dyad_ctx_t *ctx) { DYAD_C_FUNCTION_START (); diff --git a/src/dyad/dtl/flux_dtl.h b/src/dyad/dtl/flux_dtl.h index b56a6ecf..1bce149e 100644 --- a/src/dyad/dtl/flux_dtl.h +++ b/src/dyad/dtl/flux_dtl.h @@ -21,6 +21,17 @@ struct dyad_dtl_flux { typedef struct dyad_dtl_flux dyad_dtl_flux_t; +/** + * @brief Per-request state detached from the shared @c dyad_dtl_flux + * fields by @c dyad_dtl_flux_detach_request(), so a request can be + * finished later (e.g. from a worker-thread completion queue) + * without racing a later request's unpack call. + */ +struct dyad_dtl_flux_req_state { + flux_t *h; + flux_msg_t *msg; // incref'd; released by dyad_dtl_flux_send_detached() +}; + /** * @brief Initializes the Flux RPC DTL internal state. * @@ -400,6 +411,76 @@ dyad_rc_t dyad_dtl_flux_recv (const dyad_ctx_t *ctx, void **buf, size_t *buflen) */ dyad_rc_t dyad_dtl_flux_close_connection (const dyad_ctx_t *ctx); +/** + * @brief Detaches the current request's message from the shared, + * single-slot @c msg field into an independently-owned request + * state blob. + * + * @details + * Takes a reference on the message stored by the preceding + * @c dyad_dtl_flux_rpc_unpack_range() call via @c flux_msg_incref() (the + * standard Flux idiom for retaining a message beyond the synchronous + * callback scope that received it) and stores it in a newly allocated + * @c struct dyad_dtl_flux_req_state, then clears the shared @c msg field + * so a subsequent request's unpack call cannot be confused with this one. + * + * @param[in] ctx DYAD context. + * @param[out] req_state Set to a newly allocated request-state blob owning + * an incref'd reference to the request message. Must + * be passed to @c dyad_dtl_flux_send_detached() + * exactly once. + * + * @return Always returns @c DYAD_RC_OK, unless allocation fails + * (@c DYAD_RC_SYSFAIL). + */ +dyad_rc_t dyad_dtl_flux_detach_request (const dyad_ctx_t *ctx, void **req_state); + +/** + * @brief Sends file data to the consumer using a previously detached + * request's message, instead of the shared @c msg field. + * + * @details + * Equivalent to @c dyad_dtl_flux_send() but reads the request message from + * @p req_state (as produced by @c dyad_dtl_flux_detach_request()) instead + * of the shared, single-slot field -- safe to call after other requests' + * unpack calls have since overwritten that shared field. Releases the + * message reference via @c flux_msg_decref() and frees @p req_state before + * returning, regardless of success or failure. + * + * @note Must be called from the module's reactor thread -- the @c flux_t + * handle used internally is not safe to touch from any other thread. + * @see dyad_dtl::send_detached_is_thread_safe (false for this + * backend). + * + * @param[in] ctx DYAD context. + * @param[in] req_state Request state from a prior + * @c dyad_dtl_flux_detach_request() call. Freed by + * this call. + * @param[in] buf Buffer containing the file data to send. + * @param[in] buflen Number of bytes in @p buf. + * + * @return @c dyad_rc_t return code: + * @retval DYAD_RC_OK Data sent successfully. + * @retval DYAD_RC_FLUXFAIL @c flux_respond_raw() failed. + */ +dyad_rc_t dyad_dtl_flux_send_detached (const dyad_ctx_t *ctx, + void *req_state, + void *buf, + size_t buflen); + +/** + * @brief Frees a detached request's message reference without sending + * data, for use on an I/O-error path. + * + * @param[in] ctx DYAD context. + * @param[in] req_state Request state from a prior + * @c dyad_dtl_flux_detach_request() call. Freed by + * this call. + * + * @return Always returns @c DYAD_RC_OK. + */ +dyad_rc_t dyad_dtl_flux_abort_detached (const dyad_ctx_t *ctx, void *req_state); + /** * @brief Finalizes and frees the Flux RPC DTL internal state. * diff --git a/src/dyad/dtl/margo_dtl.c b/src/dyad/dtl/margo_dtl.c index b8f7608a..bcafbc20 100644 --- a/src/dyad/dtl/margo_dtl.c +++ b/src/dyad/dtl/margo_dtl.c @@ -383,6 +383,21 @@ dyad_rc_t dyad_dtl_margo_init (const dyad_ctx_t *ctx, ctx->dtl_handle->send = dyad_dtl_margo_send; ctx->dtl_handle->recv = dyad_dtl_margo_recv; ctx->dtl_handle->close_connection = dyad_dtl_margo_close_connection; + ctx->dtl_handle->rpc_detach_request = dyad_dtl_margo_detach_request; + ctx->dtl_handle->rpc_send_detached = dyad_dtl_margo_send_detached; + ctx->dtl_handle->rpc_abort_detached = dyad_dtl_margo_abort_detached; + // send_detached() touches no Flux state, but IS Argobots/Mercury state + // -- and this DTL's producer-side margo_init() (DYAD_COMM_SEND, below) + // creates no dedicated Argobots execution stream, so `mid` is only a + // valid Argobots execution context on the thread that called + // margo_init() (the module's reactor thread). A plain pthread from the + // fetch-worker pool is not an Argobots ULT/execution-stream context at + // all, and calling margo_forward() from one hangs (confirmed: the + // consumer's RDMA pull is never triggered). So, despite touching no + // Flux handle, send_detached() must still be bounced back to the + // reactor thread like Flux RPC's -- worker threads only get the + // blocking file I/O off the reactor thread, not the RDMA send itself. + ctx->dtl_handle->send_detached_is_thread_safe = false; if (comm_mode == DYAD_COMM_SEND) { DYAD_LOG_DEBUG (ctx, "[MARGO DTL] margo dtl initialized - flux side"); @@ -648,14 +663,21 @@ dyad_rc_t dyad_dtl_margo_establish_connection (const dyad_ctx_t *ctx) return rc; } -dyad_rc_t dyad_dtl_margo_send (const dyad_ctx_t *ctx, void *buf, size_t buflen) +// Shared body for dyad_dtl_margo_send() and dyad_dtl_margo_send_detached(): +// registers buf as a bulk handle and RDMA-pushes it to remote_addr. Safe to +// call from any thread -- touches only Margo/Argobots/Mercury state, no +// Flux handle. +static dyad_rc_t dyad_dtl_margo_send_to (const dyad_ctx_t *ctx, + margo_instance_id mid, + hg_id_t sendrecv_rpc_id, + hg_addr_t remote_addr, + void *buf, + size_t buflen) { DYAD_C_FUNCTION_START (); dyad_rc_t rc = DYAD_RC_OK; hg_return_t ret = HG_SUCCESS; - dyad_dtl_margo_t *margo_handle = ctx->dtl_handle->private_dtl.margo_dtl_handle; - DYAD_LOG_DEBUG (ctx, "[MARGO DTL] margo_send is called, buflen: %ld.", buflen); hg_size_t segment_sizes[1] = {buflen}; @@ -667,12 +689,7 @@ dyad_rc_t dyad_dtl_margo_send (const dyad_ctx_t *ctx, void *buf, size_t buflen) // Register my local data // which will be pulled by the consumer - ret = margo_bulk_create (margo_handle->mid, - 1, - segment_ptrs, - segment_sizes, - HG_BULK_READ_ONLY, - &local_bulk); + ret = margo_bulk_create (mid, 1, segment_ptrs, segment_sizes, HG_BULK_READ_ONLY, &local_bulk); if (ret != HG_SUCCESS) { DYAD_LOG_ERROR (ctx, "margo_bulk_create failed: %d", (int)ret); goto margo_error_bulk; @@ -682,7 +699,7 @@ dyad_rc_t dyad_dtl_margo_send (const dyad_ctx_t *ctx, void *buf, size_t buflen) args.bulk = local_bulk; // send a message to the consumer, notifying it that my data is ready - ret = margo_create (margo_handle->mid, margo_handle->remote_addr, margo_handle->sendrecv_rpc_id, &mh); + ret = margo_create (mid, remote_addr, sendrecv_rpc_id, &mh); if (ret != HG_SUCCESS) { DYAD_LOG_ERROR (ctx, "margo_create failed: %d", (int)ret); goto margo_error; @@ -719,6 +736,74 @@ margo_error_bulk:; return DYAD_RC_MARGOINIT_FAIL; } +dyad_rc_t dyad_dtl_margo_send (const dyad_ctx_t *ctx, void *buf, size_t buflen) +{ + dyad_dtl_margo_t *margo_handle = ctx->dtl_handle->private_dtl.margo_dtl_handle; + return dyad_dtl_margo_send_to (ctx, + margo_handle->mid, + margo_handle->sendrecv_rpc_id, + margo_handle->remote_addr, + buf, + buflen); +} + +dyad_rc_t dyad_dtl_margo_detach_request (const dyad_ctx_t *ctx, void **req_state) +{ + DYAD_C_FUNCTION_START (); + struct dyad_dtl_margo_req_state *state = NULL; + dyad_dtl_margo_t *margo_handle = ctx->dtl_handle->private_dtl.margo_dtl_handle; + + state = (struct dyad_dtl_margo_req_state *)malloc (sizeof (struct dyad_dtl_margo_req_state)); + if (state == NULL) { + DYAD_C_FUNCTION_END (); + return DYAD_RC_SYSFAIL; + } + state->mid = margo_handle->mid; + state->sendrecv_rpc_id = margo_handle->sendrecv_rpc_id; + state->remote_addr = margo_handle->remote_addr; + // Transfer ownership out of the shared field so a later request's + // margo_addr_lookup() can't be mistaken for this one, and so this + // address gets freed exactly once (by send_detached()) instead of + // being silently overwritten/leaked as today's serial code does. + margo_handle->remote_addr = NULL; + *req_state = state; + + DYAD_C_FUNCTION_END (); + return DYAD_RC_OK; +} + +dyad_rc_t dyad_dtl_margo_send_detached (const dyad_ctx_t *ctx, + void *req_state, + void *buf, + size_t buflen) +{ + dyad_rc_t rc = DYAD_RC_OK; + struct dyad_dtl_margo_req_state *state = (struct dyad_dtl_margo_req_state *)req_state; + + rc = dyad_dtl_margo_send_to (ctx, + state->mid, + state->sendrecv_rpc_id, + state->remote_addr, + buf, + buflen); + + if (state->remote_addr != NULL) { + margo_addr_free (state->mid, state->remote_addr); + } + free (state); + return rc; +} + +dyad_rc_t dyad_dtl_margo_abort_detached (const dyad_ctx_t *ctx, void *req_state) +{ + struct dyad_dtl_margo_req_state *state = (struct dyad_dtl_margo_req_state *)req_state; + if (state->remote_addr != NULL) { + margo_addr_free (state->mid, state->remote_addr); + } + free (state); + return DYAD_RC_OK; +} + dyad_rc_t dyad_dtl_margo_recv (const dyad_ctx_t *ctx, void **buf, size_t *buflen) { DYAD_C_FUNCTION_START (); diff --git a/src/dyad/dtl/margo_dtl.h b/src/dyad/dtl/margo_dtl.h index d74af851..3cb24a41 100644 --- a/src/dyad/dtl/margo_dtl.h +++ b/src/dyad/dtl/margo_dtl.h @@ -25,6 +25,20 @@ struct dyad_dtl_margo { typedef struct dyad_dtl_margo dyad_dtl_margo_t; +/** + * @brief Per-request state detached from the shared @c dyad_dtl_margo + * fields by @c dyad_dtl_margo_detach_request(), so a request can be + * finished (RDMA push via @c margo_forward()) directly from a + * worker thread without racing a later request's + * @c margo_addr_lookup() call, which would otherwise overwrite the + * same shared @c remote_addr field. + */ +struct dyad_dtl_margo_req_state { + margo_instance_id mid; + hg_id_t sendrecv_rpc_id; + hg_addr_t remote_addr; // owned; freed by dyad_dtl_margo_send_detached() +}; + /** * @brief Initializes the Margo DTL internal state. * @@ -501,6 +515,90 @@ dyad_rc_t dyad_dtl_margo_recv (const dyad_ctx_t *ctx, void **buf, size_t *buflen */ dyad_rc_t dyad_dtl_margo_close_connection (const dyad_ctx_t *ctx); +/** + * @brief Detaches the current request's resolved consumer address from the + * shared, single-slot @c remote_addr field into an + * independently-owned request state blob. + * + * @details + * Copies @c mid, @c sendrecv_rpc_id (process-wide, immutable, just + * convenience copies so @c dyad_dtl_margo_send_detached() never has to + * touch the shared @c dyad_dtl_margo struct at all) and @c remote_addr + * (resolved by the preceding @c dyad_dtl_margo_rpc_unpack_range() call) out + * of the shared fields into a newly allocated + * @c struct dyad_dtl_margo_req_state, and clears the shared + * @c remote_addr field (transferring ownership -- this also fixes a + * pre-existing leak where the previous request's resolved address was + * never freed before being overwritten). + * + * @param[in] ctx DYAD context. + * @param[out] req_state Set to a newly allocated request-state blob owning + * the resolved consumer address. Must be passed to + * @c dyad_dtl_margo_send_detached() exactly once. + * + * @return Always returns @c DYAD_RC_OK, unless allocation fails + * (@c DYAD_RC_SYSFAIL). + */ +dyad_rc_t dyad_dtl_margo_detach_request (const dyad_ctx_t *ctx, void **req_state); + +/** + * @brief Sends file data to the consumer via Margo RDMA using a previously + * detached request's resolved address, instead of the shared + * @c remote_addr field. + * + * @details + * Equivalent to @c dyad_dtl_margo_send(), but reads @c mid, + * @c sendrecv_rpc_id, and @c remote_addr from @p req_state (as produced by + * @c dyad_dtl_margo_detach_request()) instead of the shared fields. + * Frees @c remote_addr via @c margo_addr_free() and frees @p req_state + * before returning. + * + * @note Must be called from the module's reactor thread, like the Flux RPC + * backend's equivalent -- @c margo_init() for this DTL's + * producer/@c DYAD_COMM_SEND side creates no dedicated Argobots + * execution stream, so @c mid is only a valid Argobots execution + * context on the thread that called @c margo_init() (the reactor + * thread). A plain worker-pool @c pthread is not an Argobots + * ULT/execution-stream context, and calling @c margo_forward() from + * one hangs (the consumer's RDMA pull is never triggered). + * @see dyad_dtl::send_detached_is_thread_safe (false for this + * backend). + * + * @param[in] ctx DYAD context. + * @param[in] req_state Request state from a prior + * @c dyad_dtl_margo_detach_request() call. Freed by + * this call. + * @param[in] buf Buffer containing the file data to send. + * @param[in] buflen Number of bytes in @p buf. + * + * @return @c dyad_rc_t return code: + * @retval DYAD_RC_OK Always returned. Error handling for Margo calls is + * not yet implemented, matching @c dyad_dtl_margo_send() + * (see TODO there). + */ +dyad_rc_t dyad_dtl_margo_send_detached (const dyad_ctx_t *ctx, + void *req_state, + void *buf, + size_t buflen); + +/** + * @brief Frees a detached request's resolved address without sending + * data, for use on an I/O-error path. + * + * @details + * Frees @c remote_addr via @c margo_addr_free() (mirroring + * @c dyad_dtl_margo_send_detached()'s cleanup) and frees @p req_state. + * Safe to call from any thread -- touches only Margo/Mercury state. + * + * @param[in] ctx DYAD context. + * @param[in] req_state Request state from a prior + * @c dyad_dtl_margo_detach_request() call. Freed by + * this call. + * + * @return Always returns @c DYAD_RC_OK. + */ +dyad_rc_t dyad_dtl_margo_abort_detached (const dyad_ctx_t *ctx, void *req_state); + /** * @brief Finalizes and frees the Margo DTL internal state. * diff --git a/src/dyad/dtl/ucx_dtl.c b/src/dyad/dtl/ucx_dtl.c index 6121b774..e487f524 100644 --- a/src/dyad/dtl/ucx_dtl.c +++ b/src/dyad/dtl/ucx_dtl.c @@ -830,6 +830,15 @@ dyad_rc_t dyad_dtl_ucx_init (const dyad_ctx_t *ctx, ctx->dtl_handle->send = dyad_dtl_ucx_send; ctx->dtl_handle->recv = dyad_dtl_ucx_recv; ctx->dtl_handle->close_connection = dyad_dtl_ucx_close_connection; + // Detached/threaded servicing (worker-thread offload in + // dyad_fetch_range_request_cb()) is only implemented for FLUX_RPC and + // MARGO. Leaving rpc_detach_request NULL makes the module fall back to + // its fully-synchronous inline path for UCX, unchanged from before + // that offload existed. + ctx->dtl_handle->rpc_detach_request = NULL; + ctx->dtl_handle->rpc_send_detached = NULL; + ctx->dtl_handle->rpc_abort_detached = NULL; + ctx->dtl_handle->send_detached_is_thread_safe = false; rc = ucx_warmup (ctx); if (DYAD_IS_ERROR (rc)) { diff --git a/src/dyad/service/flux_module/CMakeLists.txt b/src/dyad/service/flux_module/CMakeLists.txt index 8feae6b6..693f0809 100644 --- a/src/dyad/service/flux_module/CMakeLists.txt +++ b/src/dyad/service/flux_module/CMakeLists.txt @@ -15,6 +15,8 @@ set(DYAD_FLUX_MODULE_PRIVATE_HEADERS ${CMAKE_SOURCE_DIR}/include/dyad/common/dya ${CMAKE_CURRENT_SOURCE_DIR}/../../utils/utils.h) set(DYAD_FLUX_MODULE_PUBLIC_HEADERS) +find_package(Threads REQUIRED) + add_library(${DYAD_FLUX_MODULE} SHARED ${DYAD_FLUX_MODULE_SRC} ${DYAD_FLUX_MODULE_PRIVATE_HEADERS} ${DYAD_FLUX_MODULE_PUBLIC_HEADERS}) set_target_properties(${DYAD_FLUX_MODULE} PROPERTIES PREFIX "") @@ -22,6 +24,7 @@ target_link_libraries(${DYAD_FLUX_MODULE} PRIVATE Jansson::Jansson) target_link_libraries(${DYAD_FLUX_MODULE} PRIVATE ${PROJECT_NAME}_dtl) target_link_libraries(${DYAD_FLUX_MODULE} PRIVATE ${PROJECT_NAME}_ctx) target_link_libraries(${DYAD_FLUX_MODULE} PRIVATE ${PROJECT_NAME}_utils) +target_link_libraries(${DYAD_FLUX_MODULE} PRIVATE Threads::Threads) target_compile_definitions(${DYAD_FLUX_MODULE} PRIVATE BUILDING_DYAD=1) target_compile_definitions(${DYAD_FLUX_MODULE} PUBLIC DYAD_HAS_CONFIG) target_include_directories(${DYAD_FLUX_MODULE} PUBLIC diff --git a/src/dyad/service/flux_module/dyad.c b/src/dyad/service/flux_module/dyad.c index 5d116963..4b926b39 100644 --- a/src/dyad/service/flux_module/dyad.c +++ b/src/dyad/service/flux_module/dyad.c @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -90,9 +91,69 @@ * module instance. Allocated per broker handle via @c get_mod_ctx() and * freed at module finalization time via @c freectx(). */ +/** + * @brief One in-flight @c dyad.fetch_range request handed off from the + * reactor thread to the worker-thread pool. + * + * @details + * Populated by @c dyad_fetch_range_request_cb() on the reactor thread + * (everything needed to do the request's file I/O without touching the DTL + * or Flux state again), then filled in by a worker thread + * (@c dyad_fetch_worker_process()) with the outcome. Freed by whichever + * code path finishes the request -- either a worker thread directly + * (Margo, @c send_detached_is_thread_safe) or the reactor-thread completion + * callback (Flux RPC). + */ +struct dyad_fetch_work_item { + char fullpath[PATH_MAX + 1]; + char origin_fullpath[PATH_MAX + 1]; + bool has_origin; + size_t offset; + size_t length; + void *inbuf; ///< Allocated via dtl_handle->get_buffer() on the reactor thread. + void *req_state; ///< From dtl_handle->rpc_detach_request(). + flux_msg_t *msg; ///< Incref'd; used only for flux_respond_error() on failure. + bool send_detached_is_thread_safe; + struct dyad_mod_ctx *mod_ctx; + // Outcome, filled in by the worker: + dyad_rc_t rc; + int saved_errno; + ssize_t range_len; + struct dyad_fetch_work_item *next; +}; + typedef struct dyad_mod_ctx { flux_msg_handler_t **handlers; ///< Flux message handler table. dyad_ctx_t *ctx; ///< DYAD context for this module instance. + + // --- Worker-thread pool for dyad_fetch_range_request_cb()'s blocking + // I/O (and, for the Margo DTL, the RDMA send itself). See + // DYAD_FETCH_WORKER_THREADS_ENV. NULL/0/unset until mod_main() creates + // the pool; dyad_fetch_range_request_cb() falls back to the original + // fully-synchronous inline path whenever the active DTL backend's + // rpc_detach_request is NULL (currently UCX), so these fields are only + // ever touched when Flux RPC or Margo is active. + int num_fetch_workers; + pthread_t *fetch_workers; + pthread_mutex_t work_mutex; + pthread_cond_t work_cond; + struct dyad_fetch_work_item *work_head; + struct dyad_fetch_work_item *work_tail; + bool workers_shutdown; + + // --- Completion hand-back to the reactor thread, needed only for + // DTL backends where send_detached_is_thread_safe is false (Flux RPC): + // flux_respond_raw()/flux_respond_error() touch the module's flux_t + // handle, which must only be touched from the thread that owns its + // reactor. A worker thread finishing such a request pushes it onto + // completion_head/tail and writes one byte to completion_pipe[1]; the + // reactor-thread fd-watcher (completion_watcher) drains the pipe and + // finishes each item on completion_head/tail. + int completion_pipe[2]; + flux_watcher_t *completion_watcher; + pthread_mutex_t completion_mutex; + struct dyad_fetch_work_item *completion_head; + struct dyad_fetch_work_item *completion_tail; } dyad_mod_ctx_t; const struct dyad_mod_ctx dyad_mod_ctx_default = {0}; @@ -147,6 +208,19 @@ static void freectx (void *arg) dyad_ctx_fini (); mod_ctx->ctx = NULL; } + // The worker pool (if created) is already stopped and joined by + // mod_main() before it returns, and completion_watcher already + // destroyed there too -- this only frees the primitives themselves. + free (mod_ctx->fetch_workers); + pthread_mutex_destroy (&mod_ctx->work_mutex); + pthread_cond_destroy (&mod_ctx->work_cond); + pthread_mutex_destroy (&mod_ctx->completion_mutex); + if (mod_ctx->completion_pipe[0] != -1) { + close (mod_ctx->completion_pipe[0]); + } + if (mod_ctx->completion_pipe[1] != -1) { + close (mod_ctx->completion_pipe[1]); + } free (mod_ctx); } @@ -176,6 +250,19 @@ static dyad_mod_ctx_t *get_mod_ctx (flux_t *h) } mod_ctx->handlers = NULL; mod_ctx->ctx = NULL; + mod_ctx->num_fetch_workers = 0; + mod_ctx->fetch_workers = NULL; + mod_ctx->work_head = NULL; + mod_ctx->work_tail = NULL; + mod_ctx->workers_shutdown = false; + mod_ctx->completion_pipe[0] = -1; + mod_ctx->completion_pipe[1] = -1; + mod_ctx->completion_watcher = NULL; + mod_ctx->completion_head = NULL; + mod_ctx->completion_tail = NULL; + pthread_mutex_init (&mod_ctx->work_mutex, NULL); + pthread_cond_init (&mod_ctx->work_cond, NULL); + pthread_mutex_init (&mod_ctx->completion_mutex, NULL); if (flux_aux_set (h, "dyad", mod_ctx, freectx) < 0) { DYAD_LOG_STDERR ("DYAD_MOD: flux_aux_set() failed!"); @@ -429,6 +516,350 @@ end_fetch_cb:; return; } +// ========================================================================= +// Worker-thread pool for dyad_fetch_range_request_cb()'s blocking I/O. +// +// dyad_fetch_range_request_cb() (below) does the cheap, message-parsing +// part inline on the reactor thread, then -- for DTL backends that support +// it (Flux RPC, Margo; see dyad_dtl::rpc_detach_request) -- hands the rest +// of the request off to one of these worker threads as a +// dyad_fetch_work_item, so concurrent requests to this broker don't +// serialize behind whichever one happened to arrive first. +// +// Every completed item, regardless of backend, ends up on +// completion_head/tail and gets a wakeup byte written to completion_pipe -- +// even for backends where dyad_fetch_worker_finish() already did the data +// send directly on the worker thread (send_detached_is_thread_safe) -- +// because the trailing flux_respond_error(..., ENODATA, ...) call that +// closes the streaming RPC always touches the module's flux_t handle, and +// must therefore always run on the reactor thread +// (dyad_fetch_completion_watcher_cb()), regardless of backend. +// ========================================================================= + +// Frees everything a work item owns. rpc_send_detached()/rpc_abort_detached() +// must already have been called (req_state consumed) before this runs. +static void dyad_fetch_work_item_destroy (dyad_mod_ctx_t *mod_ctx, + struct dyad_fetch_work_item *item) +{ + if (item->inbuf != NULL) { + mod_ctx->ctx->dtl_handle->return_buffer (mod_ctx->ctx, (void **)&item->inbuf); + } + if (item->msg != NULL) { + flux_msg_decref (item->msg); + } + free (item); +} + +// Runs on the reactor thread (registered as a flux_fd_watcher_create() +// callback on completion_pipe's read end): drains the pipe, then finishes +// every item on the completion queue -- the data send, for backends where +// it wasn't already done on a worker thread (item->req_state still set), +// and always the trailing flux_respond_error() that closes the streaming +// RPC. +static void dyad_fetch_completion_watcher_cb (flux_reactor_t *r, + flux_watcher_t *w, + int revents, + void *arg) +{ + dyad_mod_ctx_t *mod_ctx = (dyad_mod_ctx_t *)arg; + dyad_ctx_t *ctx = mod_ctx->ctx; + char drain_buf[256]; + ssize_t n; + struct dyad_fetch_work_item *items = NULL; + + do { + n = read (mod_ctx->completion_pipe[0], drain_buf, sizeof (drain_buf)); + } while (n > 0); + + pthread_mutex_lock (&mod_ctx->completion_mutex); + items = mod_ctx->completion_head; + mod_ctx->completion_head = NULL; + mod_ctx->completion_tail = NULL; + pthread_mutex_unlock (&mod_ctx->completion_mutex); + + while (items != NULL) { + struct dyad_fetch_work_item *item = items; + items = items->next; + item->next = NULL; + + if (!DYAD_IS_ERROR (item->rc)) { + if (item->req_state != NULL) { + dyad_rc_t rc = ctx->dtl_handle->rpc_send_detached (ctx, + item->req_state, + item->inbuf, + (size_t)item->range_len); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, + "DYAD_MOD: rpc_send_detached failed for \"%s\".", + item->fullpath); + } + } + if (flux_respond_error (ctx->h, item->msg, ENODATA, NULL) < 0) { + DYAD_LOG_DEBUG (ctx, + "DYAD_MOD: %s: flux_respond_error with ENODATA failed\n", + __func__); + } + } else { + if (item->req_state != NULL) { + ctx->dtl_handle->rpc_abort_detached (ctx, item->req_state); + } + if (flux_respond_error (ctx->h, item->msg, item->saved_errno, NULL) < 0) { + DYAD_LOG_ERROR (ctx, "DYAD_MOD: %s: flux_respond_error", __func__); + } + } + dyad_fetch_work_item_destroy (mod_ctx, item); + } +} + +// Hands a finished (or failed) item to the completion queue and wakes the +// reactor thread. For thread-safe backends (Margo), the data send has +// already happened by the time this is called (item->req_state cleared); +// for others (Flux RPC) it's still pending and item->req_state is left set +// for dyad_fetch_completion_watcher_cb() to finish. +static void dyad_fetch_worker_finish (dyad_mod_ctx_t *mod_ctx, struct dyad_fetch_work_item *item) +{ + dyad_ctx_t *ctx = mod_ctx->ctx; + char byte = 1; + ssize_t written; + + if (item->send_detached_is_thread_safe) { + if (!DYAD_IS_ERROR (item->rc)) { + dyad_rc_t rc = ctx->dtl_handle->rpc_send_detached (ctx, + item->req_state, + item->inbuf, + (size_t)item->range_len); + if (DYAD_IS_ERROR (rc)) { + item->rc = rc; + item->saved_errno = ECOMM; + } + } else { + ctx->dtl_handle->rpc_abort_detached (ctx, item->req_state); + } + item->req_state = NULL; // consumed -- the completion callback must not touch it + } + + pthread_mutex_lock (&mod_ctx->completion_mutex); + item->next = NULL; + if (mod_ctx->completion_tail == NULL) { + mod_ctx->completion_head = item; + } else { + mod_ctx->completion_tail->next = item; + } + mod_ctx->completion_tail = item; + pthread_mutex_unlock (&mod_ctx->completion_mutex); + + do { + written = write (mod_ctx->completion_pipe[1], &byte, 1); + } while (written < 0 && errno == EINTR); + // A lost wakeup (e.g. EAGAIN on a full pipe -- vanishingly unlikely for + // single-byte writes) is harmless: the queue is drained in full on + // every wakeup, so any later completion's wakeup picks this item up + // too. +} + +// Runs entirely on a worker thread: the blocking file I/O that used to be +// inline in dyad_fetch_range_request_cb(). Touches no Flux/DTL state +// directly -- only item->req_state (opaque, backend-specific) via +// dyad_fetch_worker_finish() above. +static void dyad_fetch_worker_process (dyad_mod_ctx_t *mod_ctx, struct dyad_fetch_work_item *item) +{ + dyad_ctx_t *ctx = mod_ctx->ctx; + int fd = -1; + ssize_t file_size = 0l; + ssize_t range_len = 0l; + dyad_rc_t rc = DYAD_RC_OK; + int saved_errno = 0; + struct flock shared_lock; + + if (item->has_origin) { + rc = dyad_range_cache_ensure (ctx, + item->fullpath, + item->origin_fullpath, + item->offset, + item->length); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_STDERR ("DYAD_MOD: dyad_range_cache_ensure failed for file \"%s\".\n", + item->fullpath); + saved_errno = EIO; + goto worker_error_no_fd; + } + } + + fd = open (item->fullpath, O_RDONLY); + if (fd < 0) { + DYAD_LOG_STDERR ("DYAD_MOD: Failed to open file \"%s\".\n", item->fullpath); + saved_errno = errno; + goto worker_error_no_fd; + } + rc = dyad_shared_flock (ctx, fd, &shared_lock); + if (DYAD_IS_ERROR (rc)) { + saved_errno = errno; + goto worker_error_fd; + } + file_size = get_file_size (fd); + // Clamp against the real file size for safety -- offset/length come + // from the consumer's own index and should already be valid, but a + // stale/corrupt index must not turn into an out-of-bounds pread(). + if ((ssize_t)item->offset >= file_size) { + range_len = 0; + } else if ((ssize_t)(item->offset + item->length) > file_size) { + range_len = file_size - (ssize_t)item->offset; + } else { + range_len = (ssize_t)item->length; + } + + if (range_len > 0l) { + ssize_t read_data = 0; + int granularity = DYAD_POSIX_TRANSFER_GRANULARITY; + while (read_data < range_len) { + ssize_t read_size = + (range_len - read_data) > granularity ? granularity : (range_len - read_data); + ssize_t inlen = pread (fd, + (char *)item->inbuf + read_data, + (size_t)read_size, + (off_t)(item->offset + (size_t)read_data)); + if (inlen <= 0) { + DYAD_LOG_STDERR ("DYAD_MOD: Failed to load range of file \"%s\" (errno %d: %s).\n", + item->fullpath, + errno, + strerror (errno)); + saved_errno = errno ? errno : EIO; + dyad_release_flock (ctx, fd, &shared_lock); + goto worker_error_fd; + } + read_data += inlen; + } + } + dyad_release_flock (ctx, fd, &shared_lock); + close (fd); + + item->rc = DYAD_RC_OK; + item->range_len = range_len; + dyad_fetch_worker_finish (mod_ctx, item); + return; + +worker_error_fd:; + close (fd); +worker_error_no_fd:; + item->rc = DYAD_RC_BADFIO; + item->saved_errno = saved_errno; + dyad_fetch_worker_finish (mod_ctx, item); +} + +// Worker-thread entry point: pops items off the shared work queue and +// processes them until told to shut down. +static void *dyad_fetch_worker_main (void *arg) +{ + dyad_mod_ctx_t *mod_ctx = (dyad_mod_ctx_t *)arg; + for (;;) { + struct dyad_fetch_work_item *item = NULL; + pthread_mutex_lock (&mod_ctx->work_mutex); + while (mod_ctx->work_head == NULL && !mod_ctx->workers_shutdown) { + pthread_cond_wait (&mod_ctx->work_cond, &mod_ctx->work_mutex); + } + if (mod_ctx->work_head == NULL && mod_ctx->workers_shutdown) { + pthread_mutex_unlock (&mod_ctx->work_mutex); + break; + } + item = mod_ctx->work_head; + mod_ctx->work_head = item->next; + if (mod_ctx->work_head == NULL) { + mod_ctx->work_tail = NULL; + } + pthread_mutex_unlock (&mod_ctx->work_mutex); + item->next = NULL; + + dyad_fetch_worker_process (mod_ctx, item); + } + return NULL; +} + +// Creates completion_pipe/completion_watcher and the fetch-worker pool +// (size from DYAD_FETCH_WORKER_THREADS_ENV, default 8). On any failure, +// leaves mod_ctx->num_fetch_workers at 0 -- dyad_fetch_range_request_cb() +// checks this and falls back to the fully-synchronous path, so a pool +// start failure degrades performance but is not fatal to the module. +static dyad_rc_t dyad_fetch_pool_start (dyad_mod_ctx_t *mod_ctx) +{ + int i = 0; + int num_workers = 8; + int flags = 0; + const char *env_workers = getenv (DYAD_FETCH_WORKER_THREADS_ENV); + if (env_workers != NULL) { + int parsed = atoi (env_workers); + if (parsed > 0) { + num_workers = parsed; + } + } + + if (pipe (mod_ctx->completion_pipe) != 0) { + DYAD_LOG_STDERR ("DYAD_MOD: pipe() failed for fetch completion queue: %s\n", + strerror (errno)); + return DYAD_RC_SYSFAIL; + } + // Non-blocking on both ends: the reactor thread must never block + // draining an empty pipe, and a worker thread must never block writing + // a single wakeup byte (see the "lost wakeup" comment in + // dyad_fetch_worker_finish()). + flags = fcntl (mod_ctx->completion_pipe[0], F_GETFL, 0); + fcntl (mod_ctx->completion_pipe[0], F_SETFL, flags | O_NONBLOCK); + flags = fcntl (mod_ctx->completion_pipe[1], F_GETFL, 0); + fcntl (mod_ctx->completion_pipe[1], F_SETFL, flags | O_NONBLOCK); + + mod_ctx->completion_watcher = flux_fd_watcher_create (flux_get_reactor (mod_ctx->ctx->h), + mod_ctx->completion_pipe[0], + FLUX_POLLIN, + dyad_fetch_completion_watcher_cb, + mod_ctx); + if (mod_ctx->completion_watcher == NULL) { + DYAD_LOG_STDERR ("DYAD_MOD: flux_fd_watcher_create() failed for fetch completion queue\n"); + return DYAD_RC_SYSFAIL; + } + flux_watcher_start (mod_ctx->completion_watcher); + + mod_ctx->fetch_workers = (pthread_t *)calloc ((size_t)num_workers, sizeof (pthread_t)); + if (mod_ctx->fetch_workers == NULL) { + return DYAD_RC_SYSFAIL; + } + for (i = 0; i < num_workers; i++) { + if (pthread_create (&mod_ctx->fetch_workers[i], NULL, dyad_fetch_worker_main, mod_ctx) + != 0) { + DYAD_LOG_STDERR ("DYAD_MOD: pthread_create() failed for fetch worker %d\n", i); + break; + } + } + mod_ctx->num_fetch_workers = i; + if (mod_ctx->num_fetch_workers == 0) { + return DYAD_RC_SYSFAIL; + } + DYAD_LOG_STDOUT ("DYAD_MOD: started %d fetch-worker thread(s)\n", mod_ctx->num_fetch_workers); + return DYAD_RC_OK; +} + +// Signals shutdown and joins all fetch-worker threads, then stops the +// completion watcher. Safe to call even if dyad_fetch_pool_start() was +// never called or failed before creating any threads. +static void dyad_fetch_pool_stop (dyad_mod_ctx_t *mod_ctx) +{ + int i; + if (mod_ctx->fetch_workers == NULL) { + return; + } + pthread_mutex_lock (&mod_ctx->work_mutex); + mod_ctx->workers_shutdown = true; + pthread_cond_broadcast (&mod_ctx->work_cond); + pthread_mutex_unlock (&mod_ctx->work_mutex); + + for (i = 0; i < mod_ctx->num_fetch_workers; i++) { + pthread_join (mod_ctx->fetch_workers[i], NULL); + } + if (mod_ctx->completion_watcher != NULL) { + flux_watcher_stop (mod_ctx->completion_watcher); + flux_watcher_destroy (mod_ctx->completion_watcher); + mod_ctx->completion_watcher = NULL; + } +} + /** * @brief Flux message handler callback that serves a byte range of a file * to a consumer via RPC. @@ -513,6 +944,75 @@ static void dyad_fetch_range_request_cb (flux_t *h, concat_str (fullpath, upath, "/", PATH_MAX); DYAD_C_FUNCTION_UPDATE_STR ("fullpath", fullpath); + if (mod_ctx->ctx->dtl_handle->rpc_detach_request != NULL && mod_ctx->num_fetch_workers > 0) { + // Threaded path (Flux RPC or Margo, worker pool available): hand + // the blocking I/O -- and, for thread-safe backends, the send + // itself -- off to the worker-thread pool instead of doing it + // inline on this reactor thread. See the worker-pool block above + // dyad_fetch_range_request_cb() for the full design. + struct dyad_fetch_work_item *item = + (struct dyad_fetch_work_item *)calloc (1ul, sizeof (struct dyad_fetch_work_item)); + if (item == NULL) { + DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: Could not allocate fetch work item"); + errno = ENOMEM; + goto fetch_range_error_wo_flock; + } + snprintf (item->fullpath, sizeof (item->fullpath), "%s", fullpath); + item->has_origin = (mod_ctx->ctx->origin_path != NULL); + if (item->has_origin) { + strncpy (item->origin_fullpath, mod_ctx->ctx->origin_path, PATH_MAX - 1); + concat_str (item->origin_fullpath, upath, "/", PATH_MAX); + } + item->offset = offset; + item->length = length; + item->send_detached_is_thread_safe = mod_ctx->ctx->dtl_handle->send_detached_is_thread_safe; + item->mod_ctx = mod_ctx; + // Standard Flux idiom: retain a reference to the request message + // past this synchronous callback -- needed regardless of backend + // for the trailing flux_respond_error() call in + // dyad_fetch_completion_watcher_cb(). + item->msg = (flux_msg_t *)flux_msg_incref (msg); + + rc = mod_ctx->ctx->dtl_handle->rpc_detach_request (mod_ctx->ctx, &item->req_state); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: rpc_detach_request failed"); + flux_msg_decref (item->msg); + free (item); + errno = EPROTO; + goto fetch_range_error_wo_flock; + } + // Sized to the client-requested length as an upper bound -- the + // true clamped range_len isn't known until the worker opens the + // file, functionally identical to the fallback path below, just + // computed earlier. + rc = mod_ctx->ctx->dtl_handle->get_buffer (mod_ctx->ctx, length, &item->inbuf); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (mod_ctx->ctx, + "DYAD_MOD: Could not allocate DTL buffer for ranged fetch"); + mod_ctx->ctx->dtl_handle->rpc_abort_detached (mod_ctx->ctx, item->req_state); + flux_msg_decref (item->msg); + free (item); + errno = ENOMEM; + goto fetch_range_error_wo_flock; + } + + pthread_mutex_lock (&mod_ctx->work_mutex); + item->next = NULL; + if (mod_ctx->work_tail == NULL) { + mod_ctx->work_head = item; + } else { + mod_ctx->work_tail->next = item; + } + mod_ctx->work_tail = item; + pthread_cond_signal (&mod_ctx->work_cond); + pthread_mutex_unlock (&mod_ctx->work_mutex); + + DYAD_C_FUNCTION_END (); + return; + } + + // Fallback path (UCX, or if the worker pool failed to start): fully + // synchronous, unchanged from before threaded servicing existed. if (mod_ctx->ctx->origin_path != NULL) { char origin_fullpath[PATH_MAX + 1] = {'\0'}; strncpy (origin_fullpath, mod_ctx->ctx->origin_path, PATH_MAX - 1); @@ -1077,10 +1577,21 @@ DYAD_DLL_EXPORTED int mod_main (flux_t *h, int argc, char **argv) goto mod_error; } + // Not fatal if it fails -- dyad_fetch_range_request_cb() checks + // num_fetch_workers and falls back to the fully-synchronous path, so a + // pool-start failure degrades performance but not correctness. + if (DYAD_IS_ERROR (dyad_fetch_pool_start (mod_ctx))) { + DYAD_LOG_STDERR ( + "DYAD_MOD: fetch worker pool failed to start; falling back to " + "synchronous fetch servicing\n"); + } + if (flux_reactor_run (flux_get_reactor (mod_ctx->ctx->h), 0) < 0) { DYAD_LOG_ERROR (mod_ctx->ctx, "DYAD_MOD: flux_reactor_run: %s\n", strerror (errno)); + dyad_fetch_pool_stop (mod_ctx); goto mod_error; } + dyad_fetch_pool_stop (mod_ctx); DYAD_LOG_STDOUT ("DYAD_MOD: Finished\n"); goto mod_done; diff --git a/src/dyad/utils/CMakeLists.txt b/src/dyad/utils/CMakeLists.txt index b51b1acc..65b68def 100644 --- a/src/dyad/utils/CMakeLists.txt +++ b/src/dyad/utils/CMakeLists.txt @@ -13,13 +13,16 @@ set(DYAD_MURMUR3_SRC ${CMAKE_CURRENT_SOURCE_DIR}/murmur3.c) set(DYAD_MURMUR3_PRIVATE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/murmur3.h) set(DYAD_MURMUR3_PUBLIC_HEADERS) +find_package(Threads REQUIRED) + add_library(${PROJECT_NAME}_utils SHARED ${DYAD_UTILS_SRC} ${DYAD_UTILS_PRIVATE_HEADERS} ${DYAD_UTILS_PUBLIC_HEADERS}) # set_target_properties(${PROJECT_NAME}_utils PROPERTIES CMAKE_INSTALL_RPATH # "${DYAD_INSTALL_LIBDIR}") target_link_libraries(${PROJECT_NAME}_utils PUBLIC ${PROJECT_NAME}_base64 - ${PROJECT_NAME}_murmur3) + ${PROJECT_NAME}_murmur3 + Threads::Threads) if(DYAD_LOGGER STREQUAL "CPP_LOGGER") target_link_libraries(${PROJECT_NAME}_utils PRIVATE ${cpp-logger_LIBRARIES}) diff --git a/src/dyad/utils/range_cache.c b/src/dyad/utils/range_cache.c index 3df17fcf..140ac5b0 100644 --- a/src/dyad/utils/range_cache.c +++ b/src/dyad/utils/range_cache.c @@ -10,6 +10,7 @@ // clang-format off #include +#include #include #include #include @@ -37,6 +38,24 @@ #define DYAD_RANGE_CACHE_SUFFIX ".dyad_cached" +// fcntl()-based advisory locks (dyad_shared_flock()/dyad_excl_flock(), used +// below) only provide mutual exclusion between *processes*, not between +// threads of the same process -- two threads in one process can both hold +// a conflicting fcntl() lock simultaneously. That's fine for this range +// cache's original use (one thread per process), but now that a single +// DYAD broker module process can service multiple fetch requests +// concurrently via a worker-thread pool, two of its own worker threads can +// race on the same bitmap file's read-modify-write without this mutex. +// Held *in addition to*, not instead of, the fcntl() locks below, which +// remain necessary for the cross-process case (e.g. this module's worker +// threads racing a local consumer process reading the same shard directly, +// see dyad_client.c's dyad_consume_range()). A single global mutex is fine +// here since the guarded work is a few KB of pread()/bit-check/pwrite() on +// the bitmap file (microseconds) -- the slow origin fetch always happens +// with neither lock held (see the "no lock held" comment on +// range_cache_fetch_span() below). +static pthread_mutex_t g_range_cache_mutex = PTHREAD_MUTEX_INITIALIZER; + static dyad_rc_t range_cache_bitmap_path (const char *local_path, char *out, size_t out_capacity) { if (snprintf (out, out_capacity, "%s%s", local_path, DYAD_RANGE_CACHE_SUFFIX) @@ -182,10 +201,21 @@ dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, return DYAD_RC_BADFIO; } + // g_range_cache_mutex guards against races between worker threads of + // *this* process (fcntl() below only excludes other processes -- see + // the comment on g_range_cache_mutex's declaration). Held continuously + // across the fast-path check and the metadata-init+recheck below (both + // read-modify-check the bitmap), then released before the slow origin + // fetch, then re-acquired just to record the fetched span -- mirroring + // the existing fcntl() shared/exclusive escalation dance below, just + // without a shared/exclusive distinction (pthread_mutex_t has none). + pthread_mutex_lock (&g_range_cache_mutex); + // Fast path: under a shared lock, check whether the requested span is // already fully cached. rc = dyad_shared_flock (ctx, bitmap_fd, &lock); if (DYAD_IS_ERROR (rc)) { + pthread_mutex_unlock (&g_range_cache_mutex); close (bitmap_fd); return rc; } @@ -196,6 +226,7 @@ dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, && range_cache_blocks_set (bitmap, block_start, block_end)) { free (bitmap); dyad_release_flock (ctx, bitmap_fd, &lock); + pthread_mutex_unlock (&g_range_cache_mutex); close (bitmap_fd); return DYAD_RC_OK; } @@ -214,6 +245,7 @@ dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, // the same shard queued behind the lock. rc = dyad_excl_flock (ctx, bitmap_fd, &lock); if (DYAD_IS_ERROR (rc)) { + pthread_mutex_unlock (&g_range_cache_mutex); close (bitmap_fd); return rc; } @@ -221,6 +253,7 @@ dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, if (stat (origin_path, &origin_st) != 0 || origin_st.st_size <= 0) { DYAD_LOG_ERROR (ctx, "DYAD RANGE_CACHE: cannot stat origin file '%s'", origin_path); dyad_release_flock (ctx, bitmap_fd, &lock); + pthread_mutex_unlock (&g_range_cache_mutex); close (bitmap_fd); return DYAD_RC_BADFIO; } @@ -241,6 +274,7 @@ dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, close (size_fd); } dyad_release_flock (ctx, bitmap_fd, &lock); + pthread_mutex_unlock (&g_range_cache_mutex); close (bitmap_fd); return DYAD_RC_BADFIO; } @@ -250,6 +284,7 @@ dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, bitmap = (uint8_t *)calloc (1ul, bitmap_bytes); if (bitmap == NULL) { dyad_release_flock (ctx, bitmap_fd, &lock); + pthread_mutex_unlock (&g_range_cache_mutex); close (bitmap_fd); return DYAD_RC_SYSFAIL; } @@ -261,36 +296,42 @@ dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, free (bitmap); bitmap = NULL; dyad_release_flock (ctx, bitmap_fd, &lock); + pthread_mutex_unlock (&g_range_cache_mutex); if (already_cached) { - // Another process filled this span while we waited. + // Another process (or thread) filled this span while we waited. close (bitmap_fd); return DYAD_RC_OK; } - // Fetch from origin_path into local_path with *no* lock held. If - // another process is concurrently missing on an overlapping span (same - // shard), both will redundantly pread the same origin bytes and pwrite - // them to the same local_path offset -- since both write identical data - // to the same range, this races safely (worst case is duplicated I/O, - // never corruption), and is a better trade than serializing unrelated - // misses behind a single slow PFS read. + // Fetch from origin_path into local_path with *no* lock held (neither + // fcntl() nor g_range_cache_mutex). If another process/thread is + // concurrently missing on an overlapping span (same shard), both will + // redundantly pread the same origin bytes and pwrite them to the same + // local_path offset -- since both write identical data to the same + // range, this races safely (worst case is duplicated I/O, never + // corruption), and is a better trade than serializing unrelated misses + // behind a single slow PFS read. rc = range_cache_fetch_span (ctx, local_path, origin_path, block_start, block_end, origin_size); if (DYAD_IS_ERROR (rc)) { close (bitmap_fd); return rc; } - // Re-acquire the lock just long enough to record that this span is now - // cached, merging with whatever bits any concurrent racers have set. + // Re-acquire both locks just long enough to record that this span is + // now cached, merging with whatever bits any concurrent racers have + // set. + pthread_mutex_lock (&g_range_cache_mutex); rc = dyad_excl_flock (ctx, bitmap_fd, &lock); if (DYAD_IS_ERROR (rc)) { + pthread_mutex_unlock (&g_range_cache_mutex); close (bitmap_fd); return rc; } bitmap = (uint8_t *)calloc (1ul, bitmap_bytes); if (bitmap == NULL) { dyad_release_flock (ctx, bitmap_fd, &lock); + pthread_mutex_unlock (&g_range_cache_mutex); close (bitmap_fd); return DYAD_RC_SYSFAIL; } @@ -302,6 +343,7 @@ dyad_rc_t dyad_range_cache_ensure (const dyad_ctx_t *ctx, } free (bitmap); dyad_release_flock (ctx, bitmap_fd, &lock); + pthread_mutex_unlock (&g_range_cache_mutex); close (bitmap_fd); return rc; } From f0f3dcdc9e7f512e3de4b2c0b2cf9f7ef2fa3e0e Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Thu, 16 Jul 2026 07:22:16 -0700 Subject: [PATCH 4/8] Fix Margo DTL RDMA memory-registration leak and add address cache Two bugs surfaced under sustained, full-scale byte-range fetch load (not visible in short test runs): - dyad_dtl_margo_send_to() and data_ready_rpc() both registered a bulk handle via margo_bulk_create() but only freed it on error paths, never on success -- a per-request RDMA memory-registration leak that exhausts the CXI NIC's finite MR table (fi_mr_enable() ENOSPC) after enough concurrent requests. - Every fetch resolved the consumer's Margo address via margo_addr_lookup() and freed it again after use, even though the same consumer is looked up repeatedly over a job's lifetime. Churning through resolve/free this often was found to exhaust or corrupt state in Mercury's CXI provider under sustained request volume, surfacing as indefinite address-lookup failures on addresses that had resolved successfully many times before. Added a per-consumer address cache (populated once, reused for the life of the DTL handle) matching the pattern the UCX backend already uses (ucx_ep_cache.cpp). Also checks previously-ignored return codes from margo_addr_lookup(), margo_addr_self(), and margo_addr_to_string() instead of silently proceeding on failure. Co-Authored-By: Claude Sonnet 5 --- src/dyad/dtl/margo_dtl.c | 101 ++++++++++++++++++++++++++++--- src/dyad/dtl/margo_dtl.h | 124 +++++++++++++++++++++++++++++++++------ 2 files changed, 199 insertions(+), 26 deletions(-) diff --git a/src/dyad/dtl/margo_dtl.c b/src/dyad/dtl/margo_dtl.c index bcafbc20..1916c9ea 100644 --- a/src/dyad/dtl/margo_dtl.c +++ b/src/dyad/dtl/margo_dtl.c @@ -135,6 +135,13 @@ static void data_ready_rpc (hg_handle_t h) // DYAD_LOG_DEBUG(ctx, "[MARGO DTL] RDMA pulled from the producer."); + // Every margo_bulk_create() above must be paired with a free, or the + // registration leaks on the CXI NIC's finite memory-registration table + // (fi_mr_enable() ENOSPC after a few dozen requests under concurrent + // load -- this was previously unfreed on every single request). + ret = margo_bulk_free (local_bulk); + assert (ret == HG_SUCCESS); + out.ret = 0; ret = margo_respond (h, &out); assert (ret == HG_SUCCESS); @@ -293,6 +300,9 @@ dyad_rc_t dyad_dtl_margo_init (const dyad_ctx_t *ctx, margo_handle->h = (flux_t *)ctx->h; // flux handle margo_handle->debug = debug; margo_handle->recv_ready = 0; + margo_handle->addr_cache = NULL; + margo_handle->addr_cache_len = 0; + margo_handle->addr_cache_cap = 0; // Determine the Mercury network abstraction (NA) protocol (communication fabric) to use. // @@ -368,7 +378,13 @@ dyad_rc_t dyad_dtl_margo_init (const dyad_ctx_t *ctx, } // both margo client and server - margo_addr_self (margo_handle->mid, &margo_handle->local_addr); + { + hg_return_t addr_self_ret = margo_addr_self (margo_handle->mid, &margo_handle->local_addr); + if (addr_self_ret != HG_SUCCESS) { + DYAD_LOG_ERROR (ctx, "[MARGO DTL] margo_addr_self failed: %d", (int)addr_self_ret); + goto error; + } + } margo_handle->remote_addr = NULL; ctx->dtl_handle->rpc_pack = dyad_dtl_margo_rpc_pack; @@ -418,6 +434,54 @@ error: __attribute__((unused)); return DYAD_RC_MARGOINIT_FAIL; } +dyad_rc_t dyad_dtl_margo_addr_cache_lookup (const dyad_ctx_t *ctx, + const char *addr_str, + hg_addr_t *out_addr) +{ + dyad_dtl_margo_t *margo_handle = ctx->dtl_handle->private_dtl.margo_dtl_handle; + + for (size_t i = 0; i < margo_handle->addr_cache_len; i++) { + if (strcmp (margo_handle->addr_cache[i].addr_str, addr_str) == 0) { + *out_addr = margo_handle->addr_cache[i].addr; + return DYAD_RC_OK; + } + } + + hg_addr_t addr = HG_ADDR_NULL; + hg_return_t ret = margo_addr_lookup (margo_handle->mid, addr_str, &addr); + if (ret != HG_SUCCESS) { + DYAD_LOG_ERROR (ctx, "[MARGO DTL] margo_addr_lookup failed for '%s': %d", addr_str, (int)ret); + return DYAD_RC_MARGOINIT_FAIL; + } + + if (margo_handle->addr_cache_len == margo_handle->addr_cache_cap) { + size_t new_cap = margo_handle->addr_cache_cap == 0 ? 8 : margo_handle->addr_cache_cap * 2; + struct dyad_dtl_margo_addr_cache_entry *grown = + (struct dyad_dtl_margo_addr_cache_entry *)realloc ( + margo_handle->addr_cache, new_cap * sizeof (*grown)); + if (grown == NULL) { + DYAD_LOG_ERROR (ctx, "[MARGO DTL] Could not grow address cache"); + margo_addr_free (margo_handle->mid, addr); + return DYAD_RC_SYSFAIL; + } + margo_handle->addr_cache = grown; + margo_handle->addr_cache_cap = new_cap; + } + + char *addr_str_copy = strdup (addr_str); + if (addr_str_copy == NULL) { + DYAD_LOG_ERROR (ctx, "[MARGO DTL] Could not duplicate address string for cache"); + margo_addr_free (margo_handle->mid, addr); + return DYAD_RC_SYSFAIL; + } + margo_handle->addr_cache[margo_handle->addr_cache_len].addr_str = addr_str_copy; + margo_handle->addr_cache[margo_handle->addr_cache_len].addr = addr; + margo_handle->addr_cache_len++; + + *out_addr = addr; + return DYAD_RC_OK; +} + dyad_rc_t dyad_dtl_margo_rpc_pack (const dyad_ctx_t *ctx, const char *restrict upath, uint32_t producer_rank, @@ -513,7 +577,11 @@ dyad_rc_t dyad_dtl_margo_rpc_unpack (const dyad_ctx_t *ctx, const flux_msg_t *ms "[MARGO DTL] recv/unpack margo sever addr: %s, %ld.", addr_str, addr_str_size); - margo_addr_lookup (margo_handle->mid, addr_str, &margo_handle->remote_addr); + rc = dyad_dtl_margo_addr_cache_lookup (ctx, addr_str, &margo_handle->remote_addr); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, "[MARGO DTL] Could not resolve consumer address '%s'", addr_str); + goto dtl_margo_rpc_unpack_region_finish; + } dtl_margo_rpc_unpack_region_finish:; DYAD_C_FUNCTION_END (); @@ -634,7 +702,11 @@ dyad_rc_t dyad_dtl_margo_rpc_unpack_range (const dyad_ctx_t *ctx, "[MARGO DTL] recv/unpack ranged margo sever addr: %s, %ld.", addr_str, addr_str_size); - margo_addr_lookup (margo_handle->mid, addr_str, &margo_handle->remote_addr); + rc = dyad_dtl_margo_addr_cache_lookup (ctx, addr_str, &margo_handle->remote_addr); + if (DYAD_IS_ERROR (rc)) { + DYAD_LOG_ERROR (ctx, "[MARGO DTL] Could not resolve consumer address '%s'", addr_str); + goto dtl_margo_rpc_unpack_range_region_finish; + } dtl_margo_rpc_unpack_range_region_finish:; DYAD_C_FUNCTION_END (); @@ -717,6 +789,12 @@ static dyad_rc_t dyad_dtl_margo_send_to (const dyad_ctx_t *ctx, } margo_free_output (mh, &resp); margo_destroy (mh); + // Every margo_bulk_create() above must be paired with a free, or the + // registration leaks on the CXI NIC's finite memory-registration table + // (fi_mr_enable() ENOSPC after a few dozen requests under concurrent + // load -- this was previously unfreed on this, the success, path; only + // the error paths below freed it). + margo_bulk_free (local_bulk); DYAD_LOG_DEBUG (ctx, "[MARGO DTL] margo_send completed, buflen: %lu", buflen); @@ -787,9 +865,10 @@ dyad_rc_t dyad_dtl_margo_send_detached (const dyad_ctx_t *ctx, buf, buflen); - if (state->remote_addr != NULL) { - margo_addr_free (state->mid, state->remote_addr); - } + // state->remote_addr is borrowed from the address cache (see + // struct dyad_dtl_margo_addr_cache_entry) -- NOT freed here. It's + // reused by future requests to the same consumer and only freed at + // dyad_dtl_margo_finalize(). free (state); return rc; } @@ -797,9 +876,8 @@ dyad_rc_t dyad_dtl_margo_send_detached (const dyad_ctx_t *ctx, dyad_rc_t dyad_dtl_margo_abort_detached (const dyad_ctx_t *ctx, void *req_state) { struct dyad_dtl_margo_req_state *state = (struct dyad_dtl_margo_req_state *)req_state; - if (state->remote_addr != NULL) { - margo_addr_free (state->mid, state->remote_addr); - } + // state->remote_addr is borrowed from the address cache -- not freed + // here. See dyad_dtl_margo_send_detached(). free (state); return DYAD_RC_OK; } @@ -861,8 +939,13 @@ dyad_rc_t dyad_dtl_margo_finalize (const dyad_ctx_t *ctx) margo_addr_free (margo_handle->mid, margo_handle->local_addr); if (margo_handle->remote_addr != NULL) margo_addr_free (margo_handle->mid, margo_handle->remote_addr); + for (size_t i = 0; i < margo_handle->addr_cache_len; i++) { + margo_addr_free (margo_handle->mid, margo_handle->addr_cache[i].addr); + free (margo_handle->addr_cache[i].addr_str); + } margo_finalize (margo_handle->mid); } + free (margo_handle->addr_cache); free (margo_handle); ctx->dtl_handle->private_dtl.margo_dtl_handle = NULL; diff --git a/src/dyad/dtl/margo_dtl.h b/src/dyad/dtl/margo_dtl.h index 3cb24a41..30aa910c 100644 --- a/src/dyad/dtl/margo_dtl.h +++ b/src/dyad/dtl/margo_dtl.h @@ -11,6 +11,26 @@ #include #include +/** + * @brief One cached resolution of a consumer's Margo address string. + * + * @details + * @c addr_str is an owned copy of the address string a consumer embeds in + * its Flux RPC request (see @c dyad_dtl_margo_rpc_pack()); @c addr is the + * @c hg_addr_t Mercury resolved it to. Cached indefinitely (freed only at + * @c dyad_dtl_margo_finalize()) so a producer that repeatedly services the + * same consumer over the life of a job -- the normal case, since a training + * job's consumers are a small, fixed set of ranks issuing thousands of + * fetches -- resolves each consumer's address once instead of on every + * single request. See @c dyad_dtl_margo_addr_cache_lookup() for why doing + * the naive thing (resolve + free per request, as this DTL originally did) + * breaks down at sustained request volume. + */ +struct dyad_dtl_margo_addr_cache_entry { + char *addr_str; + hg_addr_t addr; +}; + struct dyad_dtl_margo { flux_t *h; bool debug; @@ -21,6 +41,12 @@ struct dyad_dtl_margo { bool recv_ready; size_t recv_len; void *recv_buffer; + // Address cache -- see struct dyad_dtl_margo_addr_cache_entry. Only ever + // touched from the module's reactor thread (same thread that calls + // rpc_unpack()/rpc_unpack_range()), so no locking is needed. + struct dyad_dtl_margo_addr_cache_entry *addr_cache; + size_t addr_cache_len; + size_t addr_cache_cap; }; typedef struct dyad_dtl_margo dyad_dtl_margo_t; @@ -36,7 +62,9 @@ typedef struct dyad_dtl_margo dyad_dtl_margo_t; struct dyad_dtl_margo_req_state { margo_instance_id mid; hg_id_t sendrecv_rpc_id; - hg_addr_t remote_addr; // owned; freed by dyad_dtl_margo_send_detached() + // Borrowed from the address cache -- NOT owned/freed here. See + // struct dyad_dtl_margo_addr_cache_entry. + hg_addr_t remote_addr; }; /** @@ -122,6 +150,57 @@ dyad_rc_t dyad_dtl_margo_init (const dyad_ctx_t *ctx, dyad_dtl_comm_mode_t comm_mode, bool debug); +/** + * @brief Resolves a consumer's Margo address string to an @c hg_addr_t, + * reusing a cached resolution if one already exists. + * + * @details + * Linearly scans @c margo_handle->addr_cache for an entry whose + * @c addr_str matches @p addr_str. On a hit, returns the cached @c hg_addr_t + * directly -- no Margo/Mercury call at all. On a miss, resolves it via + * @c margo_addr_lookup(), and on success appends a new owned entry (a + * @c strdup() of @p addr_str plus the resolved address) to the cache before + * returning it. The cache is never evicted from; entries live until + * @c dyad_dtl_margo_finalize() frees the whole cache. + * + * A linear scan is intentional: the cache holds one entry per distinct + * consumer a given producer services, which for DYAD's usage (a job's + * consumer ranks are a small, fixed set) is at most a few dozen -- far + * cheaper than the RDMA operation each lookup guards anyway. + * + * @note This exists because doing a fresh @c margo_addr_lookup() + + * @c margo_addr_free() on every single request (this DTL's original + * behavior) is fine at low request volume but was found to exhaust + * or corrupt state in Mercury's CXI (Slingshot) provider under + * sustained full-scale request volume (thousands of requests over + * many minutes), manifesting as indefinite + * @c HG_PROTONOSUPPORT/"Could not lookup address" failures on later + * lookups for addresses that had resolved and been used successfully + * many times before. Caching resolved addresses avoids the + * repeated create/destroy churn entirely, matching the UCX DTL + * backend's existing per-consumer endpoint cache + * (@c ucx_ep_cache.cpp) for the same reason. + * + * @note Must be called from the module's reactor thread (same constraint as + * @c margo_addr_lookup() itself, and matching where this is called + * from: @c dyad_dtl_margo_rpc_unpack()/@c _rpc_unpack_range()). + * + * @param[in] ctx DYAD context. + * @param[in] addr_str Consumer's Margo address string. + * @param[out] out_addr Set to the resolved (cached or freshly looked up) + * address on success. Borrowed from the cache -- + * the caller must not free it. + * + * @return @c dyad_rc_t return code: + * @retval DYAD_RC_OK Resolution succeeded (cache hit or fresh + * lookup). + * @retval DYAD_RC_SYSFAIL Cache growth allocation failed. + * @retval DYAD_RC_MARGOINIT_FAIL @c margo_addr_lookup() failed. + */ +dyad_rc_t dyad_dtl_margo_addr_cache_lookup (const dyad_ctx_t *ctx, + const char *addr_str, + hg_addr_t *out_addr); + /** * @brief Packs a file fetch request into a JSON object for a Margo RPC call. * @@ -524,12 +603,15 @@ dyad_rc_t dyad_dtl_margo_close_connection (const dyad_ctx_t *ctx); * Copies @c mid, @c sendrecv_rpc_id (process-wide, immutable, just * convenience copies so @c dyad_dtl_margo_send_detached() never has to * touch the shared @c dyad_dtl_margo struct at all) and @c remote_addr - * (resolved by the preceding @c dyad_dtl_margo_rpc_unpack_range() call) out - * of the shared fields into a newly allocated - * @c struct dyad_dtl_margo_req_state, and clears the shared - * @c remote_addr field (transferring ownership -- this also fixes a - * pre-existing leak where the previous request's resolved address was - * never freed before being overwritten). + * (resolved by the preceding @c dyad_dtl_margo_rpc_unpack_range() call, + * borrowed from the address cache -- see + * @c struct dyad_dtl_margo_addr_cache_entry) out of the shared fields into + * a newly allocated @c struct dyad_dtl_margo_req_state, and clears the + * shared @c remote_addr field so a later request's + * @c margo_addr_lookup()/cache hit can't be mistaken for this one. + * @c remote_addr is NOT owned by the returned state -- it is not freed by + * @c dyad_dtl_margo_send_detached()/@c dyad_dtl_margo_abort_detached(), + * only by @c dyad_dtl_margo_finalize() tearing down the whole cache. * * @param[in] ctx DYAD context. * @param[out] req_state Set to a newly allocated request-state blob owning @@ -550,8 +632,9 @@ dyad_rc_t dyad_dtl_margo_detach_request (const dyad_ctx_t *ctx, void **req_state * Equivalent to @c dyad_dtl_margo_send(), but reads @c mid, * @c sendrecv_rpc_id, and @c remote_addr from @p req_state (as produced by * @c dyad_dtl_margo_detach_request()) instead of the shared fields. - * Frees @c remote_addr via @c margo_addr_free() and frees @p req_state - * before returning. + * @c remote_addr is borrowed from the address cache and is NOT freed here + * (only @p req_state itself is freed) -- see + * @c struct dyad_dtl_margo_addr_cache_entry. * * @note Must be called from the module's reactor thread, like the Flux RPC * backend's equivalent -- @c margo_init() for this DTL's @@ -582,13 +665,14 @@ dyad_rc_t dyad_dtl_margo_send_detached (const dyad_ctx_t *ctx, size_t buflen); /** - * @brief Frees a detached request's resolved address without sending - * data, for use on an I/O-error path. + * @brief Frees a detached request's state without sending data, for use on + * an I/O-error path. * * @details - * Frees @c remote_addr via @c margo_addr_free() (mirroring - * @c dyad_dtl_margo_send_detached()'s cleanup) and frees @p req_state. - * Safe to call from any thread -- touches only Margo/Mercury state. + * Frees @p req_state. @c remote_addr is borrowed from the address cache + * and is NOT freed here (mirroring @c dyad_dtl_margo_send_detached()) -- + * see @c struct dyad_dtl_margo_addr_cache_entry. Safe to call from any + * thread -- touches only Margo/Mercury state. * * @param[in] ctx DYAD context. * @param[in] req_state Request state from a prior @@ -610,10 +694,16 @@ dyad_rc_t dyad_dtl_margo_abort_detached (const dyad_ctx_t *ctx, void *req_state) * local Margo address via @c margo_addr_free(). * 2. If @c margo_handle->remote_addr is non-@c NULL, frees the remote * address (the consumer's resolved Margo server address) via - * @c margo_addr_free(). - * 3. Finalizes the Margo instance via @c margo_finalize(), which shuts + * @c margo_addr_free(). In practice this is normally @c NULL, since + * @c dyad_dtl_margo_detach_request() clears it after every request -- + * this is a safety net for the non-worker-pool (UCX-style synchronous) + * path, which doesn't go through detach/cache at all. + * 3. Frees every entry in the address cache (see + * @c struct dyad_dtl_margo_addr_cache_entry) via @c margo_addr_free() + * and frees the cache array itself. + * 4. Finalizes the Margo instance via @c margo_finalize(), which shuts * down the Mercury progress loop and any associated Argobots ESs. - * 4. Frees the @c dyad_dtl_margo struct and sets the handle pointer + * 5. Frees the @c dyad_dtl_margo struct and sets the handle pointer * to @c NULL. * * If @c ctx->dtl_handle is @c NULL or From 59fdcc4ebb867efcfa2f91c0635e3f84271ec7f4 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Thu, 16 Jul 2026 07:22:35 -0700 Subject: [PATCH 5/8] Fix pydyad double-finalize / use-after-free Dyad.finalize() guards against a second call by checking self.initialized, but never reset it back to False after finalizing -- so the guard never actually fired. A second call (e.g. an explicit finalize() followed by __del__() at interpreter exit) re-invoked dyad_finalize() on already-freed C-side state, more likely to crash at higher rank counts. Co-Authored-By: Claude Sonnet 5 --- pydyad/pydyad/bindings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pydyad/pydyad/bindings.py b/pydyad/pydyad/bindings.py index dff8879c..68a7ff72 100644 --- a/pydyad/pydyad/bindings.py +++ b/pydyad/pydyad/bindings.py @@ -494,5 +494,11 @@ def finalize(self): ) return res = self.dyad_finalize() + # Without this, `initialized` stays True forever, so the guard at + # the top of this method never actually prevents a second call -- + # e.g. an explicit finalize() followed by __del__() at interpreter + # exit -- from calling dyad_finalize() twice on already-freed C-side + # state (a double-finalize/use-after-free). + self.initialized = False if int(res) != 0: raise RuntimeError("Cannot finalize DYAD!") From 877161e8b6621f850c059473f951c2454cf189b0 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Thu, 16 Jul 2026 18:06:36 -0700 Subject: [PATCH 6/8] Apply clang formating Signed-off-by: Chen Wang --- include/dyad/common/dyad_cache.h | 2 +- include/dyad/common/dyad_dtl.h | 2 +- include/dyad/core/dyad_ctx.h | 3 +- include/dyad/stream/dyad_stream_api.hpp | 218 ++++++++++++------------ src/dyad/cache/dyad_cache.c | 18 +- src/dyad/client/dyad_client.c | 8 +- src/dyad/common/dyad_structures_int.h | 38 ++--- src/dyad/core/dyad_ctx.c | 60 +++---- src/dyad/dtl/margo_dtl.c | 9 +- src/dyad/service/dyad_exe.c | 26 +-- src/dyad/utils/read_all.h | 3 +- src/dyad/utils/test_murmur3.c | 8 +- src/dyad/utils/utils.c | 70 ++++---- 13 files changed, 238 insertions(+), 227 deletions(-) diff --git a/include/dyad/common/dyad_cache.h b/include/dyad/common/dyad_cache.h index 3e15a6fe..ddfe5050 100644 --- a/include/dyad/common/dyad_cache.h +++ b/include/dyad/common/dyad_cache.h @@ -65,7 +65,7 @@ typedef enum dyad_cache_policy_mode dyad_cache_policy_mode_t; * Marked @c unused to suppress warnings when included in translation * units that do not reference it directly. */ -static const char* dyad_cache_policy_name[DYAD_CACHE_END + 1] +static const char *dyad_cache_policy_name[DYAD_CACHE_END + 1] __attribute__ ((unused)) = {"NONE", "LRU", "FIFO", "CACHE_UNKNOWN"}; /** diff --git a/include/dyad/common/dyad_dtl.h b/include/dyad/common/dyad_dtl.h index 909d5ccf..5bc58a03 100644 --- a/include/dyad/common/dyad_dtl.h +++ b/include/dyad/common/dyad_dtl.h @@ -61,7 +61,7 @@ typedef enum dyad_dtl_mode dyad_dtl_mode_t; * Marked @c unused to suppress warnings when included in translation * units that do not reference it directly. */ -static const char* dyad_dtl_mode_name[DYAD_DTL_END + 1] +static const char *dyad_dtl_mode_name[DYAD_DTL_END + 1] __attribute__ ((unused)) = {"UCX", "MARGO", "FLUX_RPC", "DTL_UNKNOWN"}; /** diff --git a/include/dyad/core/dyad_ctx.h b/include/dyad/core/dyad_ctx.h index 65296a55..fafa6868 100644 --- a/include/dyad/core/dyad_ctx.h +++ b/include/dyad/core/dyad_ctx.h @@ -217,7 +217,8 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_init (bool debug, * | @c DYAD_CACHE_POLICY | @c LRU | Cache-eviction policy: LRU/FIFO/NONE | * | @c DYAD_CACHE_LOW_WATERMARK | @c 0.8 | Fraction of capacity to evict down to | * | @c DYAD_CACHE_GRACE_PERIOD | @c 5 | Skip candidates accessed within N seconds | - * | @c DYAD_PATH_ORIGIN | @c NULL (disabled) | Fallback path for lazy origin-backed range caching | + * | @c DYAD_PATH_ORIGIN | @c NULL (disabled) | Fallback path for lazy origin-backed range + * caching | * * If @c DYAD_DTL_MODE is not set, defaults to @c DYAD_DTL_DEFAULT and logs * a warning to @c stderr. diff --git a/include/dyad/stream/dyad_stream_api.hpp b/include/dyad/stream/dyad_stream_api.hpp index 507da861..3d2236e4 100644 --- a/include/dyad/stream/dyad_stream_api.hpp +++ b/include/dyad/stream/dyad_stream_api.hpp @@ -62,7 +62,7 @@ namespace dyad */ template ().make_preferred ().filename ())> + typename _Path2 = decltype (std::declval<_Path &> ().make_preferred ().filename ())> using dyad_if_fs_path = std::enable_if_t, _Result>; #endif // c++17 filesystem @@ -98,7 +98,7 @@ using dyad_if_fs_path = std::enable_if_t, _Result> * https://stackoverflow.com/questions/676787 for background. */ template -void fsync_ofstream (std::basic_ofstream<_CharT, _Traits>& os) +void fsync_ofstream (std::basic_ofstream<_CharT, _Traits> &os) { #if defined(DYAD_HAS_STD_FSTREAM_FD) class my_filebuf : public std::basic_filebuf<_CharT> @@ -112,7 +112,7 @@ void fsync_ofstream (std::basic_ofstream<_CharT, _Traits>& os) if (os.is_open ()) { os.flush (); - fsync (static_cast (*os.rdbuf ()).handle ()); + fsync (static_cast (*os.rdbuf ()).handle ()); } #else if (os.is_open ()) { @@ -133,7 +133,7 @@ void fsync_ofstream (std::basic_ofstream<_CharT, _Traits>& os) * @param[in,out] os File stream to flush and sync. */ template -void fsync_fstream (std::basic_fstream<_CharT, _Traits>& os) +void fsync_fstream (std::basic_fstream<_CharT, _Traits> &os) { #if defined(DYAD_HAS_STD_FSTREAM_FD) class my_filebuf : public std::basic_filebuf<_CharT> @@ -147,7 +147,7 @@ void fsync_fstream (std::basic_fstream<_CharT, _Traits>& os) if (os.is_open ()) { os.flush (); - fsync (static_cast (*os.rdbuf ()).handle ()); + fsync (static_cast (*os.rdbuf ()).handle ()); } #else if (os.is_open ()) { @@ -175,8 +175,8 @@ void fsync_fstream (std::basic_fstream<_CharT, _Traits>& os) * across all standard library implementations. */ template -void lock_exclusive_ofstream (std::basic_ofstream<_CharT, _Traits>& os, - const dyad_stream_core& core) +void lock_exclusive_ofstream (std::basic_ofstream<_CharT, _Traits> &os, + const dyad_stream_core &core) { class my_filebuf : public std::basic_filebuf<_CharT> { @@ -188,7 +188,7 @@ void lock_exclusive_ofstream (std::basic_ofstream<_CharT, _Traits>& os, }; if (os.is_open ()) { - int fd = static_cast (*os.rdbuf ()).handle (); + int fd = static_cast (*os.rdbuf ()).handle (); core.file_lock_exclusive (fd); } } @@ -206,7 +206,7 @@ void lock_exclusive_ofstream (std::basic_ofstream<_CharT, _Traits>& os, * @param[in] core DYAD stream core providing the locking operation. */ template -void lock_exclusive_fstream (std::basic_fstream<_CharT, _Traits>& os, const dyad_stream_core& core) +void lock_exclusive_fstream (std::basic_fstream<_CharT, _Traits> &os, const dyad_stream_core &core) { class my_filebuf : public std::basic_filebuf<_CharT> { @@ -218,7 +218,7 @@ void lock_exclusive_fstream (std::basic_fstream<_CharT, _Traits>& os, const dyad }; if (os.is_open ()) { - int fd = static_cast (*os.rdbuf ()).handle (); + int fd = static_cast (*os.rdbuf ()).handle (); core.file_lock_exclusive (fd); } } @@ -240,7 +240,7 @@ void lock_exclusive_fstream (std::basic_fstream<_CharT, _Traits>& os, const dyad * across all standard library implementations. */ template -void lock_shared_ifstream (std::basic_ifstream<_CharT, _Traits>& is, const dyad_stream_core& core) +void lock_shared_ifstream (std::basic_ifstream<_CharT, _Traits> &is, const dyad_stream_core &core) { class my_filebuf : public std::basic_filebuf<_CharT> { @@ -252,7 +252,7 @@ void lock_shared_ifstream (std::basic_ifstream<_CharT, _Traits>& is, const dyad_ }; if (is.is_open ()) { - int fd = static_cast (*is.rdbuf ()).handle (); + int fd = static_cast (*is.rdbuf ()).handle (); core.file_lock_shared (fd); } } @@ -269,7 +269,7 @@ void lock_shared_ifstream (std::basic_ifstream<_CharT, _Traits>& is, const dyad_ * @param[in] core DYAD stream core providing the locking operation. */ template -void lock_shared_fstream (std::basic_fstream<_CharT, _Traits>& is, const dyad_stream_core& core) +void lock_shared_fstream (std::basic_fstream<_CharT, _Traits> &is, const dyad_stream_core &core) { class my_filebuf : public std::basic_filebuf<_CharT> { @@ -281,7 +281,7 @@ void lock_shared_fstream (std::basic_fstream<_CharT, _Traits>& is, const dyad_st }; if (is.is_open ()) { - int fd = static_cast (*is.rdbuf ()).handle (); + int fd = static_cast (*is.rdbuf ()).handle (); core.file_lock_shared (fd); } } @@ -303,7 +303,7 @@ void lock_shared_fstream (std::basic_fstream<_CharT, _Traits>& is, const dyad_st * across all standard library implementations. */ template -void unlock_ofstream (std::basic_ofstream<_CharT, _Traits>& os, const dyad_stream_core& core) +void unlock_ofstream (std::basic_ofstream<_CharT, _Traits> &os, const dyad_stream_core &core) { class my_filebuf : public std::basic_filebuf<_CharT> { @@ -315,7 +315,7 @@ void unlock_ofstream (std::basic_ofstream<_CharT, _Traits>& os, const dyad_strea }; if (os.is_open ()) { - int fd = static_cast (*os.rdbuf ()).handle (); + int fd = static_cast (*os.rdbuf ()).handle (); core.file_unlock (fd); } } @@ -333,7 +333,7 @@ void unlock_ofstream (std::basic_ofstream<_CharT, _Traits>& os, const dyad_strea * @param[in] core DYAD stream core providing the unlock operation. */ template -void unlock_ifstream (std::basic_ifstream<_CharT, _Traits>& is, const dyad_stream_core& core) +void unlock_ifstream (std::basic_ifstream<_CharT, _Traits> &is, const dyad_stream_core &core) { class my_filebuf : public std::basic_filebuf<_CharT> { @@ -345,7 +345,7 @@ void unlock_ifstream (std::basic_ifstream<_CharT, _Traits>& is, const dyad_strea }; if (is.is_open ()) { - int fd = static_cast (*is.rdbuf ()).handle (); + int fd = static_cast (*is.rdbuf ()).handle (); core.file_unlock (fd); } } @@ -363,7 +363,7 @@ void unlock_ifstream (std::basic_ifstream<_CharT, _Traits>& is, const dyad_strea * @param[in] core DYAD stream core providing the unlock operation. */ template -void unlock_fstream (std::basic_fstream<_CharT, _Traits>& os, const dyad_stream_core& core) +void unlock_fstream (std::basic_fstream<_CharT, _Traits> &os, const dyad_stream_core &core) { class my_filebuf : public std::basic_filebuf<_CharT> { @@ -375,7 +375,7 @@ void unlock_fstream (std::basic_fstream<_CharT, _Traits>& os, const dyad_stream_ }; if (os.is_open ()) { - int fd = static_cast (*os.rdbuf ()).handle (); + int fd = static_cast (*os.rdbuf ()).handle (); core.file_unlock (fd); } } @@ -472,7 +472,7 @@ class basic_ifstream_dyad * * @param[in] core Initialized stream core to copy. */ - basic_ifstream_dyad (const dyad_stream_core& core); + basic_ifstream_dyad (const dyad_stream_core &core); /** Constructs an unopened stream and initializes the core from environment * variables via @c m_core.init(). */ @@ -489,7 +489,7 @@ class basic_ifstream_dyad * @param[in] filename Path to the file to open. * @param[in] mode Open mode. Defaults to @c ios_base::in. */ - explicit basic_ifstream_dyad (const char* filename, ios_base::openmode mode = ios_base::in); + explicit basic_ifstream_dyad (const char *filename, ios_base::openmode mode = ios_base::in); /** * @brief Destroys the stream, releasing the underlying @c basic_ifstream. @@ -514,7 +514,7 @@ class basic_ifstream_dyad * @param[in] filename Path to the file to open. * @param[in] mode Open mode. Defaults to @c ios_base::in. */ - void open (const char* filename, ios_base::openmode mode = ios_base::in); + void open (const char *filename, ios_base::openmode mode = ios_base::in); #if __cplusplus < 201103L /** Returns @c true if the underlying stream is open. Returns @c false if @@ -523,13 +523,13 @@ class basic_ifstream_dyad #else /** Constructs and opens the stream from a @c std::string filename. See the * @c const char* constructor for full details. */ - explicit basic_ifstream_dyad (const string& filename, ios_base::openmode mode = ios_base::in); + explicit basic_ifstream_dyad (const string &filename, ios_base::openmode mode = ios_base::in); /** Copy construction is disabled. */ - basic_ifstream_dyad (const basic_ifstream_dyad&) = delete; + basic_ifstream_dyad (const basic_ifstream_dyad &) = delete; /** Move constructor. Transfers ownership of the stream and core from @p rhs. */ - basic_ifstream_dyad (basic_ifstream_dyad&& rhs); + basic_ifstream_dyad (basic_ifstream_dyad &&rhs); #if (__cplusplus >= 201703L) && __has_include() /** * @brief Constructs and opens the stream from a @c std::filesystem::path. @@ -543,23 +543,23 @@ class basic_ifstream_dyad * @param[in] mode Open mode. Defaults to @c ios_base::in. */ template > - basic_ifstream_dyad (const _Path& filepath, std::ios_base::openmode mode = std::ios_base::in); + basic_ifstream_dyad (const _Path &filepath, std::ios_base::openmode mode = std::ios_base::in); #endif // c++17 filesystem /** Opens the file from a @c std::string filename. Delegates to the * @c const char* overload. */ - void open (const string& filename, ios_base::openmode mode = ios_base::in); + void open (const string &filename, ios_base::openmode mode = ios_base::in); /** Returns @c true if the underlying stream is open. Returns @c false if * the stream pointer is @c nullptr. */ bool is_open () const; /** Move assignment. Takes ownership of @p rhs's stream and core. */ - basic_ifstream_dyad& operator= (basic_ifstream_dyad&& rhs); + basic_ifstream_dyad &operator= (basic_ifstream_dyad &&rhs); /** Swaps the underlying stream with @p rhs. Has no effect if the stream * pointer is @c nullptr (TODO: set fail bit). */ - void swap (basic_ifstream_dyad& rhs); + void swap (basic_ifstream_dyad &rhs); #endif /** @@ -573,12 +573,12 @@ class basic_ifstream_dyad /** Returns the underlying stream buffer, or @c nullptr if the stream * pointer is @c nullptr. */ - filebuf* rdbuf () const; + filebuf *rdbuf () const; /** Returns a reference to the underlying @c std::basic_ifstream. * @warning If the stream pointer is @c nullptr, behavior is undefined * (TODO: throw). */ - basic_ifstream& get_stream (); + basic_ifstream &get_stream (); /** * @brief Reinitializes the stream core from an existing @c dyad_stream_core. @@ -589,10 +589,10 @@ class basic_ifstream_dyad * * @param[in] core Stream core to copy into this instance. */ - void init (const dyad_stream_core& core); + void init (const dyad_stream_core &core); /** Allow read-only access to the embedded @c dyad_stream_core. */ - const dyad_stream_core& core () const + const dyad_stream_core &core () const { return m_core; } @@ -618,7 +618,7 @@ using wifstream_dyad = basic_ifstream_dyad; #if __cplusplus < 201103L //---------------------------------------------------- template -basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const dyad_stream_core& core) +basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const dyad_stream_core &core) : m_core (core) { m_stream = new basic_ifstream (); @@ -632,7 +632,7 @@ basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad () } template -basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const char* filename, +basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const char *filename, std::ios_base::openmode mode) { m_core.init (); @@ -650,7 +650,7 @@ bool basic_ifstream_dyad<_CharT, _Traits>::is_open () } #else //----------------------------------------------------------------------- template -basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const dyad_stream_core& core) +basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const dyad_stream_core &core) : m_core (core) { m_stream = std::unique_ptr (new basic_ifstream ()); @@ -664,7 +664,7 @@ basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad () } template -basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const char* filename, +basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const char *filename, std::ios_base::openmode mode) { m_core.init (); @@ -676,7 +676,7 @@ basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const char* filename, } template -basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const string& filename, +basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const string &filename, std::ios_base::openmode mode) { m_core.init (); @@ -688,7 +688,7 @@ basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const string& filenam } template -basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (basic_ifstream_dyad&& rhs) +basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (basic_ifstream_dyad &&rhs) : m_stream (std::move (rhs.m_stream)), m_core (std::move (rhs.m_core)) { } @@ -696,7 +696,7 @@ basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (basic_ifstream_dyad&& #if (__cplusplus >= 201703L) && __has_include() template template -basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const _Path& filepath, +basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const _Path &filepath, std::ios_base::openmode mode) { m_core.init (); @@ -709,8 +709,8 @@ basic_ifstream_dyad<_CharT, _Traits>::basic_ifstream_dyad (const _Path& filepath #endif // c++17 filesystem template -basic_ifstream_dyad<_CharT, _Traits>& basic_ifstream_dyad<_CharT, _Traits>::operator= ( - basic_ifstream_dyad&& rhs) +basic_ifstream_dyad<_CharT, _Traits> &basic_ifstream_dyad<_CharT, _Traits>::operator= ( + basic_ifstream_dyad &&rhs) { m_stream = std::move (rhs.m_stream); m_core = std::move (rhs.m_core); @@ -732,7 +732,7 @@ basic_ifstream_dyad<_CharT, _Traits>::~basic_ifstream_dyad () } template -void basic_ifstream_dyad<_CharT, _Traits>::open (const string& filename, +void basic_ifstream_dyad<_CharT, _Traits>::open (const string &filename, std::ios_base::openmode mode) { open (filename.c_str (), mode); @@ -745,7 +745,7 @@ bool basic_ifstream_dyad<_CharT, _Traits>::is_open () const } template -void basic_ifstream_dyad<_CharT, _Traits>::swap (basic_ifstream_dyad& rhs) +void basic_ifstream_dyad<_CharT, _Traits>::swap (basic_ifstream_dyad &rhs) { if (m_stream == nullptr) { // TODO: set fail bit if nullptr @@ -756,7 +756,7 @@ void basic_ifstream_dyad<_CharT, _Traits>::swap (basic_ifstream_dyad& rhs) #endif //----------------------------------------------------------------------- template -void basic_ifstream_dyad<_CharT, _Traits>::open (const char* filename, std::ios_base::openmode mode) +void basic_ifstream_dyad<_CharT, _Traits>::open (const char *filename, std::ios_base::openmode mode) { if (m_stream == nullptr) { // TODO: set fail bit if nullptr @@ -781,7 +781,7 @@ void basic_ifstream_dyad<_CharT, _Traits>::close () } template -std::filebuf* basic_ifstream_dyad<_CharT, _Traits>::rdbuf () const +std::filebuf *basic_ifstream_dyad<_CharT, _Traits>::rdbuf () const { if (m_stream == nullptr) { return nullptr; @@ -790,7 +790,7 @@ std::filebuf* basic_ifstream_dyad<_CharT, _Traits>::rdbuf () const } template -std::basic_ifstream<_CharT, _Traits>& basic_ifstream_dyad<_CharT, _Traits>::get_stream () +std::basic_ifstream<_CharT, _Traits> &basic_ifstream_dyad<_CharT, _Traits>::get_stream () { if (m_stream == nullptr) { // TODO: throw @@ -799,7 +799,7 @@ std::basic_ifstream<_CharT, _Traits>& basic_ifstream_dyad<_CharT, _Traits>::get_ } template -void basic_ifstream_dyad<_CharT, _Traits>::init (const dyad_stream_core& core) +void basic_ifstream_dyad<_CharT, _Traits>::init (const dyad_stream_core &core) { m_core = core; m_core.set_initialized (); @@ -850,7 +850,7 @@ class basic_ofstream_dyad * * @param[in] core Initialized stream core to copy. */ - basic_ofstream_dyad (const dyad_stream_core& core); + basic_ofstream_dyad (const dyad_stream_core &core); /** Constructs an unopened stream and initializes the core from environment * variables via @c m_core.init(). */ @@ -869,7 +869,7 @@ class basic_ofstream_dyad * @param[in] filename Path to the file to open. * @param[in] mode Open mode. Defaults to @c ios_base::out. */ - explicit basic_ofstream_dyad (const char* filename, ios_base::openmode mode = ios_base::out); + explicit basic_ofstream_dyad (const char *filename, ios_base::openmode mode = ios_base::out); /** * @brief Destroys the stream, syncing and publishing if still open. @@ -895,7 +895,7 @@ class basic_ofstream_dyad * @param[in] filename Path to the file to open. * @param[in] mode Open mode. Defaults to @c ios_base::out. */ - void open (const char* filename, ios_base::openmode mode = ios_base::out); + void open (const char *filename, ios_base::openmode mode = ios_base::out); #if __cplusplus < 201103L /** Returns @c true if the underlying stream is open. Returns @c false if @@ -904,13 +904,13 @@ class basic_ofstream_dyad #else /** Constructs and opens the stream from a @c std::string filename. See the * @c const char* constructor for full details. */ - explicit basic_ofstream_dyad (const string& filename, ios_base::openmode mode = ios_base::out); + explicit basic_ofstream_dyad (const string &filename, ios_base::openmode mode = ios_base::out); /** Copy construction is disabled. */ - basic_ofstream_dyad (const basic_ofstream_dyad&) = delete; + basic_ofstream_dyad (const basic_ofstream_dyad &) = delete; /** Move constructor. Transfers ownership of the stream and core from @p rhs. */ - basic_ofstream_dyad (basic_ofstream_dyad&& rhs); + basic_ofstream_dyad (basic_ofstream_dyad &&rhs); #if (__cplusplus >= 201703L) && __has_include() /** * @brief Constructs and opens the stream from a @c std::filesystem::path. @@ -924,23 +924,23 @@ class basic_ofstream_dyad * @param[in] mode Open mode. Defaults to @c ios_base::out. */ template > - basic_ofstream_dyad (const _Path& filepath, std::ios_base::openmode mode = std::ios_base::out); + basic_ofstream_dyad (const _Path &filepath, std::ios_base::openmode mode = std::ios_base::out); #endif // c++17 filesystem /** Opens the file from a @c std::string filename. Delegates to the * @c const char* overload. */ - void open (const string& filename, ios_base::openmode mode = ios_base::out); + void open (const string &filename, ios_base::openmode mode = ios_base::out); /** Returns @c true if the underlying stream is open. Returns @c false if * the stream pointer is @c nullptr. */ bool is_open () const; /** Move assignment. Transfers ownership of the stream and core from @p rhs. */ - basic_ofstream_dyad& operator= (basic_ofstream_dyad&& rhs); + basic_ofstream_dyad &operator= (basic_ofstream_dyad &&rhs); /** Swaps the underlying stream with @p rhs. Has no effect if the stream * pointer is @c nullptr. */ - void swap (basic_ofstream_dyad& rhs); + void swap (basic_ofstream_dyad &rhs); #endif /** @@ -956,10 +956,10 @@ class basic_ofstream_dyad /** Returns the underlying stream buffer, or @c nullptr if the stream * pointer is @c nullptr. */ - filebuf* rdbuf () const; + filebuf *rdbuf () const; /** Returns a reference to the underlying @c std::basic_ofstream. */ - basic_ofstream& get_stream (); + basic_ofstream &get_stream (); /** * @brief Reinitializes the stream core from an existing @c dyad_stream_core. @@ -970,10 +970,10 @@ class basic_ofstream_dyad * * @param[in] core Stream core to copy into this instance. */ - void init (const dyad_stream_core& core); + void init (const dyad_stream_core &core); /** Allow read-only access to the embedded @c dyad_stream_core. */ - const dyad_stream_core& core () const + const dyad_stream_core &core () const { return m_core; } @@ -999,7 +999,7 @@ using wofstream_dyad = basic_ofstream_dyad; #if __cplusplus < 201103L //---------------------------------------------------- template -basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const dyad_stream_core& core) +basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const dyad_stream_core &core) : m_core (core) { m_stream = new basic_ofstream (); @@ -1013,7 +1013,7 @@ basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad () } template -basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const char* filename, +basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const char *filename, std::ios_base::openmode mode) { m_core.init (); @@ -1036,7 +1036,7 @@ bool basic_ofstream_dyad<_CharT, _Traits>::is_open () } #else //----------------------------------------------------------------------- template -basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const dyad_stream_core& core) +basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const dyad_stream_core &core) : m_core (core) { m_stream = std::unique_ptr (new basic_ofstream ()); @@ -1050,7 +1050,7 @@ basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad () } template -basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const char* filename, +basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const char *filename, std::ios_base::openmode mode) { m_core.init (); @@ -1066,14 +1066,14 @@ basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const char* filename, } template -basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const string& filename, +basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const string &filename, std::ios_base::openmode mode) { m_core.init (); m_stream = std::unique_ptr (new basic_ofstream (filename, mode)); if ((m_stream != nullptr) && (*m_stream) #if DYAD_HAS_STD_FSTREAM_FD - && m_core.cmp_canonical_path_prefix (true, (const char* const)filename.c_str ()) + && m_core.cmp_canonical_path_prefix (true, (const char *const)filename.c_str ()) #endif // DYAD_HAS_STD_FSTREAM_FD ) { DYAD_EXCLUSIVE_LOCK_CPP_OFSTREAM (*m_stream, m_core); @@ -1082,7 +1082,7 @@ basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const string& filenam } template -basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (basic_ofstream_dyad&& rhs) +basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (basic_ofstream_dyad &&rhs) : m_stream (std::move (rhs.m_stream)), m_core (std::move (rhs.m_core)) { } @@ -1090,12 +1090,12 @@ basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (basic_ofstream_dyad&& #if (__cplusplus >= 201703L) && __has_include() template template -basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const _Path& filepath, +basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const _Path &filepath, std::ios_base::openmode mode) { m_core.init (); m_stream = std::unique_ptr ( - new basic_ofstream ((const char* const)filepath.c_str (), mode)); + new basic_ofstream ((const char *const)filepath.c_str (), mode)); if ((m_stream != nullptr) && (*m_stream) #if DYAD_HAS_STD_FSTREAM_FD @@ -1109,8 +1109,8 @@ basic_ofstream_dyad<_CharT, _Traits>::basic_ofstream_dyad (const _Path& filepath #endif // c++17 filesystem template -basic_ofstream_dyad<_CharT, _Traits>& basic_ofstream_dyad<_CharT, _Traits>::operator= ( - basic_ofstream_dyad&& rhs) +basic_ofstream_dyad<_CharT, _Traits> &basic_ofstream_dyad<_CharT, _Traits>::operator= ( + basic_ofstream_dyad &&rhs) { m_stream = std::move (rhs.m_stream); m_core = std::move (rhs.m_core); @@ -1146,7 +1146,7 @@ basic_ofstream_dyad<_CharT, _Traits>::~basic_ofstream_dyad () } template -void basic_ofstream_dyad<_CharT, _Traits>::open (const string& filename, +void basic_ofstream_dyad<_CharT, _Traits>::open (const string &filename, std::ios_base::openmode mode) { open (filename.c_str (), mode); @@ -1159,7 +1159,7 @@ bool basic_ofstream_dyad<_CharT, _Traits>::is_open () const } template -void basic_ofstream_dyad<_CharT, _Traits>::swap (basic_ofstream_dyad& rhs) +void basic_ofstream_dyad<_CharT, _Traits>::swap (basic_ofstream_dyad &rhs) { if (m_stream == nullptr) { // TODO: set fail bit if nullptr @@ -1170,7 +1170,7 @@ void basic_ofstream_dyad<_CharT, _Traits>::swap (basic_ofstream_dyad& rhs) #endif //----------------------------------------------------------------------- template -void basic_ofstream_dyad<_CharT, _Traits>::open (const char* filename, std::ios_base::openmode mode) +void basic_ofstream_dyad<_CharT, _Traits>::open (const char *filename, std::ios_base::openmode mode) { if (m_stream == nullptr) { // TODO: set fail bit if nullptr @@ -1202,7 +1202,7 @@ void basic_ofstream_dyad<_CharT, _Traits>::close () } template -std::filebuf* basic_ofstream_dyad<_CharT, _Traits>::rdbuf () const +std::filebuf *basic_ofstream_dyad<_CharT, _Traits>::rdbuf () const { if (m_stream == nullptr) { return nullptr; @@ -1211,7 +1211,7 @@ std::filebuf* basic_ofstream_dyad<_CharT, _Traits>::rdbuf () const } template -std::basic_ofstream<_CharT, _Traits>& basic_ofstream_dyad<_CharT, _Traits>::get_stream () +std::basic_ofstream<_CharT, _Traits> &basic_ofstream_dyad<_CharT, _Traits>::get_stream () { if (m_stream == nullptr) { // TODO: throw @@ -1220,7 +1220,7 @@ std::basic_ofstream<_CharT, _Traits>& basic_ofstream_dyad<_CharT, _Traits>::get_ } template -void basic_ofstream_dyad<_CharT, _Traits>::init (const dyad_stream_core& core) +void basic_ofstream_dyad<_CharT, _Traits>::init (const dyad_stream_core &core) { m_core = core; m_core.set_initialized (); @@ -1273,7 +1273,7 @@ class basic_fstream_dyad * * @param[in] core Initialized stream core to copy. */ - basic_fstream_dyad (const dyad_stream_core& core); + basic_fstream_dyad (const dyad_stream_core &core); /** Constructs an unopened stream and initializes the core from environment * variables via @c m_core.init(). */ @@ -1294,7 +1294,7 @@ class basic_fstream_dyad * @param[in] filename Path to the file to open. * @param[in] mode Open mode. Defaults to @c ios_base::in|out. */ - explicit basic_fstream_dyad (const char* filename, + explicit basic_fstream_dyad (const char *filename, ios_base::openmode mode = ios_base::in | ios_base::out); /** @@ -1322,7 +1322,7 @@ class basic_fstream_dyad * @param[in] filename Path to the file to open. * @param[in] mode Open mode. Defaults to @c ios_base::in|out. */ - void open (const char* filename, ios_base::openmode mode = ios_base::in | ios_base::out); + void open (const char *filename, ios_base::openmode mode = ios_base::in | ios_base::out); #if __cplusplus < 201103L /** Returns @c true if the underlying stream is open. Returns @c false if @@ -1331,13 +1331,13 @@ class basic_fstream_dyad #else /** Constructs and opens the stream from a @c std::string filename. See the * @c const char* constructor for full details. */ - explicit basic_fstream_dyad (const string& filename, + explicit basic_fstream_dyad (const string &filename, ios_base::openmode mode = ios_base::in | ios_base::out); /** Copy construction is disabled. */ - basic_fstream_dyad (const basic_fstream_dyad&) = delete; + basic_fstream_dyad (const basic_fstream_dyad &) = delete; /** Move constructor. Transfers ownership of the stream and core from @p rhs. */ - basic_fstream_dyad (basic_fstream_dyad&& rhs); + basic_fstream_dyad (basic_fstream_dyad &&rhs); #if (__cplusplus >= 201703L) && __has_include() /** @@ -1352,23 +1352,23 @@ class basic_fstream_dyad * @param[in] mode Open mode. Defaults to @c ios_base::out. */ template > - basic_fstream_dyad (const _Path& filepath, std::ios_base::openmode mode = std::ios_base::out); + basic_fstream_dyad (const _Path &filepath, std::ios_base::openmode mode = std::ios_base::out); #endif // c++17 filesystem /** Opens the file from a @c std::string filename. Delegates to the * @c const char* overload. */ - void open (const string& filename, ios_base::openmode mode = ios_base::in | ios_base::out); + void open (const string &filename, ios_base::openmode mode = ios_base::in | ios_base::out); /** Returns @c true if the underlying stream is open. Returns @c false if * the stream pointer is @c nullptr. */ bool is_open () const; /** Move assignment. Transfers ownership of the stream and core from @p rhs. */ - basic_fstream_dyad& operator= (basic_fstream_dyad&& rhs); + basic_fstream_dyad &operator= (basic_fstream_dyad &&rhs); /** Swaps the underlying stream with @p rhs. Has no effect if the stream * pointer is @c nullptr. */ - void swap (basic_fstream_dyad& rhs); + void swap (basic_fstream_dyad &rhs); #endif /** @@ -1385,10 +1385,10 @@ class basic_fstream_dyad /** Returns the underlying stream buffer, or @c nullptr if the stream * pointer is @c nullptr. */ - filebuf* rdbuf () const; + filebuf *rdbuf () const; /** Returns a reference to the underlying @c std::basic_fstream. */ - basic_fstream& get_stream (); + basic_fstream &get_stream (); /** * @brief Reinitializes the stream core from an existing @c dyad_stream_core. @@ -1399,10 +1399,10 @@ class basic_fstream_dyad * * @param[in] core Stream core to copy into this instance. */ - void init (const dyad_stream_core& core); + void init (const dyad_stream_core &core); /** Allow read-only access to the embedded @c dyad_stream_core. */ - const dyad_stream_core& core () const + const dyad_stream_core &core () const { return m_core; } @@ -1428,7 +1428,7 @@ using wfstream_dyad = basic_fstream_dyad; #if __cplusplus < 201103L //---------------------------------------------------- template -basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const dyad_stream_core& core) +basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const dyad_stream_core &core) : m_core (core) { m_stream = new basic_fstream (); @@ -1442,7 +1442,7 @@ basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad () } template -basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const char* filename, +basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const char *filename, std::ios_base::openmode mode) { m_core.init (); @@ -1467,7 +1467,7 @@ bool basic_fstream_dyad<_CharT, _Traits>::is_open () } #else //----------------------------------------------------------------------- template -basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const dyad_stream_core& core) +basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const dyad_stream_core &core) : m_core (core) { m_stream = std::unique_ptr (new basic_fstream ()); @@ -1481,7 +1481,7 @@ basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad () } template -basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const char* filename, +basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const char *filename, std::ios_base::openmode mode) { m_core.init (); @@ -1496,7 +1496,7 @@ basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const char* filename, } template -basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const string& filename, +basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const string &filename, std::ios_base::openmode mode) { m_core.init (); @@ -1511,7 +1511,7 @@ basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const string& filename, } template -basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (basic_fstream_dyad&& rhs) +basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (basic_fstream_dyad &&rhs) : m_stream (std::move (rhs.m_stream)), m_core (std::move (rhs.m_core)) { } @@ -1519,7 +1519,7 @@ basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (basic_fstream_dyad&& rh #if (__cplusplus >= 201703L) && __has_include() template template -basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const _Path& filepath, +basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const _Path &filepath, std::ios_base::openmode mode) { m_core.init (); @@ -1535,8 +1535,8 @@ basic_fstream_dyad<_CharT, _Traits>::basic_fstream_dyad (const _Path& filepath, #endif // c++17 filesystem template -basic_fstream_dyad<_CharT, _Traits>& basic_fstream_dyad<_CharT, _Traits>::operator= ( - basic_fstream_dyad&& rhs) +basic_fstream_dyad<_CharT, _Traits> &basic_fstream_dyad<_CharT, _Traits>::operator= ( + basic_fstream_dyad &&rhs) { m_stream = std::move (rhs.m_stream); m_core = std::move (rhs.m_core); @@ -1574,7 +1574,7 @@ basic_fstream_dyad<_CharT, _Traits>::~basic_fstream_dyad () } template -void basic_fstream_dyad<_CharT, _Traits>::open (const string& filename, +void basic_fstream_dyad<_CharT, _Traits>::open (const string &filename, std::ios_base::openmode mode) { open (filename.c_str (), mode); @@ -1587,7 +1587,7 @@ bool basic_fstream_dyad<_CharT, _Traits>::is_open () const } template -void basic_fstream_dyad<_CharT, _Traits>::swap (basic_fstream_dyad& rhs) +void basic_fstream_dyad<_CharT, _Traits>::swap (basic_fstream_dyad &rhs) { if (m_stream == nullptr) { // TODO: set fail bit if nullptr @@ -1598,7 +1598,7 @@ void basic_fstream_dyad<_CharT, _Traits>::swap (basic_fstream_dyad& rhs) #endif //----------------------------------------------------------------------- template -void basic_fstream_dyad<_CharT, _Traits>::open (const char* filename, std::ios_base::openmode mode) +void basic_fstream_dyad<_CharT, _Traits>::open (const char *filename, std::ios_base::openmode mode) { if (m_stream == nullptr) { // TODO: set fail bit if nullptr @@ -1635,7 +1635,7 @@ void basic_fstream_dyad<_CharT, _Traits>::close () } template -std::filebuf* basic_fstream_dyad<_CharT, _Traits>::rdbuf () const +std::filebuf *basic_fstream_dyad<_CharT, _Traits>::rdbuf () const { if (m_stream == nullptr) { return nullptr; @@ -1644,7 +1644,7 @@ std::filebuf* basic_fstream_dyad<_CharT, _Traits>::rdbuf () const } template -std::basic_fstream<_CharT, _Traits>& basic_fstream_dyad<_CharT, _Traits>::get_stream () +std::basic_fstream<_CharT, _Traits> &basic_fstream_dyad<_CharT, _Traits>::get_stream () { if (m_stream == nullptr) { // TODO: throw @@ -1653,7 +1653,7 @@ std::basic_fstream<_CharT, _Traits>& basic_fstream_dyad<_CharT, _Traits>::get_st } template -void basic_fstream_dyad<_CharT, _Traits>::init (const dyad_stream_core& core) +void basic_fstream_dyad<_CharT, _Traits>::init (const dyad_stream_core &core) { m_core = core; m_core.set_initialized (); diff --git a/src/dyad/cache/dyad_cache.c b/src/dyad/cache/dyad_cache.c index 85c5af97..991612a2 100644 --- a/src/dyad/cache/dyad_cache.c +++ b/src/dyad/cache/dyad_cache.c @@ -38,7 +38,8 @@ dyad_rc_t dyad_cache_scan_dir (const dyad_ctx_t *ctx, dirp = opendir (managed_path); if (dirp == NULL) { - DYAD_LOG_ERROR (ctx, "DYAD CACHE: Cannot open managed directory %s for scanning", + DYAD_LOG_ERROR (ctx, + "DYAD CACHE: Cannot open managed directory %s for scanning", managed_path); return DYAD_RC_BADFIO; } @@ -67,7 +68,8 @@ dyad_rc_t dyad_cache_scan_dir (const dyad_ctx_t *ctx, struct dyad_cache_entry *new_entries = realloc (entries, new_capacity * sizeof (struct dyad_cache_entry)); if (new_entries == NULL) { - DYAD_LOG_ERROR (ctx, "DYAD CACHE: Cannot allocate memory while scanning %s", + DYAD_LOG_ERROR (ctx, + "DYAD CACHE: Cannot allocate memory while scanning %s", managed_path); free (entries); closedir (dirp); @@ -97,8 +99,10 @@ static int dyad_cache_entry_cmp (const void *a, const void *b) { const struct dyad_cache_entry *ea = (const struct dyad_cache_entry *)a; const struct dyad_cache_entry *eb = (const struct dyad_cache_entry *)b; - if (ea->recency_key < eb->recency_key) return -1; - if (ea->recency_key > eb->recency_key) return 1; + if (ea->recency_key < eb->recency_key) + return -1; + if (ea->recency_key > eb->recency_key) + return 1; return 0; } @@ -123,7 +127,8 @@ dyad_rc_t dyad_cache_maybe_evict (dyad_ctx_t *ctx, const char *managed_path) return DYAD_RC_OK; } - if (DYAD_IS_ERROR (dyad_cache_scan_dir (ctx, managed_path, &entries, &n_entries, &total_size))) { + if (DYAD_IS_ERROR ( + dyad_cache_scan_dir (ctx, managed_path, &entries, &n_entries, &total_size))) { // Best-effort: a scan failure should not fail the caller's produce/consume. return DYAD_RC_OK; } @@ -150,7 +155,8 @@ dyad_rc_t dyad_cache_maybe_evict (dyad_ctx_t *ctx, const char *managed_path) } // Re-check: another process may have already evicted while we waited. - if (DYAD_IS_ERROR (dyad_cache_scan_dir (ctx, managed_path, &entries, &n_entries, &total_size))) { + if (DYAD_IS_ERROR ( + dyad_cache_scan_dir (ctx, managed_path, &entries, &n_entries, &total_size))) { dyad_release_flock (ctx, lock_fd, &dir_lock); close (lock_fd); return DYAD_RC_OK; diff --git a/src/dyad/client/dyad_client.c b/src/dyad/client/dyad_client.c index 37d97393..ddcb8de6 100644 --- a/src/dyad/client/dyad_client.c +++ b/src/dyad/client/dyad_client.c @@ -841,8 +841,8 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_get_data_range (const dyad_ctx_t *restrict ctx, DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Packing ranged payload for RPC to DYAD module"); DYAD_C_FUNCTION_UPDATE_INT ("owner_rank", mdata->owner_rank); DYAD_C_FUNCTION_UPDATE_STR ("fpath", mdata->fpath); - rc = ctx->dtl_handle->rpc_pack_range (ctx, mdata->fpath, mdata->owner_rank, offset, length, - &rpc_payload); + rc = ctx->dtl_handle + ->rpc_pack_range (ctx, mdata->fpath, mdata->owner_rank, offset, length, &rpc_payload); if (DYAD_IS_ERROR (rc)) { DYAD_LOG_ERROR (ctx, "Cannot create JSON payload for ranged Flux RPC to " @@ -900,7 +900,9 @@ get_range_done:; rc = DYAD_RC_BADRPC; } } - DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Read %zd bytes of range from %s file", *file_len, + DYAD_LOG_DEBUG (ctx, + "DYAD CLIENT: Read %zd bytes of range from %s file", + *file_len, mdata->fpath); DYAD_LOG_DEBUG (ctx, "DYAD CLIENT: Destroy the Flux future for the RPC."); flux_future_destroy (f); diff --git a/src/dyad/common/dyad_structures_int.h b/src/dyad/common/dyad_structures_int.h index 15015888..3ca47d83 100644 --- a/src/dyad/common/dyad_structures_int.h +++ b/src/dyad/common/dyad_structures_int.h @@ -42,27 +42,27 @@ struct dyad_ctx { uint32_t cons_real_hash; ///< hash of consumer managed real path uint32_t delim_len; ///< length of path delimiter // User Facing - bool debug; ///< if true, perform debug logging - bool check; ///< if true, perform some check logging - bool reenter; ///< if false, do not recursively enter DYAD - bool initialized; ///< if true, DYAD is initialized - bool shared_storage; ///< if true, the managed path is shared - bool async_publish; ///< Enable asynchronous publish by producer - bool fsync_write; ///< Apply fsync after write by producer - unsigned int key_depth; ///< Depth of bins for the Flux KVS - unsigned int key_bins; ///< Number of bins for the Flux KVS - uint32_t rank; ///< Flux rank for DYAD - uint32_t service_mux; ///< Number of Flux brokers sharing node-local storage - uint32_t node_idx; ///< Index of the node hosting broker(s) - int pid; ///< unix process id, obtained by getpid() - char *kvs_namespace; ///< Flux KVS namespace for DYAD - char *prod_managed_path; ///< producer path managed by DYAD - char *cons_managed_path; ///< consumer path managed by DYAD - bool relative_to_managed_path; ///< relative path is relative to the managed path + bool debug; ///< if true, perform debug logging + bool check; ///< if true, perform some check logging + bool reenter; ///< if false, do not recursively enter DYAD + bool initialized; ///< if true, DYAD is initialized + bool shared_storage; ///< if true, the managed path is shared + bool async_publish; ///< Enable asynchronous publish by producer + bool fsync_write; ///< Apply fsync after write by producer + unsigned int key_depth; ///< Depth of bins for the Flux KVS + unsigned int key_bins; ///< Number of bins for the Flux KVS + uint32_t rank; ///< Flux rank for DYAD + uint32_t service_mux; ///< Number of Flux brokers sharing node-local storage + uint32_t node_idx; ///< Index of the node hosting broker(s) + int pid; ///< unix process id, obtained by getpid() + char *kvs_namespace; ///< Flux KVS namespace for DYAD + char *prod_managed_path; ///< producer path managed by DYAD + char *cons_managed_path; ///< consumer path managed by DYAD + bool relative_to_managed_path; ///< relative path is relative to the managed path struct dyad_cache_policy *cache_policy; ///< Opaque handle to the active cache-eviction policy dyad_cache_policy_mode_t cache_policy_mode; ///< DYAD_CACHE_NONE (default), LRU, or FIFO - uint64_t cache_capacity_bytes; ///< 0 (default) disables eviction; else max bytes in DMD - double cache_low_watermark_frac; ///< Fraction of capacity to evict down to (default 0.8) + uint64_t cache_capacity_bytes; ///< 0 (default) disables eviction; else max bytes in DMD + double cache_low_watermark_frac; ///< Fraction of capacity to evict down to (default 0.8) unsigned int cache_grace_period_sec; ///< Skip candidates accessed within this many seconds char *origin_path; ///< Optional PFS/origin fallback path for dyad_range_cache_ensure(); ///< NULL (default) disables lazy origin-backed caching diff --git a/src/dyad/core/dyad_ctx.c b/src/dyad/core/dyad_ctx.c index a9e5cd68..5c649568 100644 --- a/src/dyad/core/dyad_ctx.c +++ b/src/dyad/core/dyad_ctx.c @@ -52,29 +52,29 @@ const struct dyad_ctx dyad_ctx_default = { 0u, ///< cons_real_hash 0u, ///< delim_len // User facing - false, ///< debug - false, ///< check - false, ///< reenter - true, ///< initialized - false, ///< shared_storage - false, ///< async_publish - false, ///< fsync_write - 3u, ///< key_depth - 1024u, ///< key_bins - 0u, ///< rank - 1u, ///< service_mux - 0u, ///< node_idx - -1, ///< pid - NULL, ///< kvs_namespace - NULL, ///< prod_managed_path - NULL, ///< cons_managed_path - false, ///< relative_to_managed_path - NULL, ///< cache_policy + false, ///< debug + false, ///< check + false, ///< reenter + true, ///< initialized + false, ///< shared_storage + false, ///< async_publish + false, ///< fsync_write + 3u, ///< key_depth + 1024u, ///< key_bins + 0u, ///< rank + 1u, ///< service_mux + 0u, ///< node_idx + -1, ///< pid + NULL, ///< kvs_namespace + NULL, ///< prod_managed_path + NULL, ///< cons_managed_path + false, ///< relative_to_managed_path + NULL, ///< cache_policy DYAD_CACHE_NONE, ///< cache_policy_mode - 0ull, ///< cache_capacity_bytes - 0.8, ///< cache_low_watermark_frac - 5u, ///< cache_grace_period_sec - NULL ///< origin_path + 0ull, ///< cache_capacity_bytes + 0.8, ///< cache_low_watermark_frac + 5u, ///< cache_grace_period_sec + NULL ///< origin_path }; DYAD_DLL_EXPORTED dyad_ctx_t *dyad_ctx_get (void) @@ -303,12 +303,12 @@ dyad_rc_t dyad_init (bool debug, // Initialize the cache-eviction policy. A NULL/unset cache_policy_str // is treated as DYAD_CACHE_NONE (eviction disabled) rather than an // error, since eviction is opt-in. - rc = dyad_set_and_init_cache_policy ( - (cache_capacity_bytes == 0ull) - ? dyad_cache_policy_name[DYAD_CACHE_NONE] - : ((cache_policy_str != NULL) ? cache_policy_str - : dyad_cache_policy_name[DYAD_CACHE_DEFAULT]), - cache_capacity_bytes); + rc = dyad_set_and_init_cache_policy ((cache_capacity_bytes == 0ull) + ? dyad_cache_policy_name[DYAD_CACHE_NONE] + : ((cache_policy_str != NULL) + ? cache_policy_str + : dyad_cache_policy_name[DYAD_CACHE_DEFAULT]), + cache_capacity_bytes); if (DYAD_IS_ERROR (rc)) { DYAD_LOG_ERROR (ctx, "Cannot initialize the cache-eviction policy %s", cache_policy_str); goto init_region_failed; @@ -594,12 +594,12 @@ DYAD_DLL_EXPORTED dyad_rc_t dyad_set_and_init_cache_policy (const char *cache_po } else if (strncmp (cache_policy_name, dyad_cache_policy_name[DYAD_CACHE_LRU], cache_policy_name_len) - == 0) { + == 0) { cache_policy_mode = DYAD_CACHE_LRU; } else if (strncmp (cache_policy_name, dyad_cache_policy_name[DYAD_CACHE_FIFO], cache_policy_name_len) - == 0) { + == 0) { cache_policy_mode = DYAD_CACHE_FIFO; } else { DYAD_LOG_STDERR ("Invalid env %s = %s.\n", DYAD_CACHE_POLICY_ENV, cache_policy_name); diff --git a/src/dyad/dtl/margo_dtl.c b/src/dyad/dtl/margo_dtl.c index 1916c9ea..fed95704 100644 --- a/src/dyad/dtl/margo_dtl.c +++ b/src/dyad/dtl/margo_dtl.c @@ -450,15 +450,18 @@ dyad_rc_t dyad_dtl_margo_addr_cache_lookup (const dyad_ctx_t *ctx, hg_addr_t addr = HG_ADDR_NULL; hg_return_t ret = margo_addr_lookup (margo_handle->mid, addr_str, &addr); if (ret != HG_SUCCESS) { - DYAD_LOG_ERROR (ctx, "[MARGO DTL] margo_addr_lookup failed for '%s': %d", addr_str, (int)ret); + DYAD_LOG_ERROR (ctx, + "[MARGO DTL] margo_addr_lookup failed for '%s': %d", + addr_str, + (int)ret); return DYAD_RC_MARGOINIT_FAIL; } if (margo_handle->addr_cache_len == margo_handle->addr_cache_cap) { size_t new_cap = margo_handle->addr_cache_cap == 0 ? 8 : margo_handle->addr_cache_cap * 2; struct dyad_dtl_margo_addr_cache_entry *grown = - (struct dyad_dtl_margo_addr_cache_entry *)realloc ( - margo_handle->addr_cache, new_cap * sizeof (*grown)); + (struct dyad_dtl_margo_addr_cache_entry *)realloc (margo_handle->addr_cache, + new_cap * sizeof (*grown)); if (grown == NULL) { DYAD_LOG_ERROR (ctx, "[MARGO DTL] Could not grow address cache"); margo_addr_free (margo_handle->mid, addr); diff --git a/src/dyad/service/dyad_exe.c b/src/dyad/service/dyad_exe.c index 1c8d81eb..3300ba9d 100644 --- a/src/dyad/service/dyad_exe.c +++ b/src/dyad/service/dyad_exe.c @@ -15,9 +15,9 @@ #endif struct dyad_cli_args { - char* prod_managed_path; - char* dtl_mode; - char* origin_path; + char *prod_managed_path; + char *dtl_mode; + char *origin_path; bool debug; }; typedef struct dyad_cli_args dyad_cli_args_t; @@ -27,7 +27,7 @@ static dyad_cli_args_t cli_args = {NULL, NULL, NULL, false}; typedef enum { INVALID_ACTION = -1, ACT_START = 0, ACT_STOP = 1, N_ACT = 2 } action_e; -static char* actions[N_ACT] = {"start", "stop"}; +static char *actions[N_ACT] = {"start", "stop"}; // global variable storing the action to perform static action_e action = INVALID_ACTION; @@ -76,7 +76,7 @@ static void usage (int status) exit (status); } -static void parse_cmd_arguments (int argc, char** argv) +static void parse_cmd_arguments (int argc, char **argv) { int ch = 0; int optidx = 2; @@ -90,7 +90,7 @@ static void parse_cmd_arguments (int argc, char** argv) {"error_log", required_argument, 0, 'e'}, {"origin_path", required_argument, 0, 'o'}, {0, 0, 0, 0}}; - static char* short_options = "hdm:i:e:p:o:"; + static char *short_options = "hdm:i:e:p:o:"; while ((ch = getopt_long (argc, argv, short_options, long_options, &optidx)) >= 0) { switch (ch) { @@ -124,7 +124,7 @@ static void parse_cmd_arguments (int argc, char** argv) } } -static int fork_exec_wait (char* const argv[]) +static int fork_exec_wait (char *const argv[]) { pid_t pid = fork (); if (pid < 0) { @@ -144,7 +144,7 @@ static int fork_exec_wait (char* const argv[]) return WIFEXITED (status) ? WEXITSTATUS (status) : EXIT_FAILURE; } -int dyad_start_service (dyad_cli_args_t* cli_args) +int dyad_start_service (dyad_cli_args_t *cli_args) { // DYAD_INSTALL_LIBDIR is defined in dyad_config.h.in char dyad_module_path[PATH_MAX + 1] = {0}; @@ -152,7 +152,7 @@ int dyad_start_service (dyad_cli_args_t* cli_args) // Fixed-size buffer sized for the largest possible combination of // flags below; grow this if another optional flag is added. - char* argv[16]; + char *argv[16]; int i = 0; argv[i++] = "flux"; argv[i++] = "exec"; @@ -176,16 +176,16 @@ int dyad_start_service (dyad_cli_args_t* cli_args) return fork_exec_wait (argv); } -int dyad_stop_service (dyad_cli_args_t* cli_args) +int dyad_stop_service (dyad_cli_args_t *cli_args) { (void)cli_args; - char* const argv[] = {"flux", "exec", "-r", "all", "flux", "module", "remove", "dyad", NULL}; + char *const argv[] = {"flux", "exec", "-r", "all", "flux", "module", "remove", "dyad", NULL}; return fork_exec_wait (argv); } -int main (int argc, char** argv) +int main (int argc, char **argv) { - char* cmd = NULL; + char *cmd = NULL; // Usage: dyad start|stop [options] if (argc < 2) { diff --git a/src/dyad/utils/read_all.h b/src/dyad/utils/read_all.h index e1b10a6b..44283686 100644 --- a/src/dyad/utils/read_all.h +++ b/src/dyad/utils/read_all.h @@ -84,8 +84,7 @@ ssize_t write_all (int fd, const void *buf, size_t len); #if DYAD_PERFFLOW __attribute__ ((annotate ("@critical_path()"))) #endif -ssize_t -read_all (int fd, void **bufp); +ssize_t read_all (int fd, void **bufp); #if defined(__cplusplus) } diff --git a/src/dyad/utils/test_murmur3.c b/src/dyad/utils/test_murmur3.c index b384d263..59986b75 100644 --- a/src/dyad/utils/test_murmur3.c +++ b/src/dyad/utils/test_murmur3.c @@ -46,8 +46,8 @@ #include "dyad/utils/murmur3.h" -static int gen_path_key (const char* restrict str, - char* restrict path_key, +static int gen_path_key (const char *restrict str, + char *restrict path_key, const size_t len, const uint32_t depth, const uint32_t width) @@ -60,7 +60,7 @@ static int gen_path_key (const char* restrict str, char buf[256] = {'\0'}; size_t cx = 0ul; int n = 0; - const char* str_long = str; + const char *str_long = str; size_t str_len = strlen (str); if (str == NULL || path_key == NULL || len == 0ul || str_len == 0ul) { @@ -97,7 +97,7 @@ static int gen_path_key (const char* restrict str, return 0; } -int main (int argc, char** argv) +int main (int argc, char **argv) { if (argc < 4) { printf ("Usage: %s depth width str1 [str2 [str3 ...]]\n", argv[0]); diff --git a/src/dyad/utils/utils.c b/src/dyad/utils/utils.c index 1cf62c9b..33a80d2d 100644 --- a/src/dyad/utils/utils.c +++ b/src/dyad/utils/utils.c @@ -60,11 +60,11 @@ #define DYAD_PATH_DELIM "/" #endif -uint32_t hash_str (const char* str, const uint32_t seed) +uint32_t hash_str (const char *str, const uint32_t seed) { if (!str) return 0u; - const char* str_long = str; + const char *str_long = str; size_t str_len = strlen (str); char buf[256] = {'\0'}; uint32_t hash[4] = {0u}; // Output for the hash @@ -85,12 +85,12 @@ uint32_t hash_str (const char* str, const uint32_t seed) return (hash[0] ^ hash[1] ^ hash[2] ^ hash[3]) + 1; } -uint32_t hash_path_prefix (const char* str, const uint32_t seed, const size_t len) +uint32_t hash_path_prefix (const char *str, const uint32_t seed, const size_t len) { if (!str || len == 0ul) { return 0u; } - const char* str_long = str; + const char *str_long = str; size_t str_len = strlen (str); char buf[256] = {'\0'}; uint32_t hash[4] = {0u}; // Output for the hash @@ -112,9 +112,9 @@ uint32_t hash_path_prefix (const char* str, const uint32_t seed, const size_t le return (hash[0] ^ hash[1] ^ hash[2] ^ hash[3]) + 1; } -char* concat_str (char* __restrict__ str, - const char* __restrict__ to_append, - const char* __restrict__ connector, +char *concat_str (char *__restrict__ str, + const char *__restrict__ to_append, + const char *__restrict__ connector, size_t str_capacity) { const size_t str_len_org = strlen (str); @@ -133,7 +133,7 @@ char* concat_str (char* __restrict__ str, } const size_t str_len = (con_end ? (str_len_org - con_len) : str_len_org); - const char* const str_end = str + str_capacity; + const char *const str_end = str + str_capacity; bool no_overlap = ((to_append + strlen (to_append) <= str) || (str_end <= to_append)) && ((connector + strlen (connector) <= str) || (str_end <= connector)); @@ -142,10 +142,10 @@ char* concat_str (char* __restrict__ str, return NULL; } - char* buf = (char*)calloc (all_len + 1ul, sizeof (char)); + char *buf = (char *)calloc (all_len + 1ul, sizeof (char)); memcpy (buf, str, str_len); - char* buf_pos = buf + str_len; + char *buf_pos = buf + str_len; if (connector != NULL) { memcpy (buf_pos, connector, con_len); @@ -160,10 +160,10 @@ char* concat_str (char* __restrict__ str, return str; } -bool extract_user_path (const char* __restrict__ prefix, - const char* __restrict__ full, - const char* __restrict__ delim, - char* __restrict__ upath, +bool extract_user_path (const char *__restrict__ prefix, + const char *__restrict__ full, + const char *__restrict__ delim, + char *__restrict__ upath, const size_t upath_capacity) { size_t prefix_len = strlen (prefix); @@ -176,7 +176,7 @@ bool extract_user_path (const char* __restrict__ prefix, } { - const char* const upath_end = upath + upath_capacity; + const char *const upath_end = upath + upath_capacity; bool no_overlap = ((prefix + prefix_len <= upath) || (upath_end <= prefix)) && ((full + full_len <= upath) || (upath_end <= full)) && ((delim + delim_len <= upath) || (upath_end <= delim)); @@ -226,10 +226,10 @@ bool extract_user_path (const char* __restrict__ prefix, return true; } -bool cmp_canonical_path_prefix (const dyad_ctx_t* __restrict__ ctx, +bool cmp_canonical_path_prefix (const dyad_ctx_t *__restrict__ ctx, const bool is_prod, - const char* __restrict__ path, - char* __restrict__ upath, + const char *__restrict__ path, + char *__restrict__ upath, const size_t upath_capacity) { // Only works when there are no multiple absolute paths via hardlinks @@ -239,8 +239,8 @@ bool cmp_canonical_path_prefix (const dyad_ctx_t* __restrict__ ctx, return false; } - const char* prefix = NULL; - const char* can_prefix = NULL; + const char *prefix = NULL; + const char *can_prefix = NULL; uint32_t prefix_len = 0u; uint32_t can_prefix_len = 0u; uint32_t prefix_hash = 0u; @@ -342,7 +342,7 @@ bool cmp_canonical_path_prefix (const dyad_ctx_t* __restrict__ ctx, * are not checked. Only the return value of the final @c mkdir() for * @p dir itself is returned to the caller. */ -int mkpath (const char* dir, const mode_t m) +int mkpath (const char *dir, const mode_t m) { struct stat sb; @@ -359,7 +359,7 @@ int mkpath (const char* dir, const mode_t m) return mkdir (dir, m); } -int mkdir_as_needed (const char* path, const mode_t m) +int mkdir_as_needed (const char *path, const mode_t m) { if (path == NULL || strlen (path) == 0ul) { DYAD_LOG_ERROR (NULL, "DYAD UTL: Cannot create a directory with no name\n"); @@ -422,7 +422,7 @@ int mkdir_as_needed (const char* path, const mode_t m) return 0; // The new directory has been succesfully created } -int get_path (const int fd, const size_t max_size, char* path) +int get_path (const int fd, const size_t max_size, char *path) { if (max_size < 1ul) { DYAD_LOG_DEBUG (NULL, "DYAD UTIL: Invalid max path size."); @@ -447,7 +447,7 @@ int get_path (const int fd, const size_t max_size, char* path) return 0; } -bool is_path_dir (const char* path) +bool is_path_dir (const char *path) { if (path == NULL || strlen (path) == 0ul) { return false; @@ -476,7 +476,7 @@ bool is_fd_dir (int fd) } #if DYAD_SPIN_WAIT -bool get_stat (const char* path, unsigned int max_retry, long ns_sleep) +bool get_stat (const char *path, unsigned int max_retry, long ns_sleep) { struct stat sb; unsigned int num_retry = 0u; @@ -521,9 +521,9 @@ ssize_t get_file_size (int fd) } #endif -dyad_rc_t dyad_excl_flock (const dyad_ctx_t* __restrict__ ctx, +dyad_rc_t dyad_excl_flock (const dyad_ctx_t *__restrict__ ctx, int fd, - struct flock* __restrict__ lock) + struct flock *__restrict__ lock) { dyad_rc_t rc = DYAD_RC_OK; DYAD_C_FUNCTION_START (); @@ -554,9 +554,9 @@ excl_flock_end:; return rc; } -dyad_rc_t dyad_try_excl_flock (const dyad_ctx_t* __restrict__ ctx, +dyad_rc_t dyad_try_excl_flock (const dyad_ctx_t *__restrict__ ctx, int fd, - struct flock* __restrict__ lock) + struct flock *__restrict__ lock) { dyad_rc_t rc = DYAD_RC_OK; DYAD_C_FUNCTION_START (); @@ -592,9 +592,9 @@ try_excl_flock_end:; return rc; } -dyad_rc_t dyad_shared_flock (const dyad_ctx_t* __restrict__ ctx, +dyad_rc_t dyad_shared_flock (const dyad_ctx_t *__restrict__ ctx, int fd, - struct flock* __restrict__ lock) + struct flock *__restrict__ lock) { DYAD_C_FUNCTION_START (); DYAD_C_FUNCTION_UPDATE_INT ("fd", fd); @@ -625,9 +625,9 @@ shared_flock_end:; return rc; } -dyad_rc_t dyad_release_flock (const dyad_ctx_t* __restrict__ ctx, +dyad_rc_t dyad_release_flock (const dyad_ctx_t *__restrict__ ctx, int fd, - struct flock* __restrict__ lock) + struct flock *__restrict__ lock) { DYAD_C_FUNCTION_START (); DYAD_C_FUNCTION_UPDATE_INT ("fd", fd); @@ -655,11 +655,11 @@ release_flock_end:; } #if DYAD_SYNC_DIR -int sync_containing_dir (const char* path) +int sync_containing_dir (const char *path) { char fullpath[PATH_MAX + 2] = {'\0'}; strncpy (fullpath, path, PATH_MAX); - const char* containing_dir = dirname (fullpath); + const char *containing_dir = dirname (fullpath); int dir_fd = open (containing_dir, O_RDONLY); if (strlen (containing_dir) == 0) { return 0; From 033df7ae9829a4428293e00a28a0b21564f23509 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Thu, 16 Jul 2026 18:28:17 -0700 Subject: [PATCH 7/8] Fix utils CMAKE file Signed-off-by: Chen Wang --- src/dyad/utils/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dyad/utils/CMakeLists.txt b/src/dyad/utils/CMakeLists.txt index 65b68def..9f6029a1 100644 --- a/src/dyad/utils/CMakeLists.txt +++ b/src/dyad/utils/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(${PROJECT_NAME}_utils SHARED ${DYAD_UTILS_SRC} target_link_libraries(${PROJECT_NAME}_utils PUBLIC ${PROJECT_NAME}_base64 ${PROJECT_NAME}_murmur3 + flux::core Threads::Threads) if(DYAD_LOGGER STREQUAL "CPP_LOGGER") @@ -35,6 +36,7 @@ target_compile_definitions(${PROJECT_NAME}_utils PUBLIC DYAD_HAS_CONFIG) target_include_directories(${PROJECT_NAME}_utils PUBLIC $ $) +target_include_directories(${PROJECT_NAME}_utils SYSTEM PRIVATE ${FluxCore_INCLUDE_DIRS}) add_library(${PROJECT_NAME}_murmur3 SHARED ${DYAD_MURMUR3_SRC}) # set_target_properties(${PROJECT_NAME}_murmur3 PROPERTIES CMAKE_INSTALL_RPATH From ad12a21228299d52f1dcac89a9819257e8bd7408 Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Fri, 17 Jul 2026 09:32:38 +0800 Subject: [PATCH 8/8] Fix formating Signed-off-by: Chen Wang --- src/dyad/service/flux_module/dyad.c | 12 ++++-------- src/dyad/utils/read_all.h | 3 ++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/dyad/service/flux_module/dyad.c b/src/dyad/service/flux_module/dyad.c index 4b926b39..072cbe12 100644 --- a/src/dyad/service/flux_module/dyad.c +++ b/src/dyad/service/flux_module/dyad.c @@ -334,10 +334,8 @@ getctx_error:; #if DYAD_PERFFLOW __attribute__ ((annotate ("@critical_path()"))) #endif -static void dyad_fetch_request_cb (flux_t *h, - flux_msg_handler_t *w, - const flux_msg_t *msg, - void *arg) +static void +dyad_fetch_request_cb (flux_t *h, flux_msg_handler_t *w, const flux_msg_t *msg, void *arg) { DYAD_C_FUNCTION_START (); dyad_mod_ctx_t *mod_ctx = get_mod_ctx (h); @@ -888,10 +886,8 @@ static void dyad_fetch_pool_stop (dyad_mod_ctx_t *mod_ctx) #if DYAD_PERFFLOW __attribute__ ((annotate ("@critical_path()"))) #endif -static void dyad_fetch_range_request_cb (flux_t *h, - flux_msg_handler_t *w, - const flux_msg_t *msg, - void *arg) +static void +dyad_fetch_range_request_cb (flux_t *h, flux_msg_handler_t *w, const flux_msg_t *msg, void *arg) { DYAD_C_FUNCTION_START (); dyad_mod_ctx_t *mod_ctx = get_mod_ctx (h); diff --git a/src/dyad/utils/read_all.h b/src/dyad/utils/read_all.h index 44283686..e1b10a6b 100644 --- a/src/dyad/utils/read_all.h +++ b/src/dyad/utils/read_all.h @@ -84,7 +84,8 @@ ssize_t write_all (int fd, const void *buf, size_t len); #if DYAD_PERFFLOW __attribute__ ((annotate ("@critical_path()"))) #endif -ssize_t read_all (int fd, void **bufp); +ssize_t +read_all (int fd, void **bufp); #if defined(__cplusplus) }