Skip to content

fix(ha): harden failover runtime and controller - #347

Open
bpopadiuk wants to merge 179 commits into
mainfrom
agent/ha-failover-runtime-hardening
Open

fix(ha): harden failover runtime and controller#347
bpopadiuk wants to merge 179 commits into
mainfrom
agent/ha-failover-runtime-hardening

Conversation

@bpopadiuk

@bpopadiuk bpopadiuk commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Codex (GPT-5): Delivers the Antfly runtime and operator foundation for production-ready hot-standby HA, paired with the Colony control-plane work in https://github.com/antflydb/colony/pull/346.

Current Antfly revision: 7bd93ae4b1e1d98db09731c81b19d3f2f971042c.

Production-readiness scope

  • Runtime-owned portable seed capture, publication, materialization, activation, catch-up, cleanup, and durable receipts.
  • Fail-closed startup, public mutation gating, Lease watchdog fencing, promotion, rewind/rejoin, replacement, and stale-primary rejection.
  • Exact topology, PVC incarnation, timeline, epoch, LSN, generation, process, and operation authority throughout the HA lifecycle.
  • Operator orchestration for seed actions, startup dependencies, source-PVC scheduling, persistent extension packages, and HA status/admin surfaces.
  • Backward-compatible one-way migration from legacy Swarm resources to the Standalone runtime without changing StatefulSet, Service, PVC, selector, mount, or storage identity.

Safety properties

  • Lease authority is bound to the exact runtime process boot ID. Fresh self-held Leases without a process annotation remain pending until the runtime observes its own bound proof; mismatched authority fences closed.
  • Acknowledged logical mutations use synchronous RemoteApply durability and a final authority check before returning success. Fence transitions wait for admitted mutations to finish their local commit and HA-tail append, so a rejected acknowledgement cannot strand an unlogged local fork.
  • Watchdog observation, renewal, TLS, timeout, response, and validation failures fence closed.
  • Promotion and rejoin preserve exact timeline and authority identity; stale or isolated former primaries cannot remain acknowledgement authorities.
  • Seed publication and prefix cleanup are serialized by Zig-native object-store CAS authority outside the deleted prefix. Cleanup leaves a durable tombstone, and an old publisher cannot recreate a deleted generation.
  • Restore completion is withheld until replay, checkpoint, coverage, and query availability prove the restored projection usable.

Dependency-free runtime transport

The Lease transport remains dependency-free Zig through Antfly's existing httpx path. It preserves projected Kubernetes CA validation, DNS hostname verification and SNI, service-account bearer authentication, one absolute monotonic request deadline, bounded response handling, and fail-closed watchdog fencing.

The checked-in Zig-native TLS compatibility fix handles an optional TLS 1.3 CertificateRequest by sending an empty client certificate while preserving transcript boundaries. The abandoned OpenSSL Lease transport, system links, package additions, and host-glibc workaround are absent.

Deterministic evidence

  • Operator full module tests passed through make test.
  • HA suite: 373/373 passed, including the commit-versus-fence ordering regression, Lease fencing, startup gates, action dependency scoping, publication/cleanup writer exclusion, durable tombstones, mutation barriers, migration identity, mixed-table whole-instance replication, and TLS optional CertificateRequest.
  • Standalone runtime suite: 54/54 passed.
  • Linux amd64 musl and Linux arm64 musl release builds passed and produced statically linked executables.
  • Operator startup-gate receipt selection now skips unrelated namespace collisions and passed the full controller package.
  • git diff --check passed.

Review and certification boundary

  • Seed snapshot preparation still holds the exclusive mutation barrier while producing the exact backup_lsn-bound source. Safely shortening that outage requires an immutable storage-engine snapshot/checkpoint handle or equivalent two-phase boundary.
  • Current feature-related CI is green: operator, SDK, generated-source checks, and the full zig-base gate passed. The isolated e2e-base rerun reproduced only two unrelated timing failures: managed-embedding retry sequencing and automatic shard-split finalization. Neither exercises HA or the changed paths.
  • No live KinD or full live matrix was run. The authoritative live pass count remains zero, and live exact-pair certification remains explicit manual follow-up.

Colony PR346 pins this exact Antfly gitlink and operator pseudo-version/checksum.

Boris Popadiuk and others added 28 commits July 13, 2026 15:15
…-seed-artifacts

# Conflicts:
#	zig/lib/httpx/src/client/client.zig
…time-hardening

# Conflicts:
#	zig/pkg/antfly/src/common/http/std_http_listener.zig
@bpopadiuk
bpopadiuk marked this pull request as ready for review July 16, 2026 22:03

@bpopadiuk bpopadiuk left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex (GPT-5): This is an independent review written and posted by Codex, not by Boris.

Blocking review — approval should be withheld. GitHub does not allow the PR author's account to submit a formal “Request changes” event on its own PR, but I do not consider the current b5c5881 head production-ready. The overall direction is strong—typed receipts, fail-closed identity checks, narrow RBAC, generated client coverage, and the operator/runbook work are valuable—but the current implementation has multiple availability and correctness blockers on the core failover path.

Blocking findings

  1. Slow seed capture can self-fence the active primary. go/pkg/operator/controllers/antfly/antflycluster_controller.go:5711 performs capture synchronously in the same default single-worker reconcile that renews the fencing Lease. The watchdog only extends authority after a newer renewTime, and its default grace matches the HTTP client's 10-second timeout. A capture exceeding that window can block renewal long enough to latch api_unreachable and reject writes; canceling the client does not cancel the Zig capture, and a retry can start another attempt. Make capture asynchronous or independent from Lease renewal, add single-flight semantics, and test a capture longer than watchdog grace without fencing.

  2. The mutation barrier can deadlock permanently. It is writer-preferring but not reentrant. At zig/pkg/antfly/src/storage/db/db.zig:11614, setSchemaJson takes a shared guard and calls setSchema, which takes another. If capture has taken reader_gate while waiting for the outer guard's resource_mutex, the nested acquisition waits on reader_gate forever while capture waits on the outer guard. Other guarded call chains nest similarly. Acquire once at the public boundary/use internal “guard already held” helpers (or implement provably safe execution-context reentrancy), and add a deterministic interleaving test.

  3. Real portable-artifact Job receipts are rejected. The nested decoder at go/pkg/operator/controllers/antfly/antflycluster_controller.go:7003 declares only path while using DisallowUnknownFields. Zig emits path, size_bytes, crc32, sha256, and chunks; the first real size_bytes therefore makes a successful Job undecodable, so no typed evidence is recorded and dependent actions remain blocked. Existing tests use truncated path-only fixtures. Share the complete wire schema and contract-test an actual Zig CLI receipt.

  4. Every seed generation leaks a full prepared snapshot on the primary PVC. The return at zig/pkg/antfly/src/data/runtime.zig:4019 transfers a path whose deinit frees only the string. Nothing removes <capture_root>.runtime-snapshots/<generation>; existing capture GC prunes a different <capture_root>/generations tree. Repeated reseeds will exhaust disk. Define ownership and remove the directory after canonical capture with crash recovery, or add paired generation GC, plus a multi-generation disk-bound test.

  5. Restore publishes false readiness. The checks at zig/pkg/antfly/src/storage/db/db.zig:11566 do not prove the restored index has an active/queryable generation. Current zig-base reaches “restore runtime repair marked complete” and the immediately following query fails with IndexRebuilding. Gate completion on an index-manager invariant that guarantees queryability and retain an immediate-query regression test.

  6. Admission accepts portable-seed CRs the planner/executor cannot run. At go/pkg/operator/api/antfly/v1/antflycluster_webhook.go:1510, the topology tuple may be entirely absent and PVC bindings are optional. The planner then creates no executable chain without source/target PVC data, while the executor requires exact topology, target PVC name, and target PVC UID. Require the complete execution tuple/PVC bindings at admission, or implement and document a genuinely supported unbound path.

Production, UX, performance, and design gates

  • Authentication: HA admin auth needs one coherent boundary. On the shared public listener, native API-key authentication can reject a dedicated HA bearer token before the inner handler sees it; with native auth disabled and no HA token configured, the inner handler authorizes destructive routes. Put these routes on a private/admin listener or define one explicit auth path, require its secret at admission, and exercise the real handler end to end.
  • Write availability and scale: zig/pkg/antfly/src/data/runtime.zig:4284 holds the global exclusive mutation barrier through snapshot preparation and full recursive copy/link work, making write unavailability proportional to database size. Establish a point-in-time/checkpoint view under a bounded critical section, then copy/hash/upload outside it. Sequential chunk upload, a 600-second Job deadline, BestEffort pods, advertised 8 GiB/file and 64 GiB/artifact limits, and quadratic validation scans at high file counts also need measured bounds and production-sized load tests.
  • Current verification state: the checked head is merge-conflicting, zig-base and e2e-base are red, and at least the restore-readiness failure is directly in this PR's changed behavior. Those failures cannot be treated as unrelated.
  • Missing live proof: the PR says the live KinD/full matrix was not run. Before approval, run an exact Antfly/Colony-pair matrix covering normal failover, API/network partition, old-primary rejoin, controller restart, large seed/load, object-store outage, backup/restore, and upgrade/rollback, with bounded write-unavailability evidence.
  • Security-maintenance surface: the 1,759-line Zig TLS client fork is imported globally by httpx for every HTTPS caller. Isolate the compatibility change to the Lease transport, or document and pin the exact upstream delta and add interoperability/fuzz coverage.
  • Code shape: the controller, planner/status, and webhook files are now very large. Split artifact lifecycle, fencing/renewal, startup gates, and planner/executor state machines so their invariants can be tested independently and rollback risk is smaller.

Please fix these blockers, make the head conflict-free and green, and attach the live exact-pair evidence before treating this as a production-ready HA foundation.

@bpopadiuk

Copy link
Copy Markdown
Contributor Author

Codex (GPT-5): This response was written and posted by Codex, not Boris.

I re-evaluated the blocking review against final Antfly head 4d3d7cc86ddb5de70765f6475fea0b231e72d793.

The six concrete blockers are addressed:

  1. Seed capture is separated from Lease renewal with single-flight behavior, so slow capture cannot starve the watchdog/renewal path.
  2. Schema mutation acquires the HA barrier once; the nested writer-preferring deadlock has deterministic interleaving coverage.
  3. Portable-artifact receipt decoding uses the complete file/chunk integrity schema emitted by Zig.
  4. Prepared runtime snapshots are removed after canonical capture and covered across multiple generations.
  5. Restore readiness now requires queryable index coverage/generation state; the immediate-query regression and focused three-test restore set pass.
  6. Portable-seed admission requires the complete executable topology and PVC-incarnation tuple.

Authentication is now one explicit fail-closed boundary: the dedicated HA bearer is required and is aligned across the outer listener and inner handler. The real handler path is exercised end to end.

The Lease transport remains Zig-native. The OpenSSL C transport, ssl/crypto links, package additions, and host-glibc workaround were removed. The narrow Zig 0.16 TLS compatibility delta handles an optional initial-handshake CertificateRequest, is pinned by upstream and patch hashes, and has projected-CA, DNS verification, bearer-auth, hostname-mismatch, bounded-response, monotonic-timeout, and watchdog-fencing coverage.

Final deterministic evidence includes operator make test, standalone 54/54, data runtime 75/75, HA 358/358, focused restore 3/3, and tls-compat-check. Final-head CI passes operator, SDK, and zig-base, including the Linux GNU build and relevant TLS/standalone aggregates. Linux amd64/arm64 musl archives, the macOS arm64 archive, and the runtime image were built and inspected; the Antfly runtime has no dynamic ssl/crypto dependency.

I agree with these remaining review concerns and am deferring them explicitly rather than claiming closure:

  • snapshot preparation still holds the exclusive mutation barrier while establishing the exact backup_lsn-bound source; shortening it safely needs an immutable storage-engine snapshot/checkpoint design;
  • production-scale capture throughput, Job resources/deadlines, and artifact-size measurements;
  • broader TLS interoperability/fuzzing beyond the deterministic regression;
  • controller/state-machine decomposition;
  • live exact-pair certification, explicitly excluded from this deterministic task (authoritative live pass count: zero).

The initial final-head e2e-base run failed only two unrelated managed-embedding/autoscaling cases after 148 base and 34 inference tests passed. The related IndexRebuilding restore failure from the earlier review was not dismissed: it was fixed and its focused final-head regressions pass. No unrelated product test or implementation was changed.

The PR is current with main. I have not merged it.

restore: RestoreSource,
options: RestoreOptions,
) !?db_mod.generation_lifecycle.StagedGeneration {
if (try restoreSnapshotAlreadyApplied(alloc, path, group_id, restore, options)) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not your fault but maybe you could look into) Not sure why this code is in raft/ seems a little bit of a smell? Does it get used for HA?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch this is a weird + unused merge conflict resolution artifact. i am reverting it to the state of main

const httpx = @import("httpx");
const common = @import("../common/http/http_common.zig");

pub const ZigLeaseExecutor = struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in this package? Should it just be LeaseExecutor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah think you're right about the name - going to change it

//! portable storage artifacts. Keep this module below both layers so decoding
//! a seed never imports the metadata control loop into storage-only binaries.

pub const TableRecord = struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in this package?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex seems to like this particular layout/separation of concerns, what do you make of this?

Codex (GPT-5): This placement is intentional. These topology wire records are consumed by both metadata/table_manager.zig and storage-only HA seed materialization. Defining them under metadata would force the storage layer to import the metadata control-plane dependency tree. Keeping the types in common provides a lower-level shared boundary, while table_manager.zig re-exports them for metadata callers. I think the current package is therefore the right dependency direction.

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.

3 participants