Files
cdp/ingestion/ingest/internal/dedup/dedup.go
2026-05-25 10:16:31 +07:00

53 lines
1.3 KiB
Go

// Package dedup provides idempotent event acceptance via Redis SETNX.
//
// Key shape: dedup:{workspace_id}:{message_id}
// TTL: 24h by default (configurable)
//
// CheckAndSet returns true when the message_id is new (first time seen).
// If it returns false the caller MUST drop the event silently and return 200.
package dedup
import (
"context"
"fmt"
"time"
"github.com/redis/rueidis"
)
type Dedup interface {
CheckAndSet(ctx context.Context, workspaceID, messageID string) (bool, error)
}
type redisDedup struct {
client rueidis.Client
ttl time.Duration
}
func New(client rueidis.Client, ttl time.Duration) Dedup {
return &redisDedup{client: client, ttl: ttl}
}
func key(workspaceID, messageID string) string {
return fmt.Sprintf("dedup:%s:%s", workspaceID, messageID)
}
func (d *redisDedup) CheckAndSet(ctx context.Context, workspaceID, messageID string) (bool, error) {
k := key(workspaceID, messageID)
cmd := d.client.B().Set().Key(k).Value("1").
Nx().
Ex(d.ttl).
Build()
resp := d.client.Do(ctx, cmd)
err := resp.Error()
// SET NX returns nil reply when the key already exists; rueidis surfaces
// that as a "redis nil" error, which is *not* a real failure.
if rueidis.IsRedisNil(err) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("dedup setnx: %w", err)
}
return true, nil
}