P1.2: индексация сообщений — tombstone-удаление O(1), receipt/uuid-индексы, tick по in-flight

- models.Queue: deadIdx/receiptIdx/uuidIdx/liveCount/deadCount + примитивы
  (AppendMessage/RemoveIndex/Compact/CompactIfNeeded/Reset/EnsureIndexes/
  IndexOfReceipt/IndexOfUUID/MarkInFlight/ClearInFlight/InFlightIndexes/LiveCount).
  Удаление — tombstone за O(1), физическая чистка амортизированная.
- DeleteMessage/ChangeMessageVisibility(±Batch): поиск по индексу вместо скана
  среза; удаление без сдвига остатка.
- DeleteMessageBatch: tombstone вместо полной пересборки среза.
- processQueueTick: visibility/DLQ по индексу in-flight (O(in-flight)),
  retention — tombstone, в конце — CompactIfNeeded.
- send-пути: AppendMessage + LiveCount (OOM-лимит по живым).
- admin/metrics/persistence: учёт живых сообщений, индексы после загрузки из Redis.
- Redis-схема не менялась (сообщения и так в per-message HASH).
This commit is contained in:
Repinoid
2026-10-07 08:01:49 +03:00
parent e735563a04
commit c5b2c11f5d
15 changed files with 519 additions and 130 deletions
+4 -4
View File
@@ -90,7 +90,7 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
defer queue.UnlockMu()
maxMessageSize := queue.MaximumMessageSize
currentMsgCount := len(queue.Messages)
currentMsgCount := queue.LiveCount()
queueIsFIFO := queue.IsFIFO
delaySecs := queue.DelaySeconds
@@ -129,7 +129,7 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
// Повторная проверка лимита под per-queue Lock (между снимком выше и текущим
// моментом другие горутины могли добавить сообщения — мягкий OOM-лимит).
if len(queue.Messages) >= MaxMessagesForQueue(queue.IsFIFO) {
if queue.LiveCount() >= MaxMessagesForQueue(queue.IsFIFO) {
return utils.CreateErrorResponseV1("OverLimit", true)
}
@@ -140,14 +140,14 @@ func SendMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
persistErr := false
if !queue.IsDuplicate(messageDeduplicationID) {
queue.Messages = append(queue.Messages, msg)
idx := queue.AppendMessage(msg)
// Персистим сообщение СИНХРОННО до ответа клиенту: краш пода в окне
// между 200 OK и async-записью приводил бы к потере подтверждённого
// сообщения. При ошибке — откатываем append (не оставляем в памяти
// сообщение, которого нет в Redis).
if err := persistence.SaveMessageSync(key, &msg); err != nil {
log.Errorf("SendMessage: не удалось персистить сообщение %s в очередь %s: %v", msg.Uuid, key, err)
queue.Messages = queue.Messages[:len(queue.Messages)-1]
queue.RemoveIndex(idx)
persistErr = true
}
} else {