package net.i2p.router; import java.util.Date; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import net.i2p.data.DataHelper; import net.i2p.router.time.RouterTimestamper; import net.i2p.time.BuildTime; import net.i2p.time.Timestamper; import net.i2p.util.Clock; import net.i2p.util.Log; /** * Alternate location for determining the time which takes into account an offset. * This offset will ideally be periodically updated so as to serve as the difference * between the local computer's current time and the time as known by some reference * (such as an NTP synchronized clock). * * RouterClock is a subclass of Clock with access to router transports. * Configuration permitting, it will block clock offset changes * which would increase peer clock skew. */ public class RouterClock extends Clock { /** * How often we will slew the clock * i.e. ppm = 1000000/MAX_SLEW * We should be able to slew really fast, * this is probably a lot faster than what NTP does * 1/50 is 12s in a 10m tunnel lifetime, that should be fine. * All of this is @since 0.7.12 */ private static final long MAX_SLEW = 25; public static final int DEFAULT_STRATUM = 8; private static final int WORST_STRATUM = 16; /** the max NTP Timestamper delay is 30m right now, make this longer than that */ private static final long MIN_DELAY_FOR_WORSE_STRATUM = 45*60*1000; private volatile long _desiredOffset; private volatile long _lastSlewed; /** use system time for this */ private long _lastChanged; private int _lastStratum; private final RouterTimestamper _timeStamper; /** * If the system clock shifts by this much, * call the callback, we probably need a soft restart. * @since 0.8.8 */ private static final long MASSIVE_SHIFT_FORWARD = 150*1000; private static final long MASSIVE_SHIFT_BACKWARD = 61*1000; /** testing only */ private static final String PROP_DISABLE_ADJUSTMENT = "time.disableOffset"; private final Set<ClockShiftListener> _shiftListeners; private volatile long _lastShiftNanos; /** * Does not start. Caller MUST call start() */ public RouterClock(RouterContext context) { super(context); _lastStratum = WORST_STRATUM; _lastSlewed = System.currentTimeMillis(); _shiftListeners = new CopyOnWriteArraySet<ClockShiftListener>(); _lastShiftNanos = System.nanoTime(); _timeStamper = new RouterTimestamper(context, this); } /** * Cannot be stopped, but RouterTimestamper registers a shutdown task. * @since 0.9.20 */ public void start() { _timeStamper.startTimestamper(); } /** * The RouterTimestamper */ @Override public Timestamper getTimestamper() { return _timeStamper; } /** * Specify how far away from the "correct" time the computer is - a positive * value means that the system time is slow, while a negative value means the system time is fast. * * @param offsetMs the delta from System.currentTimeMillis() (NOT the delta from now()) */ @Override public void setOffset(long offsetMs, boolean force) { setOffset(offsetMs, force, DEFAULT_STRATUM); } /** * @since 0.7.12 * @param offsetMs the delta from System.currentTimeMillis() (NOT the delta from now()) */ private void setOffset(long offsetMs, int stratum) { setOffset(offsetMs, false, stratum); } /** * @since 0.7.12 * @param offsetMs the delta from System.currentTimeMillis() (NOT the delta from now()) */ private synchronized void setOffset(long offsetMs, boolean force, int stratum) { long delta = offsetMs - _offset; if (!force) { if (!_isSystemClockBad && (offsetMs > MAX_OFFSET || offsetMs < 0 - MAX_OFFSET)) { Log log = getLog(); if (log.shouldLog(Log.WARN)) log.warn("Maximum offset shift exceeded [" + offsetMs + "], NOT HONORING IT"); return; } // only allow substantial modifications before the first 10 minutes if (_alreadyChanged && (System.currentTimeMillis() - _startedOn > 10 * 60 * 1000)) { if ( (delta > MAX_LIVE_OFFSET) || (delta < 0 - MAX_LIVE_OFFSET) ) { Log log = getLog(); if (log.shouldLog(Log.WARN)) log.warn("The clock has already been updated, ignoring request to change it by " + delta + " to " + offsetMs, new Exception()); return; } } // let's be perfect if (delta == 0) { getLog().debug("Not changing offset, delta=0"); _alreadyChanged = true; return; } // only listen to a worse stratum if it's been a while if (_alreadyChanged && stratum > _lastStratum && System.currentTimeMillis() - _lastChanged < MIN_DELAY_FOR_WORSE_STRATUM) { Log log = getLog(); if (log.shouldLog(Log.DEBUG)) log.debug("Ignoring update from a stratum " + stratum + " clock, we recently had an update from a stratum " + _lastStratum + " clock"); return; } // If so configured, check sanity of proposed clock offset if (_context.getBooleanPropertyDefaultTrue("router.clockOffsetSanityCheck") && _alreadyChanged) { // Try calculating peer clock skew long currentPeerClockSkew = ((RouterContext)_context).commSystem().getFramedAveragePeerClockSkew(33); // Predict the effect of applying the proposed clock offset long predictedPeerClockSkew = currentPeerClockSkew + delta; // Fail sanity check if applying the offset would increase peer clock skew Log log = getLog(); if ((Math.abs(predictedPeerClockSkew) > (Math.abs(currentPeerClockSkew) + 5*1000)) || (Math.abs(predictedPeerClockSkew) > 20*1000)) { if (log.shouldWarn()) log.warn("Ignoring clock offset " + offsetMs + "ms (current " + _offset + "ms) since it would increase peer clock skew from " + currentPeerClockSkew + "ms to " + predictedPeerClockSkew + "ms. Stratrum: " + stratum); return; } else { if (log.shouldInfo()) log.info("Approving clock offset " + offsetMs + "ms (current " + _offset + "ms) since it would decrease peer clock skew from " + currentPeerClockSkew + "ms to " + predictedPeerClockSkew + "ms. Stratrum: " + stratum); } } // check sanity } // In first minute, allow a lower (better) stratum to do a step adjustment after // a previous step adjustment. // This allows NTP to trump a peer offset after a soft restart if (_alreadyChanged && (stratum >= _lastStratum || System.currentTimeMillis() - _startedOn > 60*1000)) { // Update the target offset, slewing will take care of the rest if (delta > 15*1000) getLog().logAlways(Log.WARN, "Warning - Updating target clock offset to " + offsetMs + "ms from " + _offset + "ms, Stratum " + stratum); else if (getLog().shouldLog(Log.INFO)) getLog().info("Updating target clock offset to " + offsetMs + "ms from " + _offset + "ms, Stratum " + stratum); if (!_statCreated) { _context.statManager().createRequiredRateStat("clock.skew", "Clock step adjustment (ms)", "Clock", new long[] { 10*60*1000, 3*60*60*1000, 24*60*60*1000 }); _statCreated = true; } _context.statManager().addRateData("clock.skew", delta); if (_context.getBooleanProperty(PROP_DISABLE_ADJUSTMENT)) { getLog().error("Clock adjustment disabled", new Exception()); } else { _desiredOffset = offsetMs; } } else { Log log = getLog(); if (log.shouldLog(Log.INFO)) log.info("Initializing clock offset to " + offsetMs + "ms, Stratum " + stratum); _alreadyChanged = true; if (_context.getBooleanProperty(PROP_DISABLE_ADJUSTMENT)) { log.error("Clock adjustment disabled", new Exception()); } else { _offset = offsetMs; _desiredOffset = offsetMs; // this is used by the JobQueue fireOffsetChanged(delta); } } _lastChanged = System.currentTimeMillis(); _lastStratum = stratum; } /** * @param stratum used to determine whether we should ignore * @since 0.7.12 */ @Override public void setNow(long realTime, int stratum) { if (realTime < BuildTime.getEarliestTime() || realTime > BuildTime.getLatestTime()) { Log log = getLog(); String msg = "Invalid time received: " + new Date(realTime); if (log.shouldWarn()) log.warn(msg, new Exception()); else log.logAlways(Log.WARN, msg); return; } long diff = realTime - System.currentTimeMillis(); setOffset(diff, stratum); } /** * Retrieve the current time synchronized with whatever reference clock is in use. * Do really simple clock slewing, like NTP but without jitter prevention. * Slew the clock toward the desired offset, but only up to a maximum slew rate, * and never let the clock go backwards because of slewing. * * Take care to only access the volatile variables once for speed and to * avoid having another thread change them * * This is called about a zillion times a second, so we can do the slewing right * here rather than in some separate thread to keep it simple. * Avoiding backwards clocks when updating in a thread would be hard too. */ @Override public long now() { long systemNow = System.currentTimeMillis(); // copy the global, so two threads don't both increment or decrement _offset long offset = _offset; long sinceLastSlewed = systemNow - _lastSlewed; if (sinceLastSlewed >= MASSIVE_SHIFT_FORWARD || sinceLastSlewed <= 0 - MASSIVE_SHIFT_BACKWARD) { _lastSlewed = systemNow; notifyMassive(sinceLastSlewed); } else if (sinceLastSlewed >= MAX_SLEW) { // copy the global long desiredOffset = _desiredOffset; if (desiredOffset > offset) { // slew forward offset += Math.min(10, sinceLastSlewed / MAX_SLEW); _offset = offset; } else if (desiredOffset < offset) { // slew backward, but don't let the clock go backward // this should be the first call since systemNow // was greater than lastSled + MAX_SLEW, i.e. different // from the last systemNow, thus we won't let the clock go backward, // no need to track when we were last called. _offset = --offset; } _lastSlewed = systemNow; } return offset + systemNow; } /* * A large system clock shift happened. Tell people about it. * * @since 0.8.8 */ private synchronized void notifyMassive(long shift) { long nowNanos = System.nanoTime(); // try to prevent dups, not guaranteed // nanoTime() isn't guaranteed to be monotonic either :( if (nowNanos < _lastShiftNanos + (MASSIVE_SHIFT_FORWARD * 1000*1000L)) return; _lastShiftNanos = nowNanos; // reset these so the offset can be reset by the timestamper again _startedOn = System.currentTimeMillis(); _alreadyChanged = false; getTimestamper().timestampNow(); if (shift > 0) getLog().log(Log.CRIT, "Large clock shift forward by " + DataHelper.formatDuration(shift)); else getLog().log(Log.CRIT, "Large clock shift backward by " + DataHelper.formatDuration(0 - shift)); for (ClockShiftListener lsnr : _shiftListeners) { lsnr.clockShift(shift); } } /* * Get notified of massive System clock shifts, positive or negative - * generally a minute or more. * The adjusted (offset) clock changes by the same amount. * The offset itself did not change. * Warning - duplicate notifications may occur. * * @since 0.8.8 */ public void addShiftListener(ClockShiftListener lsnr) { _shiftListeners.add(lsnr); } /* * @since 0.8.8 */ public void removeShiftListener(ClockShiftListener lsnr) { _shiftListeners.remove(lsnr); } /* * @since 0.8.8 */ public interface ClockShiftListener { /** * @param delta The system clock and adjusted clock just changed by this much, * in milliseconds (approximately) */ public void clockShift(long delta); } /* * How far we still have to slew, for diagnostics * @since 0.7.12 * @deprecated for debugging only */ @Deprecated public long getDeltaOffset() { return _desiredOffset - _offset; } }