Skip to content

Config writes and activation outcomes (plan C2) - #37

Merged
Zakkaus merged 7 commits into
daeuniverse:honkfrom
Zakkaus:fix/contract-config-outcomes
Sep 28, 2026
Merged

Zakkaus merged 7 commits into
daeuniverse:honkfrom
Zakkaus:fix/contract-config-outcomes

Conversation

@Zakkaus

@Zakkaus Zakkaus commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Plan PR C2: config writes and outcomes. Decisions Q2, Q12 and Q15 of the plan and its F2 paragraph apply. honk references are to Glassyiris/honk feat/native-api 253c342, crates/honk-core/src/native_api/. dae has no native REST listener yet, so each dae line says what an implementation would use.

F2: activation outcomes

  • errors.md gets an "Activation outcomes" section. Every activation failure on reload, source replacement, source creation, node and provider writes and a config-editing group patch reports error.details.committed: false (previous generation still active), true (new generation active, active_generation_id names it or is null, the request still fails), or null (unknown; read back GET /runtime). written is a separate detail: whether the store holds the change. The shared codes reload_rejected, reload_degraded, supervisor_reconciliation_failed, activation_unconfirmed and store_unavailable are listed with their committed value. An operation carries the code in error.code; a synchronous 503 carries it in details.stage. ErrorCode and the embedded-error sentence name these as the exception to "adapter-defined codes". reload.md and configuration.md drop "the previous generation remains active" as an unconditional claim.
  • honk: follows in HA. Operations: Rejected on a plain reload gets committed:false (completion.rs:181, today only the replace path passes it); Unconfirmed reports code activation_unconfirmed with committed:null (it is engine_unavailable in reason(), completion.rs:11-14, and has no details on the file-store path); the store failure keeps store_unavailable and adds written:false (:103-108, :150). Synchronous node and provider writes: management_error (completion.rs:27-38) adds active_generation_id for Degraded and Reconciliation, and the store failure (coordinator.rs record error) uses stage: store_unavailable instead of stage: store.
  • dae: the reload worker already restores the previous generation when a fresh datapath cutover fails (cmd/run.go restorePreviousFreshDatapathGeneration), which is committed:false; a failed restore is committed:null. dae has no degraded-but-active state today, so it never reports true except through store_unavailable if it adds a database store.

F42: a created source is removed only when activation did not commit (Q15)

  • configuration.md, errors.md and createConfigSource: a failure before the source is stored creates nothing. With committed: false (reload could not start or was rejected, previous generation active) the server removes the created source only if the store still holds it with the same identity and content, and reports written: false; a replaced or modified source, or a failed removal, is kept and reported as the cleanup conflict written: true with committed: false, and has to be reconciled outside the API. With committed: true (active, degraded) or committed: null the source stays; with null the client reads back GET /config and GET /runtime. Replacement never removes anything, so a replaced source keeps its accepted ID and PUT can repair it. honk-mapping.md's create row says the same.
  • honk: follows in HA. At 253c342 the completion path removes only for committed: false (completion.rs:93-102) and the dispatch-failure path never activated, but CreatedFile::remove compares only the inode it created (config_write.rs:277-307), so an in-place edit that keeps the inode is deleted. It must also compare the content it wrote and keep the file on a mismatch.
  • dae: write the file, then reload; if the reload is rejected or cannot start and os.SameFile still matches the file it wrote, delete it and report written:false; otherwise, or if the delete fails, keep it and report true. If the restore after a failed cutover also fails (committed:null), keep the file.

F13: store-neutral write rules

  • "Validation and atomic write" becomes "Validation and commit". The rules are stated for the configuration store (files or database records): If-Match against the stored hash, one transaction (readers see old or new bytes), serialized hash check, validation and commit, recheck at commit (412). A store that records only after activation is allowed; written then says whether it did. Temp file, rename and file mode move into a "file store" sentence. The 412 texts in errors.md, common.yaml and config.yaml say "stored" instead of "on-disk". honk-mapping.md evidence rows about the old pinned revision are left as they are.
  • honk: none. The database store (store.rs) already records after activation (completion.rs:130-162).
  • dae: a file store; the file-store sentence is its implementation.

F10: PUT refusal reasons

  • PUT now lists 403 for a replacement that sets or changes API listener settings or secrets and, in a file store, for a source path that is no longer a regular file; and 422 with one restart-required error diagnostic per setting the engine applies only at startup. Create lists the same refusals. Dry-run validation reports restart-required as a warning. ConfigSource.writable says it is false for a source holding listener settings or secrets and while the store cannot accept writes.
  • read_only_reason is not added: kind, writable and the capability switches already give the reasons dae would have, and the rest are honk policy that the new text describes.
  • honk: none (coordinator.rs:988-1023, config_write.rs:459-462, config/coordinator/validation.rs:64-89, config.rs:477-488).
  • dae: compare the candidate's listener section with the running one before writing; list the settings its reload does not apply and report each as restart-required.

F7: config.content removed (Q2)

  • The flag is gone from discovery.yaml, capabilities.md, configuration.md and config.yaml. config.available means every returned source carries content (listener-secret values masked), so ConfigSource.content is required. The "redacted" examples now show masked content and writable: false. Tests updated: no content in the capability, a source without content is invalid.
  • honk: follows in HA: drop "content" from the capability object (types.rs:166). It already returns content on every source (config.rs:565-580).
  • dae: return the accepted text of every source.

F36: config.max_bytes below the body limit

  • config.max_bytes must be less than limits.max_json_body_bytes minus the request envelope; the body limit still applies, and escaping can exceed it. The capability example uses 61440 with a 65536 body limit, and a test checks the example against a create envelope whose permitted path has the longest JSON encoding (1020 quotation marks plus .dae).
  • honk: follows in HA: types.rs:166 advertises MAX_BODY_BYTES for both; advertise a smaller value.
  • dae: advertise a constant below its body limit.

F22: node and provider writes may be asynchronous (Q12)

  • Create returns 201 or 202 with an operation; delete returns 200 or 202 with an operation. New operation kinds node_create, node_delete, provider_create, provider_delete; the create result is the created Node or Provider (with its ID), the delete result is the deleted count. A failure after the change is stored uses the F2 details. node-latency.md, providers.md and operations.md describe it; the 202 responses have examples.
  • honk: none. It stays synchronous (201 and 200); its partial-commit 503 already carries the F2 details, with the additions listed under F2.
  • dae: a reload is asynchronous in dae (cmd/run_reload_worker.go), so the natural implementation is 202 with an operation that completes when the reload does.

Revision after the dae compatibility and copy reviews

  • Created source cleanup: removed only while the store still holds the source this operation created with the same identity and content (a file store compares file identity and content); a replaced or modified file, or a failed removal, is kept and reported as the cleanup conflict written: true, committed: false. honk: follows in HA (see F42, CreatedFile::remove checks identity only). dae: os.SameFile and a byte comparison with the written content before os.Remove.
  • Provider create: new optional resources.providers.create_unfetched. When true the provider starts unfetched; otherwise the backend may fetch during activation and returns the actual state. honk: follows in HA, advertise create_unfetched: true in types.rs. dae: leave it false and let its normal reload resolve the subscription.
  • Outcome guarantees hold only for a surviving instance; a stopped or restarted engine may lose operations and returns 404 for their IDs. honk: matches (in-process operation store). dae: the in-process API dies with the daemon, which this allows.
  • Serialization: each write activates the candidate it validated, the next write waits for that activation or gets 409 state_conflict on a server that does not queue, an out-of-band edit before commit fails it with 412, one after commit waits for a later reload. honk: matches (one coordinator worker runs each Work through activation, coordinator.rs:114; commit recheck :1038-1060). dae: hold a write mutex across validation and reload and reload the validated sources instead of rereading disk.
  • Storage order: one rule, commit before activation or after the new generation is active, both finish before success, 202 means accepted, atomic rename kept for file stores (sol copy fix 3). honk: matches (file store first, database store after, completion.rs:133-165). dae: file first.
  • Check order (errors.md, also fixes C1 text): authentication and routing, request boundary (428/400 on If-Match, 415, 413, 400), replay lookup, 412, 422, 409; exceptions: creation's path 409 precedes validation, the listener 403 is decided during validation, and group PATCH checks 415 first and reports a missing or malformed If-Match after the replay lookup, so a retained replay succeeds without the header. honk: matches (config/http.rs:126-209, operations.rs:206-273, config/coordinator.rs prepare_create; group PATCH per groups.rs:334-450 groups::patch). dae: same order in the handler, with the group PATCH exception.
  • restart-required now means a requested change that cannot take effect through reload, compared with the active configuration. honk: matches (restart_diagnostics against the active config, coordinator.rs:1019). dae: compare against its running config.
  • Visibility (config.yaml:11-12) is left to C3 (docs: access model, geodata lifecycle and one SSRF policy (C3) #38). This PR adds no visibility rule; capabilities.md now links to configuration.md instead of restating it.
  • Replay retention (fixes C1 operations.md): unfinished keys are never evicted and a full store that holds only unfinished operations returns 503; finished keys, including synchronous 200 replies, are retained from completion; new optional resources.operations.max_replay_keys bounds early eviction, oldest-finished first. honk: matches #56 (32 records, 1024 tombstones, operations.rs:24-26,523-546); follows in HA: advertise max_replay_keys: 1024. dae: a small in-process registry.
  • Cursors (fixes C1 common.yaml): bound to the endpoint and resource as well; a cursor from another endpoint is 410. honk: matches (each list has its own snapshot store, flows.rs:483-492). Seen while checking: honk also returns 410 for a filter mismatch (flows.rs:491) where C1 says 400; that is outside this PR.
  • max_bytes: at most the body limit minus the compact UTF-8 JSON envelope, taking for POST the permitted path with the longest compact JSON encoding (each " or \ in the path encodes to two bytes); escaped content can exceed the body limit, which then applies. The "fits" claim is gone. honk: the HA line under F36 stands.
  • stage and durability_confirmed are defined in the outcomes table. honk: matches (management.rs:147-158, types.rs:174-178).
  • Outcome codes are used when the case applies; supervisor_reconciliation_failed and store_unavailable apply only to engines with a separate supervisor or a record-after-activation store, so dae need not produce them.
  • reload_rejected stays as the fifth code: honk emits it (completion.rs:15).
  • The redacted GET /config and single-source examples now say bytes: 90, the length of their content.
  • Copy fixes 1-9 from the writing review are applied; fix 1 links to configuration.html#Editing because the page has two Request headings.

Check: npx -y yarn@1.22.22 check:contract passes (74 tests).

A failed reload, source replacement or source creation now reports
written and committed (true, false or null) with active_generation_id,
and the error contract lists the shared outcome codes. A created source
is never removed after it is stored. Config writes are described as a
transaction on the configuration store rather than a file rename, and
PUT lists the listener-settings and restart-required refusals.
… provider writes

Readback always returns source content when config.available is true,
so the content flag goes and ConfigSource.content becomes required.
config.max_bytes must leave room for the request envelope under the
JSON body limit. Node and provider create and delete may return 202
with an operation whose result is the created resource or the deletion
count.
@Zakkaus
Zakkaus force-pushed the fix/contract-config-outcomes branch from 79e3075 to 073f3f4 Compare September 28, 2026 16:36
A create whose activation reports committed false removes the source
again, so the store matches the active configuration and no source is
left that the API cannot address. written then reports false, or true
when the removal failed. With committed true or null the source stays.
Replacement never removes anything, so PUT can repair a replaced source.
@Zakkaus
Zakkaus merged commit 4da863e into daeuniverse:honk Sep 28, 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