mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: hot-reload aibridged and aibridgeproxyd providers on DB changes (#25673)
Previously the in-process aibridge daemon and the enterprise aibridgeproxy daemon both snapshotted their provider routing once at boot. Any `ai_providers` or `ai_provider_keys` mutation required a restart for either to pick it up. Add an `ai_providers_changed` pubsub channel that the CRUD handlers publish on after Create / Update / Delete. Both daemons subscribe: - **aibridged** rebuilds its `[]aibridge.Provider` snapshot via `BuildProviders` and swaps it into the pool atomically. Inflight requests keep serving against the bridge they already acquired; new acquires build against the new snapshot. Per-provider construction errors stay scoped to the offending row. - **aibridgeproxyd** rebuilds its routing snapshot from `GetAIProviders` and swaps the host→provider map atomically. The MITM listener picks up new providers without restart. DB read for aibridgeproxyd uses the existing `AsAIProviderMetadataReader` subject for routing-only access.
This commit is contained in:
+50
-4
@@ -24,7 +24,12 @@ import (
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func newAIBridgeDaemon(coderAPI *coderd.API, providers []aibridge.Provider) (*aibridged.Server, error) {
|
||||
// newAIBridgeDaemon constructs the in-memory aibridge daemon and wires
|
||||
// up a subscription that hot-reloads the provider pool from the
|
||||
// database on every ai_providers change event. The returned unsubscribe
|
||||
// function tears down the subscription; callers must invoke it
|
||||
// alongside Server.Close on shutdown.
|
||||
func newAIBridgeDaemon(coderAPI *coderd.API, providers []aibridge.Provider, cfg codersdk.AIBridgeConfig) (*aibridged.Server, func(), error) {
|
||||
ctx := context.Background()
|
||||
coderAPI.Logger.Debug(ctx, "starting in-memory aibridge daemon")
|
||||
|
||||
@@ -37,7 +42,25 @@ func newAIBridgeDaemon(coderAPI *coderd.API, providers []aibridge.Provider) (*ai
|
||||
// Create pool for reusable stateful [aibridge.RequestBridge] instances (one per user).
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger.Named("pool"), metrics, tracer) // TODO: configurable size.
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create request pool: %w", err)
|
||||
return nil, nil, xerrors.Errorf("create request pool: %w", err)
|
||||
}
|
||||
|
||||
// Subscribe to ai_providers change events so the pool tracks the
|
||||
// database without a restart. The boot-time `providers` snapshot
|
||||
// derives from env config and serves as a fallback if the database
|
||||
// load fails inside the reloader.
|
||||
reloader := &poolDBReloader{
|
||||
pool: pool,
|
||||
db: coderAPI.Database,
|
||||
cfg: cfg,
|
||||
logger: logger.Named("provider-loader"),
|
||||
}
|
||||
unsubscribe, err := aibridged.SubscribeProviderReload(ctx, coderAPI.Pubsub, reloader, logger.Named("provider-reload"))
|
||||
if err != nil {
|
||||
// Pool is still usable with the boot-time snapshot; subscription
|
||||
// failure is logged but not fatal so the daemon still serves.
|
||||
logger.Warn(ctx, "subscribe to ai providers change channel", slog.Error(err))
|
||||
unsubscribe = func() {}
|
||||
}
|
||||
|
||||
// Create daemon.
|
||||
@@ -45,9 +68,32 @@ func newAIBridgeDaemon(coderAPI *coderd.API, providers []aibridge.Provider) (*ai
|
||||
return coderAPI.CreateInMemoryAIBridgeServer(dialCtx)
|
||||
}, logger, tracer)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("start in-memory aibridge daemon: %w", err)
|
||||
unsubscribe()
|
||||
return nil, nil, xerrors.Errorf("start in-memory aibridge daemon: %w", err)
|
||||
}
|
||||
return srv, nil
|
||||
return srv, unsubscribe, nil
|
||||
}
|
||||
|
||||
// poolDBReloader implements [aibridged.ProviderReloader] by loading
|
||||
// the live provider set from the database and forwarding it to the
|
||||
// pool.
|
||||
type poolDBReloader struct {
|
||||
pool *aibridged.CachedBridgePool
|
||||
db database.Store
|
||||
cfg codersdk.AIBridgeConfig
|
||||
logger slog.Logger
|
||||
}
|
||||
|
||||
func (r *poolDBReloader) Reload(ctx context.Context) error {
|
||||
providers, err := BuildProviders(ctx, r.db, r.cfg, r.logger)
|
||||
if err != nil {
|
||||
// Keep the previous snapshot in place: dropping all providers
|
||||
// because the DB read failed would compound the visible failure
|
||||
// mode beyond the operator's actual misconfiguration.
|
||||
return xerrors.Errorf("load ai providers from database: %w", err)
|
||||
}
|
||||
r.pool.ReplaceProviders(providers)
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildProviders loads every enabled ai_providers row, attaches its
|
||||
|
||||
+3
-1
@@ -1046,7 +1046,8 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
if err != nil {
|
||||
return xerrors.Errorf("build AI providers: %w", err)
|
||||
}
|
||||
aibridgeDaemon, err = newAIBridgeDaemon(coderAPI, aibridgeProviders)
|
||||
var unsubscribeProviderReload func()
|
||||
aibridgeDaemon, unsubscribeProviderReload, err = newAIBridgeDaemon(coderAPI, aibridgeProviders, vals.AI.BridgeConfig)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create aibridged: %w", err)
|
||||
}
|
||||
@@ -1055,6 +1056,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
// daemon does not affect in-flight requests but is needed to
|
||||
// release pool/recorder resources at shutdown.
|
||||
defer aibridgeDaemon.Close()
|
||||
defer unsubscribeProviderReload()
|
||||
}
|
||||
|
||||
if vals.Prometheus.Enable {
|
||||
|
||||
Reference in New Issue
Block a user