- пятая, необязательная часть правила: код парой «плохо → хорошо» после обоснования; метка добавлена в словарь (EXAMPLES) и в строку о версии языка во всех тринадцати файлах - сказано, чем примеры не являются: требований в блоке нет, дословным сниппетом он не служит, при расхождении с нормой правят пример - READING.md обновлён по META-30, в машинные проверки добавлен порядок блоков, в читательские — что примеры норму не расширяют
15 KiB
topic, prefix
| topic | prefix |
|---|---|
| time | TIME |
Время
Как приложение записывает моменты и длительности: в каком формате, откуда берётся значение и где появляется не-UTC.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Область действия
Конвенция описывает фиксацию свершившихся моментов — того, что уже произошло и попало в базу, лог или ответ 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); календарная логика, которой нужна другая, получает её тем же явным аргументом.
Связано
- конвенция
config— где задаётся зона отображения. - конвенция
db-identifiers— то же правило «генерирует приложение» для идентификаторов.