Repository navigation
feat(edgesync): spoke agent and manual sync pass (#569) - #579
Merged
Merged
Conversation
Completes the edge-sync story: an edge Arc now pushes its Parquet files to a hub on demand. The hub receive side, ledger, transport interface, HMAC scheme, reconcile index, and spoke registry landed in #570-#576; this is the client that drives them. A pass recovers transfers interrupted by a crash, discovers new files, reconciles the backlog in one round-trip, then streams what the hub lacks — newest first, so a contact window that closes mid-backlog has already delivered the freshest telemetry. It pages until the backlog drains, so one pass on a spoke returning from a long outage moves everything rather than the first batch. Operator surface (admin-only, on /api/v1/spoke-sync): POST /run run one pass, return what it did GET /status pending/synced/failed counts and sync lag GET /ledger per-file state, attempts, and last error Config lives in [edge_sync.spoke]; the secret is environment-only via ARC_EDGE_SYNC_SPOKE_SECRET. This is the manual form and is OSS; the scheduled agent is Enterprise and lands later. Bugs found and fixed during review, each with a regression test verified to fail against the pre-fix code: - Secret guard used v.IsSet, which consults the environment under AutomaticEnv — so Arc refused to start in the one configuration the guard exists to require. Now v.InConfig, which reads only the file. - Run fetched a single page: it sent batch_size files, reported success, and silently stranded the rest. - A negative batch_size reached make()'s capacity argument and panicked the spoke on its first pass. Clamped in NewAgent, rejected at load. - Fiber's Group().Use() matches by string prefix, not path segment, so the hub's group at /api/v1/sync also matched /api/v1/sync-spoke/*, and its body limit ran on the operator routes. Moved to /api/v1/spoke-sync, with a test pinning the property rather than the name. - The ledger view showed only pending entries, hiding the exhausted files it exists to diagnose. Added Ledger.Unfinished. - Reconcile-path conflicts stayed pending and rode along in every future reconcile payload forever. Added Ledger.MarkConflicted, since MarkFailed requires in_flight and silently matched no rows. Also: max_concurrent is bounded (64), cancellation is checked before the select rather than as a case (both being ready made it a coin flip), hub_url is parsed rather than prefix-matched, and every new key has a v.SetDefault. Test plan: - [x] 37 new tests across agent, spoke API, e2e, and config - [x] e2e drives the real agent against real hub handlers over real HTTP - [x] go test -race ./internal/edgesync/ — clean - [x] two live processes: 20 files, batch_size=7, all byte-identical on the hub, idempotent re-run, no staging residue - [x] non-default max_attempts/max_concurrent/batch_size exercised - [x] auth boundary: forged MAC 401, unknown spoke 401, wrong secret writes nothing - [x] negative batch_size refused at startup with a clear message - [x] go vet, gofmt clean
9 tasks done
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.
Summary
Completes the edge-sync story: an edge Arc now pushes its Parquet files to a hub on demand. The hub receive side, ledger, transport interface, HMAC scheme, reconcile index, and spoke registry landed in #570–#576; this is the client that drives them. Closes item 8 of #569.
A pass recovers transfers interrupted by a crash, discovers new files, reconciles the backlog in one round-trip, then streams what the hub lacks — newest first, so a contact window that closes mid-backlog has already delivered the freshest telemetry. It pages until the backlog drains, so one pass on a spoke returning from a long outage moves everything rather than the first batch.
Operator surface (admin-only, on
/api/v1/spoke-sync):POST /runGET /statusGET /ledgerThe issue planned
/api/v1/sync/run; it ships under/api/v1/spoke-syncbecause Fiber'sGroup().Use()matches by string prefix, not path segment — see the fourth row below.Config lives in
[edge_sync.spoke]; the secret is environment-only viaARC_EDGE_SYNC_SPOKE_SECRET, and Arc refuses to start if one appears in the config file. This is the manual form and is OSS; the scheduled agent is Enterprise and lands later.Bugs found and fixed during review
Each has a regression test verified to fail against the pre-fix code:
v.IsSetAutomaticEnv, so Arc refused to start in the one configuration the guard exists to require. Nowv.InConfig, which reads only the file.Runfetched a single pagebatch_sizefiles, reported success, and silently stranded the rest.batch_sizemake()'s capacity argument and panicked the spoke on its first pass. Clamped inNewAgent, rejected at load.Group().Use()matches by string prefix, so the hub's/api/v1/syncgroup also matched/api/v1/sync-spoke/*and its body limit ran on the operator routes. Moved to/api/v1/spoke-sync, with a test pinning the property rather than the name.Ledger.Unfinished.Ledger.MarkConflicted—MarkFailedrequiresin_flightand silently matched no rows.Also:
max_concurrentis bounded (64), cancellation is checked before theselectrather than as a case (both being ready made it a coin flip, ~50% of transfers still launching),hub_urlis parsed rather than prefix-matched, and every new key has av.SetDefault.Test plan
go test -count=1 -race ./internal/edgesync/ ./internal/api/— clean on the rebuilt treebatch_size=7, all byte-identical on the hub, idempotent re-run, no staging residuemax_attempts/max_concurrent/batch_sizeexercised togetherARC_ENCRYPTION_KEYrotation → startup warning + spoke fails closedbatch_sizerefused at startup with a clear messagego vet,gofmtcleanFollow-ups
🤖 Generated with Claude Code