заведён реестр префиксов, правила канона перенумерованы
- идентификатор правила теперь `<ПРЕФИКС>-<номер>` вместо `R<номер>`: префикс уникален по всему канону, поэтому ссылка больше не требует пути к файлу и не зависит от того, на какой оси файл лежит - префикс выбирается под файл, а не выводится по формуле, и хранится в conventions/prefixes.toml вместе с выбывшими; номера сохранены один в один вместе с дырами
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
---
|
||||
prefix: DIRS
|
||||
---
|
||||
|
||||
# Категории директорий приложения
|
||||
|
||||
Всё, что приложение пишет на диск, делится на три категории по принципу
|
||||
@@ -16,32 +20,32 @@
|
||||
|
||||
## Правила
|
||||
|
||||
### R1. Записываемые пути разложены по трём категориям
|
||||
### DIRS-1. Записываемые пути разложены по трём категориям
|
||||
|
||||
**ДОЛЖЕН.** Каждая директория, в которую пишет приложение или деплой,
|
||||
относится к одной из трёх категорий:
|
||||
|
||||
| № | Категория | Директория | Создаёт | Потеря содержимого | В бэкапе |
|
||||
|---|---|---|---|---|---|
|
||||
| R1.1 | конфигурация | `config/` | деплой | восстанавливается прогоном деплоя | нет |
|
||||
| R1.2 | данные | `data/` | приложение | невосполнима | да |
|
||||
| R1.3 | кеш | `cache/` | приложение | приложение перегенерирует | нет |
|
||||
| DIRS-1.1 | конфигурация | `config/` | деплой | восстанавливается прогоном деплоя | нет |
|
||||
| DIRS-1.2 | данные | `data/` | приложение | невосполнима | да |
|
||||
| DIRS-1.3 | кеш | `cache/` | приложение | приложение перегенерирует | нет |
|
||||
|
||||
Имена в таблице — умолчание для случая «одна директория на категорию».
|
||||
|
||||
**Почему.** Все дальнейшие решения — что попадает в бэкап (R4), что можно
|
||||
**Почему.** Все дальнейшие решения — что попадает в бэкап (DIRS-4), что можно
|
||||
снести при нехватке места, что переживает переезд на другой диск —
|
||||
читаются из категории, а не выясняются по коду приложения. Без единой
|
||||
классификации каждое такое решение принимается заново и каждый раз чуть
|
||||
по-другому, а цена ошибки несимметрична: лишний кеш в снапшоте стоит места,
|
||||
потерянные данные не стоят ничего, потому что их больше нет.
|
||||
|
||||
### R2. Категория может состоять из нескольких директорий
|
||||
### DIRS-2. Категория может состоять из нескольких директорий
|
||||
|
||||
**ДОПУСКАЕТСЯ.** Директорий в категории столько, сколько нужно раскладке;
|
||||
принадлежность к категории задаётся не именем, а участием в списке бэкапа.
|
||||
|
||||
**Почему.** Явное разрешение снимает вопрос, не читается ли R1 как «ровно
|
||||
**Почему.** Явное разрешение снимает вопрос, не читается ли DIRS-1 как «ровно
|
||||
три директории». Крупные файлы отделяют от базы, чтобы двигать их между
|
||||
дисками независимо (`media/`, `uploads/` — та же категория «данные», что и
|
||||
`data/`); запрет на такое деление вынуждал бы либо держать всё на одном
|
||||
@@ -49,15 +53,15 @@
|
||||
задавать именем ровно поэтому: имён в категории несколько, и выбираются они
|
||||
по содержимому.
|
||||
|
||||
### R3. Данные и кеш разделяются по тесту на пересоздание
|
||||
### DIRS-3. Данные и кеш разделяются по тесту на пересоздание
|
||||
|
||||
**ДОЛЖЕН.** Записываемый путь относят к данным или к кешу по содержимому:
|
||||
|
||||
| № | Что лежит | Категория |
|
||||
|---|---|---|
|
||||
| R3.1 | база, загруженные файлы, сгенерированные артефакты | данные |
|
||||
| R3.2 | миниатюры, распакованные ассеты, кеш внешних ответов, перестраиваемые индексы | кеш |
|
||||
| R3.3 | спорный случай | `rm -rf` и запуск приложения заново: поднялось и наверстало само — кеш; не поднялось или поднялось пустым — данные |
|
||||
| DIRS-3.1 | база, загруженные файлы, сгенерированные артефакты | данные |
|
||||
| DIRS-3.2 | миниатюры, распакованные ассеты, кеш внешних ответов, перестраиваемые индексы | кеш |
|
||||
| DIRS-3.3 | спорный случай | `rm -rf` и запуск приложения заново: поднялось и наверстало само — кеш; не поднялось или поднялось пустым — данные |
|
||||
|
||||
**Почему.** Без внешнего теста граница проводится по ощущению «жалко
|
||||
потерять», а оно смещено в одну сторону: дорогой в пересборке кеш
|
||||
@@ -67,7 +71,7 @@
|
||||
обнаруживается в тот же момент, но дёшево: при попытке пересоздать, а не
|
||||
при попытке восстановить.
|
||||
|
||||
### R4. В бэкап идут данные, и только они
|
||||
### DIRS-4. В бэкап идут данные, и только они
|
||||
|
||||
**ДОЛЖЕН.** Директории категории «данные» — в списке бэкапа; конфигурация и
|
||||
кеш — нет.
|
||||
@@ -79,9 +83,9 @@
|
||||
Ошибка в другую сторону дороже: директория данных, не попавшая в список,
|
||||
обнаруживается в единственный момент, когда исправить её уже нечем.
|
||||
|
||||
### R5. Список бэкапа ссылается на те же пути, что и создание директорий
|
||||
### DIRS-5. Список бэкапа ссылается на те же пути, что и создание директорий
|
||||
|
||||
**ДОЛЖЕН.** Список выводится из категорий по R4 и ссылается на те же
|
||||
**ДОЛЖЕН.** Список выводится из категорий по DIRS-4 и ссылается на те же
|
||||
**объявления путей**, по которым директории создаются, а не набирается
|
||||
независимо.
|
||||
|
||||
@@ -96,25 +100,25 @@
|
||||
на это не жалуются, — и расхождение между тем, что бэкапится, и тем, что
|
||||
нужно, проявляется при восстановлении.
|
||||
|
||||
### R6. Способ попадания данных в бэкап зависит от того, кто отвечает за консистентность
|
||||
### DIRS-6. Способ попадания данных в бэкап зависит от того, кто отвечает за консистентность
|
||||
|
||||
**ДОЛЖЕН.** Способ выбирается по тому, самодостаточны ли файлы на диске:
|
||||
|
||||
| № | Данные | В бэкап |
|
||||
|---|---|---|
|
||||
| R6.1 | файлы самодостаточны на любой момент времени | копированием |
|
||||
| R6.2 | консистентность обеспечивает только сама СУБД | дампом: бэкапится директория дампов, сырой каталог базы — нет |
|
||||
| DIRS-6.1 | файлы самодостаточны на любой момент времени | копированием |
|
||||
| DIRS-6.2 | консистентность обеспечивает только сама СУБД | дампом: бэкапится директория дампов, сырой каталог базы — нет |
|
||||
|
||||
**Почему.** Файловый снапшот работающей СУБД не гарантирует
|
||||
консистентности: скопированный каталог может не восстановиться, и узнают
|
||||
об этом при восстановлении. Директория дампов — тоже данные, просто
|
||||
производные, поэтому R4 покрывает её без оговорок. Сырой каталог базы из
|
||||
производные, поэтому DIRS-4 покрывает её без оговорок. Сырой каталог базы из
|
||||
списка при этом исключается: он удваивает объём снапшота и добавляет к
|
||||
надёжной копии заведомо ненадёжную.
|
||||
|
||||
### R7. Способ выбирается при заведении приложения
|
||||
### DIRS-7. Способ выбирается при заведении приложения
|
||||
|
||||
**ДОЛЖЕН.** Решение «копировать или дампить» (R6) принимается, когда
|
||||
**ДОЛЖЕН.** Решение «копировать или дампить» (DIRS-6) принимается, когда
|
||||
приложение заводят.
|
||||
|
||||
**Почему.** Неверный выбор ничем себя не проявляет, пока бэкап не
|
||||
@@ -123,7 +127,7 @@
|
||||
первой неудачной попытки восстановления, то есть тогда, когда данных уже
|
||||
нет.
|
||||
|
||||
### R8. Приложение разводит записываемые пути по категориям
|
||||
### DIRS-8. Приложение разводит записываемые пути по категориям
|
||||
|
||||
**ДОЛЖЕН.** Конфигурация приложения задаёт отдельные пути для данных и для
|
||||
кеша, а не один каталог на всё.
|
||||
@@ -135,12 +139,12 @@
|
||||
появляется молча. Приложение, которое не умеет разделять, тем самым
|
||||
дефектно; раскладка под этот дефект не подстраивается.
|
||||
|
||||
### R9. Приложение не пишет в директорию конфигурации
|
||||
### DIRS-9. Приложение не пишет в директорию конфигурации
|
||||
|
||||
**НЕ ДОЛЖЕН.** Записываемые пути приложения не указывают внутрь
|
||||
конфигурации.
|
||||
|
||||
**Почему.** Конфигурация восстанавливается прогоном деплоя (R1.1), поэтому
|
||||
**Почему.** Конфигурация восстанавливается прогоном деплоя (DIRS-1.1), поэтому
|
||||
всё, что приложение туда записало, следующий деплой затирает без
|
||||
предупреждения. Вдобавок директория конфигурации может быть подключена
|
||||
только на чтение — тогда запись отказывает в рантайме. Ни то ни другое не
|
||||
|
||||
+61
-56
@@ -1,3 +1,7 @@
|
||||
---
|
||||
prefix: CONF
|
||||
---
|
||||
|
||||
# Конфигурация приложения
|
||||
|
||||
Как устроена конфигурация: где лежит, как попадает в процесс, что с
|
||||
@@ -12,7 +16,7 @@
|
||||
|
||||
## Правила
|
||||
|
||||
### R1. Конфигурация — файл, а не окружение
|
||||
### CONF-1. Конфигурация — файл, а не окружение
|
||||
|
||||
**ДОЛЖЕН.** Приложение читает параметры из файла конфигурации; переменные
|
||||
окружения источником конфигурации не служат.
|
||||
@@ -36,17 +40,17 @@
|
||||
`PTRACE_MODE_READ`, то есть доступен ровно тому же кругу, что и файл под
|
||||
`0600`.
|
||||
|
||||
### R2. Формат конфигурации — текстовый, с секциями и комментариями
|
||||
### CONF-2. Формат конфигурации — текстовый, с секциями и комментариями
|
||||
|
||||
**СЛЕДУЕТ.** Конкретный формат (TOML, YAML) выбирается по стеку.
|
||||
|
||||
**Почему.** Комментарий у каждого поля (R9) — часть того, ради чего конфиг
|
||||
вообще читают; формат, в котором комментарий негде разместить, делает R9
|
||||
**Почему.** Комментарий у каждого поля (CONF-9) — часть того, ради чего конфиг
|
||||
вообще читают; формат, в котором комментарий негде разместить, делает CONF-9
|
||||
невыполнимым. Секции дают структуру, которую валидатор проверяет целиком, —
|
||||
плоский список пар такой возможности не даёт и возвращает нас к тем же
|
||||
свойствам, из-за которых отвергнуто окружение (R1).
|
||||
свойствам, из-за которых отвергнуто окружение (CONF-1).
|
||||
|
||||
### R3. Имя файла фиксировано, путь переопределяется опцией
|
||||
### CONF-3. Имя файла фиксировано, путь переопределяется опцией
|
||||
|
||||
**СЛЕДУЕТ.** Имя по умолчанию ищется в рабочей директории процесса, а путь
|
||||
задаётся опцией командной строки.
|
||||
@@ -55,22 +59,22 @@
|
||||
контейнере и на сервере, и способ запуска не приходится помнить отдельно
|
||||
для каждой среды. Опция нужна ровно для случаев, когда конфигов несколько
|
||||
(тесты, второй инстанс): без неё их разводят переменной окружения — тем
|
||||
самым каналом, который закрывает R1.
|
||||
самым каналом, который закрывает CONF-1.
|
||||
|
||||
### R20. Отсутствие файла конфигурации — ошибка старта
|
||||
### CONF-20. Отсутствие файла конфигурации — ошибка старта
|
||||
|
||||
**ДОЛЖЕН.** Если файла нет ни по пути из опции, ни по имени по умолчанию в
|
||||
рабочей директории (R3), приложение не стартует: сообщение называет
|
||||
рабочей директории (CONF-3), приложение не стартует: сообщение называет
|
||||
искомый путь, код возврата ненулевой.
|
||||
|
||||
**Почему.** Конфиг — артефакт деплоя (R12), и его отсутствие означает, что
|
||||
**Почему.** Конфиг — артефакт деплоя (CONF-12), и его отсутствие означает, что
|
||||
развёртывание не довело работу до конца, а не что приложение попросили
|
||||
работать на умолчаниях. Умолчания (R7) существуют, чтобы работал
|
||||
работать на умолчаниях. Умолчания (CONF-7) существуют, чтобы работал
|
||||
**неполный** файл, а не отсутствующий, — именно здесь читатель спотыкается
|
||||
чаще всего.
|
||||
|
||||
Старт без файла ничего не спасает: у приложения с обязательными полями или
|
||||
секретами всё равно упадёт валидация (R18), только вместо одного сообщения
|
||||
секретами всё равно упадёт валидация (CONF-18), только вместо одного сообщения
|
||||
«нет `config.toml`» получится каскад «поле пусто», за которым настоящая
|
||||
причина — деплой не отрендерил файл — не видна.
|
||||
|
||||
@@ -79,16 +83,16 @@
|
||||
|
||||
<!-- local:проверки -->
|
||||
<!-- /local -->
|
||||
### R4. В репозитории лежит образец, а не рабочий конфиг
|
||||
### CONF-4. В репозитории лежит образец, а не рабочий конфиг
|
||||
|
||||
**НЕ ДОЛЖЕН.** Реальный конфиг не коммитится; в репозитории — образец.
|
||||
|
||||
**Почему.** Рабочий конфиг содержит отрендеренные секреты (R12), а секрет,
|
||||
**Почему.** Рабочий конфиг содержит отрендеренные секреты (CONF-12), а секрет,
|
||||
попавший в историю, чинится ротацией, а не удалением файла. Кроме того,
|
||||
закоммиченный конфиг конкретной среды становится вторым источником истины:
|
||||
он расходится с тем, что реально развёрнуто, и расходится молча.
|
||||
|
||||
### R5. Конфиг разбирается один раз при старте
|
||||
### CONF-5. Конфиг разбирается один раз при старте
|
||||
|
||||
**ДОЛЖЕН.** Разбор — при старте, в одну типизированную структуру; чтения
|
||||
файла конфигурации в бизнес-коде нет.
|
||||
@@ -96,10 +100,10 @@
|
||||
**Почему.** Второе место чтения — это второй момент времени: две части кода
|
||||
начинают видеть разные значения одного параметра, и расхождение не
|
||||
воспроизводится, потому что зависит от того, когда файл потрогали.
|
||||
Типизированная структура вдобавок переносит ошибку формата в старт (R17),
|
||||
Типизированная структура вдобавок переносит ошибку формата в старт (CONF-17),
|
||||
где она видна сразу, а не в первый вызов ветки, которая это поле читает.
|
||||
|
||||
### R6. Конфиг неизменяем после старта
|
||||
### CONF-6. Конфиг неизменяем после старта
|
||||
|
||||
**ДОЛЖЕН.** Смена параметров — рестарт процесса.
|
||||
|
||||
@@ -111,7 +115,7 @@
|
||||
Горячая перезагрузка — отдельное решение с отдельным обоснованием, а не
|
||||
умолчание.
|
||||
|
||||
### R7. Умолчания живут в коде
|
||||
### CONF-7. Умолчания живут в коде
|
||||
|
||||
**ДОЛЖЕН.** Значение по умолчанию задаётся в коде, файл его перекрывает.
|
||||
|
||||
@@ -120,17 +124,17 @@
|
||||
из двух значений не выглядит ошибкой. Умолчание в коде — одно определённое
|
||||
поведение для неполного конфига и одно место, где это значение меняется.
|
||||
|
||||
### R8. Образец перечисляет все поля
|
||||
### CONF-8. Образец перечисляет все поля
|
||||
|
||||
**ДОЛЖЕН.** В образце присутствуют все секции и все поля, включая те, у
|
||||
которых есть умолчание (R7).
|
||||
которых есть умолчание (CONF-7).
|
||||
|
||||
**Почему.** Поле, живущее только в коде, для читателя конфига не
|
||||
существует: он не знает, что параметр вообще можно менять, и добивается
|
||||
нужного поведения обходным путём. Полнота образца — цена, которой R7
|
||||
нужного поведения обходным путём. Полнота образца — цена, которой CONF-7
|
||||
покупает себе видимость.
|
||||
|
||||
### R9. У каждого поля образца есть комментарий
|
||||
### CONF-9. У каждого поля образца есть комментарий
|
||||
|
||||
**ДОЛЖЕН.** Каждое поле сопровождается комментарием, из которого ясно:
|
||||
|
||||
@@ -145,35 +149,35 @@
|
||||
дают валидное значение и работающий процесс, а ошибка обнаруживается по
|
||||
последствиям — таймаут в тысячу раз не тот.
|
||||
|
||||
### R10. Обязательность полей определяется дискриминатором `type`
|
||||
### CONF-10. Обязательность полей определяется дискриминатором `type`
|
||||
|
||||
**ДОЛЖЕН.** Когда набор полей секции зависит от поля-дискриминатора (выбор
|
||||
бекенда или внешнего сервиса), валидация идёт по его значению:
|
||||
|
||||
| № | Значение `type` | Валидация |
|
||||
|---|---|---|
|
||||
| R10.1 | поддерживаемое | обязательны поля этого варианта; поля прочих вариантов не требуются |
|
||||
| R10.2 | неизвестное | ошибка на старте с перечислением поддерживаемых значений |
|
||||
| CONF-10.1 | поддерживаемое | обязательны поля этого варианта; поля прочих вариантов не требуются |
|
||||
| CONF-10.2 | неизвестное | ошибка на старте с перечислением поддерживаемых значений |
|
||||
|
||||
**Почему.** Фиксированный на секцию набор обязательных полей оставляет
|
||||
выбор из двух плохих: заполнять поля бекенда, который не используется, или
|
||||
не проверять обязательность вовсе — то есть выключить валидацию ровно там,
|
||||
где вариантов много и ошибиться легче всего. Перечисление поддерживаемых
|
||||
значений (R10.2) нужно потому, что опечатка в `type` иначе неотличима от
|
||||
значений (CONF-10.2) нужно потому, что опечатка в `type` иначе неотличима от
|
||||
неподдерживаемого варианта, и за списком приходится идти в код.
|
||||
|
||||
### R11. Образец показывает все варианты `type`
|
||||
### CONF-11. Образец показывает все варианты `type`
|
||||
|
||||
**СЛЕДУЕТ.** Основной вариант предзаполнен рабочими значениями,
|
||||
альтернативные — блоками-комментариями ниже, каждый со своим описанием
|
||||
полей.
|
||||
|
||||
**Почему.** Иначе набор вариантов виден только из кода валидации, и образец
|
||||
теряет свойство справочника (R8, R9) ровно на той секции, где выбор
|
||||
теряет свойство справочника (CONF-8, CONF-9) ровно на той секции, где выбор
|
||||
действительно есть. Закомментированный блок вдобавок переключается правкой
|
||||
на месте, а не сборкой секции с нуля по документации.
|
||||
|
||||
### R12. Секреты в конфиг приносит деплой
|
||||
### CONF-12. Секреты в конфиг приносит деплой
|
||||
|
||||
**ДОЛЖЕН.** Деплой рендерит значения секретов прямо в файл конфигурации;
|
||||
отдельного слоя секретов в приложении нет.
|
||||
@@ -185,27 +189,28 @@
|
||||
этом остаётся тривиальным: оно читает файл и про секреты не знает ничего
|
||||
особенного.
|
||||
|
||||
### R13. Рендеренный конфиг — `0600` и владелец-рантайм
|
||||
### CONF-13. Рендеренный конфиг — `0600` и владелец-рантайм
|
||||
|
||||
**ДОЛЖЕН.** Права `0600`, владелец — пользователь, от имени которого
|
||||
работает процесс.
|
||||
|
||||
**Почему.** После R1 и R12 файл конфигурации — единственная поверхность, на
|
||||
которой секреты лежат, и весь довод «файл вместо окружения» держится на его
|
||||
правах: конфиг, читаемый всеми на машине, раздаёт секреты шире, чем раздало
|
||||
бы окружение, — и тогда R1 меняет одну утечку на другую.
|
||||
**Почему.** После CONF-1 и CONF-12 файл конфигурации — единственная
|
||||
поверхность, на которой секреты лежат, и весь довод «файл вместо окружения»
|
||||
держится на его правах: конфиг, читаемый всеми на машине, раздаёт секреты
|
||||
шире, чем раздало бы окружение, — и тогда CONF-1 меняет одну утечку на
|
||||
другую.
|
||||
|
||||
### R14. В образце секретные поля — пустые строки
|
||||
### CONF-14. В образце секретные поля — пустые строки
|
||||
|
||||
**ДОЛЖЕН.** Значение секретного поля в образце — пустая строка, а не
|
||||
пример.
|
||||
|
||||
**Почему.** Правдоподобная заглушка доезжает до продакшена как настоящее
|
||||
значение: шаблон отрендерился криво, поле осталось от образца, и проверка
|
||||
непустоты (R15) его пропускает. Пустая строка делает недорендеренный конфиг
|
||||
непустоты (CONF-15) его пропускает. Пустая строка делает недорендеренный конфиг
|
||||
механически отличимым от заполненного.
|
||||
|
||||
### R15. Загрузчик проверяет, что обязательные секреты не пусты
|
||||
### CONF-15. Загрузчик проверяет, что обязательные секреты не пусты
|
||||
|
||||
**ДОЛЖЕН.** Непустота обязательных секретов проверяется на старте.
|
||||
|
||||
@@ -213,12 +218,12 @@
|
||||
в 401 от внешнего API через час работы, — то есть в момент, когда причина
|
||||
ещё очевидна и связана с деплоем.
|
||||
|
||||
### R16. Секреты не попадают в логи
|
||||
### CONF-16. Секреты не попадают в логи
|
||||
|
||||
**НЕ ДОЛЖЕН.** Значение секретного поля не появляется в записи лога ни на
|
||||
одном уровне.
|
||||
|
||||
**Почему.** У логов круг доступа шире, чем у файла под `0600` (R13): они
|
||||
**Почему.** У логов круг доступа шире, чем у файла под `0600` (CONF-13): они
|
||||
собираются, пересылаются и попадают в бэкапы, где права исходного файла уже
|
||||
ничего не значат. Попавший в лог секрет чинится ротацией, а не удалением
|
||||
записи. Типичный источник утечки — отладочный дамп разобранного конфига при
|
||||
@@ -227,7 +232,7 @@
|
||||
<!-- local:секретные-поля -->
|
||||
<!-- /local -->
|
||||
|
||||
### R17. Конфиг валидируется на старте, до приёма трафика
|
||||
### CONF-17. Конфиг валидируется на старте, до приёма трафика
|
||||
|
||||
**ДОЛЖЕН.** Невалидный конфиг — запись уровня `ERROR` и выход с ненулевым
|
||||
кодом; процесс не стартует «наполовину».
|
||||
@@ -238,24 +243,24 @@
|
||||
чтобы неудачный старт увидел супервизор: без него он неотличим от штатного
|
||||
завершения, и приложение считается развёрнутым.
|
||||
|
||||
### R18. Минимальный набор проверок
|
||||
### CONF-18. Минимальный набор проверок
|
||||
|
||||
**ДОЛЖЕН.** Валидация покрывает как минимум:
|
||||
|
||||
| № | Что проверяется | Когда всплывёт без проверки |
|
||||
|---|---|---|
|
||||
| R18.1 | обязательные поля заданы (непустота секретов — R15) | в ветке, которая это поле читает |
|
||||
| R18.2 | пути существуют и доступны на запись/чтение по назначению | при первой записи, уже после отчёта об успешном старте |
|
||||
| R18.3 | числовые диапазоны и единицы: доли, таймауты, счётчики попыток | искажённым поведением без единого сообщения об ошибке |
|
||||
| R18.4 | строки, которые парсятся во что-то (длительности, зоны, URL, идентификаторы сущностей), реально парсятся | при первом обращении к тому, что за ними стоит |
|
||||
| R18.5 | включённые секции консистентны: у включённой интеграции заданы все обязательные поля | при первом вызове интеграции, часто по расписанию |
|
||||
| CONF-18.1 | обязательные поля заданы (непустота секретов — CONF-15) | в ветке, которая это поле читает |
|
||||
| CONF-18.2 | пути существуют и доступны на запись/чтение по назначению | при первой записи, уже после отчёта об успешном старте |
|
||||
| CONF-18.3 | числовые диапазоны и единицы: доли, таймауты, счётчики попыток | искажённым поведением без единого сообщения об ошибке |
|
||||
| CONF-18.4 | строки, которые парсятся во что-то (длительности, зоны, URL, идентификаторы сущностей), реально парсятся | при первом обращении к тому, что за ними стоит |
|
||||
| CONF-18.5 | включённые секции консистентны: у включённой интеграции заданы все обязательные поля | при первом вызове интеграции, часто по расписанию |
|
||||
|
||||
**Почему.** Список минимальный и собран по одному признаку — правый
|
||||
столбец: каждая из этих ошибок иначе всплывает там, где связь с деплоем уже
|
||||
потеряна, и диагностируется как дефект приложения. Проверка на старте
|
||||
сводит их все к одному моменту и одному сообщению.
|
||||
|
||||
### R19. Проблемы конфига показываются разом
|
||||
### CONF-19. Проблемы конфига показываются разом
|
||||
|
||||
**ДОЛЖЕН.** Валидация собирает все найденные проблемы и выводит их одним
|
||||
списком, а не падает на первой.
|
||||
@@ -266,25 +271,25 @@
|
||||
одного источника: разом они читаются как одна причина, по одной — как
|
||||
череда несвязанных мелочей.
|
||||
|
||||
### R21. Значение поля в сообщении валидатора — по признаку секретности
|
||||
### CONF-21. Значение поля в сообщении валидатора — по признаку секретности
|
||||
|
||||
**ДОЛЖЕН.** Состав сообщения определяется тем же признаком секретности
|
||||
поля, которым уже пользуются R15 и R16:
|
||||
поля, которым уже пользуются CONF-15 и CONF-16:
|
||||
|
||||
| № | Поле | В сообщении |
|
||||
|---|---|---|
|
||||
| R21.1 | несекретное | имя поля, ожидание и полученное значение: «ожидалось 0–1, получено `1.5`» |
|
||||
| R21.2 | секретное | имя поля и суть нарушения, без значения |
|
||||
| CONF-21.1 | несекретное | имя поля, ожидание и полученное значение: «ожидалось 0–1, получено `1.5`» |
|
||||
| CONF-21.2 | секретное | имя поля и суть нарушения, без значения |
|
||||
|
||||
**Почему.** Сообщение без значения отправляет читателя в файл — сличать
|
||||
глазами каждую строку списка R19; ошибки вида «секунды вместо миллисекунд»
|
||||
глазами каждую строку списка CONF-19; ошибки вида «секунды вместо миллисекунд»
|
||||
или пробел в конце значения из такого сообщения не читаются вовсе. Значение
|
||||
секретного поля при этом печатать некуда: вывод старта уходит в лог
|
||||
супервизора и вывод CI, где круг доступа шире прав файла `0600`, — тот же
|
||||
канал утечки, который закрывает R16. Отдельный список «что не печатать» не
|
||||
заводится: признак один на R15, R16 и R21, а второй список разошёлся бы с
|
||||
первым — и поле оказалось бы секретным для логов, но печатаемым
|
||||
валидатором.
|
||||
канал утечки, который закрывает CONF-16. Отдельный список «что не печатать»
|
||||
не заводится: признак один на CONF-15, CONF-16 и CONF-21, а второй список
|
||||
разошёлся бы с первым — и поле оказалось бы секретным для логов, но
|
||||
печатаемым валидатором.
|
||||
|
||||
## Связано
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
prefix: KEYS
|
||||
---
|
||||
|
||||
# Идентификаторы сущностей
|
||||
|
||||
Как выбираются и как выглядят первичные ключи сущностей. Форма записи —
|
||||
@@ -14,7 +18,7 @@
|
||||
|
||||
## Правила
|
||||
|
||||
### R1. Первичный ключ новой сущности — ULID
|
||||
### KEYS-1. Первичный ключ новой сущности — ULID
|
||||
|
||||
**ДОЛЖЕН.** Новая сущность получает сортируемый строковый идентификатор,
|
||||
который порождает приложение, — во **всех** таблицах, включая те, что
|
||||
@@ -31,12 +35,12 @@
|
||||
Второй довод дешевле, но важнее в быту: одна ментальная модель избавляет от
|
||||
спора при заведении каждой таблицы и делает идентификатор **глобальным** —
|
||||
уникальным across таблиц, а не только внутри своей. На этом держится
|
||||
корреляция по логам (R7).
|
||||
корреляция по логам (KEYS-7).
|
||||
|
||||
Правило про **сгенерированные суррогатные** ключи. Естественные и составные
|
||||
ключи у таблиц-деталей (R6) — третья категория, они допустимы всегда.
|
||||
ключи у таблиц-деталей (KEYS-6) — третья категория, они допустимы всегда.
|
||||
|
||||
### R2. Идентификатор генерирует приложение, а не база
|
||||
### KEYS-2. Идентификатор генерирует приложение, а не база
|
||||
|
||||
**ДОЛЖЕН.** Значение ключа известно до вставки строки.
|
||||
|
||||
@@ -46,26 +50,26 @@
|
||||
и достраивать связи вторым проходом, либо иметь два источника истины о
|
||||
моменте создания.
|
||||
|
||||
### R3. Генерация и разбор идентификаторов — в единственной точке
|
||||
### KEYS-3. Генерация и разбор идентификаторов — в единственной точке
|
||||
|
||||
**ДОЛЖЕН.** Один модуль порождает идентификаторы, он же их разбирает.
|
||||
Самодельных генераторов и парсеров в коде нет.
|
||||
|
||||
**Почему.** Нормализация регистра (R4) и проверка формата обязаны
|
||||
**Почему.** Нормализация регистра (KEYS-4) и проверка формата обязаны
|
||||
применяться ко всем идентификаторам без исключения. Любая вторая точка
|
||||
входа рано или поздно окажется той, где нормализацию забыли, — и дефект
|
||||
проявится не там, где создан.
|
||||
|
||||
### R4. Канонический вид — нижний регистр
|
||||
### KEYS-4. Канонический вид — нижний регистр
|
||||
|
||||
**ДОЛЖЕН.** Идентификаторы порождаются и хранятся в нижнем регистре.
|
||||
|
||||
**Почему.** Сравнение строк в базе обычно побайтовое, поэтому регистр — не
|
||||
косметика, а корректность поиска. Спецификация ULID канонизирует **верхний**
|
||||
регистр, и библиотеки по умолчанию отдают именно его: без единой точки (R3)
|
||||
регистр, и библиотеки по умолчанию отдают именно его: без единой точки (KEYS-3)
|
||||
разный регистр появится в базе сам собой.
|
||||
|
||||
### R5. Внешний идентификатор разбирается до обращения к базе
|
||||
### KEYS-5. Внешний идентификатор разбирается до обращения к базе
|
||||
|
||||
**ДОЛЖЕН.** Значение, пришедшее снаружи, проходит разбор и нормализацию
|
||||
раньше, чем по нему делается запрос. Реакция на неудачный разбор зависит от
|
||||
@@ -73,28 +77,28 @@
|
||||
|
||||
| № | Откуда пришёл | Разбор не удался → |
|
||||
|---|---|---|
|
||||
| R5.1 | путь или query URL | «не найдено» без обращения к хранилищу |
|
||||
| R5.2 | собственная форма, данные кнопки | «некорректный ввод» либо «элемент устарел» |
|
||||
| KEYS-5.1 | путь или query URL | «не найдено» без обращения к хранилищу |
|
||||
| KEYS-5.2 | собственная форма, данные кнопки | «некорректный ввод» либо «элемент устарел» |
|
||||
|
||||
**Почему.** Синтаксически невалидное значение не может соответствовать
|
||||
записи, поэтому поход в базу за ним — заведомо холостой; отсекая его на
|
||||
границе, мы дёшево снимаем целый класс мусорного трафика.
|
||||
|
||||
Разделение R5.1 и R5.2 нужно, потому что источники значат разное. Мусор в
|
||||
URL — это чужая или протухшая ссылка, и «не найдено» описывает ситуацию
|
||||
точно. Мусор из собственной формы — это баг интерфейса или устаревший
|
||||
экран; ответ «не найдено» здесь скрывает дефект и лишает диагностики
|
||||
единственный момент, когда он заметен.
|
||||
Разделение KEYS-5.1 и KEYS-5.2 нужно, потому что источники значат разное.
|
||||
Мусор в URL — это чужая или протухшая ссылка, и «не найдено» описывает
|
||||
ситуацию точно. Мусор из собственной формы — это баг интерфейса или
|
||||
устаревший экран; ответ «не найдено» здесь скрывает дефект и лишает
|
||||
диагностики единственный момент, когда он заметен.
|
||||
|
||||
Таблица перечисляет **внешние** источники — те, откуда значение приходит
|
||||
вместе с запросом, и правило говорит, что отдать в ответ. Идентификатор из
|
||||
конфигурации, из собственной базы или из фикстуры сюда не относится: он
|
||||
ничего не отдаёт наружу, а его невалидность означает, что сломано у нас.
|
||||
Формат идентификатора в конфигурации проверяется на старте
|
||||
(`arch/config.md` R18), невалидное значение в собственной базе — нарушенный
|
||||
инвариант единой точки (R3).
|
||||
(`CONF-18`), невалидное значение в собственной базе — нарушенный
|
||||
инвариант единой точки (KEYS-3).
|
||||
|
||||
### R6. У таблиц-деталей допустим естественный или составной ключ
|
||||
### KEYS-6. У таблиц-деталей допустим естественный или составной ключ
|
||||
|
||||
**ДОПУСКАЕТСЯ.** Когда ключ есть по природе данных, отдельный
|
||||
сгенерированный идентификатор не заводится.
|
||||
@@ -104,10 +108,10 @@ URL — это чужая или протухшая ссылка, и «не на
|
||||
лишний вопрос «по какому из них искать» на каждом запросе. Дополнительной
|
||||
информации он не несёт.
|
||||
|
||||
### R7. Прочие генерируемые идентификаторы — через ту же точку
|
||||
### KEYS-7. Прочие генерируемые идентификаторы — через ту же точку
|
||||
|
||||
**ДОЛЖЕН.** Идентификаторы, не являющиеся первичными ключами (батч,
|
||||
задание, корреляционный ключ), порождаются тем же модулем (R3) и в том же
|
||||
задание, корреляционный ключ), порождаются тем же модулем (KEYS-3) и в том же
|
||||
формате.
|
||||
|
||||
**Почему.** Единый формат делает работающим главный побочный эффект
|
||||
@@ -118,7 +122,7 @@ URL — это чужая или протухшая ссылка, и «не на
|
||||
|
||||
## Почему ULID, а не UUID
|
||||
|
||||
R1 требует **сортируемый** строковый идентификатор. UUIDv4 не
|
||||
KEYS-1 требует **сортируемый** строковый идентификатор. UUIDv4 не
|
||||
сортируется по времени вовсе. UUIDv7 (RFC 9562) сортируется — и против него
|
||||
остаются два довода: 36 символов против 26 и дефисы, из-за которых
|
||||
идентификатор не берётся ни двойным кликом, ни `grep`-ом как одно слово.
|
||||
|
||||
+31
-26
@@ -1,3 +1,7 @@
|
||||
---
|
||||
prefix: TIME
|
||||
---
|
||||
|
||||
# Время
|
||||
|
||||
Как приложение записывает моменты и длительности: в каком формате, откуда
|
||||
@@ -14,7 +18,7 @@
|
||||
|
||||
## Правила
|
||||
|
||||
### R1. Единый формат — RFC 3339, UTC, суффикс `Z`
|
||||
### TIME-1. Единый формат — RFC 3339, UTC, суффикс `Z`
|
||||
|
||||
**ДОЛЖЕН.** Момент времени записывается как `2026-06-28T11:23:45Z` —
|
||||
одинаково в хранении, логах, API и обмене с внешними системами.
|
||||
@@ -25,7 +29,7 @@
|
||||
совпадать с тем, которое подразумевалось при написании кода. UTC с явным `Z`
|
||||
убирает из данных и смещение, и сам вопрос «в какой зоне это записано».
|
||||
|
||||
### R2. Ширина строки фиксируется на каждый носитель
|
||||
### TIME-2. Ширина строки фиксируется на каждый носитель
|
||||
|
||||
**ДОЛЖЕН.** Внутри одной колонки БД и внутри одного потока логов длина
|
||||
строки времени одна и от записи к записи не плавает.
|
||||
@@ -38,18 +42,18 @@
|
||||
везде, а только на тех парах записей, где дробная часть оказалась короче, —
|
||||
то есть редко, выборочно и невоспроизводимо.
|
||||
|
||||
### R3. Точность разных носителей может различаться
|
||||
### TIME-3. Точность разных носителей может различаться
|
||||
|
||||
**ДОПУСКАЕТСЯ.** У колонки БД и у потока логов каждая своя точность.
|
||||
|
||||
**Почему.** Квантор в R2 — на носитель, а не на приложение, потому что
|
||||
**Почему.** Квантор в TIME-2 — на носитель, а не на приложение, потому что
|
||||
строки разных носителей между собой не сравниваются: сортировка идёт внутри
|
||||
колонки, чтение — внутри потока. Явное разрешение нужно, чтобы R2 не читался
|
||||
колонки, чтение — внутри потока. Явное разрешение нужно, чтобы TIME-2 не читался
|
||||
как «одна точность на всё приложение»: от подгонки формата логов под формат
|
||||
колонки ни одна пара строк не становится сравнимой, зато точность режется до
|
||||
худшего из носителей.
|
||||
|
||||
### R4. Локальное время не хранится и не передаётся
|
||||
### TIME-4. Локальное время не хранится и не передаётся
|
||||
|
||||
**НЕ ДОЛЖЕН.** Ни в базе, ни в логах, ни в JSON API нет меток в локальной
|
||||
зоне.
|
||||
@@ -60,44 +64,45 @@
|
||||
разберёт час перехода на зимнее время: этот час идёт дважды, две записи
|
||||
получают одинаковую метку, и порядок между ними не восстанавливается ничем.
|
||||
|
||||
### R13. Чужой вход нормализуется при разборе, а не отклоняется
|
||||
### TIME-13. Чужой вход нормализуется при разборе, а не отклоняется
|
||||
|
||||
**ДОЛЖЕН.** Валидное по RFC 3339 значение с офсетом, отличным от `Z`, или с
|
||||
долями секунды принимается от внешней системы и приводится к каноническому
|
||||
виду (R1) в точке разбора (R5).
|
||||
виду (TIME-1) в точке разбора (TIME-5).
|
||||
|
||||
**Почему.** Канонический вид — обязательство нашего писателя, а не
|
||||
контракт, наложенный на внешние системы: `…14:23:45+03:00` называет тот же
|
||||
момент, что `…11:23:45Z`, и отклонять его — значит ломать интеграцию со
|
||||
стороной, которая стандарт соблюла. Пропущенное же как есть, такое значение
|
||||
нарушает форму и ширину носителя (R1, R2) и портит сортировку выборочно —
|
||||
только на записях, пришедших извне, и далеко от места разбора. Нормализация
|
||||
нарушает форму и ширину носителя (TIME-1, TIME-2) и портит сортировку
|
||||
выборочно — только на записях, пришедших извне, и далеко от места разбора.
|
||||
Нормализация
|
||||
в единой точке разбора оставляет ровно одно место, где неканонический вид
|
||||
существует, — по ту сторону границы его уже нет.
|
||||
|
||||
### R5. Единая точка получения «сейчас», форматирования и разбора
|
||||
### TIME-5. Единая точка получения «сейчас», форматирования и разбора
|
||||
|
||||
**ДОЛЖЕН.** Один модуль отдаёт текущий момент, он же форматирует и разбирает
|
||||
метки; прямые вызовы часов по коду не разбросаны.
|
||||
|
||||
**Почему.** Формат, зона (R1) и ширина (R2) обязаны выполняться для всех
|
||||
**Почему.** Формат, зона (TIME-1) и ширина (TIME-2) обязаны выполняться для всех
|
||||
меток без исключения, а каждый прямой вызов часов заводит ещё одно место,
|
||||
где их можно не соблюсти. Промах такого вызова проявляется не в коде, а в
|
||||
данных, и обнаруживается, когда испорченных записей уже накопилось.
|
||||
Соображение то же, что для идентификаторов (`arch/db-identifiers.md R3`).
|
||||
Соображение то же, что для идентификаторов (`arch/db-identifiers.md TIME-3`).
|
||||
|
||||
### R6. Дефолтов времени в схеме БД нет
|
||||
### TIME-6. Дефолтов времени в схеме БД нет
|
||||
|
||||
**НЕ ДОЛЖЕН.** Колонки времени не имеют `DEFAULT` с текущим моментом.
|
||||
|
||||
**Почему.** Дефолт превращает забытую вставку `created_at` в тихо работающий
|
||||
код: значение появляется, но приходит от сервера БД — то есть с других часов
|
||||
и в формате, который выбирала не единая точка (R5). Без дефолта та же ошибка
|
||||
и в формате, который выбирала не единая точка (TIME-5). Без дефолта та же ошибка
|
||||
падает громко и чинится в момент написания, а не при разборе расхождения
|
||||
между временем в записи и временем в логе. Правило то же, что для
|
||||
идентификаторов (`arch/db-identifiers.md R2`).
|
||||
идентификаторов (`arch/db-identifiers.md TIME-2`).
|
||||
|
||||
### R7. Длительность — отдельная величина, а не пара меток
|
||||
### TIME-7. Длительность — отдельная величина, а не пара меток
|
||||
|
||||
**ДОЛЖЕН.** Длительность операции записывается числом (обычно
|
||||
миллисекундами) в поле вида `duration_ms`.
|
||||
@@ -107,9 +112,9 @@
|
||||
образуют операцию, и вычитать их самому — в запросе, в дашборде и глазами в
|
||||
логе; число сравнивается, агрегируется и попадает в перцентили без этого
|
||||
шага. Кроме того, разность сохранённых меток считается по стенным часам и
|
||||
наследует их дефект (R9).
|
||||
наследует их дефект (TIME-9).
|
||||
|
||||
### R8. Длительность засекает слой, который делает вызов
|
||||
### TIME-8. Длительность засекает слой, который делает вызов
|
||||
|
||||
**СЛЕДУЕТ.** Замер живёт там же, где вызов, границы которого он измеряет.
|
||||
|
||||
@@ -118,14 +123,14 @@
|
||||
вызова. В обоих случаях число остаётся правдоподобным и потому не
|
||||
оспаривается, хотя отвечает не на тот вопрос, который к нему задают.
|
||||
|
||||
### R9. Момент и интервал берутся с разных часов
|
||||
### TIME-9. Момент и интервал берутся с разных часов
|
||||
|
||||
**ДОЛЖЕН.** Источник зависит от того, что записывается:
|
||||
|
||||
| № | Величина | Источник |
|
||||
|---|---|---|
|
||||
| R9.1 | момент события | стенные часы через единую точку (R5) |
|
||||
| R9.2 | длительность операции | монотонные часы процесса |
|
||||
| TIME-9.1 | момент события | стенные часы через единую точку (TIME-5) |
|
||||
| TIME-9.2 | длительность операции | монотонные часы процесса |
|
||||
|
||||
**Почему.** Стенные часы подводит NTP: они могут шагнуть назад, и тогда
|
||||
интервал, посчитанный вычитанием, выйдет отрицательным, а при шаге вперёд —
|
||||
@@ -135,7 +140,7 @@
|
||||
упустить: источник меток времени и источник интервалов — разные, даже если
|
||||
оба называются «часы».
|
||||
|
||||
### R10. Не-UTC существует только на слое отображения
|
||||
### TIME-10. Не-UTC существует только на слое отображения
|
||||
|
||||
**ДОЛЖЕН.** Преобразование в зону пользователя происходит при выводе и не
|
||||
проникает в хранение, сортировку и логи.
|
||||
@@ -147,7 +152,7 @@
|
||||
смещение удваивается, результат остаётся похожим на правду, а найти
|
||||
виновный слой можно только перечитав их все.
|
||||
|
||||
### R11. Зона отображения берётся из конфигурации, по умолчанию `UTC`
|
||||
### TIME-11. Зона отображения берётся из конфигурации, по умолчанию `UTC`
|
||||
|
||||
**ДОЛЖЕН.** Значение приходит из конфигурации (`arch/config.md`), значение
|
||||
по умолчанию — `UTC`.
|
||||
@@ -158,7 +163,7 @@
|
||||
тем, что лежит в базе и в логах, поэтому несовпадение с ожиданиями читается
|
||||
как «зону не задали», а не как «где-то потерялось смещение».
|
||||
|
||||
### R12. В календарных вычислениях зона указывается явно
|
||||
### TIME-12. В календарных вычислениях зона указывается явно
|
||||
|
||||
**ДОЛЖЕН.** «Сегодня», «за месяц» и прочие календарные границы считаются с
|
||||
явно переданной зоной, а не с системной зоной процесса.
|
||||
@@ -168,7 +173,7 @@
|
||||
расхождение не воспроизводится там, где его заметили, и объясняется средой,
|
||||
а не кодом. Явно переданная зона делает результат функцией от аргументов.
|
||||
|
||||
Зона по умолчанию здесь та же, что и для отображения (R11); календарная
|
||||
Зона по умолчанию здесь та же, что и для отображения (TIME-11); календарная
|
||||
логика, которой нужна другая, получает её тем же явным аргументом.
|
||||
|
||||
<!-- local:механизировано -->
|
||||
|
||||
Reference in New Issue
Block a user