⟨/⟩ 40_Arch/ADR/ADR_014_PacketProcessingQueue.cs

178 строк · в начало

using System;
using Ban.Sdaid.Notation.Documents;

namespace Ban.Sdaid.Icd.Arch.Adr
{
    /// <summary>
    /// Решение: обработка пакета запускается автоматически фоновой очередью при постановке в очередь;
    /// к четырём состояниям добавляются два машинных — «обрабатывается» и «готов к проверке».
    /// Расширяет модель состояний ADR-008.
    /// </summary>
    public class ADR_014_PacketProcessingQueue : IAdrDocument
    {
        public static string _refName = "ADR-014 «Фоновая очередь обработки пакетов и шесть состояний»";

        public string Name => "ADR-014. Фоновая очередь обработки пакетов и шесть состояний";

        public string Description =>
            @"Обработку пакета (нормализация orthocover + опознание вида) запускает не оператор кнопкой, а фоновый воркер — автоматически при постановке пакета в очередь. Очередь — процессная, in-memory (канал идентификаторов + hosted-воркер с ограничением параллелизма); персистентность обеспечивает состояние пакета в БД, а не сам канал. К четырём состояниям ADR-008 добавляются два машинных: «обрабатывается» и «готов к проверке» — они снимают двойную роль «в очереди». Ручной повтор и восстановление недоделанных пакетов при рестарте сохраняются.";

        public string Version => "0.1";
        public string Status => "proposed";

        public string[] Comments => new[]
        {
            "2026-08-20. Заведён по итогам разбора родственного сервиса biblioservice-dispatcher: там пакет после отправки автоматически уходит в фоновую очередь (asyncio-семафор + фоновые задачи), а недоделанные переочередиваются при старте. Переносим подход на .NET-конвейер ICD.",
        };

        // ADR-008 не отменяется, а расширяется: его тезис «нет пяти подтверждаемых этапов» сохраняется —
        // два новых состояния машинные, подтверждать в них нечего.
        public Type? Supersedes => null;

        public static string S1_Context = $"""
            ## Контекст

            {nameof(ADR_008_PacketStatesAndTwoTabs)} зафиксировал у пакета четыре хранимых состояния и
            упразднил пять подтверждаемых этапов. Но машинную половину жизненного цикла он оставил
            неявной, и по мере реализации это стало мешать.

            **Обработка запускается вручную.** По {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_005_ImageNormalizationOrthocover)}
            нормализация кадров (и, вместе с ней, опознание вида — {nameof(ADR_015_FormPageDetection)})
            выполняется синхронно по кнопке «Обработать заново». Оператор обязан помнить, что после сдачи
            пакета его надо ещё и запустить; до нажатия пакет лежит нетронутым.

            **Состояние «в очереди» перегружено.** Одним значением обозначены и «ждёт, пока его возьмут
            в обработку», и «обработан, ждёт, пока его возьмёт оператор». Отличить их можно только по
            наличию нормализованных кадров — скрытый признак, из-за которого индикатор очереди
            ({nameof(Ban.Sdaid.Icd.Requirements.FR_009_PacketQueue)}, «чья очередь ходить») неоднозначен.

            **После рестарта ничего не возобновляется.** Синхронная обработка живёт в рамках запроса;
            прерванный пакет остаётся полуобработанным без механизма подхвата.

            Родственный сервис оцифровки **biblioservice-dispatcher** решает это иначе: пакет после отправки
            автоматически ставится в фоновую очередь (in-memory семафор + фоновые задачи), воркер его
            обрабатывает под ограничением параллелизма, а при старте приложения недоделанные пакеты
            (в статусах «в очереди» / «обрабатывается») переочередиваются. Переносим этот подход,
            адаптировав под .NET-конвейер ICD.
            """;

        public static string S2_Decision = $"""
            ## Решение

            **Обработка запускается автоматически.** При постановке пакета в очередь (отправка из ввода)
            он попадает в фоновую очередь и обрабатывается воркером без действия оператора. Оператор
            вступает в работу уже с готовым результатом. **Ручной повтор** («Обработать заново») сохраняется
            как явная команда — для пересъёмки, правки состава кадров и отклонения вида ИК; в этих случаях
            пакет возвращается в очередь по команде пользователя (как требует
            {nameof(Ban.Sdaid.Icd.Requirements.FR_009_PacketQueue)}), а не автоматически на каждое движение кадра.

            **Очередь — процессная, in-memory.** Канал идентификаторов пакетов + hosted-воркер, ограниченный
            параллелизмом (настройка, по умолчанию 1). Сам канал не персистентен — источник истины о том,
            что пакет ждёт обработки, это его **состояние в БД**; канал лишь будит воркер. Постановка в канал
            выполняется **после фиксации** пакета в БД; воркер обрабатывает пакет в собственной единице работы,
            а не в контексте запроса-отправителя.

            **Состояний становится шесть.** К прежним — **в очереди**, **проверка**, **завершён**, **ошибка** —
            добавляются два машинных:

            | Состояние | Когда наступает |
            |---|---|
            | Обрабатывается | воркер прогоняет нормализацию и опознание вида |
            | Готов к проверке | система закончила обработку, пакет ждёт оператора |

            Это снимает двойную роль «в очереди»: теперь «в очереди» = «ждёт воркера», «готов к проверке» =
            «ждёт оператора». Оба новых состояния **машинные** — пользователь их не подтверждает, переход
            происходит сам. Поэтому тезис {nameof(ADR_008_PacketStatesAndTwoTabs)} — «нет пяти подтверждаемых
            этапов» — сохраняется: мы делаем наблюдаемой системную половину жизненного цикла, а не возвращаем
            подтверждения.

            **Машина состояний:**

            ```
            в очереди ──► обрабатывается ──┬─(успех)─► готов к проверке ──► проверка ──► завершён
                 ▲                          └─(вид не опознан)─► ошибка          │
                 └──── ручной повтор / отклонение вида / правка состава ◄────────┘
            ```

            **Восстановление при старте.** Пакеты в состояниях «в очереди» и «обрабатывается»
            переочередиваются (ставятся в канал заново); «готов к проверке» не трогаются — они уже
            обработаны. Управляется настройкой (локально можно отключить, чтобы не перезапускать зависшие
            пакеты при отладке).
            """;

        public static string S3_Rationale = """
            ## Обоснование

            Автоматический запуск убирает лишний шаг и класс ошибок «забыл обработать»: сдача пакета и есть
            запрос на обработку. Ручной повтор остаётся не как основной путь, а как явное действие там, где
            исходные данные изменились и прежний разбор недействителен, — это ровно тот случай, для которого
            FR-009 требует постановки в очередь **командой пользователя**.

            Отдельные машинные состояния стоят одного дополнительного перечислимого значения на каждое, зато
            делают очередь честной: индикатор «чья очередь ходить» отвечает однозначно, а скрытый признак
            «есть ли нормализованные кадры» перестаёт нести смысл состояния. Цена — та же, что принял
            ADR-008: состояний по-прежнему наперечёт, переходы явные.

            Очередь в памяти процесса выбрана намеренно простой. biblioservice-dispatcher показывает, что
            для одного инстанса связки «канал + воркер + семафор + восстановление при старте» достаточно;
            вводить брокер или таблицу-очередь означало бы платить за мультиинстанс, которого пока нет.
            """;

        public static string S4_Consequences = $"""
            ## Следствия

            **Перечисление состояний расширяется до шести** ({nameof(Ban.Sdaid.Icd.Domain.Capture.PacketState)}):
            добавляются «обрабатывается» и «готов к проверке». Обновляются доменная сущность
            {nameof(Ban.Sdaid.Icd.Domain.Capture.InputPacket)} (таблица состояний, инварианты переходов) и
            требование {nameof(Ban.Sdaid.Icd.Requirements.FR_009_PacketQueue)} (индикатор и отбор — по шести
            состояниям).

            **Запуск нормализации по {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_005_ImageNormalizationOrthocover)}
            уточняется**: основной запуск — автоматический (воркер при постановке в очередь), ручной остаётся
            повтором. Абстракция `IImageNormalizer` не меняется.

            **Появляется инфраструктурный артефакт очереди** — канал + hosted-воркер + ограничение параллелизма
            и хук восстановления при старте (backend-спека / инфраструктура). Хендлер отправки пакета после
            фиксации ставит идентификатор в канал; воркер работает в собственной единице работы.

            **UI очереди** ({nameof(Ban.Sdaid.Icd.Requirements.FR_009_PacketQueue)} / страница очереди) показывает
            новые состояния и не требует от оператора ручного запуска обработки для штатного пути.

            **Компромисс — один процесс.** Очередь в памяти не переживает мультиинстанс: у каждого инстанса
            своя. Как и локальный кэш контекста в {nameof(ADR_011_AbacAuthorization)}, вынос в общий брокер/таблицу
            оставлен точкой расширения на случай горизонтального масштабирования.
            """;

        public static string S5_Alternatives = """
            ## Рассмотренные альтернативы

            **Оставить запуск ручным (ничего не менять).** Отвергнуто: сохраняется шаг «не забудь обработать»
            и перегруженное «в очереди»; наблюдаемости и восстановления нет.

            **Не вводить машинные состояния, отличать по наличию нормализованных кадров.** Одно «в очереди»
            на оба смысла. Отвергнуто: состояние очереди становится скрытым и выводимым, а индикатор «чья
            очередь ходить» — неоднозначным; оператор не видит, обрабатывается пакет или уже готов.

            **Брокер (Redis/RabbitMQ) или таблица-очередь в БД.** Отвергнуто на текущем этапе: один инстанс,
            in-memory + восстановление при старте достаточно (подтверждено biblioservice-dispatcher). Оставлено
            точкой расширения при мультиинстансе — аддитивно, без слома модели состояний.

            **Автозапуск без ручного повтора.** Отвергнуто: пересъёмка, правка состава и отклонение вида
            требуют явной команды пользователя (FR-009, «Повторная обработка») — иначе каждое движение кадра
            запускало бы обработку заново.
            """;

        public static string S99_Related = $"""
            ## Связанные артефакты

            - {nameof(ADR_008_PacketStatesAndTwoTabs)} — четыре состояния пакета; настоящий ADR расширяет модель до шести, добавляя машинные состояния.
            - {nameof(ADR_007_InputWithoutPlan)} — ввод без плана: вид ИК определяет обработка, для которой и заводится очередь.
            - {nameof(ADR_015_FormPageDetection)} — опознание формы и страницы кадра (шаг «опознание вида» конвейера); низкая уверенность уводит пакет в «ошибка».
            - {nameof(ADR_009_RecognitionModelIntegration)} — извлечение полей уже опознанной формы (следующий шаг конвейера).
            - {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_005_ImageNormalizationOrthocover)} — нормализация orthocover; запуск теперь автоматический (воркер), ручной остаётся повтором.
            - {nameof(Ban.Sdaid.Icd.Requirements.FR_009_PacketQueue)} — очередь: индикатор состояний расширяется до шести.
            - {nameof(Ban.Sdaid.Icd.Domain.Capture.InputPacket)} / {nameof(Ban.Sdaid.Icd.Domain.Capture.PacketState)} — целевая сущность и перечисление состояний.
            """;
    }
}