diff --git a/server/store/group_message_runtime.go b/server/store/group_message_runtime.go index 3f0260a..33b2d72 100644 --- a/server/store/group_message_runtime.go +++ b/server/store/group_message_runtime.go @@ -851,7 +851,7 @@ func (r *redisGroupMessageRuntime) StartRealtimeFanoutSubscriber(ctx context.Con continue } if envelope.TenantID.IsValid() && envelope.DedupKey != "" { - id := fmt.Sprintf("%d:%s", envelope.TenantID, envelope.DedupKey) + id := realtimeFanoutDedupClaimID(envelope.TenantID, nodeID, envelope.DedupKey) claimed, err := r.ClaimShortState("rt_dedup", id, 5*time.Minute) if err != nil || !claimed { continue @@ -1110,6 +1110,13 @@ func (r *redisGroupMessageRuntime) realtimeFanoutPattern() string { return strings.TrimSuffix(r.prefix, ":") + ":*:rt:fanout" } +func realtimeFanoutDedupClaimID(tenantID types.TenantID, nodeID, dedupKey string) string { + if nodeID == "" { + nodeID = "local" + } + return fmt.Sprintf("%d:%s:%s", tenantID, nodeID, dedupKey) +} + func (r *redisGroupMessageRuntime) objectCacheKey(tenantID types.TenantID, namespace, id string) string { return r.key(tenantID, "cache", namespace, id) } diff --git a/server/store/group_message_runtime_test.go b/server/store/group_message_runtime_test.go index bb57127..0fdd6bf 100644 --- a/server/store/group_message_runtime_test.go +++ b/server/store/group_message_runtime_test.go @@ -191,6 +191,20 @@ func TestLoginTokenRevocation(t *testing.T) { } } +func TestRealtimeFanoutDedupClaimIDIsNodeScoped(t *testing.T) { + tenantID := types.TenantID(7) + dedupKey := "data:p2pABC:42" + + nodeA := realtimeFanoutDedupClaimID(tenantID, "pod-a", dedupKey) + nodeB := realtimeFanoutDedupClaimID(tenantID, "pod-b", dedupKey) + if nodeA == nodeB { + t.Fatalf("dedup claim id must be node-scoped: %q", nodeA) + } + if got, want := realtimeFanoutDedupClaimID(tenantID, "", dedupKey), "7:local:data:p2pABC:42"; got != want { + t.Fatalf("unexpected local fallback claim id: got %q want %q", got, want) + } +} + func TestMessageRuntimeSeqAndClientMidIdempotency(t *testing.T) { oldRuntime := groupMsgRuntime runtime := newTestGroupMessageRuntime()