pcp: слой толерантности времени — читать naive(=UTC) и Z во всём флоте

PcpTimeModule (libs/pcp-types-lib): толерантные Jackson-десериализаторы
LocalDateTime/OffsetDateTime/Instant — любой потребитель читает оба формата
на проводе и нормализует к UTC. Только чтение, формат записи не меняется.

Подключение фронтом по флоту:
- Spring Boot auto-configuration + AutoConfiguration.imports — Boot-маппер
  всех 12 сервисов на pcp-types-lib (без правок в сервисах);
- Jackson SPI (META-INF/services) — ручные мапперы с findAndRegisterModules();
- точечно: dynamic-plan/JacksonConfig (ручной ObjectMapper);
- tgu (WebFlux, не зависит от либы) — локальное зеркало модуля в
  CamundaJacksonConfig (Jackson-2 путь).

Зачем: микросервисы деплоятся независимо; с толерантностью продюсера можно
флипать LocalDateTime -> OffsetDateTime (Z) в любом порядке без синхронного
деплоя — потребитель не падает на парсинге офсета.

Тесты: PcpTimeModuleTest (оба формата -> один UTC-момент + авто-подхват SPI).
Доки: DATETIME_UTC.md (раздел 2bis + найденные баги формата), CHECKLIST.md.
This commit is contained in:
Дмитрий Соловьев
2026-06-02 22:35:50 +03:00
parent 240e12f442
commit 405f02aa86
10 changed files with 391 additions and 3 deletions
+1
View File
@@ -22,6 +22,7 @@
- добавлены или обновлены релевантные тесты;
- не внесено несвязанных правок;
- время на API — UTC; backend использует UTC-`now` (`Instant.now()` / `LocalDateTime.now(ZoneOffset.UTC)`), фронт парсит/форматирует через `utils/utcTime` (см. `docs/standards/DATETIME_UTC.md`);
- чтение времени на бэке толерантно к обоим форматам (naive=UTC и `Z`) — модуль наследуется из `pcp-types-lib` (`PcpTimeModule`) авто-конфигурацией; ручной `ObjectMapper`/не-pcp-types-lib сервис регистрирует модуль явно;
- validation выполнен по правилам `AGENTS.md`, либо пропуски явно объяснены.
### Дополнительно для BPM / Kafka / Zeebe / external integration изменений
+58 -3
View File
@@ -32,9 +32,50 @@
## 2. Эталоны переиспользования
- Frontend: `services/pcp-tgu-ops-ui/src/utils/utcTime.ts` — единая точка парсинга/форматирования.
- Backend: `pcp-request-service` отдаёт `OffsetDateTime` (строки с `Z`) и конвертирует вход
- Backend (чтение): `libs/pcp-types-lib` `utils/json/PcpTimeModule` — слой толерантности
(бэковый аналог `parseUtc`): любой потребитель читает и naive(=UTC), и `Z`/офсет (см. раздел ниже).
- Backend (запись): `pcp-request-service` отдаёт `OffsetDateTime` (строки с `Z`) и конвертирует вход
через `OffsetDateTime.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()` — корректный образец.
## 2bis. Слой толерантности времени (включатель безопасного флипа)
`PcpTimeModule` (`libs/pcp-types-lib/.../utils/json/PcpTimeModule.kt`) — Jackson-модуль с
толерантными **десериализаторами** для `LocalDateTime`, `OffsetDateTime`, `Instant`. Любой
потребитель с ним читает время в **обоих** форматах на проводе и нормализует к UTC:
| Тип поля | naive `…T01:00:00` | с зоной `…T01:00:00Z` / `+03:00` |
|---|---|---|
| `LocalDateTime` | как есть (= UTC) | приводится к UTC, зона отбрасывается |
| `OffsetDateTime` | `atOffset(UTC)` | `withOffsetSameInstant(UTC)` |
| `Instant` | трактуется как момент UTC | штатно |
Модуль трогает **только чтение** — формат записи определяется типом поля и не меняется, поэтому
подключение слоя само по себе не сдвигает ни один контракт.
**Зачем.** Микросервисы деплоятся независимо. Раньше перевод продюсера `LocalDateTime``OffsetDateTime`
(на проводе появляется `Z`) ломал ещё не обновлённого потребителя — штатный `LocalDateTimeDeserializer`
кидает исключение на офсете. С толерантностью во всём флоте продюсера можно флипать **в любом порядке,
без синхронного деплоя**. Это снимает требование «продюсер + все потребители одним коммитом».
**Подключение (три механизма, чтобы покрыть все мапперы):**
- Spring Boot auto-configuration `PcpTimeJacksonAutoConfiguration` (`@Bean Module`) +
`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` — Boot-managed
`ObjectMapper` каждого из 12 сервисов, зависящих от pcp-types-lib (правок в сервисах не требуется).
- Jackson SPI `META-INF/services/com.fasterxml.jackson.databind.Module` — ручные мапперы с
`findAndRegisterModules()` (напр. `pcp-request-service`).
- Точечная регистрация в мапперах, что обходят оба: `pcp-dynamic-plan-service/JacksonConfig`.
**Особый случай — pcp-tgu-service.** Он не зависит от pcp-types-lib и работает на WebFlux
(подключение либы притянуло бы `spring-boot-starter-web`/MVC и переключило тип приложения на
сервлетный). Поэтому в tgu лежит **локальное зеркало** `config/PcpTimeModule.kt` (поведение обязано
совпадать с библиотечным), зарегистрированное в Jackson-2 бине `CamundaJacksonConfig` — покрывает
Camunda/внутренний парсинг. Остаётся под-задача: REST-codec WebFlux на **Jackson 3**
(`tools.jackson.*`, иной API) ещё не толерантен — нужен Jackson-3 вариант модуля + регистрация в
кодеках.
Контракт слоя закреплён `PcpTimeModuleTest` (pcp-types-lib): оба формата → один и тот же UTC-момент,
плюс проверка авто-подхвата через SPI.
## 3. Состояние сервисов (срез аудита)
Формат на проводе определяется Jackson-сериализацией: `LocalDateTime` → ISO без зоны (naive, = UTC),
@@ -93,8 +134,22 @@ mission-planing) используют тот же дефолтный Jackson-к
конвертация только на границе (`atOffset(ZoneOffset.UTC)`). Потребитель один (ui-service), обновлён
синхронно; фронт `parseUtc` совместим. Контракт закреплён тестами выше.
Следующие кандидаты на миграцию (по согласованию, по сервису): остальные naive-DTO ui-service,
ballistics, complex-mission, dynamic-plan, slots, coverage-scheme.
- **Слой толерантности (`PcpTimeModule`) — раскатан по флоту.** 12 сервисов на pcp-types-lib
получают его авто-конфигурацией/SPI; dynamic-plan — точечно; tgu — локальным зеркалом (Jackson-2
путь). Это включатель: дальнейший флип продюсеров на `Z` больше не требует синхронного деплоя.
После толерантности продюсеров можно флипать **в любом порядке**. Кандидаты: остальные naive-DTO
ui-service, ballistics, complex-mission, dynamic-plan, slots, coverage-scheme.
### Найденные баги формата (теперь безопасно чинить — потребители толерантны)
- **`KafkaMessage.time` (общий Kafka-конверт, pcp-types-lib).** `time = LocalDateTime.now()` (локальное!)
+ сериализуется `LocalDateTimeTimeZoneSerializer` через `ZoneId.systemDefault()` → конверт несёт
МСК-стенные часы с `+03:00`, а потребитель (формат `…SSS[xxx]` в `LocalDateTime`) отбрасывает зону →
сдвиг. Под-задача: `now()` → UTC и сериализатор конверта → UTC/`Z`. Раньше блокировалось риском
поломки потребителей; со слоем толерантности — безопасно.
- **pcp-tgu-service REST-codec на Jackson 3** — ещё не толерантен (см. 2bis); под-задача на Jackson-3
вариант модуля.
## 5. Правило для нового кода