Files
healthlog/openspec/changes/archive/2026-08-02-cena-chitayushchego-marshruta/specs/catalog/spec.md
T
av 8db2ec7ff4 Цена читающего маршрута: чекпойнт WAL по таймеру и условный запрос
- рядом с воркером свёртки живёт горутина, раз в минуту разбирающая журнал
  пассивным чекпойнтом; «журнал не разбирается» видно строкой владельцу, а не
  только по `df`. Признак — пара чисел, а не флаг занятости: тот молчит под
  удерживаемым читателем (`busy=0` при 6256 страницах и пяти перенесённых), а
  при занятой блокировке отдаёт `-1` вместо ответа, и `-1 >= -1` читалось бы как
  «разобрано целиком»
- каталог отвечает `304` на `If-None-Match`, не открывая снимок витрины. Метка
  собрана из всего, от чего зависит ответ: версии витрины (`data_version` с
  закреплённого соединения плюс поколение — значение локально для соединения и
  не переживает переоткрытия), горизонта измерения и области действия ресурса.
  Версия снимается до и после сборки: снятая после пометила бы устаревший снимок
  свежим номером
- предел и дедлайн ответа отложены в задачу Read API точек вместе с измеренной
  ценой первого запроса; попутно починен флаки-тест чужой задачи, искавший
  значение точки в сыром буфере записи лога
2026-08-02 20:42:22 +03:00

9.9 KiB
Raw Blame History

ADDED Requirements

Requirement: Каталог отвечает на условный запрос

Система SHALL выставлять на ответе каталога заголовок ETag и SHALL отвечать 304 Not Modified на запрос с If-None-Match, чья метка совпадает с текущей версией витрины. При совпадении снимок витрины открываться MUST NOT: смысл условного запроса в том, что самый частый запрос потребителя — повтор неизменившегося — не стоит ничего.

Метка MUST строиться из всего, от чего зависит ответ: версии витрины и горизонта измерения. Горизонт едет вместе с часами, и метка из будущего, лежащая в витрине, въезжает в окно сама — без единого коммита. Путь построен и прогнан: та же версия витрины, cumulative против unknown. Значит версии витрины для метки НЕ ДОСТАТОЧНО, и слабая форма метки этого не лечит: смена измеренного рода — изменение семантическое, на нём Read API строит арифметику года.

Горизонт входит в метку огрублённым до часа, и огрубление точное, а не приблизительное: метки объектов лежат ровно на часах, поэтому отбор по горизонту меняется ровно при переходе через час. Цена названа: один полный ответ в час на потребителя при неизменившейся витрине.

Форма метки MUST оставаться слабой (W/"…"): она выведена из состояния, а не из байтов ответа. На исход 304 это не влияет — If-None-Match сравнивается слабо в любом случае.

Метка MUST выставляться, только если за всё время сборки ответа в базу никто не коммитил; правило снятия версии принадлежит хранилищу и здесь не повторяется. Ответ без метки — законный исход, а не отказ: клиент просто не сможет спросить условно в следующий раз.

Метка действительна только в пределах одного ресурса, и область действия MUST входить в саму метку. Маршрут, чей ответ есть функция параметров запроса (Read API точек), и транспорт, у которого адреса нет вовсе (MCP), обязаны подмешивать в неё канонизированную форму запроса — иначе «не изменилось» ответит на другой набор данных. Требовать этого прозой недостаточно: правило MUST быть выражено формой вызова, потому что забыть его — единственный путь всей задачи, ведущий к выдаче не тех данных.

Ответ 304 MUST нести ту же метку и MUST NOT нести тела и представленческих заголовков. Клиент, не приславший If-None-Match, MUST получать ровно то же, что и до появления условного запроса.

Ответы каталога MUST быть помечены непригодными для разделяемого кеша (Cache-Control: private, no-cache). До появления валидатора эвристическое кеширование посредником было маловероятным; с меткой ответ становится штатно кешируемым, а при выключенной проверке токенов (законная конфигурация) в запросе нет и Authorization — тогда выгрузку истории здоровья вправе сохранить любой прокси на пути.

Проверка токена чтения MUST предшествовать условному запросу: 304 без токена подтверждал бы состояние витрины тому, кому она не открыта.

Разбор условия MUST следовать HTTP и MUST NOT превращать кривой заголовок в отказ:

  • звёздочка (*) совпадает с любой существующей меткой; метки нет — условие не выполнено, и клиент со звёздочкой получает данные, а не вечный 304;
  • неразбираемое значение условия не выполняет и даёт 200, а не 400.

Следствие названо вслух: 304 не выполняет измерения и потому не пишет предупреждений владельцу. Предупреждения каталога (данные из будущего, противоречащий род агрегации) привязаны к сборке ответа; с условным опросом они становятся функцией смены версии витрины, а не числа запросов. Состояние при этом не исчезает: следующая доставка меняет версию, ответ собирается, и предупреждение пишется — а пока витрина стоит, повторять его на каждый опрос трёх потребителей значило бы обесценить уровень.

Scenario: Повтор на неизменившейся витрине

  • GIVEN клиент получил каталог и запомнил его ETag
  • WHEN он повторяет запрос с If-None-Match этой метки, а витрина не менялась
  • THEN ответ — 304 без тела, с той же меткой

Scenario: Витрина изменилась

  • GIVEN клиент получил каталог и запомнил его ETag
  • WHEN свёртка записала объект и клиент повторяет запрос с прежней меткой
  • THEN ответ — 200 с полным каталогом и новой меткой

Scenario: Горизонт сдвинулся

  • GIVEN витрина не менялась
  • WHEN горизонт измерения перешёл через час
  • THEN метка отличается от прежней

Scenario: Метка другого ресурса

  • GIVEN клиент присылает метку, выданную другим читающим маршрутом
  • WHEN совпадает версия витрины
  • THEN условие не выполнено, и ответ — 200

Scenario: Клиент не спрашивает условно

  • WHEN каталог запрашивается без If-None-Match
  • THEN ответ — 200 с полным каталогом, меткой и правилом кеширования

Scenario: Две метки на неизменившейся витрине совпадают

  • GIVEN витрина не менялась между двумя запросами
  • WHEN каталог запрошен дважды
  • THEN метки совпадают, и тела ответов совпадают побайтово

Scenario: Звёздочка в условии

  • WHEN каталог запрашивается с If-None-Match: *
  • THEN ответ — 304 с текущей меткой

Scenario: Условие нечитаемо

  • WHEN каталог запрашивается с If-None-Match, который меткой не является
  • THEN ответ — 200 с полным каталогом, а не 400 и не 304

Scenario: Условный запрос без токена чтения

  • GIVEN список токенов чтения непуст
  • WHEN каталог запрашивается с If-None-Match, но без токена
  • THEN ответ — 401, а не 304

Scenario: Витрина изменилась во время сборки ответа

  • GIVEN между снятием версии до и после сборки в базу был коммит
  • WHEN ответ сформирован
  • THEN он уходит с полным телом и без заголовка ETag

Scenario: Версия витрины недоступна

  • GIVEN версию витрины прочитать не удалось
  • WHEN каталог запрашивается, в том числе с If-None-Match
  • THEN ответ — 200 с полным каталогом и без метки, а не 500 и не 304

Scenario: Условный ответ не собирает каталог

  • GIVEN в витрине лежат данные, помеченные будущим
  • WHEN каталог отвечает 304 по совпавшей метке
  • THEN предупреждение владельцу не пишется, потому что измерения не было