Version: 0.5.0.dev.260310
refactor: ♻️ 调整 Outbox 写入时序并移除 Kafka 首包同步投递逻辑 * 将 `outbox` 表写入逻辑后置到 LLM 请求之后,减少主链路阻塞 * 删除 Codex 生成的 Kafka 首包同步投递抽象逻辑,简化消息发送流程 * 优化 SSE 首字到达时间,整体降低约 1s 延迟 * 当前在请求 LLM 之前的流程全部为 Redis 操作,显著降低 IO 开销 docs: 📊 保留 SSE 链路性能打点逻辑 * 保留原有 SSE 全链路打点计时代码,便于后续性能排查与分析 * 当前默认注释,如需使用可手动启用进行性能调试
This commit is contained in:
@@ -15,11 +15,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AgentAsyncPipeline 负责 outbox 的“异步可靠链路”:
|
||||
// 1) 业务侧写 outbox(pending);
|
||||
// 2) dispatch loop 扫描并投递 Kafka(published);
|
||||
// 3) consume loop 消费并落库(consumed);
|
||||
// 4) 任一步失败按重试策略回到 pending 或 dead。
|
||||
// AgentAsyncPipeline 负责 outbox 扫描、Kafka 投递与消费落库。
|
||||
type AgentAsyncPipeline struct {
|
||||
outboxRepo *dao.OutboxDAO
|
||||
producer *kafkabus.Producer
|
||||
@@ -56,9 +52,6 @@ func NewAgentAsyncPipeline(outboxRepo *dao.OutboxDAO, cfg kafkabus.Config) (*Age
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start 启动两个后台协程:
|
||||
// - startDispatchLoop:扫描 pending 并投递 Kafka
|
||||
// - startConsumeLoop:消费 Kafka 并执行业务落库
|
||||
func (p *AgentAsyncPipeline) Start(ctx context.Context) {
|
||||
if p == nil {
|
||||
return
|
||||
@@ -66,8 +59,6 @@ func (p *AgentAsyncPipeline) Start(ctx context.Context) {
|
||||
|
||||
log.Printf("Kafka async pipeline starting: topic=%s brokers=%v retry_scan=%s batch=%d", p.topic, p.brokers, p.scanEvery, p.scanBatch)
|
||||
if err := kafkabus.WaitTopicReady(ctx, p.brokers, p.topic, 30*time.Second); err != nil {
|
||||
// 首次部署常见情况:broker 已起来但 topic/partition 尚未可用。
|
||||
// 这里明确打印,避免“消息堆积但控制台无提示”的观感。
|
||||
log.Printf("Kafka topic not ready before consume loop start: %v", err)
|
||||
} else {
|
||||
log.Printf("Kafka topic is ready: %s", p.topic)
|
||||
@@ -89,25 +80,22 @@ func (p *AgentAsyncPipeline) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
// EnqueueChatHistoryPersist 是业务侧入口:
|
||||
// 1) 先写 outbox;
|
||||
// 2) 立刻尝试“首发投递”一次(非阻塞主流程);
|
||||
// 3) 失败后由扫描器按 next_retry_at 继续重试。
|
||||
// EnqueueChatHistoryPersist 仅把消息写入 outbox。
|
||||
//
|
||||
// 关键设计:
|
||||
// 1) 不再在请求路径里做“首次同步投递 Kafka”;
|
||||
// 2) 投递统一由 startDispatchLoop 异步扫描执行;
|
||||
// 3) CreateChatHistoryMessage 会设置 next_retry_at=now,扫描器下一轮即可捞取。
|
||||
//
|
||||
// 这样可以把请求链路成本收敛到“写 outbox”,避免 Kafka 写入延迟污染首字和主链路时延。
|
||||
func (p *AgentAsyncPipeline) EnqueueChatHistoryPersist(ctx context.Context, payload model.ChatHistoryPersistPayload) error {
|
||||
if p == nil {
|
||||
return errors.New("Kafka 异步链路未初始化")
|
||||
}
|
||||
outboxID, err := p.outboxRepo.CreateChatHistoryMessage(ctx, p.topic, payload.ConversationID, payload, p.maxRetry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = p.dispatchOne(context.Background(), outboxID); err != nil {
|
||||
log.Printf("outbox 消息 %d 首次投递失败,等待扫描重试: %v", outboxID, err)
|
||||
}
|
||||
return nil
|
||||
_, err := p.outboxRepo.CreateChatHistoryMessage(ctx, p.topic, payload.ConversationID, payload, p.maxRetry)
|
||||
return err
|
||||
}
|
||||
|
||||
// startDispatchLoop 定时扫描 pending 且到期的消息,逐条尝试投递。
|
||||
func (p *AgentAsyncPipeline) startDispatchLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(p.scanEvery)
|
||||
defer ticker.Stop()
|
||||
@@ -134,11 +122,6 @@ func (p *AgentAsyncPipeline) startDispatchLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchOne 执行单条 outbox 投递:
|
||||
// 1) 读取 outbox 行;
|
||||
// 2) 组装 Envelope;
|
||||
// 3) 写 Kafka;
|
||||
// 4) 回写 published。
|
||||
func (p *AgentAsyncPipeline) dispatchOne(ctx context.Context, outboxID int64) error {
|
||||
outboxMsg, err := p.outboxRepo.GetByID(ctx, outboxID)
|
||||
if err != nil {
|
||||
@@ -147,7 +130,6 @@ func (p *AgentAsyncPipeline) dispatchOne(ctx context.Context, outboxID int64) er
|
||||
}
|
||||
return err
|
||||
}
|
||||
// 终态不再重复投递。
|
||||
if outboxMsg.Status == model.OutboxStatusConsumed || outboxMsg.Status == model.OutboxStatusDead {
|
||||
return nil
|
||||
}
|
||||
@@ -159,7 +141,6 @@ func (p *AgentAsyncPipeline) dispatchOne(ctx context.Context, outboxID int64) er
|
||||
}
|
||||
raw, err := json.Marshal(envelope)
|
||||
if err != nil {
|
||||
// 序列化都失败通常是坏数据,直接死信。
|
||||
markErr := p.outboxRepo.MarkDead(ctx, outboxMsg.ID, "序列化 outbox 包装失败: "+err.Error())
|
||||
if markErr != nil {
|
||||
log.Printf("标记 outbox 死信失败(id=%d): %v", outboxMsg.ID, markErr)
|
||||
@@ -178,7 +159,6 @@ func (p *AgentAsyncPipeline) dispatchOne(ctx context.Context, outboxID int64) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// startConsumeLoop 持续从 Kafka 拉取消息并处理。
|
||||
func (p *AgentAsyncPipeline) startConsumeLoop(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
@@ -202,11 +182,9 @@ func (p *AgentAsyncPipeline) startConsumeLoop(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleMessage 先解析 Envelope,再按 biz_type 分发到具体处理器。
|
||||
func (p *AgentAsyncPipeline) handleMessage(ctx context.Context, msg segmentkafka.Message) error {
|
||||
var envelope kafkabus.Envelope
|
||||
if err := json.Unmarshal(msg.Value, &envelope); err != nil {
|
||||
// 包装体坏数据,提交 offset 跳过,避免阻塞分区。
|
||||
_ = p.consumer.Commit(ctx, msg)
|
||||
return fmt.Errorf("解析 Kafka 包装失败: %w", err)
|
||||
}
|
||||
@@ -219,7 +197,6 @@ func (p *AgentAsyncPipeline) handleMessage(ctx context.Context, msg segmentkafka
|
||||
case model.OutboxBizTypeChatHistoryPersist:
|
||||
return p.consumeChatHistory(ctx, msg, envelope)
|
||||
default:
|
||||
// 未知业务类型直接死信并提交,避免反复重试无意义数据。
|
||||
_ = p.outboxRepo.MarkDead(ctx, envelope.OutboxID, "未知业务类型: "+envelope.BizType)
|
||||
if err := p.consumer.Commit(ctx, msg); err != nil {
|
||||
return err
|
||||
@@ -228,10 +205,6 @@ func (p *AgentAsyncPipeline) handleMessage(ctx context.Context, msg segmentkafka
|
||||
}
|
||||
}
|
||||
|
||||
// consumeChatHistory 执行“聊天记录持久化”消费逻辑。
|
||||
// 提交策略说明:
|
||||
// 1) 成功落库后提交;
|
||||
// 2) 失败时先回写 outbox(便于重试/排障),再提交,避免分区阻塞。
|
||||
func (p *AgentAsyncPipeline) consumeChatHistory(ctx context.Context, msg segmentkafka.Message, envelope kafkabus.Envelope) error {
|
||||
var payload model.ChatHistoryPersistPayload
|
||||
if err := json.Unmarshal(envelope.Payload, &payload); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user