pcp-types-lib: Kafka-конверт time в UTC (фикс сдвига пояса)
KafkaMessage.time: LocalDateTime.now() -> now(ZoneOffset.UTC); LocalDateTimeTimeZoneSerializer: atZone(systemDefault()) -> atOffset(UTC). Было: локальное now() + systemDefault давали на проводе "...+03:00", и потребитель (паттерн ...SSS[xxx] в LocalDateTime) дропал зону -> сдвиг на пояс. Стало: "...+00:00" — инстант тот же, но стенные часы совпадают с UTC, сдвиг ушёл. Тест LocalDateTimeTimeZoneSerializerTest: UTC-офсет без сдвига + round-trip. DATETIME_UTC.md: баг перенесён в «Исправленные».
This commit is contained in:
@@ -141,15 +141,19 @@ mission-planing) используют тот же дефолтный Jackson-к
|
|||||||
После толерантности продюсеров можно флипать **в любом порядке**. Кандидаты: остальные naive-DTO
|
После толерантности продюсеров можно флипать **в любом порядке**. Кандидаты: остальные naive-DTO
|
||||||
ui-service, ballistics, complex-mission, dynamic-plan, slots, coverage-scheme.
|
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(ZoneOffset.UTC)` + сериализация `atOffset(ZoneOffset.UTC)` → на проводе
|
||||||
|
`…+00:00`. **Инстант не изменился** (тот же момент), но стенные часы теперь совпадают с UTC, поэтому
|
||||||
|
потребитель, дропающий зону, получает корректное время. Закреплено `LocalDateTimeTimeZoneSerializerTest`.
|
||||||
|
|
||||||
|
### Остаток (под-задачи)
|
||||||
|
|
||||||
- **`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
|
- **pcp-tgu-service REST-codec на Jackson 3** — ещё не толерантен (см. 2bis); под-задача на Jackson-3
|
||||||
вариант модуля.
|
вариант модуля + регистрация в кодеках WebFlux.
|
||||||
|
|
||||||
## 5. Правило для нового кода
|
## 5. Правило для нового кода
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonAlias
|
|||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||||
import space.nstart.pcp.pcp_types_lib.utils.json.LocalDateTimeTimeZoneSerializer
|
import space.nstart.pcp.pcp_types_lib.utils.json.LocalDateTimeTimeZoneSerializer
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
@@ -20,9 +21,12 @@ class KafkaMessage<out T> (
|
|||||||
val id = UUID.randomUUID().toString()
|
val id = UUID.randomUUID().toString()
|
||||||
var source = "Example"
|
var source = "Example"
|
||||||
|
|
||||||
|
// Время конверта в UTC: naive LocalDateTime трактуется как UTC (см. docs/standards/DATETIME_UTC.md),
|
||||||
|
// сериализатор помечает его офсетом +00:00. Раньше now() + systemDefault давали локальные часы и
|
||||||
|
// несовпадение момента у потребителей, дропающих зону.
|
||||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]")
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]")
|
||||||
@JsonSerialize(using = LocalDateTimeTimeZoneSerializer::class)
|
@JsonSerialize(using = LocalDateTimeTimeZoneSerializer::class)
|
||||||
val time: LocalDateTime = LocalDateTime.now()
|
val time: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
|
||||||
|
|
||||||
val dataContentType = "application/json"
|
val dataContentType = "application/json"
|
||||||
val specVersion = "1.0"
|
val specVersion = "1.0"
|
||||||
|
|||||||
+9
-5
@@ -4,9 +4,16 @@ import com.fasterxml.jackson.core.JsonGenerator
|
|||||||
import com.fasterxml.jackson.databind.JsonSerializer
|
import com.fasterxml.jackson.databind.JsonSerializer
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider
|
import com.fasterxml.jackson.databind.SerializerProvider
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneOffset
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сериализует naive [LocalDateTime] как момент в **UTC** (офсет `+00:00`).
|
||||||
|
* По соглашению группы `pcp` naive LocalDateTime = UTC (см. docs/standards/DATETIME_UTC.md),
|
||||||
|
* поэтому зона навешивается через [ZoneOffset.UTC], а не через пояс машины.
|
||||||
|
* Прежняя реализация (`ZoneId.systemDefault()`) помечала те же часы локальным офсетом, из-за чего
|
||||||
|
* потребитель, дропающий зону, получал сдвиг на пояс.
|
||||||
|
*/
|
||||||
class LocalDateTimeTimeZoneSerializer: JsonSerializer<LocalDateTime?>() {
|
class LocalDateTimeTimeZoneSerializer: JsonSerializer<LocalDateTime?>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -14,9 +21,6 @@ class LocalDateTimeTimeZoneSerializer: JsonSerializer<LocalDateTime?>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(p0: LocalDateTime?, p1: JsonGenerator, p2: SerializerProvider) {
|
override fun serialize(p0: LocalDateTime?, p1: JsonGenerator, p2: SerializerProvider) {
|
||||||
|
p1.writeString(p0?.atOffset(ZoneOffset.UTC)?.format(fmt))
|
||||||
//todo: проверить корректность зоны по нормальным правилам
|
|
||||||
// p1.writeString(p0?.atZone(ZoneId.of("Europe/Moscow"))?.format(fmt))
|
|
||||||
p1.writeString(p0?.atZone(ZoneId.systemDefault())?.format(fmt))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package space.nstart.pcp.pcp_types_lib.utils.json
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Контракт сериализатора времени Kafka-конверта ([LocalDateTimeTimeZoneSerializer]):
|
||||||
|
* naive LocalDateTime помечается офсетом UTC (`+00:00`), часы не сдвигаются.
|
||||||
|
* Закрывает баг прежней реализации через `ZoneId.systemDefault()`
|
||||||
|
* (см. docs/standards/DATETIME_UTC.md).
|
||||||
|
*/
|
||||||
|
class LocalDateTimeTimeZoneSerializerTest {
|
||||||
|
|
||||||
|
private val mapper = jacksonObjectMapper().findAndRegisterModules()
|
||||||
|
|
||||||
|
// Зеркалит аннотации реального поля KafkaMessage.time (паттерн с опциональным офсетом).
|
||||||
|
private data class Holder(
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS[xxx]")
|
||||||
|
@JsonSerialize(using = LocalDateTimeTimeZoneSerializer::class)
|
||||||
|
val t: LocalDateTime
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `serializes naive LocalDateTime with UTC offset, no wall-clock shift`() {
|
||||||
|
val json = mapper.writeValueAsString(Holder(LocalDateTime.of(2026, 5, 29, 1, 0, 0)))
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
json.contains("2026-05-29T01:00:00.000+00:00"),
|
||||||
|
"ожидался UTC-офсет без сдвига часов, получено: $json"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `round-trips through the tolerance layer to the same UTC moment`() {
|
||||||
|
val original = LocalDateTime.of(2026, 5, 29, 1, 0, 0)
|
||||||
|
|
||||||
|
val json = mapper.writeValueAsString(Holder(original))
|
||||||
|
val back: Holder = mapper.readValue(json)
|
||||||
|
|
||||||
|
assertEquals(original, back.t)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user