Version: 0.4.9.dev.260309
feat: 🗄️ 新增自动建表功能 * 新增项目启动时自动建表能力,减少手动初始化数据库步骤 * 解决 `agent_chat` 与 `chat_history` 结构体互相持有对方结构体用于 `preload` 导致的循环依赖问题 * 修复因结构体互相依赖引发的建表失败问题,保证数据库初始化流程稳定 feat: 🐳 Docker Compose 引入 Kafka 分区自动初始化 * 更新 `docker-compose` 配置,引入 Kafka partition 自动初始化脚本 * 保证服务启动后 Topic 即具备可用 partition,实现开箱即用 * 修复转移环境后 MySQL 等容器数据无法持久化的问题,统一改为使用命名卷进行数据持久化 docs: 📚 补充 Outbox + Kafka 持久化链路注释 * 为 Outbox + Kafka 消息持久化链路补充详细代码注释 * 提升异步消息链路的可读性与维护性 * 当前代码 Review 进度约 50% undo: ⚠️ Kafka 初始化阶段出现消息短暂堆积 * 初次初始化项目时观察到消息在 Kafka 中短暂堆积现象 * 后续被消费者一次性消费且未再次复现 * 已在生产者启动、消费者启动以及消息消费流程中增加控制台日志输出,降低系统黑箱程度 * 后续若条件允许将进一步排查该现象的触发原因
This commit is contained in:
@@ -11,6 +11,11 @@ import (
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// OutboxDAO 封装 outbox 表读写逻辑。
|
||||
// outbox 状态机约定:
|
||||
// pending -> published -> consumed(成功终态)
|
||||
// pending/published -> pending(失败重试)
|
||||
// pending/published -> dead(不可恢复或达到最大重试)
|
||||
type OutboxDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
@@ -19,6 +24,11 @@ func NewOutboxDAO(db *gorm.DB) *OutboxDAO {
|
||||
return &OutboxDAO{db: db}
|
||||
}
|
||||
|
||||
// CreateChatHistoryMessage 创建“聊天记录持久化”的 outbox 消息。
|
||||
// 关键点:
|
||||
// 1) 初始状态为 pending;
|
||||
// 2) NextRetryAt=now,允许被“首次同步投递”或“扫描器”立即处理;
|
||||
// 3) payload 以 JSON 形式落表,保证消费端可重放。
|
||||
func (d *OutboxDAO) CreateChatHistoryMessage(ctx context.Context, topic, messageKey string, payload model.ChatHistoryPersistPayload, maxRetry int) (int64, error) {
|
||||
if maxRetry <= 0 {
|
||||
maxRetry = 20
|
||||
@@ -52,6 +62,8 @@ func (d *OutboxDAO) GetByID(ctx context.Context, id int64) (*model.AgentOutboxMe
|
||||
return &msg, nil
|
||||
}
|
||||
|
||||
// ListDueMessages 查询“到期可重试”的 pending 消息。
|
||||
// 查询条件:status=pending 且 next_retry_at<=当前时间。
|
||||
func (d *OutboxDAO) ListDueMessages(ctx context.Context, limit int) ([]model.AgentOutboxMessage, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
@@ -69,7 +81,10 @@ func (d *OutboxDAO) ListDueMessages(ctx context.Context, limit int) ([]model.Age
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
// MarkPublished 仅在消息未进入最终态时更新为 published,避免覆盖 consumed/dead。
|
||||
// MarkPublished 将消息标记为“已写入 Kafka”。
|
||||
// 注意:
|
||||
// 1) 仅在非终态(非 consumed/dead)下更新,避免覆盖最终状态;
|
||||
// 2) 清理 next_retry_at,避免已投递消息继续被扫描器重复拉取。
|
||||
func (d *OutboxDAO) MarkPublished(ctx context.Context, id int64) error {
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
@@ -85,6 +100,7 @@ func (d *OutboxDAO) MarkPublished(ctx context.Context, id int64) error {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
// MarkDead 将消息置为死信终态。
|
||||
func (d *OutboxDAO) MarkDead(ctx context.Context, id int64, reason string) error {
|
||||
now := time.Now()
|
||||
lastErr := truncateError(reason)
|
||||
@@ -97,6 +113,11 @@ func (d *OutboxDAO) MarkDead(ctx context.Context, id int64, reason string) error
|
||||
return d.db.WithContext(ctx).Model(&model.AgentOutboxMessage{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
// MarkFailedForRetry 在失败时推进重试状态。
|
||||
// 关键点:
|
||||
// 1) 事务 + FOR UPDATE 防并发覆盖(尤其是 dispatch/consume 并发场景);
|
||||
// 2) retry_count 自增;
|
||||
// 3) 达到 max_retry 后转 dead,否则按指数退避设置 next_retry_at。
|
||||
func (d *OutboxDAO) MarkFailedForRetry(ctx context.Context, id int64, reason string) error {
|
||||
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var msg model.AgentOutboxMessage
|
||||
@@ -104,6 +125,7 @@ func (d *OutboxDAO) MarkFailedForRetry(ctx context.Context, id int64, reason str
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 终态直接跳过,保持幂等。
|
||||
if msg.Status == model.OutboxStatusConsumed || msg.Status == model.OutboxStatusDead {
|
||||
return nil
|
||||
}
|
||||
@@ -131,6 +153,8 @@ func (d *OutboxDAO) MarkFailedForRetry(ctx context.Context, id int64, reason str
|
||||
})
|
||||
}
|
||||
|
||||
// PersistChatHistoryAndMarkConsumed 执行“消费业务”并回写 consumed。
|
||||
// 这里把“写 chat_histories”与“更新 outbox 状态”放进同一事务,保证原子性。
|
||||
func (d *OutboxDAO) PersistChatHistoryAndMarkConsumed(ctx context.Context, outboxID int64, payload model.ChatHistoryPersistPayload) error {
|
||||
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var outboxMsg model.AgentOutboxMessage
|
||||
@@ -141,6 +165,7 @@ func (d *OutboxDAO) PersistChatHistoryAndMarkConsumed(ctx context.Context, outbo
|
||||
}
|
||||
return err
|
||||
}
|
||||
// 幂等保护:重复消费不重复落库。
|
||||
if outboxMsg.Status == model.OutboxStatusConsumed {
|
||||
return nil
|
||||
}
|
||||
@@ -172,6 +197,7 @@ func (d *OutboxDAO) PersistChatHistoryAndMarkConsumed(ctx context.Context, outbo
|
||||
})
|
||||
}
|
||||
|
||||
// calcRetryBackoff 指数退避(上限 2^5=32 秒)。
|
||||
func calcRetryBackoff(retryCount int) time.Duration {
|
||||
if retryCount <= 0 {
|
||||
return time.Second
|
||||
|
||||
Reference in New Issue
Block a user