fix: lost-update в extensions.config — атомарный patchConfig (теряется решение совета при одновременном утверждении) #116

Closed
claude wants to merge 0 commits from fix/extension-config-lost-update into marketplace2
Owner

Симптом

Председатель утверждает оба решения совета на онбординге ЦПП «Стол заказов» почти одновременно (одно, сразу второе) → сохраняется только второе, первое навсегда зависает в «Ожидаем решение совета». Если кликать с паузой — проблемы нет.

Причина (lost-update, системная)

Каждый @OnEvent(DecisionTrackedEvent)-листенер (generic + capital + chairman) и сервисы онбординга делали read-modify-write всего extensions.config (JSONB): читали блоб → ставили один флаг → писали весь объект назад через repository.update(). marketplace идёт через generic-листенер. Два конкурентных события одного расширения читают config без обоих флагов → последний writer затирает первого.

Вторичный баг: при гонке ни один обработчик не видит оба _doneallDone/isL1Complete не срабатывает → coopAcceptance.accepted/auto-restart зависают.

То же проявлялось ранее в настройках расширений — тот же анти-паттерн на общем config-блобе.

Фикс — глобальный примитив

ExtensionDomainRepository.patchConfig(name, patch): read-merge-write под строчным локом (SELECT … FOR UPDATE) в одной транзакции, shallow-merge top-level ключей, возврат свежего слитого config.

  • Лок сериализует писателей → патчи разных ключей не теряются.
  • Второй писатель читает уже закоммиченный результат первого → детекция «все шаги завершены» больше не зависает (allDone читается из возвращённого merged).

Переведены на patchConfig все write-path онбординг-семейства:

  • generic extension-onboarding-events + extension-onboarding (completeStep, loadPlugin)
  • capital onboarding-events + onboarding (completeStep, loadPlugin)
  • chairman onboarding-events + onboarding (completeAgendaStep, completeGeneralMeet, loadPlugin)
  • marketplace coop-acceptance + syncCoopAcceptanceFromOnboarding

Тесты

Моки репозитория дополнены patchConfig (shallow-merge), ассерт coop-acceptance переведён на patchConfig. Полный tsc/jest — в CI.

Остаётся (вне scope)

Тот же findByName+update есть ещё в chatcoop/powerup/meet-tracker/decision-expired — низкая конкуренция, можно переводить на patchConfig инкрементально. Истинный concurrency-регресс-тест требует testcontainers Postgres (unit-мок гонку не воспроизводит).

🤖 Generated with Claude Code

## Симптом Председатель утверждает оба решения совета на онбординге ЦПП «Стол заказов» почти одновременно (одно, сразу второе) → сохраняется только второе, первое навсегда зависает в «Ожидаем решение совета». Если кликать с паузой — проблемы нет. ## Причина (lost-update, системная) Каждый `@OnEvent(DecisionTrackedEvent)`-листенер (**generic** + capital + chairman) и сервисы онбординга делали **read-modify-write всего `extensions.config` (JSONB)**: читали блоб → ставили один флаг → писали весь объект назад через `repository.update()`. marketplace идёт через generic-листенер. Два конкурентных события одного расширения читают config без обоих флагов → последний writer затирает первого. Вторичный баг: при гонке ни один обработчик не видит оба `_done` → `allDone`/`isL1Complete` не срабатывает → `coopAcceptance.accepted`/auto-restart зависают. То же проявлялось ранее в настройках расширений — тот же анти-паттерн на общем config-блобе. ## Фикс — глобальный примитив `ExtensionDomainRepository.patchConfig(name, patch)`: read-merge-write под строчным локом (`SELECT … FOR UPDATE`) в одной транзакции, shallow-merge top-level ключей, возврат **свежего слитого** config. - Лок **сериализует** писателей → патчи разных ключей не теряются. - Второй писатель читает уже закоммиченный результат первого → детекция «все шаги завершены» больше не зависает (allDone читается из возвращённого merged). Переведены на `patchConfig` все write-path онбординг-семейства: - generic `extension-onboarding-events` + `extension-onboarding` (completeStep, loadPlugin) - capital `onboarding-events` + `onboarding` (completeStep, loadPlugin) - chairman `onboarding-events` + `onboarding` (completeAgendaStep, completeGeneralMeet, loadPlugin) - marketplace `coop-acceptance` + `syncCoopAcceptanceFromOnboarding` ## Тесты Моки репозитория дополнены `patchConfig` (shallow-merge), ассерт coop-acceptance переведён на `patchConfig`. Полный tsc/jest — в CI. ## Остаётся (вне scope) Тот же `findByName`+`update` есть ещё в chatcoop/powerup/meet-tracker/decision-expired — низкая конкуренция, можно переводить на `patchConfig` инкрементально. Истинный concurrency-регресс-тест требует testcontainers Postgres (unit-мок гонку не воспроизводит). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude added 1 commit 2026-06-09 11:57:07 +00:00
Причина бага «один шаг онбординга теряется (вечно „Ожидаем решение совета“),
если оба утвердить почти одновременно»: каждый @OnEvent(DecisionTrackedEvent)
-листенер (generic + capital + chairman) и сервисы онбординга делали
read-modify-write всего extensions.config JSONB — читали блоб, ставили один
флаг, писали весь объект назад. Два конкурентных события одного расширения
читали config без обоих флагов → последний writer затирал первого. Плюс
вторичный баг: при гонке ни один обработчик не видел оба _done →
allDone/isL1Complete не срабатывал → coopAcceptance.accepted/auto-restart
зависали.

Фикс — глобальный примитив ExtensionDomainRepository.patchConfig(name, patch):
read-merge-write под строчным локом (SELECT … FOR UPDATE) в одной транзакции,
shallow-merge top-level ключей, возврат СВЕЖЕГО слитого config. Лок
сериализует писателей → патчи разных ключей не теряются, а второй писатель
видит коммит первого, поэтому детекция «все шаги завершены» больше не зависает.

Переведены на patchConfig все write-path онбординг-семейства: generic
events+service, capital events+service, chairman events+service (+general_meet),
marketplace coop-acceptance и syncCoopAcceptanceFromOnboarding. В
events-сервисах allDone/isL1Complete теперь читается из возвращённого merged.

Тест-моки репозитория дополнены patchConfig (shallow-merge), ассерт
coop-acceptance переведён на patchConfig.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ant approved these changes 2026-06-09 12:07:46 +00:00
claude closed this pull request 2026-06-09 12:35:24 +00:00

Pull request closed

Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: C9S/mono#116