Router timestamper:

- Add country-to-continent mapping
 - Add continent pool.ntp.org zones as first fallback,
   this will improve time service for countries that don't have a zone
 - Don't start threads in constructors
 - Fix logging, better prevention of initialization loops
 - Log severe errors to wrapper log also
continent.txt file from http://dev.maxmind.com/geoip/legacy/codes/country_continent/
Creative Commons Attribution-ShareAlike 3.0 Unported License
http://dev.maxmind.com/geoip/legacy/geolite/
Terms already met in LICENSE.txt
This commit is contained in:
zzz
2015-04-21 14:22:05 +00:00
parent fd82fff07a
commit 1caf3e778b
7 changed files with 504 additions and 45 deletions

View File

@@ -40,7 +40,7 @@ public class RouterClock extends Clock {
/** use system time for this */
private long _lastChanged;
private int _lastStratum;
private final Timestamper _timeStamper;
private final RouterTimestamper _timeStamper;
/**
* If the system clock shifts by this much,
@@ -53,6 +53,9 @@ public class RouterClock extends Clock {
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;
@@ -62,6 +65,14 @@ public class RouterClock extends Clock {
_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
*/

View File

@@ -483,8 +483,11 @@ public class RouterContext extends I2PAppContext {
@Override
protected void initializeClock() {
synchronized (_lock1) {
if (_clock == null)
_clock = new RouterClock(this);
if (_clock == null) {
RouterClock rc = new RouterClock(this);
rc.start();
_clock = rc;
}
_clockInitialized = true;
}
}

View File

@@ -20,9 +20,10 @@ import net.i2p.util.Log;
*/
public class RouterTimestamper extends Timestamper {
private final I2PAppContext _context;
// warning, may be null
private Log _log;
private final List<String> _servers;
private List<String> _priorityServers;
private List<List<String>> _priorityServers;
private final List<UpdateListener> _listeners;
private int _queryFrequency;
private int _concurringServers;
@@ -33,11 +34,12 @@ public class RouterTimestamper extends Timestamper {
private boolean _wellSynced;
private volatile boolean _isRunning;
private Thread _timestamperThread;
private final Zones _zones;
private static final int MIN_QUERY_FREQUENCY = 5*60*1000;
private static final int DEFAULT_QUERY_FREQUENCY = 11*60*1000;
private static final String DEFAULT_SERVER_LIST = "0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org";
private static final String DEFAULT_DISABLED = "true";
private static final boolean DEFAULT_DISABLED = true;
/** how many times do we have to query if we are changing the clock? */
private static final int DEFAULT_CONCURRING_SERVERS = 3;
private static final int MAX_CONSECUTIVE_FAILS = 10;
@@ -51,13 +53,23 @@ public class RouterTimestamper extends Timestamper {
/** if different SNTP servers differ by more than 10s, someone is b0rked */
private static final int MAX_VARIANCE = 10*1000;
/**
* Does not start. Caller MUST call startTimestamper()
*/
public RouterTimestamper(I2PAppContext ctx) {
this(ctx, null, true);
}
/**
* Does not start. Caller MUST call startTimestamper()
*/
public RouterTimestamper(I2PAppContext ctx, UpdateListener lsnr) {
this(ctx, lsnr, true);
}
/**
* Does not start. Caller MUST call startTimestamper()
*/
public RouterTimestamper(I2PAppContext ctx, UpdateListener lsnr, boolean daemon) {
super();
// moved here to prevent problems with synchronized statements.
@@ -71,15 +83,17 @@ public class RouterTimestamper extends Timestamper {
// This means we no longer check every 5 minutes to see if we got enabled,
// so the property must be set at startup.
// We still need to be instantiated since the router calls clock().getTimestamper().waitForInitialization()
String disabled = ctx.getProperty(PROP_DISABLED, DEFAULT_DISABLED);
if (Boolean.parseBoolean(disabled)) {
_disabled = ctx.getProperty(PROP_DISABLED, DEFAULT_DISABLED);
if (_disabled) {
_initialized = true;
_zones = null;
System.out.println("Warning: NTP is disabled");
return;
}
if (lsnr != null)
_listeners.add(lsnr);
_zones = new Zones(ctx);
updateConfig();
startTimestamper();
}
public int getServerCount() {
@@ -110,7 +124,9 @@ public class RouterTimestamper extends Timestamper {
return _listeners.get(index);
}
private void startTimestamper() {
public void startTimestamper() {
if (_disabled || _initialized)
return;
_timestamperThread = new I2PThread(this, "Timestamper", _daemon);
_timestamperThread.setPriority(I2PThread.MIN_PRIORITY);
_isRunning = true;
@@ -149,48 +165,72 @@ public class RouterTimestamper extends Timestamper {
@Override
public void run() {
try { Thread.sleep(1000); } catch (InterruptedException ie) {}
_log = _context.logManager().getLog(Timestamper.class);
if (_log.shouldLog(Log.INFO))
_log.info("Starting timestamper");
boolean lastFailed = false;
try {
while (_isRunning) {
// NOTE: _log is null the first time through, to prevent problems and stack overflows
updateConfig();
if (!_disabled) {
// first the servers for our country, if we know what country we're in...
// first the servers for our country and continent, we know what country we're in...
if (_priorityServers != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Querying servers " + _priorityServers);
try {
lastFailed = !queryTime(_priorityServers.toArray(new String[_priorityServers.size()]));
} catch (IllegalArgumentException iae) {
if ( (!lastFailed) && (_log.shouldLog(Log.WARN)) )
_log.warn("Unable to reach country-specific NTP servers");
lastFailed = true;
for (List<String> servers : _priorityServers) {
if (_log != null && _log.shouldDebug())
_log.debug("Querying servers " + servers);
try {
lastFailed = !queryTime(servers.toArray(new String[servers.size()]));
} catch (IllegalArgumentException iae) {
if (!lastFailed && _log != null && _log.shouldWarn())
_log.warn("Unable to reach any regional NTP servers: " + servers);
lastFailed = true;
}
if (!lastFailed)
break;
}
}
// ... and then the global list, if that failed
if (_priorityServers == null || lastFailed) {
if (_log.shouldLog(Log.DEBUG))
if (_log != null && _log.shouldDebug())
_log.debug("Querying servers " + _servers);
try {
lastFailed = !queryTime(_servers.toArray(new String[_servers.size()]));
} catch (IllegalArgumentException iae) {
if ( (!_initialized) && (_log.shouldLog(Log.ERROR)) ) {
List<String> all = new ArrayList<String>();
if (_priorityServers != null)
all.addAll(_priorityServers);
all.addAll(_servers);
_log.error("Unable to reach any of the NTP servers " + all + " - network disconnected? Or set time.sntpServerList=myserver1.com,myserver2.com in advanced configuration.");
}
lastFailed = true;
}
}
}
_initialized = true;
boolean wasInitialized = _initialized;
if (!wasInitialized)
_initialized = true;
synchronized (this) { notifyAll(); }
if (!wasInitialized) {
// let the log manager get initialized
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
// NOW we set up logging
_log = _context.logManager().getLog(RouterTimestamper.class);
if (lastFailed) {
List<String> all = new ArrayList<String>(9);
if (_priorityServers != null) {
for (List<String> servers : _priorityServers) {
all.addAll(servers);
}
}
all.addAll(_servers);
String msg = "Unable to reach any of the NTP servers " + all +
" - network disconnected? Or set time.sntpServerList=myserver1.com,myserver2.com in advanced configuration.";
_log.logAlways(Log.WARN, msg);
System.out.println("Warning: " + msg);
} else if (_log.shouldDebug()) {
_log.debug("NTP initialization successful");
int i = 1;
if (_priorityServers != null) {
for (List<String> servers : _priorityServers) {
_log.debug("NTP Server list " + (i++) + ": " + servers);
}
}
_log.debug("NTP Server list " + i + ": " + _servers);
}
}
long sleepTime;
if (lastFailed) {
if (++_consecutiveFails >= MAX_CONSECUTIVE_FAILS)
@@ -206,8 +246,10 @@ public class RouterTimestamper extends Timestamper {
try { Thread.sleep(sleepTime); } catch (InterruptedException ie) {}
}
} catch (Throwable t) {
_log.log(Log.CRIT, "Timestamper died!", t);
synchronized (this) { notifyAll(); }
if (_log != null)
_log.log(Log.CRIT, "Timestamper died!", t);
t.printStackTrace();
}
}
@@ -232,7 +274,7 @@ public class RouterTimestamper extends Timestamper {
found[i] = delta;
if (i == 0) {
if (Math.abs(delta) < MAX_VARIANCE) {
if (_log.shouldLog(Log.INFO))
if (_log != null && _log.shouldInfo())
_log.info("a single SNTP query was within the tolerance (" + delta + "ms)");
// If less than a half second on the first try, we're in good shape
_wellSynced = Math.abs(delta) < 500;
@@ -243,7 +285,7 @@ public class RouterTimestamper extends Timestamper {
}
} else {
if (Math.abs(delta - expectedDelta) > MAX_VARIANCE) {
if (_log.shouldLog(Log.ERROR)) {
if (_log != null && _log.shouldError()) {
StringBuilder err = new StringBuilder(96);
err.append("SNTP client variance exceeded at query ").append(i);
err.append(". expected = ");
@@ -260,7 +302,7 @@ public class RouterTimestamper extends Timestamper {
}
}
stampTime(now, stratum);
if (_log.shouldLog(Log.DEBUG)) {
if (_log != null && _log.shouldDebug()) {
StringBuilder buf = new StringBuilder(64);
buf.append("Deltas: ");
for (int i = 0; i < found.length; i++)
@@ -280,13 +322,13 @@ public class RouterTimestamper extends Timestamper {
for (UpdateListener lsnr : _listeners) {
lsnr.setNow(now, stratum);
}
if (_log.shouldLog(Log.DEBUG))
if (_log != null && _log.shouldDebug())
_log.debug("Stamped the time as " + now + " (delta=" + (now-before) + ")");
}
/**
* Reload all the config elements from the appContext
*
* Reload all the config elements from the appContext.
* No logging allowed here
*/
private void updateConfig() {
String serverList = _context.getProperty(PROP_SERVER_LIST);
@@ -298,10 +340,22 @@ public class RouterTimestamper extends Timestamper {
if (country != null)
country = country.toLowerCase(Locale.US);
}
if (country != null && country.length() > 0) {
_priorityServers = new ArrayList<String>(3);
for (int i = 0; i < 3; i++)
_priorityServers.add(i + "." + country + ".pool.ntp.org");
if (country != null && country.length() > 0 &&
!country.equals("a1") && !country.equals("a2")) {
_priorityServers = new ArrayList<List<String>>(2);
List<String> p1 = new ArrayList<String>(3);
for (int i = 0; i < 3; i++) {
p1.add(i + "." + country + ".pool.ntp.org");
}
_priorityServers.add(p1);
String zone = _zones.getZone(country);
if (zone != null) {
List<String> p2 = new ArrayList<String>(3);
for (int i = 0; i < 3; i++) {
p2.add(i + "." + zone + ".pool.ntp.org");
}
_priorityServers.add(p2);
}
} else {
_priorityServers = null;
}
@@ -320,8 +374,7 @@ public class RouterTimestamper extends Timestamper {
_queryFrequency = Math.max(MIN_QUERY_FREQUENCY,
_context.getProperty(PROP_QUERY_FREQUENCY, DEFAULT_QUERY_FREQUENCY));
String disabled = _context.getProperty(PROP_DISABLED, DEFAULT_DISABLED);
_disabled = Boolean.parseBoolean(disabled);
_disabled = _context.getProperty(PROP_DISABLED, DEFAULT_DISABLED);
_concurringServers = Math.min(4, Math.max(1,
_context.getProperty(PROP_CONCURRING_SERVERS, DEFAULT_CONCURRING_SERVERS)));

View File

@@ -0,0 +1,135 @@
package net.i2p.router.time;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import net.i2p.I2PAppContext;
import net.i2p.router.transport.GeoIP;
/**
* Country to continent mapping for NTP.
* @since 0.9.20
*/
class Zones {
private final I2PAppContext _context;
// can't log here, called from RouterClock constructor, stack overflow
//private final Log _log;
// lower case country to NTP region
private final Map<String, String> _countryToZone;
// upper case continent to NTP region
private final Map<String, String> _continentToZone;
private static final String CONTINENT_FILE_DEFAULT = "continents.txt";
/**
* ref: http://dev.maxmind.com/geoip/legacy/codes/country_continent/
* ref: http://www.pool.ntp.org/zone/@
*/
private static final String[] ZONES = {
// not an NTP zone
//"--", "anonymous-proxy",
"AF", "africa",
// not an NTP zone
//"AN", "antarctica",
"AS", "asia",
"EU", "europe",
"NA", "north-america",
"OC", "oceania",
"SA", "south-america"
};
/**
* Reads in the file in the constructor,
* so hold onto this.
*/
public Zones(I2PAppContext ctx) {
_context = ctx;
//_log = ctx.logManager().getLog(Zones.class);
_countryToZone = new HashMap<String, String>(256);
_continentToZone = new HashMap<String, String>(8);
for (int i = 0; i < ZONES.length; i += 2) {
_continentToZone.put(ZONES[i], ZONES[i+1]);
}
readContinentFile();
}
/**
* Get the NTP zone for a country
*
* @param country non-null, two letter code, case-independent
* @return lower-case NTP zone, e.g. "africa", or null
*/
public String getZone (String country) {
return _countryToZone.get(country.toLowerCase(Locale.US));
}
/**
* Read in and parse the continent file.
* The file need not be sorted.
*
* Format:
* #comment (# must be in column 1)
* country code,continent code
*
* Example:
* US,NA
*
* Modified from GeoIP.readCountryFile()
* ref: http://dev.maxmind.com/geoip/legacy/codes/country_continent/
*/
private void readContinentFile() {
String geoDir = _context.getProperty(GeoIP.PROP_GEOIP_DIR, GeoIP.GEOIP_DIR_DEFAULT);
File geoFile = new File(geoDir);
if (!geoFile.isAbsolute())
geoFile = new File(_context.getBaseDir(), geoDir);
geoFile = new File(geoFile, CONTINENT_FILE_DEFAULT);
if (!geoFile.exists()) {
//if (_log.shouldWarn())
// _log.warn("Continent file not found: " + geoFile.getAbsolutePath());
return;
}
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(
new FileInputStream(geoFile), "UTF-8"));
String line = null;
while ((line = br.readLine()) != null) {
try {
if (line.charAt(0) == '#')
continue;
String[] s = line.split(",");
String ucContinent = s[1].toUpperCase(Locale.US).trim();
String zone = _continentToZone.get(ucContinent);
if (zone == null)
continue;
String lcCountry = s[0].toLowerCase(Locale.US).trim();
_countryToZone.put(lcCountry, zone);
//if (_log.shouldDebug())
// _log.debug("Country " + lcCountry + " is in " + zone);
} catch (IndexOutOfBoundsException ioobe) {}
}
} catch (IOException ioe) {
System.out.println("Error reading the continent file " + geoFile.getAbsolutePath());
} finally {
if (br != null) try { br.close(); } catch (IOException ioe) {}
}
}
/****
public static void main(String args[]) {
Zones z = new Zones(I2PAppContext.getGlobalContext());
String tests[] = {"us", "US", "nz", "fr", "vU", "br", "cn", "ao", "A1", "foo" };
for (int i = 0; i < tests.length; i++) {
System.out.println(tests[i] + " : " + z.getZone(tests[i]));
}
}
****/
}

View File

@@ -73,7 +73,7 @@ public class GeoIP {
static final String PROP_GEOIP_ENABLED = "routerconsole.geoip.enable";
public static final String PROP_GEOIP_DIR = "geoip.dir";
static final String GEOIP_DIR_DEFAULT = "geoip";
public static final String GEOIP_DIR_DEFAULT = "geoip";
static final String GEOIP_FILE_DEFAULT = "geoip.txt";
static final String COUNTRY_FILE_DEFAULT = "countries.txt";
public static final String PROP_IP_COUNTRY = "i2np.lastCountry";