diff --git a/src/foundation/mem.c b/src/foundation/mem.c index 65f57b1a8..61d13fe91 100644 --- a/src/foundation/mem.c +++ b/src/foundation/mem.c @@ -248,9 +248,19 @@ cbm_mem_budget_t cbm_mem_resolve_budget(size_t total_ram, double ram_fraction, cbm_mem_budget_t cbm_mem_resolve_budget_capped(size_t total_ram, double ram_fraction, const char *budget_mb, size_t hard_cap_bytes) { cbm_mem_budget_t result = cbm_mem_resolve_budget(total_ram, ram_fraction, budget_mb); + /* The parent already divided the aggregate budget (env override or + * ram_fraction) across job slots. That per-slot share is the hard cap: + * N workers × a per-worker absolute override would oversubscribe the host + * (#1654). A lower explicit value still wins. Keep CBM_MEM_BUDGET_MB as + * the source when the env discriminator fired so the ceiling is visible + * as the user's aggregate, not as a silent daemon_worker_cap rewrite. */ if (hard_cap_bytes > 0 && (result.budget == 0 || result.budget > hard_cap_bytes)) { + bool explicit_override = + result.source != NULL && strcmp(result.source, "CBM_MEM_BUDGET_MB") == 0; result.budget = hard_cap_bytes; - result.source = "daemon_worker_cap"; + if (!explicit_override) { + result.source = "daemon_worker_cap"; + } result.hard_capped = true; } return result; diff --git a/src/foundation/mem.h b/src/foundation/mem.h index a2ae37376..bbd9d769f 100644 --- a/src/foundation/mem.h +++ b/src/foundation/mem.h @@ -31,7 +31,7 @@ void cbm_mem_init_with_cap(double ram_fraction, size_t hard_cap_bytes); * the caller never re-parses the env string. */ typedef struct { size_t budget; /* resolved budget in bytes */ - const char *source; /* log token: "ram_fraction" | "CBM_MEM_BUDGET_MB" */ + const char *source; /* "ram_fraction" | "CBM_MEM_BUDGET_MB" | "daemon_worker_cap" */ bool clamped; /* override was valid but exceeded total_ram → clamped down */ bool invalid; /* override was present but unparseable / out-of-range / ≤0 */ bool hard_capped; /* internal worker hard cap reduced the resolved budget */ diff --git a/tests/test_mem.c b/tests/test_mem.c index 31c5a686f..1ded83dad 100644 --- a/tests/test_mem.c +++ b/tests/test_mem.c @@ -516,6 +516,11 @@ TEST(resolve_budget_override_when_total_unknown) { PASS(); } +/* CBM_MEM_BUDGET_MB is an aggregate ceiling the parent divides across job + * slots. A lower explicit value still wins; a raise is clipped to the per-slot + * share so N workers cannot oversubscribe the host (#1654). The source stays + * CBM_MEM_BUDGET_MB so the clip is the user's aggregate, not a silent + * daemon_worker_cap rewrite of a fraction-derived default. */ TEST(resolve_budget_worker_cap_preserves_lower_user_override) { size_t total = 8192 * CBM_TEST_MB; size_t worker_cap = 16 * CBM_TEST_MB; @@ -524,10 +529,15 @@ TEST(resolve_budget_worker_cap_preserves_lower_user_override) { ASSERT_STR_EQ(lower.source, "CBM_MEM_BUDGET_MB"); ASSERT_FALSE(lower.hard_capped); - cbm_mem_budget_t capped = cbm_mem_resolve_budget_capped(total, 0.5, "64", worker_cap); - ASSERT_EQ(capped.budget, worker_cap); - ASSERT_STR_EQ(capped.source, "daemon_worker_cap"); - ASSERT_TRUE(capped.hard_capped); + cbm_mem_budget_t raised = cbm_mem_resolve_budget_capped(total, 0.5, "64", worker_cap); + ASSERT_EQ(raised.budget, worker_cap); + ASSERT_STR_EQ(raised.source, "CBM_MEM_BUDGET_MB"); + ASSERT_TRUE(raised.hard_capped); + + cbm_mem_budget_t fraction = cbm_mem_resolve_budget_capped(total, 0.5, NULL, worker_cap); + ASSERT_EQ(fraction.budget, worker_cap); + ASSERT_STR_EQ(fraction.source, "daemon_worker_cap"); + ASSERT_TRUE(fraction.hard_capped); PASS(); }