SQS: DeletePersist возвращает ошибку (500 при сбое Redis), fail-fast загрузка очередей, дефолты атрибутов при CreateQueue, защита SqsErrors от nil-deref
This commit is contained in:
+1
-1
@@ -127,7 +127,7 @@ func main() {
|
|||||||
// Загружаем очереди
|
// Загружаем очереди
|
||||||
queues, err := persistence.LoadAllQueues()
|
queues, err := persistence.LoadAllQueues()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Ошибка загрузки очередей из Redis: %v", err)
|
log.Fatalf("Ошибка загрузки очередей из Redis: %v", err)
|
||||||
} else {
|
} else {
|
||||||
models.SyncQueues.Lock()
|
models.SyncQueues.Lock()
|
||||||
for k, q := range queues {
|
for k, q := range queues {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func ChangeMessageVisibilityV1(req *http.Request) (int, interfaces.AbstractRespo
|
|||||||
if msgRemoved {
|
if msgRemoved {
|
||||||
// Сообщение удалено (перемещено в DLQ) — удаляем из Redis исходной
|
// Сообщение удалено (перемещено в DLQ) — удаляем из Redis исходной
|
||||||
// очереди и сохраняем в DLQ, иначе после рестарта оно пропадёт из DLQ.
|
// очереди и сохраняем в DLQ, иначе после рестарта оно пропадёт из DLQ.
|
||||||
persistence.DeleteMessagePersist(key, changedMsgUuid)
|
_ = persistence.DeleteMessagePersist(key, changedMsgUuid)
|
||||||
if dlqMsg != nil {
|
if dlqMsg != nil {
|
||||||
persistence.SaveMessage(dlqKeyFor(key, queue.DeadLetterQueue.Name), dlqMsg)
|
persistence.SaveMessage(dlqKeyFor(key, queue.DeadLetterQueue.Name), dlqMsg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func ChangeMessageVisibilityBatchV1(req *http.Request) (int, interfaces.Abstract
|
|||||||
if moved {
|
if moved {
|
||||||
// Сообщение перенесено в DLQ и удалено из очереди — чистим Redis
|
// Сообщение перенесено в DLQ и удалено из очереди — чистим Redis
|
||||||
// исходной и сохраняем в DLQ (иначе пропадёт после рестарта).
|
// исходной и сохраняем в DLQ (иначе пропадёт после рестарта).
|
||||||
persistence.DeleteMessagePersist(key, uuid)
|
_ = persistence.DeleteMessagePersist(key, uuid)
|
||||||
if dlqMsg != nil {
|
if dlqMsg != nil {
|
||||||
persistence.SaveMessage(dlqKeyFor(key, queue.DeadLetterQueue.Name), dlqMsg)
|
persistence.SaveMessage(dlqKeyFor(key, queue.DeadLetterQueue.Name), dlqMsg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,13 @@ func CreateQueueV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
|||||||
IsFIFO: utils.HasFIFOQueueName(queueName),
|
IsFIFO: utils.HasFIFOQueueName(queueName),
|
||||||
EnableDuplicates: models.CurrentEnvironment.EnableDuplicates,
|
EnableDuplicates: models.CurrentEnvironment.EnableDuplicates,
|
||||||
Duplicates: make(map[string]time.Time),
|
Duplicates: make(map[string]time.Time),
|
||||||
|
// Дефолты (как в InitQueues): без них очередь из API получала бы
|
||||||
|
// VisibilityTimeout=0 и т.д. — сообщение сразу видимо, retention 0 = нет
|
||||||
|
// эвикции. Переданные атрибуты перезапишут дефолты в setQueueAttributesV1.
|
||||||
|
VisibilityTimeout: models.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout,
|
||||||
|
MaximumMessageSize: models.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize,
|
||||||
|
MessageRetentionPeriod: models.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod,
|
||||||
|
ReceiveMessageWaitTimeSeconds: models.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds,
|
||||||
// CreatedTimestamp/LastModifiedTimestamp в GetQueueAttributes (AWS их отдаёт).
|
// CreatedTimestamp/LastModifiedTimestamp в GetQueueAttributes (AWS их отдаёт).
|
||||||
CreatedAt: time.Now().UTC(),
|
CreatedAt: time.Now().UTC(),
|
||||||
LastModifiedAt: time.Now().UTC(),
|
LastModifiedAt: time.Now().UTC(),
|
||||||
|
|||||||
@@ -59,8 +59,12 @@ func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
|
|||||||
// ВАЖНО: запись в Duplicates НЕ удаляем — AWS держит dedup-окно 5 минут
|
// ВАЖНО: запись в Duplicates НЕ удаляем — AWS держит dedup-окно 5 минут
|
||||||
// независимо от удаления сообщения (иначе повторный send с тем же
|
// независимо от удаления сообщения (иначе повторный send с тем же
|
||||||
// dedupId доставлялся бы повторно). Чистит только истечение окна (PeriodicTasks).
|
// dedupId доставлялся бы повторно). Чистит только истечение окна (PeriodicTasks).
|
||||||
// Удаляем одно сообщение из Redis — O(1)
|
// Удаляем одно сообщение из Redis — O(1). Ошибка → 500: иначе клиент
|
||||||
persistence.DeleteMessagePersist(key, msg.Uuid)
|
// получил бы 200, а после рестарта сообщение «воскресло» бы.
|
||||||
|
if err := persistence.DeleteMessagePersist(key, msg.Uuid); err != nil {
|
||||||
|
log.Errorf("DeleteMessage: не удалось удалить сообщение из Redis: %v", err)
|
||||||
|
return utils.CreateErrorResponseV1("InternalError", true)
|
||||||
|
}
|
||||||
// Метаданные очереди (duplicates, FIFO state)
|
// Метаданные очереди (duplicates, FIFO state)
|
||||||
persistence.SaveQueue(key, queue)
|
persistence.SaveQueue(key, queue)
|
||||||
respStruct := models.DeleteMessageResponse{
|
respStruct := models.DeleteMessageResponse{
|
||||||
|
|||||||
@@ -110,8 +110,11 @@ func DeleteMessageBatchV1(req *http.Request) (int, interfaces.AbstractResponseBo
|
|||||||
deletedUuids = append(deletedUuids, uuid)
|
deletedUuids = append(deletedUuids, uuid)
|
||||||
queue.RemoveIndex(i)
|
queue.RemoveIndex(i)
|
||||||
}
|
}
|
||||||
// Удаляем сообщения из Redis отдельно — O(batch_size)
|
// Удаляем сообщения из Redis отдельно — O(batch_size). Ошибка → InternalError.
|
||||||
persistence.DeleteMessagesPersist(key, deletedUuids)
|
if err := persistence.DeleteMessagesPersist(key, deletedUuids); err != nil {
|
||||||
|
log.Errorf("DeleteMessageBatch: не удалось удалить сообщения из Redis: %v", err)
|
||||||
|
return utils.CreateErrorResponseV1("InternalError", true)
|
||||||
|
}
|
||||||
// Метаданные очереди (duplicates, FIFO state)
|
// Метаданные очереди (duplicates, FIFO state)
|
||||||
persistence.SaveQueue(key, queue)
|
persistence.SaveQueue(key, queue)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -113,7 +113,7 @@ func processQueueTick(key string, queue *models.Queue) {
|
|||||||
if len(expired) > 0 {
|
if len(expired) > 0 {
|
||||||
log.Infof("Queue [%s]: evicted %d messages by MessageRetentionPeriod", queue.Name, len(expired))
|
log.Infof("Queue [%s]: evicted %d messages by MessageRetentionPeriod", queue.Name, len(expired))
|
||||||
// Удаляем просроченные из Redis, чтобы не воскресли после рестарта.
|
// Удаляем просроченные из Redis, чтобы не воскресли после рестарта.
|
||||||
persistence.DeleteMessagesPersist(key, expired)
|
_ = persistence.DeleteMessagesPersist(key, expired)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ func processQueueTick(key string, queue *models.Queue) {
|
|||||||
queue.RemoveIndex(i)
|
queue.RemoveIndex(i)
|
||||||
// Персист переноса: иначе после рестарта сообщение «воскреснет»
|
// Персист переноса: иначе после рестарта сообщение «воскреснет»
|
||||||
// в исходной очереди (не удалено из Redis) и пропадёт из DLQ.
|
// в исходной очереди (не удалено из Redis) и пропадёт из DLQ.
|
||||||
persistence.DeleteMessagePersist(key, msgUuid)
|
_ = persistence.DeleteMessagePersist(key, msgUuid)
|
||||||
persistence.SaveMessage(dlqKeyFor(key, dlqName), &dlqMsg)
|
persistence.SaveMessage(dlqKeyFor(key, dlqName), &dlqMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,30 +361,32 @@ func SaveMessagesSync(queueKey string, msgs []models.SqsMessage) error {
|
|||||||
// Синхронность обязательна: если удаление потеряется при рестарте пода,
|
// Синхронность обязательна: если удаление потеряется при рестарте пода,
|
||||||
// сообщение «воскреснет» из Redis (факт: удалённые очереди возвращались
|
// сообщение «воскреснет» из Redis (факт: удалённые очереди возвращались
|
||||||
// после рестарта, когда удаления были асинхронными).
|
// после рестарта, когда удаления были асинхронными).
|
||||||
func DeleteMessagePersist(queueKey string, msgUuid string) {
|
func DeleteMessagePersist(queueKey string, msgUuid string) error {
|
||||||
if Client.Load() == nil || msgUuid == "" {
|
if Client.Load() == nil || msgUuid == "" {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
hashKey := redisMsgHashPrefix + queueKey
|
hashKey := redisMsgHashPrefix + queueKey
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := Client.Load().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)
|
return fmt.Errorf("HDel message %s/%s: %w", queueKey, msgUuid, err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteMessagesPersist — удаляет несколько сообщений из Redis СИНХРОННО
|
// DeleteMessagesPersist — удаляет несколько сообщений из Redis СИНХРОННО
|
||||||
// (иначе удалённые сообщения воскреснут после рестарта пода).
|
// (иначе удалённые сообщения воскреснут после рестарта пода).
|
||||||
func DeleteMessagesPersist(queueKey string, uuids []string) {
|
func DeleteMessagesPersist(queueKey string, uuids []string) error {
|
||||||
if Client.Load() == nil || len(uuids) == 0 {
|
if Client.Load() == nil || len(uuids) == 0 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
hashKey := redisMsgHashPrefix + queueKey
|
hashKey := redisMsgHashPrefix + queueKey
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := Client.Load().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)
|
return fmt.Errorf("HDel messages %s (%d): %w", queueKey, len(uuids), err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurgeMessagesPersist — удаляет ВСЕ сообщения очереди из Redis СИНХРОННО.
|
// PurgeMessagesPersist — удаляет ВСЕ сообщения очереди из Redis СИНХРОННО.
|
||||||
@@ -519,8 +521,9 @@ func LoadAllQueues() (map[string]*models.Queue, error) {
|
|||||||
hashKey := redisMsgHashPrefix + k
|
hashKey := redisMsgHashPrefix + k
|
||||||
msgRaw, err := Client.Load().HGetAll(ctx, hashKey).Result()
|
msgRaw, err := Client.Load().HGetAll(ctx, hashKey).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("persistence: HGetAll messages %q: %v", k, err)
|
// Fail-fast: скрытая пустая очередь при недоступных сообщениях
|
||||||
continue
|
// хуже явной ошибки — сообщения остались бы невидимыми в Redis.
|
||||||
|
return nil, fmt.Errorf("redis HGetAll messages %q: %w", k, err)
|
||||||
}
|
}
|
||||||
if len(msgRaw) > 0 {
|
if len(msgRaw) > 0 {
|
||||||
msgs := make([]models.SqsMessage, 0, len(msgRaw))
|
msgs := make([]models.SqsMessage, 0, len(msgRaw))
|
||||||
|
|||||||
+5
-1
@@ -83,7 +83,11 @@ func ExtractQueueAttributes(u url.Values) map[string]string {
|
|||||||
// CreateErrorResponseV1 — формирует SQS error response по ключу ошибки.
|
// CreateErrorResponseV1 — формирует SQS error response по ключу ошибки.
|
||||||
// Параметр isSqs оставлен для обратной совместимости (всегда true).
|
// Параметр isSqs оставлен для обратной совместимости (всегда true).
|
||||||
func CreateErrorResponseV1(errKey string, isSqs bool) (int, interfaces.AbstractResponseBody) {
|
func CreateErrorResponseV1(errKey string, isSqs bool) (int, interfaces.AbstractResponseBody) {
|
||||||
err := models.SqsErrors[errKey]
|
err, ok := models.SqsErrors[errKey]
|
||||||
|
if !ok {
|
||||||
|
// Неизвестный ключ → InternalError вместо nil-deref (ревью 2026-10-11).
|
||||||
|
err = models.SqsErrors["InternalError"]
|
||||||
|
}
|
||||||
|
|
||||||
respStruct := models.ErrorResponse{
|
respStruct := models.ErrorResponse{
|
||||||
Result: err.Response(),
|
Result: err.Response(),
|
||||||
|
|||||||
Reference in New Issue
Block a user