diff --git a/base/src/main/java/io/spine/base/Time.java b/base/src/main/java/io/spine/base/Time.java index 94a5edbd33..7b2e65c539 100644 --- a/base/src/main/java/io/spine/base/Time.java +++ b/base/src/main/java/io/spine/base/Time.java @@ -30,6 +30,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; /** * Utilities for working with time information. @@ -147,14 +148,30 @@ static class SystemTimeProvider implements Provider { private SystemTimeProvider() { } + /** + * {@inheritDoc} + * + *

Starting from Java 9 the precision of time may differ from platform to platform and + * depends on the underlying clock used by the JVM. See the corresponding + * issue on this matter. + * + *

In order to provide consistent behavior on different platforms and avoid the overflow + * of the emulated nanosecond value, this method intentionally uses the system time + * in millisecond precision. Therefore even if the system clock may offer more precise + * values, the {@code System.currentTimeMillis()} is used as a base for the returned values. + */ @Override public Timestamp currentTime() { - Instant now = Instant.now(); - int nanosOnly = IncrementalNanos.valueForTime(now); - Timestamp result = Timestamp.newBuilder() - .setSeconds(now.getEpochSecond()) - .setNanos(now.getNano() + nanosOnly) - .build(); + long millis = System.currentTimeMillis(); + long seconds = (millis / 1000); + @SuppressWarnings("NumericCastThatLosesPrecision") + int nanos = (int) (millis % 1000) * (int) MILLISECONDS.toNanos(1); + int nanosOnly = IncrementalNanos.valueForTime(seconds, nanos); + Timestamp result = Timestamp + .newBuilder() + .setSeconds(seconds) + .setNanos(nanos + nanosOnly) + .build(); return result; } } @@ -178,8 +195,8 @@ public Timestamp currentTime() { *

The returned nanosecond value starts at {@code 0} and never exceeds {@code 999 999}. * It is designed to keep the millisecond value provided by a typical-JVM system clock intact. * - *

The nanosecond value is reset for each new passed {@link Instant} value. - * That allows to receive {@code 1 000} distinct time values per millisecond. + *

The nanosecond value is reset for each new passed {@code seconds} and {@code nanos} + * values. That allows to receive {@code 1 000} distinct time values per millisecond. * *

In case the upper bound of the nanos is reached, meaning that there were more than * {@code 1 000} calls to this class within a millisecond, the nanosecond value is reset @@ -196,25 +213,27 @@ static final class IncrementalNanos { private static final IncrementalNanos instance = new IncrementalNanos(); private int counter; - private Instant previousValue; - private synchronized int getNextValue(Instant forTime) { - if (forTime.equals(previousValue)) { - previousValue = forTime; + private long previousSeconds; + private int previousNanos; + + private synchronized int getNextValue(long seconds, int nanos) { + if (previousSeconds == seconds && previousNanos == nanos) { counter += NANOS_PER_MICROSECOND; counter = counter % MAX_VALUE; } else { - previousValue = forTime; counter = 0; } + previousSeconds = seconds; + previousNanos = nanos; return counter; } /** * Obtains the next nanosecond value. */ - static int valueForTime(Instant forTime) { - return instance.getNextValue(forTime); + static int valueForTime(long seconds, int nanos) { + return instance.getNextValue(seconds, nanos); } } } diff --git a/base/src/test/java/io/spine/base/TimeTest.java b/base/src/test/java/io/spine/base/TimeTest.java index ca11036880..cf663b93ec 100644 --- a/base/src/test/java/io/spine/base/TimeTest.java +++ b/base/src/test/java/io/spine/base/TimeTest.java @@ -106,8 +106,10 @@ class IncrementalNanosEmulator { " within a single point of wall-clock time") void differentValuesForTheSameTime() { Instant now = Instant.now(); - assertThat(IncrementalNanos.valueForTime(now)) - .isLessThan(IncrementalNanos.valueForTime(now)); + long seconds = now.getEpochSecond(); + int nanos = now.getNano(); + assertThat(IncrementalNanos.valueForTime(seconds, nanos)) + .isLessThan(IncrementalNanos.valueForTime(seconds, nanos)); } @SuppressWarnings("ResultOfMethodCallIgnored") @@ -116,8 +118,11 @@ void differentValuesForTheSameTime() { void resetsNanosForNewInstant() { Instant now = Instant.now(); Instant oneMsLater = now.plus(1, ChronoUnit.MILLIS); - IncrementalNanos.valueForTime(now); // Ignore this value. - int value = IncrementalNanos.valueForTime(oneMsLater); + IncrementalNanos + .valueForTime(now.getEpochSecond(), now.getNano()); // Ignore this value. + long oneMsLaterSeconds = oneMsLater.getEpochSecond(); + int oneMsLaterNanos = oneMsLater.getNano(); + int value = IncrementalNanos.valueForTime(oneMsLaterSeconds, oneMsLaterNanos); assertThat(value).isEqualTo(0); } } diff --git a/version.gradle.kts b/version.gradle.kts index 325bb7dc1d..485c83b5b1 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -25,7 +25,7 @@ * as we want to manage the versions in a single source. */ -val SPINE_VERSION = "1.5.22" +val SPINE_VERSION = "1.5.23" project.extra.apply { this["spineVersion"] = SPINE_VERSION