Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 34 additions & 15 deletions base/src/main/java/io/spine/base/Time.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -147,14 +148,30 @@ static class SystemTimeProvider implements Provider {
private SystemTimeProvider() {
}

/**
* {@inheritDoc}
*
* <p>Starting from Java 9 the precision of time may differ from platform to platform and
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nick-dolgiy

Please see my suggested edit below.

<p>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
 <a href='https://bugs.openjdk.java.net/browse/JDK-8068730'>issue</a> on this matter.

<p>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 
with the 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.

* depends on the underlying clock used by the JVM. See the corresponding
* <a href='https://bugs.openjdk.java.net/browse/JDK-8068730'>issue</a> on this matter.
*
* <p>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;
}
}
Expand All @@ -178,8 +195,8 @@ public Timestamp currentTime() {
* <p>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.
*
* <p>The nanosecond value is reset for each new passed {@link Instant} value.
* That allows to receive {@code 1 000} distinct time values per millisecond.
* <p>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.
*
* <p>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
Expand All @@ -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);
}
}
}
13 changes: 9 additions & 4 deletions base/src/test/java/io/spine/base/TimeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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);
}
}
Expand Down
2 changes: 1 addition & 1 deletion version.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down