SQS: sendMessageToQueue лимиты+sync persist, Reset чистит FIFO, nil-map Duplicates, ApproximateReceiveCount без +1, billing.Close после Shutdown, запрет ':' в access_key, recover в PeriodicTasks
This commit is contained in:
+15
-5
@@ -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
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -48,9 +48,18 @@ func PeriodicTasks(d time.Duration, quit chan bool) {
|
|||||||
// Каждую очередь обрабатываем под ОТДЕЛЬНЫМ Lock — блокировка одной
|
// Каждую очередь обрабатываем под ОТДЕЛЬНЫМ Lock — блокировка одной
|
||||||
// очереди не мешает обработке остальных и HTTP-запросам.
|
// очереди не мешает обработке остальных и HTTP-запросам.
|
||||||
for _, qi := range queues {
|
for _, qi := range queues {
|
||||||
|
func() {
|
||||||
|
// recover: паника в processQueueTick одной очереди не должна
|
||||||
|
// убивать весь процесс; 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()
|
qi.queue.LockMu()
|
||||||
processQueueTick(qi.key, qi.queue)
|
processQueueTick(qi.key, qi.queue)
|
||||||
qi.queue.UnlockMu()
|
}()
|
||||||
}
|
}
|
||||||
case <-quit:
|
case <-quit:
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
|
|||||||
@@ -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)), // Фикс: реальное время отправки
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: строит индексы,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user