Files
dc-observatio/docs/standards/DATETIME_UTC.md
T
Дмитрий Соловьев 260583b134 pcp: единообразие времени (UTC) по группе сервисов
Фронт (pcp-tgu-ops-ui):
- тест-страж utcTime.guard.test.ts: запрещает Date.parse / new Date(string) /
  toLocale* без timeZone вне utils/utcTime; тестовые фикстуры переведены на parseUtc.

Бэкенд:
- UTC-now: LocalDateTime.now() -> LocalDateTime.now(ZoneOffset.UTC) по всем сервисам
  (entity-дефолты, воркеры, use case'ы) и в тестовых фикстурах.
- Конвертации к UTC (баги на не-UTC JVM, каждая с регресс-тестом в не-UTC поясе):
  - tgu: VisibilityPayloadParser, SyncSpacecraftFromNsiUseCase (Instant -> UTC LocalDateTime);
  - mission-planing: MissionPlaningService (OffsetDateTime -> UTC через toUtcLocalDateTime);
  - dynamic-plan: RequestServiceClient (OffsetDateTime -> UTC);
  - slots: SlotRepositoryImpl (getObject(LocalDateTime) вместо getTimestamp().toLocalDateTime()).
- Контрактные сериализационные тесты формата на проводе (naive без зоны / Z):
  PlanResponse (tgu), RequestResponseDto (request, эталон), TguPlanning DTO (ui).
- KDoc-инвариант «=UTC» на UI-facing DTO (tgu PlanResponse, ui-service DTO вкладки ТГУ).

Документация: docs/standards/DATETIME_UTC.md (соглашение, таблица сервисов, под-задачи на миграцию к Z),
docs/standards/CHECKLIST.md (пункт про UTC на API).
2026-06-02 21:51:38 +03:00

8.6 KiB
Raw Blame History

DATETIME_UTC.md — единообразие времени (UTC) в группе сервисов pcp

Назначение

Этот документ фиксирует соглашение по работе со временем во всей группе сервисов pcp и состояние сервисов на момент аудита. На него ссылаются KDoc у DTO и комментарии в коде. Цель — убрать неоднозначность парсинга/форматирования и неоднородность формата на проводе, которая уже приводила к сдвигу времени на офсет пояса (для МСК — +3 ч).

1. Соглашение (canonical)

  1. Всё время — UTC. На любом API времена представляют момент в UTC. В БД — тоже UTC.
  2. Формат на проводе — ISO-8601 с явной зоной Z (предпочтительно). Где сервис исторически отдаёт naive LocalDateTime (ISO без зоны, напр. "2026-05-29T01:00:00"), это допустимо только при железном правиле «naive = UTC»; такой эндпоинт — кандидат на миграцию к Z.
  3. Backend:
    • «сейчас» — только UTC: Instant.now() или LocalDateTime.now(ZoneOffset.UTC); не LocalDateTime.now();
    • конвертация Instant/OffsetDateTimeLocalDateTime для naive-полей — только через UTC (LocalDateTime.ofInstant(instant, ZoneOffset.UTC), withOffsetSameInstant(ZoneOffset.UTC)), никогда через ZoneId.systemDefault() / atZone(systemDefault());
    • Jackson во всех сервисах настроен с JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS), поэтому LocalDateTime уходит как ISO без зоны, а OffsetDateTime/Instantс Z/офсетом.
  4. Frontend (pcp-tgu-ops-ui):
    • парсинг строк бэка — только через src/utils/utcTime.ts (parseUtc); никаких «голых» new Date(string) / Date.parse(string);
    • отображение — только UTC (formatUtcDateTime/Date/Time либо toLocale* с timeZone: "UTC");
    • <input type="datetime-local"> — значение через toUtcInputValue, чтение через parseUtc, отправка на offset-бэк — через toUtcIso.
    • Инвариант закреплён тестом-стражем src/utils/utcTime.guard.test.ts (падает на запрещённых паттернах).
  5. Никакой неявной локализации под пояс машины ни на фронте, ни в логах/ответах бэка.

2. Эталоны переиспользования

  • Frontend: services/pcp-tgu-ops-ui/src/utils/utcTime.ts — единая точка парсинга/форматирования.
  • Backend: pcp-request-service отдаёт OffsetDateTime (строки с Z) и конвертирует вход через OffsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime() — корректный образец.

3. Состояние сервисов (срез аудита)

Формат на проводе определяется Jackson-сериализацией: LocalDateTime → ISO без зоны (naive, = UTC), OffsetDateTime/Instant → ISO с Z. «Потребители» — кто читает время этого сервиса.

Сервис Преобладает на границе API Naive? Вердикт
pcp-request-service OffsetDateTime (Z) нет эталон: явная зона на проводе
pcp-tgu-service LocalDateTime (planId/plan times) да задокументирован «=UTC» (PlanResponse); баги конвертаций исправлены
pcp-ui-service LocalDateTime (агрегатор/CZML, summaries) да задокументировать «=UTC»; кандидат на Z
pcp-complex-mission-service LocalDateTime (+ часть Instant) смешано проверить смешение; задокументировать
pcp-dynamic-plan-service LocalDateTime (+ offset на входе клиента) да задокументировать «=UTC»
pcp-ballistics-service LocalDateTime да задокументировать «=UTC»
pcp-mission-planing-service LocalDateTime (вход — OffsetDateTime) да задокументировать «=UTC»
slots-service LocalDateTime да задокументировать «=UTC»; проверить rs.getTimestamp().toLocalDateTime()
pcp-coverage-scheme-service LocalDateTime да задокументировать «=UTC»
pcp-route-processing-service нет времени на границе API
pcp-satellite-catalog-service нет времени на границе API
pcp-stations-service нет времени на границе API
tle-monitoring-service нет времени на границе API

4. Известные расхождения и под-задачи (миграция к Z — не «большим взрывом»)

Систематический класс: LocalDateTime.now() без UTC-зоны (~80 мест: entity-дефолты createdAt/updatedAt, воркеры, use case'ы). На не-UTC JVM пишет локальное «сейчас» в UTC-поля. Низкий риск для audit-колонок, но формально неверно. Рекомендованные под-задачи по сервисам:

  • pcp-tgu-service — заменить оставшиеся LocalDateTime.now() (воркеры выдачи/решений, PlanQueryService cutoff, entity-дефолты) на LocalDateTime.now(ZoneOffset.UTC); тест на cutoff истории.
  • slots-service — проверить SlotRepositoryImpl.rs.getTimestamp("tn").toLocalDateTime() (зависит от зоны драйвера); зафиксировать чтение в UTC.
  • pcp-mission-planing-service / pcp-dynamic-plan-serviceOffsetDateTime.toLocalDateTime() теряет офсет; убедиться, что вход всегда UTC, иначе конвертировать через withOffsetSameInstant(UTC).
  • Все сервисы с naive LocalDateTime на границе — добавить сериализационный тест, фиксирующий формат на проводе (есть Z / нет зоны), затем по согласованию мигрировать DTO к OffsetDateTime/Instant (UTC). Фронтовый parseUtc уже совместим с обоими форматами, поэтому миграция безопасна при пер-сервисном раскате.

Контрактные тесты формата (зафиксировано)

Оба формата на проводе закреплены сериализационными тестами (mapper зеркалит конфиг Spring Boot — JavaTimeModule + disable(WRITE_DATES_AS_TIMESTAMPS)):

  • PlanResponseContractTest (pcp-tgu-service) — naive LocalDateTime без зоны.
  • RequestResponseDtoContractTest (pcp-request-service) — OffsetDateTime с Z (эталон).
  • TguPlanningDtoContractTest (pcp-ui-service) — оба формата: naive LocalDateTime + Instant с Z.

Прочие сервисы с naive-временем (ballistics, complex-mission, dynamic-plan, slots, coverage-scheme, mission-planing) используют тот же дефолтный Jackson-конфиг → тот же naive-формат; при желании пер-сервисный страж копируется по образцу PlanResponseContractTest.

5. Правило для нового кода

  • Frontend: только utils/utcTime (страж utcTime.guard.test.ts не даст просочиться нарушителю).
  • Backend: UTC-now; на новых API предпочтителен явный Z (OffsetDateTime/Instant).
  • Меняешь формат на проводе — обнови потребителей и добавь сериализационный тест (контракт не должен «уехать» молча).