Files
SQS-service/HISTORY/2026-10-07-session-log.md
T

7.2 KiB
Raw Blame History

История правок shared-sqs — 2026-10-07 (P1.2 + фикс утечки блокировки)

Запись по требованию «документировать всё, включая собственные ошибки». Предыдущие записи: doc/thinking/*-2026-10-0[56].md, HISTORY/2026-08-1*-session-log.md.

1. Задача

Пользователь: «доделать ПОЛНОСТЬЮ и протестировать SQS». Незакрытым по плану plan-for-flash-2026-10-05.md оставался единственный пункт — P1.2 (индексация сообщений), отложенный 2026-10-06 с обоснованием.

2. Что сделано

2.1. Дизайн (коммит b58e958)

doc/thinking/p1.2-message-index-design-2026-10-07.md.

Ключевое решение: tombstone + индексы + амортизированная компакция. Удаление не сдвигает срез (O(1)), поэтому индексы receipt→index и uuid→index остаются валидными. Физическая чистка — Compact/CompactIfNeeded в processQueueTick. Redis-схема не меняется (сообщения и так лежат в per-message HASH).

2.2. Код (коммит 22e9bc8)

  • models.Queue: deadIdx, receiptIdx, uuidIdx, liveCount, deadCount (все json:"-")
    • примитивы: AppendMessage, RemoveIndex, Compact, CompactIfNeeded, Reset, EnsureIndexes/RebuildIndexes, IndexOfReceipt, IndexOfUUID, MarkInFlight, ClearInFlight, InFlightIndexes, IsDead, LiveCount.
  • DeleteMessage / ChangeMessageVisibility(±Batch) — поиск по индексу вместо скана среза; удаление без сдвига остатка.
  • DeleteMessageBatch — tombstone вместо полной пересборки среза.
  • processQueueTick — visibility/DLQ по индексу in-flight (O(in-flight)), retention — через tombstone, в конце — CompactIfNeeded.
  • send-пути — AppendMessage/LiveCount; admin/metrics/persistence — учёт живых сообщений; LoadAllQueues — RebuildIndexes после загрузки.

2.3. Тесты (коммиты e05af04, 5b3fb51)

  • app/models/message_index_test.go — 6 тестов примитивов (tombstone, компакция, индексы, Reset, порог).
  • app/gosqs/delete_middle_test.go — удаление СЕРЕДИНЫ очереди через API: остальные не теряются, удалённое не возвращается.
  • app/gosqs/tenant_flow_test.go — существующие тесты переведены на LiveCount(); добавлен TestReceiveEmptyQueue_DoesNotLeakLock.

3. НАЙДЕННЫЙ ДЕФЕКТ (моя регрессия) — утечка per-queue блокировки

3.1. Проявление

Стресс (2000 сообщений × циклы): ReadTimeout, всплеск 1.9 с на delete_batch, дальше — зависание сервиса (health при этом отвечает).

3.2. Диагностика (факты, не догадки)

  • Снят стек горутин по SIGQUIT: десятки горутин в sync.RWMutex.RLock (models.go:101 Queue.RLockMu), включая metrics.updateGauges (ждала 15 минут) и HTTP-хендлеры GetQueueAttributesV1.
  • Аудит всех GetLocked/GetRLocked: дефект ровно один.
  • receive_message.go:
    queue := models.SyncQueues.GetLocked(key)     // Lock взят
    if queue == nil || queue.LiveCount() == 0 {
        return nil, false                          // ← возврат БЕЗ Unlock
    }
    defer queue.UnlockMu()
    

3.3. Причина и почему проявилось именно сейчас

Возврат до defer оставлял queue.mu захваченным навсегда (в Go мьютекс не освобождается при выходе из функции). Прежнее условие len(queue.Messages) == 0 срабатывало только на полностью пустой очереди; после перехода на tombstone появился новый случай LiveCount()==0 при len(Messages)>0 (все сообщения удалены, компакция ещё не прошла) — типовой для последовательных циклов «залить → выгрести».

3.4. Фикс

defer queue.UnlockMu() — до любой проверки/возврата. Регрессионный тест TestReceiveEmptyQueue_DoesNotLeakLock проверен на дефекте (падает за 2 с) и на фиксе (зелёный).

4. Результаты проверок (после фикса)

Проверка Результат
go build ./..., go vet ./... OK
go test -race ./app/... зелёные
tests/api_test.sh (aws CLI, локальный сервис) PASS=22 FAIL=0
tests/sdk_test.py (boto3 1.43.108) 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 с нет

5. Инфраструктурные заметки (для повторного прогона)

  • Локальный сервис: SHARED_SQS_SEED_DEMO=true /tmp/shared-sqs --admin-token=... --port=4100 (memory-only без REDIS_ADDR; креды демо-тенанта печатаются в лог при старте).
  • boto3 в системе НЕ установлен (PEP 668) и python3.12-venv отсутствует → wheels скачаны через pip download и распакованы в /tmp/sqlibs_unpacked, запуск с PYTHONPATH=/tmp/sqlibs_unpacked.
  • Лимит очередей тенанта — 10: падавшие скрипты оставляют очереди, перед прогоном чистить (aws sqs delete-queue).
  • ENDPOINT (api_test.sh) и ENDPOINT_URL (python-тесты) — разные имена переменных.

6. Уроки

  1. Стресс-проверка обязательна: api/sdk/fifo проходили и с дефектом, он проявлялся только на циклах полного опустошения очереди.
  2. SIGQUIT — быстрый и точный способ найти зависание (кто держит мьютекс), дешевле догадок.
  3. При замене условия на новый критерий (len → LiveCount) надо перечитывать весь блок, включая порядок defer — старый код содержал латентную утечку лока, которую новый критерий сделал массовой.
  4. Не запускать сервис с | head — SIGPIPE убивает процесс на середине теста.