Skip to content

Size the Linux compose store's workers and work_mem from the product's own formula (#4322) - #4400

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/4322-compose-store-sizing
Sep 26, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
fix/4322-compose-store-sizing

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Refs #4322.

Why

On Linux the service never manages the store container, and it doesn't read the container's own memory limit. timescaledb-tune,
which the official timescale/timescaledb image runs on every start, already sizes the container from its
cgroup limit the way the managed Windows store sizes from host RAM. So the compose file should carry
explicit flags ONLY where tune's own choice is a known miss.

Measured (#4322 comment 5841262514) under docker run --memory against
timescale/timescaledb:latest-pg17:

limit shared_buffers work_mem effective_cache_size maintenance_work_mem
2 GB 512MB 4MB 1536MB 256MB
4 GB 1GB 4MB 3GB 512MB
8 GB 1983MB 3966kB 5950MB ~992MB

I re-measured worker sizing myself with the same command (2 GB / 8 GB, SHOW timescaledb.max_background_workers, max_worker_processes, max_connections): tune gives 16 / 34 background workers / worker processes at
both sizes — PostgreSQL's own default shape. .github/workflows/build.yml (~936-952) says why that's wrong
for this product: TimescaleDB's per-hypertable compression/retention/CAGG policy jobs are launched by
background workers, and at 8-16 slots most policy runs never happen.

The spill evidence (#4310, ~4 MB work_mem): QueryStoreTopSql at 7 days spilled ~38 MB per
worker across 5 workers. Tune's own work_mem (~4 MB, measured above) is below the managed store's own
floor — DeriveMemorySettings' clamp(RAM/512, 16 MB, 64 MB) already floors to 16 MB for any host at
or under 8 GB (8 GB / 512 = 16 MB exactly) — at every size this container is measured at, so a single fixed
literal is the right answer; no TS_TUNE_MAX_CONNS companion needed (fewer hard-coded numbers than
deriving a second one to make tune's own number come out right). cc #4310.

What changes

Darling/compose/docker-compose.yml's store.command: gains three -c flags, each commented with its
floor/cap and reasoning inline:

  • timescaledb.max_background_workers=74
  • max_worker_processes=85
  • work_mem=16MB

74/85 is the SAME formula the managed Windows store writes (DarlingManagedPostgres.BuildWorkerSizingConfAppend):
max_background_workers = HypertableCount + 2, max_worker_processes = 3 + max_background_workers + 8.
TimescaleSupport.HypertableCount is 72 today → 74 and 85.

Also added: a commented mem_limit: 8g EXAMPLE only (never a default — hosts vary, a limit could starve a
big host's store), and a block comment above command: explaining the boundary (what tune keeps,
what's overridden, and why).

shared_preload_libraries=timescaledb,pg_stat_statements is untouched — still the first -c flag, still
wins by command-line precedence.

Reading the container's cgroup limit from inside the service is not part of this change: the service doesn't manage this container, and tune already reads the same limit directly.

Values vs the Windows derivation

Setting Windows managed-store rule Compose store Match?
shared_buffers min(RAM/4, 1 GB) tune's own cgroup-derived value (same formula, unpinned) Match (left to tune)
effective_cache_size RAM × 3/4 tune's own value Match (left to tune)
maintenance_work_mem min(min(max(RAM/20,1536MB),RAM/4), 2047MB cap) tune's own value Match (left to tune)
work_mem clamp(RAM/512, 16 MB, 64 MB) pinned to 16MB Differs from tune's own (~4MB), matches the Windows FLOOR — net positive, so defaulted
max_connections fixed 200 tune's own value (25 at 2 GB, 100 at 8 GB) Differs from Windows' fixed 200, left alone — tune's per-container sizing stays, because the service doesn't read the container's own limit to reconcile it
timescaledb.max_background_workers / max_worker_processes HypertableCount+2 / 3+that+8 pinned, same formula Match — net positive, so defaulted

Test plan

New CiComposeWorkerSizingTests (run in-process on macOS against the real Darling.Tests.dll):

=== TEST EXECUTION SUMMARY ===
   Darling.Tests  Total: 4, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0, Time: 0.073s

RED confirmed against pre-fix origin/dev (6e501af), same test file dropped into a detached worktree:

Darling.Tests.CiComposeWorkerSizingTests.ComposeStoreCommand_SetsWorkMemAtTheFixedFloor [FAIL]
Darling.Tests.CiComposeWorkerSizingTests.ParsedCommand_Comparison_FailsOnAnInjectedDrift [FAIL]
Darling.Tests.CiComposeWorkerSizingTests.ComposeStoreCommand_SizesWorkersFromTheProductsFormula [FAIL]
   Darling.Tests  Total: 4, Errors: 0, Failed: 3, Skipped: 0, Not Run: 0, Time: 0.074s

(The fourth pin, shared_preload_libraries, correctly passed on dev too — that line predates this change.)

Live check, new compose flags, docker run --memory (same image, same command line):

limit work_mem max_connections max_background_workers max_worker_processes shared_buffers preload libs healthy
2 GB 16MB 25 74 85 512MB timescaledb,pg_stat_statements yes
8 GB 16MB 100 74 85 1983MB timescaledb,pg_stat_statements yes

2 GB safety arithmetic (all-connections-busy worst case, same bound DeriveMemorySettings documents —
~3 concurrent sort/hash nodes per connection): 25 × 3 × 16 MB = 1200 MB, plus shared_buffers' 512 MB =
1712 MB, under the 2 GB limit with headroom for the rest of the server.

Build: Darling.Tests.csproj and Lite.Tests.csproj both 0 Warning(s) / 0 Error(s) with
-p:EnableWindowsTargeting=true. Lite.Tests carries no compose-related change and is unaffected.

Notes

CHANGELOG entry

SECTION: Changed

ENTRY:

  • Sized the Linux compose store's background-worker slots and work_mem from the product's own
    formula, instead of leaving them at timescaledb-tune's CPU/memory-derived defaults
    ([Size the Linux compose store's workers and work_mem from the product's own formula (#4322) #4400]) - the
    compose deployment's store container was running PostgreSQL's default worker ceiling (most TimescaleDB
    policy jobs never launched) and a work_mem low enough to spill sorts that a modest analytical query
    needs. The background-worker slots now use the managed store's formula, and work_mem is pinned to the managed store's 16 MB floor.

REF:
[#4400]: #4400

…s own formula (#4322)

timescaledb-tune, which the official image runs on every start, sizes worker counts from CPU count and
work_mem from the container's memory limit -- neither matches this product's needs. Measured under
docker run --memory at 2/4/8 GB: tune gives 16/34 background/worker-process slots (PostgreSQL's own
default shape, which starves TimescaleDB's per-hypertable compression/retention/CAGG policy jobs) and
~4 MB work_mem (below the managed store's own 16 MB floor at these sizes, and the #4310 spill evidence
shows the cost of staying there).

Path B (claude-desktop 2026-09-26 07:17Z): the service doesn't manage this container, so explicit flags
land ONLY where tune's own choice is a known miss, each with its floor/cap documented in a comment.
Everything else (shared_buffers, effective_cache_size, maintenance_work_mem, max_connections) stays on
tune's own container-derived sizing.

- store.command now sets timescaledb.max_background_workers=74 and max_worker_processes=85, the same
  formula BuildWorkerSizingConfAppend writes for the managed Windows store
  (HypertableCount + 2 / 3 + that + 8), and work_mem=16MB, the managed store's own RAM/512 floor for any
  host at or under 8 GB.
- a commented mem_limit example, never a default.
- CiComposeWorkerSizingTests pins the compose file's flags against the live HypertableCount formula and
  against the shared_preload_libraries line, the same shape as CiClusterWorkerSizingTests for CI's
  throwaway cluster.

RED confirmed on dev's pre-fix compose file: 3 of 4 pins fail (worker sizing absent, work_mem absent);
the fourth (preload libraries) already passed since that line predates this change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant