fix(dispatcher,mcp-go): 配置拉取改为后台重试,根治启动竞态
此前 dispatcher(chat)/mcp-go(embedding) 启动时一次性请求控制面配置,3s 扑空即 降级,且只能干等热更新广播——若消费方早于 gateway 启动,会全程降级(LLM 跑桩、 RAG 无向量),必须手动重启才恢复。 改为:先订阅热更新,再后台 RequestConfigWithRetry(重试至拿到配置,容忍 gateway 晚启)。新增 shared/bus.RequestConfigWithRetry + dispatcher Subscriber 包装。 验收:故意先起 dispatcher/mcp-go、后起 gateway,二者自动重试拿到 chat/embedding 配置,无需手动重启;make test-go 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,17 +29,12 @@ func main() {
|
|||||||
sub := dnats.MustConnect(natsURL)
|
sub := dnats.MustConnect(natsURL)
|
||||||
defer sub.Close()
|
defer sub.Close()
|
||||||
|
|
||||||
// 配置控制面:启动时取激活模型配置,并订阅热更新。
|
// 配置控制面:先订阅热更新,再后台重试拉初始配置(容忍 gateway 晚于本服务启动,
|
||||||
cctx, ccancel := context.WithTimeout(context.Background(), 3*time.Second)
|
// 避免一次性请求扑空后只能干等热更新 → 降级桩跑全程)。
|
||||||
if cfg, _ := sub.RequestModelConfig(cctx); cfg != nil {
|
|
||||||
pool.SetConfig(cfg)
|
|
||||||
} else {
|
|
||||||
log.Println("[dispatcher] 未取到在线模型配置,降级桩运行(控制台配置后将热更新)")
|
|
||||||
}
|
|
||||||
ccancel()
|
|
||||||
if _, err := sub.SubscribeModelConfigUpdated(pool.SetConfig); err != nil {
|
if _, err := sub.SubscribeModelConfigUpdated(pool.SetConfig); err != nil {
|
||||||
log.Printf("[dispatcher] subscribe model config: %v", err)
|
log.Printf("[dispatcher] subscribe model config: %v", err)
|
||||||
}
|
}
|
||||||
|
go sub.FetchModelConfigWithRetry(context.Background(), pool.SetConfig)
|
||||||
|
|
||||||
// sub 同时作为 Token 回流(TokenSink)、MCP 工具调用(ToolCaller)、执行事件(ExecSink)与任务状态回写(StatusSink)出口。
|
// sub 同时作为 Token 回流(TokenSink)、MCP 工具调用(ToolCaller)、执行事件(ExecSink)与任务状态回写(StatusSink)出口。
|
||||||
orch, err := eino.NewOrchestrator(pool, breaker, eval, sub, sub, sub, sub)
|
orch, err := eino.NewOrchestrator(pool, breaker, eval, sub, sub, sub, sub)
|
||||||
|
|||||||
@@ -91,4 +91,9 @@ func (s *Subscriber) SubscribeModelConfigUpdated(onUpdate func(*contract.ModelCo
|
|||||||
return s.inner.SubscribeConfigUpdated(contract.ConfigKindChat, onUpdate)
|
return s.inner.SubscribeConfigUpdated(contract.ConfigKindChat, onUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchModelConfigWithRetry 后台重试拉取初始对话模型配置(容忍 gateway 晚于 dispatcher 启动)。
|
||||||
|
func (s *Subscriber) FetchModelConfigWithRetry(ctx context.Context, apply func(*contract.ModelConfig)) {
|
||||||
|
s.inner.RequestConfigWithRetry(ctx, contract.ConfigKindChat, apply)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Subscriber) Close() { s.inner.Close() }
|
func (s *Subscriber) Close() { s.inner.Close() }
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
|
||||||
sharedbus "github.com/sundynix/sundynix-shared/bus"
|
sharedbus "github.com/sundynix/sundynix-shared/bus"
|
||||||
"github.com/sundynix/sundynix-shared/contract"
|
"github.com/sundynix/sundynix-shared/contract"
|
||||||
@@ -70,22 +69,16 @@ func main() {
|
|||||||
ragEngine.SetChat(cfg.BaseURL, cfg.APIKey, cfg.Model)
|
ragEngine.SetChat(cfg.BaseURL, cfg.APIKey, cfg.Model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cctx, ccancel := context.WithTimeout(ctx, 3*time.Second)
|
// 先订阅热更新(控制台改配置即生效)。
|
||||||
if cfg, _ := b.RequestConfig(cctx, contract.ConfigKindEmbedding); cfg != nil {
|
|
||||||
applyEmbed(cfg)
|
|
||||||
} else {
|
|
||||||
log.Println("[mcp_go] 未取到 embedding 控制面配置(用 env 或降级)")
|
|
||||||
}
|
|
||||||
if cfg, _ := b.RequestConfig(cctx, contract.ConfigKindChat); cfg != nil {
|
|
||||||
applyChat(cfg)
|
|
||||||
}
|
|
||||||
ccancel()
|
|
||||||
if _, err := b.SubscribeConfigUpdated(contract.ConfigKindChat, applyChat); err != nil {
|
if _, err := b.SubscribeConfigUpdated(contract.ConfigKindChat, applyChat); err != nil {
|
||||||
log.Printf("[mcp_go] subscribe chat config: %v", err)
|
log.Printf("[mcp_go] subscribe chat config: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := b.SubscribeConfigUpdated(contract.ConfigKindEmbedding, applyEmbed); err != nil {
|
if _, err := b.SubscribeConfigUpdated(contract.ConfigKindEmbedding, applyEmbed); err != nil {
|
||||||
log.Printf("[mcp_go] subscribe embedding config: %v", err)
|
log.Printf("[mcp_go] subscribe embedding config: %v", err)
|
||||||
}
|
}
|
||||||
|
// 后台重试拉初始配置:容忍 gateway 晚于本服务启动(避免一次性扑空致 RAG 长期降级)。
|
||||||
|
go b.RequestConfigWithRetry(ctx, contract.ConfigKindEmbedding, applyEmbed)
|
||||||
|
go b.RequestConfigWithRetry(ctx, contract.ConfigKindChat, applyChat)
|
||||||
|
|
||||||
gw := mcp.NewGateway(b, engine, mem, hist, ragEngine)
|
gw := mcp.NewGateway(b, engine, mem, hist, ragEngine)
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,26 @@ func (b *Bus) RequestConfig(ctx context.Context, kind string) (*contract.ModelCo
|
|||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestConfigWithRetry 后台重试拉取某 kind 的初始配置,直到成功或重试耗尽。
|
||||||
|
// 容忍消费方(dispatcher/mcp-go)早于控制面(gateway)启动——一次性请求扑空后不再干等热更新。
|
||||||
|
// 拿到即调 apply 并返回;ctx 取消或重试上限到则放弃(此后仍可由热更新广播兜底)。
|
||||||
|
func (b *Bus) RequestConfigWithRetry(ctx context.Context, kind string, apply func(*contract.ModelConfig)) {
|
||||||
|
for i := 0; i < 60; i++ {
|
||||||
|
cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||||
|
cfg, _ := b.RequestConfig(cctx, kind)
|
||||||
|
cancel()
|
||||||
|
if cfg != nil {
|
||||||
|
apply(cfg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ServeConfig 让控制面响应某 kind 的配置请求;provide 返回当前激活配置(可为 nil)。
|
// ServeConfig 让控制面响应某 kind 的配置请求;provide 返回当前激活配置(可为 nil)。
|
||||||
func (b *Bus) ServeConfig(kind string, provide func() *contract.ModelConfig) (unsub func() error, err error) {
|
func (b *Bus) ServeConfig(kind string, provide func() *contract.ModelConfig) (unsub func() error, err error) {
|
||||||
sub, err := b.nc.Subscribe(contract.ConfigGetSubject(kind), func(m *nats.Msg) {
|
sub, err := b.nc.Subscribe(contract.ConfigGetSubject(kind), func(m *nats.Msg) {
|
||||||
|
|||||||
Reference in New Issue
Block a user