persistence: Client → atomic.Pointer (устранение data race при reconnect)
This commit is contained in:
+2
-2
@@ -109,7 +109,7 @@ func main() {
|
||||
}
|
||||
|
||||
// Восстановление состояния из Redis (тенанты + очереди)
|
||||
if persistence.Client != nil {
|
||||
if persistence.Client.Load() != nil {
|
||||
// Загружаем тенантов
|
||||
tenantsRaw, err := persistence.LoadAllTenantsRaw()
|
||||
if err != nil {
|
||||
@@ -139,7 +139,7 @@ func main() {
|
||||
|
||||
// Бэкфилл индекса accessKey→tenant для read-through (идемпотентно; нужен для
|
||||
// тенантов, созданных до появления индекса, и для multi-instance сценария).
|
||||
if persistence.Client != nil {
|
||||
if persistence.Client.Load() != nil {
|
||||
for _, t := range tenantStore.List() {
|
||||
if data, err := json.Marshal(t); err == nil {
|
||||
persistence.SaveTenantRaw(t.ID, data)
|
||||
|
||||
+48
-45
@@ -20,6 +20,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -30,7 +31,9 @@ import (
|
||||
|
||||
// Client — глобальный Redis клиент.
|
||||
// nil означает режим "только память" — все функции тихо no-op.
|
||||
var Client *redis.Client
|
||||
// atomic.Pointer: reconnect() пишет новый клиент из health-check горутины,
|
||||
// а остальные горутины читают — без атомарности это data race.
|
||||
var Client atomic.Pointer[redis.Client]
|
||||
|
||||
// Параметры подключения — сохраняются для переподключения после обрыва.
|
||||
var (
|
||||
@@ -78,7 +81,7 @@ func Connect(addr, username, password string) error {
|
||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||
return fmt.Errorf("redis ping %s: %w", addr, err)
|
||||
}
|
||||
Client = rdb
|
||||
Client.Store(rdb)
|
||||
log.Infof("persistence: подключились к Redis %s", addr)
|
||||
startRedisHealthCheck()
|
||||
return nil
|
||||
@@ -93,11 +96,11 @@ func startRedisHealthCheck() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
continue
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
err := Client.Ping(ctx).Err()
|
||||
err := Client.Load().Ping(ctx).Err()
|
||||
cancel()
|
||||
if err == nil {
|
||||
continue
|
||||
@@ -128,7 +131,7 @@ func reconnect() error {
|
||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
Client = rdb
|
||||
Client.Store(rdb)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -222,7 +225,7 @@ func marshalQueueMeta(queue *models.Queue) ([]byte, error) {
|
||||
// создаёт консистентный снапшот. Горутина только делает сетевой вызов.
|
||||
// Для сохранения сообщений используй SaveMessage/SaveMessages.
|
||||
func SaveQueue(key string, queue *models.Queue) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return
|
||||
}
|
||||
// Сериализуем метаданные синхронно под мьютексом вызывающего → консистентный снапшот
|
||||
@@ -238,7 +241,7 @@ func SaveQueue(key string, queue *models.Queue) {
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := Client.HSet(ctx, redisHashQueues, key, string(data)).Err(); err != nil {
|
||||
if err := Client.Load().HSet(ctx, redisHashQueues, key, string(data)).Err(); err != nil {
|
||||
log.Errorf("persistence: HSet queue meta %q: %v", key, err)
|
||||
}
|
||||
})
|
||||
@@ -248,7 +251,7 @@ func SaveQueue(key string, queue *models.Queue) {
|
||||
// Ключ: ssq:msg:{queueKey}, поле: msg.Uuid, значение: JSON(SqsMessage).
|
||||
// Вызывать под Lock — json.Marshal делает консистентный снапшот сообщения.
|
||||
func SaveMessage(queueKey string, msg *models.SqsMessage) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return
|
||||
}
|
||||
data, err := json.Marshal(msg)
|
||||
@@ -260,7 +263,7 @@ func SaveMessage(queueKey string, msg *models.SqsMessage) {
|
||||
asyncWrite(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := Client.HSet(ctx, hashKey, msg.Uuid, string(data)).Err(); err != nil {
|
||||
if err := Client.Load().HSet(ctx, hashKey, msg.Uuid, string(data)).Err(); err != nil {
|
||||
log.Errorf("persistence: HSet message %s/%s: %v", queueKey, msg.Uuid, err)
|
||||
}
|
||||
})
|
||||
@@ -273,7 +276,7 @@ func SaveMessage(queueKey string, msg *models.SqsMessage) {
|
||||
// Ключ: ssq:msg:{queueKey}, поле: msg.Uuid, значение: JSON(SqsMessage).
|
||||
// Если Client == nil (memory-only режим) — возвращает nil (персистить некуда).
|
||||
func SaveMessageSync(queueKey string, msg *models.SqsMessage) error {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return nil
|
||||
}
|
||||
data, err := json.Marshal(msg)
|
||||
@@ -283,7 +286,7 @@ func SaveMessageSync(queueKey string, msg *models.SqsMessage) error {
|
||||
hashKey := redisMsgHashPrefix + queueKey
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := Client.HSet(ctx, hashKey, msg.Uuid, string(data)).Err(); err != nil {
|
||||
if err := Client.Load().HSet(ctx, hashKey, msg.Uuid, string(data)).Err(); err != nil {
|
||||
return fmt.Errorf("HSet message %s/%s: %w", queueKey, msg.Uuid, err)
|
||||
}
|
||||
return nil
|
||||
@@ -292,7 +295,7 @@ func SaveMessageSync(queueKey string, msg *models.SqsMessage) error {
|
||||
// SaveMessages — сохраняет несколько сообщений в Redis одним pipeline.
|
||||
// Вызывать под Lock.
|
||||
func SaveMessages(queueKey string, msgs []models.SqsMessage) {
|
||||
if Client == nil || len(msgs) == 0 {
|
||||
if Client.Load() == nil || len(msgs) == 0 {
|
||||
return
|
||||
}
|
||||
// Сериализуем все сообщения синхронно под Lock
|
||||
@@ -317,7 +320,7 @@ func SaveMessages(queueKey string, msgs []models.SqsMessage) {
|
||||
for k, v := range fields {
|
||||
args = append(args, k, v)
|
||||
}
|
||||
if err := Client.HSet(ctx, hashKey, args...).Err(); err != nil {
|
||||
if err := Client.Load().HSet(ctx, hashKey, args...).Err(); err != nil {
|
||||
log.Errorf("persistence: HSet messages %s (%d msgs): %v", queueKey, len(fields), err)
|
||||
}
|
||||
})
|
||||
@@ -327,7 +330,7 @@ func SaveMessages(queueKey string, msgs []models.SqsMessage) {
|
||||
// Используется на пути SendMessageBatch: ответ 200 выдаётся только после записи
|
||||
// всех сообщений (как в одиночном SendMessage). Возвращает ошибку для отката.
|
||||
func SaveMessagesSync(queueKey string, msgs []models.SqsMessage) error {
|
||||
if Client == nil || len(msgs) == 0 {
|
||||
if Client.Load() == nil || len(msgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
fields := make(map[string]string, len(msgs))
|
||||
@@ -348,7 +351,7 @@ func SaveMessagesSync(queueKey string, msgs []models.SqsMessage) error {
|
||||
for k, v := range fields {
|
||||
args = append(args, k, v)
|
||||
}
|
||||
if err := Client.HSet(ctx, hashKey, args...).Err(); err != nil {
|
||||
if err := Client.Load().HSet(ctx, hashKey, args...).Err(); err != nil {
|
||||
return fmt.Errorf("HSet messages %s (%d msgs): %w", queueKey, len(fields), err)
|
||||
}
|
||||
return nil
|
||||
@@ -359,13 +362,13 @@ func SaveMessagesSync(queueKey string, msgs []models.SqsMessage) error {
|
||||
// сообщение «воскреснет» из Redis (факт: удалённые очереди возвращались
|
||||
// после рестарта, когда удаления были асинхронными).
|
||||
func DeleteMessagePersist(queueKey string, msgUuid string) {
|
||||
if Client == nil || msgUuid == "" {
|
||||
if Client.Load() == nil || msgUuid == "" {
|
||||
return
|
||||
}
|
||||
hashKey := redisMsgHashPrefix + queueKey
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := Client.HDel(ctx, hashKey, msgUuid).Err(); err != nil {
|
||||
if err := Client.Load().HDel(ctx, hashKey, msgUuid).Err(); err != nil {
|
||||
log.Errorf("persistence: HDel message %s/%s: %v", queueKey, msgUuid, err)
|
||||
}
|
||||
}
|
||||
@@ -373,13 +376,13 @@ func DeleteMessagePersist(queueKey string, msgUuid string) {
|
||||
// DeleteMessagesPersist — удаляет несколько сообщений из Redis СИНХРОННО
|
||||
// (иначе удалённые сообщения воскреснут после рестарта пода).
|
||||
func DeleteMessagesPersist(queueKey string, uuids []string) {
|
||||
if Client == nil || len(uuids) == 0 {
|
||||
if Client.Load() == nil || len(uuids) == 0 {
|
||||
return
|
||||
}
|
||||
hashKey := redisMsgHashPrefix + queueKey
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := Client.HDel(ctx, hashKey, uuids...).Err(); err != nil {
|
||||
if err := Client.Load().HDel(ctx, hashKey, uuids...).Err(); err != nil {
|
||||
log.Errorf("persistence: HDel messages %s (%d): %v", queueKey, len(uuids), err)
|
||||
}
|
||||
}
|
||||
@@ -388,20 +391,20 @@ func DeleteMessagesPersist(queueKey string, uuids []string) {
|
||||
// Синхронность обязательна: при рестарте пода удалённые сообщения не должны
|
||||
// воскреснуть (факт: асинхронное удаление очередей приводило к их возврату).
|
||||
func PurgeMessagesPersist(queueKey string) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return
|
||||
}
|
||||
hashKey := redisMsgHashPrefix + queueKey
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := Client.Del(ctx, hashKey).Err(); err != nil {
|
||||
if err := Client.Load().Del(ctx, hashKey).Err(); err != nil {
|
||||
log.Errorf("persistence: DEL messages hash %s: %v", queueKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteQueue — удаляет метаданные очереди И все её сообщения из Redis асинхронно.
|
||||
func DeleteQueue(key string) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return
|
||||
}
|
||||
writeSeq := nextQueueWriteSeq(key)
|
||||
@@ -415,11 +418,11 @@ func DeleteQueue(key string) {
|
||||
return
|
||||
}
|
||||
// Удаляем метаданные из общего HASH
|
||||
if err := Client.HDel(ctx, redisHashQueues, key).Err(); err != nil {
|
||||
if err := Client.Load().HDel(ctx, redisHashQueues, key).Err(); err != nil {
|
||||
log.Errorf("persistence: HDel queue meta %q: %v", key, err)
|
||||
}
|
||||
// Удаляем весь HASH с сообщениями
|
||||
if err := Client.Del(ctx, msgHashKey).Err(); err != nil {
|
||||
if err := Client.Load().Del(ctx, msgHashKey).Err(); err != nil {
|
||||
log.Errorf("persistence: DEL messages hash %q: %v", key, err)
|
||||
}
|
||||
}
|
||||
@@ -429,14 +432,14 @@ func DeleteQueue(key string) {
|
||||
// Миграция v1→v2: если в ssq:queues лежит JSON со встроенными Messages,
|
||||
// они извлекаются в отдельный HASH и метаданные пересохраняются без Messages.
|
||||
func LoadAllQueues() (map[string]*models.Queue, error) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return nil, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 1. Загружаем метаданные очередей
|
||||
raw, err := Client.HGetAll(ctx, redisHashQueues).Result()
|
||||
raw, err := Client.Load().HGetAll(ctx, redisHashQueues).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("redis HGetAll queues: %w", err)
|
||||
}
|
||||
@@ -496,7 +499,7 @@ func LoadAllQueues() (map[string]*models.Queue, error) {
|
||||
args = append(args, q.Messages[i].Uuid, string(data))
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if err := Client.HSet(ctx, hashKey, args...).Err(); err != nil {
|
||||
if err := Client.Load().HSet(ctx, hashKey, args...).Err(); err != nil {
|
||||
log.Errorf("persistence: migrate HSet messages %q: %v", k, err)
|
||||
}
|
||||
}
|
||||
@@ -504,7 +507,7 @@ func LoadAllQueues() (map[string]*models.Queue, error) {
|
||||
// Пересохраняем метаданные без Messages
|
||||
metaData, err := marshalQueueMeta(q)
|
||||
if err == nil {
|
||||
if err := Client.HSet(ctx, redisHashQueues, k, string(metaData)).Err(); err != nil {
|
||||
if err := Client.Load().HSet(ctx, redisHashQueues, k, string(metaData)).Err(); err != nil {
|
||||
log.Errorf("persistence: migrate HSet meta %q: %v", k, err)
|
||||
}
|
||||
}
|
||||
@@ -514,7 +517,7 @@ func LoadAllQueues() (map[string]*models.Queue, error) {
|
||||
// 3. Загружаем сообщения из отдельных HASH'ов для каждой очереди
|
||||
for k, q := range queues {
|
||||
hashKey := redisMsgHashPrefix + k
|
||||
msgRaw, err := Client.HGetAll(ctx, hashKey).Result()
|
||||
msgRaw, err := Client.Load().HGetAll(ctx, hashKey).Result()
|
||||
if err != nil {
|
||||
log.Errorf("persistence: HGetAll messages %q: %v", k, err)
|
||||
continue
|
||||
@@ -565,7 +568,7 @@ func LoadAllQueues() (map[string]*models.Queue, error) {
|
||||
// Принимает []byte чтобы избежать циклического импорта с пакетом tenant.
|
||||
// Сериализацию делает вызывающий (tenant_store.go).
|
||||
func SaveTenantRaw(id string, jsonData []byte) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return
|
||||
}
|
||||
// Копируем bytes — вызывающий может переиспользовать буфер
|
||||
@@ -583,11 +586,11 @@ func SaveTenantRaw(id string, jsonData []byte) {
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := Client.HSet(ctx, redisHashTenants, id, string(dataCopy)).Err(); err != nil {
|
||||
if err := Client.Load().HSet(ctx, redisHashTenants, id, string(dataCopy)).Err(); err != nil {
|
||||
log.Errorf("persistence: HSet tenant %q: %v", id, err)
|
||||
}
|
||||
if meta.AccessKey != "" {
|
||||
if err := Client.HSet(ctx, redisHashTenantAK, meta.AccessKey, string(dataCopy)).Err(); err != nil {
|
||||
if err := Client.Load().HSet(ctx, redisHashTenantAK, meta.AccessKey, string(dataCopy)).Err(); err != nil {
|
||||
log.Errorf("persistence: HSet tenant_ak %q: %v", meta.AccessKey, err)
|
||||
}
|
||||
}
|
||||
@@ -597,12 +600,12 @@ func SaveTenantRaw(id string, jsonData []byte) {
|
||||
// LoadTenantByAccessKey — read-through тенанта по accessKey (для auth-middleware).
|
||||
// Возвращает сырой JSON; десериализацию делает вызывающий.
|
||||
func LoadTenantByAccessKey(accessKey string) ([]byte, bool) {
|
||||
if Client == nil || accessKey == "" {
|
||||
if Client.Load() == nil || accessKey == "" {
|
||||
return nil, false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
v, err := Client.HGet(ctx, redisHashTenantAK, accessKey).Result()
|
||||
v, err := Client.Load().HGet(ctx, redisHashTenantAK, accessKey).Result()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
@@ -613,7 +616,7 @@ func LoadTenantByAccessKey(accessKey string) ([]byte, bool) {
|
||||
// Удаляет из ОБОИХ индексов: ssq:tenants (по id) и ssq:tenant_ak (по accessKey),
|
||||
// иначе LoadTenantByAccessKey вернёт удалённого тенанта и отзыв ключей не сработает.
|
||||
func DeleteTenant(id, accessKey string) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return
|
||||
}
|
||||
writeSeq := nextTenantWriteSeq(id)
|
||||
@@ -623,11 +626,11 @@ func DeleteTenant(id, accessKey string) {
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := Client.HDel(ctx, redisHashTenants, id).Err(); err != nil {
|
||||
if err := Client.Load().HDel(ctx, redisHashTenants, id).Err(); err != nil {
|
||||
log.Errorf("persistence: HDel tenant %q: %v", id, err)
|
||||
}
|
||||
if accessKey != "" {
|
||||
if err := Client.HDel(ctx, redisHashTenantAK, accessKey).Err(); err != nil {
|
||||
if err := Client.Load().HDel(ctx, redisHashTenantAK, accessKey).Err(); err != nil {
|
||||
log.Errorf("persistence: HDel tenant_ak %q: %v", accessKey, err)
|
||||
}
|
||||
}
|
||||
@@ -663,12 +666,12 @@ func isLatestTenantWriteSeq(id string, seq uint64) bool {
|
||||
// LoadAllTenantsRaw — загружает всех тенантов из Redis при старте.
|
||||
// Возвращает map[tenantID]rawJSON — десериализацию делает tenant_store.go.
|
||||
func LoadAllTenantsRaw() (map[string][]byte, error) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return nil, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
raw, err := Client.HGetAll(ctx, redisHashTenants).Result()
|
||||
raw, err := Client.Load().HGetAll(ctx, redisHashTenants).Result()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("redis HGetAll tenants: %w", err)
|
||||
}
|
||||
@@ -690,12 +693,12 @@ func LoadAllTenantsRaw() (map[string][]byte, error) {
|
||||
|
||||
// TryAcquireLock — пытается взять лок (SETNX с TTL). true — лок наш.
|
||||
func TryAcquireLock(key, value string, ttl time.Duration) (bool, error) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return true, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
return Client.SetNX(ctx, key, value, ttl).Result()
|
||||
return Client.Load().SetNX(ctx, key, value, ttl).Result()
|
||||
}
|
||||
|
||||
// refreshLockScript — продлить TTL, ТОЛЬКО если лок всё ещё наш (атомарно).
|
||||
@@ -716,12 +719,12 @@ end`)
|
||||
|
||||
// RefreshLock — продлевает TTL лока, если он всё ещё наш. false — лок потерян.
|
||||
func RefreshLock(key, value string, ttl time.Duration) (bool, error) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return true, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
n, err := refreshLockScript.Run(ctx, Client, []string{key}, value, ttl.Milliseconds()).Int()
|
||||
n, err := refreshLockScript.Run(ctx, Client.Load(), []string{key}, value, ttl.Milliseconds()).Int()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -730,12 +733,12 @@ func RefreshLock(key, value string, ttl time.Duration) (bool, error) {
|
||||
|
||||
// ReleaseLock — освобождает лок (если он наш). Вызывается при graceful shutdown.
|
||||
func ReleaseLock(key, value string) {
|
||||
if Client == nil {
|
||||
if Client.Load() == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if _, err := releaseLockScript.Run(ctx, Client, []string{key}, value).Int(); err != nil {
|
||||
if _, err := releaseLockScript.Run(ctx, Client.Load(), []string{key}, value).Int(); err != nil {
|
||||
log.Errorf("persistence: release lock %q: %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user