docs: close the deployment gaps from the dev.9 field report (#42 item 7) - #9
Merged
Conversation
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
force-pushed
the
docs/42-deployment-gaps
branch
from
August 30, 2026 13:35
6203e89 to
c877a28
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes item 7 of floruntime/flo#42, reported from a production-shaped Docker deployment.
The one that actually blocks people
The documented
docker runquick 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, logstopology manifest created, and exits with a bareerror.PermissionDeniedthat mentions neither io_uring nor seccomp.Added
--security-opt seccomp=unconfinedto 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] binddefaults to loopbackAlready 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 /healthWas 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_flushdurabilityThe 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_secondsgoverns 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.
syncasync_flushephemeralCorrections found while writing
[metrics] bindwas documented as0.0.0.0; it defaults to127.0.0.1.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 buildpasses, 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, andio_uringslugifies toio-uring.