From 3b1c426a7c03ed494d66f724a615e974f0e34265 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 07:06:01 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`guy-rdm?= =?UTF-8?q?a-pr2`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @guymguym. * https://github.com/noobaa/noobaa-core/pull/9337#issuecomment-3625317160 The following files were modified: * `src/deploy/RPM_build/rpmbuild.sh` * `src/native/cuda/cuda_napi.cpp` * `src/native/cuobj/cuobj_client_napi.cpp` * `src/native/cuobj/cuobj_server_napi.cpp` * `src/native/fs/fs_napi.cpp` * `src/native/nb_native.cpp` * `src/native/tools/crypto_napi.cpp` * `src/native/util/backtrace.h` * `src/native/util/common.h` * `src/native/util/napi.h` * `src/native/util/worker.h` * `src/util/js_utils.js` --- src/deploy/RPM_build/rpmbuild.sh | 89 ++++ src/native/cuda/cuda_napi.cpp | 423 +++++++++++++++ src/native/cuobj/cuobj_client_napi.cpp | 468 ++++++++++++++++ src/native/cuobj/cuobj_server_napi.cpp | 709 +++++++++++++++++++++++++ src/native/fs/fs_napi.cpp | 674 ++++++++++++++++++++--- src/native/nb_native.cpp | 38 +- src/native/tools/crypto_napi.cpp | 131 +++-- src/native/util/backtrace.h | 72 ++- src/native/util/common.h | 10 +- src/native/util/napi.h | 280 +++++++++- src/native/util/worker.h | 136 +++++ src/util/js_utils.js | 32 +- 12 files changed, 2899 insertions(+), 163 deletions(-) create mode 100644 src/deploy/RPM_build/rpmbuild.sh create mode 100644 src/native/cuda/cuda_napi.cpp create mode 100644 src/native/cuobj/cuobj_client_napi.cpp create mode 100644 src/native/cuobj/cuobj_server_napi.cpp create mode 100644 src/native/util/worker.h diff --git a/src/deploy/RPM_build/rpmbuild.sh b/src/deploy/RPM_build/rpmbuild.sh new file mode 100644 index 0000000000..d5ca7709b7 --- /dev/null +++ b/src/deploy/RPM_build/rpmbuild.sh @@ -0,0 +1,89 @@ +#!/bin/bash +set -eou pipefail +set -x + +SKIP_NODE_INSTALL=1 source ./src/deploy/NVA_build/install_nodejs.sh +NODE_PATH="${NODE_PATH:-/usr/local/node}" + +noobaaver="$(npm pkg get version | tr -d '"')" +releasedate=$(date '+%a %b %d %Y') +nodever=$(node --version | tr -d 'v') +revision="$(date -u '+%Y%m%d')" +changelogdata="Initial release of NooBaa ${noobaaver}" +ARCHITECTURE=$(uname -m) + +SRC_DIR=$(realpath "$1") +BUILD_DIR="${2:-/tmp}" + +SPEC_TEMPLATE="./src/deploy/RPM_build/noobaa.spec" +RPMBUILD_DIR="$BUILD_DIR/rpmbuild" +SPEC_FILE="$RPMBUILD_DIR/SPECS/noobaa.spec" +SOURCE_TAR="$RPMBUILD_DIR/SOURCES/noobaa-core-${noobaaver}-${revision}.tar.gz" + +mkdir -p $RPMBUILD_DIR/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + +# create_source_tar creates a gzipped tarball of SRC_DIR at SOURCE_TAR, excluding build artifacts, CI/tooling directories, VCS metadata, and any paths listed in .gitignore. +function create_source_tar() { + exclude_flag_local="--exclude ./build --exclude ./images --exclude ./tools --exclude ./submodules --exclude .github --exclude .travis --exclude .jenkins" + + # using awk to parse .gitignore and create --exclude flags, ignoring comments and empty lines, and making paths relative + exclude_flag_gitignore="$(awk '!/^[[:blank:]]*(#|$)/ { $1=$1; c1=substr($0,1,1); printf "--exclude=%s%s ",(c1=="/" ? "." : ""),$0; }' .gitignore)" + + # No quotes around flags is intentional + tar $exclude_flag_local $exclude_flag_gitignore --exclude-vcs -czvf $SOURCE_TAR $SRC_DIR +} + +# set_changelog sets the `changelogdata` variable to a Lua snippet that prints the contents of `changelog.txt` when that file exists. +function set_changelog() { + if [ -f changelog.txt ]; then + local path=$(realpath changelog.txt) + changelogdata="%{lua: print(io.open(\"$path\"):read(\"*a\"))}" + fi +} + +# Generate the spec file from the template by replacing '%define NAME null' +# generate_spec_from_template creates the RPM spec file at $SPEC_FILE from $SPEC_TEMPLATE by replacing lines of the form "%define null" with the value of the corresponding shell variable and writing the processed lines to $SPEC_FILE. +function generate_spec_from_template() { + ls -l $SPEC_TEMPLATE + ls -l $RPMBUILD_DIR/SPECS/ + cat $SPEC_FILE || true + while IFS= read -r line; do + # Check if the line starts with '%define' and contains 'null' + if [[ $line =~ ^%define[[:space:]]+([^[:space:]]+)[[:space:]]+null$ ]]; then + # Extract the variable name + VAR_NAME="${BASH_REMATCH[1]}" + + # Get the value of the variable + VAR_VALUE=$(eval echo "\${${VAR_NAME}}") + + # Replace 'null' with the variable value + line="${line/null/$VAR_VALUE}" + fi + + # Write the updated line to the output file + echo "$line" >>$SPEC_FILE + done <$SPEC_TEMPLATE + cat $SPEC_FILE || true +} + +# build_rpm builds binary RPMs or a source RPM from the spec file and moves produced artifacts into BUILD_DIR. +function build_rpm() { + # Print the generated spec file if NRPM_DEBUG is set to true + [[ "${NRPM_DEBUG:-false}" == "true" ]] && cat $SPEC_FILE + + if [[ "$SRPM_ONLY" = "true" ]]; then + rpmbuild --define "_topdir $RPMBUILD_DIR" -bs $SPEC_FILE + else + rpmbuild --define "_topdir $RPMBUILD_DIR" -ba $SPEC_FILE + + # Move the RPM package to the build directory + mv $RPMBUILD_DIR/RPMS/${ARCHITECTURE}/noobaa-core-${noobaaver}-${revision}.*.rpm $BUILD_DIR/ + fi + # Move the SRPM package to the build directory + mv $RPMBUILD_DIR/SRPMS/noobaa-core-${noobaaver}-${revision}.*.rpm $BUILD_DIR/ +} + +create_source_tar +set_changelog +generate_spec_from_template +build_rpm \ No newline at end of file diff --git a/src/native/cuda/cuda_napi.cpp b/src/native/cuda/cuda_napi.cpp new file mode 100644 index 0000000000..dc0477b55c --- /dev/null +++ b/src/native/cuda/cuda_napi.cpp @@ -0,0 +1,423 @@ +/* Copyright (C) 2016 NooBaa */ +#include "../util/common.h" +#include "../util/napi.h" +#include "../util/worker.h" +#include + +#define CU_TRY(fn) \ + do { \ + CUresult r = fn; \ + if (r != CUDA_SUCCESS) { \ + const char* cuda_err = ""; \ + cuGetErrorName(r, &cuda_err); \ + throw Napi::Error::New(env, \ + XSTR() << __func__ << " " #fn " " \ + << DVAL(r) << DVAL(cuda_err)); \ + } \ + } while (0) + +#define CU_WARN(fn) \ + do { \ + CUresult r = fn; \ + if (r != CUDA_SUCCESS) { \ + const char* cuda_err = ""; \ + cuGetErrorName(r, &cuda_err); \ + LOG("WARNING: " \ + << __func__ << " " #fn " " \ + << DVAL(r) << DVAL(cuda_err)); \ + } \ + } while (0) + +#define CUDA_TRY(fn) \ + do { \ + cudaError_t r = fn; \ + if (r != cudaSuccess) { \ + const char* cuda_err = cudaGetErrorName(r); \ + throw Napi::Error::New(env, \ + XSTR() << __func__ << " " #fn " " \ + << DVAL(r) << DVAL(cuda_err)); \ + } \ + } while (0) + +namespace noobaa +{ + +DBG_INIT(1); + +CUdevice cuda_napi_dev_num = -1; +CUdevice cuda_napi_dev = -1; +CUcontext cuda_napi_ctx = 0; + +// About cuda context management: +// read this most helpful answer - https://forums.developer.nvidia.com/t/cuda-context-and-threading/26625/6 +// Main points: +// 1) a context belongs to a single device. +// 2) a thread has a single context bound at a time (ignoring context stack stuff) +// 3) a context can be bound to multiple threads simultaneously +// so we use the primary context on each device and bind it to our worker threads. +/** + * @brief Ensure a CUDA context is initialized and bound for the current process and worker threads. + * + * Initializes CUDA for the specified device (if not already initialized for that device), retains + * the device's primary context, sets that context as current, and updates internal globals so + * other native threads (including cuobj_client worker threads) can use the same CUDA context. + * + * @param env N-API environment (unused by this function but provided for call sites that have it). + * @param dev_num Device ordinal to initialize and bind the context for. Defaults to 0. + * + * @throws Napi::Error If any CUDA driver API call fails during initialization or context binding. + */ +static void +cuda_napi_ctx_init(Napi::Env env, int dev_num = 0) +{ + if (cuda_napi_dev_num == dev_num && cuda_napi_ctx) return; + + CUdevice dev = -1; + CUcontext ctx = 0; + + CU_TRY(cuInit(0)); + CU_TRY(cuDeviceGet(&dev, dev_num)); + + CU_TRY(cuDevicePrimaryCtxRetain(&ctx, dev)); + CU_TRY(cuCtxSetCurrent(ctx)); + + cuda_napi_dev_num = dev_num; + cuda_napi_dev = dev; + cuda_napi_ctx = ctx; + + // rdma_napi needs worker threads to set the cuda context + // and since we depend on rdma_napi we set the context here. + extern CUcontext cuobj_client_napi_cuda_ctx; + cuobj_client_napi_cuda_ctx = ctx; + + LOG("cuda_napi_ctx_init " << DVAL(dev_num) << DVAL(dev) << DVAL(ctx)); +} + +/** + * Helper class for CudaMemory + * Represents a slice of CUDA device memory that can be sub-sliced. + */ +struct CudaSlice +{ + CUdeviceptr ptr; + size_t size; + + /** + * @brief Return a sub-slice of this device memory region. + * + * Creates a new CudaSlice that represents the byte range from `start` (inclusive) + * to `end` (exclusive) relative to this slice's base pointer. Values are clamped + * to this slice's bounds; if `end` is less than `start` an empty slice is + * returned. + * + * @param start Offset in bytes from the start of this slice (inclusive). + * @param end Offset in bytes from the start of this slice (exclusive). + * @return CudaSlice The resulting sub-slice covering [start, end). + */ + CudaSlice slice(size_t start, size_t end) + { + if (start > size) start = size; + if (end > size) end = size; + if (end < start) end = start; + return { ptr + start, end - start }; + } + + friend /** + * @brief Formats a CudaSlice as "[+]" into an output stream. + * + * Produces a short textual representation containing the slice's device pointer and size. + * + * @param os Output stream to write into. + * @param x CudaSlice to format. + * @return std::ostream& The same output stream after writing the representation. + */ + std::ostream& + operator<<(std::ostream& os, CudaSlice& x) + { + return os << "[" << ((void*)x.ptr) << "+" << ((void*)x.size) << "]"; + } +}; + +/** + * CudaMemory N-API ObjectWrap + * Manages a chunk of CUDA device memory. + */ +struct CudaMemory : public Napi::ObjectWrap +{ + static Napi::FunctionReference constructor; + CudaSlice mem; + + static Napi::Function Init(Napi::Env env); + CudaMemory(const Napi::CallbackInfo& info); + ~CudaMemory(); + Napi::Value free(const Napi::CallbackInfo& info); + Napi::Value fill(const Napi::CallbackInfo& info); + Napi::Value as_buffer(const Napi::CallbackInfo& info); + Napi::Value copy_to_host_new(const Napi::CallbackInfo& info); + Napi::Value copy_to_host(const Napi::CallbackInfo& info); + Napi::Value copy_from_host(const Napi::CallbackInfo& info); +}; + +Napi::FunctionReference CudaMemory::constructor; + +/** + * @brief Define and export the JavaScript CudaMemory class on the given N-API environment. + * + * Creates the N-API class "CudaMemory" with instance methods for freeing, filling, + * obtaining a Buffer view, and copying to/from host memory, stores a persistent + * reference to its constructor, and returns the constructor function. + * + * @param env N-API environment used to define the class. + * @return Napi::Function The JavaScript constructor function for the `CudaMemory` class. + */ +Napi::Function +CudaMemory::Init(Napi::Env env) +{ + constructor = Napi::Persistent(DefineClass(env, + "CudaMemory", + { + InstanceMethod<&CudaMemory::free>("free"), + InstanceMethod<&CudaMemory::fill>("fill"), + InstanceMethod<&CudaMemory::as_buffer>("as_buffer"), + InstanceMethod<&CudaMemory::copy_to_host_new>("copy_to_host_new"), + InstanceMethod<&CudaMemory::copy_to_host>("copy_to_host"), + InstanceMethod<&CudaMemory::copy_from_host>("copy_from_host"), + })); + constructor.SuppressDestruct(); + return constructor.Value(); +} + +/** + * @brief Construct a CudaMemory object and allocate a CUDA device memory region. + * + * Allocates a device memory region of the size specified by the first argument + * and stores it in the object's internal slice. Ensures the CUDA context is + * initialized before allocation. + * + * @param info N-API callback info where the first argument is the allocation + * size in bytes (Number or BigInt-compatible). + * @throws Napi::Error If CUDA context initialization or device memory allocation fails. + */ +CudaMemory::CudaMemory(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(info) +{ + auto env = info.Env(); + size_t size = info[0].As().Int64Value(); + CUdeviceptr ptr = 0; + cuda_napi_ctx_init(env); + CU_TRY(cuMemAlloc(&ptr, size)); + mem = { ptr, size }; + DBG1("CudaMemory::ctor " << DVAL(mem)); +} + +/** + * @brief Releases the CUDA device memory held by this object and resets its slice to empty. + * + * If a device pointer is present, the destructor frees that device memory and sets `mem` to `{0, 0}`. + */ +CudaMemory::~CudaMemory() +{ + if (mem.ptr) { + auto free_mem = mem; + mem = { 0, 0 }; + CU_WARN(cuMemFree(free_mem.ptr)); + DBG1("CudaMemory::dtor " << DVAL(free_mem)); + } +} + +/** + * @brief Releases the underlying CUDA device memory held by this instance and clears the stored pointer and size. + * + * If no device memory is allocated, the call does nothing. + * + * @return Napi::Value JavaScript `undefined`. + */ +Napi::Value +CudaMemory::free(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + if (mem.ptr) { + auto free_mem = mem; + mem = { 0, 0 }; + CU_TRY(cuMemFree(free_mem.ptr)); + DBG1("CudaMemory::free " << DVAL(free_mem)); + } + return env.Undefined(); +} + +/** + * @brief Fill a sub-range of the CUDA device memory with a byte value. + * + * Expects JS arguments: a numeric value whose low byte is used to fill, and + * optional start and end offsets (in bytes) that specify the sub-range to fill. + * + * @param info Callback arguments where: + * - info[0]: fill value (number) — low 8 bits used as the byte pattern. + * - info[1] (optional): start offset in bytes (defaults to 0). + * - info[2] (optional): end offset in bytes (defaults to the allocation size). + * @return size_t Number of bytes filled in the device memory slice. + */ +Napi::Value +CudaMemory::fill(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + uint32_t value = info[0].As().Uint32Value(); + size_t start = info.Length() > 1 ? info[1].As().Int64Value() : 0; + size_t end = info.Length() > 2 ? info[2].As().Int64Value() : mem.size; + auto slice = mem.slice(start, end); + uint8_t byte = value & 0xff; + CU_TRY(cuMemsetD8(slice.ptr, byte, slice.size)); + DBG1("CudaMemory::fill " << DVAL(mem) << DVAL(slice) << DVAL(byte)); + return Napi::Number::New(info.Env(), slice.size); +} + +/** + * @brief Create a Node.js Buffer that views a sub-range of the CUDA device memory managed by this object. + * + * The returned Buffer wraps the device pointer for the requested slice and does not copy memory to host. + * Because its underlying storage is device memory, the Buffer cannot be directly used for JavaScript-level + * reads/writes; it is intended for passing to native addons or APIs that understand CUDA device pointers. + * + * @param info Callback arguments where: + * - info[0] (optional) start index, in bytes, within the allocation (default: 0). + * - info[1] (optional) end index, in bytes, within the allocation (default: allocation size). + * @return Napi::Value A Node.js Buffer whose data pointer references the specified device memory slice. + */ +Napi::Value +CudaMemory::as_buffer(const Napi::CallbackInfo& info) +{ + size_t start = info.Length() > 0 ? info[0].As().Int64Value() : 0; + size_t end = info.Length() > 1 ? info[1].As().Int64Value() : mem.size; + auto slice = mem.slice(start, end); + auto buffer = Napi::Buffer::New(info.Env(), (uint8_t*)slice.ptr, slice.size); + DBG1("CudaMemory::as_buffer " << DVAL(mem) << DVAL(slice) << DBUF(buffer)); + return buffer; +} + +/** + * @brief Copies a sub-range of the device memory into a newly allocated Node.js Buffer. + * + * Copies device bytes from the range [start, end) of this CudaMemory into a new Buffer and returns that Buffer. + * + * @param info[0] Optional start index (byte offset) within the device memory; defaults to 0. + * @param info[1] Optional end index (byte offset, exclusive) within the device memory; defaults to the allocation size. + * @return Napi::Buffer A Buffer containing the copied bytes from the requested sub-range. + */ +Napi::Value +CudaMemory::copy_to_host_new(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + size_t start = info.Length() > 0 ? info[0].As().Int64Value() : 0; + size_t end = info.Length() > 1 ? info[1].As().Int64Value() : mem.size; + auto slice = mem.slice(start, end); + auto buffer = Napi::Buffer::New(info.Env(), slice.size); + CU_TRY(cuMemcpyDtoH(buffer.Data(), slice.ptr, slice.size)); + DBG1("CudaMemory::copy_to_host_new " << DVAL(mem) << DVAL(slice) << DVAL(buffer.Data())); + return buffer; +} + +/** + * @brief Copy a sub-range of the device memory into a provided host Buffer. + * + * Expects a Node.js Buffer as the first argument and optional numeric + * start and end indices as the second and third arguments. The copy range + * is the slice [start, end) clamped to the allocation; the number of bytes + * copied is the lesser of the slice size and the provided buffer's length. + * + * @param info CallbackInfo whose arguments are: + * - info[0]: Napi::Buffer destination buffer (required). + * - info[1]: start index within the device memory slice (optional, default 0). + * - info[2]: end index within the device memory slice (optional, default mem.size). + * @return Napi::Number Number of bytes actually copied into the provided buffer. + */ +Napi::Value +CudaMemory::copy_to_host(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + auto buffer = info[0].As>(); + size_t start = info.Length() > 1 ? info[1].As().Int64Value() : 0; + size_t end = info.Length() > 2 ? info[2].As().Int64Value() : mem.size; + auto slice = mem.slice(start, end); + size_t len = std::min(slice.size, buffer.Length()); + CU_TRY(cuMemcpyDtoH(buffer.Data(), slice.ptr, len)); + DBG1("CudaMemory::copy_to_host " << DVAL(mem) << DVAL(slice) << DBUF(buffer) << DVAL(len)); + return Napi::Number::New(info.Env(), len); +} + +/** + * @brief Copies data from a host buffer into a sub-range of the device memory. + * + * Copies up to the lesser of the target slice size and the host buffer length. + * + * @param buffer Host Buffer whose contents will be copied into device memory. + * @param start Optional start index within the device memory slice (default 0). + * @param end Optional end index within the device memory slice (exclusive, default = mem.size). + * @return size_t Number of bytes actually copied. + */ +Napi::Value +CudaMemory::copy_from_host(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + auto buffer = info[0].As>(); + size_t start = info.Length() > 1 ? info[1].As().Int64Value() : 0; + size_t end = info.Length() > 2 ? info[2].As().Int64Value() : mem.size; + auto slice = mem.slice(start, end); + size_t len = std::min(slice.size, buffer.Length()); + CU_TRY(cuMemcpyHtoD(slice.ptr, buffer.Data(), len)); + DBG1("CudaMemory::copy_from_host " << DVAL(mem) << DVAL(slice) << DBUF(buffer) << DVAL(len)); + return Napi::Number::New(info.Env(), len); +} + +/** + * @brief Allocate CUDA device memory and return a Buffer that wraps it. + * + * Allocates a device memory region using the CUDA Runtime API, initializes it, + * and returns a Node.js Buffer whose lifecycle finalizer frees the device memory. + * + * @returns Napi::Value A Buffer pointing to the allocated CUDA device memory; when + * the Buffer is garbage-collected or its finalizer runs, `cudaFree` is + * invoked to release the device memory. + * + * @throws Napi::Error If the build does not enable the CUDA Runtime API (USE_CUDART). + */ +Napi::Value +cuda_malloc(const Napi::CallbackInfo& info) +{ +#if USE_CUDART + size_t size = info[0].As().Int64Value(); + // CUDA_TRY(cudaMemcpy(host_ptr, cuda_ptr, size, cudaMemcpyDeviceToHost)); + // CUDA_TRY(cudaMemcpy(cuda_ptr, host_ptr, size, cudaMemcpyHostToDevice)); + size_t size = info[0].As().Int64Value(); + void* cuda_ptr = 0; + CUDA_TRY(cudaMalloc(&cuda_ptr, size)); + CUDA_TRY(cudaMemset(cuda_ptr, 'A', size)); + CUDA_TRY(cudaStreamSynchronize(0)); + cuObjMemoryType_t mem_type = cuObjClient::getMemoryType(cuda_ptr); + LOG("cuda_malloc: " << DVAL(cuda_ptr) << DVAL(size) << DVAL(mem_type)); + + auto finalizer = [](Napi::Env, uint8_t* ptr) { cudaFree(ptr); }; + auto buf = Napi::Buffer::New(info.Env(), (uint8_t*)cuda_ptr, size, finalizer); + return buf; +#else + throw Napi::Error::New(info.Env(), + "CUDA Runtime API is not enabled in this build (USE_CUDART)"); +#endif +} + +/** + * @brief Registers the CudaMemory class on the provided module exports object. + * + * Attaches the CudaMemory constructor to `exports` under the property "CudaMemory" + * and logs that the CUDA library has loaded. + * + * @param env N-API environment used to create the class binding. + * @param exports Module exports object that will receive the `CudaMemory` binding. + */ +void +cuda_napi(Napi::Env env, Napi::Object exports) +{ + exports["CudaMemory"] = CudaMemory::Init(env); + DBG0("CUDA: library loaded."); +} + +} // namespace noobaa \ No newline at end of file diff --git a/src/native/cuobj/cuobj_client_napi.cpp b/src/native/cuobj/cuobj_client_napi.cpp new file mode 100644 index 0000000000..70bfdbbe53 --- /dev/null +++ b/src/native/cuobj/cuobj_client_napi.cpp @@ -0,0 +1,468 @@ +/* Copyright (C) 2016 NooBaa */ +#include "../util/common.h" +#include "../util/napi.h" +#include "../util/worker.h" +#include + +// cuobj headers +typedef off_t loff_t; +#include +#include + +namespace noobaa +{ + +DBG_INIT(0); + +typedef enum cuObjOpType_enum +{ + CUOBJ_GET = 0, /**< GET operation */ + CUOBJ_PUT = 1, /**< PUT operation */ + CUOBJ_INVALID = 9999 +} cuObjOpType_t; + +typedef Napi::External ExternalIovec; + +/** + * CuObjClientNapi is a napi object wrapper for cuObjClient. + */ +struct CuObjClientNapi : public Napi::ObjectWrap +{ + static Napi::FunctionReference constructor; + std::shared_ptr _client; + Napi::ThreadSafeFunction _thread_callback; + + static Napi::Function Init(Napi::Env env); + CuObjClientNapi(const Napi::CallbackInfo& info); + ~CuObjClientNapi(); + Napi::Value close(const Napi::CallbackInfo& info); + Napi::Value rdma(const Napi::CallbackInfo& info); +}; + +/** + * CuObjClientWorker is a napi worker for CuObjClientNapi::rdma() + */ +struct CuObjClientWorker : public ObjectWrapWorker +{ + cuObjOpType_t _op_type; + void* _ptr; + size_t _size; + std::string _rdma_desc; + std::string _rdma_addr; + size_t _rdma_size; + loff_t _rdma_offset; + ssize_t _ret_size; + std::mutex _mutex; + std::condition_variable _cond; + Napi::FunctionReference _func; + + CuObjClientWorker(const Napi::CallbackInfo& info); + virtual void Execute() override; + virtual void OnOK() override; + + ssize_t start_op( + cuObjOpType_t op_type, + const void* handle, + const void* ptr, + size_t size, + loff_t offset, + const cufileRDMAInfo_t* rdma_info); + void send_op(Napi::Env env); +}; + +Napi::FunctionReference CuObjClientNapi::constructor; + +/** + * @brief Defines the JavaScript class "CuObjClientNapi" and returns its constructor. + * + * Creates and persists the N-API constructor for the native CuObjClientNapi class, + * including its instance methods `close` and `rdma`, and returns the constructor function. + * + * @param env The N-API environment used to create the class. + * @return Napi::Function The JavaScript constructor function for `CuObjClientNapi`. + */ +Napi::Function +CuObjClientNapi::Init(Napi::Env env) +{ + constructor = Napi::Persistent(DefineClass(env, + "CuObjClientNapi", + { + InstanceMethod<&CuObjClientNapi::close>("close"), + InstanceMethod<&CuObjClientNapi::rdma>("rdma"), + })); + constructor.SuppressDestruct(); + return constructor.Value(); +} + +/** + * @brief Start a CUOBJ GET operation for the context identified by `handle`. + * + * Initiates a GET operation that reads up to `size` bytes into `ptr` from the + * remote object at `offset`, using the optional RDMA parameters in `rdma_info`. + * + * @param handle Opaque context handle identifying the operation context. + * @param ptr Destination buffer for received data. + * @param size Maximum number of bytes to read into `ptr`. + * @param offset Byte offset within the remote object to begin the read. + * @param rdma_info Optional RDMA descriptor and address/size/offset; may be null. + * @return ssize_t Number of bytes read on success, or a negative error code on failure. + */ +static ssize_t +get_op_fn(const void* handle, char* ptr, size_t size, loff_t offset, const cufileRDMAInfo_t* rdma_info) +{ + CuObjClientWorker* w = reinterpret_cast(cuObjClient::getCtx(handle)); + return w->start_op(CUOBJ_GET, handle, ptr, size, offset, rdma_info); +} + +/** + * @brief Initiates a PUT RDMA operation for the client context identified by `handle`. + * + * @param handle Opaque client context handle identifying the target CuObjClientWorker. + * @param ptr Pointer to the source buffer containing data to PUT. + * @param size Number of bytes available in `ptr` to transfer. + * @param offset Offset within the remote object where data should be written. + * @param rdma_info Optional RDMA metadata (description, address, size, offset) used for the transfer; may be null. + * @return ssize_t Number of bytes transferred on success, or a negative error code on failure. + */ +static ssize_t +put_op_fn(const void* handle, const char* ptr, size_t size, loff_t offset, const cufileRDMAInfo_t* rdma_info) +{ + CuObjClientWorker* w = reinterpret_cast(cuObjClient::getCtx(handle)); + return w->start_op(CUOBJ_PUT, handle, ptr, size, offset, rdma_info); +} + +/** + * @brief Constructs a CuObjClientNapi wrapper and initializes the underlying cuObjClient. + * + * Loads cuObjClient configuration (for example, cufile.json located via CUFILE_ENV_PATH_JSON), + * enables telemetry/logging, creates a cuObjClient configured for RDMA, and initializes a + * ThreadSafeFunction used to dispatch callbacks to the main thread. + * + * @note The underlying cuObjClient performs synchronous operations and relies on a main-thread + * callback to send the HTTP/RDMA request. For concurrent operations prefer creating a separate + * client per concurrent request to avoid contention. + * + * @throws Napi::Error If the underlying cuObjClient fails to connect (for example due to invalid + * RDMA configuration in the cufile.json). + */ +CuObjClientNapi::CuObjClientNapi(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(info) +{ + DBG0("CuObjClientNapi::ctor"); + + uint32_t log_flags = + // CUOBJ_LOG_PATH_DEBUG | + // CUOBJ_LOG_PATH_INFO | + CUOBJ_LOG_PATH_ERROR; + + cuObjClient::setupTelemetry(true, &std::cout); + cuObjClient::setTelemFlags(log_flags); + + CUObjOps_t ops = { + .get = &get_op_fn, + .put = &put_op_fn, + }; + std::shared_ptr client(new cuObjClient(ops, CUOBJ_PROTO_RDMA_DC_V1)); + + if (!client->isConnected()) { + throw Napi::Error::New(info.Env(), + XSTR() << "CuObjClientNapi::ctor connect failed (check rdma_dev_addr_list in cufile.json)"); + } + + // initialize a thread safe callback to the main thread + // actual callback will be set in the worker + auto noop = Napi::Function::New( + info.Env(), [](const Napi::CallbackInfo& info) {}); + _thread_callback = Napi::ThreadSafeFunction::New( + info.Env(), noop, "CuObjClientNapiThreadCallback", 0, 1, [](Napi::Env) {}); + + _client = client; +} + +/** + * @brief Destroy the CuObjClientNapi instance and release its resources. + * + * Resets the owned cuObjClient shared pointer to release the underlying client + * connection and associated resources. + */ +CuObjClientNapi::~CuObjClientNapi() +{ + DBG0("CuObjClientNapi::dtor"); + _client.reset(); +} + +/** + * @brief Releases the wrapped cuObjClient instance and frees associated resources. + * + * @return Napi::Value JavaScript `undefined`. + */ +Napi::Value +CuObjClientNapi::close(const Napi::CallbackInfo& info) +{ + DBG0("CuObjClientNapi::close"); + _client.reset(); + return info.Env().Undefined(); +} + +/** + * @brief Initiates an RDMA GET or PUT operation using a CuObjClientWorker. + * + * Starts a worker that performs the requested RDMA operation and invokes the + * provided JavaScript callback to complete the server-side part of the transfer. + * + * @param op_type Operation type: either "GET" to read into `buf` or "PUT" to write from `buf`. + * @param buf Buffer whose memory will be used for the RDMA transfer. + * @param func JavaScript function called on the main thread with RDMA info and a node-style callback + * to report completion: func(rdma_info, (err, size) => void). + * @return long The number of bytes transferred on success, or a negative error code on failure. + */ +Napi::Value +CuObjClientNapi::rdma(const Napi::CallbackInfo& info) +{ + return await_worker(info); +} + +/** + * @brief Construct a CuObjClientWorker from JavaScript arguments. + * + * Initializes the worker's operation type, buffer pointer/length, and persists + * the JavaScript callback based on the supplied CallbackInfo. + * + * @param info JavaScript call arguments: expects + * - index 0: a string with value "GET" or "PUT" to select the operation; + * - index 1: a Buffer containing the data for the operation; + * - index 2: a callback function to be invoked when the main-thread RDMA + * exchange completes. + * + * @throws Napi::Error if the operation type is not "GET" or "PUT". + */ +CuObjClientWorker::CuObjClientWorker(const Napi::CallbackInfo& info) + : ObjectWrapWorker(info) + , _op_type(CUOBJ_INVALID) + , _ptr(0) + , _size(0) + , _rdma_size(0) + , _rdma_offset(0) + , _ret_size(-1) +{ + auto op_type = info[0].As().Utf8Value(); + auto buf = info[1].As>(); + auto func = info[2].As(); + + if (op_type == "GET") { + _op_type = CUOBJ_GET; + } else if (op_type == "PUT") { + _op_type = CUOBJ_PUT; + } else { + throw Napi::Error::New(info.Env(), + XSTR() << "CuObjClientWorker: bad op type " << DVAL(op_type)); + } + + _ptr = buf.Data(); + _size = buf.Length(); + _func = Napi::Persistent(func); +} + +// will be set by cuda_napi when loaded +CUcontext cuobj_client_napi_cuda_ctx = 0; + +/** + * @brief Execute the worker's RDMA operation (GET or PUT) using the wrapped cuObjClient. + * + * Executes the operation indicated by the worker's `_op_type` against the buffer (`_ptr`, `_size`) + * through the wrapped `cuObjClient`. If a global CUDA context is configured, the context is set + * before performing the operation. On successful completion the transferred byte count is stored + * in `_ret_size`. On failure the worker records an error via `SetError`. An invalid `_op_type` + * triggers a fatal panic. + */ +void +CuObjClientWorker::Execute() +{ + DBG1("CuObjClientWorker: Execute " + << DVAL(_op_type) + << DVAL(_ptr) + << DVAL(_size)); + std::shared_ptr client(_wrap->_client); + + cuObjMemoryType_t mem_type = cuObjClient::getMemoryType(_ptr); + DBG1("CuObjClientWorker: buffer " << DVAL(_ptr) << DVAL(_size) << DVAL(mem_type)); + + // mem_type doesn't seem to identify the memory type correctly + // so we need to set the context manually instead of this condition + // mem_type == CUOBJ_MEMORY_CUDA_DEVICE || mem_type == CUOBJ_MEMORY_CUDA_MANAGED + + if (cuobj_client_napi_cuda_ctx) { + CUresult res = cuCtxSetCurrent(cuobj_client_napi_cuda_ctx); + if (res != CUDA_SUCCESS) { + SetError(XSTR() << "CuObjClientWorker: Failed to set current context " << DVAL(res)); + return; + } + } + + // register rdma buffer + // cuObjErr_t ret_get_mem = client->cuMemObjGetDescriptor(_ptr, _size); + // if (ret_get_mem != CU_OBJ_SUCCESS) { + // SetError(XSTR() << "CuObjClientWorker: Failed to register rdma buffer " << DVAL(ret_get_mem)); + // return; + // } + // StackCleaner cleaner([&] { + // // release rdma buffer + // cuObjErr_t ret_put_mem = client->cuMemObjPutDescriptor(_ptr); + // if (ret_put_mem != CU_OBJ_SUCCESS) { + // SetError(XSTR() << "CuObjClientWorker: Failed to release rdma buffer " << DVAL(ret_put_mem)); + // } + // }); + + if (_op_type == CUOBJ_GET) { + _ret_size = client->cuObjGet(this, _ptr, _size); + } else if (_op_type == CUOBJ_PUT) { + _ret_size = client->cuObjPut(this, _ptr, _size); + } else { + PANIC("bad op type " << DVAL(_op_type)); + } + + if (_ret_size < 0 || _ret_size != ssize_t(_size)) { + SetError(XSTR() << "CuObjClientWorker: failed " + << DVAL(_op_type) << DVAL(_ret_size)); + } +} + +/** + * @brief Deliver the operation result to JavaScript. + * + * Resolves the worker's pending promise with `_ret_size` converted to a JavaScript + * Number representing the number of bytes transferred by the RDMA operation. + */ +void +CuObjClientWorker::OnOK() +{ + _promise.Resolve(Napi::Number::New(Env(), _ret_size)); +} + +/** + * @brief Dispatches an RDMA GET or PUT operation to the main thread and waits for the server response. + * + * Sends operation metadata to the main thread via the thread-safe callback, blocks until the operation + * completes, and returns the result reported by the server. + * + * @param op_type Operation type (`CUOBJ_GET` or `CUOBJ_PUT`). + * @param handle Opaque handle associated with the operation context. + * @param ptr Pointer to the buffer involved in the operation. + * @param size Size of the buffer in bytes. + * @param obj_offset Offset within the remote object (object offset, not buffer offset). Must be zero in current usage. + * @param rdma_info RDMA descriptor containing connection/registration details used to build the request. + * @return ssize_t Number of bytes transferred on success, or a negative error code on failure. + */ +ssize_t +CuObjClientWorker::start_op( + cuObjOpType_t op_type, + const void* handle, + const void* ptr, + size_t size, + loff_t obj_offset, + const cufileRDMAInfo_t* rdma_info) +{ + std::string rdma_desc(rdma_info->desc_str, rdma_info->desc_len - 1); + DBG1("CuObjClientWorker::start_op " << DVAL(op_type) << DVAL(ptr) << DVAL(size) << DVAL(rdma_desc)); + + // this lock and condition variable are used to synchronize the worker thread + // with the main thread, as the main threas is sending the http request to the server. + std::unique_lock lock(_mutex); + + // check that the parameters are as expected + ASSERT(op_type == _op_type, DVAL(op_type) << DVAL(_op_type)); + ASSERT(ptr == _ptr, DVAL(ptr) << DVAL(_ptr)); + ASSERT(size == _size, DVAL(size) << DVAL(_size)); + ASSERT(obj_offset == 0, DVAL(obj_offset)); + + // save info for the server request + _rdma_desc = rdma_desc; + _rdma_addr = XSTR() << std::hex << uintptr_t(ptr); + _rdma_size = size; + // obj_offset refers to the object offset, not the buffer offset + _rdma_offset = 0; + + // send the op on the main thread by calling a Napi::ThreadSafeFunction. + // this model is cumbwersome and would be replaced by an async worker in the future. + // but for now the library requires us to make the http request sychronously from the worker thread, + // so we need to send the op on the main thread and then wait for the worker to be woken up. + _wrap->_thread_callback.Acquire(); + _wrap->_thread_callback.BlockingCall( + [this](Napi::Env env, Napi::Function noop) { + send_op(env); + }); + _wrap->_thread_callback.Release(); + + // after sending the op on main thread, the worker now waits for wakeup + _cond.wait(lock); + lock.unlock(); + + // _ret_size was set by the server response in the callback + DBG1("CuObjClientWorker::start_op done " << DVAL(_ret_size)); + return _ret_size; +} + +/** + * @brief Dispatches RDMA operation details to the main thread and registers a response callback. + * + * Constructs a JavaScript object containing `desc`, `addr`, `size`, and `offset`, then invokes the user-provided + * JavaScript function with that object and a node-style callback. When the callback is invoked by the JavaScript + * caller, it stores the server result in `_ret_size` (`-1` on error) while holding the worker mutex and notifies + * the worker's condition variable so the worker can resume. + * + * @param env N-API environment used to create JavaScript values and functions. + */ +void +CuObjClientWorker::send_op(Napi::Env env) +{ + DBG1("CuObjClientWorker::send_op"); + Napi::HandleScope scope(env); + + auto rdma_info = Napi::Object::New(env); + rdma_info["desc"] = Napi::String::New(env, _rdma_desc); + rdma_info["addr"] = Napi::String::New(env, _rdma_addr); + rdma_info["size"] = Napi::Number::New(env, _rdma_size); + rdma_info["offset"] = Napi::Number::New(env, _rdma_offset); + + // prepare a node-style callback function(err, result) + auto callback = Napi::Function::New(env, [this](const Napi::CallbackInfo& info) { + // this lock can be problematic because it is on the main thread + // but it works well if we a separate clients per each concurrent request + // and then locking is immediate because at this point the worker is already waiting + // on the condition and the mutex is free. + std::unique_lock lock(_mutex); + + // setting _ret_size according to the server response + // and waking up the worker to continue + if (info[0].ToBoolean() || !info[1].IsNumber()) { + _ret_size = -1; + } else { + _ret_size = info[1].As().Int64Value(); + } + + _cond.notify_one(); + lock.unlock(); + }); + + // call the user provided function with the rdma_info and the callback + // notice that we do not await here so the function must call the callback + _func.Call({ rdma_info, callback }); +} + +/** + * @brief Registers the CuObjClientNapi class on the provided N-API module exports. + * + * Attaches the CuObjClientNapi constructor to the given exports object so the class + * becomes available to JavaScript code that imports the native addon. + * + * @param env The N-API environment for the current module initialization. + * @param exports The module exports object where `CuObjClientNapi` will be assigned. + */ +void +cuobj_client_napi(Napi::Env env, Napi::Object exports) +{ + exports["CuObjClientNapi"] = CuObjClientNapi::Init(env); + DBG0("CUOBJ: client library loaded."); +} + +} // namespace noobaa \ No newline at end of file diff --git a/src/native/cuobj/cuobj_server_napi.cpp b/src/native/cuobj/cuobj_server_napi.cpp new file mode 100644 index 0000000000..d23c221b19 --- /dev/null +++ b/src/native/cuobj/cuobj_server_napi.cpp @@ -0,0 +1,709 @@ +/* Copyright (C) 2016 NooBaa */ +#include "../util/common.h" +#include "../util/napi.h" +#include "../util/worker.h" +#include +#include + +// cuobj headers +typedef off_t loff_t; +struct rdma_buffer; // opaque handle returned by cuObjServer registerBuffer +#include +#include + +namespace noobaa +{ + +DBG_INIT(0); + +typedef struct rdma_buffer RdmaBuf; + +// helper struct for async RDMA event handling +// used in CuObjServerNapi::_handle_async_events() +struct AsyncEvent +{ + std::shared_ptr deferred; + ssize_t size; + uint16_t channel_id; +}; + +/** + * CuObjServerNapi is a node-api object wrapper for cuObjServer. + */ +struct CuObjServerNapi : public Napi::ObjectWrap +{ + static Napi::FunctionReference constructor; + std::shared_ptr _server; + Napi::Reference _buffer_symbol; + std::set _async_channels; + uv_prepare_t _uv_async_handler; + bool _use_async_events = false; + + static Napi::Function Init(Napi::Env env); + CuObjServerNapi(const Napi::CallbackInfo& info); + ~CuObjServerNapi(); + Napi::Value close(const Napi::CallbackInfo& info); + Napi::Value register_buffer(const Napi::CallbackInfo& info); + Napi::Value deregister_buffer(const Napi::CallbackInfo& info); + Napi::Value is_registered_buffer(const Napi::CallbackInfo& info); + Napi::Value rdma(const Napi::CallbackInfo& info); + Napi::Value rdma_async_event(const Napi::CallbackInfo& info); + void _start_async_events(); + void _stop_async_events(); + void _handle_async_events(); +}; + +/** + * CuObjServerWorker is a node-api worker for CuObjServerNapi::rdma() + */ +struct CuObjServerWorker : public ObjectWrapWorker +{ + std::shared_ptr _server; + cuObjOpType_t _op_type; + std::string _op_key; + void* _ptr; + size_t _size; + RdmaBuf* _rdma_buf; + std::string _rdma_desc; + uint64_t _rdma_addr; + size_t _rdma_size; + loff_t _rdma_offset; + ssize_t _ret_size; + thread_local static uint16_t _thread_channel_id; + + CuObjServerWorker(const Napi::CallbackInfo& info); + virtual void Execute() override; + virtual void OnOK() override; +}; + +Napi::FunctionReference CuObjServerNapi::constructor; +thread_local uint16_t CuObjServerWorker::_thread_channel_id = INVALID_CHANNEL_ID; +typedef Napi::External ExternalRdmaBuf; + +/** + * @brief Create the N-API constructor for CuObjServerNapi and register its instance methods. + * + * @param env The N-API environment used to define the class. + * @return Napi::Function The constructor function object for the CuObjServerNapi class. + */ +Napi::Function +CuObjServerNapi::Init(Napi::Env env) +{ + constructor = Napi::Persistent(DefineClass(env, + "CuObjServerNapi", + { + InstanceMethod<&CuObjServerNapi::close>("close"), + InstanceMethod<&CuObjServerNapi::register_buffer>("register_buffer"), + InstanceMethod<&CuObjServerNapi::deregister_buffer>("deregister_buffer"), + InstanceMethod<&CuObjServerNapi::is_registered_buffer>("is_registered_buffer"), + InstanceMethod<&CuObjServerNapi::rdma>("rdma"), + })); + constructor.SuppressDestruct(); + return constructor.Value(); +} + +/** + * @brief Constructs a CuObjServerNapi wrapper and initializes the underlying cuObjServer. + * + * Initializes telemetry and logging, applies optional RDMA tunables, creates and connects + * a cuObjServer instance, and prepares the internal libuv handle for optional async events. + * + * @param params JS object with initialization options: + * - ip: Server IP address to connect to. + * - port: Server port to connect to. + * - log_level: Optional log level; one of "ERROR", "INFO", or "DEBUG". + * - use_telemetry: Optional flag to enable telemetry output. + * - num_dcis: Optional RDMA tunable for number of DCIs. + * - cq_depth: Optional RDMA completion queue depth. + * - dc_key: Optional RDMA DC key. + * - ibv_poll_max_comp_event: Optional maximum completion events to poll. + * - service_level: Optional RDMA service level. + * - min_rnr_timer: Optional minimum RNR timer. + * - hop_limit: Optional RDMA hop limit. + * - pkey_index: Optional partition key index. + * - max_wr: Optional maximum work requests. + * - max_sge: Optional maximum scatter/gather entries. + * - delay_mode: Optional delay mode for RDMA operations. + * - delay_interval: Optional delay interval value. + * + * @throws Napi::Error If an unrecognized `log_level` is provided or if connecting to the server fails. + */ +CuObjServerNapi::CuObjServerNapi(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(info) +{ + auto env = info.Env(); + const Napi::Object params = info[0].As(); + std::string ip = napi_get_str(params, "ip"); + unsigned short port = napi_get_u32(params, "port"); + + bool use_telemetry = params["use_telemetry"].ToBoolean(); + uint32_t log_flags = 0; + if (params["log_level"].IsString()) { + std::string log_level = napi_get_str(params, "log_level"); + if (log_level == "DEBUG") { + log_flags |= CUOBJ_LOG_PATH_ERROR; + log_flags |= CUOBJ_LOG_PATH_INFO; + log_flags |= CUOBJ_LOG_PATH_DEBUG; + } else if (log_level == "INFO") { + log_flags |= CUOBJ_LOG_PATH_ERROR; + log_flags |= CUOBJ_LOG_PATH_INFO; + } else if (log_level == "ERROR") { + log_flags |= CUOBJ_LOG_PATH_ERROR; + } else { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi::ctor bad " << DVAL(log_level)); + } + } + + cuObjRDMATunable rdma_params; + if (params["num_dcis"].IsNumber()) { + rdma_params.setNumDcis(napi_get_i32(params, "num_dcis")); + } + if (params["cq_depth"].IsNumber()) { + rdma_params.setCqDepth(napi_get_u32(params, "cq_depth")); + } + if (params["dc_key"].IsNumber()) { + rdma_params.setDcKey(napi_get_i64(params, "dc_key")); + } + if (params["ibv_poll_max_comp_event"].IsNumber()) { + rdma_params.setIbvPollMaxCompEv(napi_get_i32(params, "ibv_poll_max_comp_event")); + } + if (params["service_level"].IsNumber()) { + rdma_params.setServiceLevel(napi_get_i32(params, "service_level")); + } + if (params["min_rnr_timer"].IsNumber()) { + rdma_params.setMinRnrTimer(napi_get_i32(params, "min_rnr_timer")); + } + if (params["hop_limit"].IsNumber()) { + rdma_params.setHopLimit(napi_get_u32(params, "hop_limit")); + } + if (params["pkey_index"].IsNumber()) { + rdma_params.setPkeyIndex(napi_get_i32(params, "pkey_index")); + } + if (params["max_wr"].IsNumber()) { + rdma_params.setMaxWr(napi_get_i32(params, "max_wr")); + } + if (params["max_sge"].IsNumber()) { + rdma_params.setMaxSge(napi_get_i32(params, "max_sge")); + } + if (params["delay_mode"].IsNumber()) { + rdma_params.setDelayMode(cuObjDelayMode_t(napi_get_i32(params, "delay_mode"))); + } else { + // rdma_params.setDelayMode(CUOBJ_DELAY_NONE); + } + if (params["delay_interval"].IsNumber()) { + rdma_params.setDelayInterval(napi_get_u32(params, "delay_interval")); + } else { + // rdma_params.setDelayInterval(0); + } + + DBG0("CuObjServerNapi::ctor " + << DVAL(ip) << DVAL(port) << DVAL(log_flags) + << "num_dcis=" << rdma_params.getNumDcis() << " " + << "cq_depth=" << rdma_params.getCqDepth() << " " + << "dc_key=" << rdma_params.getDcKey() << " " + << "ibv_poll_max_comp_event=" << rdma_params.getIbvPollMaxCompEv() << " " + << "service_level=" << rdma_params.getServiceLevel() << " " + << "min_rnr_timer=" << rdma_params.getMinRnrTimer() << " " + << "hop_limit=" << rdma_params.getHopLimit() << " " + << "pkey_index=" << rdma_params.getPkeyIndex() << " " + << "max_wr=" << rdma_params.getMaxWr() << " " + << "max_sge=" << rdma_params.getMaxSge() << " " + << "delay_mode=" << rdma_params.getDelayMode() << " " + << "delay_interval=" << rdma_params.getDelayInterval() << " "); + + cuObjServer::setupTelemetry(use_telemetry, &std::cout); + cuObjServer::setTelemFlags(log_flags); + + std::shared_ptr server(new cuObjServer( + ip.c_str(), port, CUOBJ_PROTO_RDMA_DC_V1, rdma_params)); + + if (!server->isConnected()) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi::ctor connect failed " << DVAL(ip) << DVAL(port)); + } + + _server = server; + _buffer_symbol = Napi::Persistent(Napi::Symbol::New(env, "CuObjServerNapiBufferSymbol")); + + // NOTE: initial tests of async events mode showed that it is slower than using worker threads, so keep it disabled by default + _use_async_events = params["use_async_events"].ToBoolean(); + _uv_async_handler.data = this; + uv_prepare_init(uv_default_loop(), &_uv_async_handler); +} + +/** + * @brief Stops async event polling and releases the underlying cuObjServer. + * + * Stops the libuv prepare handle used for async RDMA event polling and resets + * the stored server shared pointer to free the cuObjServer instance. + */ +CuObjServerNapi::~CuObjServerNapi() +{ + DBG0("CuObjServerNapi::dtor"); + uv_prepare_stop(&_uv_async_handler); + _server.reset(); +} + +/** + * @brief Stop async event polling and close the wrapped CuObjServer instance. + * + * Stops the libuv prepare handler used for async RDMA events (if running), + * releases the internal cuObjServer instance, and returns to JavaScript. + * + * @return Napi::Value JavaScript `undefined`. + */ +Napi::Value +CuObjServerNapi::close(const Napi::CallbackInfo& info) +{ + DBG0("CuObjServerNapi::close"); + uv_prepare_stop(&_uv_async_handler); + _server.reset(); + return info.Env().Undefined(); +} + +/** + * @brief Register a JavaScript Buffer for RDMA and attach an external RDMA handle to it. + * + * Attaches an External to the provided Buffer (info[0]) using the internal buffer symbol. + * If the Buffer already has an external RDMA handle attached, the call is a no-op. On failure to + * allocate or register the RDMA handle, the function throws a Napi::Error. + * + * @param buf The JavaScript Buffer to register (passed as info[0]); its memory pointer and length are used. + * @throws Napi::Error If the underlying RDMA buffer registration fails. + * + * Note: The attached RDMA handle must be explicitly released via deregister_buffer to avoid leaking + * the RDMA registration handle. + */ +Napi::Value +CuObjServerNapi::register_buffer(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + auto buf = info[0].As>(); + void* ptr = buf.Data(); + size_t size = buf.Length(); + auto sym = _buffer_symbol.Value(); + + // check if already registered and return so callers can easily lazy register any buffer + if (buf.Get(sym).IsExternal()) { + return env.Undefined(); + } + + RdmaBuf* rdma_buf = _server->registerBuffer(ptr, size); + if (!rdma_buf) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi::register_buffer Failed to register rdma buffer " + << DVAL(ptr) << DVAL(size)); + } + + // TODO add a finalizer to de-register on GC of the external, currently we need to manuall call de-register or we leak the RDMA handle + buf.Set(sym, ExternalRdmaBuf::New(env, rdma_buf)); + return env.Undefined(); +} + +/** + * @brief Deregisters an RDMA handle previously attached to a Node.js buffer. + * + * Removes the external RDMA buffer handle stored on the provided Buffer and + * unregisters it from the underlying cuObjServer. The Buffer must have been + * registered earlier via register_buffer(). + * + * @param buf Node.js Buffer that holds the previously registered RDMA handle. + * + * @throws Napi::Error if the provided buffer does not have a registered RDMA handle. + */ +Napi::Value +CuObjServerNapi::deregister_buffer(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + auto buf = info[0].As>(); + void* ptr = buf.Data(); + size_t size = buf.Length(); + auto sym = _buffer_symbol.Value(); + + if (!buf.Get(sym).IsExternal()) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi::deregister_buffer No registered rdma buffer " + << DVAL(ptr) << DVAL(size)); + } + + auto rdma_buf = buf.Get(sym).As().Data(); + _server->deRegisterBuffer(rdma_buf); + + buf.Delete(sym); + return env.Undefined(); +} + +/** + * @brief Checks whether the given Node.js Buffer has an external RDMA buffer handle attached. + * + * @param buf Node.js Buffer whose registration status will be checked. + * @return bool `true` if the buffer has an external RDMA handle attached, `false` otherwise. + */ +Napi::Value +CuObjServerNapi::is_registered_buffer(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + auto buf = info[0].As>(); + auto sym = _buffer_symbol.Value(); + bool is_registered = buf.Get(sym).IsExternal(); + return Napi::Boolean::New(env, is_registered); +} + +/** + * @brief Dispatches a RDMA GET or PUT operation using the configured execution path. + * + * The method reads operation parameters from the provided callback info and either + * invokes the async-event path or schedules a worker thread depending on the + * instance configuration. + * + * @returns Napi::Value The JavaScript result of the RDMA operation (for example, the transferred byte count or an object describing completion status). + */ +Napi::Value +CuObjServerNapi::rdma(const Napi::CallbackInfo& info) +{ + if (_use_async_events) { + return rdma_async_event(info); + } else { + return await_worker(info); + } +} + +/** + * @brief Constructs a worker that performs a CUOBJ GET or PUT RDMA operation. + * + * Parses and validates JavaScript arguments (operation type, key, local buffer, + * and RDMA descriptor), initializes internal fields (buffer pointer/size, + * RDMA descriptor, address, size, offset, and registered RdmaBuf), and prepares + * the worker for Execute(). + * + * Expected arguments in `info`: + * - [0] op_type: string "GET" or "PUT". + * - [1] op_key: string key for the object operation. + * - [2] Buffer: local buffer previously registered with the server. + * - [3] rdma_info: object with properties: + * - desc: string RDMA descriptor (must match RDMA_DESC_STR length). + * - addr: string RDMA address (hex). + * - size: number RDMA buffer size (positive). + * - offset: number RDMA offset (>= 0). + * + * @param info N-API callback info containing the arguments described above. + * + * @throws Napi::Error if: + * - op_type is not "GET" or "PUT"; + * - RDMA descriptor length is incorrect; + * - RDMA address is zero or cannot be parsed; + * - RDMA size is not greater than zero; + * - RDMA offset is negative; + * - the provided buffer does not have a registered external RdmaBuf. + */ +CuObjServerWorker::CuObjServerWorker(const Napi::CallbackInfo& info) + : ObjectWrapWorker(info) + , _server(_wrap->_server) + , _op_type(CUOBJ_INVALID) + , _ptr(0) + , _size(0) + , _rdma_buf(0) + , _rdma_addr(0) + , _rdma_size(0) + , _rdma_offset(0) + , _ret_size(-1) +{ + auto env = info.Env(); + auto op_type = info[0].As().Utf8Value(); + _op_key = info[1].As().Utf8Value(); + auto buf = info[2].As>(); + auto rdma_info = info[3].As(); + + _rdma_desc = rdma_info.Get("desc").As().Utf8Value(); + auto rdma_addr = rdma_info.Get("addr").As().Utf8Value(); + auto rdma_size = rdma_info.Get("size").As().Int64Value(); + _rdma_offset = rdma_info.Get("offset").As().Int64Value(); + + if (op_type == "GET") { + _op_type = CUOBJ_GET; + } else if (op_type == "PUT") { + _op_type = CUOBJ_PUT; + } else { + throw Napi::Error::New(env, + XSTR() << "CuObjServerWorker: bad op type " << DVAL(op_type)); + } + + _ptr = buf.Data(); + _size = buf.Length(); + _rdma_addr = strtoull(rdma_addr.c_str(), 0, 16); + _rdma_size = size_t(rdma_size); + auto sym = _wrap->_buffer_symbol.Value(); + + if (_rdma_desc.size() + 1 != sizeof RDMA_DESC_STR) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerWorker: bad rdma desc " << DVAL(_rdma_desc)); + } + if (_rdma_addr == 0) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerWorker: bad rdma addr " << DVAL(rdma_addr) << DVAL(_rdma_addr)); + } + if (rdma_size <= 0) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerWorker: bad rdma size " << DVAL(rdma_size)); + } + if (_rdma_offset < 0) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerWorker: bad rdma offset " << DVAL(_rdma_offset)); + } + if (!buf.Get(sym).IsExternal()) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerWorker: No registered rdma buffer " << DVAL(_ptr) << DVAL(_size)); + } + + _rdma_buf = buf.Get(sym).As().Data(); +} + +/** + * @brief Executes the configured RDMA GET or PUT operation and records the outcome. + * + * Computes the transfer size as the minimum of the local buffer size and the RDMA region size, + * lazily allocates a thread-local channel id if one is not already assigned, and dispatches + * the operation to the wrapped server's GET or PUT handler. The number of bytes transferred + * or a negative error code is stored in `_ret_size`. On channel allocation failure or when the + * server reports an error, the worker's error state is set via `SetError`. + */ +void +CuObjServerWorker::Execute() +{ + DBG1("CuObjServerWorker: Execute " + << DVAL(_op_type) + << DVAL(_op_key) + << DVAL(_ptr) + << DVAL(_size) + << DVAL(_rdma_buf) + << DVAL(_rdma_desc) + << DVAL(_rdma_addr) + << DVAL(_rdma_size) + << DVAL(_rdma_offset)); + + size_t real_size = std::min(_size, _rdma_size); + + // lazy allocate channel id and keep it in thread local storage + // we currently do not free those channel ids + if (_thread_channel_id == INVALID_CHANNEL_ID) { + _thread_channel_id = _server->allocateChannelId(); + if (_thread_channel_id == INVALID_CHANNEL_ID) { + SetError(XSTR() << "CuObjServerWorker: Failed to allocate channel id"); + return; + } + } + + if (_op_type == CUOBJ_GET) { + _ret_size = _server->handleGetObject( + _op_key, _rdma_buf, _rdma_addr, real_size, _rdma_desc, _thread_channel_id); + } else if (_op_type == CUOBJ_PUT) { + _ret_size = _server->handlePutObject( + _op_key, _rdma_buf, _rdma_addr, real_size, _rdma_desc, _thread_channel_id); + } else { + PANIC("bad op type " << DVAL(_op_type)); + } + + if (_ret_size < 0) { + SetError(XSTR() << "CuObjServerWorker: op failed " + << DVAL(_op_type) << DVAL(_ret_size)); + } +} + +/** + * @brief Resolve the worker's JavaScript promise with the operation result size. + * + * Converts the worker's internal return size into a JavaScript Number and resolves + * the associated Promise with that value. + */ +void +CuObjServerWorker::OnOK() +{ + _promise.Resolve(Napi::Number::New(Env(), _ret_size)); +} + +/** + * @brief Initiates an RDMA GET or PUT operation using an allocated async channel. + * + * Expects JS arguments: opType ("GET" or "PUT"), key (object key), buffer (registered RDMA Buffer), + * and rdmaInfo object with fields `{ desc, addr, size, offset }`. Validates descriptor, address, + * size, offset, and that the buffer has a registered external RDMA handle, allocates a channel, + * queues the RDMA operation with the server, and starts libuv-based polling for completion. + * + * @param info N-API callback info containing the four arguments described above. + * @return Napi::Value A JavaScript Promise that resolves to the number of bytes transferred on + * successful completion or rejects with an error if the operation fails or validation fails. + */ +Napi::Value +CuObjServerNapi::rdma_async_event(const Napi::CallbackInfo& info) +{ + auto env = info.Env(); + auto op_type = info[0].As().Utf8Value(); + auto op_key = info[1].As().Utf8Value(); + auto buf = info[2].As>(); + auto sym = _buffer_symbol.Value(); + auto rdma_info = info[3].As(); + auto rdma_desc = rdma_info.Get("desc").As().Utf8Value(); + auto rdma_addr = rdma_info.Get("addr").As().Utf8Value(); + auto rdma_size = rdma_info.Get("size").As().Int64Value(); + auto rdma_offset = rdma_info.Get("offset").As().Int64Value(); + + void* ptr = buf.Data(); + size_t size = buf.Length(); + size_t real_size = std::min(size, size_t(rdma_size)); + uint64_t remote_addr = strtoull(rdma_addr.c_str(), 0, 16); + + if (rdma_desc.size() + 1 != sizeof RDMA_DESC_STR) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi: bad rdma desc " << DVAL(rdma_desc)); + } + if (remote_addr == 0) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi: bad rdma addr " << DVAL(remote_addr) << DVAL(rdma_addr)); + } + if (rdma_size <= 0) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi: bad rdma size " << DVAL(rdma_size)); + } + if (rdma_offset < 0) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi: bad rdma offset " << DVAL(rdma_offset)); + } + if (!buf.Get(sym).IsExternal()) { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi: No registered rdma buffer " << DVAL(ptr) << DVAL(size)); + } + auto rdma_buf = buf.Get(sym).As().Data(); + + uint16_t channel_id = _server->allocateChannelId(); + auto deferred = std::make_shared(env); + auto async_event = std::make_unique(deferred, real_size, channel_id); + + // LOG("CuObjServerNapi: queue async event " << DVAL(deferred.get()) << DVAL(_num_pending) << DVAL(size)); + int r = 0; + ibv_wc_status status = IBV_WC_SUCCESS; + if (op_type == "GET") { + r = _server->handleGetObject( + op_key, rdma_buf, remote_addr, real_size, rdma_desc, channel_id, 0, &status, async_event.get()); + } else if (op_type == "PUT") { + r = _server->handlePutObject( + op_key, rdma_buf, remote_addr, real_size, rdma_desc, channel_id, 0, &status, async_event.get()); + } else { + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi: bad op type " << DVAL(op_type)); + } + if (r < 0) { + _server->freeChannelId(channel_id); + throw Napi::Error::New(env, + XSTR() << "CuObjServerNapi: handle call error " << DVAL(r)); + } else { + async_event.release(); + _start_async_events(); + _async_channels.insert(channel_id); + return deferred->Promise(); + } +} + +/** + * @brief Libuv prepare callback that forwards event handling to the associated CuObjServerNapi instance. + * + * The provided `uv_prepare_t` handle is expected to have its `data` field set to a pointer + * to a `CuObjServerNapi` instance; this function casts that pointer and invokes the + * instance method that processes pending async RDMA events. + * + * @param handle Pointer to the libuv prepare handle whose `data` is a `CuObjServerNapi*`. + */ +static void +_uv_handle_async_events(uv_prepare_t* handle) +{ + static_cast(handle->data)->_handle_async_events(); +} + +/** + * @brief Start libuv preparation handler for polling RDMA async events. + * + * Starts the uv_prepare handle used to poll and process pending asynchronous + * RDMA events when there are registered async channels to monitor. + */ +void +CuObjServerNapi::_start_async_events() +{ + if (_async_channels.empty()) { + uv_prepare_start(&_uv_async_handler, _uv_handle_async_events); + } +} + +/** + * @brief Stop the libuv prepare handler when there are no pending async channels. + * + * If the internal set of pending asynchronous channel IDs is empty, stops the + * associated uv_prepare_t handler to cease polling for async RDMA events. + */ +void +CuObjServerNapi::_stop_async_events() +{ + if (_async_channels.empty()) { + uv_prepare_stop(&_uv_async_handler); + } +} + +// handler called from the uv event loop to poll for rdma async events +// it checks all the channels that have pending async events +/** + * @brief Polls pending RDMA async channels and completes their associated promises. + * + * For each channel tracked in the internal async set, polls the server for a completion event; + * when an event is available, resolves the associated JavaScript promise with the completed + * transfer size or rejects it with an error, frees the channel id, and removes the channel from + * the pending set. After processing, stops the libuv prepare handler if there are no remaining + * async channels. + */ +void +CuObjServerNapi::_handle_async_events() +{ + // LOG("CuObjServerNapi::_handle_async_events " << DVAL(_async_channels.size())); + for (auto it = _async_channels.begin(); it != _async_channels.end();) { + uint16_t channel_id = *it; + cuObjAsyncEvent_t poll_event = { nullptr, IBV_WC_SUCCESS }; + int num_events = _server->poll(&poll_event, 1, channel_id); + if (num_events == 0) { + ++it; + continue; + } + assert(num_events == 1); + auto async_event = std::unique_ptr(static_cast(poll_event.async_handle)); + assert(async_event->channel_id == channel_id); + auto deferred = async_event->deferred; + auto env = deferred->Env(); + Napi::HandleScope scope(env); + // LOG("CuObjServerNapi::_handle_async_events complete " << DVAL(deferred.get()) << DVAL(event.status)); + if (poll_event.status != IBV_WC_SUCCESS) { + auto err = Napi::Error::New(env, + XSTR() << "CuObjServerNapi: op failed " << DVAL(poll_event.status)); + deferred->Reject(err.Value()); + } else { + deferred->Resolve(Napi::Number::New(env, async_event->size)); + } + _server->freeChannelId(channel_id); + it = _async_channels.erase(it); // erase and advance + } + _stop_async_events(); +} + +/** + * @brief Register the CuObjServerNapi JavaScript class on the given module exports. + * + * Attaches the N-API constructor returned by CuObjServerNapi::Init to the provided + * exports object under the name "CuObjServerNapi" and logs library load. + * + * @param env The N-API environment. + * @param exports The module exports object to which the class will be added. + */ +void +cuobj_server_napi(Napi::Env env, Napi::Object exports) +{ + exports["CuObjServerNapi"] = CuObjServerNapi::Init(env); + DBG0("CUOBJ: server library loaded."); +} + +} // namespace noobaa \ No newline at end of file diff --git a/src/native/fs/fs_napi.cpp b/src/native/fs/fs_napi.cpp index 4fd4f895c8..7d4b837f4b 100644 --- a/src/native/fs/fs_napi.cpp +++ b/src/native/fs/fs_napi.cpp @@ -4,23 +4,30 @@ #include "../util/common.h" #include "../util/napi.h" #include "../util/os.h" + +// Disable pedantic warning temporarily to include GPFS headers which have zero-length arrays +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#include "./gpfs.h" #include "./gpfs_fcntl.h" +#include "./gpfs_rdma_experimental.h" +#pragma GCC diagnostic pop #include #include #include #include -#include #include #include +#include #include #include +#include #include #include #include #include #include -#include #include #include #include @@ -38,10 +45,10 @@ #define ENOATTR ENODATA #endif -#define ROUNDUP(X, Y) ((Y) * (((X) + (Y)-1) / (Y))) +#define ROUNDUP(X, Y) ((Y) * (((X) + (Y) - 1) / (Y))) -// Should total to 256 (sizeof(buffer) 212 + sizeof(header) 16 + sizeof(payload) 28) -#define GPFS_XATTR_BUFFER_SIZE 212 +// Should total to 256 (sizeof(buffer) 216 + sizeof(header) 16 + sizeof(payload) 24) +#define GPFS_XATTR_BUFFER_SIZE 216 #define GPFS_BACKEND "GPFS" #define GPFS_XATTR_PREFIX "gpfs" #define GPFS_DOT_ENCRYPTION_EA "Encryption" @@ -151,10 +158,10 @@ #endif #ifdef __APPLE__ - typedef unsigned long long DirOffset; +typedef unsigned long long DirOffset; #define DIR_OFFSET_FIELD d_seekoff #else - typedef long DirOffset; +typedef long DirOffset; #define DIR_OFFSET_FIELD d_off #endif @@ -169,20 +176,17 @@ const char* gpfs_dl_path = std::getenv("GPFS_DL_PATH"); int gpfs_lib_file_exists = -1; -static int (*dlsym_gpfs_fcntl)(gpfs_file_t file, void* arg) = 0; - -static int (*dlsym_gpfs_linkat)( - gpfs_file_t fileDesc, const char* oldpath, gpfs_file_t newdirfd, const char* newpath, int flags) = 0; - -static int (*dlsym_gpfs_linkatif)( - gpfs_file_t fileDesc, const char* oldpath, gpfs_file_t newdirfd, const char* newpath, int flags, gpfs_file_t replace_fd) = 0; - -static int (*dlsym_gpfs_unlinkat)( - gpfs_file_t fileDesc, const char* path, gpfs_file_t fd) = 0; - -static int (*dlsym_gpfs_ganesha)( - int op, void *oarg) = 0; - +static decltype(&gpfs_fcntl) dlsym_gpfs_fcntl = 0; +static decltype(&gpfs_linkat) dlsym_gpfs_linkat = 0; +static decltype(&gpfs_linkatif) dlsym_gpfs_linkatif = 0; +static decltype(&gpfs_unlinkat) dlsym_gpfs_unlinkat = 0; +static decltype(&gpfs_rdma_pread) dlsym_gpfs_rdma_pread = 0; +static decltype(&gpfs_rdma_pwrite) dlsym_gpfs_rdma_pwrite = 0; +static decltype(&gpfs_rdma_shadow_buffer_size) dlsym_gpfs_rdma_shadow_buffer_size = 0; + +// gpfs_ganesha is defined in gpfs_nfs.h which we do not include directly +// but we use it to register noobaa specific options +static int (*dlsym_gpfs_ganesha)(int op, void* oarg) = 0; struct gpfs_ganesha_noobaa_arg { int noobaa_version; @@ -191,8 +195,45 @@ struct gpfs_ganesha_noobaa_arg }; #define OPENHANDLE_REGISTER_NOOBAA 157 +/** + * @brief Create a Napi::Error populated from a system errno and optional message. + * + * Constructs a JavaScript Error whose message includes the system error description + * and whose `code` property is set to the libuv/errno error name. + * + * @param env The N-API environment used to create the Error. + * @param msg Optional contextual message to prefix the system error description. + * If empty, the Error message will be the system error description alone. + * @param errno_val System error value to use; defaults to the current `errno`. + * @return Napi::Error Error object with `message` containing the description and + * `code` set to the libuv error name corresponding to `errno_val`. + */ +static Napi::Error +napi_sys_error(Napi::Env env, std::string msg = "", int errno_val = errno) +{ + const char* err_code = uv_err_name(uv_translate_sys_error(errno_val)); + const char* err_desc = uv_strerror(uv_translate_sys_error(errno_val)); + if (msg.empty()) { + msg = err_desc; + } else { + msg += ": "; + msg += err_desc; + } + auto err = Napi::Error::New(env, msg); + err.Set("code", Napi::String::New(env, err_code)); + return err; +} + static const int DIO_BUFFER_MEMALIGN = 4096; +/** + * @brief Releases memory used by an external N-API buffer. + * + * This function is intended to be used as the finalize/release callback for + * Napi::Buffer that wraps externally allocated memory. + * + * @param buf Pointer to the buffer memory to free; if null, no action is taken. + */ static void buffer_releaser(Napi::Env env, uint8_t* buf) { @@ -279,13 +320,28 @@ struct Entry DirOffset off; }; +// Disable pedantic warning temporarily to use GPFS struct which have zero-length arrays +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" struct gpfsRequest_t { gpfsFcntlHeader_t header; gpfsGetSetXAttr_t payload; char buffer[GPFS_XATTR_BUFFER_SIZE]; }; +#pragma GCC diagnostic pop +static_assert(sizeof(struct gpfsRequest_t) == 256, "gpfsRequest_t size mismatch"); + +/** + * @brief Populate a gpfsRequest_t with a GPFS "get extended attribute" request for the given attribute name. + * + * Fills the request header and payload fields (lengths, type, flags, and buffer) and copies the attribute name + * into the request payload buffer. The request is prepared for a GPFS fcntl GET_XATTR operation. + * + * @param reqP Pointer to the gpfsRequest_t structure to populate; must point to writable storage. + * @param key The extended attribute name to request; its bytes are copied into the request payload buffer. + */ static void build_gpfs_get_ea_request(gpfsRequest_t* reqP, std::string key) { @@ -471,10 +527,26 @@ get_fd_xattr(int fd, XattrMap& xattr, const std::vector& xattr_keys return 0; } +/** + * @brief Retrieve GPFS-specific extended attributes for an open file descriptor. + * + * Queries the GPFS fcntl interface for each attribute name in the GPFS xattr list + * (and additionally DMAPI xattrs when `use_dmapi` is true) and inserts any found + * attribute values into `xattr`. Updates `gpfs_error` with the GPFS fcntl error + * reason code returned by the last attribute request. + * + * @param fd File descriptor of the target file. + * @param xattr Map to populate with found attribute key -> value pairs. + * @param gpfs_error Receives the GPFS fcntl error reason code from the last request. + * @param use_dmapi When true, include DMAPI attribute names in the queries. + * @return int `0` on success; if the underlying dlsym_gpfs_fcntl call fails returns its error code + * (errno is set); if GPFS reports an error for an attribute (other than "no attribute") + * returns that GPFS error code. + */ static int get_fd_gpfs_xattr(int fd, XattrMap& xattr, int& gpfs_error, bool use_dmapi) { - auto gpfs_xattrs { GPFS_XATTRS }; + auto gpfs_xattrs{ GPFS_XATTRS }; if (use_dmapi) { gpfs_xattrs.insert(gpfs_xattrs.end(), GPFS_DMAPI_XATTRS.begin(), GPFS_DMAPI_XATTRS.end()); } @@ -536,6 +608,18 @@ clear_xattr(int fd, std::string _prefix) return 0; } +/** + * @brief Load xattr keys from a JavaScript options object into a C++ vector. + * + * If the `options` object contains a truthy `xattr_get_keys` property, replaces + * `_xattr_get_keys` with the string elements of that array. Otherwise, if + * `options` has a truthy `skip_user_xattr` property, sets `_xattr_get_keys` + * to the predefined `USER_XATTRS`. + * + * @param options JavaScript options object that may contain `xattr_get_keys` (Array) + * or `skip_user_xattr` (boolean). + * @param _xattr_get_keys Output vector that will be replaced with the selected keys. + */ static void load_xattr_get_keys(Napi::Object& options, std::vector& _xattr_get_keys) { @@ -554,13 +638,25 @@ load_xattr_get_keys(Napi::Object& options, std::vector& _xattr_get_ } /** -* converts Napi::Array of numbers to std::vector -* typename T - type of the vector to convert to (e.g int, uint, gid_t) -* warning: function will only work on vector with numeric types. should not be used with other types -*/ -template + * converts Napi::Array of numbers to std::vector + * typename T - type of the vector to convert to (e.g int, uint, gid_t) + * warning: function will only work on vector with numeric types. should not be used with other types + */ +template +/** + * @brief Convert a JavaScript numeric array to a C++ vector of numbers. + * + * Converts each element of the provided Napi::Array to a numeric value using + * N-API's ToNumber and returns a std::vector containing those values converted + * to type `T`. + * + * @tparam T Numeric type of the resulting vector elements (e.g., int, double). + * @param arr JavaScript array whose elements are expected to be numbers. + * @return std::vector Vector of values from `arr` converted to `T`. + */ static std::vector -convert_napi_number_array_to_number_vector(const Napi::Array& arr) { +convert_napi_number_array_to_number_vector(const Napi::Array& arr) +{ std::vector new_vector; const std::size_t arr_length = arr.Length(); for (std::size_t i = 0; i < arr_length; ++i) { @@ -572,20 +668,37 @@ convert_napi_number_array_to_number_vector(const Napi::Array& arr) { /** * converts std::vector to comma seperated string so it can be printed to logs */ -template +template +/** + * @brief Converts a vector of values into a comma-separated string. + * + * Produces a single string containing each element of the input vector in order, + * separated by commas with no surrounding spaces. + * + * @tparam T Type of the vector elements; must be stream-insertable (operator<<). + * @param vec Vector of elements to stringify. + * @return std::string Comma-separated representation of the vector elements (empty string for an empty vector). + */ static std::string -stringfy_vector(std::vector& vec) { +stringfy_vector(std::vector& vec) +{ std::stringstream ss; std::size_t size = vec.size(); - for(std::size_t i = 0; i < size; ++i) { + for (std::size_t i = 0; i < size; ++i) { if (i > 0) ss << ','; ss << vec[i]; } return ss.str(); } - -static std::string get_groups_as_string() { +/** + * @brief Formats the current process's supplemental group IDs into a comma-separated string. + * + * @return std::string Comma-separated list of group IDs (gid_t). Returns an empty string if the process has no supplemental groups. + */ +static std::string +get_groups_as_string() +{ std::vector groups = ThreadScope::get_process_groups(); return stringfy_vector(groups); } @@ -664,6 +777,18 @@ struct FSWorker : public Napi::AsyncWorker DBG1("FS::FSWorker::Begin: " << _desc); } virtual void Work() = 0; + /** + * @brief Prepare thread credentials, run the worker's task, and record execution time. + * + * Prepares a ThreadScope using the worker's UID, GID, and supplemental groups (and optionally adds thread capabilities), + * invokes the virtual Work() implementation, measures the elapsed wall-clock time in milliseconds and stores it in + * _took_time, and emits a warning log if the duration exceeds _warn_threshold_ms. + * + * Side effects: + * - Changes the thread's effective UID/GID and supplemental groups for the duration of the call. + * - May enable additional thread capabilities when _should_add_thread_capabilities is true. + * - Calls the virtual Work() method, which performs the actual operation and may modify member state and set errors. + */ void Execute() override { const std::string supplemental_groups = stringfy_vector(_supplemental_groups); @@ -673,7 +798,7 @@ struct FSWorker : public Napi::AsyncWorker std::string new_supplemental_groups = get_groups_as_string(); DBG1("FS::FSWorker::Execute: " << _desc << DVAL(_uid) << DVAL(_gid) << DVAL(geteuid()) << DVAL(getegid()) << DVAL(getuid()) << DVAL(getgid()) << DVAL(new_supplemental_groups)); - if(_should_add_thread_capabilities) { + if (_should_add_thread_capabilities) { tx.add_thread_capabilities(); } auto start_time = std::chrono::high_resolution_clock::now(); @@ -706,13 +831,35 @@ struct FSWorker : public Napi::AsyncWorker _report_fs_stats.Call({ fs_worker_stats }); } } + /** + * @brief Determines whether the GPFS dynamic library should be used. + * + * Checks that a GPFS library path was provided, that the library existence check + * has been performed, and that the current backend is configured as GPFS. + * + * @return `true` if the GPFS library path is set, the library existence check has completed (value > -1), + * and the configured backend equals `GPFS_BACKEND`; `false` otherwise. + */ bool use_gpfs_lib() { return gpfs_dl_path != NULL && gpfs_lib_file_exists > -1 && _backend == GPFS_BACKEND; } - void AddThreadCapabilities() { + /** + * @brief Mark the worker to add elevated thread capabilities before execution. + * + * When set, the worker will attempt to enable additional thread capabilities + * (e.g., for privileged operations) during its Execute phase. + */ + void AddThreadCapabilities() + { _should_add_thread_capabilities = true; } + /** + * @brief Finalize a successful worker by resolving its promise and reporting stats. + * + * Resolves the worker's stored JavaScript promise with `undefined` and invokes + * ReportWorkerStats to publish success metrics. + */ virtual void OnOK() override { DBG1("FS::FSWorker::OnOK: undefined " << _desc); @@ -772,7 +919,7 @@ struct FSWrapWorker : public FSWorker /** * Stat is an fs op - * + * * Note: this stat operation contains the system call of open. * Currently, we use it in list objects, but might want to create a different stat call * (or add changes inside this) to avoid permission check during list objects @@ -1238,6 +1385,28 @@ struct Readfile : public FSWorker buffer_releaser(NULL, _data); _data = 0; } + /** + * @brief Read the entire file at `_path` into an aligned in-memory buffer. + * + * Reads file metadata, optionally collects extended attributes, allocates an + * aligned buffer sized to the file (`_len`), and fills it with the file's + * contents. After reading, performs either a ctime or mtime change check + * depending on `_do_ctime_check`. + * + * Populates member fields on success: + * - `_stat_res` with the file stat, + * - `_xattr` with extended attributes when `_read_xattr` is true (and GPFS + * xattrs via `use_gpfs_lib()` when available and `_use_dmapi` may be used), + * - `_len` with the file size, + * - `_data` with a pointer to the allocated, aligned buffer containing file data. + * + * On failure sets the worker error via SetError or SetSyscallError and returns + * without populating the data buffer. + * + * @note Buffer allocation is performed with posix_memalign using + * `DIO_BUFFER_MEMALIGN`. If allocation fails or a syscall fails, an error + * is recorded. + */ virtual void Work() { int fd = open(_path.c_str(), O_RDONLY); @@ -1275,9 +1444,18 @@ struct Readfile : public FSWorker CHECK_MTIME_CHANGE(fd, _stat_res, _path); } } + /** + * @brief Compose the successful result for a completed readfile operation and resolve the JavaScript promise. + * + * Constructs a JavaScript object with two properties: + * - `stat`: file metadata populated from the worker's stat result. + * - `data`: a Node.js `Buffer` that takes ownership of the read memory; ownership is transferred so the worker no longer frees it and `buffer_releaser` will release it when GC collects the buffer. + * + * After constructing the result object the function resolves the worker's deferred promise and reports worker statistics. + */ virtual void OnOK() { - DBG1("FS::FSWorker::OnOK: Readfile " << DVAL(_path)); + DBG1("FS::Readfile::OnOK: " << DVAL(_path)); Napi::Env env = Env(); auto res_stat = Napi::Object::New(env); @@ -1311,6 +1489,13 @@ struct Readdir : public FSWorker // _withFileTypes = info[1].As(); Begin(XSTR() << "Readdir " << DVAL(_path)); } + /** + * @brief Reads directory entries from the path and appends them to `_entries`. + * + * Opens the directory at `_path`, iterates all entries, skips "." and "..", and for each entry + * appends an Entry containing the name, inode, type, and directory offset to `_entries`. + * On failure of opendir, readdir, or closedir, records the error by calling `SetSyscallError()`. + */ virtual void Work() { DIR* dir; @@ -1344,9 +1529,23 @@ struct Readdir : public FSWorker int r = closedir(dir); if (r) SetSyscallError(); } + /** + * @brief Resolve the worker's promise with an array of directory entry objects. + * + * Constructs a JavaScript array where each element is an object describing a + * directory entry and resolves the worker's deferred promise with that array. + * + * Each directory entry object contains the following properties: + * - `name`: entry name as a string. + * - `ino`: inode number as a number. + * - `type`: entry type (directory entry type constant) as a number. + * - `off`: directory offset as a BigInt. + * + * The method also reports worker statistics after resolving the promise. + */ virtual void OnOK() { - DBG1("FS::FSWorker::OnOK: Readdir " << DVAL(_path)); + DBG1("FS::Readdir::OnOK: " << DVAL(_path)); Napi::Env env = Env(); Napi::Array res = Napi::Array::New(env, _entries.size()); int index = 0; @@ -1398,8 +1597,13 @@ struct GetPwName : public FSWorker { std::string _user; struct passwd _pwd; - struct passwd *_getpwnam_res; + struct passwd* _getpwnam_res; std::unique_ptr _buf; + /** + * @brief Constructs a GetPwName worker that captures the target username from JS arguments. + * + * Initializes internal state and begins the worker with a descriptive name including the requested username. + */ GetPwName(const Napi::CallbackInfo& info) : FSWorker(info) , _getpwnam_res(NULL) @@ -1438,6 +1642,14 @@ struct FileWrap : public Napi::ObjectWrap std::string _path; int _fd; static Napi::FunctionReference constructor; + /** + * @brief Initialize and register the JavaScript `File` class on the given N-API environment. + * + * Defines the `File` class with its instance methods and accessor and stores a persistent + * constructor reference so the class remains available to JavaScript consumers. + * + * @param env N-API environment in which to define the `File` class. + */ static void init(Napi::Env env) { constructor = Napi::Persistent(DefineClass( @@ -1448,6 +1660,8 @@ struct FileWrap : public Napi::ObjectWrap InstanceMethod<&FileWrap::read>("read"), InstanceMethod<&FileWrap::write>("write"), InstanceMethod<&FileWrap::writev>("writev"), + InstanceMethod<&FileWrap::read_rdma>("read_rdma"), + InstanceMethod<&FileWrap::write_rdma>("write_rdma"), InstanceMethod<&FileWrap::replacexattr>("replacexattr"), InstanceMethod<&FileWrap::linkfileat>("linkfileat"), InstanceMethod<&FileWrap::unlinkfileat>("unlinkfileat"), @@ -1477,6 +1691,8 @@ struct FileWrap : public Napi::ObjectWrap Napi::Value read(const Napi::CallbackInfo& info); Napi::Value write(const Napi::CallbackInfo& info); Napi::Value writev(const Napi::CallbackInfo& info); + Napi::Value read_rdma(const Napi::CallbackInfo& info); + Napi::Value write_rdma(const Napi::CallbackInfo& info); Napi::Value replacexattr(const Napi::CallbackInfo& info); Napi::Value linkfileat(const Napi::CallbackInfo& info); Napi::Value unlinkfileat(const Napi::CallbackInfo& info); @@ -1569,6 +1785,11 @@ struct FileRead : public FSWrapWorker _pos = info[4].As(); Begin(XSTR() << "FileRead " << DVAL(_wrap->_path) << DVAL(_wrap->_fd) << DVAL(_pos) << DVAL(_offset) << DVAL(_len)); } + /** + * @brief Reads from the wrapped file descriptor into the worker's buffer at the specified buffer offset and file position. + * + * Performs a positional read using the FileWrap's file descriptor; on success stores the number of bytes read in `_br`. On failure records a syscall error via `SetSyscallError()`. + */ virtual void Work() { int fd = _wrap->_fd; @@ -1579,9 +1800,14 @@ struct FileRead : public FSWrapWorker return; } } + /** + * @brief Resolve the worker's promise with the number of bytes read and report worker statistics. + * + * Resolves the stored deferred Promise with the read byte count and calls ReportWorkerStats with a 0 status. + */ virtual void OnOK() { - DBG1("FS::FSWorker::OnOK: FileRead " << DVAL(_wrap->_path)); + DBG1("FS::FileRead::OnOK: " << DVAL(_wrap->_path)); Napi::Env env = Env(); _deferred.Resolve(Napi::Number::New(env, _br)); ReportWorkerStats(0); @@ -1652,6 +1878,15 @@ struct FileWritev : public FSWrapWorker } Begin(XSTR() << "FileWritev " << DVAL(_wrap->_path) << DVAL(_total_len) << DVAL(buffers_len) << DVAL(_offset)); } + /** + * @brief Performs the vectorized write to the underlying file descriptor. + * + * Writes the provided iovec buffers to the wrapper's file descriptor using `pwritev` + * when an offset is specified or `writev` when no offset is provided. On syscall + * failure the worker error state is set with the corresponding system error; if + * the written byte count does not match the expected total length the worker is + * marked with an error describing the partial write. + */ virtual void Work() { int fd = _wrap->_fd; @@ -1671,6 +1906,171 @@ struct FileWritev : public FSWrapWorker } }; +#define RDMA_DEFAULT_DC_KEY (0xffeeddcc) + +/** + * @brief Populate GPFS RDMA parameters from a JavaScript object and adjust sizes/offsets. + * + * Extracts RDMA connection and transfer parameters from the provided N-API object into + * the supplied gpfs_rdma_info_t structure, and sets the transfer byte count and file + * offset. If an `offset` field is present in `params`, it is applied to both the + * remote virtual address and the file offset (sliding-window adjustment). + * + * @param params JavaScript object containing RDMA fields (e.g. `addr`, `rkey`, `lid`, `qp_num`, `gid0`, `gid1`, `dc_key`, `fab_num`, `size`, `file_offset`, `offset`). + * @param[out] _rdma_info Filled gpfs_rdma_info_t describing the remote DC endpoint and keys. + * @param[out] _count Set to the requested transfer size (`size` field). + * @param[out] _file_offset Set to the requested file offset (`file_offset` field), then incremented by `offset` if provided. + */ +static void +init_gpfs_rdma_params( + Napi::Object params, + gpfs_rdma_info_t& _rdma_info, + gpfs_size64_t& _count, + gpfs_off64_t& _file_offset) +{ + memset(&_rdma_info, 0, sizeof _rdma_info); + _rdma_info.gpfs_rdma_info_type = GPFS_RDMA_INFO_TYPE_DC; + _rdma_info.rdma_info_dc.rem_vaddr = napi_get_u64_hex(params, "addr"); + _rdma_info.rdma_info_dc.rkey = napi_get_u32(params, "rkey"); + _rdma_info.rdma_info_dc.lid = napi_get_u32(params, "lid"); + _rdma_info.rdma_info_dc.qp_num = napi_get_u32(params, "qp_num"); + _rdma_info.rdma_info_dc.gid[0] = napi_get_u64_hex_or(params, "gid0", 0); + _rdma_info.rdma_info_dc.gid[1] = napi_get_u64_hex_or(params, "gid1", 0); + _rdma_info.rdma_info_dc.dc_key = napi_get_u32_or(params, "dc_key", RDMA_DEFAULT_DC_KEY); + _rdma_info.rdma_info_dc.fab_num = napi_get_i32_or(params, "fab_num", GPFS_RDMA_FABRIC_ANY); + _count = napi_get_i64(params, "size"); + _file_offset = napi_get_i64(params, "file_offset"); + int64_t offset = napi_get_i64(params, "offset"); + // NOTICE - we apply same offset to *both* the remote addr and file offset, like a sliding window + _rdma_info.rdma_info_dc.rem_vaddr += offset; + _file_offset += offset; +} + +struct FileReadRdma : public FSWrapWorker +{ + gpfs_rdma_info_t _rdma_info; + gpfs_size64_t _count; + gpfs_off64_t _file_offset; + gpfs_ssize64_t _result; + /** + * @brief Constructs a FileReadRdma async worker that performs a GPFS RDMA read. + * + * Initializes RDMA parameters from the JS callback info, validates that the + * GPFS RDMA pread symbol is available, and begins the worker with a descriptive + * name including the target path, byte count, and file offset. + * + * @param info N-API callback info whose second argument (info[1]) is an object + * containing RDMA parameters parsed by init_gpfs_rdma_params. + * + * @throws Napi::Error if the GPFS RDMA pread symbol is not supported on the system. + */ + FileReadRdma(const Napi::CallbackInfo& info) + : FSWrapWorker(info) + , _count(0) + , _file_offset(0) + , _result(-1) + { + if (!dlsym_gpfs_rdma_pread) { + throw napi_sys_error(info.Env(), "gpfs_rdma_pread not supported", EOPNOTSUPP); + } + Napi::Object params = info[1].As(); + init_gpfs_rdma_params(params, _rdma_info, _count, _file_offset); + Begin(XSTR() << "FileReadRdma " << DVAL(_wrap->_path) << DVAL(_count) << DVAL(_file_offset)); + } + /** + * @brief Performs an RDMA read from the wrapped file into the RDMA buffer. + * + * Executes a GPFS RDMA pread using the worker's configured file offset, count, and RDMA parameters, + * stores the number of bytes read in `_result`, and records a syscall error if the operation fails. + */ + virtual void Work() + { + int fd = _wrap->_fd; + CHECK_WRAP_FD(fd); + _result = dlsym_gpfs_rdma_pread(fd, _count, _file_offset, &_rdma_info); + if (_result < 0) { + SetSyscallError(); + return; + } + } + /** + * @brief Finalize a successful RDMA file read operation. + * + * Resolves the associated JavaScript promise with the number of bytes read + * and reports worker statistics. + */ + virtual void OnOK() + { + DBG1("FS::FileReadRdma::OnOK: " << DVAL(_wrap->_path) << DVAL(_result)); + _deferred.Resolve(Napi::Number::New(Env(), _result)); + ReportWorkerStats(0); + } +}; + +struct FileWriteRdma : public FSWrapWorker +{ + gpfs_rdma_info_t _rdma_info; + gpfs_size64_t _count; + gpfs_off64_t _file_offset; + gpfs_ssize64_t _result; + /** + * @brief Constructs a worker that performs an RDMA-based write (gpfs_rdma_pwrite) for a FileWrap. + * + * Initializes internal RDMA parameters from the provided JS callback arguments and begins the async + * worker with a descriptive operation name. + * + * @param info N-API callback info whose argument at index 1 must be an object of RDMA parameters + * used to initialize the underlying `gpfs_rdma_info_t`, and which also provides the + * target write count and file offset. + * + * @throws Napi::Error Thrown with code `EOPNOTSUPP` if `gpfs_rdma_pwrite` is not available. + */ + FileWriteRdma(const Napi::CallbackInfo& info) + : FSWrapWorker(info) + , _count(0) + , _file_offset(0) + , _result(-1) + { + if (!dlsym_gpfs_rdma_pwrite) { + throw napi_sys_error(info.Env(), "gpfs_rdma_pwrite not supported", EOPNOTSUPP); + } + Napi::Object params = info[1].As(); + init_gpfs_rdma_params(params, _rdma_info, _count, _file_offset); + Begin(XSTR() << "FileWriteRdma " << DVAL(_wrap->_path) << DVAL(_count) << DVAL(_file_offset)); + } + /** + * @brief Perform an RDMA-backed pwrite through the GPFS RDMA provider. + * + * Executes a GPFS RDMA pwrite using the worker's file descriptor, requested + * byte count, file offset, and RDMA parameters, and stores the returned + * result in the worker's `_result` field. + * + * On error, `_result` will be negative and the worker's syscall error state + * will be recorded via SetSyscallError(). + */ + virtual void Work() + { + int fd = _wrap->_fd; + CHECK_WRAP_FD(fd); + _result = dlsym_gpfs_rdma_pwrite(fd, _count, _file_offset, &_rdma_info); + if (_result < 0) { + SetSyscallError(); + return; + } + } + /** + * @brief Finalizes a successful RDMA file write operation. + * + * Resolves the worker's deferred promise with the number of bytes written and reports worker statistics indicating no error. + */ + virtual void OnOK() + { + DBG1("FS::FileWriteRdma::OnOK: " << DVAL(_wrap->_path) << DVAL(_result)); + _deferred.Resolve(Napi::Number::New(Env(), _result)); + ReportWorkerStats(0); + } +}; + /** * TODO: Not atomic and might cause partial updates of MD */ @@ -1710,6 +2110,18 @@ struct LinkFileAt : public FSWrapWorker std::string _filepath; int _replace_fd; bool _should_not_override; + /** + * @brief Constructs a LinkFileAt worker to create a hard link from the wrapped file to a target path. + * + * Parses arguments from the JS callback info to configure the link operation: + * - info[1]: destination path (string). + * - info[2] (optional): file descriptor of an existing file to replace (number). If omitted or negative, no replace FD is used. + * - info[3] (optional): when true, indicates the operation must not override an existing target (boolean). + * + * When a replace FD is not provided and `should_not_override` is true, thread capabilities are elevated to allow linkat operations from non-root users. + * + * @param info JavaScript callback arguments (see description for expected indices and types). + */ LinkFileAt(const Napi::CallbackInfo& info) : FSWrapWorker(info) , _replace_fd(-1) @@ -1722,12 +2134,21 @@ struct LinkFileAt : public FSWrapWorker if (info.Length() > 3 && !info[3].IsUndefined()) { _should_not_override = info[3].As(); } - if(_replace_fd < 0 && _should_not_override) { - //set thread capabilities to allow linkat from user other than root. + if (_replace_fd < 0 && _should_not_override) { + // set thread capabilities to allow linkat from user other than root. AddThreadCapabilities(); } Begin(XSTR() << "LinkFileAt " << DVAL(_wrap->_path) << DVAL(_wrap->_fd) << DVAL(_filepath) << DVAL(_should_not_override)); } + /** + * @brief Create a hard link from the wrapped file descriptor to the target path. + * + * Creates a hard link pointing at the file represented by this worker's FileWrap + * to _filepath. Behavior depends on the worker flags: + * - If _replace_fd >= 0, replace the target atomically using the provided descriptor. + * - If _should_not_override is true, do not overwrite an existing target (fail if it exists). + * - Otherwise, create the link (overwriting the target if the underlying syscall permits). + */ virtual void Work() { int fd = _wrap->_fd; @@ -1737,7 +2158,7 @@ struct LinkFileAt : public FSWrapWorker // Linux will fail the linkat() if the file already exist and we want to replace it if it existed. if (_replace_fd >= 0) { SYSCALL_OR_RETURN(dlsym_gpfs_linkatif(fd, "", AT_FDCWD, _filepath.c_str(), AT_EMPTY_PATH, _replace_fd)); - } else if (_should_not_override){ + } else if (_should_not_override) { SYSCALL_OR_RETURN(linkat(fd, "", AT_FDCWD, _filepath.c_str(), AT_EMPTY_PATH)); } else { SYSCALL_OR_RETURN(dlsym_gpfs_linkat(fd, "", AT_FDCWD, _filepath.c_str(), AT_EMPTY_PATH)); @@ -1825,7 +2246,19 @@ struct FileFsync : public FSWrapWorker struct FileFlock : public FSWrapWorker { - int lock_mode; + int lock_mode; + /** + * @brief Constructs a FileFlock worker and configures the requested lock mode. + * + * Parses an optional second argument (string) to select the lock mode and records the operation description. + * + * Accepted lock mode strings: + * - "EXCLUSIVE" — exclusive lock + * - "SHARED" — shared lock (default) + * - "UNLOCK" — release lock + * + * If an invalid mode string is provided, the worker is marked with an error. + */ FileFlock(const Napi::CallbackInfo& info) : FSWrapWorker(info) , lock_mode(LOCK_SH) @@ -1996,12 +2429,50 @@ FileWrap::write(const Napi::CallbackInfo& info) return api(info); } +/** + * @brief Enqueues a vectored write operation on this file. + * + * Performs a writev (multiple-buffer write) according to the provided callback arguments. + * + * @return The number of bytes written. + */ Napi::Value FileWrap::writev(const Napi::CallbackInfo& info) { return api(info); } +/** + * @brief Initiates an RDMA-backed read operation on this FileWrap. + * + * @return Napi::Value A JavaScript value representing the started RDMA read operation. On success the operation yields an object with fields `bytes` (number of bytes read) and `buffer` (the read data as a Node.js Buffer); on failure it results in an error. + */ +Napi::Value +FileWrap::read_rdma(const Napi::CallbackInfo& info) +{ + return api(info); +} + +/** + * Queue an RDMA-capable write operation bound to this FileWrap. + * + * @param info The JavaScript call arguments and execution context. + * @return Napi::Value The JavaScript value representing the initiated `write_rdma` operation. + */ +Napi::Value +FileWrap::write_rdma(const Napi::CallbackInfo& info) +{ + return api(info); +} + +/** + * @brief Enqueue a replace-xattr operation for this FileWrap instance. + * + * Starts an operation that replaces extended attributes on the file represented + * by this FileWrap. + * + * @return Napi::Value JavaScript handle for the initiated operation. + */ Napi::Value FileWrap::replacexattr(const Napi::CallbackInfo& info) { @@ -2281,6 +2752,18 @@ set_debug_level(const Napi::CallbackInfo& info) return info.Env().Undefined(); } +/** + * @brief Configure global logging outputs and syslog facility. + * + * Sets the module-global flags that control whether logging is emitted to + * stderr and/or to syslog, and updates the syslog debug facility name. + * + * @param info JavaScript callback info; expected arguments: + * - info[0]: boolean — enable logging to stderr. + * - info[1]: boolean — enable logging to syslog. + * - info[2]: string — syslog debug facility name to use. + * @return Napi::Value JavaScript `undefined`. + */ static Napi::Value set_log_config(const Napi::CallbackInfo& info) { @@ -2290,15 +2773,21 @@ set_log_config(const Napi::CallbackInfo& info) LOG_TO_STDERR_ENABLED = stderr_enabled; LOG_TO_SYSLOG_ENABLED = syslog_enabled; SYSLOG_DEBUG_FACILITY = syslog_debug_facility; - DBG1("FS::set_log_config: " - << DVAL(LOG_TO_STDERR_ENABLED) + DBG1("FS::set_log_config: " + << DVAL(LOG_TO_STDERR_ENABLED) << DVAL(LOG_TO_SYSLOG_ENABLED) << DVAL(SYSLOG_DEBUG_FACILITY)); return info.Env().Undefined(); } /** - * register noobaa args to GPFS + * @brief Register NooBaa runtime arguments with the GPFS library. + * + * Extracts NooBaa registration fields ("version", "delay", "flags") from the + * provided JavaScript parameters and calls the GPFS ganesha registration + * entrypoint. Logs the submitted values. If the GPFS call returns + * EOPNOTSUPP the function emits a warning and continues; any other failure + * triggers a panic. */ static Napi::Value register_gpfs_noobaa(const Napi::CallbackInfo& info) @@ -2313,7 +2802,7 @@ register_gpfs_noobaa(const Napi::CallbackInfo& info) if (dlsym_gpfs_ganesha(OPENHANDLE_REGISTER_NOOBAA, &args)) { if (errno == EOPNOTSUPP) { - LOG("Warning: register with libgpfs gpfs_ganesha returned EOPNOTSUPP" ); + LOG("Warning: register with libgpfs gpfs_ganesha returned EOPNOTSUPP"); } else { PANIC("Error: register with libgpfs gpfs_ganesha failed"); } @@ -2322,7 +2811,15 @@ register_gpfs_noobaa(const Napi::CallbackInfo& info) } /** - * Allocate memory aligned buffer for direct IO. + * @brief Create a POSIX-aligned buffer suitable for direct I/O. + * + * Allocates a memory buffer aligned to DIO_BUFFER_MEMALIGN and wraps it in a + * Napi::Buffer for JavaScript consumption. The buffer is freed using + * buffer_releaser when the JS Buffer is garbage-collected. + * + * @param info CallbackInfo where info[0] is the requested buffer size in bytes. + * @return Napi::Value A Buffer containing the allocated, aligned memory of the requested size. + * @throws Napi::Error If allocation fails or the requested buffer cannot be created. */ static Napi::Value dio_buffer_alloc(const Napi::CallbackInfo& info) @@ -2336,6 +2833,43 @@ dio_buffer_alloc(const Napi::CallbackInfo& info) return Napi::Buffer::New(info.Env(), buf, size, buffer_releaser); } +/** + * @brief Get the GPFS RDMA shadow buffer size for a requested buffer length. + * + * Expects a single numeric argument (size in bytes) at info[0]. Calls the + * GPFS `gpfs_rdma_shadow_buffer_size` symbol to translate the requested size + * into the required shadow buffer size for RDMA operations and returns that + * value. + * + * @param info Callback info whose first element is the requested size in bytes. + * @return int The shadow buffer size required by GPFS for the given request. + * + * @throws Napi::Error If the GPFS `gpfs_rdma_shadow_buffer_size` symbol is not + * available (error code EOPNOTSUPP) or if the underlying GPFS call + * returns a negative result. + */ +static Napi::Value +gpfs_rdma_shadow_buffer_size_napi(const Napi::CallbackInfo& info) +{ + if (!dlsym_gpfs_rdma_shadow_buffer_size) { + throw napi_sys_error(info.Env(), "gpfs_rdma_shadow_buffer_size not supported", EOPNOTSUPP); + } + gpfs_size64_t size = napi_get_i64(info[0]); + int r = dlsym_gpfs_rdma_shadow_buffer_size(size); + if (r < 0) { + throw napi_sys_error(info.Env(), "gpfs_rdma_shadow_buffer_size failed"); + } + return Napi::Number::New(info.Env(), r); +} + +/** + * @brief Initializes filesystem bindings on the provided N-API environment and attaches them to the given exports object, including optional GPFS integration when a GPFS library path is configured. + * + * Exports a top-level "fs" object containing filesystem functions, constants, and a "gpfs" sub-object that indicates GPFS availability and exposes GPFS-specific helpers when the GPFS shared library is present. + * + * @param env The N-API environment used to create and register functions and objects. + * @param exports The module exports object to which the "fs" namespace will be attached. + */ void fs_napi(Napi::Env env, Napi::Object exports) { @@ -2343,42 +2877,46 @@ fs_napi(Napi::Env env, Napi::Object exports) if (gpfs_dl_path != NULL) { LOG("FS::GPFS GPFS_DL_PATH=" << gpfs_dl_path); struct stat _stat_res; - gpfs_lib_file_exists = stat(gpfs_dl_path, &_stat_res); //SYSCALL_OR_RETURN + gpfs_lib_file_exists = stat(gpfs_dl_path, &_stat_res); if (gpfs_lib_file_exists == -1) { LOG("FS::GPFS WARN couldn't find GPFS lib file GPFS_DL_PATH=" << gpfs_dl_path); } else { LOG("FS::GPFS found GPFS lib file GPFS_DL_PATH=" << gpfs_dl_path); uv_lib_t* lib = (uv_lib_t*)malloc(sizeof(uv_lib_t)); + // required symbols if (uv_dlopen(gpfs_dl_path, lib)) { - PANIC("Error: %s\n" - << uv_dlerror(lib)); + PANIC("FS::GPFS Error: dlopen libgpfs failed " << DVAL(gpfs_dl_path) << uv_dlerror(lib)); } if (uv_dlsym(lib, "gpfs_linkat", (void**)&dlsym_gpfs_linkat)) { - PANIC("Error: %s\n" - << uv_dlerror(lib)); + PANIC("FS::GPFS Error: dlsym gpfs_linkat failed " << uv_dlerror(lib)); } if (uv_dlsym(lib, "gpfs_linkatif", (void**)&dlsym_gpfs_linkatif)) { - PANIC("Error: %s\n" - << uv_dlerror(lib)); + PANIC("FS::GPFS Error: dlsym gpfs_linkatif failed " << uv_dlerror(lib)); } if (uv_dlsym(lib, "gpfs_unlinkat", (void**)&dlsym_gpfs_unlinkat)) { - PANIC("Error: %s\n" - << uv_dlerror(lib)); + PANIC("FS::GPFS Error: dlsym gpfs_unlinkat failed " << uv_dlerror(lib)); } if (uv_dlsym(lib, "gpfs_fcntl", (void**)&dlsym_gpfs_fcntl)) { - PANIC("Error: %s\n" - << uv_dlerror(lib)); + PANIC("FS::GPFS Error: dlsym gpfs_fcntl failed " << uv_dlerror(lib)); } if (uv_dlsym(lib, "gpfs_ganesha", (void**)&dlsym_gpfs_ganesha)) { - PANIC("Error: %s\n" - << uv_dlerror(lib)); + PANIC("FS::GPFS Error: dlsym gpfs_ganesha failed " << uv_dlerror(lib)); } - if (sizeof(struct gpfsRequest_t) != 256) { - PANIC("The gpfs get extended attributes is of wrong size" << sizeof(struct gpfsRequest_t)); + // optional symbols + if (uv_dlsym(lib, "gpfs_rdma_pread", (void**)&dlsym_gpfs_rdma_pread)) { + DBG1("FS::GPFS dlsym gpfs_rdma_pread is not available " << uv_dlerror(lib)); } - + if (uv_dlsym(lib, "gpfs_rdma_pwrite", (void**)&dlsym_gpfs_rdma_pwrite)) { + DBG1("FS::GPFS dlsym gpfs_rdma_pwrite is not available " << uv_dlerror(lib)); + } + if (uv_dlsym(lib, "gpfs_rdma_shadow_buffer_size", (void**)&dlsym_gpfs_rdma_shadow_buffer_size)) { + DBG1("FS::GPFS dlsym gpfs_rdma_shadow_buffer_size is not available " << uv_dlerror(lib)); + } + bool gpfs_rdma_enabled = (dlsym_gpfs_rdma_pread && dlsym_gpfs_rdma_pwrite); auto gpfs = Napi::Object::New(env); gpfs["register_gpfs_noobaa"] = Napi::Function::New(env, register_gpfs_noobaa); + gpfs["rdma_enabled"] = Napi::Boolean::New(env, gpfs_rdma_enabled); + gpfs["rdma_shadow_buffer_size"] = Napi::Function::New(env, gpfs_rdma_shadow_buffer_size_napi); // we export the gpfs object, which can be checked to indicate that // gpfs lib was loaded and its api's can be used. exports_fs["gpfs"] = gpfs; @@ -2406,7 +2944,7 @@ fs_napi(Napi::Env env, Napi::Object exports) exports_fs["realpath"] = Napi::Function::New(env, api); exports_fs["getsinglexattr"] = Napi::Function::New(env, api); exports_fs["getpwname"] = Napi::Function::New(env, api); - exports_fs["symlink"] = Napi::Function::New(env, api); + exports_fs["symlink"] = Napi::Function::New(env, api); FileWrap::init(env); exports_fs["open"] = Napi::Function::New(env, api); @@ -2422,7 +2960,6 @@ fs_napi(Napi::Env env, Napi::Object exports) exports_fs["PLATFORM_IOV_MAX"] = Napi::Number::New(env, IOV_MAX); ThreadScope::init_passwd_buf_size(); - #ifdef O_DIRECT exports_fs["O_DIRECT"] = Napi::Number::New(env, O_DIRECT); #endif @@ -2435,7 +2972,6 @@ fs_napi(Napi::Env env, Napi::Object exports) exports_fs["set_log_config"] = Napi::Function::New(env, set_log_config); exports["fs"] = exports_fs; - } -} // namespace noobaa +} // namespace noobaa \ No newline at end of file diff --git a/src/native/nb_native.cpp b/src/native/nb_native.cpp index 1187a1060f..91fcc27634 100644 --- a/src/native/nb_native.cpp +++ b/src/native/nb_native.cpp @@ -11,10 +11,31 @@ void splitter_napi(Napi::Env env, Napi::Object exports); void chunk_coder_napi(napi_env env, napi_value exports); void fs_napi(Napi::Env env, Napi::Object exports); void crypto_napi(Napi::Env env, Napi::Object exports); -#ifdef BUILD_S3SELECT + +#if USE_CUOBJ_SERVER +void cuobj_server_napi(Napi::Env env, Napi::Object exports); +#endif +#if USE_CUOBJ_CLIENT +void cuobj_client_napi(Napi::Env env, Napi::Object exports); +#endif +#if USE_CUDA +void cuda_napi(Napi::Env env, Napi::Object exports); +#endif +#if BUILD_S3SELECT void s3select_napi(Napi::Env env, Napi::Object exports); #endif +/** + * @brief Register native N-API bindings onto the provided module exports object. + * + * Populates the given `exports` object with the module's native bindings (base64, + * SSL, syslog, splitter, chunk coder, filesystem, crypto, and optionally + * CUDA/cuobj and s3select implementations depending on build flags). + * + * @param env The N-API environment for the current addon initialization. + * @param exports The module exports object to attach native bindings to. + * @return Napi::Object The same `exports` object after native exports have been attached. + */ Napi::Object nb_native_napi(Napi::Env env, Napi::Object exports) { @@ -25,11 +46,22 @@ nb_native_napi(Napi::Env env, Napi::Object exports) chunk_coder_napi(env, exports); fs_napi(env, exports); crypto_napi(env, exports); -#ifdef BUILD_S3SELECT + +#if USE_CUOBJ_SERVER + cuobj_server_napi(env, exports); +#endif +#if USE_CUOBJ_CLIENT + cuobj_client_napi(env, exports); +#endif +#if USE_CUDA + cuda_napi(env, exports); +#endif +#if BUILD_S3SELECT s3select_napi(env, exports); #endif + return exports; } NODE_API_MODULE(nb_native, nb_native_napi) -} +} // namespace noobaa \ No newline at end of file diff --git a/src/native/tools/crypto_napi.cpp b/src/native/tools/crypto_napi.cpp index 4cfb1ff234..1341304b44 100644 --- a/src/native/tools/crypto_napi.cpp +++ b/src/native/tools/crypto_napi.cpp @@ -5,73 +5,11 @@ #include "../util/common.h" #include "../util/endian.h" #include "../util/napi.h" +#include "../util/worker.h" namespace noobaa { -template -static Napi::Value -api(const Napi::CallbackInfo& info) -{ - auto w = new T(info); - Napi::Promise promise = w->_deferred.Promise(); - w->Queue(); - return promise; -} - -/** - * CryptoWorker is a general async worker for our crypto operations - */ -struct CryptoWorker : public Napi::AsyncWorker -{ - Napi::Promise::Deferred _deferred; - // _args_ref is used to keep refs to all the args for the worker lifetime, - // which is needed for workers that receive buffers, - // because in their ctor they copy the pointers to the buffer's memory, - // and if the JS caller scope does not keep a ref to the buffers until after the call, - // then the worker may access invalid memory... - Napi::ObjectReference _args_ref; - - CryptoWorker(const Napi::CallbackInfo& info) - : AsyncWorker(info.Env()) - , _deferred(Napi::Promise::Deferred::New(info.Env())) - , _args_ref(Napi::Persistent(Napi::Object::New(info.Env()))) - { - for (int i = 0; i < (int)info.Length(); ++i) _args_ref.Set(i, info[i]); - } - virtual void OnOK() override - { - // LOG("CryptoWorker::OnOK: undefined"); - _deferred.Resolve(Env().Undefined()); - } - virtual void OnError(Napi::Error const& error) override - { - LOG("CryptoWorker::OnError: " << DVAL(error.Message())); - auto obj = error.Value(); - _deferred.Reject(obj); - } -}; - -/** - * CryptoWrapWorker is meant to simplify adding async CryptoWorker instance methods to ObjectWrap types - * like MD5Wrap, while keeping the object referenced during that action. - */ -template -struct CryptoWrapWorker : public CryptoWorker -{ - Wrapper* _wrap; - CryptoWrapWorker(const Napi::CallbackInfo& info) - : CryptoWorker(info) - { - _wrap = Wrapper::Unwrap(info.This().As()); - _wrap->Ref(); - } - ~CryptoWrapWorker() - { - _wrap->Unref(); - } -}; - struct MD5Wrap : public Napi::ObjectWrap { size_t _NWORDS = MD5_DIGEST_NWORDS; @@ -117,12 +55,19 @@ struct MD5Wrap : public Napi::ObjectWrap Napi::FunctionReference MD5Wrap::constructor; -struct MD5Update : public CryptoWrapWorker +struct MD5Update : public ObjectWrapWorker { uint8_t* _buf; size_t _len; + /** + * @brief Create an MD5Update worker and capture the input buffer for the update. + * + * Binds the worker to the wrapped MD5 object and stores a pointer and length referencing the Buffer provided as the first argument. + * + * @param info N-API callback info; its first argument (info[0]) must be a Buffer whose data pointer and length are stored for the update operation. + */ MD5Update(const Napi::CallbackInfo& info) - : CryptoWrapWorker(info) + : ObjectWrapWorker(info) , _buf(0) , _len(0) { @@ -130,19 +75,36 @@ struct MD5Update : public CryptoWrapWorker _buf = buf.Data(); _len = buf.Length(); } + /** + * @brief Applies the worker's buffer to the associated MD5 context. + * + * Submits the stored input bytes to the wrapped MD5 context as an update operation. + */ virtual void Execute() { _wrap->submit_and_flush(_buf, _len, HASH_UPDATE); } }; -struct MD5Digest : public CryptoWrapWorker +struct MD5Digest : public ObjectWrapWorker { std::vector _digest; + /** + * @brief Create an MD5Digest worker bound to the JavaScript callback context. + * + * @param info N-API callback information used to construct and bind the worker to the JS object. + */ MD5Digest(const Napi::CallbackInfo& info) - : CryptoWrapWorker(info) + : ObjectWrapWorker(info) { } + /** + * @brief Finalizes the MD5 computation and populates the native digest vector. + * + * Ensures the digest vector has capacity for all digest words, finalizes the + * underlying hash context, and fills _digest with the resulting 32-bit words + * converted to host endianness according to _wrap->_WORDS_BE. + */ virtual void Execute() { _digest.reserve(_wrap->_NWORDS); @@ -151,25 +113,52 @@ struct MD5Digest : public CryptoWrapWorker _digest[i] = _wrap->_WORDS_BE ? be32toh(hash_ctx_digest(&_wrap->_ctx)[i]) : le32toh(hash_ctx_digest(&_wrap->_ctx)[i]); } } + /** + * @brief Resolve the worker's promise with the computed MD5 digest as a Node Buffer. + * + * Resolves the internal promise with a Napi::Buffer that copies the digest words + * stored in `_digest`. The buffer contains `_wrap->_NWORDS` 32-bit words representing the MD5 digest. + */ virtual void OnOK() { Napi::Env env = Env(); - _deferred.Resolve(Napi::Buffer::Copy(env, _digest.data(), _wrap->_NWORDS)); + _promise.Resolve(Napi::Buffer::Copy(env, _digest.data(), _wrap->_NWORDS)); } }; +/** + * @brief Starts an MD5 update operation using the provided callback arguments. + * + * Initiates an asynchronous MD5Update worker bound to this MD5Wrap instance which processes the input buffer supplied in the JavaScript call. + * + * @returns Napi::Value JavaScript value produced by the MD5Update worker. + */ Napi::Value MD5Wrap::update(const Napi::CallbackInfo& info) { - return api(info); + return await_worker(info); } +/** + * @brief Finalizes the MD5 computation and obtains the resulting digest. + * + * @returns A JavaScript Buffer containing the MD5 digest as 32-bit words converted to host byte order. + */ Napi::Value MD5Wrap::digest(const Napi::CallbackInfo& info) { - return api(info); + return await_worker(info); } +/** + * @brief Registers the crypto namespace on the module exports and exposes the MD5Async class. + * + * Initializes the MD5Wrap class constructor and attaches an object named "crypto" to the provided + * exports containing the "MD5Async" constructor for asynchronous MD5 hashing. + * + * @param env The N-API environment for the current addon initialization. + * @param exports The addon exports object to which the "crypto" property will be attached. + */ void crypto_napi(Napi::Env env, Napi::Object exports) { @@ -181,4 +170,4 @@ crypto_napi(Napi::Env env, Napi::Object exports) exports["crypto"] = exports_crypto_async; } -} // namespace noobaa +} // namespace noobaa \ No newline at end of file diff --git a/src/native/util/backtrace.h b/src/native/util/backtrace.h index 8944bc0e71..598ce72978 100644 --- a/src/native/util/backtrace.h +++ b/src/native/util/backtrace.h @@ -12,13 +12,49 @@ #include #endif +/** + * Collects and represents a stack backtrace for the current thread. + */ + +/** + * Represents a single stack frame entry. + * + * @param addr_ Address of the stack frame. + * @param file_ Source file path or executable name associated with the frame (may be empty). + * @param line_ Source line number if available, otherwise 0. + * @param func_ Function name (demangled when possible) or an address-based hex string. + */ + +/** + * Capture the current call stack up to the specified depth, skipping the initial frames. + * + * This constructor resolves symbol names (attempting C++ demangling) and file names for + * each captured frame and stores them as Entry objects. Processing stops when symbol or + * file information is not available for subsequent frames. + * + * @param depth Maximum number of stack frames to capture (must be <= MAX_DEPTH). + * @param skip Number of initial frames to skip (typically used to omit the Backtrace constructor and its callers). + */ + +/** + * Format and write the backtrace to the provided output stream. + * + * @param os Output stream to receive the formatted backtrace. + * @param bt Backtrace instance whose entries will be written. + * @returns Reference to the same output stream `os`. + */ + +/** + * Print the backtrace to standard output. + */ namespace noobaa { class Backtrace { public: - struct Entry { + struct Entry + { explicit Entry( void* addr_, std::string file_, @@ -36,7 +72,10 @@ class Backtrace std::string func; }; - enum { MAX_DEPTH = 96 }; + enum + { + MAX_DEPTH = 96 + }; explicit Backtrace(int depth = 32, int skip = 0) { @@ -51,15 +90,26 @@ class Backtrace if (!dladdr(trace[i], &info)) { break; } - int status; - std::string file(info.dli_fname); - std::string func(info.dli_sname); - char* demangled = abi::__cxa_demangle(info.dli_sname, NULL, 0, &status); - if (status == 0 && demangled) { - func = demangled; + std::string func; + if (info.dli_sname) { + int status = -1; + char* demangled = abi::__cxa_demangle(info.dli_sname, NULL, 0, &status); + if (status == 0 && demangled) { + func = demangled; + } else { + func = info.dli_sname; + } + if (demangled) { + free(demangled); + } + } else { + std::stringstream s; + s << "0x" << std::hex << uintptr_t(info.dli_saddr); + func = s.str(); } - if (demangled) { - free(demangled); + std::string file; + if (info.dli_fname) { + file = info.dli_fname; } if (file.empty()) { break; // entries after main @@ -86,4 +136,4 @@ class Backtrace std::vector _stack; }; -} // namespace noobaa +} // namespace noobaa \ No newline at end of file diff --git a/src/native/util/common.h b/src/native/util/common.h index 77af3d3f08..e03a13c37f 100644 --- a/src/native/util/common.h +++ b/src/native/util/common.h @@ -20,6 +20,11 @@ #include "os.h" #include "syslog.h" +/** + * Build a standardized log message prefix containing timestamp and process/thread IDs. + * + * @returns A string formatted as ". [PID-/TID-] " where the date/time is local to the system and microseconds provide subsecond precision. + */ namespace noobaa { @@ -32,6 +37,9 @@ namespace noobaa #endif #define DVAL(x) #x "=" << x << " " +#define DMEM(x,ptr,len) #x "=[" << ((void*)ptr) << "+" << ((void*)len) << "] " +#define DBUF(x) DMEM(x,(x).Data(),(x).Length()) +#define STRINGIFY(x) #x extern bool LOG_TO_STDERR_ENABLED; extern bool LOG_TO_SYSLOG_ENABLED; @@ -202,4 +210,4 @@ LOG_PREFIX() << " [PID-" << getpid() << "/TID-" << get_current_tid() << "] "; } -} // namespace noobaa +} // namespace noobaa \ No newline at end of file diff --git a/src/native/util/napi.h b/src/native/util/napi.h index 16efc32f3d..56fdda73df 100644 --- a/src/native/util/napi.h +++ b/src/native/util/napi.h @@ -5,12 +5,288 @@ #include #ifdef __cplusplus -#include + #include #endif +/** + * Convert a JavaScript number value to a 32-bit unsigned integer. + * @param v JavaScript value expected to be a Number. + * @returns The value converted to a `uint32_t`. + */ +/** + * Convert a JavaScript number value to a 32-bit signed integer. + * @param v JavaScript value expected to be a Number. + * @returns The value converted to an `int32_t`. + */ +/** + * Convert a JavaScript number value to a 64-bit signed integer. + * @param v JavaScript value expected to be a Number. + * @returns The value converted to an `int64_t`. + */ +/** + * Convert a JavaScript string value to a UTF-8 std::string. + * @param v JavaScript value expected to be a String. + * @returns The UTF-8 encoded `std::string`. + */ +/** + * Parse a JavaScript string value as a hexadecimal unsigned 64-bit integer. + * @param v JavaScript value expected to be a String representing a hex number. + * @returns The parsed `uint64_t` value. + */ +/** + * Retrieve a named property from a JavaScript object and convert it to a 32-bit unsigned integer. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @returns The property value converted to a `uint32_t`. + */ +/** + * Retrieve a named property from a JavaScript object and convert it to a 32-bit signed integer. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @returns The property value converted to an `int32_t`. + */ +/** + * Retrieve a named property from a JavaScript object and convert it to a 64-bit signed integer. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @returns The property value converted to an `int64_t`. + */ +/** + * Retrieve a named property from a JavaScript object and convert it to a UTF-8 std::string. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @returns The property value as a UTF-8 `std::string`. + */ +/** + * Retrieve a named property from a JavaScript object and parse it as a hexadecimal unsigned 64-bit integer. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @returns The parsed `uint64_t` value. + */ +/** + * Retrieve a named numeric property from a JavaScript object; return a default if the property is not a number. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @param default_value Value to return when the property is not a Number. + * @returns The property value converted to a `uint32_t`, or `default_value` if not a number. + */ +/** + * Retrieve a named numeric property from a JavaScript object; return a default if the property is not a number. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @param default_value Value to return when the property is not a Number. + * @returns The property value converted to an `int32_t`, or `default_value` if not a number. + */ +/** + * Retrieve a named numeric property from a JavaScript object; return a default if the property is not a number. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @param default_value Value to return when the property is not a Number. + * @returns The property value converted to an `int64_t`, or `default_value` if not a number. + */ +/** + * Retrieve a named string property from a JavaScript object; return a default if the property is not a string. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @param default_value Value to return when the property is not a String. + * @returns The property value as a UTF-8 `std::string`, or `default_value` if not a string. + */ +/** + * Retrieve a named string property from a JavaScript object and parse it as a hex unsigned 64-bit integer; return a default if the property is not a string. + * @param obj JavaScript object containing the property. + * @param key Property name to read. + * @param default_value Value to return when the property is not a String. + * @returns The parsed `uint64_t` value, or `default_value` if not a string. + */ +/** + * Read an integer property from a JavaScript object into a C `int`. + * @param env N-API environment. + * @param obj JavaScript object to read from. + * @param name Property name to read. + * @param p_num Pointer to an `int` to receive the value. + */ +/** + * Set an integer property on a JavaScript object. + * @param env N-API environment. + * @param obj JavaScript object to modify. + * @param name Property name to set. + * @param num Integer value to assign. + */ +/** + * Read a string property from a JavaScript object into a C buffer. + * @param env N-API environment. + * @param obj JavaScript object to read from. + * @param name Property name to read. + * @param str Destination buffer to receive the string (UTF-8). + * @param max Maximum number of bytes to write into `str`. + */ +/** + * Set a string property on a JavaScript object from a C buffer. + * @param env N-API environment. + * @param obj JavaScript object to modify. + * @param name Property name to set. + * @param str Source buffer containing the string (UTF-8). + * @param len Number of bytes from `str` to use. + */ +/** + * Read a binary buffer property from a JavaScript object into an `NB_Buf`. + * @param env N-API environment. + * @param obj JavaScript object to read from. + * @param name Property name to read. + * @param b Pointer to an `NB_Buf` to receive the data. + */ +/** + * Set a binary buffer property on a JavaScript object from an `NB_Buf`. + * @param env N-API environment. + * @param obj JavaScript object to modify. + * @param name Property name to set. + * @param b Pointer to an `NB_Buf` containing the data. + */ +/** + * Read a base64-encoded buffer property from a JavaScript object into an `NB_Buf`. + * @param env N-API environment. + * @param obj JavaScript object to read from. + * @param name Property name to read. + * @param b Pointer to an `NB_Buf` to receive the decoded data. + */ +/** + * Set a property on a JavaScript object from an `NB_Buf`, encoding the data as base64. + * @param env N-API environment. + * @param obj JavaScript object to modify. + * @param name Property name to set. + * @param b Pointer to an `NB_Buf` containing the data to encode. + */ +/** + * Read an array of buffers property from a JavaScript object into an `NB_Bufs`. + * @param env N-API environment. + * @param obj JavaScript object to read from. + * @param name Property name to read. + * @param bufs Pointer to an `NB_Bufs` to receive the data. + */ +/** + * Set an array of buffers property on a JavaScript object from an `NB_Bufs`. + * @param env N-API environment. + * @param obj JavaScript object to modify. + * @param name Property name to set. + * @param bufs Pointer to an `NB_Bufs` containing the data. + */ +/** + * Finalizer used to free native data associated with a N-API object. + * @param env N-API environment. + * @param data Pointer to the native data to free. + * @param hint User-provided hint passed to the finalizer. + */ namespace noobaa { +// single value getters + +inline uint32_t +napi_get_u32(Napi::Value v) +{ + return v.As().Uint32Value(); +} + +inline int32_t +napi_get_i32(Napi::Value v) +{ + return v.As().Int32Value(); +} + +inline int64_t +napi_get_i64(Napi::Value v) +{ + return v.As().Int64Value(); +} + +inline std::string +napi_get_str(Napi::Value v) +{ + return v.As().Utf8Value(); +} + +inline uint64_t +napi_get_u64_hex(Napi::Value v) +{ + return std::stoull(napi_get_str(v), nullptr, 16); +} + +// object property getters + +inline uint32_t +napi_get_u32(Napi::Object obj, const char* key) +{ + return napi_get_u32(obj.Get(key)); +} + +inline int32_t +napi_get_i32(Napi::Object obj, const char* key) +{ + return napi_get_i32(obj.Get(key)); +} + +inline int64_t +napi_get_i64(Napi::Object obj, const char* key) +{ + return napi_get_i64(obj.Get(key)); +} + +inline std::string +napi_get_str(Napi::Object obj, const char* key) +{ + return napi_get_str(obj.Get(key)); +} + +inline uint64_t +napi_get_u64_hex(Napi::Object obj, const char* key) +{ + return napi_get_u64_hex(obj.Get(key)); +} + +// object property getters with default value + +inline uint32_t +napi_get_u32_or(Napi::Object obj, const char* key, uint32_t default_value) +{ + auto v = obj.Get(key); + if (!v.IsNumber()) return default_value; + return napi_get_u32(v); +} + +inline int32_t +napi_get_i32_or(Napi::Object obj, const char* key, int32_t default_value) +{ + auto v = obj.Get(key); + if (!v.IsNumber()) return default_value; + return napi_get_i32(v); +} + +inline int64_t +napi_get_i64_or(Napi::Object obj, const char* key, int64_t default_value) +{ + auto v = obj.Get(key); + if (!v.IsNumber()) return default_value; + return napi_get_i64(v); +} + +inline std::string +napi_get_str_or(Napi::Object obj, const char* key, const std::string& default_value) +{ + auto v = obj.Get(key); + if (!v.IsString()) return default_value; + return napi_get_str(v); +} + +inline uint64_t +napi_get_u64_hex_or(Napi::Object obj, const char* key, uint64_t default_value) +{ + auto v = obj.Get(key); + if (!v.IsString()) return default_value; + return napi_get_u64_hex(v); +} + +// low level c helpers + void nb_napi_get_int(napi_env env, napi_value obj, const char* name, int* p_num); void nb_napi_set_int(napi_env env, napi_value obj, const char* name, int num); void nb_napi_get_str(napi_env env, napi_value obj, const char* name, char* str, int max); @@ -22,4 +298,4 @@ void nb_napi_set_buf_b64(napi_env env, napi_value obj, const char* name, struct void nb_napi_get_bufs(napi_env env, napi_value obj, const char* name, struct NB_Bufs* bufs); void nb_napi_set_bufs(napi_env env, napi_value obj, const char* name, struct NB_Bufs* bufs); void nb_napi_finalize_free_data(napi_env env, void* data, void* hint); -} +} // namespace noobaa \ No newline at end of file diff --git a/src/native/util/worker.h b/src/native/util/worker.h new file mode 100644 index 0000000000..21336b516f --- /dev/null +++ b/src/native/util/worker.h @@ -0,0 +1,136 @@ +/* Copyright (C) 2016 NooBaa */ +#pragma once + +#include "napi.h" + +/** + * Base asynchronous worker that runs off the main thread and exposes its result + * via a JavaScript Promise while keeping JS arguments and the `this` value alive + * for the lifetime of the worker. + * + * The worker holds a Deferred promise in `_promise`, a persistent object in + * `_args_ref` containing copies of the original call arguments, and a persistent + * reference to `this` in `_this_ref`. Subclasses should override `Execute()` + * to perform blocking work in the worker thread and override `OnOK()` to build + * the resulting JS value and resolve the promise. + * + * @param info Callback invocation info used to capture arguments and `this`. + */ + +/** + * Default OnOK implementation that resolves the associated promise with + * `undefined`. Override to resolve with a computed result. + */ + +/** + * OnError implementation that rejects the associated promise with the + * provided Napi::Error value. + */ + +/** + * Template helper for instance-bound workers that unwraps and retains the + * ObjectWrap instance for the duration of the worker. + * + * The unwrapped object pointer is stored in `_wrap`; the constructor calls + * `Ref()` to keep the object alive and the destructor calls `Unref()` to + * release that reference. + * + * @tparam ObjectWrapType Type of the ObjectWrap-derived class being wrapped. + * @param info Callback invocation info passed to the base PromiseWorker. + */ +namespace noobaa +{ + +/** + * PromiseWorker is a base async worker that runs in a separate thread and + * returns a promise. It makes sure to hold reference to keep the JS arguments + * alive until the worker is done. + * Inherit from this class and override Execute() to do the async work. + * Override OnOK() to resolve the promise with the result. + */ +struct PromiseWorker : public Napi::AsyncWorker +{ + Napi::Promise::Deferred _promise; + + // keep refs to all the args/this for the worker lifetime. + // this is needed mainly for workers that receive buffers, + // and uses them to access the buffer memory. + // these refs are released when the worker is deleted. + Napi::ObjectReference _args_ref; + Napi::Reference _this_ref; + + PromiseWorker(const Napi::CallbackInfo& info) + : AsyncWorker(info.Env()) + , _promise(Napi::Promise::Deferred::New(info.Env())) + , _args_ref(Napi::Persistent(Napi::Object::New(info.Env()))) + , _this_ref(Napi::Persistent(info.This())) + { + for (int i = 0; i < (int)info.Length(); ++i) _args_ref.Set(i, info[i]); + } + + /** + * This is a simple OnOK() that just resolves the promise with undefined. + * However, most workers will needs to return a value that they compute + * during Execute(), but Execute() runs in another thread and cannot access + * JS objects. Instead, Execute() should keep native values/structures in + * member variables, and override OnOK() to build the resulting JS value + * and resolve the promise with it. + */ + virtual void OnOK() override + { + // DBG1("PromiseWorker::OnOK: resolved (empty)"); + _promise.Resolve(Env().Undefined()); + } + + /** + * Handle worker error by rejecting the promise with the error message. + */ + virtual void OnError(Napi::Error const& error) override + { + LOG("PromiseWorker::OnError: " << DVAL(error.Message())); + auto obj = error.Value(); + _promise.Reject(obj); + } +}; + +/** + * ObjectWrapWorker is a base class that simplifies adding async instance methods + * to ObjectWrap types while keeping the object referenced during that action. + */ +template +struct ObjectWrapWorker : public PromiseWorker +{ + ObjectWrapType* _wrap; + ObjectWrapWorker(const Napi::CallbackInfo& info) + : PromiseWorker(info) + { + _wrap = ObjectWrapType::Unwrap(info.This().As()); + _wrap->Ref(); + } + ~ObjectWrapWorker() + { + _wrap->Unref(); + } +}; + +/** + * await_worker is a helper function to submit a PromiseWorker or ObjectWrapWorker + * WorkerType should anyway be a subclass of PromiseWorker. + */ +template +Napi::/** + * Create and enqueue a WorkerType instance using the provided callback info and return its associated Promise. + * + * @param info JavaScript callback arguments and `this` value forwarded to the worker's constructor. + * @returns A JavaScript Promise that will be resolved or rejected by the queued worker. + */ +Value +await_worker(const Napi::CallbackInfo& info) +{ + PromiseWorker* worker = new WorkerType(info); + Napi::Promise promise = worker->_promise.Promise(); + worker->Queue(); // this will delete the worker when done + return promise; +} + +} // namespace noobaa \ No newline at end of file diff --git a/src/util/js_utils.js b/src/util/js_utils.js index e0604c4d0f..30d0e88708 100644 --- a/src/util/js_utils.js +++ b/src/util/js_utils.js @@ -215,14 +215,15 @@ function hasOwnProperty(obj, prop_name_or_sym) { } /** - * Unlike lodash omit, this omit will not convert null, undefined, value typed, - * arrays or functions into an object (empty or not) and will not clone the passed - * object if the symbol does not exists on the object own properties + * Return the input with the given own symbol-keyed property removed, leaving other values unchanged. + * + * If the input is not an object-like value, is an array, or does not have the symbol as an own property, + * the original input is returned unchanged; otherwise a new object without the symbol-keyed property is returned. * * @template T - * @param {T} maybe_obj - * @param {symbol} sym - * @returns {Omit | T} + * @param {T} maybe_obj - Value that may be an object from which to omit the symbol property. + * @param {symbol} sym - The symbol key to remove from the object if present as an own property. + * @returns {Omit | T} The original value unchanged, or a new object with the symbol property omitted. */ function omit_symbol(maybe_obj, sym) { if ( @@ -237,6 +238,24 @@ function omit_symbol(maybe_obj, sym) { return _.omit(obj, sym); } +/** + * Load a CommonJS module by name, returning null when the module is not installed. + * + * @param {string} module_name - Module name or path to pass to require. + * @returns {any|null} The required module, or `null` if the module cannot be found. + * @throws {Error} Re-throws the original error if requiring fails for any reason other than the module being missing. + */ +function require_optional(module_name) { + try { + return require(module_name); + } catch (err) { + if (err.code === 'MODULE_NOT_FOUND') { + return null; + } + throw err; + } +} + exports.self_bind = self_bind; exports.array_push_all = array_push_all; exports.array_push_keep_latest = array_push_keep_latest; @@ -250,3 +269,4 @@ exports.inspect_lazy = inspect_lazy; exports.make_array = make_array; exports.map_get_or_create = map_get_or_create; exports.omit_symbol = omit_symbol; +exports.require_optional = require_optional; \ No newline at end of file