51 lines
1.2 KiB
Go
51 lines
1.2 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)
|
|
if err := resp.Error(); err != nil {
|
|
return false, fmt.Errorf("dedup setnx: %w", err)
|
|
}
|
|
// SET with NX returns "OK" when set, nil reply when key already exists.
|
|
if resp.IsNil() {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|