From 6f91621991b4e3695a994d53a7429a786facdd66 Mon Sep 17 00:00:00 2001 From: Manuel Polo Date: Tue, 23 Apr 2024 01:27:12 +0200 Subject: [PATCH] Fix #1125 NatsJetStreamMetaData timestamp timezone (#1126) --- .../client/impl/NatsJetStreamMetaData.java | 17 ++-------- .../io/nats/client/support/DateTimeUtils.java | 33 ++++++++++++++++++- .../impl/NatsJetStreamMetaDataTests.java | 12 +++++-- .../client/support/DateTimeUtilsTests.java | 6 ++++ 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/main/java/io/nats/client/impl/NatsJetStreamMetaData.java b/src/main/java/io/nats/client/impl/NatsJetStreamMetaData.java index fd2b47c66..8716b207a 100644 --- a/src/main/java/io/nats/client/impl/NatsJetStreamMetaData.java +++ b/src/main/java/io/nats/client/impl/NatsJetStreamMetaData.java @@ -13,9 +13,7 @@ package io.nats.client.impl; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZoneId; +import io.nats.client.support.DateTimeUtils; import java.time.ZonedDateTime; /** @@ -23,8 +21,6 @@ */ public class NatsJetStreamMetaData { - private static final long NANO_FACTOR = 10_00_000_000; - private final String prefix; private final String domain; private final String accountHash; @@ -99,15 +95,8 @@ else if (parts.length >= 11) { delivered = Long.parseLong(parts[streamIndex + 2]); streamSeq = Long.parseLong(parts[streamIndex + 3]); consumerSeq = Long.parseLong(parts[streamIndex + 4]); - - // not so clever way to separate nanos from seconds - long tsi = Long.parseLong(parts[streamIndex + 5]); - long seconds = tsi / NANO_FACTOR; - int nanos = (int) (tsi - ((tsi / NANO_FACTOR) * NANO_FACTOR)); - LocalDateTime ltd = LocalDateTime.ofEpochSecond(seconds, nanos, OffsetDateTime.now().getOffset()); - timestamp = ZonedDateTime.of(ltd, ZoneId.systemDefault()); // I think this is safe b/c the zone should match local - - this.pending = hasPending ? Long.parseLong(parts[streamIndex + 6]) : -1L; + timestamp = DateTimeUtils.parseDateTimeNanos(parts[streamIndex + 5]); + pending = hasPending ? Long.parseLong(parts[streamIndex + 6]) : -1L; } catch (Exception e) { throw new IllegalArgumentException(notAJetStreamMessage(natsMessage.getReplyTo())); diff --git a/src/main/java/io/nats/client/support/DateTimeUtils.java b/src/main/java/io/nats/client/support/DateTimeUtils.java index c84f53ac6..48fb560a6 100644 --- a/src/main/java/io/nats/client/support/DateTimeUtils.java +++ b/src/main/java/io/nats/client/support/DateTimeUtils.java @@ -15,7 +15,9 @@ import java.time.Duration; import java.time.Instant; +import java.time.OffsetDateTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -29,7 +31,8 @@ private DateTimeUtils() {} /* ensures cannot be constructed */ public static final ZoneId ZONE_ID_GMT = ZoneId.of("GMT"); public static final ZonedDateTime DEFAULT_TIME = ZonedDateTime.of(1, 1, 1, 0, 0, 0, 0, ZONE_ID_GMT); public static final DateTimeFormatter RFC3339_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.nnnnnnnnn'Z'"); - + private static final long NANO_FACTOR = 1_000_000_000; + public static ZonedDateTime toGmt(ZonedDateTime zonedDateTime) { return zonedDateTime.withZoneSameInstant(ZONE_ID_GMT); } @@ -69,6 +72,34 @@ public static ZonedDateTime parseDateTime(String dateTime, ZonedDateTime dflt) { public static ZonedDateTime parseDateTimeThrowParseError(String dateTime) { return toGmt(ZonedDateTime.parse(dateTime)); } + + /** + * Parses a long timestamp with nano precission in epoch UTC to the system + * default time-zone date time + * + * @param timestampNanos String timestamp + * @return a local Zoned Date time. + */ + public static ZonedDateTime parseDateTimeNanos(String timestampNanos) { + return parseDateTimeNanos(timestampNanos, ZoneId.systemDefault()); + } + + /** + * Parses a long timestamp with nano precission in epoch UTC to a Zoned date + * time + * + * @param timestampNanos String timestamp + * @param zoneId ZoneId + * @return a Zoned Date time. + */ + public static ZonedDateTime parseDateTimeNanos(String timestampNanos, ZoneId zoneId) { + long ts = Long.parseLong(timestampNanos); + long seconds = ts / NANO_FACTOR; + long nanos = ts % NANO_FACTOR; + Instant utcInstant = Instant.ofEpochSecond(seconds, nanos); + OffsetDateTime utcOffsetDT = OffsetDateTime.ofInstant(utcInstant, ZoneOffset.UTC); + return utcOffsetDT.atZoneSameInstant(zoneId); + } public static ZonedDateTime fromNow(long millis) { return ZonedDateTime.ofInstant(Instant.now().plusMillis(millis), ZONE_ID_GMT); diff --git a/src/test/java/io/nats/client/impl/NatsJetStreamMetaDataTests.java b/src/test/java/io/nats/client/impl/NatsJetStreamMetaDataTests.java index 36642500a..9bf350681 100644 --- a/src/test/java/io/nats/client/impl/NatsJetStreamMetaDataTests.java +++ b/src/test/java/io/nats/client/impl/NatsJetStreamMetaDataTests.java @@ -14,6 +14,8 @@ package io.nats.client.impl; import io.nats.client.Message; +import java.time.ZoneId; +import java.time.ZonedDateTime; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -52,9 +54,13 @@ private void validateMeta(boolean hasPending, boolean hasDomainHashToken, NatsJe assertEquals(2, meta.streamSequence()); assertEquals(3, meta.consumerSequence()); - assertEquals(2020, meta.timestamp().getYear()); - assertEquals(6, meta.timestamp().getMinute()); - assertEquals(113260000, meta.timestamp().getNano()); + ZonedDateTime localTs = meta.timestamp(); + assertEquals(2020, localTs.getYear()); + assertEquals(6, localTs.getMinute()); + assertEquals(113260000, localTs.getNano()); + + ZonedDateTime utcTs = localTs.withZoneSameInstant(ZoneId.of("UTC")); + assertEquals(0, utcTs.getHour()); assertEquals(hasPending ? 4L : -1L, meta.pendingCount()); diff --git a/src/test/java/io/nats/client/support/DateTimeUtilsTests.java b/src/test/java/io/nats/client/support/DateTimeUtilsTests.java index 27e2520ac..546ee662d 100644 --- a/src/test/java/io/nats/client/support/DateTimeUtilsTests.java +++ b/src/test/java/io/nats/client/support/DateTimeUtilsTests.java @@ -23,6 +23,12 @@ import static org.junit.jupiter.api.Assertions.*; public final class DateTimeUtilsTests { + + @Test + public void testParseDateTimeNanos() { + assertEquals(1605139610, DateTimeUtils.parseDateTimeNanos("1605139610113260000").toEpochSecond()); + assertEquals(113261234, DateTimeUtils.parseDateTimeNanos("1605139610113261234").toInstant().getNano()); + } @Test public void testParseDateTime() {