• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2016 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc;
18 
19 import java.util.Arrays;
20 import java.util.Locale;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 
25 /**
26  * An absolute point in time, generally for tracking when a task should be completed. A deadline is
27  * immutable except for the passage of time causing it to expire.
28  *
29  * <p>Many systems use timeouts, which are relative to the start of the operation. However, being
30  * relative causes them to be poorly suited for managing higher-level tasks where there are many
31  * components and sub-operations that may not know the time of the initial "start of the operation."
32  * However, a timeout can be converted to a {@code Deadline} at the start of the operation and then
33  * passed to the various components unambiguously.
34  */
35 public final class Deadline implements Comparable<Deadline> {
36   private static final SystemTicker SYSTEM_TICKER = new SystemTicker();
37   // nanoTime has a range of just under 300 years. Only allow up to 100 years in the past or future
38   // to prevent wraparound as long as process runs for less than ~100 years.
39   private static final long MAX_OFFSET = TimeUnit.DAYS.toNanos(100 * 365);
40   private static final long MIN_OFFSET = -MAX_OFFSET;
41   private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1);
42 
43   /**
44    * Returns the ticker that's based on system clock.
45    *
46    * <p>This is <strong>EXPERIMENTAL</strong> API and may subject to change.  If you'd like it to be
47    * stabilized or have any feedback, please
48    * <a href="https://github.com/grpc/grpc-java/issues/6030">let us know</a>.
49    *
50    * @since 1.24.0
51    */
getSystemTicker()52   public static Ticker getSystemTicker() {
53     return SYSTEM_TICKER;
54   }
55 
56   /**
57    * Create a deadline that will expire at the specified offset based on the {@link #getSystemTicker
58    * system ticker}.
59    *
60    * <p>If the given offset is extraordinarily long, say 100 years, the actual deadline created
61    * might saturate.
62    *
63    * @param duration A non-negative duration.
64    * @param units The time unit for the duration.
65    * @return A new deadline.
66    */
after(long duration, TimeUnit units)67   public static Deadline after(long duration, TimeUnit units) {
68     return after(duration, units, SYSTEM_TICKER);
69   }
70 
71   /**
72    * Create a deadline that will expire at the specified offset based on the given {@link Ticker}.
73    *
74    * <p>If the given offset is extraordinarily long, say 100 years, the actual deadline created
75    * might saturate.
76    *
77    * <p><strong>CAUTION</strong>: Only deadlines created with the same {@link Ticker} instance can
78    * be compared by methods like {@link #minimum}, {@link #isBefore} and {@link #compareTo}.  Custom
79    * Tickers should only be used in tests where you fake out the clock.  Always use the {@link
80    * #getSystemTicker system ticker} in production, or serious errors may occur.
81    *
82    * <p>This is <strong>EXPERIMENTAL</strong> API and may subject to change.  If you'd like it to be
83    * stabilized or have any feedback, please
84    * <a href="https://github.com/grpc/grpc-java/issues/6030">let us know</a>.
85    *
86    * @param duration A non-negative duration.
87    * @param units The time unit for the duration.
88    * @param ticker Where this deadline refer the current time
89    * @return A new deadline.
90    *
91    * @since 1.24.0
92    */
after(long duration, TimeUnit units, Ticker ticker)93   public static Deadline after(long duration, TimeUnit units, Ticker ticker) {
94     checkNotNull(units, "units");
95     return new Deadline(ticker, units.toNanos(duration), true);
96   }
97 
98   private final Ticker ticker;
99   private final long deadlineNanos;
100   private volatile boolean expired;
101 
Deadline(Ticker ticker, long offset, boolean baseInstantAlreadyExpired)102   private Deadline(Ticker ticker, long offset, boolean baseInstantAlreadyExpired) {
103     this(ticker, ticker.nanoTime(), offset, baseInstantAlreadyExpired);
104   }
105 
Deadline(Ticker ticker, long baseInstant, long offset, boolean baseInstantAlreadyExpired)106   private Deadline(Ticker ticker, long baseInstant, long offset,
107       boolean baseInstantAlreadyExpired) {
108     this.ticker = ticker;
109     // Clamp to range [MIN_OFFSET, MAX_OFFSET]
110     offset = Math.min(MAX_OFFSET, Math.max(MIN_OFFSET, offset));
111     deadlineNanos = baseInstant + offset;
112     expired = baseInstantAlreadyExpired && offset <= 0;
113   }
114 
115   /**
116    * Returns whether this has deadline expired.
117    *
118    * @return {@code true} if it has, otherwise {@code false}.
119    */
isExpired()120   public boolean isExpired() {
121     if (!expired) {
122       if (deadlineNanos - ticker.nanoTime() <= 0) {
123         expired = true;
124       } else {
125         return false;
126       }
127     }
128     return true;
129   }
130 
131   /**
132    * Is {@code this} deadline before another.  Two deadlines must be created using the same {@link
133    * Ticker}.
134    */
isBefore(Deadline other)135   public boolean isBefore(Deadline other) {
136     checkTicker(other);
137     return this.deadlineNanos - other.deadlineNanos < 0;
138   }
139 
140   /**
141    * Return the minimum deadline of {@code this} or an other deadline.  They must be created using
142    * the same {@link Ticker}.
143    *
144    * @param other deadline to compare with {@code this}.
145    */
minimum(Deadline other)146   public Deadline minimum(Deadline other) {
147     checkTicker(other);
148     return isBefore(other) ? this : other;
149   }
150 
151   /**
152    * Create a new deadline that is offset from {@code this}.
153    *
154    * <p>If the given offset is extraordinarily long, say 100 years, the actual deadline created
155    * might saturate.
156    */
157   // TODO(ejona): This method can cause deadlines to grow too far apart. For example:
158   // Deadline.after(100 * 365, DAYS).offset(100 * 365, DAYS) would be less than
159   // Deadline.after(-100 * 365, DAYS)
offset(long offset, TimeUnit units)160   public Deadline offset(long offset, TimeUnit units) {
161     // May already be expired
162     if (offset == 0) {
163       return this;
164     }
165     return new Deadline(ticker, deadlineNanos, units.toNanos(offset), isExpired());
166   }
167 
168   /**
169    * How much time is remaining in the specified time unit. Internal units are maintained as
170    * nanoseconds and conversions are subject to the constraints documented for
171    * {@link TimeUnit#convert}. If there is no time remaining, the returned duration is how
172    * long ago the deadline expired.
173    */
timeRemaining(TimeUnit unit)174   public long timeRemaining(TimeUnit unit) {
175     final long nowNanos = ticker.nanoTime();
176     if (!expired && deadlineNanos - nowNanos <= 0) {
177       expired = true;
178     }
179     return unit.convert(deadlineNanos - nowNanos, TimeUnit.NANOSECONDS);
180   }
181 
182   /**
183    * Schedule a task to be run when the deadline expires.
184    *
185    * <p>Note if this deadline was created with a custom {@link Ticker}, the {@code scheduler}'s
186    * underlying clock should be synchronized with that Ticker.  Otherwise the task won't be run at
187    * the expected point of time.
188    *
189    * @param task to run on expiration
190    * @param scheduler used to execute the task
191    * @return {@link ScheduledFuture} which can be used to cancel execution of the task
192    */
runOnExpiration(Runnable task, ScheduledExecutorService scheduler)193   public ScheduledFuture<?> runOnExpiration(Runnable task, ScheduledExecutorService scheduler) {
194     checkNotNull(task, "task");
195     checkNotNull(scheduler, "scheduler");
196     return scheduler.schedule(task, deadlineNanos - ticker.nanoTime(), TimeUnit.NANOSECONDS);
197   }
198 
199   @Override
toString()200   public String toString() {
201     long remainingNanos = timeRemaining(TimeUnit.NANOSECONDS);
202     long seconds = Math.abs(remainingNanos) / NANOS_PER_SECOND;
203     long nanos = Math.abs(remainingNanos) % NANOS_PER_SECOND;
204 
205     StringBuilder buf = new StringBuilder();
206     if (remainingNanos < 0) {
207       buf.append('-');
208     }
209     buf.append(seconds);
210     if (nanos > 0) {
211       buf.append(String.format(Locale.US, ".%09d", nanos));
212     }
213     buf.append("s from now");
214     if (ticker != SYSTEM_TICKER) {
215       buf.append(" (ticker=" + ticker + ")");
216     }
217     return buf.toString();
218   }
219 
220   /**
221    * {@inheritDoc}
222    *
223    * <p>Both deadlines must be created with the same {@link Ticker}.
224    */
225   @Override
compareTo(Deadline that)226   public int compareTo(Deadline that) {
227     checkTicker(that);
228     long diff = this.deadlineNanos - that.deadlineNanos;
229     if (diff < 0) {
230       return -1;
231     } else if (diff > 0) {
232       return 1;
233     }
234     return 0;
235   }
236 
237   @Override
hashCode()238   public int hashCode() {
239     return Arrays.asList(this.ticker, this.deadlineNanos).hashCode();
240   }
241 
242   @Override
equals(final Object o)243   public boolean equals(final Object o) {
244     if (o == this) {
245       return true;
246     }
247     if (!(o instanceof Deadline)) {
248       return false;
249     }
250 
251     final Deadline other = (Deadline) o;
252     if (this.ticker == null ? other.ticker != null : this.ticker != other.ticker) {
253       return false;
254     }
255     if (this.deadlineNanos != other.deadlineNanos) {
256       return false;
257     }
258     return true;
259   }
260 
261   /**
262    * Time source representing nanoseconds since fixed but arbitrary point in time.
263    *
264    * <p>DO NOT use custom {@link Ticker} implementations in production, because deadlines created
265    * with custom tickers are incompatible with those created with the system ticker.  Always use
266    * the {@link #getSystemTicker system ticker} whenever you need to provide one in production code.
267    *
268    * <p>This is <strong>EXPERIMENTAL</strong> API and may subject to change.  If you'd like it to be
269    * stabilized or have any feedback, please
270    * <a href="https://github.com/grpc/grpc-java/issues/6030">let us know</a>.
271    *
272    * <p>In general implementations should be thread-safe, unless it's implemented and used in a
273    * localized environment (like unit tests) where you are sure the usages are synchronized.
274    *
275    * @since 1.24.0
276    */
277   public abstract static class Ticker {
278     /** Returns the number of nanoseconds since this source's epoch. */
nanoTime()279     public abstract long nanoTime();
280   }
281 
282   private static class SystemTicker extends Ticker {
283     @Override
nanoTime()284     public long nanoTime() {
285       return System.nanoTime();
286     }
287   }
288 
checkNotNull(T reference, Object errorMessage)289   private static <T> T checkNotNull(T reference, Object errorMessage) {
290     if (reference == null) {
291       throw new NullPointerException(String.valueOf(errorMessage));
292     }
293     return reference;
294   }
295 
checkTicker(Deadline other)296   private void checkTicker(Deadline other) {
297     if (ticker != other.ticker) {
298       throw new AssertionError(
299           "Tickers (" + ticker + " and " + other.ticker + ") don't match."
300           + " Custom Ticker should only be used in tests!");
301     }
302   }
303 }
304