SQS: sendMessageToQueue лимиты+sync persist, Reset чистит FIFO, nil-map Duplicates, ApproximateReceiveCount без +1, billing.Close после Shutdown, запрет ':' в access_key, recover в PeriodicTasks

This commit is contained in:
Repinoid
2026-10-11 08:54:21 +03:00
parent e8e0879914
commit 120a44cb6a
6 changed files with 47 additions and 12 deletions
+15 -5
View File
@@ -658,18 +658,28 @@ func (h *Handler) sendMessageToQueue(w http.ResponseWriter, r *http.Request) {
jsonErr(w, http.StatusBadRequest, "body is required") jsonErr(w, http.StatusBadRequest, "body is required")
return return
} }
// Сообщение строится через общий билдер (UUID + MD5), + проверка размера — // Сообщение строится через общий билдер (UUID + MD5), + проверка размера и
// как в SQS SendMessage. // лимита — как в SQS SendMessage (иначе UI обходил бы OOM-защиту).
q.LockMu() q.LockMu()
if q.MaximumMessageSize > 0 && len(req.Body) > q.MaximumMessageSize { if q.MaximumMessageSize > 0 && len(req.Body) > q.MaximumMessageSize {
q.UnlockMu() q.UnlockMu()
jsonErr(w, http.StatusBadRequest, "message too big") jsonErr(w, http.StatusBadRequest, "message too big")
return return
} }
if q.LiveCount() >= gosqs.MaxMessagesForQueue(q.IsFIFO) {
q.UnlockMu()
jsonErr(w, http.StatusTooManyRequests, "queue is full")
return
}
msg := gosqs.BuildSqsMessage(req.Body) msg := gosqs.BuildSqsMessage(req.Body)
q.AppendMessage(msg) idx := q.AppendMessage(msg)
// Персистим одно сообщение отдельно // Персистим СИНХРОННО (как SendMessage) — иначе 201 без гарантии записи.
persistence.SaveMessage(key, &msg) if err := persistence.SaveMessageSync(key, &msg); err != nil {
q.RemoveIndex(idx)
q.UnlockMu()
jsonErr(w, http.StatusInternalServerError, "failed to persist message")
return
}
persistence.SaveQueue(key, q) persistence.SaveQueue(key, q)
q.UnlockMu() q.UnlockMu()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
+4 -3
View File
@@ -257,9 +257,6 @@ func main() {
elector.Stop() elector.Stop()
} }
// Закрыть billing (если был подключён)
billing.Close()
// Дать 10 секунд на завершение текущих HTTP запросов // Дать 10 секунд на завершение текущих HTTP запросов
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel() defer cancel()
@@ -269,5 +266,9 @@ func main() {
if err := controlSrv.Shutdown(ctx); err != nil { if err := controlSrv.Shutdown(ctx); err != nil {
log.Errorf("Control-plane shutdown error: %v", err) log.Errorf("Control-plane shutdown error: %v", err)
} }
// Закрыть billing ПОСЛЕ Shutdown — иначе in-flight запросы писали бы usage
// в уже закрытую БД (ревью 2026-10-11).
billing.Close()
log.Info("shared-sqs stopped") log.Info("shared-sqs stopped")
} }
+12 -3
View File
@@ -48,9 +48,18 @@ func PeriodicTasks(d time.Duration, quit chan bool) {
// Каждую очередь обрабатываем под ОТДЕЛЬНЫМ Lock — блокировка одной // Каждую очередь обрабатываем под ОТДЕЛЬНЫМ Lock — блокировка одной
// очереди не мешает обработке остальных и HTTP-запросам. // очереди не мешает обработке остальных и HTTP-запросам.
for _, qi := range queues { for _, qi := range queues {
qi.queue.LockMu() func() {
processQueueTick(qi.key, qi.queue) // recover: паника в processQueueTick одной очереди не должна
qi.queue.UnlockMu() // убивать весь процесс; mutex освобождается в defer.
defer qi.queue.UnlockMu()
defer func() {
if r := recover(); r != nil {
log.Errorf("gosqs: panic in processQueueTick for queue %q: %v", qi.key, r)
}
}()
qi.queue.LockMu()
processQueueTick(qi.key, qi.queue)
}()
} }
case <-quit: case <-quit:
ticker.Stop() ticker.Stop()
+1 -1
View File
@@ -244,7 +244,7 @@ func buildResultMessage(m *models.SqsMessage) *models.ResultMessage {
Attributes: map[string]string{ Attributes: map[string]string{
"ApproximateFirstReceiveTimestamp": fmt.Sprintf("%d", m.ReceiptTime.UnixNano()/int64(time.Millisecond)), "ApproximateFirstReceiveTimestamp": fmt.Sprintf("%d", m.ReceiptTime.UnixNano()/int64(time.Millisecond)),
"SenderId": models.CurrentEnvironment.AccountID, "SenderId": models.CurrentEnvironment.AccountID,
"ApproximateReceiveCount": fmt.Sprintf("%d", m.NumberOfReceives+1), "ApproximateReceiveCount": fmt.Sprintf("%d", m.NumberOfReceives),
"SentTimestamp": fmt.Sprintf("%d", m.SentTime.UnixNano()/int64(time.Millisecond)), // Фикс: реальное время отправки "SentTimestamp": fmt.Sprintf("%d", m.SentTime.UnixNano()/int64(time.Millisecond)), // Фикс: реальное время отправки
}, },
} }
+9
View File
@@ -183,6 +183,11 @@ func (q *Queue) InitDuplicatation(deduplicationId string) {
return return
} }
// Ленивая инициализация: очередь, собранная литералом или загруженная из
// Redis до появления поля, может иметь nil-map — иначе запись паникует.
if q.Duplicates == nil {
q.Duplicates = make(map[string]time.Time)
}
if _, ok := q.Duplicates[deduplicationId]; !ok { if _, ok := q.Duplicates[deduplicationId]; !ok {
q.Duplicates[deduplicationId] = time.Now() q.Duplicates[deduplicationId] = time.Now()
} }
@@ -344,6 +349,10 @@ func (q *Queue) Reset() {
q.uuidIdx = make(map[string]int) q.uuidIdx = make(map[string]int)
q.liveCount = 0 q.liveCount = 0
q.deadCount = 0 q.deadCount = 0
// Чистим FIFO-состояние: иначе блокировки групп и порядковые номера остались
// бы, и сообщения этих групп не выдавались после purge (ревью 2026-10-11).
q.FIFOMessages = make(map[string]int)
q.FIFOSequenceNumbers = make(map[string]int)
} }
// EnsureIndexes — экспортируемая обёртка над ensureIndexes: строит индексы, // EnsureIndexes — экспортируемая обёртка над ensureIndexes: строит индексы,
+6
View File
@@ -7,6 +7,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"sync" "sync"
"time" "time"
@@ -121,6 +122,11 @@ func (s *TenantStore) CreateFixedAutoID(name string, maxQueues int, accessKey, s
} }
func (s *TenantStore) CreateFixed(name string, maxQueues int, tenantID, accessKey, secretKey string) (*Tenant, error) { func (s *TenantStore) CreateFixed(name string, maxQueues int, tenantID, accessKey, secretKey string) (*Tenant, error) {
// access_key используется как префикс ключа "{accessKey}:{queueName}" —
// ':' в нём ломает изоляцию тенантов (ListQueues/коллизии ключей).
if strings.Contains(accessKey, ":") {
return nil, fmt.Errorf("access key must not contain ':'")
}
s.mu.Lock() s.mu.Lock()
if _, exists := s.byID[tenantID]; exists { if _, exists := s.byID[tenantID]; exists {
s.mu.Unlock() s.mu.Unlock()