Files
libfuse/test/test_loop_config.c
Amir Goldstein dfe8a0f8c9 fuse_loop_cfg_convert: restore v1 max_idle_threads semantics
Commit 30a126c5 ("API update for fuse_loop_config additions") introduced
fuse_loop_cfg_convert() to translate legacy fuse_loop_config_v1 structs
to the new v2 API, but only converted max_idle_threads — it did not set
max_threads.

In the v1 API, max_idle_threads was the effective pool cap: the thread
pool would grow on demand and threads exceeding max_idle_threads were
destroyed after processing each request.  A filesystem that configured
max_idle_threads=100 expected up to 100 threads to be available.

After the conversion, max_threads retains its default of 10, silently
capping the pool at 10 workers regardless of max_idle_threads.  A busy
filesystem with all 10 threads processing long-running requests has no
thread available to receive the next kernel request, causing it to hang.

Fix fuse_loop_cfg_convert() to call fuse_loop_cfg_set_max_threads() with
the same value as max_idle_threads before calling fuse_loop_cfg_set_idle_threads(),
preserving the original v1 semantics.

Also add warnings to both setters when max_idle_threads and max_threads
are set in an inconsistent order (max_idle > max_threads).  Reorder the
two setter calls in helper.c so that max_threads is set before
max_idle_threads, preventing a spurious warning when both are supplied as
consistent command-line options.  Add a unit test to document and verify
the correct behaviour.

Signed-off-by: Amir Goldstein <amir73il@gmail.com>
2026-05-01 21:44:03 +02:00

289 lines
7.8 KiB
C

/*
* Unit tests for fuse_loop_cfg_set_idle_threads() /
* fuse_loop_cfg_set_max_threads() interaction.
*
* No FUSE mount is needed; the tests exercise the setter logic directly.
*/
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
#include "fuse_i.h" /* internal struct fuse_loop_config (v2) */
#include "fuse_lowlevel.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
/* ------------------------------------------------------------------ */
/* Log capture */
/* ------------------------------------------------------------------ */
static char last_log_msg[512];
static enum fuse_log_level last_log_level;
static void test_log_handler(enum fuse_log_level level, const char *fmt,
va_list ap)
{
last_log_level = level;
vsnprintf(last_log_msg, sizeof(last_log_msg), fmt, ap);
}
static void clear_log(void)
{
last_log_msg[0] = '\0';
last_log_level = FUSE_LOG_DEBUG;
}
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
#define PASS(name) do { printf("PASS: %s\n", name); } while (0)
#define FAIL(name, fmt, ...) \
do { fprintf(stderr, "FAIL: %s — " fmt "\n", name, ##__VA_ARGS__); \
exit(1); } while (0)
#define CHECK(name, cond) \
do { if (!(cond)) FAIL(name, "assertion failed: %s", #cond); } while (0)
/* ------------------------------------------------------------------ */
/* Tests */
/* ------------------------------------------------------------------ */
/*
* Normal case: set_max_threads with no idle constraint → value accepted as-is.
*/
static void test_max_threads_normal(void)
{
const char *name = "max_threads_normal";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
clear_log();
fuse_loop_cfg_set_max_threads(cfg, 20);
CHECK(name, cfg->max_threads == 20);
CHECK(name, last_log_msg[0] == '\0'); /* no warning */
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/*
* idle disabled (default -1): set_max_threads to any value is accepted.
*/
static void test_max_threads_idle_disabled(void)
{
const char *name = "max_threads_idle_disabled";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
/* default max_idle_threads == -1 (disabled) */
clear_log();
fuse_loop_cfg_set_max_threads(cfg, 5);
CHECK(name, cfg->max_threads == 5);
CHECK(name, last_log_msg[0] == '\0'); /* no warning */
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/*
* idle == 0 (disabled): set_max_threads to a small value is accepted.
*/
static void test_max_threads_idle_zero(void)
{
const char *name = "max_threads_idle_zero";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
fuse_loop_cfg_set_idle_threads(cfg, 0);
clear_log();
fuse_loop_cfg_set_max_threads(cfg, 5);
CHECK(name, cfg->max_threads == 5);
CHECK(name, last_log_msg[0] == '\0'); /* no warning */
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/*
* Warning: set_idle(100) then set_max(5) → value is set but a warning is logged.
* The resulting config is technically invalid (idle reaping can never trigger)
* but we honour the caller's explicit request.
*/
static void test_warn_max_below_idle(void)
{
const char *name = "warn_max_below_idle";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
fuse_loop_cfg_set_idle_threads(cfg, 100);
clear_log();
fuse_loop_cfg_set_max_threads(cfg, 5);
/* value is set as requested */
CHECK(name, cfg->max_threads == 5);
CHECK(name, cfg->max_idle_threads == 100);
/* a warning must have been logged */
CHECK(name, last_log_level == FUSE_LOG_WARNING);
CHECK(name, strstr(last_log_msg, "max_threads") != NULL);
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/*
* set_idle(100) then set_max(100) → exact equality is valid, no warning.
*/
static void test_max_equals_idle(void)
{
const char *name = "max_equals_idle";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
fuse_loop_cfg_set_idle_threads(cfg, 100);
clear_log();
fuse_loop_cfg_set_max_threads(cfg, 100);
CHECK(name, cfg->max_threads == 100);
CHECK(name, last_log_msg[0] == '\0'); /* no warning */
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/*
* set_idle(100) then set_max(200) → valid, no warning.
*/
static void test_max_above_idle(void)
{
const char *name = "max_above_idle";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
fuse_loop_cfg_set_idle_threads(cfg, 100);
clear_log();
fuse_loop_cfg_set_max_threads(cfg, 200);
CHECK(name, cfg->max_threads == 200);
CHECK(name, cfg->max_idle_threads == 100);
CHECK(name, last_log_msg[0] == '\0'); /* no warning */
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/*
* helper.c order: set_max(200) first, then set_idle(100).
* Neither setter should warn when called in this order.
*/
static void test_helper_order_no_warning(void)
{
const char *name = "helper_order_no_warning";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
clear_log();
fuse_loop_cfg_set_max_threads(cfg, 200);
fuse_loop_cfg_set_idle_threads(cfg, 100);
CHECK(name, cfg->max_threads == 200);
CHECK(name, cfg->max_idle_threads == 100);
CHECK(name, last_log_msg[0] == '\0');
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/*
* Reverse order: set_max(10) first, then set_idle(100).
* Value is set but a warning is logged.
*/
static void test_warn_idle_above_max(void)
{
const char *name = "warn_idle_above_max";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
fuse_loop_cfg_set_max_threads(cfg, 10);
clear_log();
fuse_loop_cfg_set_idle_threads(cfg, 100);
CHECK(name, cfg->max_threads == 10);
CHECK(name, cfg->max_idle_threads == 100);
CHECK(name, last_log_level == FUSE_LOG_WARNING);
CHECK(name, strstr(last_log_msg, "max_idle_threads") != NULL);
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/*
* v1 legacy API: fuse_loop_cfg_convert with max_idle_threads=100.
* In v1 semantics, max_idle_threads WAS the effective pool cap.
* convert() must set max_threads = max_idle_threads to preserve that.
*/
static void test_v1_convert_sets_max_threads(void)
{
const char *name = "v1_convert_sets_max_threads";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
struct fuse_loop_config_v1 v1 = {
.max_idle_threads = 100,
.clone_fd = 0,
};
clear_log();
fuse_loop_cfg_convert(cfg, &v1);
/* both must be 100 — v1 max_idle_threads was the effective cap */
CHECK(name, cfg->max_idle_threads == 100);
CHECK(name, cfg->max_threads == 100);
/* convert sets max_threads first, so no ordering warning */
CHECK(name, last_log_msg[0] == '\0');
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/*
* v1 legacy API: default max_idle_threads=10 (old libfuse default).
* convert() → max_idle=10, max_threads=10.
*/
static void test_v1_convert_default(void)
{
const char *name = "v1_convert_default";
struct fuse_loop_config *cfg = fuse_loop_cfg_create();
struct fuse_loop_config_v1 v1 = {
.max_idle_threads = 10, /* old libfuse default */
.clone_fd = 0,
};
clear_log();
fuse_loop_cfg_convert(cfg, &v1);
CHECK(name, cfg->max_idle_threads == 10);
CHECK(name, cfg->max_threads == 10);
CHECK(name, last_log_msg[0] == '\0');
fuse_loop_cfg_destroy(cfg);
PASS(name);
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
int main(void)
{
fuse_set_log_func(test_log_handler);
test_max_threads_normal();
test_max_threads_idle_disabled();
test_max_threads_idle_zero();
test_warn_max_below_idle();
test_max_equals_idle();
test_max_above_idle();
test_helper_order_no_warning();
test_warn_idle_above_max();
test_v1_convert_sets_max_threads();
test_v1_convert_default();
printf("All loop_config tests passed.\n");
return 0;
}