Skip to content

docs: close the deployment gaps from the dev.9 field report (#42 item 7) - #9

Merged
oreofeolurin merged 1 commit into
mainfrom
docs/42-deployment-gaps
Aug 30, 2026
Merged

docs: close the deployment gaps from the dev.9 field report (#42 item 7)#9
oreofeolurin merged 1 commit into
mainfrom
docs/42-deployment-gaps

Conversation

@oreofeolurin

Copy link
Copy Markdown
Contributor

Closes item 7 of floruntime/flo#42, reported from a production-shaped Docker deployment.

The one that actually blocks people

The documented docker run quick start could not work on a Linux host. Docker's default seccomp profile blocks the io_uring syscalls Flo's event loop needs, so the server starts, logs topology manifest created, and exits with a bare error.PermissionDenied that mentions neither io_uring nor seccomp.

Added --security-opt seccomp=unconfined to the quick start and to all four Compose services, plus a section covering the cause, a narrower allowlist profile for anyone who doesn't want to drop the whole thing, and the Linux 5.1 kernel floor.

The error message itself is being fixed on the flo side in the companion PR.

[dashboard] bind defaults to loopback

Already documented, but not where it bites. An orchestrator's readiness probe comes from outside the container, so the loopback default presents as a service that never goes ready — which reads like a broken health check rather than a bind address. Called out in the Docker health-check section and in the config reference.

GET /health

Was a single line with no response shape and no stability promise. Now documents the body, what a non-200 means, its always-public status, and an explicit stability guarantee — including that fields may be added, so probes shouldn't match the body exactly.

async_flush durability

The report guessed the exposure window was the hot buffer, bounded by hot_flush_seconds. That's not right, and I checked the code rather than repeating it: hot_flush_seconds governs hot → warm tiering and has no bearing on crash safety. The real window is the background segment-flush task, which runs at most once per second.

Documented as a per-mode "at risk on abrupt termination" column plus a paragraph making the shape of the loss explicit — segments are fsynced and published by atomic rename, so it's a clean tail truncation, never a torn store, and a graceful shutdown flushes and loses nothing.

Mode At risk on abrupt termination
sync Nothing
async_flush Up to ~1 second of acknowledged writes
ephemeral Everything

Corrections found while writing

  • [metrics] bind was documented as 0.0.0.0; it defaults to 127.0.0.1.
  • The Docker port table listed 9001 and 9002 unconditionally; both bind only when their section is enabled.

Also documents the Raft port behaviour changing in the companion PR (#42 item 5): the port binds only when the node can actually have peers, so a single-node server has no undeclared port to account for.

Verification

npm run build passes, and I checked every anchor link added here against the generated HTML. Two were broken on the first pass and are fixed: ### headings get no anchor id in Markline, and io_uring slugifies to io-uring.

oreofeolurin added a commit to floruntime/flo that referenced this pull request Aug 29, 2026
…ain io_uring (#42)

Issue #42 items 5, 6 and the code half of item 7.

## Item 5 — Raft bound its port with `[cluster] enabled = false`

Two separate faults, both silent.

`[cluster] enabled` was parsed and thrown away — `_ = table.getBool("enabled")`
with a comment calling the field deprecated because "cluster always runs". The
operator's setting had no effect and nothing said so.

The gate that decided whether to start the Raft network was
`effectiveRaftPort() != listen_port`, but that helper *derives* `listen_port +
500` when no port is configured, so the condition was true for every
single-node server. Raft always bound its port, while the startup banner
printed the unset config value — "Raft port: 0" — beside a live listener on
9500.

`enabled` is now honored, and the listener starts only when this node can
actually have peers: `enabled = true`, seeds, an explicit `raft_port`, or
`replication_factor > 1`. A plain single-node server leaves the port unbound.
The banner reports the port Raft will really bind, or "not listening
(single-node)".

Every consumer of `shard.raft_network` already null-checked it and the field
already initialised to null, so the unbound path was supported — just
unreachable.

## Item 6 — `flo cluster status` returned "not implemented"

The opcode, the wire format and the whole CLI renderer existed; only the
server-side handler was missing, so the request fell through the dispatcher to
its generic "not implemented" reply — for a command `flo --help` advertises in
its own examples. Registered a handler that reports this node's Raft identity,
role, term and member count. A single node answers as leader of a one-member
cluster, which is the truthful answer rather than an error.

The node's cluster ID is now computed whether or not the listener runs, so it
is reportable single-node.

## Item 7 (code half) — undiagnosable io_uring failure

Under a container runtime's default seccomp profile the io_uring syscalls are
blocked and Flo died with a bare `error.PermissionDenied` naming neither
io_uring nor seccomp. The reactor now explains the failure and names the fix,
distinguishing a blocked profile from a pre-5.1 kernel.

Docs are in floruntime/docs#9.

## Tests

Unit tests pin the gate, including an explicit assertion that the old
condition was true for a plain single-node config. E2E tests cover `cluster
status` in both table and JSON form, asserting on output rather than exit
status. Both e2e tests fail against the unfixed code.

The Linux-only reactor branch does not compile on this host, so it was
type-checked by forcing analysis under `-target x86_64-linux`. Its two error
prongs were checked against std's `io_uring_setup` mapping (EPERM →
PermissionDenied, ENOSYS → SystemOutdated).

test-unit and test-integration pass. test-e2e: 575/579, the 4 failures being
the `kv/cluster` set that fails identically on a stashed baseline — notably
unchanged by the Raft gating.
oreofeolurin added a commit to floruntime/flo that referenced this pull request Aug 29, 2026
…ain io_uring (#42 items 5, 6, 7) (#47)

* fix(cluster): honor [cluster] enabled, implement cluster status, explain io_uring (#42)

Issue #42 items 5, 6 and the code half of item 7.

## Item 5 — Raft bound its port with `[cluster] enabled = false`

Two separate faults, both silent.

`[cluster] enabled` was parsed and thrown away — `_ = table.getBool("enabled")`
with a comment calling the field deprecated because "cluster always runs". The
operator's setting had no effect and nothing said so.

The gate that decided whether to start the Raft network was
`effectiveRaftPort() != listen_port`, but that helper *derives* `listen_port +
500` when no port is configured, so the condition was true for every
single-node server. Raft always bound its port, while the startup banner
printed the unset config value — "Raft port: 0" — beside a live listener on
9500.

`enabled` is now honored, and the listener starts only when this node can
actually have peers: `enabled = true`, seeds, an explicit `raft_port`, or
`replication_factor > 1`. A plain single-node server leaves the port unbound.
The banner reports the port Raft will really bind, or "not listening
(single-node)".

Every consumer of `shard.raft_network` already null-checked it and the field
already initialised to null, so the unbound path was supported — just
unreachable.

## Item 6 — `flo cluster status` returned "not implemented"

The opcode, the wire format and the whole CLI renderer existed; only the
server-side handler was missing, so the request fell through the dispatcher to
its generic "not implemented" reply — for a command `flo --help` advertises in
its own examples. Registered a handler that reports this node's Raft identity,
role, term and member count. A single node answers as leader of a one-member
cluster, which is the truthful answer rather than an error.

The node's cluster ID is now computed whether or not the listener runs, so it
is reportable single-node.

## Item 7 (code half) — undiagnosable io_uring failure

Under a container runtime's default seccomp profile the io_uring syscalls are
blocked and Flo died with a bare `error.PermissionDenied` naming neither
io_uring nor seccomp. The reactor now explains the failure and names the fix,
distinguishing a blocked profile from a pre-5.1 kernel.

Docs are in floruntime/docs#9.

## Tests

Unit tests pin the gate, including an explicit assertion that the old
condition was true for a plain single-node config. E2E tests cover `cluster
status` in both table and JSON form, asserting on output rather than exit
status. Both e2e tests fail against the unfixed code.

The Linux-only reactor branch does not compile on this host, so it was
type-checked by forcing analysis under `-target x86_64-linux`. Its two error
prongs were checked against std's `io_uring_setup` mapping (EPERM →
PermissionDenied, ENOSYS → SystemOutdated).

test-unit and test-integration pass. test-e2e: 575/579, the 4 failures being
the `kv/cluster` set that fails identically on a stashed baseline — notably
unchanged by the Raft gating.

* fix(cluster): don't leak RaftNetwork when its bind fails

Follow-up on the same code path, both found by CI on Linux.

`Runtime.start` allocated the RaftNetwork and then called a fallible
`RaftNetwork.init` with no errdefer, so a failed bind leaked the allocation.
The DebugAllocator reported it alongside the bind failure.

The bind failure itself is already fixed by this PR's main change, and is a
sharper example of the always-true gate than the one in the description:

  `listen_port = 0` means "pick an ephemeral port", but effectiveRaftPort()
  adds 500 unconditionally and returns 500 — privileged. The old gate then
  started the listener anyway, so the two-shard runtime test tried to bind
  port 500. macOS permits bind(0.0.0.0:500) and Linux returns EACCES, so
  `Runtime: boot 2 shards and shutdown` passed locally and failed in CI.

With the new gate that config wants no listener at all, so the privileged port
is never bound. Added a unit test pinning both halves.
Four gaps reported from a production-shaped Docker deployment, plus two
corrections found while verifying them.

**io_uring and seccomp.** The documented `docker run` quick start could not
work on a Linux host: Docker's default seccomp profile blocks the io_uring
syscalls, so the server starts, logs `topology manifest created`, and exits
with a bare `error.PermissionDenied` that names neither io_uring nor seccomp.
Added `--security-opt seccomp=unconfined` to the quick start and to all four
Compose services, plus a section explaining the cause, giving a narrower
allowlist profile as an alternative, and noting the 5.1 kernel floor.

**`[dashboard] bind` defaults to loopback.** Documented, but not where it
bites: an orchestrator's readiness probe comes from outside the container, so
the default presents as a service that never goes ready rather than as a bind
address. Called out in the Docker health-check section and in the config
reference.

**`GET /health`.** Was one line with no response shape and no stability
promise. Now documents the body, what a non-200 means, its always-public
status, and an explicit stability guarantee — including that fields may be
added, so probes should not match the body exactly.

**`async_flush` durability.** The report guessed the exposure window was the
hot buffer bounded by `hot_flush_seconds`. It is not: `hot_flush_seconds`
governs hot → warm tiering and has no bearing on crash safety. The real window
is the background segment flush, which runs at most once per second. Documented
as a per-mode "at risk on abrupt termination" column plus a paragraph stating
that the loss is a clean tail truncation (segments are fsynced and published by
atomic rename), and that a graceful shutdown flushes and loses nothing.

Corrections found while writing:

- `[metrics] bind` was documented as `0.0.0.0`; it defaults to `127.0.0.1`.
- The Docker port table listed 9001 and 9002 unconditionally; both bind only
  when their section is enabled.

Also documents the Raft port behaviour that changes alongside this (#42 item
5): the port is bound only when the node can actually have peers, so a
single-node server has no undeclared port for an operator to account for.

Verified: `npm run build` passes and every anchor link added here resolves in
the generated HTML (`###` headings get no anchor id, so the /health link
targets its enclosing `## Cluster & metrics` section).
@oreofeolurin
oreofeolurin force-pushed the docs/42-deployment-gaps branch from 6203e89 to c877a28 Compare August 30, 2026 13:35
@oreofeolurin
oreofeolurin merged commit 1e78d95 into main Aug 30, 2026
1 check passed
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