- «Почему» у TIME-5 и TIME-6 после перенумерации в dcdd922 указывало на
TIME-3 и TIME-2 вместо KEYS-3 и KEYS-2: массовая замена подставила
префикс своего файла, и ссылки молча поехали на чужие по смыслу правила
- заодно ушли два последних нарушения META-21 — путь `arch/db-identifiers.md`
в тексте конвенции
193 lines
14 KiB
Markdown
193 lines
14 KiB
Markdown
---
|
||
prefix: TIME
|
||
---
|
||
|
||
# Время
|
||
|
||
Как приложение записывает моменты и длительности: в каком формате, откуда
|
||
берётся значение и где появляется не-UTC. Форма записи —
|
||
`LANGUAGE.md`.
|
||
|
||
## Область действия
|
||
|
||
Конвенция описывает фиксацию **свершившихся моментов** — того, что уже
|
||
произошло и попало в базу, лог или ответ API. Планирование будущих событий —
|
||
отдельный случай: там хранят локальное время плюс имя зоны, потому что
|
||
правила зон меняются в промежутке между планированием и наступлением. Пока
|
||
такой сущности нет, правил для неё в файле нет.
|
||
|
||
## Правила
|
||
|
||
### TIME-1. Единый формат — RFC 3339, UTC, суффикс `Z`
|
||
|
||
**ДОЛЖЕН.** Момент времени записывается как `2026-06-28T11:23:45Z` —
|
||
одинаково в хранении, логах, API и обмене с внешними системами.
|
||
|
||
**Почему.** Разные форматы в разных слоях требуют преобразования на каждой
|
||
границе, а ошибка в таком преобразовании не видна сразу: она всплывает через
|
||
полгода, на переходе на летнее время, когда реальное смещение перестаёт
|
||
совпадать с тем, которое подразумевалось при написании кода. UTC с явным `Z`
|
||
убирает из данных и смещение, и сам вопрос «в какой зоне это записано».
|
||
|
||
### TIME-2. Ширина строки фиксируется на каждый носитель
|
||
|
||
**ДОЛЖЕН.** Внутри одной колонки БД и внутри одного потока логов длина
|
||
строки времени одна и от записи к записи не плавает.
|
||
|
||
**Почему.** Лексикографическая сортировка совпадает с хронологией только
|
||
среди строк одинаковой длины: `…00.123Z` сортируется раньше `…00.12Z`, хотя
|
||
произошло позже. Ради этого ширина и фиксируется — `ORDER BY created_at` по
|
||
текстовому полю обязан давать порядок событий. Плавающая ширина (типичный
|
||
источник — форматирование, отбрасывающее незначащие нули) ломает порядок не
|
||
везде, а только на тех парах записей, где дробная часть оказалась короче, —
|
||
то есть редко, выборочно и невоспроизводимо.
|
||
|
||
### TIME-3. Точность разных носителей может различаться
|
||
|
||
**ДОПУСКАЕТСЯ.** У колонки БД и у потока логов каждая своя точность.
|
||
|
||
**Почему.** Квантор в TIME-2 — на носитель, а не на приложение, потому что
|
||
строки разных носителей между собой не сравниваются: сортировка идёт внутри
|
||
колонки, чтение — внутри потока. Явное разрешение нужно, чтобы TIME-2 не читался
|
||
как «одна точность на всё приложение»: от подгонки формата логов под формат
|
||
колонки ни одна пара строк не становится сравнимой, зато точность режется до
|
||
худшего из носителей.
|
||
|
||
### TIME-4. Локальное время не хранится и не передаётся
|
||
|
||
**НЕ ДОЛЖЕН.** Ни в базе, ни в логах, ни в JSON API нет меток в локальной
|
||
зоне.
|
||
|
||
**Почему.** Метка без зоны неинтерпретируема вне процесса, который её
|
||
записал: чтобы понять, какому моменту она соответствует, читателю нужно
|
||
знать настройки чужой машины на момент записи. И даже зная их, он не
|
||
разберёт час перехода на зимнее время: этот час идёт дважды, две записи
|
||
получают одинаковую метку, и порядок между ними не восстанавливается ничем.
|
||
|
||
### TIME-13. Чужой вход нормализуется при разборе, а не отклоняется
|
||
|
||
**ДОЛЖЕН.** Валидное по RFC 3339 значение с офсетом, отличным от `Z`, или с
|
||
долями секунды принимается от внешней системы и приводится к каноническому
|
||
виду (TIME-1) в точке разбора (TIME-5).
|
||
|
||
**Почему.** Канонический вид — обязательство нашего писателя, а не
|
||
контракт, наложенный на внешние системы: `…14:23:45+03:00` называет тот же
|
||
момент, что `…11:23:45Z`, и отклонять его — значит ломать интеграцию со
|
||
стороной, которая стандарт соблюла. Пропущенное же как есть, такое значение
|
||
нарушает форму и ширину носителя (TIME-1, TIME-2) и портит сортировку
|
||
выборочно — только на записях, пришедших извне, и далеко от места разбора.
|
||
Нормализация
|
||
в единой точке разбора оставляет ровно одно место, где неканонический вид
|
||
существует, — по ту сторону границы его уже нет.
|
||
|
||
### TIME-5. Единая точка получения «сейчас», форматирования и разбора
|
||
|
||
**ДОЛЖЕН.** Один модуль отдаёт текущий момент, он же форматирует и разбирает
|
||
метки; прямые вызовы часов по коду не разбросаны.
|
||
|
||
**Почему.** Формат, зона (TIME-1) и ширина (TIME-2) обязаны выполняться для всех
|
||
меток без исключения, а каждый прямой вызов часов заводит ещё одно место,
|
||
где их можно не соблюсти. Промах такого вызова проявляется не в коде, а в
|
||
данных, и обнаруживается, когда испорченных записей уже накопилось.
|
||
Соображение то же, что для идентификаторов (KEYS-3).
|
||
|
||
### TIME-6. Дефолтов времени в схеме БД нет
|
||
|
||
**НЕ ДОЛЖЕН.** Колонки времени не имеют `DEFAULT` с текущим моментом.
|
||
|
||
**Почему.** Дефолт превращает забытую вставку `created_at` в тихо работающий
|
||
код: значение появляется, но приходит от сервера БД — то есть с других часов
|
||
и в формате, который выбирала не единая точка (TIME-5). Без дефолта та же ошибка
|
||
падает громко и чинится в момент написания, а не при разборе расхождения
|
||
между временем в записи и временем в логе. Правило то же, что для
|
||
идентификаторов (KEYS-2).
|
||
|
||
### TIME-7. Длительность — отдельная величина, а не пара меток
|
||
|
||
**ДОЛЖЕН.** Длительность операции записывается числом (обычно
|
||
миллисекундами) в поле вида `duration_ms`.
|
||
|
||
**Почему.** Метка отвечает на вопрос «когда», длительность — на «сколько».
|
||
Пара меток заставляет каждого потребителя знать, какие именно две из них
|
||
образуют операцию, и вычитать их самому — в запросе, в дашборде и глазами в
|
||
логе; число сравнивается, агрегируется и попадает в перцентили без этого
|
||
шага. Кроме того, разность сохранённых меток считается по стенным часам и
|
||
наследует их дефект (TIME-9).
|
||
|
||
### TIME-8. Длительность засекает слой, который делает вызов
|
||
|
||
**СЛЕДУЕТ.** Замер живёт там же, где вызов, границы которого он измеряет.
|
||
|
||
**Почему.** Обе границы операции видит только этот слой: замер уровнем выше
|
||
приписывает операции чужие накладные расходы, уровнем ниже — теряет часть
|
||
вызова. В обоих случаях число остаётся правдоподобным и потому не
|
||
оспаривается, хотя отвечает не на тот вопрос, который к нему задают.
|
||
|
||
### TIME-9. Момент и интервал берутся с разных часов
|
||
|
||
**ДОЛЖЕН.** Источник зависит от того, что записывается:
|
||
|
||
| № | Величина | Источник |
|
||
|---|---|---|
|
||
| TIME-9.1 | момент события | стенные часы через единую точку (TIME-5) |
|
||
| TIME-9.2 | длительность операции | монотонные часы процесса |
|
||
|
||
**Почему.** Стенные часы подводит NTP: они могут шагнуть назад, и тогда
|
||
интервал, посчитанный вычитанием, выйдет отрицательным, а при шаге вперёд —
|
||
правдоподобным, но выдуманным. Монотонные часы, наоборот, не годятся для
|
||
меток: их ноль произволен и не переживает перезапуск процесса, так что вне
|
||
процесса такое значение ничего не означает. Отсюда следствие, которое легко
|
||
упустить: источник меток времени и источник интервалов — разные, даже если
|
||
оба называются «часы».
|
||
|
||
### TIME-10. Не-UTC существует только на слое отображения
|
||
|
||
**ДОЛЖЕН.** Преобразование в зону пользователя происходит при выводе и не
|
||
проникает в хранение, сортировку и логи.
|
||
|
||
**Почему.** Как только конвертация уходит вглубь, результат вычислений
|
||
начинает зависеть от настроек конкретного зрителя: одна и та же выборка даёт
|
||
разные группировки, а порядок записей перестаёт быть общим для всех. Ещё
|
||
хуже, что при конвертации в нескольких слоях её легко выполнить дважды —
|
||
смещение удваивается, результат остаётся похожим на правду, а найти
|
||
виновный слой можно только перечитав их все.
|
||
|
||
### TIME-11. Зона отображения берётся из конфигурации, по умолчанию `UTC`
|
||
|
||
**ДОЛЖЕН.** Значение приходит из конфигурации, значение по умолчанию —
|
||
`UTC`.
|
||
|
||
**Почему.** Зашитая в код зона превращает переезд или второго пользователя в
|
||
другом поясе в правку кода и релиз. Значение по умолчанию `UTC` выбрано
|
||
потому, что оно не притворяется настроенным: показанное время совпадает с
|
||
тем, что лежит в базе и в логах, поэтому несовпадение с ожиданиями читается
|
||
как «зону не задали», а не как «где-то потерялось смещение».
|
||
|
||
### TIME-12. В календарных вычислениях зона указывается явно
|
||
|
||
**ДОЛЖЕН.** «Сегодня», «за месяц» и прочие календарные границы считаются с
|
||
явно переданной зоной, а не с системной зоной процесса.
|
||
|
||
**Почему.** Системная зона разная на ноутбуке разработчика и в контейнере на
|
||
сервере, поэтому граница суток уезжает, а вместе с ней — содержимое отчёта:
|
||
расхождение не воспроизводится там, где его заметили, и объясняется средой,
|
||
а не кодом. Явно переданная зона делает результат функцией от аргументов.
|
||
|
||
Зона по умолчанию здесь та же, что и для отображения (TIME-11); календарная
|
||
логика, которой нужна другая, получает её тем же явным аргументом.
|
||
|
||
<!-- local:механизировано -->
|
||
<!-- /local -->
|
||
|
||
<!-- local:отступления -->
|
||
<!-- /local -->
|
||
|
||
## Связано
|
||
|
||
- конвенция `config` — где задаётся зона отображения.
|
||
- конвенция `db-identifiers` — то же правило «генерирует приложение» для
|
||
идентификаторов.
|
||
|
||
<!-- local:связано -->
|
||
<!-- /local -->
|