Перейти к основному содержимому

ADR 0199 — Минимальный ресурс контейнера выводится из module_map: каждый модуль объявляет свой, а Infra поднимает с суммой

Статус​

Accepted. 2026-10-01 (AT-261, история HS-065, эпик EP-030, раунд 36; решение владельца от 01.10: "o recurso mínimo é derivado do module_map — o que o Arquiteto declara por módulo —, não um preset fixo por stack"). Меняет схему артефакта artifact.module_map (Фаза 3b) и то, что выполнение container_start делает с опущенным resources, ссылаясь, не редактируя их, на ADR 0065 (ресурсы с пределом в артефакте образа), ADR 0130 (pidsLimit в spec broker), ADR 0131 (маршрутизация Arquiteto), ADR 0133 (Infra выбирает) и ADR 0190 (подъём на сервере при принятии, который использовал "recursos padrão").

Контекст​

Измерено в dev 2026-10-01, до изменения:

  • RECURSOS_PADRAO = {cpus 2, memoryMb 4096, pidsLimit 512} и предел RECURSOS_MAXIMOS = {8, 16384, 4096} в apps/api/src/domain/containers/project-container.ts. Опущенное поле resources становилось значением по умолчанию в validarDecisaoDeImagem; выше предела — отказ. Понятия МИНИМУМА не было.
  • Подъём на сервере при принятии (subir_no_aceite/2, ADR 0190) всегда отправляет resources: %{}, так что каждый поднятый им контейнер получал значение по умолчанию. Реальный контейнер использования 29.09 имел Mem=4 GiB, NanoCpus=2e9, Pids=512 — значение по умолчанию.
  • У module_map было {name, stack, responsibility, dependsOn} на модуль (ModuleNode в domain/architecture/module-graph.ts, JSONB-колонка module_maps.modules, инструмент create_module_map в engine) и ни одного поля ресурса. У маршрутизации (artifact.module_routing) и образа тоже нет. "Совместимый минимум" нельзя было вывести из существующих данных.
  • У проекта ОДИН контейнер: project_id UNIQUE в project_containers.

Решение​

  1. module_map получает resources на модуль. Необязательно в артефакте (у старых карт его нет; никакой миграции — это поле JSON), с ТРЕМЯ числами spec broker: cpus, memoryMb, pidsLimit. Означает то, что нужно ЭТОМУ модулю самому по себе внутри контейнера. Инструмент create_module_map начинает его запрашивать (required в схеме элемента, с тремя обязательными полями внутри). api валидирует при создании карты: все три или ни одного (объявление наполовину — 400; дополнение недостающего поля значением по умолчанию было бы выдумыванием числа), каждое положительное и в пределе контейнера.
  2. Минимум — это СУММА по модулям, а не максимум. Все модули живут в одном контейнере одновременно: dev agent каждого работает там, параллельно, когда область параллелизует, и dev-сервер одного не выключается ради работы другого. Максимум был бы правдой, только если бы никогда не стояли два одновременно, и его ошибка — OOM-kill посреди команды, а не понятный отказ. Сумма, превышающая предел, отклоняется УЖЕ при создании карты (400 для Arquiteto, который может исправить), и снова, в качестве защиты, при подъёме.
  3. Модуль без объявления учитывается с сегодняшним значением по умолчанию как ПОРОГОМ. Число для него никогда не выдумывается: единственное существующее — то, что он получает сегодня, значение по умолчанию контейнера. Поэтому при наличии модуля без объявления минимум — это НАИБОЛЬШЕЕ из суммы объявленных и RECURSOS_PADRAO, поле за полем; только когда все объявлены, минимум оказывается ниже значения по умолчанию. Карта совсем без объявлений — каждая карта до этого ADR — даёт ровно значение по умолчанию, и её поведение не меняется. Порог на КОНТЕЙНЕР, а не "значение по умолчанию на модуль в сумме", — намеренно: суммирование 2 cpus на каждый необъявленный модуль заставило бы старую карту из пяти модулей превысить предел и перестать подниматься.
  4. Выводит api, при выполнении container_start. ExecuteContainerStartUseCase читает действующий module_map (тот же, что выдвинула маршрутизация), выводит минимум и разрешает ресурсы поле за полем: ОПУЩЕННОЕ становится минимумом; НИЖЕ минимума — названный failed (без записи решения и без подъёма); выше действует до предела, проверяемого как всегда validarDecisaoDeImagem. Без действующего module_map минимум — значение по умолчанию. Engine НЕ выводит: подъём на сервере продолжает отправлять пустой resources, а его rationale говорит, что ресурсы — выведенный минимум, — одно мерило, на TypeScript, никогда вторая копия суммы на Elixir.
  5. rationale артефакта говорит, откуда взялось каждое число. Решение, которое записывает DecidirImagemDoProjetoUseCase (artifact.project_image, decidedBy: 'infra-lead'), получает фразу "Recursos mínimos (RN-683): …", с суммированными модулями, НЕ объявившими (названными) и версией карты. Тот, кто проверяет контейнер, читает, почему у него эти числа.

Последствия​

  • Подъём на сервере (ADR 0190) начинает поднимать с НАИМЕНЬШИМ допустимым ресурсом: полностью объявленный проект может оказаться ниже значения по умолчанию в 2 cpus/4 GiB/512 pids, что и было расточительством, измеренным AT-261.
  • Arquiteto начинает объявлять ресурс на модуль, и модель может ошибиться с числом. Ошибка В БОЛЬШУЮ сторону стоит машинных ресурсов и проверяема в rationale; ошибка выше предела — 400 с причиной; ошибка в МЕНЬШУЮ сторону — оставшийся риск, и он того же калибра, что Arquiteto уже решал при выборе образа. Число ОБЪЯВЛЕНО, а не измерено — измерение реального потребления контейнера — другое направление, здесь не взятое.
  • Infra Lead запрашивает ВЫШЕ минимума, когда хочет (до предела), никогда ниже. Отказ ниже минимума происходит при ВЫПОЛНЕНИИ: предложение pending с ресурсом ниже минимума падает только при одобрении, с причиной в executionResult. Отказ уже при предложении потребовал бы читать карту в ProposeActionUseCase, и здесь это не сделано.
  • choose_project_image (Arquiteto, решающий образ с нуля, RN-105) НЕ меняется: там Arquiteto сам выбирает свои ресурсы, а просьба владельца была о выборе Infra. Подъём через runner (container_start_via_runner) тоже не меняется: он поднимает уже решённый образ с записанными в нём ресурсами.
  • Предел остаётся тем же, и никакой другой не сдвигается.
  • Сумма долей CPU округляется до трёх знаков, чтобы артефакт не записывал 0.30000000000000004.