Welcome! Please see the About page for a little more info on how this works.

+8 votes
in core.async by
edited by

clojure.core.async/timeout uses System/currentTimeMillis to calculate its deadline (see here and here). While the docstring for System/currentTimeMillis doesn't mention it explicitly, it's quite clearly unsuitable for this purpose because it returns the current time as reported by the OS. Since that time can change arbitrarily (e.g. by correction via NTP), the deadline calculations may end up inconsistent. This behavior is very much intended, see e.g. this ticket where it was suggested to switch System/currentTimeMillis over to a monotonic clock where possible. It was refused with the following comment:

System.currentTimeMillis() is required to return milliseconds since the epoch and should reflect external changes to the system/wall-clock time. CLOCK_MONOTONIC_COARSE does not provide that and so can not be used for this purpose.

Also, the docstring for java.util.concurrent.ScheduledExecutorService states this quite plainly:

All schedule methods accept relative delays and periods as arguments, not absolute times or dates. It is a simple matter to transform an absolute time represented as a Date to the required form. For example, to schedule at a certain future date, you can use: schedule(task, date.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS). Beware however that expiration of a relative delay need not coincide with the current Date at which the task is enabled due to network time synchronization protocols, clock drift, or other factors.

Since all we care about here is deltas, it seems advisable to switch to System/nanoTime instead. Unfortunately, its docstring also doesn't make its properties very obvious. It doesn't mention that it's a monotonic clock but only hints at it by stating:

Returns the current value of the running Java Virtual Machine's high-resolution time source, in nanoseconds. This method can only be used to measure elapsed time and is not related to any other notion of system or wall-clock time. [...] The values returned by this method become meaningful only when the difference between two such values, obtained within the same instance of a Java virtual machine, is computed.

A case in point for using it is OpenJDK's implementation of java.util.concurrent.ScheduledThreadPoolExecutor whose deadline calculation method does so, as well as its getDelay implementation.

Furthermore--if it's anything to go by--various StackOverflow posts also discuss this and arrive at the same conclusion, e.g. this one:

Nonetheless, nanoTime() should still be preferred for implementing timed blocking, interval waiting, timeouts, etc. to currentTimeMillis() because the latter is a subject to the "time going backward" phenomenon (e. g. due to server time correction), i. e. currentTimeMillis() is not suitable for measuring time intervals at all. See this answer for more information.

See also a similar question about core.cache.

1 Answer

0 votes

Small correction: On POSIX systems, OpenJDK uses clock_gettime with CLOCK_MONOTONIC for System/nanoTime which is subject to incremental adjustments as done e.g. by NTP. However, it does guarantee to never go backwards and it's not affected by discontinuous jumps (e.g. by manually setting the system time) That's in contrast to CLOCK_REALTIME which is used by System/currentTimeMillis there. See man 2 clock_gettime for details.

The discussion around JDK-8006942 has some insight into why such incremental adjustment might be desirable even for System/nanoTime:

Having been bitten by various time-jumping bugs in the past due to NTP (and other) adjustments, my initial reaction was that doing any kind of adjustment to CLOCK_MONOTONIC was "crazy" and obviously we should prefer the unadjusted CLOCK_MONOTONIC_RAW. However after looking into this more carefully I agree with Martin's comment that the NTP-slew that is applied is generally desirable to keep the reported elapsed times in-line with actual elapsed time. This slew simply adjusts the rate at which the reported time is updated to account for hardware that runs faster or slower than true time (which is pretty much all hardware). It does not introduce observable jumps in the time reported and should not be observable to the end user.