fix: harden message seq allocation against stale redis replicas
- use sentinel failover client for group message runtime when sentinel_master_name/sentinel_addrs are configured, so seq counter reads never hit a stale replica after failover - re-seed missing redis seq counter from DB topic seqid instead of trusting the caller in-memory baseline - on unique-key conflict during async persist, re-allocate seq from DB latest, bump redis counter and fix the idempotency record so resends with the same client mid return the final seq - add uk(topic, seqid) unique keys for im_messages in tenant DDL
This commit is contained in:
+55
-20
@@ -3597,7 +3597,7 @@ func (messagesMapper) PreparePersist(tenantID types.TenantID, msg *types.Message
|
||||
return nil, types.ErrDuplicate
|
||||
}
|
||||
}
|
||||
seq, err := groupMsgRuntime.NextSeq(tenantID, msg.Topic, msg.SeqId-1)
|
||||
seq, err := nextMessageSeqWithFloor(tenantID, msg.Topic, msg.SeqId-1)
|
||||
if err != nil {
|
||||
if clientMsgID != "" {
|
||||
_ = groupMsgRuntime.MarkFailed(tenantID, msg.Topic, fromUid, clientMsgID)
|
||||
@@ -3661,21 +3661,21 @@ func (messagesMapper) PersistPrepared(tenantID types.TenantID, msg *types.Messag
|
||||
err = tenantAdp.TenantMessageSave(tenantID, msg)
|
||||
}
|
||||
if err != nil {
|
||||
if isGroupMessage && err == types.ErrDuplicate && clientMsgID != "" {
|
||||
duplicate, dupErr := tenantAdp.TenantGroupMessageGetByClientID(tenantID, msg.Topic, fromUid, clientMsgID)
|
||||
if dupErr != nil {
|
||||
return nil, dupErr
|
||||
}
|
||||
if duplicate != nil {
|
||||
msg.SeqId = duplicate.SeqId
|
||||
if groupMsgRuntime != nil {
|
||||
_ = groupMsgRuntime.MarkDone(tenantID, msg.Topic, fromUid, clientMsgID, duplicate.SeqId, duplicate.Uid().String())
|
||||
if err == types.ErrDuplicate {
|
||||
if isGroupMessage && clientMsgID != "" {
|
||||
duplicate, dupErr := tenantAdp.TenantGroupMessageGetByClientID(tenantID, msg.Topic, fromUid, clientMsgID)
|
||||
if dupErr != nil {
|
||||
return nil, dupErr
|
||||
}
|
||||
if duplicate != nil {
|
||||
msg.SeqId = duplicate.SeqId
|
||||
if groupMsgRuntime != nil {
|
||||
_ = groupMsgRuntime.MarkDone(tenantID, msg.Topic, fromUid, clientMsgID, duplicate.SeqId, duplicate.Uid().String())
|
||||
}
|
||||
return &types.MessageSaveResult{SeqId: duplicate.SeqId, Duplicate: true}, nil
|
||||
}
|
||||
return &types.MessageSaveResult{SeqId: duplicate.SeqId, Duplicate: true}, nil
|
||||
}
|
||||
}
|
||||
if !isGroupMessage && err == types.ErrDuplicate {
|
||||
if clientMsgID != "" {
|
||||
if !isGroupMessage && clientMsgID != "" {
|
||||
duplicate, dupErr := tenantAdp.TenantMessageGetByClientID(tenantID, fromUid, clientMsgID)
|
||||
if dupErr != nil {
|
||||
return nil, dupErr
|
||||
@@ -3685,12 +3685,14 @@ func (messagesMapper) PersistPrepared(tenantID types.TenantID, msg *types.Messag
|
||||
return &types.MessageSaveResult{SeqId: duplicate.SeqId, Duplicate: true}, nil
|
||||
}
|
||||
}
|
||||
if retryErr := retryTenantMessageSaveAfterSeqConflict(tenantAdp, tenantID, msg); retryErr != nil {
|
||||
if retryErr := retryTenantMessageSaveAfterSeqConflict(tenantAdp, tenantID, msg, isGroupMessage); retryErr != nil {
|
||||
if isGroupMessage && groupMsgRuntime != nil && clientMsgID != "" {
|
||||
_ = groupMsgRuntime.MarkFailed(tenantID, msg.Topic, fromUid, clientMsgID)
|
||||
}
|
||||
return nil, retryErr
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
if isGroupMessage && groupMsgRuntime != nil && clientMsgID != "" {
|
||||
} else if isGroupMessage && groupMsgRuntime != nil && clientMsgID != "" {
|
||||
_ = groupMsgRuntime.MarkFailed(tenantID, msg.Topic, fromUid, clientMsgID)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -3786,19 +3788,52 @@ func NormalizeClientMsgID(head types.KVMap) (string, error) {
|
||||
return normalizeClientMsgID(head)
|
||||
}
|
||||
|
||||
func retryTenantMessageSaveAfterSeqConflict(tenantAdp adapter.TenantBusinessAdapter, tenantID types.TenantID, msg *types.Message) error {
|
||||
latest, err := tenantAdp.TenantMessageGetAll(tenantID, msg.Topic, types.ZeroUid, &types.QueryOpt{Before: 1 << 62, Limit: 1})
|
||||
func retryTenantMessageSaveAfterSeqConflict(tenantAdp adapter.TenantBusinessAdapter, tenantID types.TenantID, msg *types.Message, isGroupMessage bool) error {
|
||||
var latest []types.Message
|
||||
var err error
|
||||
if isGroupMessage {
|
||||
latest, err = tenantAdp.TenantGroupMessageGetAll(tenantID, msg.Topic, types.ZeroUid, &types.QueryOpt{Before: 1 << 62, Limit: 1})
|
||||
} else {
|
||||
latest, err = tenantAdp.TenantMessageGetAll(tenantID, msg.Topic, types.ZeroUid, &types.QueryOpt{Before: 1 << 62, Limit: 1})
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(latest) == 0 || latest[0].SeqId < msg.SeqId {
|
||||
return types.ErrDuplicate
|
||||
}
|
||||
prevSeq := msg.SeqId
|
||||
msg.SeqId = latest[0].SeqId + 1
|
||||
if err = tenantAdp.TenantTopicUpdateOnMessage(tenantID, msg.Topic, msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return tenantAdp.TenantMessageSave(tenantID, msg)
|
||||
if isGroupMessage {
|
||||
err = tenantAdp.TenantGroupMessageSave(tenantID, msg)
|
||||
} else {
|
||||
err = tenantAdp.TenantMessageSave(tenantID, msg)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The re-allocated seq differs from the one handed out before the insert
|
||||
// failed. Sync the Redis allocator and the idempotency record with the
|
||||
// final seq, otherwise subsequent publishes keep colliding with the
|
||||
// persisted row and resends of the same client mid return the abandoned
|
||||
// seq forever.
|
||||
BumpMessageSeqFloor(tenantID, msg.Topic, msg.SeqId)
|
||||
if isGroupMessage {
|
||||
// Drop the pending hot-cache entry the producer wrote for the
|
||||
// abandoned seq so hot-range reads cannot surface a phantom message.
|
||||
if err := InvalidateHotMessages(tenantID, msg.Topic, []types.Range{{Low: prevSeq, Hi: prevSeq + 1}}); err != nil {
|
||||
logs.Warn.Printf("topic[%s]: failed to invalidate abandoned hot message seq=%d: %v", msg.Topic, prevSeq, err)
|
||||
}
|
||||
}
|
||||
if msg.ClientMsgID != "" {
|
||||
if fromUid := types.ParseUid(msg.From); !fromUid.IsZero() {
|
||||
_ = MarkMessageUniqueDone(tenantID, msg.Topic, fromUid, msg.ClientMsgID, msg.SeqId, msg.Uid().String())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeClientMsgID(head types.KVMap) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user