Skip to content

Commit

Permalink
Fix #1125 NatsJetStreamMetaData timestamp timezone (#1126)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrmx authored Apr 22, 2024
1 parent 3df1fd7 commit 6f91621
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 18 deletions.
17 changes: 3 additions & 14 deletions src/main/java/io/nats/client/impl/NatsJetStreamMetaData.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,14 @@

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;

/**
* Jetstream meta data about a message, when applicable.
*/
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;
Expand Down Expand Up @@ -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()));
Expand Down
33 changes: 32 additions & 1 deletion src/main/java/io/nats/client/support/DateTimeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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());

Expand Down
6 changes: 6 additions & 0 deletions src/test/java/io/nats/client/support/DateTimeUtilsTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 6f91621

Please sign in to comment.