Skip to content

feat: synchronise provider changes with WatchAIProviders - #26797

Merged
dannykopping merged 6 commits into
graphite-base/26797from
danny/aigov-465-provider-reload
Jul 8, 2026
Merged

feat: synchronise provider changes with WatchAIProviders#26797
dannykopping merged 6 commits into
graphite-base/26797from
danny/aigov-465-provider-reload

Conversation

@dannykopping

@dannykopping dannykopping commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Adds a WatchAIProviders streaming RPC to the ProviderConfigurator service so a running standalone AI Gateway refetches its provider set when the provider configuration changes. The server subscribes to AIProvidersChangedChannel (published by the provider CRUD endpoints) and forwards each event as a payload-free signal, plus one signal on subscribe; the gateway calls GetAIProviders on each signal to rebuild its pool. The aibridged API is bumped to v1.2.

Env-seeded providers don't need a signal: seeding finishes before coderd serves the gateway connection, so the gateway's initial fetch already reflects the seeded set. A periodic idempotent reload on the pool reloader is intentionally out of scope here and tracked separately.

Refs https://linear.app/codercom/issue/AIGOV-465/publish-provider-seed-completion-message-after-seedaiprovidersfromenv

Depends on #26605

@linear-code

linear-code Bot commented Jun 29, 2026

Copy link
Copy Markdown

AIGOV-465

dannykopping commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@dannykopping

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@dannykopping dannykopping changed the title feat(coderd/aibridged): push provider changes to gateways via WatchAIProviders feat: push provider changes to gateways via WatchAIProviders Jun 29, 2026
@dannykopping dannykopping changed the title feat: push provider changes to gateways via WatchAIProviders feat: synchronise provider changes with WatchAIProviders Jun 29, 2026
@dannykopping
dannykopping force-pushed the pawel/aigov-315-implement-basic-coder-aibridge-start-sub-command branch from 0195db9 to 7d4af4c Compare June 29, 2026 08:45
@dannykopping
dannykopping force-pushed the danny/aigov-465-provider-reload branch from d64b939 to 8c82616 Compare June 29, 2026 08:45

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-06-29 15:34 UTC by @dannykopping
Spend: $55.25 / $100.00

Review history
  • R1 (2026-06-29): 17 reviewers, 4 Nit, 2 P3, 3 P4, COMMENT. Review
  • R2 (2026-06-29), 4 Nit, 2 P3, 3 P4, COMMENT. Review
  • R3 (2026-06-29): 6 reviewers, 5 Nit, 2 P3, 4 P4, COMMENT. Review
  • R4 (2026-06-29), 5 Nit, 2 P3, 4 P4, COMMENT. Review

deep-review v0.9.0 | Round 4 | a886804..b3291f6

Last posted: Round 4, 11 findings (2 P3, 4 P4, 5 Nit), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Author fixed (6df6fd9) coderd/aibridged/reload.go:89 Backoff resets on stream open before any Recv, allowing tight reconnect loop when server pubsub is broken R1 Hisoka P3, Meruem P3 Yes
CRF-2 P3 Author fixed (6df6fd9) coderd/aibridged/reload.go:62 WatchProviderReload doc promises ctx-only cancellation but client() blocks on independent lifecycle context R1 Knov Yes
CRF-3 P4 Author fixed (6df6fd9) coderd/aibridged/reload.go:41 SubscribeProviderReload skips reload on pubsub delivery errors while WatchAIProviders correctly signals R1 Ryosuke P3, Zoro P4, Bisky P4 Yes
CRF-4 P4 Author fixed (6df6fd9) coderd/aibridgedserver/aibridgedserver_test.go:2601 No test for pubsub-error-triggers-signal contract in WatchAIProviders R1 Chopper Yes
CRF-5 P4 Author fixed (6df6fd9) coderd/aibridged/reload_test.go:107 No test for WatchProviderReload when client() fails to dial (connected=false backoff path untested) R1 Bisky Yes
CRF-6 Nit Author fixed (6df6fd9) enterprise/cli/aigatewaystart.go:134 Use wg.Go instead of Add/go/Done triple (codebase already uses wg.Go) R1 Ging-Go Yes
CRF-7 Nit Author fixed (6df6fd9) coderd/aibridged/reload.go:69 Parameter named client instead of clientFn, breaking package convention R1 Gon Yes
CRF-8 Nit Author contested; panel closed R3 (5/5 close) coderd/aibridged/reload.go:80 Retry pair 50ms/10s duplicated third time without a named constant R1 Gon Yes
CRF-9 Nit Author fixed (6df6fd9) coderd/aibridgedserver/aibridgedserver.go:785 Doc comment carries caller behavior and implementation details beyond the method contract R1 Gon Yes
CRF-10 P4 Author fixed (b3291f6) coderd/aibridgedserver/aibridgedserver_test.go:2622 No test for lifecycle context cancellation in WatchAIProviders R3 Bisky Yes
CRF-11 Nit Author fixed (b3291f6) coderd/aibridgedserver/aibridgedserver_test.go:2719 Missing compile-time interface assertion for captureListenerPubsub R3 Kite Yes

Contested and acknowledged

CRF-8 (Nit, coderd/aibridged/reload.go:80) - Retry pair duplicated without named constant

  • Finding: The 50*time.Millisecond, 10*time.Second retry pair appears three times across the package. Suggested extracting a package-level constant pair so the values don't drift independently.
  • Author defense: "Nah, let's keep it as-is." No technical argument.
  • Panel closure (R3, 5/5): All five panelists (Mafuuu, Pariston, Meruem, Chopper, Kite) closed the finding. The three call sites serve different retry contexts (watch reconnect, daemon reconnect, initial load) where independent tuning is plausible. Coupling via a shared constant would imply they should always change together, which they shouldn't. The retry pair also appears in other packages (provisionerd, tailnet), so a local constant would not govern codebase-wide instances.

Round log

Round 1

Panel. 2 P3, 3 P4, 4 Nit. 0 dropped. Reviewed against 0195db96128..d64b939ad3f.

Round 2

BLOCKED. All 9 findings silent. No PR file changes between rounds (only unrelated frontend files in rebase). No author responses on any thread. No review.

Round 3

Churn guard: PROCEED. 7 fixed, 1 fixed (CRF-3, pre-existing code), 1 contested (CRF-8). Panel: CRF-8 closed (5/5). 1 P4, 1 Nit new. Reviewed against a886804..6df6fd9.

Round 4

Churn guard: PROCEED. CRF-10 fixed, CRF-11 fixed. All findings resolved. Reviewed against a886804..b3291f6.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

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.

Clean work. The streaming RPC design is well-structured: payload-free signals, coalescing via buffered channel, initial signal on subscribe to close the gap between loadProviders and the watch stream. Shutdown ordering in aigatewaystart.go is correct. Test coverage is solid at 57% density, exercising reconnection, reload failure resilience, and clean shutdown.

2 P3, 3 P4, 4 Nit.

"A server that accepts the WatchAIProviders call but closes the stream before sending the initial signal will cause the client to reconnect at 50ms intervals indefinitely. No exponential backoff accumulates because every successful TCP+DRPC handshake resets it." (Hisoka)


coderd/aibridged/reload.go:41

P4 [CRF-3] SubscribeProviderReload returns early on a pubsub delivery error, skipping the reload. WatchAIProviders (aibridgedserver.go:814-817) logs the same error and still calls notify(), triggering a client refetch. The WatchAIProviders behavior is correct: a dropped pubsub message means a possibly-missed CRUD event, so reloading ensures convergence. The embedded path's skip-on-error means a persistent delivery failure silently prevents convergence until the next clean event.

This is pre-existing code, but the inconsistency now matters: the standalone path handles this scenario correctly while the embedded path does not. The fix is one line: remove the early return so the callback falls through to reload. (Ryosuke P3, Zoro P4, Bisky P4)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/aibridged/reload.go Outdated
Comment thread coderd/aibridged/reload.go Outdated
Comment thread coderd/aibridgedserver/aibridgedserver_test.go
Comment thread coderd/aibridged/reload_test.go
Comment thread enterprise/cli/aigatewaystart.go
Comment thread coderd/aibridged/reload.go Outdated
Comment thread coderd/aibridged/reload.go
Comment thread coderd/aibridgedserver/aibridgedserver.go Outdated

@coder-agents-review coder-agents-review Bot left a comment

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.

Round 2 is blocked. The PR files are unchanged since round 1 (the head SHA changed only due to a stack rebase), and none of the 9 open findings have an author response or code fix.

Unaddressed findings:

  • CRF-1 (P3): Backoff resets on stream open before any Recv, allowing tight reconnect loop.
  • CRF-2 (P3): WatchProviderReload doc promises ctx-only cancellation but client() blocks independently.
  • CRF-3 (P4): SubscribeProviderReload skips reload on pubsub delivery errors; WatchAIProviders correctly signals.
  • CRF-4 (P4): No test for pubsub-error-triggers-signal contract.
  • CRF-5 (P4): No test for client() dial failure path.
  • CRF-6 (Nit): Use wg.Go instead of Add/go/Done triple.
  • CRF-7 (Nit): Parameter named client instead of clientFn.
  • CRF-8 (Nit): Retry pair duplicated without named constant.
  • CRF-9 (Nit): Doc comment carries content beyond the method contract.

Further review is blocked until the author responds to or pushes fixes for the open findings. At minimum, the two P3s (CRF-1, CRF-2) need a response: fix, contest, or defer with a ticket.

🤖 This review was automatically generated with Coder Agents.

@dannykopping
dannykopping force-pushed the danny/aigov-465-provider-reload branch from 8c82616 to dfee053 Compare June 29, 2026 09:56
@dannykopping
dannykopping force-pushed the pawel/aigov-315-implement-basic-coder-aibridge-start-sub-command branch 2 times, most recently from 98e6909 to a886804 Compare June 29, 2026 11:47
@dannykopping
dannykopping force-pushed the danny/aigov-465-provider-reload branch from dfee053 to 6df6fd9 Compare June 29, 2026 11:47

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

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.

All 8 fixed findings from R1 verified. The received flag (CRF-1) correctly prevents backoff reset before any signal is received. The doc comment (CRF-2) now accurately states the ClientFunc dependency. SubscribeProviderReload (CRF-3) now reloads on delivery errors, matching WatchAIProviders. New tests lock the pubsub-error-triggers-signal contract (CRF-4) and the dial-failure backoff path (CRF-5). Test coverage is up to 65% density with 337 test lines.

CRF-8 (retry constant): panel closed 5/5. The three call sites serve different retry contexts where independent tuning is plausible.

1 P4, 1 Nit new. No blocking issues.

"The hint-based design matches the existing pubsub channel, which also carries empty payloads. Pushing full configs would couple the stream schema to the provider model and increase message size for no meaningful latency gain." (Pariston)

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/aibridgedserver/aibridgedserver_test.go
Comment thread coderd/aibridgedserver/aibridgedserver_test.go
@dannykopping
dannykopping marked this pull request as ready for review June 29, 2026 15:34

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

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.

All 11 findings resolved across 4 rounds. CRF-10 and CRF-11 verified fixed in b3291f6: TestWatchAIProvidersStopsOnLifecycleCancel locks the lifecycle cancellation contract, and captureListenerPubsub now has its compile-time interface assertion.

Test coverage is at 67% density (377 test lines / 561 effective lines) covering the critical paths: initial signal, publish signals, delivery errors, reconnection after stream drop, retry after dial failure, continuation after reload failure, and lifecycle context cancellation. No open findings.

🤖 This review was automatically generated with Coder Agents.

@dannykopping
dannykopping requested a review from pawbana June 30, 2026 09:04
@pawbana
pawbana changed the base branch from pawel/aigov-315-implement-basic-coder-aibridge-start-sub-command to graphite-base/26797 June 30, 2026 10:30
Comment thread coderd/aibridged/reload.go Outdated
// before serving.
//
// It runs until ctx is canceled, then returns ctx.Err(). Cancellation requires
// both ctx and the underlying ClientFunc to unblock: during reconnection

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.

It would be nice to be able to unblock ClientFunc using received ctx so it would be clear that there are no go-routines leaking.

I added context aware client acquisition function in later version of base branch: https://github.com/coder/coder/pull/26605/changes#diff-c360083edc6d0d454da4d4c7c4d5d19714deb8daf6e4c33bca9963438e396540R170

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.

Addressed, good call

Comment thread coderd/aibridged/reload.go Outdated
}

r := retry.New(50*time.Millisecond, 10*time.Second)
for {

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.

nit: would be shorter:

for r := retry.New(50*time.Millisecond, 10*time.Second); r.Wait(ctx) {
    ....
}

return ctx.Err()

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.

This would wait 50ms before the first attempt, no?

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.

I think first Wait should be instant since Delay is set to 0 in New.
Later Delay is set to Floor on return which will take effect on next call.

Comment thread coderd/aibridged/reload_test.go Outdated
Comment thread coderd/aibridged/reload_test.go Outdated
…Providers

Add a WatchAIProviders streaming RPC to the ProviderConfigurator service so
a running standalone AI Gateway refetches its provider set when the provider
configuration changes. The server forwards AIProvidersChangedChannel events
(published by the provider CRUD endpoints) as payload-free signals; the
gateway refetches via GetAIProviders on each signal. Bumps the aibridged API
to v1.2.
- Use context.AfterFunc to bind stream lifecycle to the server (CRF-2)
- Log pubsub delivery errors in the watch callback (CRF-7)
- Clarify the reload-failure warn log (CRF-3)
- Drain the standalone gateway watch goroutine before srv.Close() (CRF-4)
- Add a test that a failed reload does not stop the watch loop (CRF-1)
- Rename import alias coderpubsub to coderdpubsub (CRF-6)
- Drop comments that restate the code (CRF-5)
srv.Close cancels the daemon lifecycle context, the only context
WatchProviderReload's blocking Client() call observes. Call it before
watchWG.Wait() so a watch goroutine waiting to reconnect unblocks on the HTTP
server error path instead of hanging. Close is idempotent (CRF-8).
- Reset watch backoff only after a signal is received, avoiding a tight
  reconnect loop when the server fails before the first Recv.
- Document that WatchProviderReload cancellation needs both ctx and the
  ClientFunc to unblock.
- Reload on dropped-message delivery errors in SubscribeProviderReload to
  match WatchAIProviders reconverge semantics.
- Rename the ClientFunc parameter to clientFn for package consistency.
- Use sync.WaitGroup.Go in the standalone gateway watch goroutine.
- Trim the WatchAIProviders doc comment and proto response comment.
- Add tests for the pubsub-error signal path and the dial-failure backoff
  path.
…face

- Add TestWatchAIProvidersStopsOnLifecycleCancel to lock that canceling the
  server lifecycle context stops the handler while the stream context stays
  open, guarding against a shutdown goroutine leak.
- Add a compile-time interface assertion for captureListenerPubsub to match
  the sibling test helpers.
Add ClientFuncWithContext and use it for WatchProviderReload and
PoolRPCReloader, so blocking client acquisition (waiting for the daemon
to connect to coderd) unblocks when the caller's context is canceled.

Previously the reloader acquired its client via a constructor-time
closure that ignored Reload's context. On the standalone gateway's
serve-error exit path, a watch goroutine blocked inside Reload would
not observe watchCancel, and watchWG.Wait deadlocked unless srv.Close
ran first. The srv.Close drain workaround is now unnecessary and
removed; TestWatchProviderReloadCancelUnblocksClient locks the
cancellation contract.
@dannykopping
dannykopping force-pushed the graphite-base/26797 branch from a886804 to 48bc957 Compare July 7, 2026 12:37
@dannykopping
dannykopping force-pushed the danny/aigov-465-provider-reload branch from b3291f6 to 3b84826 Compare July 7, 2026 12:37
@dannykopping
dannykopping changed the base branch from graphite-base/26797 to pawel/aigov-315-implement-basic-coder-aibridge-start-sub-command July 7, 2026 12:37
@dannykopping
dannykopping requested a review from pawbana July 7, 2026 12:46
@pawbana
pawbana changed the base branch from pawel/aigov-315-implement-basic-coder-aibridge-start-sub-command to graphite-base/26797 July 7, 2026 14:39
@dannykopping
dannykopping merged commit b0021f4 into graphite-base/26797 Jul 8, 2026
35 of 56 checks passed
@dannykopping
dannykopping deleted the danny/aigov-465-provider-reload branch July 8, 2026 09:36
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants