Конфиг проходит валидацию, TOML парсер не жалуется ни единым словом, а cron задача, которая должна была запуститься в девять утра по Москве, запускается в полночь. Дело не в парсере и не в cron, дело в том, что TOML поддерживает сразу четыре разных способа записать дату и время, и только один из них однозначен.
Четыре формата дат в TOML
TOML нативно поддерживает типы дат прямо в спецификации, в отличие от JSON, где дата это просто строка. Но именно это разнообразие типов и создает путаницу.
offset-datetime = 2024-01-15T09:00:00+03:00
local-datetime = 2024-01-15T09:00:00
local-date = 2024-01-15
local-time = 09:00:00
| Тип | Есть часовой пояс | Есть дата | Есть время |
|---|---|---|---|
| offset-datetime | да | да | да |
| local-datetime | нет | да | да |
| local-date | нет | да | нет |
| local-time | нет | нет | да |
Разница между первыми двумя строками кажется незначительной, всего лишь +03:00 на конце, но именно эта разница решает, привезет конфиг девять утра по Москве или девять утра по времени того сервера, который в итоге будет читать этот TOML файл.
Где именно ломается логика
# Выглядит невинно, но это local-datetime, без явного часового пояса
scheduled_at = 2024-01-15T09:00:00
Когда парсер на Node.js читает такое значение через библиотеку вроде smol-toml или @iarna/toml, он должен решить, как интерпретировать эти девять утра. Часть библиотек трактует local-datetime как UTC время, часть как локальное время машины, на которой выполняется код. Если конфиг писали в Москве, ожидая московское время, а сервер в UTC интерпретирует те же цифры как UTC, разница набегает ровно на смещение часового пояса.
import { parse } from "smol-toml";
const config = parse('scheduled_at = 2024-01-15T09:00:00');
console.log(config.scheduled_at);
// Результат зависит от конкретной библиотеки и ее трактовки local-datetime,
// именно поэтому local-datetime и есть источник проблемы
Решение первое: всегда указывать offset явно
Самое надежное лечение, никогда не использовать local-datetime для чего-либо, что должно означать конкретный момент времени в конкретном часовом поясе.
# Однозначно: девять утра по Москве, UTC+3, где бы ни читался этот файл
scheduled_at = 2024-01-15T09:00:00+03:00
С явным offset никакой парсер не может ошибиться в интерпретации, потому что момент времени в offset-datetime абсолютен и не зависит от того, на какой машине с каким системным часовым поясом выполняется парсинг.
Решение второе: хранить только UTC, конвертировать на выводе
Если разработчик распределенной команды в разных часовых поясах, практичнее всего хранить в конфиге только UTC время и конвертировать в локальное представление уже на уровне приложения, а не в самом конфиге.
scheduled_at = 2024-01-15T06:00:00Z
Буква Z в конце это сокращение для нулевого смещения, то есть UTC. Такой формат читается однозначно вообще любым парсером TOML на любой машине, независимо от системных настроек часового пояса самого сервера.
Решение третье: если действительно нужна локальная дата без времени
Для случаев вроде “день рождения сотрудника” или “дата истечения годового контракта”, где часовой пояс вообще не имеет смысла, местный час не важен, стоит использовать именно local-date без времени вообще, а не подставлять произвольное время суток.
# Правильно для чистой даты без временной составляющей
contract_expires = 2024-12-31
# Неправильно: зачем-то добавили время, хотя оно не нужно и не имеет смысла
contract_expires = 2024-12-31T00:00:00
Как быстро проверить свой конфиг
Вставьте содержимое своего TOML файла в TOML Validator, он покажет разобранную структуру в виде JSON. Если поле с датой в результате выглядит как обычная строка без явного смещения времени, это верный признак, что перед вами local-datetime, и стоит перепроверить, действительно ли отсутствие часового пояса тут уместно, или это баг, который просто пока не выстрелил.
Итоговый чеклист
- Для конкретного момента времени в конкретном часовом поясе всегда указывайте offset явно, даже если это тот же часовой пояс, что и у сервера
- Для универсальности между командами в разных часовых поясах храните UTC с суффиксом Z, а конвертацию в локальное время делайте в коде приложения, а не в конфиге
- Для чистой даты без времени используйте local-date, не добавляйте фиктивное время суток
- Перед деплоем конфига на сервер с другим системным часовым поясом, чем у машины разработчика, перепроверьте именно поля с local-datetime без явного смещения