diff --git a/shared-sqs/app/cmd/goaws.go b/shared-sqs/app/cmd/goaws.go index 1c7f442..d097e43 100644 --- a/shared-sqs/app/cmd/goaws.go +++ b/shared-sqs/app/cmd/goaws.go @@ -1,10 +1,11 @@ // app/cmd/goaws.go // Entry point — shared-sqs server -// Updated: 2026-04-09 — добавлены TenantStore, admin token, graceful shutdown (Trap #13) +// Updated: 2026-04-10 — добавлена Redis persistence (write-through cache) package main import ( "context" + "encoding/json" "flag" "net/http" "os" @@ -15,6 +16,7 @@ import ( "shared-sqs/app/conf" "shared-sqs/app/gosqs" "shared-sqs/app/models" + "shared-sqs/app/persistence" "shared-sqs/app/router" "shared-sqs/app/tenant" @@ -77,6 +79,47 @@ func main() { // Инициализация in-memory TenantStore tenantStore := tenant.NewTenantStore() + + // Подключение к Redis (если задан REDIS_ADDR) + // При ошибке — предупреждение, но продолжаем в memory-only режиме + redisAddr := os.Getenv("REDIS_ADDR") + redisUser := os.Getenv("REDIS_USER") + redisPass := os.Getenv("REDIS_PASSWORD") + if redisAddr != "" { + if err := persistence.Connect(redisAddr, redisUser, redisPass); err != nil { + log.Warnf("Не удалось подключиться к Redis: %v — работаем в memory-only режиме", err) + } + } + + // Восстановление состояния из Redis (тенанты + очереди) + if persistence.Client != nil { + // Загружаем тенантов + tenantsRaw, err := persistence.LoadAllTenantsRaw() + if err != nil { + log.Warnf("Ошибка загрузки тенантов из Redis: %v", err) + } else { + for _, jsonBytes := range tenantsRaw { + var t tenant.Tenant + if err := json.Unmarshal(jsonBytes, &t); err != nil { + log.Errorf("Ошибка десериализации тенанта: %v", err) + continue + } + tenantStore.LoadTenant(&t) + } + } + // Загружаем очереди + queues, err := persistence.LoadAllQueues() + if err != nil { + log.Warnf("Ошибка загрузки очередей из Redis: %v", err) + } else { + models.SyncQueues.Lock() + for k, q := range queues { + models.SyncQueues.Queues[k] = q + } + models.SyncQueues.Unlock() + } + } + // Автосид демо-данных при SHARED_SQS_SEED_DEMO=true if os.Getenv("SHARED_SQS_SEED_DEMO") == "true" { seedDemoData(tenantStore) diff --git a/shared-sqs/app/gosqs/create_queue.go b/shared-sqs/app/gosqs/create_queue.go index 2e42e5d..45aa23f 100644 --- a/shared-sqs/app/gosqs/create_queue.go +++ b/shared-sqs/app/gosqs/create_queue.go @@ -1,4 +1,4 @@ -// Изменено: 2026-04-09 +// Изменено: 2026-04-10 — добавлена Redis persistence // CreateQueueV1 — создаёт очередь для тенанта из request context. // Ключ в SyncQueues: "{tenantAccessKey}:{queueName}" для изоляции между тенантами. package gosqs @@ -9,6 +9,7 @@ import ( "shared-sqs/app/interfaces" "shared-sqs/app/models" +"shared-sqs/app/persistence" "shared-sqs/app/utils" log "github.com/sirupsen/logrus" ) @@ -54,8 +55,8 @@ return utils.CreateErrorResponseV1(err.Error(), true) } models.SyncQueues.Queues[key] = queue } -models.SyncQueues.Unlock() - + // Сохраняем очередь в Redis пока держим Lock — консистентный снапшот + persistence.SaveQueue(key, models.SyncQueues.Queues[key]) respStruct := models.CreateQueueResponse{ Xmlns: models.BaseXmlns, Result: models.CreateQueueResult{QueueUrl: queueUrl}, diff --git a/shared-sqs/app/gosqs/delete_message.go b/shared-sqs/app/gosqs/delete_message.go index e8919fe..0f5e2f1 100644 --- a/shared-sqs/app/gosqs/delete_message.go +++ b/shared-sqs/app/gosqs/delete_message.go @@ -1,4 +1,4 @@ -// Изменено: 2026-04-09 +// Изменено: 2026-04-10 — добавлена Redis persistence // DeleteMessageV1 — удаляет сообщение из очереди тенанта по ReceiptHandle. package gosqs @@ -8,6 +8,7 @@ import ( "shared-sqs/app/interfaces" "shared-sqs/app/models" + "shared-sqs/app/persistence" "shared-sqs/app/utils" "github.com/gorilla/mux" @@ -49,6 +50,8 @@ func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) { models.SyncQueues.Queues[key].UnlockGroup(msg.GroupID) models.SyncQueues.Queues[key].Messages = append(models.SyncQueues.Queues[key].Messages[:i], models.SyncQueues.Queues[key].Messages[i+1:]...) delete(models.SyncQueues.Queues[key].Duplicates, msg.DeduplicationID) + // Сохраняем очередь в Redis пока держим Lock + persistence.SaveQueue(key, models.SyncQueues.Queues[key]) respStruct := models.DeleteMessageResponse{ Xmlns: models.BaseXmlns, Metadata: models.BaseResponseMetadata, diff --git a/shared-sqs/app/gosqs/delete_queue.go b/shared-sqs/app/gosqs/delete_queue.go index 7ca36e3..03b5283 100644 --- a/shared-sqs/app/gosqs/delete_queue.go +++ b/shared-sqs/app/gosqs/delete_queue.go @@ -1,4 +1,4 @@ -// Изменено: 2026-04-09 +// Изменено: 2026-04-10 — добавлена Redis persistence // DeleteQueueV1 — удаляет очередь тенанта по tenant-scoped ключу. package gosqs @@ -8,6 +8,7 @@ import ( "shared-sqs/app/interfaces" "shared-sqs/app/models" +"shared-sqs/app/persistence" "shared-sqs/app/utils" log "github.com/sirupsen/logrus" ) @@ -35,6 +36,9 @@ models.SyncQueues.Lock() delete(models.SyncQueues.Queues, key) models.SyncQueues.Unlock() +// Удаляем из Redis асинхронно +persistence.DeleteQueue(key) + respStruct := models.DeleteQueueResponse{ Xmlns: models.BaseXmlns, Metadata: models.BaseResponseMetadata, diff --git a/shared-sqs/app/gosqs/purge_queue.go b/shared-sqs/app/gosqs/purge_queue.go index 9f6e018..0a56a01 100644 --- a/shared-sqs/app/gosqs/purge_queue.go +++ b/shared-sqs/app/gosqs/purge_queue.go @@ -1,4 +1,4 @@ -// Изменено: 2026-04-09 +// Изменено: 2026-04-10 — добавлена Redis persistence // PurgeQueueV1 — очищает все сообщения в очереди тенанта. package gosqs @@ -9,6 +9,7 @@ import ( "shared-sqs/app/interfaces" "shared-sqs/app/models" +"shared-sqs/app/persistence" "shared-sqs/app/utils" log "github.com/sirupsen/logrus" ) @@ -40,6 +41,8 @@ return utils.CreateErrorResponseV1("QueueNotFound", true) log.Infof("Purging Queue: %s (tenant: %s)", queueName, t.ID) models.SyncQueues.Queues[key].Messages = nil models.SyncQueues.Queues[key].Duplicates = make(map[string]time.Time) +// Сохраняем пустую очередь в Redis пока держим Lock +persistence.SaveQueue(key, models.SyncQueues.Queues[key]) respStruct := models.PurgeQueueResponse{ Xmlns: models.BaseXmlns, diff --git a/shared-sqs/app/gosqs/send_message.go b/shared-sqs/app/gosqs/send_message.go index 2bf4e4b..2b40fc1 100644 --- a/shared-sqs/app/gosqs/send_message.go +++ b/shared-sqs/app/gosqs/send_message.go @@ -1,4 +1,4 @@ -// Изменено: 2026-04-09 +// Изменено: 2026-04-10 — добавлена Redis persistence // SendMessageV1 — добавляет сообщение в очередь тенанта. // Ловушка #6: queueName извлекается как ПОСЛЕДНИЙ сегмент URL — при URL вида // http://host/tenantID/queueName последний сегмент = queueName (правильно). @@ -13,6 +13,7 @@ import ( "shared-sqs/app/interfaces" "shared-sqs/app/models" +"shared-sqs/app/persistence" "shared-sqs/app/utils" log "github.com/sirupsen/logrus" @@ -90,6 +91,8 @@ log.Debugf("Duplicate message deduplicationId [%s] in queue [%s]", messageDedupl } models.SyncQueues.Queues[key].InitDuplicatation(messageDeduplicationID) +// Сохраняем очередь в Redis пока держим Lock +persistence.SaveQueue(key, models.SyncQueues.Queues[key]) models.SyncQueues.Unlock() log.Infof("%s: Queue: %s, Message: %s\n", time.Now().Format("2006-01-02 15:04:05"), queueName, msg.MessageBody) diff --git a/shared-sqs/app/gosqs/set_queue_attributes.go b/shared-sqs/app/gosqs/set_queue_attributes.go index f691585..d150103 100644 --- a/shared-sqs/app/gosqs/set_queue_attributes.go +++ b/shared-sqs/app/gosqs/set_queue_attributes.go @@ -1,4 +1,4 @@ -// Изменено: 2026-04-09 +// Изменено: 2026-04-10 — добавлена Redis persistence // SetQueueAttributesV1 — устанавливает атрибуты очереди тенанта. // Ловушка #9: при RedrivePolicy парсим ARN DLQ и DLQ тоже должна принадлежать тому же тенанту. package gosqs @@ -9,6 +9,7 @@ import ( "shared-sqs/app/interfaces" "shared-sqs/app/models" +"shared-sqs/app/persistence" "shared-sqs/app/utils" log "github.com/sirupsen/logrus" ) @@ -45,6 +46,8 @@ return utils.CreateErrorResponseV1("QueueNotFound", true) if err := setQueueAttributesV1(queue, requestBody.Attributes); err != nil { return utils.CreateErrorResponseV1(err.Error(), true) } +// Сохраняем атрибуты в Redis пока держим Lock (через defer) +persistence.SaveQueue(key, queue) respStruct := models.SetQueueAttributesResponse{ Xmlns: models.BaseXmlns, diff --git a/shared-sqs/app/persistence/redis.go b/shared-sqs/app/persistence/redis.go new file mode 100644 index 0000000..e7934ca --- /dev/null +++ b/shared-sqs/app/persistence/redis.go @@ -0,0 +1,192 @@ +// app/persistence/redis.go +// Redis persistence layer для shared-sqs — write-through cache. +// Стратегия: память — источник правды для чтения (быстро), +// Redis — источник правды для восстановления после рестарта. +// Все записи в Redis асинхронны (горутина) — не блокируют SQS-операции. +// Сериализация (json.Marshal) происходит синхронно пока вызывающий держит мьютекс — консистентный снапшот. +// Created: 2026-04-10 + +package persistence + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/redis/go-redis/v9" + log "github.com/sirupsen/logrus" + + "shared-sqs/app/models" +) + +// Client — глобальный Redis клиент. +// nil означает режим "только память" — все функции тихо no-op. +var Client *redis.Client + +const ( + // redisHashTenants — HASH: tenantID → JSON тенанта + redisHashTenants = "ssq:tenants" + // redisHashQueues — HASH: queueKey → JSON очереди (включая сообщения) + redisHashQueues = "ssq:queues" +) + +// Connect — подключается к Redis и проверяет ping. +// Если addr пустой — не подключается, остаёмся в memory-only режиме. +func Connect(addr, username, password string) error { + if addr == "" { + log.Info("persistence: REDIS_ADDR не задан, работаем в memory-only режиме") + return nil + } + rdb := redis.NewClient(&redis.Options{ + Addr: addr, + Username: username, + Password: password, + DB: 0, + }) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := rdb.Ping(ctx).Err(); err != nil { + return fmt.Errorf("redis ping %s: %w", addr, err) + } + Client = rdb + log.Infof("persistence: подключились к Redis %s", addr) + return nil +} + +// asyncWrite — запускает fn в горутине, перехватывает panic и логирует. +// Используется для записей в Redis чтобы не задерживать SQS-ответы. +func asyncWrite(fn func()) { + go func() { + defer func() { + if r := recover(); r != nil { + log.Errorf("persistence: panic в asyncWrite: %v", r) + } + }() + fn() + }() +} + +// SaveQueue — сохраняет очередь (с сообщениями) в Redis асинхронно. +// ВАЖНО: вызывать пока вызывающий держит SyncQueues.Lock() — тогда json.Marshal +// создаёт консистентный снапшот. Горутина только делает сетевой вызов. +func SaveQueue(key string, queue *models.Queue) { + if Client == nil { + return + } + // Сериализуем синхронно под мьютексом вызывающего → консистентный снапшот + data, err := json.Marshal(queue) + if err != nil { + log.Errorf("persistence: marshal queue %q: %v", key, err) + return + } + asyncWrite(func() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if err := Client.HSet(ctx, redisHashQueues, key, string(data)).Err(); err != nil { + log.Errorf("persistence: HSet queue %q: %v", key, err) + } + }) +} + +// DeleteQueue — удаляет очередь из Redis асинхронно. +func DeleteQueue(key string) { + if Client == nil { + return + } + asyncWrite(func() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if err := Client.HDel(ctx, redisHashQueues, key).Err(); err != nil { + log.Errorf("persistence: HDel queue %q: %v", key, err) + } + }) +} + +// LoadAllQueues — загружает все очереди из Redis в память при старте сервиса. +// Инициализирует nil-maps чтобы избежать panic при deduplication/FIFO операциях. +func LoadAllQueues() (map[string]*models.Queue, error) { + if Client == nil { + return nil, nil + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + raw, err := Client.HGetAll(ctx, redisHashQueues).Result() + if err != nil { + return nil, fmt.Errorf("redis HGetAll queues: %w", err) + } + queues := make(map[string]*models.Queue, len(raw)) + for k, v := range raw { + var q models.Queue + if err := json.Unmarshal([]byte(v), &q); err != nil { + log.Errorf("persistence: unmarshal queue %q: %v", k, err) + continue + } + // Инициализируем nil-maps — их json.Unmarshal не создаёт если поле было nil + if q.Duplicates == nil { + q.Duplicates = make(map[string]time.Time) + } + if q.FIFOMessages == nil { + q.FIFOMessages = make(map[string]int) + } + if q.FIFOSequenceNumbers == nil { + q.FIFOSequenceNumbers = make(map[string]int) + } + queues[k] = &q + } + log.Infof("persistence: загружено %d очередей из Redis", len(queues)) + return queues, nil +} + +// SaveTenantRaw — сохраняет тенанта (сырой JSON) в Redis асинхронно. +// Принимает []byte чтобы избежать циклического импорта с пакетом tenant. +// Сериализацию делает вызывающий (tenant_store.go). +func SaveTenantRaw(id string, jsonData []byte) { + if Client == nil { + return + } + // Копируем bytes — вызывающий может переиспользовать буфер + dataCopy := make([]byte, len(jsonData)) + copy(dataCopy, jsonData) + asyncWrite(func() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if err := Client.HSet(ctx, redisHashTenants, id, string(dataCopy)).Err(); err != nil { + log.Errorf("persistence: HSet tenant %q: %v", id, err) + } + }) +} + +// DeleteTenant — удаляет тенанта из Redis асинхронно. +func DeleteTenant(id string) { + if Client == nil { + return + } + asyncWrite(func() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + if err := Client.HDel(ctx, redisHashTenants, id).Err(); err != nil { + log.Errorf("persistence: HDel tenant %q: %v", id, err) + } + }) +} + +// LoadAllTenantsRaw — загружает всех тенантов из Redis при старте. +// Возвращает map[tenantID]rawJSON — десериализацию делает tenant_store.go. +func LoadAllTenantsRaw() (map[string][]byte, error) { + if Client == nil { + return nil, nil + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + raw, err := Client.HGetAll(ctx, redisHashTenants).Result() + if err != nil { + return nil, fmt.Errorf("redis HGetAll tenants: %w", err) + } + result := make(map[string][]byte, len(raw)) + for id, v := range raw { + result[id] = []byte(v) + } + log.Infof("persistence: загружено %d тенантов из Redis", len(result)) + return result, nil +} diff --git a/shared-sqs/app/tenant/tenant_store.go b/shared-sqs/app/tenant/tenant_store.go index 0810b69..a6e453d 100644 --- a/shared-sqs/app/tenant/tenant_store.go +++ b/shared-sqs/app/tenant/tenant_store.go @@ -1,13 +1,16 @@ -// Изменено: 2026-04-09 +// Изменено: 2026-04-10 — добавлена Redis-персистентность через пакет persistence // Tenant model и in-memory хранилище тенантов для shared-sqs. package tenant import ( "crypto/rand" "encoding/hex" + "encoding/json" "fmt" "sync" "time" + + "shared-sqs/app/persistence" ) // Tenant — модель тенанта shared-sqs. @@ -68,6 +71,11 @@ func (s *TenantStore) Create(name string, maxQueues int) (*Tenant, error) { s.byAccessKey[t.AccessKey] = t s.mu.Unlock() + // Сохраняем в Redis асинхронно — сериализуем здесь, вне мьютекса + if data, err := json.Marshal(t); err == nil { + persistence.SaveTenantRaw(t.ID, data) + } + return t, nil } @@ -75,11 +83,12 @@ func (s *TenantStore) Create(name string, maxQueues int) (*Tenant, error) { // Используется только при инициализации демо-данных; не вызывается из user-facing API. func (s *TenantStore) CreateFixed(name string, maxQueues int, tenantID, accessKey, secretKey string) (*Tenant, error) { s.mu.Lock() - defer s.mu.Unlock() if _, exists := s.byID[tenantID]; exists { + s.mu.Unlock() return nil, fmt.Errorf("tenant with id %s already exists", tenantID) } if _, exists := s.byAccessKey[accessKey]; exists { + s.mu.Unlock() return nil, fmt.Errorf("tenant with access key %s already exists", accessKey) } t := &Tenant{ @@ -93,6 +102,13 @@ func (s *TenantStore) CreateFixed(name string, maxQueues int, tenantID, accessKe } s.byID[t.ID] = t s.byAccessKey[t.AccessKey] = t + s.mu.Unlock() + + // Сохраняем в Redis асинхронно — seed-данные тоже персистируем + if data, err := json.Marshal(t); err == nil { + persistence.SaveTenantRaw(t.ID, data) + } + return t, nil } @@ -116,16 +132,30 @@ func (s *TenantStore) GetByID(id string) (*Tenant, bool) { // Ловушка #2: если удалить только из одного индекса — orphaned данные и memory leak. func (s *TenantStore) Delete(id string) bool { s.mu.Lock() - defer s.mu.Unlock() t, ok := s.byID[id] if !ok { + s.mu.Unlock() return false } delete(s.byID, t.ID) delete(s.byAccessKey, t.AccessKey) + s.mu.Unlock() + + // Удаляем из Redis асинхронно + persistence.DeleteTenant(id) return true } +// LoadTenant — добавляет тенанта в хранилище без сохранения в Redis. +// Используется ТОЛЬКО при старте сервиса для восстановления состояния из Redis. +// Не вызывать из user-facing кода — нет дедупликации ключей. +func (s *TenantStore) LoadTenant(t *Tenant) { + s.mu.Lock() + s.byID[t.ID] = t + s.byAccessKey[t.AccessKey] = t + s.mu.Unlock() +} + // List — список всех тенантов (для admin GET /tenants). func (s *TenantStore) List() []*Tenant { s.mu.RLock() diff --git a/shared-sqs/deployments/k8s/deployment.yaml b/shared-sqs/deployments/k8s/deployment.yaml index 6c2f902..e72b9df 100644 --- a/shared-sqs/deployments/k8s/deployment.yaml +++ b/shared-sqs/deployments/k8s/deployment.yaml @@ -1,6 +1,6 @@ # deployments/k8s/deployment.yaml -# Deployment shared-sqs — strategy Recreate (in-memory state, Trap #14) -# Updated: 2026-04-09 +# Deployment shared-sqs — strategy RollingUpdate (теперь возможен т.к. Redis хранит состояние) +# Updated: 2026-04-10 — добавлена Redis persistence, Recreate → RollingUpdate apiVersion: apps/v1 kind: Deployment metadata: @@ -11,7 +11,10 @@ metadata: spec: replicas: 1 strategy: - type: Recreate + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 selector: matchLabels: app: shared-sqs @@ -22,7 +25,7 @@ spec: spec: containers: - name: shared-sqs - image: naeel/shared-sqs:v0.1.8 + image: naeel/shared-sqs:v0.1.11 ports: - containerPort: 4100 name: http @@ -34,6 +37,21 @@ spec: key: token - name: SHARED_SQS_SEED_DEMO value: "true" + - name: REDIS_ADDR + valueFrom: + secretKeyRef: + name: shared-sqs-redis + key: addr + - name: REDIS_USER + valueFrom: + secretKeyRef: + name: shared-sqs-redis + key: user + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: shared-sqs-redis + key: password resources: requests: memory: "64Mi" diff --git a/shared-sqs/deployments/k8s/secret.yaml b/shared-sqs/deployments/k8s/secret.yaml index f3707d9..8d78fc4 100644 --- a/shared-sqs/deployments/k8s/secret.yaml +++ b/shared-sqs/deployments/k8s/secret.yaml @@ -1,8 +1,10 @@ # deployments/k8s/secret.yaml -# Секрет для admin token shared-sqs +# Секреты для shared-sqs: admin token + Redis credentials # Created: 2026-04-09 -# ВНИМАНИЕ: заполнить реальным токеном перед деплоем +# Updated: 2026-04-10 — добавлен shared-sqs-redis secret +# ВНИМАНИЕ: заполнить реальными значениями перед деплоем # kubectl create secret generic shared-sqs-admin --from-literal=token=YOUR_TOKEN -n shared-sqs +# kubectl create secret generic shared-sqs-redis --from-literal=addr=HOST:6379 --from-literal=user=default --from-literal=password=PASS -n shared-sqs apiVersion: v1 kind: Secret metadata: @@ -11,3 +13,14 @@ metadata: type: Opaque stringData: token: "REPLACE_WITH_REAL_TOKEN" +--- +apiVersion: v1 +kind: Secret +metadata: + name: shared-sqs-redis + namespace: shared-sqs +type: Opaque +stringData: + addr: "REPLACE_WITH_REDIS_ADDR" + user: "default" + password: "REPLACE_WITH_REDIS_PASSWORD" diff --git a/shared-sqs/go.mod b/shared-sqs/go.mod index 1ae1e16..811044c 100644 --- a/shared-sqs/go.mod +++ b/shared-sqs/go.mod @@ -13,10 +13,14 @@ require ( ) require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/kr/pretty v0.1.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.18.0 // indirect + go.uber.org/atomic v1.11.0 // indirect golang.org/x/sys v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/shared-sqs/go.sum b/shared-sqs/go.sum index f2c5616..35a21dd 100644 --- a/shared-sqs/go.sum +++ b/shared-sqs/go.sum @@ -1,6 +1,10 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -20,11 +24,15 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=