fix(coderd): invalidate chatd provider cache on AI provider changes (#26987)

chatd subscribes to `ChatConfigEventChannel` and invalidates its
provider cache on a `providers` event kind, but nothing ever published
that kind. AI provider CRUD only publishes on
`AIProvidersChangedChannel` (consumed by aibridged and aibridgeproxyd),
so chatd's provider cache only converged via its 10 second TTL.

Subscribe chatd to the same `AIProvidersChangedChannel` publish instead
of adding a second publish, per the review feedback on #26207: one
publish, multiple subscribers. The now-unused `ChatConfigEventProviders`
kind is removed so `ChatConfigEvent` stays scoped to model configs, user
prompts, and advisor config, and can't regrow a dead subscriber.

Follow-up to CRF-5 from the review of #25673. Supersedes #26207.

Closes CODAGT-499
This commit is contained in:
Ethan
2026-07-07 00:20:54 +10:00
committed by GitHub
parent 7c78698a6d
commit 2cbc464c72
5 changed files with 47 additions and 12 deletions
+21 -2
View File
@@ -179,6 +179,7 @@ type Server struct {
debugSvcInit sync.Once
configCache *chatConfigCache
configCacheUnsubscribe func()
providerCacheUnsubscribe func()
usageTracker *workspacestats.UsageTracker
clock quartz.Clock
@@ -3300,8 +3301,6 @@ func New(ps pubsub.Pubsub, cfg Config) *Server {
return
}
switch ev.Kind {
case coderdpubsub.ChatConfigEventProviders:
p.configCache.InvalidateProviders()
case coderdpubsub.ChatConfigEventModelConfig:
p.configCache.InvalidateModelConfig(ev.EntityID)
case coderdpubsub.ChatConfigEventUserPrompt:
@@ -3317,6 +3316,22 @@ func New(ps pubsub.Pubsub, cfg Config) *Server {
p.configCacheUnsubscribe = cancelConfigSub
}
cancelProviderSub, err := p.pubsub.SubscribeWithErr(
coderdpubsub.AIProvidersChangedChannel,
func(cbCtx context.Context, _ []byte, err error) {
if err != nil {
p.logger.Warn(cbCtx, "ai providers changed event error", slog.Error(err))
return
}
p.configCache.InvalidateProviders()
},
)
if err != nil {
p.logger.Error(ctx, "subscribe to ai providers changed events", slog.Error(err))
} else {
p.providerCacheUnsubscribe = cancelProviderSub
}
p.ctx = ctx
// Spawn background goroutines that all servers need.
@@ -4903,6 +4918,10 @@ func (p *Server) Close() error {
p.configCacheUnsubscribe = nil
unsub()
}
if unsub := p.providerCacheUnsubscribe; unsub != nil {
p.providerCacheUnsubscribe = nil
unsub()
}
if p.chatWorker != nil {
if err := p.chatWorker.Close(); err != nil {
p.logger.Warn(context.Background(), "failed to close chat worker", slog.Error(err))
@@ -14,6 +14,9 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
coderdpubsub "github.com/coder/coder/v2/coderd/pubsub"
"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
@@ -1198,3 +1201,19 @@ func TestConfigCache_InvalidateAdvisorConfig_BlocksStaleInFlight(t *testing.T) {
require.EqualValues(t, 2, third.MaxUsesPerRun)
require.Equal(t, int32(2), store.advisorConfigCalls.Load())
}
func TestConfigCache_InvalidatesProvidersOnAIProvidersChangedEvent(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{})
// The generation counter only advances through InvalidateProviders,
// so this cannot false-pass via TTL expiry.
gen := server.configCache.providersGeneration()
require.NoError(t, ps.Publish(coderdpubsub.AIProvidersChangedChannel, nil))
require.Eventually(t, func() bool {
return server.configCache.providersGeneration() > gen
}, testutil.WaitShort, testutil.IntervalFast)
}