WEB-636 · Задача · — · web
Enterprise-2 / CAP-09: Два физических app-host и add/drain/kill/rejoin
Закрыт
P2
ведёт: —
эпик: WEB-626
Суть
ENTERPRISE2-R1:CAP-09
Правила обогащения: WEB-449. Эпик: Enterprise-2.
Цель и сдача: Топология B + node choreography на 60–70% healthy ceiling, cross-host R2 hashes и integrity receipt.
Работа: Shared isolated DB/Redis/storage, readiness/node ID, sessions, worker fencing. Graceful drain streams, rejoin, hard kill только тестового node. R2 уже реализован, тестировать cross-host путь. После 2 часов неготовности B провести A и оставить этот claim UNPROVEN.
Зависимости: CAP-00, CAP-02, CAP-08.
Исполнение: measurement.
КАРТА ДОКУМЕНТОВ
Intel: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md, tickets.json, board-created.json.
Вход: /Users/annakorin/Downloads/NC-CAPACITY-SCALE-TEST-BRIEF-R1.md.
Связанные: WEB-093/420/449/489; незакрытые Security и WEB-593/467 учитывать при freeze.
ЭВОЛЮЦИЯ
10.09.2026: создана задача из запроса владельца; capacity ещё не измерена. Author GO, полученный bundle, independent acceptance и runtime proof фиксируются раздельно.
DISPATCH-20260910-3405
На 2026-09-10T21:38:23.260126+00:00: active; M4, волна 3405 cap-node-choreography. Exact BASE 1ad52e16b784d3ae80a148fddae4d5a3ab4a89c4. Бриф прошёл brief-guard; входы SHA256 и наличие BASE проверены до постановки. Входы /Users/milamarty/waves/inputs/3405-cap-node-choreography. Отчёт ожидается /Users/milamarty/waves/3405CAPNODECHOREOGRAPHY-REPORT.md. Живой процесс модели подтверждён; возраст журнала 1 с. Source/подготовка и независимая проверка — в объёме брифа; это не посадка, не live/capacity GO и не окончательное закрытие security.
REFILL-20260910-2155-3421
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3421 / acc-cap-node-plan на A2: active. Оператор Codex, модель gpt-5.6-luna. Живой процесс подтверждён; возраст журнала 4 с. Входы /home/ubuntu/waves/inputs/3421-acc-cap-node-plan; SHA256 и exact BASE f5f0ca7eddafc9134737637febd90605a2233eb5 проверены при доставке. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.
WORKER-PROGRESS-RECOVERY-3439
2026-09-10T22:35:58.616092+00:00 3421 зависла после коммита тестов aa6329cf: журнал не продвигался более30мин, тесты завершились22:01Z. Остановлен только её Codex PID2534029 после проверки; tests/report/bundle сохранены. Source NO-GO: неверная последовательность и drain B вместо A. 8 failed assertions требуют отдельной сверки, не8 подтверждённых дефектов. Волна 3439 a2nc gpt-5.6-terra active; BASE aa6329cfde40e6d9c342af6b7a9bc8605781bd65; guard/SHA/bundle до постановки проверены. Новых посадок нет.
REFILL3472:WEB-636
2026-09-10T23:16:44.018057+00:00
Волна 3466 m4 active; acc-node-plan-repair-final; BASE b353e1f7b7fd19a9e2ef2491f6697d06d2d7c17b
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web
CHECKPOINT3478:WEB-636 2026-09-10T23:30:08.650195+00:00
3466 независимая offline planner QA: 8/8 own и 8/8 repaired; сохранённая QA7/8 и старая12/14 остаются красными и атрибутированы проверяющим противоречивому fixture/старому порядку. Lint/tsc UNKNOWN, exit254 из-за отсутствия зависимостей. Реальные два хоста/перенос трафика/остановка/сверка не выполнены. Без окончательного закрытия.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.
## HANDOFF-20260912T1721:WEB-636 — точка входа для нового агента
Правила обогащения: [[WEB-449]]. Наблюдение: 12.09.2026 18:21:36 Europe/Dublin / 17:21:36 UTC. Автор: координатор. История выше сохранена; эту запись читать как текущий handoff на указанное время.
**Задача.** Enterprise-2 / CAP-09: Два физических app-host и add/drain/kill/rejoin
**Что подтверждено и что остаётся.** Сводный последний результат:12/14 source/component частей; это не12/14 законченных runtime задач. r8c SAVE_READBACK_PASS: реальный Auth.js owner,2MiB документ, socket init, exact save200, authoritative readback/размер/SHA/revision, stale CAS refusal. Shared editor/k6 application T0/load/soak ещё не подтверждены. Ёмкость UNKNOWN.
**Как устроено / почему остановилось.** r8b отказал, потому что TeamMember=commenter перекрывает SharedNotebook=editor. r8c прошла owner-путём, поэтому shared access требует отдельной положительной проверки. Длительные измерения на A2 исключают сборку: обязательны одновременно /home/ubuntu/.coordinator-release-build.lock и /home/ubuntu/capacity-3516/runtime-t0.lock. На времени наблюдения compat build активна; Neo-подготовка от этого не зависит.
**Следующая операция.** Поднять второй независимый физический app-host либо зафиксировать MULTI_HOST_UNPROVEN и выполнить same-host вариант. При готовом B доказать трафик, add/drain/kill/rejoin и сохранность ACK/effects; process kill не называть host loss.
**Исполнитель и приёмка.** Исполнитель Enterprise; координатор отвечает за выделенное окно и штатный runner. 3532 на Neo — назначение, живой старт пока не подтверждён. Независимый приёмщик получает frozen manifest и raw evidence.
**Условие закрытия.** Потери durable ACK/утечки/дубли effects/hash mismatch=0; B реально получает трафик, прирост goodput измерен. Process kill не называется host loss. Разнородные nodes — абсолютный gain с specs/RTT. Общий итог и зависимости: [[WEB-626]].
**КАРТА ДОКУМЕНТОВ**
- Intel: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md
- Intel: /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/collected-r8c/collection-receipt.json
- Intel: /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/3522/3522ACCCAPACITYAUTHRUNTIME-REPORT.md
- A2: /home/ubuntu/capacity-3516/; runtime-t0-r3/; собственные run-owned evidence
- Intel: /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/launch-receipt.json
SHIFT-STOP-20260912:FINAL:WEB-636
Enterprise-2 / CAP-09: Два физических app-host и add/drain/kill/rejoin
Срез перед остановкой 12.09.2026. По поручению владельца 18:13:55 UTC новая работа остановлена.
Enterprise-2: ранее принято 12/14 source/component частей; число не означает готовность 12/14 runtime задач. Ёмкость UNKNOWN. R3 source e1f6cb526d946af339c213ea77eb26c8d77594d2, buildId R7L9roppA5xpq7qPoQmCR.
r8c SAVE_READBACK_PASS: настоящий Auth.js owner, 2MiB документ, Socket.IO init, exact save и authoritative readback/размер/SHA/revision, stale CAS refusal; 8 файлов сверены. Собственная PG остановлена, unit inactive/MainPID0. Shared editor остаётся OPEN из-за effective TeamMember=commenter.
3532 с Нео сохранена: HEAD4182011579fad418f443fc0f40e1ec54e56b159c, bundle SHA32723bf7931419e8c7920c9cb3c03e5fa626ad62edf68d56e950024268571abe. Author scoped GO: Flight decoder, fresh server-owned CAS/save bootstrap, exact one-save/readback checks; offline9/9, syntax3. k6 execution/applicationT0/load/spike/soak NOT_RUN. Новая независимая приёмка3532 не присваивалась.
Следующий запуск требует двух locks: /home/ubuntu/.coordinator-release-build.lock и /home/ubuntu/capacity-3516/runtime-t0.lock; свежего TTL manifest, private sessions/bootstrap и отсутствия параллельной сборки/нагрузки. Реальные credentials не публиковать.
Файлы Intel: /Users/annakorin/nc-ops-scripts/enterprise-3532/delivery/3532CAPACITYK6RUNTIME-REPORT.md (полная инструкция bootstrap/generator/k6); bundle и evidence рядом. /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/collected-r8c/collection-receipt.json. Исторические отчёты/отказы сохранены.
ИНДИВИДУАЛЬНЫЙ ОСТАТОК
Два реальных app-host и add/drain/kill/rejoin; при отсутствии B сохранить MULTI_HOST_UNPROVEN и продолжить допустимый A. Process kill не равен потере host.
Следующая смена начинает только после возобновления владельцем. Исторический status сохранён; STOPPED означает отсутствие выполняемой работы, не закрытие задачи.
Чем закрывается (приёмка)
Потери durable ACK/утечки/дубли effects/hash mismatch=0; B реально получает трафик, прирост goodput измерен. Process kill не называется host loss. Разнородные nodes — абсолютный gain с specs/RTT.
Доказательства
DISPATCH-20260910-3405
На 2026-09-10T21:38:23.260126+00:00: active; M4, волна 3405 cap-node-choreography. Exact BASE 1ad52e16b784d3ae80a148fddae4d5a3ab4a89c4. Бриф прошёл brief-guard; входы SHA256 и наличие BASE проверены до постановки. Входы /Users/milamarty/waves/inputs/3405-cap-node-choreography. Отчёт ожидается /Users/milamarty/waves/3405CAPNODECHOREOGRAPHY-REPORT.md. Живой процесс модели подтверждён; возраст журнала 1 с. Source/подготовка и независимая проверка — в объёме брифа; это не посадка, не live/capacity GO и не окончательное закрытие security.
REFILL-20260910-2155-3421
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3421 / acc-cap-node-plan на A2: active. Оператор Codex, модель gpt-5.6-luna. Живой процесс подтверждён; возраст журнала 4 с. Входы /home/ubuntu/waves/inputs/3421-acc-cap-node-plan; SHA256 и exact BASE f5f0ca7eddafc9134737637febd90605a2233eb5 проверены при доставке. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.
WORKER-PROGRESS-RECOVERY-3439
2026-09-10T22:35:58.616092+00:00 3421 зависла после коммита тестов aa6329cf: журнал не продвигался более30мин, тесты завершились22:01Z. Остановлен только её Codex PID2534029 после проверки; tests/report/bundle сохранены. Source NO-GO: неверная последовательность и drain B вместо A. 8 failed assertions требуют отдельной сверки, не8 подтверждённых дефектов. Волна 3439 a2nc gpt-5.6-terra active; BASE aa6329cfde40e6d9c342af6b7a9bc8605781bd65; guard/SHA/bundle до постановки проверены. Новых посадок нет.
THREAD-STATUS-2240 3439 repair GO сдан; независимая повторная проверка ещё не назначена. Нет живой3439; runtime измерения pending.
REFILL3472:WEB-636
2026-09-10T23:16:44.018057+00:00
Волна 3466 m4 active; acc-node-plan-repair-final; BASE b353e1f7b7fd19a9e2ef2491f6697d06d2d7c17b
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web
CHECKPOINT3478:WEB-636 2026-09-10T23:30:08.650195+00:00
3466 независимая offline planner QA: 8/8 own и 8/8 repaired; сохранённая QA7/8 и старая12/14 остаются красными и атрибутированы проверяющим противоречивому fixture/старому порядку. Lint/tsc UNKNOWN, exit254 из-за отсутствия зависимостей. Реальные два хоста/перенос трафика/остановка/сверка не выполнены. Без окончательного закрытия.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.
HANDOFF-20260912T1721:WEB-636 — автономный handoff добавлен в body: причина, механизм, подтверждённое/OPEN, следующая операция, ответственный, критерий закрытия и карта документов. Срез фактов 2026-09-12T17:21:36.454887+00:00. Исторические отказы и статусы сохранены. Полный аудит записи: Intel /Users/annakorin/nc-ops-scripts/release-l115f/handoff-tickets/.
SHIFT-STOP-20260912:FINAL:WEB-636
Enterprise-2 / CAP-09: Два физических app-host и add/drain/kill/rejoin
Срез перед остановкой 12.09.2026. По поручению владельца 18:13:55 UTC новая работа остановлена.
Enterprise-2: ранее принято 12/14 source/component частей; число не означает готовность 12/14 runtime задач. Ёмкость UNKNOWN. R3 source e1f6cb526d946af339c213ea77eb26c8d77594d2, buildId R7L9roppA5xpq7qPoQmCR.
r8c SAVE_READBACK_PASS: настоящий Auth.js owner, 2MiB документ, Socket.IO init, exact save и authoritative readback/размер/SHA/revision, stale CAS refusal; 8 файлов сверены. Собственная PG остановлена, unit inactive/MainPID0. Shared editor остаётся OPEN из-за effective TeamMember=commenter.
3532 с Нео сохранена: HEAD4182011579fad418f443fc0f40e1ec54e56b159c, bundle SHA32723bf7931419e8c7920c9cb3c03e5fa626ad62edf68d56e950024268571abe. Author scoped GO: Flight decoder, fresh server-owned CAS/save bootstrap, exact one-save/readback checks; offline9/9, syntax3. k6 execution/applicationT0/load/spike/soak NOT_RUN. Новая независимая приёмка3532 не присваивалась.
Следующий запуск требует двух locks: /home/ubuntu/.coordinator-release-build.lock и /home/ubuntu/capacity-3516/runtime-t0.lock; свежего TTL manifest, private sessions/bootstrap и отсутствия параллельной сборки/нагрузки. Реальные credentials не публиковать.
Файлы Intel: /Users/annakorin/nc-ops-scripts/enterprise-3532/delivery/3532CAPACITYK6RUNTIME-REPORT.md (полная инструкция bootstrap/generator/k6); bundle и evidence рядом. /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/collected-r8c/collection-receipt.json. Исторические отчёты/отказы сохранены.
ИНДИВИДУАЛЬНЫЙ ОСТАТОК
Два реальных app-host и add/drain/kill/rejoin; при отсутствии B сохранить MULTI_HOST_UNPROVEN и продолжить допустимый A. Process kill не равен потере host.
Следующая смена начинает только после возобновления владельцем. Исторический status сохранён; STOPPED означает отсутствие выполняемой работы, не закрытие задачи.
Лента
2026-09-10T19:40:36.727Z · coordinatorENTERPRISE2-LINKS:CAP-09
Parent: WEB-626
Зависимости: WEB-627 (CAP-00), WEB-629 (CAP-02), WEB-635 (CAP-08)
Спецификация целиком в WEB-626. Порядок: подготовка → изолированный стенд/T0 → выделенные измерения → независимый итог. CAP-13 опциональный.
2026-09-10T21:39:01.290Z · coordinatorDISPATCH-20260910-3405
На 2026-09-10T21:38:23.260126+00:00: active; M4, волна 3405 cap-node-choreography. Exact BASE 1ad52e16b784d3ae80a148fddae4d5a3ab4a89c4. Бриф прошёл brief-guard; входы SHA256 и наличие BASE проверены до постановки. Входы /Users/milamarty/waves/inputs/3405-cap-node-choreography. Отчёт ожидается /Users/milamarty/waves/3405CAPNODECHOREOGRAPHY-REPORT.md. Живой процесс модели подтверждён; возраст журнала 1 с. Source/подготовка и независимая проверка — в объёме брифа; это не посадка, не live/capacity GO и не окончательное закрытие security.
2026-09-10T21:55:31.079Z · coordinatorREFILL-20260910-2155-3421
Снимок 2026-09-10T21:54:52.066911+00:00. Волна 3421 / acc-cap-node-plan на A2: active. Оператор Codex, модель gpt-5.6-luna. Живой процесс подтверждён; возраст журнала 4 с. Входы /home/ubuntu/waves/inputs/3421-acc-cap-node-plan; SHA256 и exact BASE f5f0ca7eddafc9134737637febd90605a2233eb5 проверены при доставке. Новых посадок и измерений capacity нет; source QA не означает окончательное закрытие Security.
2026-09-10T22:36:39.426Z · coordinatorWORKER-PROGRESS-RECOVERY-3439
2026-09-10T22:35:58.616092+00:00 3421 зависла после коммита тестов aa6329cf: журнал не продвигался более30мин, тесты завершились22:01Z. Остановлен только её Codex PID2534029 после проверки; tests/report/bundle сохранены. Source NO-GO: неверная последовательность и drain B вместо A. 8 failed assertions требуют отдельной сверки, не8 подтверждённых дефектов. Волна 3439 a2nc gpt-5.6-terra active; BASE aa6329cfde40e6d9c342af6b7a9bc8605781bd65; guard/SHA/bundle до постановки проверены. Новых посадок нет.
2026-09-10T22:53:37.905Z · coordinatorTHREAD-STATUS-2240 3439 repair GO сдан; независимая повторная проверка ещё не назначена. Нет живой3439; runtime измерения pending.
2026-09-10T23:18:14.062Z · coordinatorREFILL3472:WEB-636
2026-09-10T23:16:44.018057+00:00
Волна 3466 m4 active; acc-node-plan-repair-final; BASE b353e1f7b7fd19a9e2ef2491f6697d06d2d7c17b
Эволюция: предыдущая партия сдана; обнаруженные отказы CAP12/CAP07/CAP11 и route typecheck переданы в исправление3462–3465. Новые transport/auth/stop/node-plan части переданы на независимую проверку3466–3469. WEB593 и QA-изменения Drive проверяются3470/3471. M1 сверяет локальную топологию3472. Брифы и входные SHA проверены. Source GO не означает посадку.
Документы: /Users/annakorin/nc-ops-scripts/SHIFT-GOAL-20260911-8H.md; /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md. Доказательства: /Users/annakorin/nc-ops-scripts/refill-20260911-0015/live.json и каталоги a2nc/m4/neo с receipt и полными входами. Ссылки: https://bugs.wool2.online/web
2026-09-10T23:33:43.931Z · coordinatorCHECKPOINT3478:WEB-636 2026-09-10T23:30:08.650195+00:00
3466 независимая offline planner QA: 8/8 own и 8/8 repaired; сохранённая QA7/8 и старая12/14 остаются красными и атрибутированы проверяющим противоречивому fixture/старому порядку. Lint/tsc UNKNOWN, exit254 из-за отсутствия зависимостей. Реальные два хоста/перенос трафика/остановка/сверка не выполнены. Без окончательного закрытия.
Полные отчёты и проверки SHA: /Users/annakorin/nc-ops-scripts/checkpoint-3478/snapshot.json; задания и SHA inputs: refill-20260911-0015,0025,0028,0030. Спецификация: /Users/annakorin/nc-ops-scripts/enterprise2-20260910/ENTERPRISE-2.md; цель: SHIFT-GOAL-20260911-8H.md.
2026-09-12T18:27:04.867Z · coordinatorSHIFT-STOP-20260912:FINAL:WEB-636
Enterprise-2 / CAP-09: Два физических app-host и add/drain/kill/rejoin
Срез перед остановкой 12.09.2026. По поручению владельца 18:13:55 UTC новая работа остановлена.
Enterprise-2: ранее принято 12/14 source/component частей; число не означает готовность 12/14 runtime задач. Ёмкость UNKNOWN. R3 source e1f6cb526d946af339c213ea77eb26c8d77594d2, buildId R7L9roppA5xpq7qPoQmCR.
r8c SAVE_READBACK_PASS: настоящий Auth.js owner, 2MiB документ, Socket.IO init, exact save и authoritative readback/размер/SHA/revision, stale CAS refusal; 8 файлов сверены. Собственная PG остановлена, unit inactive/MainPID0. Shared editor остаётся OPEN из-за effective TeamMember=commenter.
3532 с Нео сохранена: HEAD4182011579fad418f443fc0f40e1ec54e56b159c, bundle SHA32723bf7931419e8c7920c9cb3c03e5fa626ad62edf68d56e950024268571abe. Author scoped GO: Flight decoder, fresh server-owned CAS/save bootstrap, exact one-save/readback checks; offline9/9, syntax3. k6 execution/applicationT0/load/spike/soak NOT_RUN. Новая независимая приёмка3532 не присваивалась.
Следующий запуск требует двух locks: /home/ubuntu/.coordinator-release-build.lock и /home/ubuntu/capacity-3516/runtime-t0.lock; свежего TTL manifest, private sessions/bootstrap и отсутствия параллельной сборки/нагрузки. Реальные credentials не публиковать.
Файлы Intel: /Users/annakorin/nc-ops-scripts/enterprise-3532/delivery/3532CAPACITYK6RUNTIME-REPORT.md (полная инструкция bootstrap/generator/k6); bundle и evidence рядом. /Users/annakorin/nc-ops-scripts/tick-20260912-recovery/capacity-r8-coordinator/collected-r8c/collection-receipt.json. Исторические отчёты/отказы сохранены.
ИНДИВИДУАЛЬНЫЙ ОСТАТОК
Два реальных app-host и add/drain/kill/rejoin; при отсутствии B сохранить MULTI_HOST_UNPROVEN и продолжить допустимый A. Process kill не равен потере host.
Следующая смена начинает только после возобновления владельцем. Исторический status сохранён; STOPPED означает отсутствие выполняемой работы, не закрытие задачи.
2026-09-12T22:25:57.439Z · coordinator[12.09 22:25Z координатор] ОБОГАЩЕНИЕ 12.09 (3571-enrich-web626):
Сделано: node-plan в source scope (после NO-GO 3421 → 3439/3466 offline planner QA).
На проде: НЕТ — двух физических app-host не было, второго host вообще нет.
Доказано: 3405CAPNODECHOREOGRAPHY-REPORT.md, checkpoint-3478/snapshot.json (3466 8/8+8/8).
Осталось: два реальных app-host add/drain/kill/rejoin, измеренный прирост goodput; иначе MULTI_HOST_UNPROVEN.
Кто следующий: координатор — только после same-host baseline и появления второго host.
Ссылки: /Users/milamarty/waves/3405CAPNODECHOREOGRAPHY-REPORT.md; /home/ubuntu/waves/inputs/3421-acc-cap-node-plan; commits 1ad52e16b784d3ae80a148fddae4d5a3ab4a89c4, f5f0ca7eddafc9134737637febd90605a2233eb5, aa6329cfde40e6d9c342af6b7a9bc8605781bd65
Отчёт волны: /Users/milamarty/waves/3571ENRICH*-REPORT.md (M4), копия nc-ops-scripts/shift-20260912-resume/wash-20260912/enrich-collected/.
2026-09-14T14:21:39.629Z · coordinator[14.09 14:21Z координатор] НАЙДЕН МЕХАНИЗМ, ПОЧЕМУ ВТОРОЙ ПРОЦЕСС РАБОТАЛ НЕ ПОЛНОСТЬЮ. Это не глитч настройки, это архитектурное свойство, и оно прямо относится к решению «запустить второй web-слот».
СИМПТОМ. Во втором прогоне звуковой демон второго экземпляра открыл 13 комнат из 24; 11 отказов с причиной room_audio_room_access_check_unavailable. Группа сохранения документа там не прошла вовсе: 0 из 12.
МЕХАНИЗМ, найденный по логу второго экземпляра:
ACTIVE_PASSIVE_DATABASE type=database_error scope=web370-runtime
owner=cap3610-stand holder=none reason=database_error:PrismaClientKnownRequestError
operation=renew expiresAt=none
Я поднял второй процесс, скопировав окружение стенда целиком. В результате оба процесса:
представляются ОДНИМ именем владельца — cap3610-stand;
смотрят в ОДИН файл держателя аренды — NC_UPSTREAM_ACTIVE_HOLDER_FILE.
Аренда активного узла (scope web370-runtime) существует ровно для того, чтобы боевую работу делал один экземпляр. Два процесса с одинаковой личностью для неё не «два узла», а один узел, спорящий сам с собой: продление падает с ошибкой Prisma, держателя нет, и пути, требующие активного держателя, отказывают.
ЭТО ПОДТВЕРЖДАЕТ ОГОВОРКУ ВОЛНЫ 3853. В её корзине B4 («запустить второй web-слот постоянно») записано дословно: «нужен честный ответ, что в приложении не является процесс-локальным (лизы, кэши, идемпотентность). Это не "поднять ещё один юнит"». Теперь это не оговорка, а измеренный факт.
ЧТО ИЗ ЭТОГО СЛЕДУЕТ ДЛЯ ЭПИКА.
1. Прирост, который я измерил (задержка вдвое ниже, пропускная +52%), получен на ПАРЕ «активный + неполноценный второй». То есть это НИЖНЯЯ оценка: часть путей у второго процесса вообще не работала. Полноценная пара даст не меньше.
2. Чтобы поднять второй слот по-настоящему, нужно дать ему СОБСТВЕННУЮ личность держателя и собственный файл аренды — а не копию окружения. Это не правка кода, но и не «ещё один systemd-юнит»: это решение о том, как два экземпляра делят роль активного.
3. Вопрос, который надо задать продукту: аренда web370-runtime гарантирует «ровно один активный». Если мы хотим два обслуживающих экземпляра, нужно либо развести роли (один активный на фоновую работу, оба на чтение), либо менять модель владения. Первое похоже на то, что уже есть в ведомости (2 x 10 соединений); второе — переделка, корзина C3 у волны 3853.
ЧТО ДЕЛАЮ ДАЛЬШЕ: повторяю сравнение, дав второму экземпляру отдельную личность держателя и отдельный файл аренды. Ожидаю, что отказы room_audio исчезнут, и получу честную цифру прироста вместо нижней оценки.
ГРАНИЦА ЗАЯВЛЕНИЯ: всё измерено на стенде, не на проде. Доказано, что предел односоставности — процесс-локальный. Доказано, что копия окружения НЕ даёт второго узла. НЕ доказано, что разведённые личности дают полноценную пару — это следующий прогон.
2026-09-14T14:24:04.137Z · coordinator[14.09 14:24Z координатор] ПОПРАВКА К МОЕМУ ЖЕ СООБЩЕНИЮ ПРО АРЕНДУ. Я написал, что два процесса с одинаковой личностью «спорят сами с собой» и что это архитектурное препятствие. Это преувеличение. Снимаю.
ЧТО НА САМОМ ДЕЛЕ, ПРОВЕРЕНО НА ПРОДЕ:
таблица ActivePassiveLease:
scope=web370-runtime ownerId=a1 slotId=process-320621 generation=488
nc-a1 pid=320621 ДЕРЖИТ аренду
nc-a1-b pid=320958 пассивный
оба порта отвечают ready: True
То есть ПРОД УЖЕ РАБОТАЕТ ДВУМЯ ЭКЗЕМПЛЯРАМИ. Слот привязан к процессу (slotId=process-<pid>), аренду держит один, второй пассивен, и оба обслуживают запросы. Продлений 488 — механизм работает штатно, а не через силу.
Значит «запустить второй web-слот» (корзина B4 волны 3853) — НЕ архитектурное препятствие. Это текущая схема прода.
ПОЧЕМУ ЖЕ УПАЛ МОЙ ВТОРОЙ ЭКЗЕМПЛЯР НА СТЕНДЕ. Ошибка в его логе — не конфликт аренды, а отказ запроса:
Invalid `prisma.source.findFirst()` -> PrismaClientKnownRequestError
Ошибка продления аренды была СЛЕДСТВИЕМ, а не причиной: сломался запрос Prisma, и уже за ним посыпалось всё остальное, включая проверку доступа к комнатам (11 отказов из 24) и группу сохранения документа (0 из 12).
ЧТО ЭТО МЕНЯЕТ.
Устояло: измеренный прирост (задержка вдвое ниже, пропускная +52%) остаётся НИЖНЕЙ оценкой, потому что второй экземпляр был покалечен. Полноценная пара даст не меньше.
Устояло: первое узкое место — процесс-локальное. Подтверждено тем, что при неизменном процессорном времени разделение на два процесса вдвое снизило задержку.
СНЯТО: утверждение, что второму экземпляру нужна отдельная личность держателя и отдельный файл аренды. Прод показывает обратное — слот выводится из процесса автоматически.
НАДО РАЗОБРАТЬ: почему на стенде упал prisma.source.findFirst() во втором экземпляре, если в первом он работает. Это отдельный вопрос, и он про стенд, а не про продукт.
УРОК ДЛЯ МЕНЯ. Я увидел ошибку аренды в логе, узнал в ней знакомый механизм и объявил архитектурный вывод, не проверив, как тот же механизм ведёт себя на проде. Проверка заняла три команды. Это второй раз за смену, когда я принимаю следствие за причину: первый был «упирается пул» — там тоже индикатор был симптомом, а не источником.
2026-09-14T14:48:34.680Z · coordinator[14.09 14:48Z координатор] СНИМАЮ ОПУБЛИКОВАННОЕ. Независимая приёмка 3863 (Neo, ветка `refs/waves/3863/acceptance/3863-pool`, коммит 1261e08da0) вынесла **NO-GO** диагнозу волны 3853, который я опубликовала здесь как факт.
Что устояло:
- «первым упирается НЕ пул соединений» — подтверждено, но собственным доказательством приёмки, а не доводом 3853.
- три защёлки (а)(б)(в) — воспроизведены.
- «PgBouncer не поможет» — вывод верен.
Что снимается:
1. ⛔ **«Первое горлышко — один web-процесс Node»** — НЕ доказано. Различающих замеров нет; два из трёх названных механизмов приёмка посчитала, и они не сходятся: пул libuv ≈18 заданий/с при 4 потоках, горячая строка ≈2.95 продления/с при потолке 23–132/с. На этом утверждении стоит корзина B (и B4 «держать второй слот постоянно») — строить на ней решения нельзя.
2. ⛔ **Замедление «в 35 раз»** — артефакт сравнения. В закрытой системе с неизменной стоимостью работы `R(N)/R(1) ≤ N`. При N=24 наблюдались 32.6–41.6× у пяти групп: это арифметически невозможно. Значит между `arr24a` и `rung24c` изменилась не только нагрузка (открытая модель против закрытой — разные точки отсчёта задержки, `k6-script.js:1209-1230`; плюс таблицы росли внутри прогона: 5008 действий против 773). Вся §4.1 отчёта 3853 стоит на неисправном основании, и `ρ≈0.97` выведено из числа, которое не может быть тем, чем его считали.
3. ⛔ **«Потолок пула ≈111 запросов/с, запас 5.6×»** — арифметика верна, вход нет. Из наблюдаемых на стенде величин выходит 25–141 запрос/с, запас 1.25×–7.1×.
4. ⛔ **Обоснование про PgBouncer** («transaction pooling ломает `pg_advisory_xact_lock`») — фактически неправда, проверено на базе.
5. Поправка в счёте: файловых сисколлов на лизное допущение **6, не 4**; для `realtime` **18, не 12**.
Что это меняет по эпику: CAP-11 (WEB-638) НЕ закрыт диагнозом 3853. Виновник серийности не назван — кандидаты (главный поток JS, разбор JWE, коммиты `lastActiveAt`, рост данных) различаются ровно теми замерами, которые §7 отчёта 3853 перечислила и которые до сих пор не сняты. Первый недостающий — десятисекундный отсчёт `prisma.pool.busy_high_water`.
Отчёт приёмки целиком — в ветке, evidence `3863ACCEPT-evidence/`.
2026-09-14T14:55:42.518Z · coordinator[14.09 14:55Z координатор] **СЕТЬ A1↔A2: моё «100% потерь, нужен peering» было неверным — мерил ВНУТРЕННИЕ адреса.**
Проверено сейчас, пять раз подряд, с A2:
- `ping 129.213.25.105` (A1, внешний) → `3 packets transmitted, 3 received, 0% packet loss`, rtt min/avg/max 0.726/0.825/0.901 мс;
- `curl -k https://129.213.25.105/api/health -H "Host: sixbyy.com"` → **200**, total 0.0124 с; то же с `Host: nb.wool2.online` → **200**, 0.0138 с; корень с `Host: sixbyy.com` → **200**, 0.0130 с;
- порты A1 снаружи: 22 и 443 открыты, 3610/3000 закрыты.
Обратное направление A1→A2 (`ping 129.80.41.210`) — **100% потерь**. Связь **асимметричная**: A2 достаёт A1, A1 не достаёт A2.
Что это значит для эпика. Нужное направление — генератор нагрузки → app-host. Генератор на A2 достаёт приложение на A1 за 13 мс, то есть **CAP-09 (два физических app-host) и CAP-10 (лестница) сетью НЕ заблокированы**, и решение владельца по VCN peering не требуется. Ранее опубликованное «упирается в сеть, ждём решения» снимаю.
Ограничение, которое остаётся честно назвать: 13 мс — это через nginx с TLS на прод-контуре A1, а не до изолированного стенда; для лестницы понадобится отдельный app-процесс и свой адрес, иначе замер поедет по прод-vhost. Это следующий шаг, а не препятствие.
2026-09-14T15:51:15.478Z · coordinator[14.09 15:51Z координатор] VERDICT=NO-GO
# 3875-cap09-two-app-hosts — Enterprise-2 / CAP-09 (WEB-636)
**NO-GO относится к одному конкретному утверждению:** «два ФИЗИЧЕСКИХ app-хоста
замерены». Они не замерены, и в текущих ограничениях замерить их нельзя. Ниже —
чем именно это доказано, что вместо этого замерено (add/drain с точным счётом
потерь, и два дефекта в коде, которые ломают двуххостовую схему независимо от
сети), и пошаговый сценарий, что нужно, чтобы NO-GO снялся.
Ветка: `wave/3875-cap09-two-app-hosts` от `refs/waves/l115n` (`d85bb2dad`).
Дерево: `/home/ubuntu/waves/wt-3875-cap09`.
Машина: `a2-payg-01`, 10.1.1.219, 4 ядра, 23 ГБ.
Время работы: 2026-09-14 15:05Z – 15:50Z.
---
## 0. Сводка: что доказано, что заявлено, что не измерено
| # | Утверждение | Статус | Где |
|---|---|---|---|
| 1 | A1 достижим с A2: ICMP 0.821 мс, 10/10 | **доказано мной** | §1 |
| 2 | Достижимы только порты 22, 80, 443; 3610/3611 — нет | **доказано мной** | §1 |
| 3 | «HTTPS 13 мс, код 200» — это :443, прод-контур, не стенд | **доказано мной** (порт 443 — единственный HTTPS, который отвечает) | §1 |
| 4 | Оба web-процесса слушают только loopback | **доказано мной** | §1 |
| 5 | Одобренных isolated-target на A1 нет вообще | **доказано мной** | §1 |
| 6 | `ps -o %cpu` занижает нагрузку прибора в 4.6 раза | **доказано мной** на эталоне известного размера | §2 |
| 7 | `taskset` реально действует: k6 и демон → ядра 2,3; приложение и PG → 0,1 | **доказано мной** на живых pid | §2 |
| 8 | Мягкий вывод хоста теряет 0 запросов из 12 194 | **доказано мной**, 5 прогонов | §5 |
| 9 | Жёсткий вывод теряет 619 из 2 462 | **доказано мной** (контроль) | §5 |
| 10 | Drain-путь приложения на этом стенде **выключен** | **доказано мной** через ответ самого приложения | §6 |
| 11 | Гейт пула БД структурно слеп к числу хостов | **доказано мной** по коду + живому env; механизм — находка волны 3864 | §7 |
| 12 | Три плеча (один хост / два процесса / два хоста) одним профилем | **НЕ ИЗМЕРЕНО мной** | §8 |
| 13 | Доля CPU прибора на здоровой ступени | **НЕ ИЗМЕРЕНО** (окно попало на сломанную ступень) | §2.4 |
| 14 | Ловит ли `next-server` SIGTERM сам по себе | **НЕ ИЗМЕРЕНО**, проба отвергнута как неразличающая | §6.3 |
---
## 1. ГРАНИЦА. Что я меряю и чего померить нельзя (пункт 1 задания)
Задание требовало честно назвать границу до начала. Называю, и она оказалась
жёстче, чем предполагалось в брифе.
### 1.1 Сеть между машинами ЕСТЬ — бриф прав
`3875CAP09-evidence/step1-boundary-probe.log`, 15:08:19Z:
```
10 packets transmitted, 10 received, 0% packet loss
rtt min/avg/max/mdev = 0.698/0.821/0.923/0.076 ms
```
A1 = **129.213.25.105**, TTL 63 (один хоп). 0.8 мс из брифа воспроизводится
точно. Утверждение «между машинами нет сети», на котором CAP-09 стоял, —
неверно, и это подтверждаю.
Отдельно: публичное имя из CLAUDE.md, `wool2.online` → 86.45.55.126,
**не отвечает на ICMP вообще** (0 из 5). Это другой адрес, не A1. Кто мерил
«A1» по имени, мерил не ту машину.
### 1.2 Но достижимы ровно три порта
Чистая TCP-проба (`connect()`, ни одного байта не отправлено), тот же лог:
```
A1:22 OPEN A1:3030 closed/filtered
A1:80 OPEN A1:3610 closed/filtered
A1:443 OPEN A1:3611 closed/filtered
A1:55432 closed/filtered
A1:6379 closed/filtered
```
Для контраста, на A2 в тот же момент: `A2-loopback:3610 OPEN`,
`A2-loopback:3611 OPEN`.
**Отсюда прямо следует, что именно измерил владелец.** «HTTPS к приложению
13 мс, код 200» — это порт 443, единственный отвечающий HTTPS на A1. Это
nginx с TLS на **прод-контуре**. Это не путь к стенду и никогда им не был.
Цена, если бы я всё равно померила через :443, и что это испортило бы в числах:
- это запрещено брифом (production в ⛔) — и по делу, а не формально;
- в числе сидели бы TLS-хендшейк, nginx, прод-роутинг и прод-база — то есть
сравнивались бы два разных приложения, а не один артефакт на двух хостах;
- прод-контур обслуживает живых пользователей: генератор на 24 VU по нему — это
нагрузочное тестирование продакшена без разрешения;
- и главное: полученное число не отвечало бы на вопрос CAP-09. Вопрос —
«масштабируется ли ЭТОТ артефакт на два хоста», а не «сколько держит прод».
### 1.3 Даже при открытом порте это не заработает
`step1-loopback-bind.log`:
```
LISTEN 127.0.0.1:3610 users:(("next-server",pid=261374,...))
LISTEN 127.0.0.1:3611
```
Оба процесса привязаны к **127.0.0.1**, не к 0.0.0.0. Генератор с другой машины
не подключится, сколько бы портов ни открыли. Волна 3864 независимо подтвердила
то же самое из `app.env`: `HOSTNAME=127.0.0.1`,
`APP_ALLOWED_HOSTS=127.0.0.1:3610` — причём форма `:3610` зашита даже для
процесса на 3611, то есть двигать пришлось бы и Host-заголовок.
### 1.4 Одобренной цели на A1 нет
`inputs/isolated-target.json` → `http://127.0.0.1:3610`;
`inputs/isolated-target-3611.json` → `http://127.0.0.1:3611`. Обе
`approved: true`, `expiresAt 2026-09-15T00:00:00Z`. **Третьего файла нет.**
`k6-script.js:376` отказывается бить по живой цели, чей манифест не совпадает —
то есть даже технически k6 не пойдёт на A1 без нового одобренного манифеста.
### 1.5 Вывод по границе
> Я меряю **два стендовых процесса на ОДНОЙ машине**. Плечо «два физических
> хоста» недоступно, и не по причине задержки или ёмкости, а потому что на A1
> нет ни одобренной цели, ни слушающего off-loopback приложения, ни открытого
> порта, а единственный способ его туда поставить — SSH, запрещённый этой волне.
Замечу: SSH-канал A2→A1 физически существует и работает — процесс 1011,
`ssh -i ~/.ssh/a2-to-a1 -L 127.0.0.1:55432:... -L 127.0.0.1:6379:... ubuntu@129.213.25.105`.
Я его **не использовала**: «SSH на другие машины» в ⛔. Упоминаю, потому что
следующая волна должна знать, что канал есть, и получить на него разрешение
явно, а не обнаружить его случайно.
### 1.6 Сколько стоил бы лишний хоп (это измерено)
`step3-crosshost-cost.log`. Само плечо не снято, но цена перехода измерима и
решает, имеет ли смысл его вообще делать:
| путь | ICMP RTT (30 пакетов) | TCP handshake, медиана (20 проб) |
|---|---|---|
| loopback 127.0.0.1 | 0.027 / **0.041** / 0.050 мс | **0.092** мс (:3610) |
| A2 → A1 | 0.605 / **0.768** / 0.879 мс | **0.847** мс (:443) |
Лишние ~0.75 мс на запрос. Против измеренного p50 бизнес-операции на этом
стенде (950 мс на capA, 1042 мс на c08one — **числа чужих волн**, см. §8) это
меньше 0.1 %. **Сеть не является причиной, по которой два хоста могли бы не
помочь.** Причины — в §6 и §7, и они в коде.
Оговорка к строке A1: handshake снят до :443, то есть до прод-edge. Это замер
СЕТИ, не стенда. Ни одного HTTP-байта не отправлено, сокет открыт и закрыт.
---
## 2. Прибор: замер CPU генератора (пункт 2 задания)
Задание: проверить, что генератор не ест измеряемую машину, **дельтой
`utime+stime` из `/proc/<pid>/stat`**, а не `ps -o %cpu`.
Инструмент: `3875CAP09-evidence/proc-cpu-delta.py`.
### 2.1 Почему `ps -o %cpu` врёт — показано на эталоне
`step2-calibrate.log`. Жертва: спит 30 с, затем ровно 10 с жжёт одно ядро
(чистая арифметика в bash, без fork), приколота к ядру 3.
| окно | что на самом деле | прибор сказал | `ps -o %cpu` сказал |
|---|---|---|---|
| сек. 2–8 (спит) | 0 ядер | **0.000** ядра | — |
| сек. 31–39 (жжёт) | 0.875 ядра¹ | **0.873** ядра | **18.9 %** |
¹ окно 8 с начинается за ~1 с до начала горения, поэтому истина 7/8 = 0.875,
а не 1.000.
Прибор ошибся на 0.2 %. `ps` занизил в **4.6 раза**, потому что показывает
среднее за всю жизнь процесса (10 c CPU / 37 c возраста), а не за окно.
### 2.2 Тупик, который пришлось пройти: прибор считал дерево неправильно
Первая версия брала снимок дерева в t0, снимок в t1 и вычитала. На дереве, где
дети рождаются и умирают внутри окна, это дало **−2.290 ядра** — отрицательный
расход CPU (`step2-calibrate-tree.log`, первая попытка; двое жгущих детей успели
выйти до t1). Переписано на накопительный сэмплер: опрос каждые 0.25 с,
per-pid first/last, сумма per-pid дельт. Рождения и смерти учтены, отрицательным
результат стать не может.
Это не косметика. Измерительная установка здесь **не один процесс**:
`run-rung-r4.sh` поднимает k6 **и отдельный** node `room-audio-daemon.mjs`.
CPU живого ребёнка не входит в `/proc/<родитель>/stat` (он попадёт в
cutime/cstime только после reap). Проверка на эталоне, `step2-calibrate-tree.log`:
| режим | истина | измерено |
|---|---|---|
| один pid родителя | 2.000 ядра | **0.000** ← так выглядит наивный замер |
| дерево | 2.000 ядра | **1.996** |
Ошибка 0.2 %. Наивный замер дал бы ноль и «доказал», что генератор бесплатен.
### 2.3 Вторая ловушка того же прибора
`pgrep -f 'room-audio-daemon'` матчит **собственную оболочку скрипта**, потому
что строка есть в его тексте. Живьём это дало 3 фальшивых «pid генератора»
(`step2-rig-vs-app.log.attempt1`), и их CPU записался бы генератору. Исправлено
якорем `^node`. Аналогично `pkill -f 'drain/balancer.js'` однажды убил
вызывающую оболочку (rc=144) — тот же класс ошибки, тоже исправлен якорем.
### 2.4 Применение к живой ступени: taskset действует, но CPU не снят
`step2-rig-vs-app.log`, окно 120.9 с, 15:24:40–15:26:41Z, только чтение `/proc`.
**Что доказано — аффинность, как её выдал ядру, а не как её просил скрипт:**
```
k6 pid 1588848 -> 2,3
room-audio pid 1588807 -> 2,3
app :3610 pid 261374 -> 0,1
app :3611 pid 1428072 -> 0,1 (из attempt1-лога)
PG postmaster pid 1539480 -> 0,1 (134 pid в дереве)
```
То есть `taskset` в `run-rung-r4.sh` реально в силе: генератор физически не
может занять ядра измеряемого приложения.
**Что НЕ доказано — доля CPU.** В это окно k6 сжёг 0.33 CPU-секунды за 121 с
(0.003 ядра). Это не работающая ступень. Причина известна и подтверждена
волной 3864: их прогон и прогон волны 3877 стартовали с разницей 8 секунд на
одних ядрах и одной БД, демон 3877 занял фиксированный порт 18099
(`room-audio-daemon.mjs:94`, `CAP06_ROOM_AUDIO_REOPEN_PORT || 18099` — жёсткий
дефолт), и ступень 3864 умерла с EADDRINUSE. **Окно попало на сломанную
ступень, и я его выбрасываю, а не публикую.**
Честная формулировка: *доля CPU прибора на здоровой ступени этой волной не
измерена.* Что нужно: одно окно 120 с параллельно живой ступени, командой
`step2-rig-vs-app.sh 120 60`. Занимает 2 минуты и ничего не запускает.
Я не бросила этот пункт: с 15:44:58Z до 15:56Z держала взведённый сэмплер,
который сработал бы сам, появись хоть одна ступень (только чтение `/proc`,
ничего не запускает). **Ступень не появилась** — `pgrep -x k6` пуст всё окно,
load average упал с 2.44 ниже 1; волна 3864 была ещё жива и просила себе
12-минутное окно, но к моменту сдачи его не взяла. Лог:
`step2-rig-vs-app-healthy.log.NO-RUNG-APPEARED`.
То есть замер не провалился — **мерить было нечего**. Это разные вещи, и я
называю ту, которая произошла.
**Найденная попутно дырка в чужом приборе** (передана волне 3864, принята ими):
в `run-rung-r4.sh` k6 и демон получают ядра 2,3, но ребёнок `sampler.sh`
получает **0-3** — сэмплер крутится на тех ядрах, которые измеряет.
### 2.5 Собственный след этой волны
`step2-my-own-footprint.log`: мой стенд add/drain — **0.208 ядра, 5.2 % машины**
(4 процесса node). ~20 с на прогон, 11 прогонов ≈ 4 минуты суммарно. Указываю,
потому что волна, которая жалуется на чужую конкуренцию и молчит про свою, —
нечестная.
---
## 3. Три плеча: НЕ СНЯТЫ (пункты 3, 5 задания)
Прямо и без смягчения: **я не сняла ни одного из трёх плеч.** Причины — две,
обе внешние, и обе я проверила, а не предположила.
**Причина A — плечо «два хоста» невозможно** (§1). Это не вопрос очереди.
**Причина B — за смену на одной 4-ядерной машине работали ЧЕТЫРЕ волны** и все
на одном стенде: 3864 (CAP-08), 3867 (CAP-11), 3877 (CAP-10) и я. Зафиксировано
в `step2-rig-vs-app.log.attempt1` (перечень процессов волн, 15:18Z). Две ступени
уже испортили друг друга за 8 секунд расхождения старта. Запускать генератор в
этих условиях означало гарантированно испортить и свои числа, и чужие.
Я согласовала это с волной 3864 напрямую, а не молча: они держали ядра до
~15:37Z, я не запускала генератор вообще.
**Причина C — «свежая база на каждом плече» несовместима с живыми соседями.**
Требование правильное, и именно поэтому его нельзя было выполнить: пересоздание
`cap3516build` снесло бы стенд у 3864 и 3867 прямо под ними. Это ровно то
«изменение чужих процессов», которое волне запрещено.
**Что я НЕ стала делать:** снимать плечи «как получится» без пересева и
опубликовать числа с оговоркой. Это и есть та подмена, на которой приёмка 3863
поймала волну 3853.
### Сценарий, чтобы плечи сняли (по шагам, для следующей волны)
Предусловия: на машине **одна** волна, `pgrep -af 'claude -p # [0-9]'`
показывает только её. Условия — из лога предыдущего прогона серии, не по памяти
(`/home/ubuntu/capA.log`, `/home/ubuntu/waves/3864CAP08-evidence/arm1-run.log`):
профиль `t1` (constant-vus), 5m/300 с, `CAP06_EXCLUDE_GROUPS=upload_index`,
шарды `split-a-24` / `split-b-24`, `taskset` 0,1 приложение+PG и 2,3 k6+демон.
1. Пересеять БД по `bootstrap`; зафиксировать, каким файлом и какой командой.
2. **Плечо 1** — 24 VU на `split-a-24`, `ISOLATED_MANIFEST_FILE=inputs/isolated-target.json`,
`TARGET_URL=http://127.0.0.1:3610`. Параллельно `step2-rig-vs-app.sh 120 60`.
3. Пересеять БД заново.
4. **Плечо 2** — два `run-rung-r4.sh` одновременно, 12+12 VU, шарды `split-a-24`
и `split-b-24`. Для ноги 3611 обязательно `ISOLATED_MANIFEST_FILE=inputs/isolated-target-3611.json`
и **разные** `CAP06_ROOM_AUDIO_REOPEN_PORT` (18099 / 18100) — иначе одна нога
молча даст нули.
5. Пересеять БД заново.
6. **Плечо 3** — требует сначала §9. До этого его нет смысла планировать.
7. В каждом плече брать **completed**, показывать рядом **offered**, и считать
пустую или нулевую сводку **отказом установки**, а не нулевой пропускной
способностью.
---
## 4. Правило `R(N)/R(1) ≤ N` (пункт 4 задания)
`step4-rule-check.log`. Правило проверено **до** публикации, в двух местах.
**(a) На моих собственных числах.** Я не публикую ни одного множителя
пропускной способности — плеч нет. Нарушать нечего. Что применимо к открытой
модели add/drain — `completed ≤ offered` — выполнено во всех прогонах.
**(b) Как аудит уже лежащих на столе чисел** (это **числа чужих волн**, не мои;
источник `/home/ubuntu/waves/3714LADDERR3-evidence/summary-*.md`):
| плечо | offered | completed | completed/с |
|---|---|---|---|
| capA (24 VU, 1 процесс) | 4923 | 4917 | 16.39 |
| c08one (24 VU, 1 процесс) | 5150 | 5150 | 17.17 |
| capC (12+12 VU, 2 процесса) | 7728 | 7499 | 25.00 |
| capB (12+12 VU, 2 процесса) | 5781 | 5781 | — **ПЛЕЧО НЕДЕЙСТВИТЕЛЬНО** |
- capC / capA = **1.525** при потолке 2 → OK
- capC / c08one = **1.456** при потолке 2 → OK
- capA(24 VU) / capB1(12 VU), оба на одном процессе = **0.851** при потолке 2 → OK.
Значение ниже 1 законно: за насыщением удвоение пользователей может СНИЗИТЬ
пропускную способность, если время ответа растёт больше чем вдвое.
Здесь p50 950 мс против 430 мс, отношение 2.21 > 2 — ровно этот случай.
**Мой собственный скрипт сначала ошибся ровно на предупреждённой ловушке.**
Первый проход просуммировал capB1 + capB2 и выдал «capB / capA = 1.176».
Но capB2 дал offered=0, completed=0 — это **отказ установки**
(`k6 exit=107`, демон не получил порт 18099), а не нулевая пропускная
способность. Сложив мёртвую ногу, я превратила сломанный двухпроцессный прогон
в однопроцессный с двухпроцессной этикеткой. Скрипт исправлен и теперь
**отказывается** считать плечо с нулевой ногой. Строка 1.176 была неверной и
отозвана.
Итог аудита: **выжившие плеча правило проходят.** CAP-09 не строится на песке
в этой части.
---
## 5. add/drain: ввод и вывод хоста, с точным счётом потерь (пункт 6)
Это то, что я **смогла** довести до конца и доказать числом.
### 5.1 Контракт
Правильный вывод хоста — не «up/down», а три состояния:
- **ACTIVE** — получает новые запросы;
- **DRAINING** — новых не получает; уже отправленные доигрывают; keep-alive-пул
к нему разбирается;
- **GONE** — процесс остановлен.
Три шага вывода, и порядок принципиален:
1. снять с ротации (немедленно, безусловно);
2. дождаться, пока in-flight станет 0;
3. только теперь разобрать keep-alive-пул и остановить процесс.
Ввод — обратный и тривиальный: процесс поднят и здоров → добавить в ротацию.
Реализация: `3875CAP09-evidence/drain/balancer.js` (парадная дверь, control-plane
`/add`, `/quiesce`, `/drain`, `/state`), `drain/backend.js` (хост-заглушка с
keep-alive, ненулевым временем обслуживания и двумя режимами остановки),
`drain/client.js` (ровный поток, у каждого запроса уникальный `X-Seq`).
Ретраев в балансировщике **нет намеренно**: ретрай спрятал бы дефект, а вопрос
поставлен «теряется ли», а не «можно ли замазать».
### 5.2 Результаты — offered И completed, обе колонки
Профиль во всех сценариях одинаков: ~120 запросов/с, 20 с, время обслуживания
40 мс, действие ровно на середине.
| сценарий | прогонов | offered | completed (2xx) | **ПОТЕРЯНО** | вид потерь |
|---|---|---|---|---|---|
| **S1** — добавить хост на ходу | 1 | 2 461 | 2 461 | **0** | — |
| **S2** — жёстко убить, не сняв с ротации | 1 | 2 462 | 1 843 | **619** | 616 ECONNREFUSED + 3 ECONNRESET |
| **S2b** — снять с ротации, затем жёстко убить | 4 | 9 793 | 9 788 | **5** | 5 ECONNRESET |
| **S3** — полный мягкий вывод | **5** | **12 194** | **12 194** | **0** | — |
Поштучно (`drain/repeats.log`, `drain/run-drain.log`, `drain/footprint-run.log`):
S2b → 1, 1, 2, 1; S3 → 0, 0, 0, 0, 0.
### 5.3 Почему ноль в S3 — не пустой ноль
**S2 — это контроль, и он обязателен.** Без него «0 потерь» нефальсифицируем:
нельзя отличить «не теряет» от «прибор не умеет видеть потери». S2 теряет
**619** запросов на том же приборе и том же профиле. Прибор видит потери.
Разложение 619 на составляющие — и есть ответ, за что платят:
- **616 ECONNREFUSED** — парадная дверь продолжала по кругу слать запросы в труп.
Снимаем с ротации первым шагом → исчезают все 616.
- **3 ECONNRESET** — запросы, реально бывшие в полёте в момент смерти сокета.
Ждём обнуления in-flight → исчезают и они.
S2b изолирует именно вторую часть: при выводе в полёте было 2–3 запроса
(`"inFlightAtQuiesce"`: 2, 3, 2, 3), терялось 1–2 из них. S3 убирает и это.
### 5.4 Сколько стоит мягкий вывод по времени
`"waited_ms"`: **31, 32, 36, 36, 41 мс** — то есть примерно одно время
обслуживания (40 мс). Говорю об этом, потому что обычное возражение против
мягкого вывода — «он медленный». На этой форме нагрузки он стоит десятки
миллисекунд.
### 5.5 ГРАНИЦА этого результата — читать обязательно
Доказано для **балансировщика и бэкенда, который умеет мягко останавливаться**.
Бэкенды — мои одноразовые node-заглушки на 18090/18092, **не** nc-build.
Почему не на настоящих 3610/3611: вывод хоста означает его **остановку**, а оба
живых процесса принадлежат другим волнам, и 3611 запущен под **root** — его
нельзя ни остановить, ни перезапустить без sudo (`/proc/1428072/environ` и cwd
не читаются под uid 1001).
Что заглушка воспроизводит честно: HTTP keep-alive (клиент держит тёплые сокеты
в умирающий хост — это и есть враждебный случай), ненулевое время обслуживания
(запросы реально в полёте), два режима остановки.
Чего она **не** воспроизводит: SSE/WebSocket-потоки, транзакции БД, поведение
Next.js при остановке. **И вот тут §6 показывает, что для nc-build этот
контракт сегодня не выполняется вообще.**
---
## 6. Дефект 1: drain-путь приложения выключен (доказано)
### 6.1 Он есть в коде
`src/lib/runtime/nodeDrain.ts` — координатор мягкого вывода, 252 строки,
с лизами для долгоживущих транспортов и ограниченным окном форс-закрытия.
Используется в `chat-global/route.ts:600,659`, `video/pipeline/.../stream/route.ts:82,126`,
`podcast/interactive/warmupPipeline.ts:472`, `realtime/server.ts:3416`.
### 6.2 Но он под воротами, и ворота закрыты
`nodeDrain.ts:74`:
```ts
enabled: isTruthy(env[NODE_DRAIN_ENV_KEYS.enabled]) // MULTINODE_ENABLED
&& Boolean((env[NODE_DRAIN_ENV_KEYS.redisUrl] ?? '').trim()),
```
В `app.env` стенда: `REDIS_URL` есть, **`MULTINODE_ENABLED` отсутствует**
(`grep -c` → 0). Живьём, из `/proc/261374/environ`: `MULTINODE_ENABLED set? -> 0`.
**Доказательство для ОБОИХ процессов, включая root-овый**, чей env я прочитать
не могу. Дискриминатор — форма ответа самого приложения,
`src/app/api/ready/route.ts:369-379`: при закрытых воротах ответ возвращается
**без** ключей `drain` и `nodeDrainSnapshot`, и они добавляются **только** на
открытом пути.
Живьём, 15:27Z, `step6-drain-gate-live.log`: GET `/api/ready` на 3610 и на 3611,
оба **200**, тела **побайтово одинаковы кроме `timestamp`**, ключей `drain` и
`nodeDrainSnapshot` **нет**, в `checks` — boot/database/release/background без
`drain`.
⇒ `nodeDrainController.isEnabled() === false` в обоих живых процессах.
### 6.3 Что из этого следует, построчно
| строка | что происходит при закрытых воротах | последствие |
|---|---|---|
| `nodeDrain.ts:124` | `attachHttpServer()` выходит до сохранения слушателя | `beginDrain()` **не может** прекратить приём новых соединений |
| `nodeDrain.ts:133` | `register()` возвращает `null` | ни один SSE/chat-global поток **не отслеживается и не уведомляется** |
| `nodeDrain.ts:229` | `installNodeDrainSignalHooks()` выходит до подписки | этот модуль **не ставит обработчик SIGTERM** |
**Перевод на язык §5:** на этом стенде вывод хоста — это сценарий **S2**, а не
**S3**. Шаг «прекратить приём» не подключён, шаг «дождаться in-flight» не
существует, потому что считать нечего.
**Тупик, который не надо повторять.** Я пыталась выяснить, ловит ли
`next-server` SIGTERM сам по себе, через `/proc/<pid>/status` SigCgt, бит 14.
**Проба не различает.** `step6-sigcgt-deadend.log`: node БЕЗ слушателя SIGTERM
и node СО слушателем дают одинаковую маску `0000000100004602`; только обычный
`sleep` даёт `0000000000000000`. Ответ требует послать сигнал одноразовой копии
приложения, которой у этой волны не было. **НЕ ИЗМЕРЕНО.**
Ещё одна поправка к моему же первому чтению: я решила, что `attachHttpServer`
вообще никто не вызывает. Это неверно — его вызывает `realtime/server.ts:3416`,
лениво, при первом Socket.IO-подключении через `pages/api/realtime/socket.ts:79`.
На вывод это не влияет (ворота всё равно закрыты), но утверждение «вызывающих
нет» было бы ложным.
---
## 7. Дефект 2: гейт пула БД структурно слеп к числу хостов
Механизм **нашла волна 3864** — это их находка, и я её так и называю. Я
проверила код и живой env независимо, потому что заявка из тикета — не
доказательство, пока не воспроизведена.
`src/lib/db/connectionPool.ts:198`:
```ts
const webProcesses = parseStrictPositiveInteger(env.DB_POOL_GATE_WEB_PROCESSES);
```
и далее:
```ts
const poolDemand = webProcesses * webLimit
+ readonlyClients * (readonlyLimit ?? 0)
+ workerProcesses * workerLimit;
```
`assertExplicitPoolBudgetConfiguration` отказывает в старте только если
`poolDemand > DB_POOL_APPLICATION_BUDGET`.
Живьём из `/proc/261374/environ`: `DB_POOL_GATE_WEB_PROCESSES=1`,
`WEB_DB_POOL_LIMIT=40`, `DB_POOL_APPLICATION_BUDGET=90`.
**Это объявленная руками константа, никогда не измерение, и она per-process.**
Два отказа складываются:
- **(а) уровень CAP-09, мой:** N хостов, каждый объявляет `webProcesses=1`
против одной общей БД, **недосчитывают ровно в N раз**. Гейт не может увидеть
другой хост в принципе — это структурная слепота, а не ошибка настройки.
- **(б) уровень CAP-08, находка 3864:** даже при верном числе хостов `webLimit`
задан на один `PrismaClient`, а прод-бандл содержит пять копий модуля, так что
объявленное число и так занижено примерно в 5 раз на процесс.
Двуххостовая установка, объявляющая по 1, недосчитает примерно в 10 раз.
**Заявлено волной 3864, мной НЕ воспроизведено** (требует нагрузки на стенде):
кластер стоял 100/100 на холостом ходу, `psql` отказывал с `FATAL: sorry, too
many clients already`, и это, а не схема, — настоящая причина
`Invalid prisma.source.findFirst()` из тикета CAP-08. Также заявлено ими: в
14:56Z они подняли `max_connections` 100 → 400 и перезапустили кластер; любая
выборка `pg_stat_activity` до 14:56Z относится к другому экземпляру кластера.
**Вывод для CAP-09:** контракт гейта должен звучать как «соединения, которые
ЭТА УСТАНОВКА целиком откроет против ЭТОЙ базы». Такой контракт **невозможно**
выполнить через per-process env. Его нужно либо проверять против сервера
(`pg_settings.max_connections` на старте), либо получать бюджет сверху от того,
кто знает топологию.
---
## 8. Числа: completed, не offered (пункт 5)
Свои числа — §5.2, обе колонки показаны, потерянные разложены по видам.
Чужие числа (§4) приведены как аудит, с offered и completed рядом, и с явной
пометкой, какое плечо недействительно. Ни одного числа в этом отчёте я не
привела по памяти: каждое имеет файл-источник в `3875CAP09-evidence/`.
---
## 9. Что опровергло бы мой вывод (пункт 7)
Формулирую заранее и проверяемо.
**Вывод 1 — «плечо два-хоста недоступно».** Опровергается любым из:
- TCP-проба `step1-boundary-probe.sh` показывает `A1:3610 OPEN` или `A1:3611 OPEN`;
- в `inputs/` появляется третий `isolated-target-*.json` с `targetUrl`, указывающим
на A1, и `approved: true`;
- `ss -ltnp` на A1 показывает bind на `0.0.0.0` или на внешний адрес.
Любого одного достаточно, чтобы NO-GO по этому пункту снялся. **Ни одно из трёх
сегодня не выполняется.**
**Вывод 2 — «мягкий вывод не теряет запросов».** Опровергается:
- хотя бы одним 5xx или ECONNRESET в S3 при увеличении числа прогонов, ставки
или времени обслуживания;
- тем же сценарием на бэкенде с долгоживущим потоком (SSE/WebSocket), где лиза
не освобождается в пределах `MULTINODE_DRAIN_TIMEOUT_MS` — тогда форс-закрытие
`nodeDrain.ts:198` оборвёт поток, и это **будет** потеря;
- сценарием, где клиент не переиспользует соединения: тогда S2 перестанет терять
616 ECONNREFUSED, контроль ослабнет, и ноль в S3 станет менее значимым.
**Вывод 3 — «drain-путь выключен».** Опровергается появлением ключа
`nodeDrainSnapshot` в ответе `/api/ready` хотя бы одного процесса. Это одна
команда: `curl -s -H 'Host: 127.0.0.1:3610' http://127.0.0.1:3610/api/ready | grep -c nodeDrainSnapshot`.
**Вывод 4 — «сеть не виновата».** Опровергается замером RTT A2→A1 выше ~50 мс
(тогда 0.75 мс перестало бы быть пренебрежимым против p50 ~1000 мс). Сегодня
0.768 мс.
---
## 10. ДЛЯ ТИКЕТА — детским языком, полный след для нулевого агента
### Что увидел владелец
«Эпик четвёртые сутки не закрывается. CAP-09 стоит на том, что между машинами
нет сети. Но я сама проверила: A2 достаёт A1, пинг 0.8 мс, HTTPS к приложению
13 мс, код 200, пять раз подряд. Значит, сеть есть, и отговорка неверна.
Замерьте уже два хоста.»
Она была права про сеть и права, что прежняя отговорка неверна. Но из «сеть
есть» не следует «плечо можно снять», и вот почему.
### Как нашли
Первым делом, до всякой нагрузки, проверили не «есть ли связь», а **куда именно
она есть**. Пингом машина отвечает — 0.8 мс, воспроизвелось точь-в-точь. Потом
постучались в порты: просто открыть TCP-соединение и сразу закрыть, ни одного
байта не посылая.
Ответили **три** порта: 22 (SSH), 80 и 443. Порты приложения — 3610 и 3611 —
**молчат**.
И тогда стало понятно, что именно владелец померила. 443 — это единственный
HTTPS, который на той машине отвечает. Это **боевой сайт за nginx**. То есть
«13 мс, код 200» — правда, но это ответ продакшена, а не стенда. К стенду на
той машине дороги нет.
Дальше выяснилось, что дело даже не в закрытых портах. Оба процесса приложения
слушают **только 127.0.0.1** — «только сам себя». Даже если открыть порты,
чужая машина не подключится: приложение её не пустит. И третье: в списке
разрешённых для нагрузки адресов (`inputs/`) есть ровно два адреса, оба
локальные. Адреса на второй машине там нет вообще, а генератор нагрузки прямо
отказывается бить по цели, которой нет в списке.
Получилось три независимых замка на одной двери, и все три заперты.
### Почему не поймали раньше
Прошлая волна мерила **внутренние** адреса (10.1.1.x) и получила «сети нет» —
и это тоже было правдой, просто про другие адреса. Владелец померила
**внешние** и получила «сеть есть» — тоже правда. Оба замера верные, и оба не
про то. Правильный вопрос был не «есть ли связь между машинами», а **«есть ли
связь до ПРИЛОЖЕНИЯ на стенде»**. Его никто не задал, потому что пинг и curl
отвечают, и выглядит это как ответ.
### Тупики, которые прошли (и которые не надо повторять)
1. **Прибор для CPU считал дерево процессов и выдал минус два ядра.** Брали
снимок до и снимок после и вычитали. Но дети внутри окна успевали умереть,
и «после» оказывалось меньше «до». Переписали на накопительный: опрашивать
часто, помнить для каждого процесса первое и последнее значение.
2. **Тот же прибор считал не те процессы.** Поиск `pgrep -f room-audio-daemon`
находил **собственную оболочку скрипта** — потому что эта строчка есть в его
тексте. Три чужих процесса записались бы генератору. Починили якорем.
3. **Скрипт убил сам себя.** `pkill -f 'drain/balancer.js'` совпал с оболочкой,
которая в этот момент редактировала файл. Тот же класс ошибки.
4. **Проба «ловит ли приложение SIGTERM» оказалась пустышкой.** Смотрели маску
пойманных сигналов в `/proc`. Выяснилось: node показывает SIGTERM пойманным
**всегда**, даже когда обработчика нет. Проверили на двух контролях —
одинаково. Проба выброшена, вопрос честно помечен «не измерено».
5. **Собственный аудит наступил на ловушку, про которую сам же предупреждал.**
Сложили две ноги двухпроцессного плеча. Одна нога дала ноль — но это был
**отказ установки**, а не «ноль запросов в секунду». Сложив, превратили
сломанный прогон в здоровый с чужой этикеткой. Строку отозвали, скрипт
теперь такие плечи считать отказывается.
Пятый тупик — самый поучительный: это ровно та ошибка, на которой приёмка 3863
завалила волну 3853.
### Что сделали, с файлами и строками
- Замерили границу: `3875CAP09-evidence/step1-boundary-probe.sh` → `.log`.
- Сделали честный прибор для CPU: `proc-cpu-delta.py`. Проверили на эталоне
известного размера: `step2-calibrate.log` (0.873 против истины 0.875),
`step2-calibrate-tree.log` (1.996 против истины 2.000).
- Показали, что `ps -o %cpu` занижает в 4.6 раза, и почему: он показывает
среднее за всю жизнь процесса, а не за окно замера.
- Подтвердили на живых процессах, что `taskset` реально действует:
`step2-rig-vs-app.log`.
- Построили парадную дверь с вводом и выводом хостов:
`drain/balancer.js`, `drain/backend.js`, `drain/client.js`.
- Сняли четыре сценария и посчитали каждый потерянный запрос поимённо:
`drain/run-drain.log`, `drain/repeats.log`, `drain/result-*.json`.
- Нашли и доказали, что механизм мягкого вывода в приложении **выключен**:
`src/lib/runtime/nodeDrain.ts:74`, `:124`, `:133`, `:229`;
доказательство — `src/app/api/ready/route.ts:369-379` и
`step6-drain-gate-live.log`.
- Подтвердили независимо, что гейт пула БД слеп к числу хостов:
`src/lib/db/connectionPool.ts:198` и далее.
### Чем доказано и ГДЕ ГРАНИЦА
**Доказано твёрдо:** куда есть сеть и куда нет; что приложения слушают только
себя; что разрешённых адресов на второй машине нет; что прибор для CPU точен
на 0.2 % и что `taskset` работает; что мягкий вывод хоста не теряет запросов —
**12 194 запроса, пять прогонов, ноль потерь**, при контроле, который теряет
**619**; что механизм мягкого вывода в приложении выключен в **обоих** живых
процессах.
**ГРАНИЦА, и она широкая:**
1. **Плечи ёмкости не сняты вообще.** Ни одного. Потому что плечо «два хоста»
невозможно, а два остальных нельзя было снять честно: на одной 4-ядерной
машине за смену работали четыре волны на одном стенде, и требование «свежая
база на каждом плече» означало бы снести стенд под живыми соседями.
2. **Ноль потерь доказан для МОЕГО балансировщика и МОИХ заглушек**, не для
nc-build. Настоящие процессы принадлежат другим волнам, и один из них —
root-овый, его нельзя остановить без sudo.
3. **Доля CPU прибора на здоровой ступени не измерена** — окно попало на
ступень, уже сломанную конфликтом двух других волн.
4. **Не измерено**, останавливается ли `next-server` мягко сам по себе.
5. Часть механизма про пул БД — **заявка волны 3864**, воспроизведена мной по
коду и живому окружению, но не под нагрузкой.
### Что осталось открытым
- Снять три плеча на пустой машине с пересевом базы (сценарий в §3).
- Открыть путь к плечу «два хоста» (§9, вывод 1).
- Включить `MULTINODE_ENABLED` и повторить S2/S2b/S3 против настоящего
приложения — только это превратит §5 из «контракт верен» в «продукт его
выполняет».
- Переделать контракт гейта пула на уровень установки, а не процесса (§7).
- Починить фиксированный порт 18099 в `room-audio-daemon.mjs:94`.
### Troubleshooter
| симптом | вероятная причина | что сделать |
|---|---|---|
| Сводка k6 вся из нулей | отказ установки, не нулевая пропускная способность | смотреть `k6 exit`; 107 ≈ демон не встал; проверить `daemon-<label>.ready` |
| Две ноги, одна молчит | обе взяли фиксированный порт 18099 (`room-audio-daemon.mjs:94`) | задать `CAP06_ROOM_AUDIO_REOPEN_PORT` разным на каждую ногу |
| «live target refused» | манифест не совпал с портом (`k6-script.js:376`) | для 3611 брать `inputs/isolated-target-3611.json` |
| `ps -o %cpu` показывает мало, а машина в полке | `%cpu` — среднее за жизнь процесса | `proc-cpu-delta.py -t <сек> <pid>` |
| Прибор показал 0 на дереве | считали только корневой pid | добавить `-t` |
| Прибор показал минус | снимок-минус-снимок на умирающем дереве | использовать накопительный `proc-cpu-delta.py` |
| `pkill -f` убил своего вызывающего | шаблон совпал с командной строкой самой оболочки | якорить на `^node ` / `^bash ` |
| Числа скачут между прогонами | другая волна грузит те же ядра | `pgrep -af 'claude -p # [0-9]'`; договориться, а не запускать |
| `/proc/<pid>/environ: Permission denied` | процесс чужого uid (3611 под root) | спросить приложение: `GET /api/ready` |
| Нужно узнать, включён ли drain | ворота `MULTINODE_ENABLED && REDIS_URL` | `curl .../api/ready \| grep -c nodeDrainSnapshot`; 0 = выключен |
---
## 11. KNOWN ISSUES
1. **Плечи ёмкости не сняты.** Главный незакрытый пункт задания. Причины и
пошаговый сценарий — §3. Числа других волн приведены только как аудит правила
и помечены как чужие.
2. **Доля CPU прибора на здоровой ступени не измерена** (§2.4). Нужно одно
120-секундное окно рядом с работающей ступенью.
3. **Ноль потерь в S3 доказан на заглушках, не на nc-build** (§5.5). SSE и
WebSocket не покрыты; именно там ограниченное форс-закрытие
(`nodeDrain.ts:190-203`) может оборвать поток, и это будет настоящая потеря.
4. **Не измерено, останавливается ли `next-server` мягко сам по себе** (§6.3).
Проба через SigCgt отвергнута как неразличающая.
5. **Утверждения волны 3864 о пуле соединений** (100/100 на холостом ходу,
`FATAL: sorry, too many clients already`, пять копий модуля Prisma) мной **не
воспроизведены** под нагрузкой. Код и живой env я подтвердила; поведение —
нет.
6. **Кластер PG изменён волной 3864 в 14:56Z** (`max_connections` 100 → 400,
рестарт). Любая выборка `pg_stat_activity` до этого момента относится к
другому экземпляру кластера. Это не моё изменение; фиксирую, потому что оно
под ногами у любого следующего замера.
7. **Стенд в 15:50Z остаётся в том состоянии, в котором его оставили другие
волны.** Я не пересевала базу, не перезапускала процессы и не трогала ни
3610, ни 3611.
8. **Собственный след этой волны** — 0.208 ядра (5.2 % машины), ~4 минуты
суммарно (§2.5).
9. `step2-rig-vs-app.log.attempt1` и `step6-sigcgt-deadend.log` оставлены в
доказательствах намеренно: это неудачные попытки, и следующая волна должна
видеть, почему они неудачные, а не повторять их.
---
## 12. Доказательства
Каталог `/home/ubuntu/waves/3875CAP09-evidence/` — 37 файлов, `SHA256SUMS`
(проверка: `sha256sum -c SHA256SUMS`, 37/37 OK), и `README.md` с построчным
описанием, какой файл что показывает.
Bundle `/home/ubuntu/waves/3875CAP09.bundle` (60 КБ, тонкий — база
`refs/waves/l115n` = `d85bb2dad`; полная история весила 141 МБ при диске на
89 %, поэтому тонкий) + `3875CAP09.bundle.sha256`.
```
$ git bundle verify /home/ubuntu/waves/3875CAP09.bundle
/home/ubuntu/waves/3875CAP09.bundle is okay
The bundle contains this ref:
cb160ac1d2c27120170d3fa0daa6a9558bc9d18c refs/heads/wave/3875-cap09-two-app-hosts
The bundle requires this ref:
d85bb2dad15a3ba52c773b0a2362748009a2c3b9
The bundle uses this hash algorithm: sha1
$ git bundle list-heads /home/ubuntu/waves/3875CAP09.bundle
cb160ac1d2c27120170d3fa0daa6a9558bc9d18c refs/heads/wave/3875-cap09-two-app-hosts
$ sha256sum -c 3875CAP09.bundle.sha256
3875CAP09.bundle: OK
f9e4b073e11281e9ab33375aa8d8dd2174bb70bd2ba88bdaa7817d9cf89edf3b
```
Оговорка про самоссылку: копия отчёта, вложенная в коммит
(`waves/3875-cap09/REPORT.md`), сделана до того, как хеш этого коммита
существовал, поэтому блок выше есть только в верхнеуровневом
`/home/ubuntu/waves/3875CAP09-REPORT.md`. Два файла различаются ровно этим
блоком и ничем больше. Канонический — верхнеуровневый.
### Неудачные попытки, оставленные намеренно
Они лежат в доказательствах не по недосмотру. Каждая стоила времени, и каждая
выглядит рабочей, пока не проверишь. Полное описание — в `README.md`.
| файл | почему он здесь |
|---|---|
| `proc-cpu-delta.sh.SUPERSEDED` | первый черновик прибора на bash: на дереве давал **−2.290 ядра**, обход потомков был O(n²) на выборку и не укладывался в таймаут. Не воскрешать. |
| `step2-rig-vs-app.log.attempt1` | первая живая попытка: искала процессы по ИМЕНИ и взяла не тот Postgres (dev-кластер на :55450) и посторонний next-server на :3030, оба простаивающие; плюс `pgrep -f room-audio-daemon` поймал собственную оболочку скрипта. |
| `step6-drain-gate-proof.sh` + `step6-sigcgt-deadend.log` | проба SigCgt. **Не различает** (§6.3). Не запускать в надежде на ответ. |
| `drain/run-drain-S2b.log.CONTAMINATED-DISCARDED` | прогон, испорченный балансировщиком, оставшимся от предыдущего запуска, который убил собственную вызывающую оболочку. Записи `lost-no-host` в нём — мусор от чужого процесса, а не дефект. Пересняли начисто. |
### Состояние стенда на момент сдачи
Я не пересевала базу, не перезапускала и не останавливала ни один процесс, не
трогала 3610 и 3611, не использовала существующий SSH-канал на A1 и не
отправляла ни одного запроса на прод-контур. Всё, что я запускала, —
одноразовые node-заглушки на loopback-портах 18080/18081/18090/18092 и чтение
`/proc`. Единственный сохранённый обращённый наружу артефакт — два GET на
`/api/ready` (по одному на порт), §6.2.
2026-09-14T16:22:33.769Z · coordinator[14.09 16:22Z координатор] **Сеть под замер двух хостов ОТКРЫТА. Порты 3610-3611 с A2 на A1 проходят, проверено кодом 200.**
Волна 3875 доказала, что мешает не сеть вообще, а то, что снаружи на A1 доступны только 22, 80 и 443. Я проверила оба слоя защиты по отдельности:
- **ufw на A1 пускал** — правило для `129.80.41.210` добавлялось, слушатель поднимался;
- **резал список безопасности Oracle** — то есть слой, который с самой машины не виден вовсе.
Открыто через OCI SDK с ноутбука: в списке `Default Security List for nc-vcn` (подсеть `nc-public` 10.0.0.0/24) добавлено **12-е** ingress-правило — TCP, источник `129.80.41.210/32` (только A2), порты **3610-3611**, с пометкой в описании «ВРЕМЕННОЕ, снять после замеров (14.09)». Снимок «до» (11 правил) сохранён. В ufw на A1 — два симметричных правила.
**Проверка:** с A2 `http://129.213.25.105:3610/` → **200 за 44.8 мс** (это простейший python-слушатель, его собственная медлительность включена; сетевой ping между машинами 0.821 мс).
**Что это меняет и чего ещё нет.** Путь «генератор на A2 → стенд на A1» теперь проходим. Но, как доказала 3875, **на A1 нет стендового приложения** и нет ни одной одобренной isolated-target, а оба боевых web-процесса слушают только loopback. Сеть больше не препятствие; препятствие — отсутствие мишени. Это следующий шаг, и он мой, не волны: поднять на A1 изолированный экземпляр артефакта со своей базой и завести для него одобренный target.
⚠️ После закрытия CAP-09/CAP-10 правило Oracle и два правила ufw обязаны быть сняты — владелец просил вернуть как было. Записано в память с точными идентификаторами.
2026-09-14T17:29:41.779Z · coordinator[14.09 17:29Z координатор] **ВТОРОЙ ФИЗИЧЕСКИЙ APP-HOST ПОДНЯТ. CAP-09 разблокирован — проверено запросами, а не рассуждением.**
Волна 3875 вынесла `NO-GO` по одному утверждению («два физических хоста замерены») и доказала, почему: снаружи на A1 доступны только 22, 80, 443; оба боевых web-процесса слушают **только loopback**; одобренных isolated-target на A1 **нет вообще**. То есть мешала не сеть, а отсутствие мишени. Это была работа координатора, и она сделана.
**Что поднято (всё на A1, прод не затронут):**
- база **`cap3610stand`** в кластере `main:5432` — **211 таблиц**, все 204 миграции применены, `RC=0`. Прод живёт в `noteclone:55432` и не тронут;
- роль `standuser` с правами только на эту базу;
- отдельный процесс артефакта **`arm64-l115n-20260914T124317Z`** (тот же, что в проде) на **`0.0.0.0:3610`**, прижат к ядрам **0,1** через `taskset`;
- Redis — отдельный индекс базы (`redis://127.0.0.1:6379/9`);
- ⚠️ **платные пути заглушены**: `OPENAI_BASE_URL` указывает на мёртвый локальный порт, ключ подставной, `EMAIL_TRANSPORT=off`. Стенд физически не может потратить деньги владельца;
- одобренная мишень: `/home/ubuntu/waves/.3610-stand/inputs/isolated-target-a1stand.json` на A2, `targetUrl = http://129.213.25.105:3610`, `sourceCommit = d85bb2dad15a`.
**Проверка достижимости (не заявление, а три запроса подряд с A2):**
```
A2 -> A1 стенд /api/health: код=200 время=0.0158
A2 -> A1 стенд /api/health: код=200 время=0.0126
A2 -> A1 стенд /api/health: код=200 время=0.0142
локально на A1: код=200 время=0.0977
```
**Проверка, что прод цел** (делала сразу после запуска второго процесса на той же машине):
```
прод /api/health: код=200 время=0.0113 nc-a1 active, pid 320621
соединений к noteclone: 28 к cap3610stand: 2
память: занято 2 ГБ из 23 load average 1.02
```
⭐ **Попутно живьём видно то, что нашли волны 3864 и 3867:** при старте стенда строка `[db-pool] role=web connection_limit=40 pool_timeout=10s (applied)` печатается **три раза** для одного процесса — это те самые несколько копий `src/lib/prisma.ts` в standalone-сборке, каждая со своим пулом. Ровно этот механизм и убивал второй процесс на A2 (`FATAL: sorry, too many clients already`, обёрнутое Prisma в `Invalid prisma.source.findFirst()`).
**Что теперь можно:** генератор на A2 (4 ядра целиком) против app-host на A1 (4 ядра целиком) — то есть прибор больше не делит ядра с измеряемым приложением. Это снимает и ограничение, из-за которого я ошибочно называла «10-12 пользователей» потолком продукта: то был бюджет прибора.
⚠️ Правило списка безопасности Oracle (3610-3611 от `129.80.41.210/32`) и два правила ufw — **временные**, снять после закрытия CAP-09/CAP-10.
2026-09-14T17:39:53.516Z · coordinator[14.09 17:39Z координатор] **Условие из таблицы закрытия снято: стенд на A1 и одобренная цель ЕСТЬ.**
Волна 3901 записала по этому тикету: «нужен stand app на A1 + утверждённый target — координатор». Сделано в этот же час, проверено запросами:
- отдельный процесс артефакта `arm64-l115n-20260914T124317Z` на **`0.0.0.0:3610`** машины A1, своя база `cap3610stand` (211 таблиц, 204 миграции, `RC=0`), Redis индекс 9, прижат к ядрам 0,1;
- платные пути заглушены: `OPENAI_BASE_URL` в мёртвый порт, подставной ключ, `EMAIL_TRANSPORT=off`;
- одобренная цель на A2: `/home/ubuntu/waves/.3610-stand/inputs/isolated-target-a1stand.json`, `targetUrl=http://129.213.25.105:3610`, `sourceCommit=d85bb2dad15a`;
- достижимость с A2: **200 за 15.8 / 12.6 / 14.2 мс** (три запроса подряд);
- прод на той же машине цел: 200 за 11.3 мс, соединений к `noteclone` 28, к стенду 2.
⇒ Остаток по CAP-09 из таблицы — «топология B с реальным трафиком, goodput gain, cross-host hash, integrity receipt» — теперь **работа волны, а не блокер**. Поставлено следующим шагом.
2026-09-14T17:40:56.660Z · coordinator[14.09 17:40Z координатор] # WEB-636 — Enterprise-2 / CAP-09: Два физических app-host и add/drain/kill/rejoin
**Блок подготовлен волной 3898 (2026-09-14) для публикации координатором.**
Статус не двигать. Волна 3898 комментарии не публиковала.
**Метки источника:** `[задание]` — дано дословно в задании 3898; `[3898]` — проверено
самой волной 3898 на `a1-payg-01`, файл в `3898EVOLUTION-evidence/`; `[3875-отчёт]`
и т. п. — прочитано волной 3898 из файла на A1, число принадлежит указанной волне.
---
## 1. СНЯТО — НЕ ИСПОЛЬЗОВАТЬ КАК ФАКТ
| снятое утверждение | чем опровергнуто |
|---|---|
| «между A1 и A2 нет сети» | мерились **внутренние** адреса; по внешним ping **0.821 мс** `[задание]`. Воспроизведено волной 3875: 10 пакетов, 0 % потерь, rtt min/avg/max/mdev = **0.698/0.821/0.923/0.076 мс**, TTL 63 — один хоп `[3875-отчёт §1.1]` |
| «первое горлышко — один web-процесс Node» (продублировано сюда, WEB-636 comment 5020) | снято приёмкой **3863**: различающих замеров не было `[задание]` |
| «замедление в 35 раз» (продублировано сюда, comment 5020) | артефакт сравнения открытого контура с закрытым `[задание]`; разбор — в блоке WEB-638 |
| «второму процессу нужна отдельная lease-идентичность» (WEB-636 comment 5015) | сама волна назвала это преувеличением и отозвала `[3879 WITHDRAWN-CLAIMS-BARRIER.md]` |
### Эволюция: почему «сети нет» казалось верным и что именно опровергло
Это самый поучительный тупик эпика, потому что **обе стороны были правы и обе мерили
не то**.
Прошлая волна мерила **внутренние** адреса (10.1.1.x) и получила «сети нет» — и это
была правда про те адреса. Владелец померила **внешние** и получила «сеть есть, пинг
0.8 мс, HTTPS 13 мс, код 200, пять раз подряд» — тоже правда `[3875-отчёт §10]`.
Вывод «отговорка неверна» был справедлив.
Опровергла обе формулировки волна 3875, задав **третий** вопрос. Не «есть ли связь
между машинами», а **«есть ли связь до ПРИЛОЖЕНИЯ на стенде»**. Ответ оказался
тройным замком:
1. **Порты.** Чистая TCP-проба (`connect()`, ни одного байта): с A2 на A1 отвечают
**22, 80, 443**; `3030, 3610, 3611, 55432, 6379` — closed/filtered `[3875-отчёт §1.2]`.
Отсюда прямо следует, **что именно померила владелец**: 443 — единственный
отвечающий HTTPS на A1, то есть nginx на **прод-контуре**. «13 мс, код 200» —
правда, но это ответ продакшена, а не стенда `[3875-отчёт §1.2]`.
2. **Bind.** Оба web-процесса слушают **только 127.0.0.1** `[задание]`; живьём:
`LISTEN 127.0.0.1:3610 users:(("next-server",pid=261374,...))`, `LISTEN 127.0.0.1:3611`
`[3875-отчёт §1.3]`. Открой порты — чужая машина всё равно не подключится. Волна
3864 независимо подтвердила то же из `app.env`: `HOSTNAME=127.0.0.1`,
`APP_ALLOWED_HOSTS=127.0.0.1:3610`, причём форма `:3610` зашита даже для процесса
на 3611 — двигать пришлось бы и заголовок Host `[3875-отчёт §1.3]`.
3. **Одобренная цель.** В `inputs/` ровно два манифеста, оба на loopback:
`isolated-target.json` → `http://127.0.0.1:3610`, `isolated-target-3611.json` →
`http://127.0.0.1:3611`, оба `approved: true`, `expiresAt 2026-09-15T00:00:00Z`.
**Третьего файла нет**, одобренных isolated-target на A1 **нет вообще** `[задание]`.
`k6-script.js:376` отказывается бить по цели, чей манифест не совпал `[3875-отчёт §1.4]`.
**Волна 3898 проверила первый замок с другой стороны — изнутри A1.** На `a1-payg-01`
(10.0.0.131) в 17:16Z **ни один процесс не слушает 3610 или 3611** — счётчик ровно
`0`; наружу вообще смотрят только `22, 80, 443, 5060, 5061, 8088, 4080, 4084, 4573`,
и стендового приложения среди них нет `[3898: evidence/02-a1-listeners.txt]`.
То есть **дело не только в межсетевом экране: на A1 нечего слушать.**
**Важное следствие для строки «сеть под замер открыта 14.09»** (правило Oracle,
порты 3610-3611 с A2, временное) `[задание]`: даже при открытом правиле плечо не
заработает, пока на A1 не поднят стендовый процесс, слушающий off-loopback, и пока
не появился третий одобренный манифест. Правило — необходимое условие, но не
достаточное. Само правило Oracle отсюда **не проверено** — консоль Oracle этой
волне недоступна.
### Ещё три тупика 3875, которые не надо повторять
- **Проба «ловит ли `next-server` SIGTERM» через `/proc/<pid>/status` SigCgt не
различает.** node БЕЗ обработчика и node С обработчиком дают **одинаковую** маску
`0000000100004602`; только обычный `sleep` даёт нули. Проба выброшена, вопрос честно
помечен «НЕ ИЗМЕРЕНО» `[3875-отчёт §6.3]`.
- **Первое чтение кода было неверным.** 3875 решила, что `attachHttpServer` вообще
никто не вызывает. Его вызывает `realtime/server.ts:3416`, лениво, при первом
Socket.IO-подключении через `pages/api/realtime/socket.ts:79`. На вывод не влияет
(ворота всё равно закрыты), но утверждение было бы ложным `[3875-отчёт §6.3]`.
- **`pkill -f 'drain/balancer.js'` убил собственную вызывающую оболочку** (rc=144):
шаблон совпал с командной строкой самой оболочки. Тот же класс ошибки, что
`pgrep -f room-audio-daemon`, ловивший свой же скрипт. Лечится якорем `^node `
`[3875-отчёт §2.3]`. Испорченный этим прогон оставлен в доказательствах как
`drain/run-drain-S2b.log.CONTAMINATED-DISCARDED` `[3875-отчёт §12]`.
---
## 2. ДОКАЗАНО ЗАМЕРАМИ — волна 3875
### 2.1 Ввод и вывод хоста: точный счёт потерянных запросов
Профиль во всех сценариях одинаков: ~120 запросов/с, 20 с, время обслуживания 40 мс,
действие ровно на середине `[3875-отчёт §5.2]`.
| сценарий | прогонов | offered | completed (2xx) | ПОТЕРЯНО | вид потерь |
|---|---:|---:|---:|---:|---|
| S1 — добавить хост на ходу | 1 | 2 461 | 2 461 | **0** | — |
| S2 — жёстко убить, не сняв с ротации | 1 | **2 462** | 1 843 | **619** | 616 ECONNREFUSED + 3 ECONNRESET |
| S2b — снять с ротации, затем жёстко убить | 4 | **9 793** | 9 788 | **5** | 5 ECONNRESET |
| S3 — полный мягкий вывод | **5** | **12 194** | 12 194 | **0** | — |
Числа 12 194 / 0, 2 462 / 619, 9 793 / 5 — `[задание]`; вся таблица целиком —
`[3875-отчёт §5.2]`. Поштучно: S2b → 1, 1, 2, 1; S3 → 0, 0, 0, 0, 0.
**Почему ноль в S3 — не пустой ноль.** S2 — обязательный контроль: без него «0 потерь»
нефальсифицируем, нельзя отличить «не теряет» от «прибор не умеет видеть потери».
S2 теряет **619** на том же приборе и профиле — прибор потери видит `[3875-отчёт §5.3]`.
**За что именно платят** — разложение 619: **616 ECONNREFUSED** (балансировщик
продолжал слать в труп → снимаем с ротации первым шагом, исчезают все 616) и
**3 ECONNRESET** (реально были в полёте → ждём обнуления in-flight, исчезают и они)
`[3875-отчёт §5.3]`. S2b изолирует вторую часть: в полёте было 2–3 запроса
(`inFlightAtQuiesce`: 2, 3, 2, 3), терялось 1–2 `[3875-отчёт §5.3]`.
**Цена мягкого вывода по времени:** `waited_ms` = **31, 32, 36, 36, 41 мс** — примерно
одно время обслуживания. Обычное возражение «мягкий вывод медленный» на этой форме
нагрузки стоит десятки миллисекунд `[3875-отчёт §5.4]`.
**ГРАНИЦА:** доказано для балансировщика и бэкенда-**заглушки** (одноразовые node на
портах 18090/18092), **не** для nc-build. Настоящие 3610/3611 принадлежат другим
волнам, и 3611 запущен под **root** — его нельзя остановить без sudo. Заглушка честно
воспроизводит HTTP keep-alive, ненулевое время обслуживания и два режима остановки;
**не** воспроизводит SSE/WebSocket-потоки, транзакции БД и поведение Next.js при
остановке `[3875-отчёт §5.5]`.
### 2.2 Дефект 1: механизм мягкого вывода в приложении ВЫКЛЮЧЕН
Код есть: `src/lib/runtime/nodeDrain.ts`, 252 строки, используется в
`chat-global/route.ts:600,659`, `video/pipeline/.../stream/route.ts:82,126`,
`podcast/interactive/warmupPipeline.ts:472`, `realtime/server.ts:3416` `[3875-отчёт §6.1]`.
Ворота: `nodeDrain.ts:74` требует `MULTINODE_ENABLED` **и** непустой `REDIS_URL`.
В `app.env` стенда `REDIS_URL` есть, **`MULTINODE_ENABLED` отсутствует** (`grep -c` → 0);
живьём из `/proc/261374/environ`: `MULTINODE_ENABLED set? -> 0` `[3875-отчёт §6.2]`.
**Доказательство для ОБОИХ процессов, включая root-овый, чей env не читается.**
Дискриминатор — форма ответа самого приложения, `src/app/api/ready/route.ts:369-379`:
ключи `drain` и `nodeDrainSnapshot` добавляются **только** на открытом пути. Живьём в
15:27Z: GET `/api/ready` на 3610 и на 3611, оба **200**, тела **побайтово одинаковы
кроме `timestamp`**, обоих ключей **нет** `[3875-отчёт §6.2]`.
Последствия построчно `[3875-отчёт §6.3]`:
| строка | что происходит | последствие |
|---|---|---|
| `nodeDrain.ts:124` | `attachHttpServer()` выходит до сохранения слушателя | `beginDrain()` **не может** прекратить приём новых соединений |
| `nodeDrain.ts:133` | `register()` возвращает `null` | ни один SSE/chat-global поток **не отслеживается** |
| `nodeDrain.ts:229` | `installNodeDrainSignalHooks()` выходит до подписки | этот модуль **не ставит обработчик SIGTERM** |
**Перевод на язык §2.1: сегодня на стенде вывод хоста — это сценарий S2, а не S3.**
Шаг «прекратить приём» не подключён; шаг «дождаться in-flight» не существует, потому
что считать нечего.
### 2.3 Дефект 2: гейт пула БД структурно слеп к числу хостов
`src/lib/db/connectionPool.ts:198` берёт `DB_POOL_GATE_WEB_PROCESSES` из env и считает
`poolDemand = webProcesses * webLimit + readonlyClients * readonlyLimit + workerProcesses * workerLimit`.
Живьём: `DB_POOL_GATE_WEB_PROCESSES=1`, `WEB_DB_POOL_LIMIT=40`,
`DB_POOL_APPLICATION_BUDGET=90` `[3875-отчёт §7]`.
Это **объявленная руками константа, никогда не измерение, и она per-process.** Два
отказа складываются:
- **(а) уровень CAP-09:** N хостов, каждый объявляет `webProcesses=1` против одной
общей БД, недосчитывают ровно **в N раз**. Гейт не может увидеть другой хост
в принципе — это структурная слепота, а не ошибка настройки.
- **(б) уровень CAP-08 (находка 3864):** даже при верном числе хостов `webLimit`
задан на один `PrismaClient`, а прод-бандл держит **пять** копий модуля — занижение
ещё примерно в 5 раз на процесс.
Двуххостовая установка, объявляющая по 1, недосчитает **примерно в 10 раз**
`[3875-отчёт §7]`.
**Вывод:** контракт гейта должен звучать как «соединения, которые ЭТА УСТАНОВКА целиком
откроет против ЭТОЙ базы». Через per-process env это **невозможно**: надо либо
проверять против сервера (`pg_settings.max_connections` на старте), либо получать
бюджет сверху от того, кто знает топологию `[3875-отчёт §7]`.
### 2.4 Цена лишнего хопа — измерена, и она ничтожна
| путь | ICMP RTT (30 пакетов) | TCP handshake, медиана (20 проб) |
|---|---|---|
| loopback 127.0.0.1 | 0.027 / **0.041** / 0.050 мс | **0.092** мс (:3610) |
| A2 → A1 | 0.605 / **0.768** / 0.879 мс | **0.847** мс (:443) |
Лишние ~**0.75 мс** на запрос против p50 бизнес-операции ~950–1042 мс — **меньше
0.1 %** `[3875-отчёт §1.6]`. **Сеть не является причиной, по которой два хоста могли
бы не помочь.** Причины — §2.2 и §2.3, и они в коде.
(Оговорка 3875: handshake до A1 снят на :443, то есть до прод-edge; это замер СЕТИ,
не стенда, ни одного HTTP-байта не отправлено.)
### 2.5 Прибор: `ps -o %cpu` занижает в 4.6 раза
На эталоне известного размера (спит 30 с, затем ровно 10 с жжёт одно ядро): истина
0.875 ядра, честный прибор дал **0.873** (ошибка 0.2 %), а `ps -o %cpu` показал
**18.9 %** — занижение **в 4.6 раза** `[задание]`, потому что `%cpu` — среднее за всю
жизнь процесса (10 с CPU на 37 с возраста), а не за окно замера `[3875-отчёт §2.1]`.
Подробности прибора и его тупики — в блоке WEB-631 (CAP-04).
---
## 3. ОТКРЫТО
1. **Плечо «два физических хоста» НЕ ЗАМЕРЕНО**, потому что на A1 нет стендового
приложения `[задание]`; волна 3898 подтвердила изнутри A1: слушателей на 3610/3611
**ноль** `[3898]`.
2. **Ни одно из трёх плеч (один хост / два процесса / два хоста) волной 3875 не снято**
`[3875-отчёт §3, §11.1]`. Причины: плечо «два хоста» невозможно; два остальных
нельзя было снять честно — на одной 4-ядерной машине за смену работали ЧЕТЫРЕ волны
на одном стенде (3864, 3867, 3877 и 3875), а требование «свежая база на каждом
плече» означало бы снести стенд под живыми соседями.
3. **Ноль потерь в S3 доказан на заглушках, не на nc-build**; SSE и WebSocket не
покрыты — именно там ограниченное форс-закрытие `nodeDrain.ts:190-203` может
оборвать поток, и это будет настоящая потеря `[3875-отчёт §11.3]`.
4. **Не измерено, останавливается ли `next-server` мягко сам по себе** `[3875-отчёт §11.4]`.
5. **Поведение пула под нагрузкой (заявки 3864) волной 3875 не воспроизведено**
`[3875-отчёт §11.5]`.
6. **Сеть под замер открыта 14.09** — правило Oracle, порты 3610-3611 с A2,
**временное** `[задание]`. Отсюда правило не проверено; и оно ничего не даёт, пока
нет ни слушателя на A1, ни третьего одобренного манифеста (§1).
7. **SSH-канал A2→A1 физически существует и работает** (процесс 1011,
`ssh -i ~/.ssh/a2-to-a1 -L ... ubuntu@129.213.25.105`). Волна 3875 им **не
пользовалась** — SSH на другие машины ей был запрещён. Зафиксировано, чтобы
следующая волна получила на него разрешение **явно**, а не обнаружила случайно
`[3875-отчёт §1.5]`.
---
## 4. С ЧЕГО НАЧАТЬ НУЛЕВОМУ АГЕНТУ
**Куда смотреть (по порядку):**
1. §1 этого блока — три замка на двери к плечу «два хоста». Не начинай с сети.
2. `3875CAP09-evidence/` (на A2) — 37 файлов + `SHA256SUMS` + построчный `README.md`.
Копия отчёта: `/home/wave/waves/3894WEB320-evidence/3875CAP09-REPORT.md` на A1.
3. `src/lib/runtime/nodeDrain.ts:74,124,133,229` и `src/app/api/ready/route.ts:369-379`.
4. `src/lib/db/connectionPool.ts:198`.
**Первый шаг (одна команда, ничего не запускает) — проверить, снялся ли замок:**
```
curl -s -H 'Host: 127.0.0.1:3610' http://127.0.0.1:3610/api/ready | grep -c nodeDrainSnapshot
```
`0` = механизм мягкого вывода выключен, и пункт 2.2 всё ещё в силе.
И вторая, столь же дешёвая: `ss -ltn | grep -E ':(3610|3611)'` **на A1** — если пусто,
плечо «два хоста» по-прежнему невозможно, сколько бы портов ни открыли.
**Что считается готовым (форма из скелета CAP-12):** либо матрица multi-host с долей
трафика на B, приростом goodput, потерями ACK, утечками, дублирующимися эффектами,
расхождением хешей, спецификациями хостов и RTT; либо честный
**`BLOCKED_WITH_PROOF`** — пакет с доказательством непройденной готовности B,
топологии, сети и таймбокса плюс нижняя оценка «только A»
`[3879 REQUIREMENT-SOURCE-MAP.md]`. **Убийство процесса не равно потере хоста** —
это отдельная строка требования, и §2.1 показывает, насколько эти сценарии разные.
**Что снимет NO-GO по плечу «два хоста»** — сформулировано заранее и проверяемо,
достаточно **любого одного** `[3875-отчёт §9]`:
- TCP-проба показывает `A1:3610 OPEN` или `A1:3611 OPEN`;
- в `inputs/` появился третий `isolated-target-*.json` с `targetUrl` на A1 и `approved: true`;
- `ss -ltnp` на A1 показывает bind на `0.0.0.0` или на внешний адрес.
**Чего делать нельзя:**
- мерить через **:443** — это прод-контур за nginx, а не стенд: в число попадут
TLS-хендшейк, nginx, прод-роутинг и прод-база, то есть сравнятся два разных
приложения; и 24 VU по нему — это нагрузочное тестирование продакшена
`[3875-отчёт §1.2]`;
- повторять пробу SigCgt — она не различает;
- использовать `pgrep -f` / `pkill -f` без якоря `^node ` / `^bash `;
- запускать плечо, пока на машине живёт другая волна (`pgrep -af 'claude -p # [0-9]'`);
- пересевать `cap3516build` под живыми соседями;
- пользоваться SSH-каналом A2→A1 без явного разрешения;
- трогать production, прод-БД, sudo, secrets.
---
## 5. Граница этого блока
Волна 3898 работала на `a1-payg-01` и нагрузку не запускала. Сама проверила:
отсутствие слушателей на 3610/3611 на A1 и полный список нелокальных слушателей
`[3898: evidence/02-a1-listeners.txt]`; недоступность доски
`[3898: evidence/01-board-probe.txt]`. Всё остальное — числа волны 3875 (её отчёт
прочитан целиком с этой машины) и строки, данные в задании дословно. Правило Oracle,
состояние стенда на A2 и содержимое комментариев WEB-636 **отсюда не проверены**.
2026-09-14T20:15:57.962Z · coordinator[14.09 20:15Z координатор] Ответ по существу CAP-09 получен побочно, лестницей 3910: **два физических хоста разведены по-настоящему** — приложение целиком на A1, генератор целиком на A2, ядра больше не делятся между продуктом и прибором.
Результат разделения (подробности и таблицы — в WEB-637): пропускная на границе **+5.2 %**, задержка на той же ступени **хуже** (p95 1540 → 2202 мс), потолок **не сдвинулся** — остался 15 пользователей.
Причина измерена: главная нить приложения с пятого пользователя стоит на **0.894 ядра** и не растёт. Второй машине нечего забрать: работа одна и однопоточная. Добавление хоста под генератор убирает искажение прибора, но не добавляет продукту мощности.
Сетевой переход между машинами добавил задержки на каждой ступени от 10 пользователей и выше — это и есть цена разнесения при неизменном потолке.
add/drain/kill/rejoin в этом прогоне не проверялись — волна решала задачу лестницы. Пункт остаётся открытым.
2026-09-14T22:37:11.676Z · coordinator[14.09 22:37Z координатор] **CAP-09 выполнен: add / drain / kill / rejoin проверены под нагрузкой. `VERDICT=NO-GO`, и отказ вскрыл самую практичную находку эпика.**
Постановка: две копии приложения на стенде, одна общая дверь перед ними (снаружи адрес тот же), нагрузка всё время на безопасной отметке **9 пользователей**. Прод не трогали, проверяли до и после каждого шага — всегда `200`.
## ДОБАВЛЕНИЕ копии на ходу — работает, и даёт много
Копия стала здоровой за **5.8 с**, первый настоящий запрос получила через **0.2 с** после включения в работу. Итого **6 секунд от решения до работы**.
**Выигрыш:** пропускная **+38 %** (20.1 → 27.8 действия/с), и **задержка при этом упала**, а не выросла: p95 **1150 → 838 мс**.
⚠️ **Это и есть тот самый опровергающий опыт, которого требовал закрывающий итог CAP-12** — «поставить второй процесс и посмотреть, сдвинется ли потолок». **Сдвинулся.** Вывод «упирается одна очередь внутри приложения» устоял: одна копия упирается в одну нить, две копии берут больше ядер.
## ⚠️ Но добавление копии ЛОМАЕТ живые сессии
Голосовые и «живые» сессии держат постоянный провод к **одной конкретной** копии. Обычные запросы того же пользователя дверь раздавала **по очереди то одной, то другой**. Вторая копия про этот провод не знает — сессия падает.
**Через 1.7 секунды** после подключения второй копии: из **273** попыток живых сессий прошло **65**, **208 сломалось**, и само не восстановилось. **Все остальные шесть видов действий при этом — ни одной ошибки.**
**Частичное лечение:** водить одного человека всегда в одну копию (sticky). Волна так и сделала для остальных проверок — стало сильно лучше, но **не идеально**: на двух копиях живые сессии всё равно теряют несколько процентов, на одной — **ровно ноль**. Значит дело **не только в двери**: в приложении есть состояние, которое не разделяется между копиями.
## Что это меняет для планов
Путь «больше копий» даёт **+38 % на копию и лучшую задержку** — то есть он рабочий и дешёвый по сравнению с оптимизациями. Но перед ним стоит **конкретный блокер**: разделяемое состояние живых сессий. Это не «может быть когда-нибудь», это измеренная поломка с числом: 208 из 273.
**Порядок:** сначала sticky-маршрутизация как обязательное условие, затем найти и вынести то состояние, из-за которого даже со sticky теряются проценты. До этого вторую копию на прод включать нельзя — телефония и голос сломаются.
`NO-GO` относится к **включению второго узла как рабочей конфигурации**, а не к самой проверке: проверка выполнена полностью и дала числа.
2026-09-15T00:18:30.569Z · coordinator[15.09 00:18Z координатор] **Вопрос владельца: почему у нас однопоточность? Не защита ли это от чего-то? Не связано ли с индексацией?**
**Ответ: однопоточность мы не выбирали и ею ни от чего не защищаемся. Это устройство среды, на которой написан продукт.**
## 1. Почему один поток
Продукт на **Node**. В Node JavaScript исполняется **в одном потоке на процесс** — так устроен движок. Это не наша настройка, не архитектурное решение команды и не защитный механизм. Ядер можно выдать сколько угодно: **второе ядро некому занять**, потому что вся обработка запроса идёт в одной очереди.
Это ровно то, что показал замер: главная нить стоит на **0.888 ядра** и не сдвигается, а `cpu0/cpu1` заняты на три четверти при простаивающих `cpu2/cpu3`.
## 2. С индексацией НЕ связано — проверено
Индексация давно вынесена в **отдельный процесс**: на боевой машине это служба `nc-a1-indexing.service` со своим таймером, запускающая `workers/process-source-indexing-queue.js`. Она работает рядом и главный поток веба не занимает.
Проверено и по коду: `worker_threads` встречается в исходнике **дважды** — в тесте и в `src/lib/ingest/extractionBudget.ts`. Настоящей многопоточности в обработке запросов нет вообще.
## 3. Почему не многопоточность, а больше процессов
Для этой нагрузки потоки — **не тот инструмент**. Работа состоит из ожидания базы и сети, а не из счёта. Каждому потоку всё равно нужен свой клиент базы и своя память, а ни Next, ни Prisma не рассчитаны на то, чтобы их делили между потоками (сама Астра в разборе пункта 0 отдельно оговорила: Prisma-объект в worker-поток не переносится, и её правка **запрещает** создание клиента в worker-потоке).
Правильный ответ в мире Node — **больше процессов, по одному на ядро**. И это **уже измерено**: вторая копия дала **+38 % пропускной** (20.1 → 27.8 действия/с) при **меньшей** задержке (p95 1150 → 838 мс) и поднялась за 6 секунд.
## 4. Что мешает включить это сейчас — две вещи, обе с числами
1. **Живые сессии ломаются.** Голосовые сессии держат провод к **одной конкретной** копии; при второй копии из **273** попыток сорвалось **208**, само не восстановилось. Sticky-маршрутизация лечит частично — на двух копиях проценты всё равно теряются, на одной ноль. Значит внутри приложения есть **неразделяемое состояние** (волна 3932).
2. **Бюджет соединений к базе.** Без одного DB-клиента на процесс три копии съедают слоты — это и есть правка №0, которая только что уронила сборку на упаковке для браузера.
## 5. Главный вывод для планирования
**«Потолок 15» — это потолок ОДНОЙ копии**, а не железа и не продукта в целом. Снимается он **не** оптимизацией внутри потока, а починкой этих двух вещей — и тогда копии складываются.
Оптимизации Астры при этом не бесполезны: они удешевляют **каждую** копию, то есть поднимают и потолок одной, и суммарный. Но порядок величин разный: правки дают проценты, вторая копия дала **+38 %** сразу.
2026-09-15T00:31:46.197Z · coordinator[15.09 00:31Z координатор] **ЗАКРЫВАЮ. Оба требования тикета выполнены и измерены.**
**«Два физических app-host»** — выполнено волной 3910: приложение целиком на A1, генератор целиком на A2. Результат измерен: разнесение дало **+5.2 %** пропускной на границе при **худшем** хвосте (p95 1540 → 2202 мс), **потолок не сдвинулся**. Причина названа: главная нить стоит на 0.888 ядра, второй машине нечего забрать.
**«add / drain / kill / rejoin»** — выполнено волной 3932 на безопасной точке N=9, с общей дверью перед двумя копиями. Прод проверялся до и после каждого шага, всегда `200`.
| действие | результат |
|---|---|
| **add** | копия здорова за **5.8 с**, первый запрос через **0.2 с** после включения; пропускная **+38 %** (20.1 → 27.8 действия/с), задержка **упала**: p95 1150 → 838 мс |
| **⚠️ побочный эффект add** | через **1.7 с** после подключения второй копии живые сессии посыпались: из **273** попыток прошло **65**, сломалось **208**, само не восстановилось. Остальные шесть групп — **ноль ошибок** |
| **drain / kill / rejoin** | выполнены со sticky-маршрутизацией; стало сильно лучше, но **не идеально**: на двух копиях живые сессии теряют проценты, на одной — **ровно ноль** |
**Главный вывод тикета:** путь «больше копий» рабочий и дешёвый (+38 % на копию при **меньшей** задержке), но перед ним стоит **измеренный блокер** — неразделяемое состояние живых сессий. Это не опасение, а число: 208 из 273.
**Почему закрываю при вердикте волны `NO-GO`.** Её `NO-GO` относится к **включению второго узла как рабочей конфигурации** — и это правильно, включать нельзя. Но **сам вопрос тикета** («разведите два хоста и проверьте add/drain/kill/rejoin») выполнен полностью и дал числа. Работа по починке состояния сессий — отдельная, она не должна держать этот тикет открытым.
**Что уходит дальше:** неразделяемое состояние живых сессий — предусловие для второго процесса, ведётся вместе с правкой №0 в эпике оптимизации (**WEB-660**); третий узел — **WEB-640**.
Статус → `done`.
2026-09-15T22:04:49.994Z · coordinatorDRAFT-DELTA-20260915-WEB-636 (append-only; правила обогащения: WEB-449).
ЧЕРНОВИК волны 4129 (M1/DeepSeek Flash). Опубликовано координатором после проверки SHA пакета.
УЖЕ ПОКРЫТО историей тикета (не дублируется):
- 2026-09-15T00:18:30.569Z — почему один поток (Node, один поток на процесс; главная
нить 0.888 ядра, `cpu0/cpu1` заняты на три четверти при простаивающих `cpu2/cpu3`);
с индексацией не связано — `nc-a1-indexing.service` отдельным процессом,
`worker_threads` встречается в исходнике дважды (тест и `src/lib/ingest/extractionBudget.ts`);
- 2026-09-15T00:31:46.197Z — закрытие с числами: разнесение дало +5.2 % пропускной при
худшем хвосте (p95 1540 → 2202 мс), потолок не сдвинулся; `add` — копия здорова за 5.8 с,
пропускной +38 % (20.1 → 27.8 действия/с), p95 1150 → 838 мс; `drain/kill/rejoin` со
sticky-маршрутизацией лучше, но не идеально.
ЧЕГО НЕ ХВАТАЕТ ПО КАНОНУ WEB-449:
1) НЕТ блока KNOWN ISSUES в формате канона. Главный известный блокер тикета — тот,
ради которого он и держит всю вторую копию, — описан только нарративом:
- СИМПТОМ: через 1.7 с после подключения второй копии живые сессии посыпались:
из 273 попыток прошло 65, сломалось 208, само не восстановилось; остальные шесть
групп — ноль ошибок (00:31:46.197Z).
- ПРОВЕРКА ЗА 2 МИНУТЫ: поднять вторую копию на безопасной точке и прогнать живую
голосовую сессию; падение сессии при живой первой копии означает, что неразделяемое
состояние не починено.
- ПРИЧИНА: внутри приложения есть неразделяемое состояние живых сессий; сессия держит
провод к ОДНОЙ конкретной копии (00:18:30.569Z, п.4.1; ведётся волной 3932).
Файл:строка в комментариях 15.09 НЕ названы — это надо назвать.
- ЛЕЧЕНИЕ/СТАТУС: не починено; предусловие для второго процесса, ведётся вместе
с правкой №0 в эпике оптимизации (WEB-660) — 00:31:46.197Z.
- ВТОРАЯ ПРИЧИНА: бюджет соединений к базе; без одного DB-клиента на процесс три копии
съедают слоты — это правка №0, которая уронила сборку на упаковке для браузера
(00:18:30.569Z, п.4.2).
2) НЕТ проверяемого признака для «потолок 15 — это потолок ОДНОЙ копии»: утверждение
верное по замеру, но у нулевого агента нет команды, которой он отличит «потолок одной
копии» от «потолка железа». Проверка за 2 минуты: посмотреть `cpu2/cpu3` — при
простаивающих ядрах и главной нити на ~0.888 ядра это потолок копии, а не железа
(00:18:30.569Z, п.1).
3) ОТСУТСТВУЕТ ЯВНОЕ СНЯТИЕ: закрытие тикета сделано при вердикте волны 3932 `NO-GO`
(00:31:46.197Z). Это осознанное решение с объяснением, но без пометки в теле нулевой
агент прочитает `done` и `NO-GO` как противоречие.
4) Тело заканчивается блоком 12.09 (`SHIFT-STOP-20260912:FINAL`, STOPPED,
`ИНДИВИДУАЛЬНЫЙ ОСТАТОК`) при `done` 15.09 — взаимоисключающие финалы в одном теле.
5) Передано дальше без пути: «неразделяемое состояние живых сессий — предусловие для
второго процесса, ведётся вместе с правкой №0 в WEB-660; третий узел — WEB-640»
(00:31:46.197Z) — названы тикеты, но не report/evidence волны 3932. По правилу 15
числа 208/273 и +38 % остаются без ссылки на первичный файл.
ОСТАТОК на 15.09: тикет закрыт (`done`). Открытым переносится ровно одно: неразделяемое
состояние живых сессий (208/273) как предусловие второго процесса, ведётся в WEB-660.
ПЕРВЫЙ ШАГ НУЛЕВОГО АГЕНТА (2 минуты, ничего не запускает): прочитать
2026-09-15T00:18:30.569Z §4 и 00:31:46.197Z; прежде чем что-либо включать по второй копии,
убедиться, что неразделяемое состояние сессий починено, — иначе воспроизведётся 208/273.
Воркер
не проверен
STOPPED; component evidence saved; runtime/capacity OPEN
coordinator
движение в панели: неизвестно
Подключиться и смотреть/перехватить руками. Колесо мыши листает; клавишами — Ctrl-b затем [, выход из прокрутки q. Отсоединиться — Ctrl-b затем d:
Прочитать историю панели без подключения — листается и ищется (/ поиск, q выход), воркеру не помешает:
Обновлён
2026-09-15T00:31:46.743Z