Показывает: детально что происходит в Phase 2 — поиск существующих SIMPLE-выписок, параллельная генерация недостающих, batch-save с обработкой duplicate race.
Zoom in на блок Phase 2 из protocol-generation-pipeline.md.
Актуальные ADR: ADR-0004, ADR-0009, ADR-0012.
sequenceDiagram
autonumber
participant Gen as ProtocolGenerator<br/>(protocol)
participant EP as ExtractProvider<br/>(extract.adapter)
participant ES as ExtractSynthesizer<br/>(extract.adapter)
participant Core as ExtractService<br/>(extract.service)
participant XDB as extract<br/>(DB)
participant AS as agenda-service<br/>⟶ external (через AgendaProvider)
participant ECM as ECM<br/>⟶ external
Note over Gen: Вход: AfterPhase1<br/>(agenda, questions, regularQuestions, votesByQuestion)
%% ── Step 1: find existing ──
Gen->>EP: findByAgenda(agendaId)
EP->>XDB: SELECT * FROM extract<br/>WHERE agendaId=? AND type=SIMPLE AND active=true
XDB-->>EP: existing SIMPLE extracts
EP-->>Gen: List<Extract> existing
Note over Gen: missing = regularQuestions.ids \ existing.questionIds
alt missing.isEmpty()
Note over Gen: nothing to generate — use existing only
else missing не пусто
%% ── Step 2: parallel synthesis ──
Gen->>ES: ensureForQuestions(agendaId, missing)
Note over ES: внутри адаптера:<br/>StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())<br/>ContextSnapshot.captureAll() → subtasks
rect rgb(240, 255, 240)
Note over ES,ECM: Parallel gen (N вопросов = N subtasks в virtual threads)
par для каждого missing questionId
ES->>Core: generateSimpleExtract(question, agenda, votes)
Core->>Core: выбор strategy + template (per CommitteeType)
Core->>ECM: LOAD source-file (если нужен)
ECM-->>Core: source docx bytes
Note over Core: office-stamper: stamp template<br/>+ insert source file
Core->>ECM: SAVE generated SIMPLE file
ECM-->>Core: externalStorageKey
Core-->>ES: new Extract (type=SIMPLE, fileId=...)
end
Note over ES: scope.join() — если хоть одна fork'а failed<br/>→ отмена остальных + throw
end
%% ── Step 3: batch save + duplicate handling ──
ES->>XDB: saveAll(newExtracts)
alt успех
XDB-->>ES: saved extracts
else DuplicateExtractException<br/>(race: другая параллельная генерация успела)
Note over ES: catch DuplicateException<br/>→ cache-hit recovery
ES->>XDB: findActiveByQuestionIdsAndTypes(...)
XDB-->>ES: existing extracts<br/>(те что уже были созданы параллельным потоком)
end
ES-->>Gen: List<Extract> newly-saved
end
%% ── Step 4: PRKK placeholders (in-memory только) ──
Note over Gen: для каждого prkkQuestion:<br/>validate prkkExtractFileId != null<br/>(иначе ContractMismatch)<br/>создать Extract.type=SHORT,<br/>externalStorageKey=prkkExtractFileId<br/>(НЕ сохраняем в DB — это указатель,<br/>реальный strip → в Phase 3)
Note over Gen: simpleExtracts = existing + newly + prkkPlaceholders<br/>→ AfterPhase2
Перед параллельной генерацией — findByAgenda возвращает уже существующие SIMPLE-выписки (они могли быть созданы ранее — генерация протокола идемпотентна). Генерируем только недостающие.
Каждый недостающий вопрос — отдельный virtual thread, Joiner.allSuccessfulOrThrow():
- Ждёт все subtasks.
- Если хоть одна бросила → отменяет остальные, пробрасывает exception (ADR-0009 fail-fast).
- Context (Security + MDC) автоматически propagated через
ContextPropagatingTaskDecorator (ADR-0012).
¶ Duplicate race handling
Если два потока одновременно запустили генерацию для одной и той же повестки — оба могут попытаться вставить SIMPLE-выписки для одних вопросов. Handling через DuplicateExtractException:
saveAll бросает при UNIQUE-constraint violation.
- Catch →
findActive(questionIds, types) подтягивает уже созданные параллельным потоком.
- Результат: idempotent, оба потока получают валидные extracts.
Это не нарушает ADR-0009 "всё или ничего" — это про idempotency / consistency, а не про partial success.
PRKK-вопросы получают Extract-entity в Phase 2, но не сохраняются в DB. Это data-carrying object с указателем на prkkExtractFileId — реальный файл в ECM. В Phase 3 они пойдут на PrkkExtractStripper, не на обычный SHORT-pipeline.
Валидация: если prkkExtractFileId == null у PRKK-вопроса → DomainException.ContractMismatch. Это значит agenda отдала некорректные данные для вопроса классифицированного как PRKK.