feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
@@ -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
}