Skip to content

feat(control-plane): add autonomous Provider Join outbox reconciliation - #45

Merged
flo2517 merged 1 commit into
mainfrom
feat/issue-10-provider-join-outbox-reconciler
Aug 6, 2026
Merged

feat(control-plane): add autonomous Provider Join outbox reconciliation#45
flo2517 merged 1 commit into
mainfrom
feat/issue-10-provider-join-outbox-reconciler

Conversation

@flo2517

@flo2517 flo2517 commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #10 — the last open issue in milestone v0.1 — Provider Join and First Workload.

provider_chain_registrations already persisted the outbox intent transactionally alongside the provider record in CompleteJoin (migration 000002_provider_chain_registration.sql), but nothing drove READY/RETRY rows forward except the Agent retrying CompleteJoin itself — exactly the gap README.md already documented: "Provider Join recovery currently occurs when the Agent retries CompleteJoin; an autonomous outbox reconciler remains future hardening."

Change

internal/providerjoin.Reconciler: polls provider_chain_registrations for READY/RETRY rows due for another attempt, reuses the same idempotent Registrar.EnsureActive + ActivateProvider path CompleteJoin already uses, and on failure schedules a bounded exponential-backoff retry (RETRY, next_attempt_at, last_error) or, after MaxAttempts, an explicit terminal FAILED — never a silent infinite loop. Wired into cmd/controlplane/main.go as go reconciler.Run(ctx), the same pattern already used for orchestrator.Worker.

No schema changes needed — the outbox table already had everything (state, attempt_count, next_attempt_at, last_error).

Acceptance-criteria coverage

  • transactional outbox intent: already existed, unchanged.
  • bounded retry + backoff + idempotency across restarts: new; attempt state lives only in Postgres, never in process memory, so a restart resumes correctly (TestReconcilerSurvivesRestartByReadingAttemptCountFromTheStore).
  • ACTIVE only after finalized chain state: unchanged guarantee — ActivateProvider is only ever called with the Registrar's real finalized block hash/number.
  • terminal failures explicit: FAILED after MaxAttempts, or immediately for unrecoverable malformed data (wrong-length stored key) — never retried again.
  • duplicate delivery: idempotent by construction (DueChainRegistrations only selects READY/RETRY; a FINALIZED row is never reselected) and by the existing Registrar mutex serializing concurrent EnsureActive calls (TestReconcileOnceIsIdempotentAgainstDuplicateDelivery).
  • Redis loss: doesn't apply by design — the reconciler depends only on Postgres, the authoritative store per AGENTS.md.
  • chain unavailability: covered by the backoff/max-attempts tests.

Known, called-out scope limit: distinguishing a genuine on-chain identity conflict from ordinary transient unavailability isn't possible today because blockchainbridge.Registrar returns only untyped errors, and the current single-sudo-bridge trust model (one Control Plane account performs every registration) makes a multi-party conflict essentially unreachable in practice. All EnsureActive failures are treated as retryable-then-terminal, which is the practically meaningful behavior given that constraint.

Verification

$ go build ./...
$ go vet ./...
$ gofmt -l .
$ go test ./...

Full suite green, including 10 new reconciler tests.

🤖 Generated with Claude Code

Closes #10. provider_chain_registrations already persisted the outbox
intent transactionally alongside the provider record in CompleteJoin
(migration 000002), but nothing drove READY/RETRY rows forward except
the Agent retrying CompleteJoin itself -- exactly the documented MVP
gap in README.md ('Provider Join recovery currently occurs when the
Agent retries CompleteJoin; an autonomous outbox reconciler remains
future hardening').

Adds internal/providerjoin.Reconciler: polls provider_chain_registrations
for READY/RETRY rows due for another attempt, reuses the same
idempotent Registrar.EnsureActive + ActivateProvider path CompleteJoin
already uses, and on failure schedules a bounded exponential-backoff
retry (state RETRY, next_attempt_at, last_error) or, after
MaxAttempts, an explicit terminal FAILED state -- never a silent
infinite loop. Wired into cmd/controlplane/main.go as
'go reconciler.Run(ctx)', the same pattern already used for
orchestrator.Worker.

Acceptance-criteria coverage:
- transactional outbox intent: already existed, unchanged.
- bounded retry + backoff + idempotency across restarts: new
  Reconciler; attempt_count/state/next_attempt_at live only in
  Postgres, never in process memory, so a restart resumes correctly
  (TestReconcilerSurvivesRestartByReadingAttemptCountFromTheStore).
- ACTIVE only after finalized chain state: unchanged guarantee,
  ActivateProvider is only ever called with the Registrar's real
  finalized block hash/number.
- terminal failures explicit: FAILED state after MaxAttempts or for
  unrecoverable malformed data (wrong-length stored key), never
  retried again, verified by test.
- duplicate delivery: idempotent by construction (DueChainRegistrations
  only selects READY/RETRY; a FINALIZED row is never reselected) and
  by the existing Registrar mutex serializing concurrent
  EnsureActive calls; covered by
  TestReconcileOnceIsIdempotentAgainstDuplicateDelivery.
- Redis loss: does not apply by design -- the reconciler depends only
  on Postgres (authoritative per AGENTS.md), never Redis.
- chain unavailability: covered by the backoff/max-attempts tests
  above.

Known scope limit, called out rather than overstated: distinguishing
a genuine on-chain identity *conflict* from ordinary transient
unavailability isn't possible today because blockchainbridge.Registrar
returns only untyped errors, and the current single-sudo-bridge trust
model (one Control Plane account performs every registration) makes a
multi-party conflict essentially unreachable in practice. All
EnsureActive failures are treated as retryable-then-terminal.

Verified: go build ./..., go vet ./..., gofmt -l ., go test ./...
(full suite, including 10 new reconciler tests).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@flo2517
flo2517 merged commit 44b6afd into main Aug 6, 2026
4 checks passed
@flo2517
flo2517 deleted the feat/issue-10-provider-join-outbox-reconciler branch August 6, 2026 08:14
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.

Add autonomous Provider Join outbox reconciliation

2 participants