doc: P1.2 сделан — результаты, найденный дефект (утечка per-queue блокировки) и прогоны тестов

This commit is contained in:
Repinoid
2026-10-07 08:27:10 +03:00
parent 529d208576
commit f96649b14e
3 changed files with 177 additions and 0 deletions
@@ -77,3 +77,12 @@ O(1): удаление из середины среза сдвигает все
затрагивает сериализацию и все пути — крупная рискованная правка с маргинальной
выгодой на текущих объёмах. Отложено до появления реальной нагрузки/лимитов.
## P1.2 — СДЕЛАНО (2026-10-07)
Возврат к пункту по прямому указанию. Решение — tombstone + receipt/uuid-индексы
+ амортизированная компакция (удаление НЕ сдвигает срез → индексы стабильны,
O(1)-удаление без смены контейнера и без изменения Redis-схемы).
Дизайн и результаты: `p1.2-message-index-design-2026-10-07.md`.
По ходу найден и исправлен дефект утечки per-queue блокировки в receive
(возврат до `defer UnlockMu`) — см. там же, раздел 6.
@@ -95,3 +95,60 @@ get_queue_attributes}.go`, `app/admin/admin.go`, `app/metrics/gauge_updater.go`,
2. visibility-перерасчёт в тике идёт по числу in-flight, не по размеру очереди.
3. Все существующие тесты зелёные; добавлены тесты на tombstone/компакцию.
4. Redis-схема не изменилась; рестарт не «воскрешает» удалённые сообщения.
## 6. РЕАЛИЗОВАНО (2026-10-07) — и найденный по ходу дефект
Коммиты: `22e9bc8` (код), `e05af04` (тесты примитивов), `5b3fb51` (фикс ниже).
### 6.1. Найденный дефект (МОЯ РЕГРЕССИЯ) — утечка per-queue блокировки
При переводе `receiveMessagesUnderLock` на `LiveCount()` я сохранил исходный
порядок строк:
```go
queue := models.SyncQueues.GetLocked(key) // взяли Lock
if queue == nil || queue.LiveCount() == 0 {
return nil, false // ← возврат БЕЗ Unlock
}
defer queue.UnlockMu()
```
Возврат на «пустой» очереди оставлял `queue.mu` захваченным НАВСЕГДА: любой
последующий `Lock`/`RLock` этой очереди ждал вечно (в Go мьютекс не
освобождается при выходе из функции).
Почему стало заметно именно сейчас: раньше условие было `len(queue.Messages) == 0`
и срабатывало только на полностью пустой очереди; после перехода на tombstone
добавился новый случай — «все сообщения удалены, но срез ещё не компактирован»
(`LiveCount()==0` при `len(Messages)>0`), который в стресс-сценарии возникает
на каждом цикле.
Диагностика (без гаданий):
- симптом: `ReadTimeout` при нагрузке 2000 сообщений, единичные всплески ~1.9 с на `delete_batch`;
- снят стек горутин по `SIGQUIT` → десятки горутин в `sync.RWMutex.RLock` на `Queue.RLockMu`
(`models.go:101`), включая `metrics.updateGauges`; HTTP-хендлеры `GetQueueAttributesV1` — там же.
- аудит всех `GetLocked`/`GetRLocked` показал: дефект ровно один — в `receive_message.go`.
Фикс (`5b3fb51`): `defer queue.UnlockMu()` — ДО любой проверки/возврата.
Регрессионный тест `TestReceiveEmptyQueue_DoesNotLeakLock` (на дефекте падает
за 2 с, на фиксе — зелёный).
### 6.2. Результаты проверок
| Проверка | Результат |
|---|---|
| `go build ./...`, `go vet ./...` | OK |
| `go test -race ./app/...` | зелёные (admin, models, gosqs) |
| `tests/api_test.sh` (aws CLI, локальный сервис) | **PASS=22 FAIL=0** |
| `tests/sdk_test.py` (boto3) | **PASS=15 FAIL=0** |
| `tests/fifo_dlq_probe.py` | **fifo_order/fifo_dedup/dlq = PASS** |
| Стресс 3×2000 сообщений (send-batch → receive → delete-batch) | send ≈0.6 с, drain ≈1.2 с на цикл; `left=0/0`; ни одного вызова >0.5 с |
Стресс подтверждает и отсутствие «протечки» tombstone: после каждого цикла
очередь возвращается к 0 живых (`CompactIfNeeded` в тике работает).
### 6.3. Замечание
В первом прогоне api/sdk/fifo уже проходили (22/22, 15/15, PASS×3) — то есть
дефект проявлялся не на каждом сценарии, а на последовательных циклах полного
опустошения очереди. Поэтому стресс-проверка была обязательной.