Commit Graph
1542 Commits
Author SHA1 Message Date
Antonio S. MusumeciandClaude Opus 4.6 db784e00a2 Document detached-thread lifetime safety in autoscale self-exit
After _try_remove_self_if_above detaches and erases the thread, the
destructor's snapshot will not include it — so pthread_join is never
called on it, and the ThreadPool object can be destroyed while this
thread is still returning.

This is safe because the thread only accesses local variables after
detach (name is a std::string copy made at function entry), but the
reasoning is subtle. Add an explicit safety comment explaining why
there is no use-after-free.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:42:02 -04:00
Antonio S. MusumeciandClaude Opus 4.6 96824eab31 Document _tl_should_exit protocol in worker loops
The _tl_should_exit mechanism in start_routine (and
start_routine_autoscale) is correct but the control flow is non-obvious:
_remove_thread_scaling_locked enqueues a lambda that sets the
thread-local flag and calls _remove_self (detach + erase), then the
worker checks the flag after running the func and exits.

Replace the terse one-line comments with an explanation of the full
protocol so future readers don't mistake it for dead code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:40:47 -04:00
Antonio S. MusumeciandClaude Opus 4.6 2333f6078a Reset hill-climb direction after idle period
When throughput dropped below 0.5 (idle), decline_streak was reset but
direction was preserved. When work resumed, the monitor continued in
whatever direction it held before the idle period — often a shrink
direction that was correct for the previous workload but wrong for the
new burst. This could cause the pool to shrink immediately when it
should be growing.

Reset direction to +1 (grow) when entering the idle branch so the pool
starts the next active period biased toward growth, which is the safer
default under new load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:32:47 -04:00
Antonio S. MusumeciandClaude Opus 4.6 b150f2c685 Retry nanosleep on EINTR in monitor thread
nanosleep returns -1 with errno EINTR when interrupted by a signal.
The remaining time was discarded (second arg was NULL), causing the
monitor to wake early and sample a shortened interval. This produced
artificially low throughput numbers that corrupted the EMA and could
trigger unnecessary scale-down decisions.

Loop on EINTR using the remaining time output parameter so the full
sample interval is always honored. Also check _stop on each retry
so the monitor exits promptly during shutdown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:30:13 -04:00
Antonio S. MusumeciandClaude Opus 4.6 beb6961769 Fix _maybe_grow_on_pressure burning cooldown at max threads
The CAS on _last_scale_time_usecs was performed before checking whether
the pool was already at max_threads. When the pool was at capacity, the
cooldown window was consumed but no thread was added. This suppressed
legitimate scaling attempts from the monitor thread for the entire
cooldown period.

Move the CAS after the max-threads check and the _scaling_mutex
acquisition so the cooldown is only claimed when a thread will actually
be added. The mutex serializes concurrent callers, and the CAS still
deduplicates concurrent pressure-grow attempts that pass the check
simultaneously.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:28:28 -04:00
Antonio S. MusumeciandClaude Opus 4.6 111bedf2c9 Clamp initial thread count to ScalingConfig bounds in constructor
When auto-scaling is enabled, the constructor did not enforce that the
initial thread_count falls within [min_threads, max_threads]. Starting
with fewer threads than min_threads meant the monitor had to
immediately grow the pool, and starting above max_threads created
threads that the monitor would then immediately try to shrink.

Clamp the count before spawning so the pool starts within its
configured operating range.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 22:00:25 -04:00
Antonio S. MusumeciandClaude Opus 4.6 d166418675 Replace usleep with nanosleep in monitor thread
usleep is POSIX-obsolescent and its behavior on signal interruption is
under-specified. Replace with nanosleep which is the current POSIX
standard and has well-defined EINTR semantics. Pre-compute the
timespec once outside the loop since the interval is constant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:58:43 -04:00
Antonio S. MusumeciandClaude Opus 4.6 f8ac649e6f Rename _add/_remove_thread_locked to _scaling_locked
The _locked suffix conventionally means the caller already holds the
lock being named. These functions expect _scaling_mutex to be held,
but the old name was ambiguous — it looked like _threads_mutex.

Rename to _add_thread_scaling_locked and _remove_thread_scaling_locked
to make the precondition explicit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:56:26 -04:00
Antonio S. MusumeciandClaude Opus 4.6 0bef28d154 Bound _remove_thread_locked wait to avoid indefinite _scaling_mutex hold
_remove_thread_locked enqueues a self-exit task and blocks on the
future while holding _scaling_mutex. If all workers are busy with
long-running tasks, this blocks indefinitely — starving the monitor
thread (and any concurrent add_thread/set_threads) since they all
contend on _scaling_mutex.

Replace the unbounded wait with a 5-second timed wait. On timeout,
return -EBUSY so callers can retry or report the condition. The
enqueued self-exit task will still be picked up eventually and the
thread will remove itself when a worker becomes free.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:51:50 -04:00
Antonio S. MusumeciandClaude Opus 4.6 8baf274cde Replace two-statement mutex_lockguard macro with single RAII declaration
The old macro expanded to two statements (mutex_lock + DEFER unlock).
In an unbraced if/else only the lock would be conditional while the
DEFER always executed — a latent double-unlock / unbalanced-unlock bug.

Add ScopedMutexLock to both debug and ndebug headers:
 - debug: stores __FILE__/__func__/__LINE__ from the call site and
   passes them to _mutex_lock/_mutex_unlock for diagnostics
 - ndebug: lightweight wrapper around mutex_lock/mutex_unlock

Redefine mutex_lockguard to create a single uniquely-named
ScopedMutexLock variable (via __COUNTER__), making it safe in any
statement context. Remove the now-unused scope_guard include.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:42:24 -04:00
Antonio S. MusumeciandClaude Opus 4.6 13fc1b27df Enforce cooldown_usecs in monitor hill-climb loop
cooldown_usecs was only checked in _maybe_grow_on_pressure (the
fast-path enqueue grow). The monitor thread scaled every
sample_interval_usecs unconditionally, so a recent fast-path grow
could be immediately followed by a monitor grow, defeating the
cooldown.

Check _last_scale_time_usecs against cooldown_usecs before applying
any monitor scaling decision, making the cooldown global across both
scaling paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:22:47 -04:00
Antonio S. MusumeciandClaude Opus 4.6 2aa2311a55 Add EMA smoothing and decline-streak to hill-climb monitor
A single noisy throughput sample would immediately flip the scaling
direction, causing the monitor to oscillate under bursty workloads —
continuously adding and removing threads every sample interval.

Replace raw throughput comparison with an exponential moving average
(alpha=0.3) and require two consecutive declining samples before
reversing direction. This filters out transient dips and keeps the
hill-climber from thrashing.

Also use a 2-sample warmup period to seed the EMA before making any
scaling decisions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:20:42 -04:00
Antonio S. MusumeciandClaude Opus 4.6 055a2d4abe Propagate errors from set_threads instead of ignoring them
set_threads silently ignored failures from _add_thread_locked (e.g.
pthread_create failure) and _remove_thread_locked (e.g. at min_threads).
The caller had no way to know the pool was undersized or that shrinking
stopped early.

Stop on the first error and return it so callers can detect partial
completion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:05:32 -04:00
Antonio S. MusumeciandClaude Opus 4.6 82e7980ec9 Fix TOCTOU race in _maybe_grow_on_pressure
Multiple concurrent enqueue_work_autoscale callers could all pass the
cooldown check and each add a thread, overshooting max_threads and
bypassing the intended rate limit.

Use compare_exchange_strong on _last_scale_time_usecs so only one
caller wins the cooldown window. Then hold _scaling_mutex for the
thread-count check and _add_thread_locked call so the grow is atomic
with respect to other scaling operations (monitor, set_threads).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:04:37 -04:00
Antonio S. MusumeciandClaude Opus 4.6 38fee4dd5f Fix use-after-free in start_routine_autoscale idle self-exit
After _try_remove_self_if_above detaches the thread, the destructor
will not join it. If the destructor runs before the detached thread
reaches the syslog call, btp->_name is a dangling reference.

Copy _name into a local string at thread start so the syslog after
self-removal uses stack-local data that outlives the detach.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:59:31 -04:00
Antonio S. MusumeciandClaude Opus 4.6 9222a4ff62 Move _tp_now_usecs into ThreadPool as private static _now_usecs
The file-scope function _tp_now_usecs had a leading underscore
followed by a lowercase letter, which is reserved at file scope
by the C++ standard. Move it to a private static member function
ThreadPool::_now_usecs where leading underscores on member names
are permitted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:46:42 -04:00
Antonio S. MusumeciandClaude Opus 4.6 e87acf0433 Add sane defaults to ScalingConfig members
ScalingConfig was value-initialized to all zeros when no config
is provided. While the non-autoscale path gates all access, a
zero idle_threshold_usecs would cause try_wait_dequeue_for to
return immediately if the autoscale path were ever reached by
mistake.

Add default member initializers: min_threads=2, max_threads=64,
sample_interval=100ms, cooldown=200ms, idle_threshold=5s. These
match the values recommended in the scaling policy design and
provide safe behavior even if a caller constructs ScalingConfig
without setting all fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:43:21 -04:00
Antonio S. MusumeciandClaude Opus 4.6 73b6bb3cdf Move _tasks_enqueued increment to after successful enqueue
The counter was incremented before the enqueue call, which can
block indefinitely for bounded queues. If tasks_pending() is used
as a drain-complete signal (pending == 0), the pre-increment
would overcount while a producer is blocked waiting for a slot.

Move the increment to after the enqueue returns in all paths:
enqueue_work, enqueue_work_autoscale, and enqueue_task. The
try_enqueue variants already incremented after success.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:39:32 -04:00
Antonio S. MusumeciandClaude Opus 4.6 3bcf66cdaa Fix monitor and idle-exit racing past min_threads
The hill-climb monitor and idle self-exit paths could both shrink
the pool past min_threads. The monitor read thread count, released
the lock, then called remove_thread - but between those steps an
idle worker could also self-exit, dropping below the threshold.

Three changes:
- _remove_thread_locked now checks min_threads (from ScalingConfig)
  under the same lock as the size check, refusing removal if at or
  below the bound.
- Idle self-exit replaced with _try_remove_self_if_above which
  atomically checks thread count and erases under a single lock
  acquisition, closing the TOCTOU window.
- The previous _remove_self is retained for the non-scaling paths
  (poison pill exit during set_threads/remove_thread).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:36:48 -04:00
Antonio S. MusumeciandClaude Opus 4.6 6a52d7b1a2 Fix semaphore drift from enqueue_unbounded in BoundedQueue
enqueue_unbounded bypasses the semaphore so control messages
(poison pills, thread removal tasks) can never block on a full
queue. But wait_dequeue always signals a slot after consuming,
so each unbounded item that gets dequeued inflated the semaphore
by +1, permanently raising the effective max queue depth.

Fix by calling tryWait() in enqueue_unbounded before enqueueing.
When the queue has room (common case), this consumes a slot and
the dequeue-side signal restores balance perfectly. When the queue
is genuinely full (tryWait fails), the item still enqueues but
temporarily exceeds max_depth by one until consumed - a transient
overshoot bounded by control messages in flight.

A CAS-based approach that absorbed extra signals on the dequeue
side was also considered but rejected: it can starve blocked
bounded producers by swallowing the signal they are waiting for.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:21:36 -04:00
Antonio S. MusumeciandClaude Opus 4.6 cb6f444e45 Fix double std::forward in enqueue_work_autoscale
The try-enqueue and fallback block-enqueue both called
std::forward<FuncType>(func_) on the same parameter. If
try_enqueue's internals ever change to move from the argument
before failing, the second forward would operate on a moved-from
object.

Fix by materializing into a Func up front and using std::move
for both attempts. Also extract the duplicated scaling logic into
_maybe_grow_on_pressure(), eliminating the copy-paste between the
PToken and non-PToken overloads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:39:18 -04:00
Antonio S. MusumeciandClaude Opus 4.6 c0b4ea3fae Fix destructor race with _remove_self during shutdown
The destructor and autoscale idle-exit path could race: a worker
calling _remove_self would erase itself from _threads and detach
while the destructor was iterating _threads to enqueue poison pills
and join. This could cause too few poison pills (deadlocking
remaining workers) or pthread_join on a detached thread (UB).

Two changes:
- _remove_self now checks _stop and returns immediately during
  shutdown, ensuring no thread detaches after destruction begins.
- Destructor snapshots _threads under the mutex before enqueueing
  poison pills and joining, so it operates on a stable copy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:35:35 -04:00
Antonio S. Musumeci 9ae145b7e9 checkpoint 2026-04-09 19:31:47 -04:00
Antonio S. Musumeci 9dc461c96d checkpoint 2026-04-09 19:13:58 -04:00
Antonio SJ Musumeci 3905355522 Detect OS for release-sample make target 2026-04-08 20:48:02 -05:00
Antonio SJ Musumeci 9e2e1afafa Update docs on limiting drive spinup 2026-04-06 14:01:20 -05:00
Antonio SJ Musumeci 6d218fa87f Misc updates and fixes 2026-04-04 02:46:24 -05:00
Antonio SJ Musumeci 3b10406f95 Add more details to project comparisons 2026-04-04 02:17:06 -05:00
Antonio SJ Musumeci f2c8e5d9eb Add FUSE request_timeout
Not yet hooked up to config parser to set as need to evaluate usage.
2026-04-01 23:20:05 -05:00
Antonio SJ Musumeci b581c5d5e7 Add support for FUSE_COPY_FILE_RANGE_64 2026-04-01 23:20:05 -05:00
Antonio SJ Musumeci 1ebc8cfa5f Enable statx with file handle 2026-03-31 19:53:17 -04:00
Antonio SJ Musumeci acfa797503 Misc formatting changes 2026-03-31 19:53:17 -04:00
Antonio SJ Musumeci 7fc20457ca Rework open file resource management 2026-03-31 19:53:17 -04:00
Antonio SJ Musumeci ecee84b323 Improve proj comparisons + new SnapRAID details 2026-03-31 19:51:28 -04:00
Antonio SJ Musumeci b8776d205d Update docs; add nofs, policyfs and greyhole 2026-03-27 22:54:39 -05:00
Antonio SJ Musumeci 06433a0548 Fallback to fusermount3 where available
Also general cleanup of mount_generic.
2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci f4b602340b Add more integration tests 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci a78aef36e0 Additional check for ENOTCONN on mount 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 0d05523dfd Move maintainence thread func take u64 rather than int
signed rollover is UB
2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 0cfab60f37 replace raw C-strings with std::string in converted libfuse files
util/mount.mergerfs.cpp:
- Remove xstrdup/xrealloc wrappers entirely
- Convert type, source, options, command, setuid to std::string
- Replace add_arg() pointer-rewriting with shell_quote() returning std::string
- Replace add_option() with simple std::string += concatenation
- Fix const_cast UB on string literal assignment

lib/helper.cpp:
- Convert add_default_subtype local subtype_opt: malloc+snprintf+free -> std::string construction

lib/mount_generic.h:
- Convert fuse_mount_sys locals source/type: malloc+strcpy+strcat -> std::string
- Remove 4 unnecessary free() calls (RAII cleanup)
- .c_str() at mount() and fuse_mnt_add_mount() boundaries

lib/mount_util.h:
- Rewrite fuse_mnt_resolve_path internals using std::string: rfind/substr
  instead of manual pointer arithmetic, strrchr, strlen chains
2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 21966ade97 Convert vendored/libfuse C sources to C++17
Convert all C source files in vendored/libfuse/ to C++17:
- lib/crc32b.c    -> crc32b.cpp  (const void* cast)
- lib/fuse_opt.c -> fuse_opt.cpp (designated init, void* arithmetic)
- lib/fuse_signals.c -> fuse_signals.cpp
- lib/helper.c   -> helper.cpp  (void* data cast)
- lib/mount.c    -> mount.cpp  (thin wrapper)
- lib/mount_generic.c -> mount_generic.h (static inline, #pragma once)
- lib/mount_bsd.c -> mount_bsd.h    (static inline, #pragma once)
- lib/mount_util.c -> mount_util.h   (static inline, #pragma once)
- util/fusermount.c -> fusermount.cpp
- util/mount.mergerfs.c -> mount.mergerfs.cpp

Key changes:
- Replace C99 designated initializer in fuse_opt_parse with field-by-field init
- Fix void* pointer arithmetic via char* base + offset pattern
- All C-style (T*)voidptr casts replaced with static_cast/reinterpret_cast
- Remove redundant #define _GNU_SOURCE (already in FUSE_FLAGS)
- Wrap __clone2 declaration in extern "C" for ia64 path
- Update Makefile: empty SRC_C, build utils with $(CXX) instead of $(CC)
- Remove stale .c files now replaced by .cpp/.h counterparts

All binaries build cleanly: mergerfs, mergerfs-fusermount, mount.mergerfs
2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 96eb51eecb Move fuse_session to C++ and make exit flag atomic 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 881147daa2 Fix null ptr handling issue in ioctl processing 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 0f1a8e9b69 Fix error condition in fs_copyfile 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 08f9cc6932 Fix readlink implementation, add more tests 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 3099b15db1 More aggressive error handing with pagesize queries 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 07b1e6bdba Auto-commit fuse_msgbuf.cpp 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 7763c7384a Use fatal::abort() rather than assert in places 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci 61365d226d Move scope_guard to vendored/ 2026-03-24 21:13:34 -04:00
Antonio SJ Musumeci f33e78bc24 Cleanup procfs and abort on critical issues 2026-03-24 21:13:34 -04:00