Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLIPublic
Assigneesstack72

Relationships

#2446 serve: make the datastore poll interval configurable (--datastore-poll-interval)

Opened by stack72 · 9/23/2026· Shipped 9/23/2026

Repo: swamp (checked against 379bb5f0). Scope: swamp serve only.

Problem

swamp serve runs three background pollers that pull from the remote datastore:

  • ConfigPoller (src/serve/config_poller.ts) — managedConfig definitions + pulled extensions
  • AccessDataPoller (src/serve/access_data_poller.ts) — grants + groups
  • RuntimeDataPoller (src/serve/runtime_data_poller.ts) — runtime data/

All three default to 30 s (DEFAULT_*_POLL_INTERVAL_MS = 30_000), and all three already accept pollIntervalMs in their options. But serve.ts never passes it (serve.ts:1973, :2719, :2728), so operators can't tune how quickly peers converge. Tests that exercise peer replication also can't run fast: swamp-uat's managedConfig serve lane and tests/cli/serve/access/ha_test.ts each wait up to 90 s per test.

Change

Add one setting that controls all three pollers:

Surface Name
CLI flag --datastore-poll-interval <duration:string>
Env var SWAMP_DATASTORE_POLL_INTERVAL
serve.yaml key datastore-poll-interval

Default: 30 s (unchanged behaviour when unset). Precedence: CLI > env > serve.yaml, the same as every other serve option.

One setting rather than three: the pollers do the same job (periodic scoped pull from the datastore), and operators reason about it as "how stale can a peer be". The name says datastore to separate it from GrantsDirectoryPoller (a local directory, not in scope) and --group-refresh-interval (IdP, not in scope).

Follow --heartbeat-interval / --reconciliation-interval exactly. Every step below has a sibling to copy.

1. Option definitions — src/cli/commands/serve.ts

  • serve command (~line 1314, next to --heartbeat-interval): add --datastore-poll-interval <duration:string>. Help text in the same style: "How often to pull config, access data and runtime data from the remote datastore. Accepts seconds (30), explicit units (30s, 1m). Default: 30s. Minimum: 1s. Only effective with a remote datastore (env: SWAMP_DATASTORE_POLL_INTERVAL)".
  • serve daemon enable (daemonEnableCommand, ~line 896): add the same option with short help text ("Datastore poll interval (default: 30s, env: SWAMP_DATASTORE_POLL_INTERVAL)").
  • collectServeExtraArgs (~line 605): forward it, i.e. if (options.datastorePollInterval) args.push("--datastore-poll-interval", …).

2. Config merge — src/serve/serve_config.ts

  • Env map (~line 52): datastorePollInterval: "SWAMP_DATASTORE_POLL_INTERVAL".
  • Options type (~line 784): datastorePollInterval?: string;.
  • Merge (~line 1076): resolveString("datastore-poll-interval", cliOptions.datastorePollInterval, config?.["datastore-poll-interval"], undefined).
  • Include it in the returned merged object (~line 1212).
  • If serve.yaml keys are validated against a schema or known-key list, add the key there too.

3. Parse and validate — src/cli/commands/serve.ts

  • Next to heartbeatIntervalMs (~line 1485): datastorePollIntervalMs = raw !== undefined ? parseTimeout(raw, "--datastore-poll-interval") : undefined. parseTimeout (src/cli/duration_parser.ts) already rejects ≤ 0 and bad formats.
  • Add a floor: below 1000 ms → UserError: --datastore-poll-interval must be at least 1s; got <raw>. Every tick is a remote list call, so sub-second values would hammer the bucket.
  • "No effect" warning: the access and runtime pollers start whenever syncService exists (serve.ts:2718); the config poller also needs a remote control plane + managedConfig. So warn only when there's no syncService (filesystem datastore): logger.warn("--datastore-poll-interval has no effect without a remote datastore"). Don't copy the heartbeat condition (!hasRemoteControlPlane) — it's a different gate.

4. Wire into the pollers — src/cli/commands/serve.ts

Pass pollIntervalMs: datastorePollIntervalMs (spreading only when defined, so each poller keeps its default) to:

  • new ConfigPoller({...}) (~line 1973)
  • new AccessDataPoller({...}) (~line 2719)
  • new RuntimeDataPoller({...}) (~line 2728)

No poller changes are needed. Each already logs its interval at start (e.g. Config poller started (interval: ${intervalSec}s)), and each has a #pulling guard, so a pull slower than the interval (up to POLLER_PULL_TIMEOUT_MS = 120 s) skips ticks and never overlaps.

5. Tests (unit, in swamp)

  • src/serve/serve_config_test.ts: CLI > env > yaml precedence for datastore-poll-interval; unset → undefined. Copy the heartbeat-interval cases (~line 92).
  • src/cli/commands/serve_daemon_test.ts: collectServeExtraArgs({ datastorePollInterval: "5s" }) → ["--datastore-poll-interval", "5s"]. Copy the test at ~line 111.
  • Validation: "500ms" → UserError mentioning "at least 1s"; "0" / "abc" → parseTimeout error; "2s" → 2000.
    • If validation is inline in the serve action and hard to reach from a unit test, extract a small parseDatastorePollInterval(raw): number | undefined helper next to the other parsing and test that.
  • Poller wiring: if there's an existing seam that builds pollers in tests, assert the interval arrives. Otherwise the log-line check in the acceptance criteria below covers it.

6. Docs

  • design/primitives/serve.md:
    • Options table (~line 84): add a row for --datastore-poll-interval / SWAMP_DATASTORE_POLL_INTERVAL / 30 s / "config, access and runtime pollers; min 1s; no effect without a remote datastore".
    • Update the fixed "30 s" wording at ~lines 362, 369 and 429 to say "every --datastore-poll-interval (default 30 s)".
  • design/enablers/datastores.md ~line 444 ("All three run every 30 seconds by default…"): name the setting.
  • Check .claude/skills/ for any mention of the 30 s poll. If a skill file changes, the repo's CLAUDE.md requires loading the skill-creator skill first and running deno fmt.

Acceptance criteria

  1. swamp serve --help lists --datastore-poll-interval with the env name.
  2. With an S3 datastore + managedConfig: true and --datastore-poll-interval 2s, the log shows:
    • Config poller started (interval: 2s);
    • the matching start lines for the access and runtime pollers with the same interval.
  3. With the same setup, a model created on instance A shows up on instance B in about 2 s, not 30 s.
  4. SWAMP_DATASTORE_POLL_INTERVAL=2s and serve.yaml datastore-poll-interval: 2s behave the same as the flag. CLI beats env, and env beats yaml.
  5. --datastore-poll-interval 500ms → exit 1 with "must be at least 1s". 0 and abc → exit 1 via parseTimeout.
  6. Filesystem datastore + the flag → server starts and logs the "no effect without a remote datastore" warning.
  7. Unset → every poller still logs interval: 30s (no behaviour change).
  8. serve daemon enable --datastore-poll-interval 10s forwards the flag into the generated service definition.
  9. deno check, deno lint, deno fmt and the unit tests pass.

Out of scope

  • GrantsDirectoryPoller (local --grants-dir poll, 30 s) and --group-refresh-interval.
  • Separate per-poller settings.
  • Changing the 30 s default.

Follow-up in swamp-uat (not part of this change)

  • The planned managedConfig UAT serve lane sets SWAMP_DATASTORE_POLL_INTERVAL=2s in the serve env; the planned SWAMP_UAT_POLL_INTERVAL indirection goes away.
  • tests/cli/serve/access/ha_test.ts:233 can drop its 90 s deadline.
  • Add a UAT for acceptance criteria 2–5 in tests/cli/serve/none/config_test.ts (flag parsing) and in the managedConfig serve lane (replication speed).
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 8 MOREREVIEW+ 10 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

9/23/2026, 10:56:26 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack729/23/2026, 9:53:56 PM

Sign in to post a ripple.