feat: use postgres
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package redisstate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// releaseRouteLeaseScript releases the route lease only when the supplied
|
||||
// token still owns it. The Lua script gates the DEL on the SET value match
|
||||
// so a publisher that lost the lease (TTL expiry, replica swap) cannot
|
||||
// clear another worker's claim.
|
||||
var releaseRouteLeaseScript = redis.NewScript(`
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("DEL", KEYS[1])
|
||||
end
|
||||
return 0
|
||||
`)
|
||||
|
||||
// LeaseStore owns the short-lived route lease keys that coordinate exclusive
|
||||
// route publication across replicas. The lease lives on Redis as a per-route
|
||||
// SETNX-with-TTL token; releasing it requires the same token via a Lua
|
||||
// script that compares the stored value before deleting it.
|
||||
//
|
||||
// LeaseStore is intentionally separate from the durable route-state storage
|
||||
// so the publishers can compose one storage-layer adapter (PostgreSQL since
|
||||
// Stage 5) with the runtime-coordination layer that stays on Redis per
|
||||
// `ARCHITECTURE.md §Persistence Backends`.
|
||||
type LeaseStore struct {
|
||||
client *redis.Client
|
||||
keys Keyspace
|
||||
}
|
||||
|
||||
// NewLeaseStore constructs one Redis-backed lease store.
|
||||
func NewLeaseStore(client *redis.Client) (*LeaseStore, error) {
|
||||
if client == nil {
|
||||
return nil, errors.New("new notification lease store: nil redis client")
|
||||
}
|
||||
|
||||
return &LeaseStore{client: client, keys: Keyspace{}}, nil
|
||||
}
|
||||
|
||||
// TryAcquireRouteLease attempts to acquire one temporary route lease owned
|
||||
// by token for ttl. The lease is stored at the route-lease keyspace key and
|
||||
// auto-expires; a publisher whose work outlives the TTL must accept that
|
||||
// another replica may pick the route up.
|
||||
func (store *LeaseStore) TryAcquireRouteLease(ctx context.Context, notificationID string, routeID string, token string, ttl time.Duration) (bool, error) {
|
||||
if store == nil || store.client == nil {
|
||||
return false, errors.New("try acquire route lease: nil store")
|
||||
}
|
||||
if ctx == nil {
|
||||
return false, errors.New("try acquire route lease: nil context")
|
||||
}
|
||||
if notificationID == "" {
|
||||
return false, errors.New("try acquire route lease: notification id must not be empty")
|
||||
}
|
||||
if routeID == "" {
|
||||
return false, errors.New("try acquire route lease: route id must not be empty")
|
||||
}
|
||||
if token == "" {
|
||||
return false, errors.New("try acquire route lease: token must not be empty")
|
||||
}
|
||||
if ttl <= 0 {
|
||||
return false, errors.New("try acquire route lease: ttl must be positive")
|
||||
}
|
||||
|
||||
acquired, err := store.client.SetNX(ctx, store.keys.RouteLease(notificationID, routeID), token, ttl).Result()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("try acquire route lease: %w", err)
|
||||
}
|
||||
|
||||
return acquired, nil
|
||||
}
|
||||
|
||||
// ReleaseRouteLease releases one temporary route lease only when token still
|
||||
// matches the stored owner value. Releasing a lease the caller no longer
|
||||
// owns is a silent no-op.
|
||||
func (store *LeaseStore) ReleaseRouteLease(ctx context.Context, notificationID string, routeID string, token string) error {
|
||||
if store == nil || store.client == nil {
|
||||
return errors.New("release route lease: nil store")
|
||||
}
|
||||
if ctx == nil {
|
||||
return errors.New("release route lease: nil context")
|
||||
}
|
||||
if notificationID == "" {
|
||||
return errors.New("release route lease: notification id must not be empty")
|
||||
}
|
||||
if routeID == "" {
|
||||
return errors.New("release route lease: route id must not be empty")
|
||||
}
|
||||
if token == "" {
|
||||
return errors.New("release route lease: token must not be empty")
|
||||
}
|
||||
|
||||
if err := releaseRouteLeaseScript.Run(
|
||||
ctx,
|
||||
store.client,
|
||||
[]string{store.keys.RouteLease(notificationID, routeID)},
|
||||
token,
|
||||
).Err(); err != nil {
|
||||
return fmt.Errorf("release route lease: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user