package net.i2p.util;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import net.i2p.I2PAppContext;
import net.i2p.time.BuildTime;
import net.i2p.time.Timestamper;
/**
* 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).
*
* Protected members are used in the subclass RouterClock,
* which has access to a router's transports (particularly peer clock skews)
* to second-guess the sanity of clock adjustments.
*
*/
public class Clock implements Timestamper.UpdateListener {
protected final I2PAppContext _context;
protected final boolean _isSystemClockBad;
protected long _startedOn;
protected boolean _statCreated;
protected volatile long _offset;
protected boolean _alreadyChanged;
private final Set<ClockUpdateListener> _listeners;
public Clock(I2PAppContext context) {
_context = context;
_listeners = new CopyOnWriteArraySet<ClockUpdateListener>();
long now = System.currentTimeMillis();
long min = BuildTime.getEarliestTime();
long max = BuildTime.getLatestTime();
// If the system clock is obviously bad, set our offset so our time is something "close"
// We do not call setOffset() here as it sets _alreadyChanged.
// Don't use Log here.
if (now < min) {
// positive offset
_offset = min - now;
System.out.println("ERROR: System clock is invalid: " + new Date(now));
now = min;
_isSystemClockBad = true;
} else if (now > max) {
// negative offset
_offset = max - now;
System.out.println("ERROR: System clock is invalid: " + new Date(now));
now = max;
_isSystemClockBad = true;
} else {
_isSystemClockBad = false;
}
_startedOn = now;
}
public static Clock getInstance() {
return I2PAppContext.getGlobalContext().clock();
}
/**
* This is a dummy, see RouterClock and RouterTimestamper for the real thing
*/
public Timestamper getTimestamper() { return new Timestamper(); }
/** we fetch it on demand to avoid circular dependencies (logging uses the clock) */
protected Log getLog() { return _context.logManager().getLog(Clock.class); }
/** if the clock is skewed by 3+ days, forget it */
public final static long MAX_OFFSET = 3 * 24 * 60 * 60 * 1000;
/** after we've started up and shifted the clock, don't allow shifts of more than 10 minutes */
public final static long MAX_LIVE_OFFSET = 10 * 60 * 1000;
/** if the clock skewed changes by less than this, ignore the update (so we don't slide all over the place) */
public final static long MIN_OFFSET_CHANGE = 5 * 1000;
/**
* 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())
*/
public void setOffset(long offsetMs) {
setOffset(offsetMs, false);
}
/**
* 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.
*
* Warning - overridden in RouterClock
*
* @param offsetMs the delta from System.currentTimeMillis() (NOT the delta from now())
*/
public synchronized void setOffset(long offsetMs, boolean force) {
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, but you want to change it by "
+ delta + " to " + offsetMs + "? Did something break?");
return;
}
}
if ((delta < MIN_OFFSET_CHANGE) && (delta > 0 - MIN_OFFSET_CHANGE)) {
Log log = getLog();
if (log.shouldLog(Log.DEBUG))
log.debug("Not changing offset since it is only " + delta + "ms");
_alreadyChanged = true;
return;
}
}
if (_alreadyChanged) {
if (delta > 15*1000)
getLog().log(Log.CRIT, "Updating clock offset to " + offsetMs + "ms from " + _offset + "ms");
else if (getLog().shouldLog(Log.INFO))
getLog().info("Updating clock offset to " + offsetMs + "ms from " + _offset + "ms");
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, 0);
} else {
Log log = getLog();
if (log.shouldLog(Log.INFO))
log.info("Initializing clock offset to " + offsetMs + "ms from " + _offset + "ms");
}
_alreadyChanged = true;
_offset = offsetMs;
fireOffsetChanged(delta);
}
/*
* @return the current delta from System.currentTimeMillis() in milliseconds
*/
public synchronized long getOffset() {
return _offset;
}
public boolean getUpdatedSuccessfully() { return _alreadyChanged; }
public void setNow(long realTime) {
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);
}
/**
* Warning - overridden in RouterClock
*
* @param stratum ignored
* @since 0.7.12
*/
public void setNow(long realTime, int stratum) {
setNow(realTime);
}
/**
* Retrieve the current time synchronized with whatever reference clock is in
* use.
*
*/
public long now() {
return _offset + System.currentTimeMillis();
}
public void addUpdateListener(ClockUpdateListener lsnr) {
_listeners.add(lsnr);
}
public void removeUpdateListener(ClockUpdateListener lsnr) {
_listeners.remove(lsnr);
}
protected void fireOffsetChanged(long delta) {
for (ClockUpdateListener lsnr : _listeners) {
lsnr.offsetChanged(delta);
}
}
public interface ClockUpdateListener {
/**
* @param delta = (new offset - old offset),
* where each offset = (now() - System.currentTimeMillis())
*/
public void offsetChanged(long delta);
}
}