From e32985e4909e9a38f9e423e3b2fdfb39be0a032e Mon Sep 17 00:00:00 2001 From: Antonio SJ Musumeci Date: Mon, 19 Jan 2026 15:16:01 -0600 Subject: [PATCH] Rework open file resource management --- libfuse/include/fuse.h | 10 ++ libfuse/lib/fuse.cpp | 5 +- libfuse/lib/fuse_lowlevel.cpp | 2 +- src/fileinfo.hpp | 15 +- src/fuse_create.cpp | 124 ++++--------- src/fuse_open.cpp | 242 +++++++++++--------------- src/fuse_release.cpp | 84 ++++----- src/state.hpp | 12 +- tests/TEST_io_passthrough_create_open | 103 +++++++++++ tests/TEST_io_passthrough_open_race | 132 ++++++++++++++ tests/tests.cpp | 5 +- 11 files changed, 442 insertions(+), 292 deletions(-) create mode 100755 tests/TEST_io_passthrough_create_open create mode 100755 tests/TEST_io_passthrough_open_race diff --git a/libfuse/include/fuse.h b/libfuse/include/fuse.h index 909227b0..64a42bc3 100644 --- a/libfuse/include/fuse.h +++ b/libfuse/include/fuse.h @@ -29,6 +29,16 @@ EXTERN_C_BEGIN * Basic FUSE API * * ----------------------------------------------------------- */ +#define INVALID_BACKING_ID (0) + +static +inline +int +fuse_backing_id_is_valid(const int backing_id_) +{ + return (backing_id_ > INVALID_BACKING_ID); +} + struct fuse_dirents_t; typedef struct fuse_dirents_t fuse_dirents_t; diff --git a/libfuse/lib/fuse.cpp b/libfuse/lib/fuse.cpp index 6e4efec0..59483fe2 100644 --- a/libfuse/lib/fuse.cpp +++ b/libfuse/lib/fuse.cpp @@ -3878,7 +3878,7 @@ fuse_passthrough_open(const int fd_) rv = ::ioctl(dev_fuse_fd,FUSE_DEV_IOC_BACKING_OPEN,&bm); - return rv; + return ((rv < 0) ? INVALID_BACKING_ID : rv); } int @@ -3886,6 +3886,9 @@ fuse_passthrough_close(const int backing_id_) { int dev_fuse_fd; + if(!fuse_backing_id_is_valid(backing_id_)) + return 0; + dev_fuse_fd = fuse_chan_fd(f.se->ch); return ::ioctl(dev_fuse_fd,FUSE_DEV_IOC_BACKING_CLOSE,&backing_id_); diff --git a/libfuse/lib/fuse_lowlevel.cpp b/libfuse/lib/fuse_lowlevel.cpp index ad0df588..ef5e390d 100644 --- a/libfuse/lib/fuse_lowlevel.cpp +++ b/libfuse/lib/fuse_lowlevel.cpp @@ -281,7 +281,7 @@ fill_open(struct fuse_open_out *arg_, arg_->open_flags |= FOPEN_PARALLEL_DIRECT_WRITES; if(ffi_->noflush) arg_->open_flags |= FOPEN_NOFLUSH; - if(ffi_->passthrough && (ffi_->backing_id > 0)) + if(ffi_->passthrough && fuse_backing_id_is_valid(ffi_->backing_id)) { arg_->open_flags |= FOPEN_PASSTHROUGH; arg_->backing_id = ffi_->backing_id; diff --git a/src/fileinfo.hpp b/src/fileinfo.hpp index 9a435edb..6e25ebd4 100644 --- a/src/fileinfo.hpp +++ b/src/fileinfo.hpp @@ -22,13 +22,15 @@ #include "base_types.h" +#include #include class FileInfo : public FH { public: - static FileInfo *from_fh(const u64 fh); + static FileInfo *from_fh(const u64); + static u64 to_fh(const FileInfo*); public: FileInfo(const int fd_, @@ -71,16 +73,25 @@ public: std::mutex mutex; }; +inline +u64 +FileInfo::to_fh(const FileInfo *fi_) +{ + assert(fi_ != nullptr); + return reinterpret_cast(fi_); +} + inline u64 FileInfo::to_fh() const { - return reinterpret_cast(this); + return FileInfo::to_fh(this); } inline FileInfo* FileInfo::from_fh(const u64 fh_) { + assert(fh_ != 0); return reinterpret_cast(fh_); } diff --git a/src/fuse_create.cpp b/src/fuse_create.cpp index 081628fa..fe830c38 100644 --- a/src/fuse_create.cpp +++ b/src/fuse_create.cpp @@ -36,6 +36,7 @@ #include "fuse.h" +#include #include #include @@ -225,29 +226,26 @@ _(const PassthroughIOEnum e_, static int -_create_for_insert_lambda(const fuse_req_ctx_t *ctx_, - const fs::path &fusepath_, - const mode_t mode_, - fuse_file_info_t *ffi_, - State::OpenFile *of_) +_create(const fuse_req_ctx_t *ctx_, + const fs::path &fusepath_, + mode_t mode_, + fuse_file_info_t *ffi_) { - int rv; - FileInfo *fi; + auto &of = state.open_files; ::_config_to_ffi_flags(cfg,ctx_->pid,ffi_); if(cfg.cache_writeback) ::_tweak_flags_cache_writeback(&ffi_->flags); - ffi_->noflush = !::_calculate_flush(cfg.flushonclose, - ffi_->flags); - - rv = ::_create(ctx_, - cfg.func.getattr.policy, - cfg.func.create.policy, - cfg.branches, - fusepath_, - ffi_, - mode_, - ctx_->umask); + ffi_->noflush = !::_calculate_flush(cfg.flushonclose,ffi_->flags); + + int rv = ::_create(ctx_, + cfg.func.getattr.policy, + cfg.func.create.policy, + cfg.branches, + fusepath_, + ffi_, + mode_, + ctx_->umask); if(rv == -EROFS) { cfg.branches.find_and_set_mode_ro(); @@ -264,10 +262,8 @@ _create_for_insert_lambda(const fuse_req_ctx_t *ctx_, if(rv < 0) return rv; - fi = FileInfo::from_fh(ffi_->fh); - - of_->ref_count = 1; - of_->fi = fi; + FileInfo *fi = FileInfo::from_fh(ffi_->fh); + int backing_id = INVALID_BACKING_ID; switch(_(cfg.passthrough_io,ffi_->flags)) { @@ -276,85 +272,29 @@ _create_for_insert_lambda(const fuse_req_ctx_t *ctx_, case _(PassthroughIO::ENUM::RW,O_RDONLY): case _(PassthroughIO::ENUM::RW,O_WRONLY): case _(PassthroughIO::ENUM::RW,O_RDWR): + backing_id = FUSE::passthrough_open(fi->fd); + if(fuse_backing_id_is_valid(backing_id)) + { + ffi_->backing_id = backing_id; + ffi_->passthrough = true; + ffi_->keep_cache = false; + } break; default: - return 0; + break; } - of_->backing_id = FUSE::passthrough_open(fi->fd); - if(of_->backing_id < 0) - return 0; + bool inserted = of.try_emplace(ctx_->nodeid, + backing_id, + fi); - ffi_->backing_id = of_->backing_id; - ffi_->passthrough = true; - ffi_->keep_cache = false; + // Legit... this should never happen. + assert(inserted); + (void)inserted; return 0; } -static -inline -auto -_create_insert_lambda(const fuse_req_ctx_t *ctx_, - const fs::path &fusepath_, - const mode_t mode_, - fuse_file_info_t *ffi_, - int *_rv_) -{ - return - [=](auto &val_) - { - *_rv_ = ::_create_for_insert_lambda(ctx_, - fusepath_, - mode_, - ffi_, - &val_.second); - }; -} - -// This function should never be called? -static -inline -auto -_create_update_lambda() -{ - return - [](const auto &val_) - { - fmt::println(stderr,"CREATE_UPDATE_LAMBDA: THIS SHOULD NOT HAPPEN"); - SysLog::crit("CREATE_UPDATE_LAMBDA: THIS SHOULD NOT HAPPEN"); - abort(); - }; -} - -static -int -_create(const fuse_req_ctx_t *ctx_, - const fs::path &fusepath_, - mode_t mode_, - fuse_file_info_t *ffi_) -{ - int rv; - auto &of = state.open_files; - - rv = -EINVAL; - of.try_emplace_and_visit(ctx_->nodeid, - ::_create_insert_lambda(ctx_,fusepath_,mode_,ffi_,&rv), - ::_create_update_lambda()); - - // Can't abort an emplace_and_visit and can't assume another thread - // hasn't created an entry since this failure so erase only if - // ref_count is default (0). - if(rv < 0) - of.erase_if(ctx_->nodeid, - [](const auto &val_) - { - return (val_.second.ref_count <= 0); - }); - - return rv; -} - int FUSE::create(const fuse_req_ctx_t *ctx_, const char *fusepath_, diff --git a/src/fuse_open.cpp b/src/fuse_open.cpp index 62de6d9a..9a4a8251 100644 --- a/src/fuse_open.cpp +++ b/src/fuse_open.cpp @@ -21,6 +21,7 @@ #include "config.hpp" #include "errno.hpp" #include "fileinfo.hpp" +#include "fs_close.hpp" #include "fs_cow.hpp" #include "fs_fchmod.hpp" #include "fs_lchmod.hpp" @@ -274,169 +275,126 @@ _(const PassthroughIOEnum e_, static int -_open_for_insert_lambda(const fuse_req_ctx_t *ctx_, - const fs::path &fusepath_, - fuse_file_info_t *ffi_, - State::OpenFile *of_) +_open(const fuse_req_ctx_t *ctx_, + const fs::path &fusepath_, + fuse_file_info_t *ffi_) { int rv; - FileInfo *fi; + auto &of = state.open_files; ::_config_to_ffi_flags(cfg,ctx_->pid,ffi_); - if(cfg.cache_writeback) ::_tweak_flags_cache_writeback(&ffi_->flags); + ffi_->noflush = !::_calculate_flush(cfg.flushonclose,ffi_->flags); - ffi_->noflush = !::_calculate_flush(cfg.flushonclose, - ffi_->flags); - - rv = ::_open(cfg.func.open.policy, - cfg.branches, - fusepath_, - ffi_, - cfg.link_cow, - cfg.nfsopenhack); - - if(rv < 0) - return rv; - - fi = FileInfo::from_fh(ffi_->fh); - - of_->ref_count = 1; - of_->fi = fi; - - switch(_(cfg.passthrough_io,ffi_->flags)) + while(true) { - case _(PassthroughIO::ENUM::RO,O_RDONLY): - case _(PassthroughIO::ENUM::WO,O_WRONLY): - case _(PassthroughIO::ENUM::RW,O_RDONLY): - case _(PassthroughIO::ENUM::RW,O_WRONLY): - case _(PassthroughIO::ENUM::RW,O_RDWR): - break; - default: - return 0; - } - - of_->backing_id = FUSE::passthrough_open(fi->fd); - if(of_->backing_id <= 0) - return 0; - - ffi_->backing_id = of_->backing_id; - ffi_->passthrough = true; - ffi_->keep_cache = false; - - return 0; -} - -static -int -_open_for_update_lambda(const fuse_req_ctx_t *ctx_, - const fs::path &fusepath_, - fuse_file_info_t *ffi_, - State::OpenFile *of_) -{ - int rv; - - ::_config_to_ffi_flags(cfg,ctx_->pid,ffi_); - - if(cfg.cache_writeback) - ::_tweak_flags_cache_writeback(&ffi_->flags); + FileInfo *fi = nullptr; + int backing_id = INVALID_BACKING_ID; + + // The ref increment keeps fuse_release.cpp from erasing and + // freeing the entry. + of.visit(ctx_->nodeid, + [&](auto &v_) + { + v_.second.ref_count++; + fi = v_.second.fi; + backing_id = v_.second.backing_id; + }); + + // If the file is already open... + if(fi) + { + rv = ::_open_fd(fi->fd, + &fi->branch, + fusepath_, + ffi_); + + // If we fail to open the already open file we need to treat it + // similarly to fuse_release. + if(rv < 0) + { + fi = nullptr; + backing_id = INVALID_BACKING_ID; + of.erase_if(ctx_->nodeid, + [&](auto &v_) + { + v_.second.ref_count--; + if(v_.second.ref_count > 0) + return false; + + fi = v_.second.fi; + backing_id = v_.second.backing_id; + + return true; + }); + + FUSE::passthrough_close(backing_id); + if(fi) + { + fs::close(fi->fd); + delete fi; + } - ffi_->noflush = !::_calculate_flush(cfg.flushonclose, - ffi_->flags); + return rv; + } - rv = ::_open_fd(of_->fi->fd, - &of_->fi->branch, - fusepath_, - ffi_); - if(rv < 0) - return rv; + if(!fuse_backing_id_is_valid(backing_id)) + return 0; - of_->ref_count++; + ffi_->backing_id = backing_id; + ffi_->passthrough = true; + ffi_->keep_cache = false; - if(of_->backing_id <= 0) - return 0; + return 0; + } - ffi_->backing_id = of_->backing_id; - ffi_->passthrough = true; - ffi_->keep_cache = false; + // Was not open, do first open, try to insert, if someone beat us + // to it in another thread then throw it away and try again. + rv = ::_open(cfg.func.open.policy, + cfg.branches, + fusepath_, + ffi_, + cfg.link_cow, + cfg.nfsopenhack); + if(rv < 0) + return rv; - return rv; -} + fi = FileInfo::from_fh(ffi_->fh); -static -inline -auto -_open_insert_lambda(const fuse_req_ctx_t *ctx_, - const fs::path &fusepath_, - fuse_file_info_t *ffi_, - int *rv_) -{ - return - [=](auto &val_) - { - *rv_ = ::_open_for_insert_lambda(ctx_, - fusepath_, - ffi_, - &val_.second); - }; -} - -static -inline -auto -_open_update_lambda(const fuse_req_ctx_t *ctx_, - const fs::path &fusepath_, - fuse_file_info_t *ffi_, - int *rv_) -{ - return - [=](auto &val_) - { - // For the edge case where insert succeeded but the open failed - // and hadn't been cleaned up yet. There unfortunately is no way - // to abort an insert. - if(val_.second.ref_count <= 0) + switch(_(cfg.passthrough_io,ffi_->flags)) { - *rv_ = ::_open_for_insert_lambda(ctx_, - fusepath_, - ffi_, - &val_.second); - return; + case _(PassthroughIO::ENUM::RO,O_RDONLY): + case _(PassthroughIO::ENUM::WO,O_WRONLY): + case _(PassthroughIO::ENUM::RW,O_RDONLY): + case _(PassthroughIO::ENUM::RW,O_WRONLY): + case _(PassthroughIO::ENUM::RW,O_RDWR): + backing_id = FUSE::passthrough_open(fi->fd); + if(fuse_backing_id_is_valid(backing_id)) + { + ffi_->backing_id = backing_id; + ffi_->passthrough = true; + ffi_->keep_cache = false; + } + break; + default: + break; } - *rv_ = ::_open_for_update_lambda(ctx_, - fusepath_, - ffi_, - &val_.second); - }; -} + bool inserted; -static -int -_open(const fuse_req_ctx_t *ctx_, - const fs::path &fusepath_, - fuse_file_info_t *ffi_) -{ - int rv; - auto &of = state.open_files; - - rv = -EINVAL; - of.try_emplace_and_visit(ctx_->nodeid, - ::_open_insert_lambda(ctx_,fusepath_,ffi_,&rv), - ::_open_update_lambda(ctx_,fusepath_,ffi_,&rv)); + inserted = of.try_emplace(ctx_->nodeid, + backing_id, + fi); + if(inserted) + return 0; - // Can't abort an emplace_and_visit and can't assume another thread - // hasn't created an entry since this failure so erase only if - // ref_count is default (0). - if(rv < 0) - of.erase_if(ctx_->nodeid, - [](auto &val_) - { - return (val_.second.ref_count <= 0); - }); + FUSE::passthrough_close(backing_id); + fs::close(fi->fd); + delete fi; + } - return rv; + return 0; } diff --git a/src/fuse_release.cpp b/src/fuse_release.cpp index 122d384b..c1f938b2 100644 --- a/src/fuse_release.cpp +++ b/src/fuse_release.cpp @@ -26,35 +26,6 @@ #include "fuse.h" -static -constexpr -auto -_erase_if_lambda(FileInfo *fi_, - bool *existed_in_map_) -{ - return - [=](auto &val_) - { - *existed_in_map_ = true; - - if(fi_ != val_.second.fi) - { - fs::close(fi_->fd); - delete fi_; - } - - val_.second.ref_count--; - if(val_.second.ref_count > 0) - return false; - - if(val_.second.backing_id > 0) - FUSE::passthrough_close(val_.second.backing_id); - fs::close(val_.second.fi->fd); - delete val_.second.fi; - - return true; - }; -} static int @@ -62,7 +33,10 @@ _release(const fuse_req_ctx_t *ctx_, FileInfo *fi_, const bool dropcacheonclose_) { - bool existed_in_map; + int backing_id = INVALID_BACKING_ID; + FileInfo *map_fi = nullptr; + bool fi_in_map = false; + auto &of = state.open_files; // according to Feh of nocache calling it once doesn't always work // https://github.com/Feh/nocache @@ -72,34 +46,46 @@ _release(const fuse_req_ctx_t *ctx_, fs::fadvise_dontneed(fi_->fd); } - // Because of course the API doesn't tell you if the key - // existed. Just how many it erased and in this case I only want to - // erase if there are no more open files. - existed_in_map = false; - state.open_files.erase_if(ctx_->nodeid, - ::_erase_if_lambda(fi_,&existed_in_map)); - if(existed_in_map) + u64 erased; + + erased = of.erase_if(ctx_->nodeid, + [&](auto &v_) + { + fi_in_map = (fi_ == v_.second.fi); + + v_.second.ref_count--; + if(v_.second.ref_count > 0) + return false; + + backing_id = v_.second.backing_id; + map_fi = v_.second.fi; + + return true; + }); + if(not fi_in_map) + { + fs::close(fi_->fd); + delete fi_; + } + + if(not erased) return 0; - fs::close(fi_->fd); - delete fi_; + FUSE::passthrough_close(backing_id); + if(map_fi) + { + fs::close(map_fi->fd); + delete map_fi; + } return 0; } -static int -_release(const fuse_req_ctx_t *ctx_, - const fuse_file_info_t *ffi_) +FUSE::release(const fuse_req_ctx_t *ctx_, + const fuse_file_info_t *ffi_) { FileInfo *fi = FileInfo::from_fh(ffi_->fh); return ::_release(ctx_,fi,cfg.dropcacheonclose); } - -int -FUSE::release(const fuse_req_ctx_t *ctx_, - const fuse_file_info_t *ffi_) -{ - return ::_release(ctx_,ffi_); -} diff --git a/src/state.hpp b/src/state.hpp index 6fb8e504..28558874 100644 --- a/src/state.hpp +++ b/src/state.hpp @@ -4,13 +4,13 @@ #include "fileinfo.hpp" +#include "fuse.h" + #include #include #include -constexpr int INVALID_BACKING_ID = -1; - class State { public: @@ -26,6 +26,14 @@ public: { } + OpenFile(const int backing_id_, + FileInfo * const fi_) + : ref_count(1), + backing_id(backing_id_), + fi(fi_) + { + } + int ref_count; int backing_id; FileInfo *fi; diff --git a/tests/TEST_io_passthrough_create_open b/tests/TEST_io_passthrough_create_open new file mode 100755 index 00000000..5943523c --- /dev/null +++ b/tests/TEST_io_passthrough_create_open @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +import os +import sys +import tempfile +import time + +# Test create + write + read (passthrough.io=rw) +(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1]) + +test_data = b"passthrough io test data for create\n" * 100 + +# Write to newly created file +bytes_written = os.write(fd, test_data) +if bytes_written != len(test_data): + print("create write failed: expected {} bytes, wrote {}".format(len(test_data), bytes_written)) + sys.exit(1) + +# Seek and read back +os.lseek(fd, 0, os.SEEK_SET) +read_data = os.read(fd, len(test_data)) +if read_data != test_data: + print("create read failed: data mismatch") + sys.exit(1) + +os.close(fd) + +# Test open existing file + write + read +fd = os.open(filepath, os.O_RDWR) + +# Read existing data +os.lseek(fd, 0, os.SEEK_SET) +read_data = os.read(fd, len(test_data)) +if read_data != test_data: + print("open read failed: data mismatch") + sys.exit(1) + +# Write more data at end +os.lseek(fd, 0, os.SEEK_END) +more_data = b"additional passthrough data for open\n" * 50 +bytes_written = os.write(fd, more_data) +if bytes_written != len(more_data): + print("open write failed: expected {} bytes, wrote {}".format(len(more_data), bytes_written)) + sys.exit(1) + +# Verify all data +os.lseek(fd, 0, os.SEEK_SET) +all_data = os.read(fd, len(test_data) + len(more_data)) +if all_data != test_data + more_data: + print("open final read failed: data mismatch") + sys.exit(1) + +# Test multiple opens of same file (single backing_id feature) +# When a file is already open with passthrough, subsequent opens +# must reuse the same backing_id rather than creating new ones +fd2 = os.open(filepath, os.O_RDWR) + +# Read from second fd - should see same data +os.lseek(fd2, 0, os.SEEK_SET) +read_data2 = os.read(fd2, len(test_data) + len(more_data)) +if read_data2 != test_data + more_data: + print("second open read failed: data mismatch") + os.close(fd) + os.close(fd2) + sys.exit(1) + +# Write from second fd +os.lseek(fd2, 0, os.SEEK_END) +extra_data = b"data from second file descriptor\n" * 25 +bytes_written = os.write(fd2, extra_data) +if bytes_written != len(extra_data): + print("second open write failed: expected {} bytes, wrote {}".format(len(extra_data), bytes_written)) + os.close(fd) + os.close(fd2) + sys.exit(1) + +# Verify write is visible from first fd (shared backing) +os.lseek(fd, 0, os.SEEK_SET) +combined_data = os.read(fd, len(test_data) + len(more_data) + len(extra_data)) +expected_data = test_data + more_data + extra_data +if combined_data != expected_data: + print("cross-fd read failed: data mismatch (writes from fd2 not visible on fd)") + os.close(fd) + os.close(fd2) + sys.exit(1) + +# Open a third fd while others are still open +fd3 = os.open(filepath, os.O_RDONLY) +os.lseek(fd3, 0, os.SEEK_SET) +read_data3 = os.read(fd3, len(expected_data)) +if read_data3 != expected_data: + print("third open read failed: data mismatch") + os.close(fd) + os.close(fd2) + os.close(fd3) + sys.exit(1) + +os.close(fd3) +os.close(fd2) +os.close(fd) + +# Cleanup +os.unlink(filepath) diff --git a/tests/TEST_io_passthrough_open_race b/tests/TEST_io_passthrough_open_race new file mode 100755 index 00000000..5a0c6a14 --- /dev/null +++ b/tests/TEST_io_passthrough_open_race @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +import os +import sys +import tempfile +import threading +import time + +NUM_THREADS = 50 +TEST_DATA = b"race condition test data\n" + +def open_and_operate(filepath, barrier, results, index): + """ + Wait at barrier, then open file, write, read, and store result. + """ + try: + # Wait for all threads to be ready + barrier.wait() + + # All threads try to open simultaneously + fd = os.open(filepath, os.O_RDWR) + + # Write thread-specific data at a unique offset + offset = index * len(TEST_DATA) + os.lseek(fd, offset, os.SEEK_SET) + bytes_written = os.write(fd, TEST_DATA) + + if bytes_written != len(TEST_DATA): + results[index] = ("write_error", "expected {} bytes, wrote {}".format(len(TEST_DATA), bytes_written)) + os.close(fd) + return + + # Read back what we wrote + os.lseek(fd, offset, os.SEEK_SET) + read_data = os.read(fd, len(TEST_DATA)) + + if read_data != TEST_DATA: + results[index] = ("read_error", "data mismatch at offset {}".format(offset)) + os.close(fd) + return + + results[index] = ("success", fd) + + except Exception as e: + results[index] = ("exception", str(e)) + +# Create test file with enough space for all threads +(fd, filepath) = tempfile.mkstemp(dir=sys.argv[1]) + +# Pre-allocate file to avoid issues with concurrent writes extending file +total_size = NUM_THREADS * len(TEST_DATA) +os.ftruncate(fd, total_size) +os.close(fd) + +# Set up synchronization +barrier = threading.Barrier(NUM_THREADS) +results = [None] * NUM_THREADS +threads = [] + +# Create and start all threads +for i in range(NUM_THREADS): + t = threading.Thread(target=open_and_operate, args=(filepath, barrier, results, i)) + threads.append(t) + +for t in threads: + t.start() + +for t in threads: + t.join() + +# Check results +failed = False +fds_to_close = [] + +for i, result in enumerate(results): + if result is None: + print("thread {} returned no result".format(i)) + failed = True + elif result[0] == "success": + fds_to_close.append(result[1]) + else: + print("thread {} failed: {} - {}".format(i, result[0], result[1])) + failed = True + +if failed: + for fd in fds_to_close: + os.close(fd) + os.unlink(filepath) + sys.exit(1) + +# Verify all data is consistent by reading through one fd +if fds_to_close: + verify_fd = fds_to_close[0] + os.lseek(verify_fd, 0, os.SEEK_SET) + all_data = os.read(verify_fd, total_size) + + expected_data = TEST_DATA * NUM_THREADS + if all_data != expected_data: + print("final verification failed: data mismatch") + print("expected {} bytes, got {} bytes".format(len(expected_data), len(all_data))) + for fd in fds_to_close: + os.close(fd) + os.unlink(filepath) + sys.exit(1) + +# Test cross-fd visibility: write from one fd, read from another +if len(fds_to_close) >= 2: + fd_a = fds_to_close[0] + fd_b = fds_to_close[1] + + # Write new data from fd_a + new_data = b"cross-fd visibility test\n" + os.lseek(fd_a, 0, os.SEEK_SET) + os.write(fd_a, new_data) + + # Read from fd_b - should see the new data + os.lseek(fd_b, 0, os.SEEK_SET) + read_back = os.read(fd_b, len(new_data)) + + if read_back != new_data: + print("cross-fd visibility failed: write from fd_a not visible on fd_b") + for fd in fds_to_close: + os.close(fd) + os.unlink(filepath) + sys.exit(1) + +# Close all file descriptors +for fd in fds_to_close: + os.close(fd) + +# Cleanup +os.unlink(filepath) diff --git a/tests/tests.cpp b/tests/tests.cpp index 49e01459..eb1d75e5 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -123,12 +123,11 @@ test_str_stuff() void test_config_branches() { - uint64_t minfreespace; - Branches b(minfreespace); + Branches b; Branches::Ptr bcp0; Branches::Ptr bcp1; - minfreespace = 1234; + b.minfreespace = 1234; TEST_CHECK(b->minfreespace() == 1234); TEST_CHECK(b.to_string() == "");