From 5d3dee38dbb7d5e9cb6d08c5b935170fc38240d7 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Thu, 13 Oct 2016 13:42:42 -0700 Subject: [PATCH 1/9] chore: update gradle build dependencies https://github.com/dcendents/android-maven-gradle-plugin --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index c8f3e08d..b048ef72 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.1.3' - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.4' + classpath 'com.android.tools.build:gradle:2.2.1' + classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } From 57d5e7c569f7423beb110f215f1451ac234d33ad Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Fri, 14 Oct 2016 15:02:55 -0700 Subject: [PATCH 2/9] fix: add guard for time elapsed since request if a huge enough timeout is set, we might get responses that are super long ago --- .../java/com/instacart/library/truetime/SntpClient.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/src/main/java/com/instacart/library/truetime/SntpClient.java b/library/src/main/java/com/instacart/library/truetime/SntpClient.java index 28e472fa..bbf42515 100644 --- a/library/src/main/java/com/instacart/library/truetime/SntpClient.java +++ b/library/src/main/java/com/instacart/library/truetime/SntpClient.java @@ -134,6 +134,12 @@ void requestTime(String ntpHost, int timeoutInMillis) throws IOException { throw new InvalidNtpServerResponseException("Server response delay too large for comfort " + delay); } + long timeElapsedSinceRequest = Math.abs(originateTime - System.currentTimeMillis()); + if (timeElapsedSinceRequest >= 10_000) { + throw new InvalidNtpServerResponseException("Request was sent more than 10 seconds back " + + timeElapsedSinceRequest); + } + _sntpInitialized = true; TrueLog.i(TAG, "---- SNTP successful response from " + ntpHost); From f25d281d0799019b7242700d626603db8577dccf Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Fri, 14 Oct 2016 16:24:52 -0700 Subject: [PATCH 3/9] feat: return array with response data * we could use this for better instrumentation + logging * in service of NTP synchronization we'll need to pick best times using roundtrip delay --- .../library/truetime/SntpClient.java | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/com/instacart/library/truetime/SntpClient.java b/library/src/main/java/com/instacart/library/truetime/SntpClient.java index bbf42515..befaca51 100644 --- a/library/src/main/java/com/instacart/library/truetime/SntpClient.java +++ b/library/src/main/java/com/instacart/library/truetime/SntpClient.java @@ -28,6 +28,14 @@ */ public class SntpClient { + public static final int RESPONSE_INDEX_ORIGINATE_TIME = 0; + public static final int RESPONSE_INDEX_RECEIVE_TIME = 1; + public static final int RESPONSE_INDEX_TRANSMIT_TIME = 2; + public static final int RESPONSE_INDEX_RESPONSE_TIME = 3; + public static final int RESPONSE_INDEX_ROOT_DELAY = 4; + public static final int RESPONSE_INDEX_DISPERSION = 5; + public static final int RESPONSE_INDEX_STRATUM = 6; + private static final String TAG = SntpClient.class.getSimpleName(); private static final int NTP_PORT = 123; @@ -55,7 +63,7 @@ public class SntpClient { * @param ntpHost host name of the server. * @param timeoutInMillis network timeout in milliseconds. */ - void requestTime(String ntpHost, int timeoutInMillis) throws IOException { + long[] requestTime(String ntpHost, int timeoutInMillis) throws IOException { DatagramSocket socket = null; @@ -90,25 +98,32 @@ void requestTime(String ntpHost, int timeoutInMillis) throws IOException { // ----------------------------------------------------------------------------------- // extract the results + // See here for the algorithm used: + // https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm long originateTime = _readTimeStamp(buffer, INDEX_ORIGINATE_TIME); // T0 long receiveTime = _readTimeStamp(buffer, INDEX_RECEIVE_TIME); // T1 long transmitTime = _readTimeStamp(buffer, INDEX_TRANSMIT_TIME); // T2 - - // See here for the algorithm used: - // https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm long responseTime = requestTime + (responseTicks - requestTicks); // T3 + long t[] = new long[4]; + t[RESPONSE_INDEX_ORIGINATE_TIME] = originateTime; + t[RESPONSE_INDEX_RECEIVE_TIME] = receiveTime; + t[RESPONSE_INDEX_TRANSMIT_TIME] = transmitTime; + t[RESPONSE_INDEX_RESPONSE_TIME] = responseTime; + // ----------------------------------------------------------------------------------- // check validity of response long rootDelay = _read(buffer, INDEX_ROOT_DELAY); + t[RESPONSE_INDEX_ROOT_DELAY] = rootDelay; if (rootDelay > 100) { throw new InvalidNtpServerResponseException("Invalid response from NTP server. Root delay violation " + rootDelay); } long rootDispersion = _read(buffer, INDEX_ROOT_DISPERSION); + t[RESPONSE_INDEX_DISPERSION] = rootDispersion; if (rootDispersion > 100) { throw new InvalidNtpServerResponseException( "Invalid response from NTP server. Root dispersion violation " + rootDispersion); @@ -120,6 +135,7 @@ void requestTime(String ntpHost, int timeoutInMillis) throws IOException { } final int stratum = buffer[1] & 0xff; + t[RESPONSE_INDEX_STRATUM] = stratum; if (stratum < 1 || stratum > 15) { throw new InvalidNtpServerResponseException("untrusted stratum value for TrueTime: " + stratum); } @@ -145,11 +161,13 @@ void requestTime(String ntpHost, int timeoutInMillis) throws IOException { // ----------------------------------------------------------------------------------- // θ - long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2; + long clockOffset = getClockOffset(t); _cachedSntpTime = responseTime + clockOffset; _cachedDeviceUptime = responseTicks; + return t; + } catch (Exception e) { TrueLog.d(TAG, "---- SNTP request failed for " + ntpHost); throw e; @@ -160,6 +178,10 @@ void requestTime(String ntpHost, int timeoutInMillis) throws IOException { } } + boolean wasInitialized() { + return _sntpInitialized; + } + /** * @return time value computed from NTP server response */ @@ -174,10 +196,15 @@ long getCachedDeviceUptime() { return _cachedDeviceUptime; } - boolean wasInitialized() { - return _sntpInitialized; + long getClockOffset(long[] response) { + return ((response[RESPONSE_INDEX_RECEIVE_TIME] - response[RESPONSE_INDEX_ORIGINATE_TIME]) + + (response[RESPONSE_INDEX_TRANSMIT_TIME] - response[RESPONSE_INDEX_RESPONSE_TIME])) / 2; } + long getRoundTripDelay(long[] response) { + return (response[RESPONSE_INDEX_RESPONSE_TIME] - response[RESPONSE_INDEX_ORIGINATE_TIME]) - + (response[RESPONSE_INDEX_TRANSMIT_TIME] - response[RESPONSE_INDEX_RECEIVE_TIME]); + } // ----------------------------------------------------------------------------------- // private helpers From 86a0a9e1bac1755c2463e50d5b592515a20c855e Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Sat, 15 Oct 2016 00:18:40 -0700 Subject: [PATCH 4/9] ref: return actual long[] from NTP response --- .../library/truetime/SntpClient.java | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/library/src/main/java/com/instacart/library/truetime/SntpClient.java b/library/src/main/java/com/instacart/library/truetime/SntpClient.java index befaca51..7c2f71f3 100644 --- a/library/src/main/java/com/instacart/library/truetime/SntpClient.java +++ b/library/src/main/java/com/instacart/library/truetime/SntpClient.java @@ -35,6 +35,8 @@ public class SntpClient { public static final int RESPONSE_INDEX_ROOT_DELAY = 4; public static final int RESPONSE_INDEX_DISPERSION = 5; public static final int RESPONSE_INDEX_STRATUM = 6; + public static final int RESPONSE_INDEX_RESPONSE_TICKS = 7; + public static final int RESPONSE_INDEX_SIZE = 8; private static final String TAG = SntpClient.class.getSimpleName(); @@ -57,6 +59,24 @@ public class SntpClient { private long _cachedSntpTime; private boolean _sntpInitialized = false; + /** + * See δ : + * https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm + */ + public static long getRoundTripDelay(long[] response) { + return (response[RESPONSE_INDEX_RESPONSE_TIME] - response[RESPONSE_INDEX_ORIGINATE_TIME]) - + (response[RESPONSE_INDEX_TRANSMIT_TIME] - response[RESPONSE_INDEX_RECEIVE_TIME]); + } + + /** + * See θ : + * https://en.wikipedia.org/wiki/Network_Time_Protocol#Clock_synchronization_algorithm + */ + public static long getClockOffset(long[] response) { + return ((response[RESPONSE_INDEX_RECEIVE_TIME] - response[RESPONSE_INDEX_ORIGINATE_TIME]) + + (response[RESPONSE_INDEX_TRANSMIT_TIME] - response[RESPONSE_INDEX_RESPONSE_TIME])) / 2; + } + /** * Sends an NTP request to the given host and processes the response. * @@ -91,10 +111,12 @@ long[] requestTime(String ntpHost, int timeoutInMillis) throws IOException { // ----------------------------------------------------------------------------------- // read the response + long t[] = new long[RESPONSE_INDEX_SIZE]; DatagramPacket response = new DatagramPacket(buffer, buffer.length); socket.receive(response); long responseTicks = SystemClock.elapsedRealtime(); + t[RESPONSE_INDEX_RESPONSE_TICKS] = responseTicks; // ----------------------------------------------------------------------------------- // extract the results @@ -106,7 +128,6 @@ long[] requestTime(String ntpHost, int timeoutInMillis) throws IOException { long transmitTime = _readTimeStamp(buffer, INDEX_TRANSMIT_TIME); // T2 long responseTime = requestTime + (responseTicks - requestTicks); // T3 - long t[] = new long[4]; t[RESPONSE_INDEX_ORIGINATE_TIME] = originateTime; t[RESPONSE_INDEX_RECEIVE_TIME] = receiveTime; t[RESPONSE_INDEX_TRANSMIT_TIME] = transmitTime; @@ -160,12 +181,8 @@ long[] requestTime(String ntpHost, int timeoutInMillis) throws IOException { TrueLog.i(TAG, "---- SNTP successful response from " + ntpHost); // ----------------------------------------------------------------------------------- - // θ - long clockOffset = getClockOffset(t); - - _cachedSntpTime = responseTime + clockOffset; - _cachedDeviceUptime = responseTicks; - + // TODO: + cacheTrueTimeInfo(t); return t; } catch (Exception e) { @@ -178,6 +195,17 @@ long[] requestTime(String ntpHost, int timeoutInMillis) throws IOException { } } + void cacheTrueTimeInfo(long[] response) { + _cachedSntpTime = sntpTime(response); + _cachedDeviceUptime = response[RESPONSE_INDEX_RESPONSE_TICKS]; + } + + long sntpTime(long[] response) { + long clockOffset = getClockOffset(response); + long responseTime = response[RESPONSE_INDEX_RESPONSE_TIME]; + return responseTime + clockOffset; + } + boolean wasInitialized() { return _sntpInitialized; } @@ -196,15 +224,6 @@ long getCachedDeviceUptime() { return _cachedDeviceUptime; } - long getClockOffset(long[] response) { - return ((response[RESPONSE_INDEX_RECEIVE_TIME] - response[RESPONSE_INDEX_ORIGINATE_TIME]) + - (response[RESPONSE_INDEX_TRANSMIT_TIME] - response[RESPONSE_INDEX_RESPONSE_TIME])) / 2; - } - - long getRoundTripDelay(long[] response) { - return (response[RESPONSE_INDEX_RESPONSE_TIME] - response[RESPONSE_INDEX_ORIGINATE_TIME]) - - (response[RESPONSE_INDEX_TRANSMIT_TIME] - response[RESPONSE_INDEX_RECEIVE_TIME]); - } // ----------------------------------------------------------------------------------- // private helpers From d71f02b0d3d7a969cc07d9918ca0c13d4dbc3f99 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Sat, 15 Oct 2016 00:19:27 -0700 Subject: [PATCH 5/9] feat: implement NTP algo for TrueTime --- .../truetime/extensionrx/TrueTimeRx.java | 196 ++++++++++++++---- .../instacart/library/truetime/TrueTime.java | 21 +- 2 files changed, 171 insertions(+), 46 deletions(-) diff --git a/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java b/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java index abddf77f..da91a05d 100644 --- a/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java +++ b/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java @@ -1,11 +1,19 @@ package com.instacart.library.truetime.extensionrx; import android.content.Context; +import android.util.Log; +import com.instacart.library.truetime.SntpClient; import com.instacart.library.truetime.TrueTime; -import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.concurrent.Callable; import rx.Observable; +import rx.Observable.Transformer; import rx.functions.Action1; import rx.functions.Func1; import rx.schedulers.Schedulers; @@ -42,52 +50,156 @@ public TrueTimeRx withRetryCount(int retryCount) { } /** - * Initialize the SntpClient - * Issue SNTP call via UDP to list of provided hosts - * Pick the first successful call and return - * Retry failed calls individually + * Initialize TrueTime + * See {@link #initializeNtp(String)} + * + * @return accurate NTP Date */ public Observable initialize(final List ntpHosts) { - return Observable// - .from(ntpHosts)// - .flatMap(new Func1>() { + String ntpPool = "time.apple.com"; + + return initializeNtp(ntpPool)// + .map(new Func1() { @Override - public Observable call(String ntpHost) { - return Observable// - .just(ntpHost)// - .subscribeOn(Schedulers.io())// - .flatMap(new Func1>() { - @Override - public Observable call(String ntpHost) { - try { - initialize(ntpHost); - } catch (IOException e) { - return Observable.error(e); - } - return Observable.just(now()); - } - })// - .retry(_retryCount)// - .onErrorReturn(new Func1() { - @Override - public Date call(Throwable throwable) { - throwable.printStackTrace(); - return null; - } - }).take(1).doOnNext(new Action1() { - @Override - public void call(Date date) { - cacheTrueTimeInfo(); - } - }); + public Date call(long[] longs) { + return now(); } - })// - .filter(new Func1() { + }); + } + + /** + * Initialize TrueTime + * A single NTP pool server is provided. Using DNS we resolve that to multiple IP hosts + * Against each IP host we issue a UDP call and retrieve the best response using the NTP algorithm + * + * Use this instead of {@link #initialize(List)} if you wish to also get additional info for + * instrumentation/tracking actual NTP response data + * + * @param ntpPool NTP pool server e.g. time.apple.com, 0.us.pool.ntp.org + * @return Observable of detailed long[] containing most important parts of the actual NTP response + * See RESPONSE_INDEX_ prefixes in {@link SntpClient} for details + */ + public Observable initializeNtp(String ntpPool) { + return Observable// + .just(ntpPool)// + .compose(resolveNtpPoolToIpAddresses())// + .flatMap(bestResponseAgainstSingleIp(5)) // get best response from querying the ip 5 times + .take(5) // take 5 of the best results + .toList()// + .map(filterMedianResponse())// + .doOnNext(new Action1() { @Override - public Boolean call(Date date) { - return date != null; + public void call(long[] ntpResponse) { + cacheTrueTimeInfo(ntpResponse); + saveTrueTimeInfoToDisk(); } - })// - .take(1); + }); + } + + private Transformer resolveNtpPoolToIpAddresses() { + return new Transformer() { + @Override + public Observable call(Observable ntpPoolObservable) { + return ntpPoolObservable// + .observeOn(Schedulers.io())// + .flatMap(new Func1>() { + @Override + public Observable call(String ntpPoolAddress) { + try { + Log.d("kg", "---- resolving ntpHost : " + ntpPoolAddress); + return Observable.from(InetAddress.getAllByName(ntpPoolAddress)); + } catch (UnknownHostException e) { + return Observable.error(e); + } + } + })// + .map(new Func1() { + @Override + public String call(InetAddress inetAddress) { + Log.d("kg", "---- ntphost [" + + inetAddress.getHostName() + + "] : " + + inetAddress.getHostAddress()); + return inetAddress.getHostAddress(); + } + }); + } + }; + } + + private Func1> bestResponseAgainstSingleIp(final int repeatCount) { + return new Func1>() { + @Override + public Observable call(String singleIp) { + return Observable.just(singleIp)// + .repeat(repeatCount)// + .flatMap(new Func1>() { + @Override + public Observable call(final String singleIpHostAddress) { + return Observable// + .fromCallable(new Callable() { + @Override + public long[] call() throws Exception { + Log.d("kg", "---- requestTime from: " + singleIpHostAddress); + return requestTime(singleIpHostAddress); + } + })// + .subscribeOn(Schedulers.io())// + .doOnError(new Action1() { + @Override + public void call(Throwable throwable) { + Log.e("kg", "---- Error requesting time", throwable); + } + })// + .retry(_retryCount); + } + })// + .toList()// + .onErrorResumeNext(Observable.>empty()) + .map(filterLeastRoundTripDelay()); // pick best response for each ip + } + }; + } + + private Func1, long[]> filterLeastRoundTripDelay() { + return new Func1, long[]>() { + @Override + public long[] call(List responseTimeList) { + + Log.d("kg", "---- filterLeastRoundTrip: " + responseTimeList); + + Collections.sort(responseTimeList, new Comparator() { + @Override + public int compare(long[] lhsParam, long[] rhsLongParam) { + long lhs = SntpClient.getRoundTripDelay(lhsParam); + long rhs = SntpClient.getRoundTripDelay(rhsLongParam); + return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); + } + }); + + return responseTimeList.get(0); + } + }; + } + + private Func1, long[]> filterMedianResponse() { + return new Func1, long[]>() { + @Override + public long[] call(List bestResponses) { + Collections.sort(bestResponses, new Comparator() { + @Override + public int compare(long[] lhsParam, long[] rhsParam) { + long lhs = SntpClient.getClockOffset(lhsParam); + long rhs = SntpClient.getClockOffset(rhsParam); + return lhs < rhs ? -1 : (lhs == rhs ? 0 : 1); + } + }); + + Log.d("kg", "---- bestResponses: " + bestResponses); + Log.d("kg", "---- bestResponse: " + Arrays.toString(bestResponses.get(bestResponses.size() / 2))); + + return bestResponses.get(bestResponses.size() / 2); + } + }; } } diff --git a/library/src/main/java/com/instacart/library/truetime/TrueTime.java b/library/src/main/java/com/instacart/library/truetime/TrueTime.java index 09674aa2..0e0c81fa 100644 --- a/library/src/main/java/com/instacart/library/truetime/TrueTime.java +++ b/library/src/main/java/com/instacart/library/truetime/TrueTime.java @@ -10,8 +10,8 @@ public class TrueTime { private static final String TAG = TrueTime.class.getSimpleName(); private static final TrueTime INSTANCE = new TrueTime(); - private static final SntpClient SNTP_CLIENT = new SntpClient(); private static final DiskCacheClient DISK_CACHE_CLIENT = new DiskCacheClient(); + protected static final SntpClient SNTP_CLIENT = new SntpClient(); private static int _udpSocketTimeoutInMillis = 30_000; @@ -47,7 +47,7 @@ public static void clearCachedInfo(Context context) { public void initialize() throws IOException { initialize(_ntpHost); - cacheTrueTimeInfo(); + saveTrueTimeInfoToDisk(); } /** @@ -81,10 +81,15 @@ protected void initialize(String ntpHost) throws IOException { TrueLog.i(TAG, "---- TrueTime already initialized from previous boot/init"); return; } - SNTP_CLIENT.requestTime(ntpHost, _udpSocketTimeoutInMillis); + + requestTime(ntpHost); + } + + protected long[] requestTime(String ntpHost) throws IOException { + return SNTP_CLIENT.requestTime(ntpHost, _udpSocketTimeoutInMillis); } - protected synchronized static void cacheTrueTimeInfo() { + protected synchronized static void saveTrueTimeInfoToDisk() { if (!SNTP_CLIENT.wasInitialized()) { TrueLog.i(TAG, "---- SNTP client not available. not caching TrueTime info in disk"); return; @@ -92,6 +97,14 @@ protected synchronized static void cacheTrueTimeInfo() { DISK_CACHE_CLIENT.cacheTrueTimeInfo(SNTP_CLIENT); } + protected void cacheTrueTimeInfo(long[] response) { + SNTP_CLIENT.cacheTrueTimeInfo(response); + } + + protected long sntpTime(long[] response) { + return SNTP_CLIENT.sntpTime(response); + } + private static long _getCachedDeviceUptime() { long cachedDeviceUptime = SNTP_CLIENT.wasInitialized() ? SNTP_CLIENT.getCachedDeviceUptime() From 3feb6053834dfb139b0cab1aad62711a373af462 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Sat, 15 Oct 2016 00:32:12 -0700 Subject: [PATCH 6/9] ref: cleanup logging + api for initialize --- .../library/sample/Sample2Activity.java | 4 +-- .../truetime/extensionrx/TrueTimeRx.java | 33 ++++++++----------- .../instacart/library/truetime/TrueLog.java | 2 +- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/instacart/library/sample/Sample2Activity.java b/app/src/main/java/com/instacart/library/sample/Sample2Activity.java index 229b9498..0310a6d3 100644 --- a/app/src/main/java/com/instacart/library/sample/Sample2Activity.java +++ b/app/src/main/java/com/instacart/library/sample/Sample2Activity.java @@ -55,7 +55,7 @@ protected void onCreate(Bundle savedInstanceState) { .withRetryCount(100) .withSharedPreferences(this) .withLoggingEnabled(true) - .initialize(ntpHosts) + .initializeRx(ntpHosts) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1() { @@ -66,7 +66,7 @@ public void call(Date date) { }, new Action1() { @Override public void call(Throwable throwable) { - Log.e(TAG, "something went wrong when trying to initialize TrueTime", throwable); + Log.e(TAG, "something went wrong when trying to initializeRx TrueTime", throwable); } }, new Action0() { @Override diff --git a/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java b/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java index da91a05d..c494b870 100644 --- a/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java +++ b/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java @@ -1,8 +1,8 @@ package com.instacart.library.truetime.extensionrx; import android.content.Context; -import android.util.Log; import com.instacart.library.truetime.SntpClient; +import com.instacart.library.truetime.TrueLog; import com.instacart.library.truetime.TrueTime; import java.net.InetAddress; import java.net.UnknownHostException; @@ -22,6 +22,7 @@ public class TrueTimeRx extends TrueTime { private static final TrueTimeRx RX_INSTANCE = new TrueTimeRx(); + private static final String TAG = TrueTimeRx.class.getSimpleName(); private int _retryCount = 50; @@ -51,13 +52,11 @@ public TrueTimeRx withRetryCount(int retryCount) { /** * Initialize TrueTime - * See {@link #initializeNtp(String)} + * See {@link #initializeNtp(String)} for details on working * * @return accurate NTP Date */ - public Observable initialize(final List ntpHosts) { - String ntpPool = "time.apple.com"; - + public Observable initializeRx(String ntpPool) { return initializeNtp(ntpPool)// .map(new Func1() { @Override @@ -69,10 +68,11 @@ public Date call(long[] longs) { /** * Initialize TrueTime - * A single NTP pool server is provided. Using DNS we resolve that to multiple IP hosts + * A single NTP pool server is provided. + * Using DNS we resolve that to multiple IP hosts * Against each IP host we issue a UDP call and retrieve the best response using the NTP algorithm * - * Use this instead of {@link #initialize(List)} if you wish to also get additional info for + * Use this instead of {@link #initializeRx(String)} if you wish to also get additional info for * instrumentation/tracking actual NTP response data * * @param ntpPool NTP pool server e.g. time.apple.com, 0.us.pool.ntp.org @@ -106,7 +106,7 @@ public Observable call(Observable ntpPoolObservable) { @Override public Observable call(String ntpPoolAddress) { try { - Log.d("kg", "---- resolving ntpHost : " + ntpPoolAddress); + TrueLog.d(TAG, "---- resolving ntpHost : " + ntpPoolAddress); return Observable.from(InetAddress.getAllByName(ntpPoolAddress)); } catch (UnknownHostException e) { return Observable.error(e); @@ -116,10 +116,7 @@ public Observable call(String ntpPoolAddress) { .map(new Func1() { @Override public String call(InetAddress inetAddress) { - Log.d("kg", "---- ntphost [" + - inetAddress.getHostName() + - "] : " + - inetAddress.getHostAddress()); + TrueLog.d(TAG, "---- resolved address [" + inetAddress + "]"); return inetAddress.getHostAddress(); } }); @@ -140,7 +137,7 @@ public Observable call(final String singleIpHostAddress) { .fromCallable(new Callable() { @Override public long[] call() throws Exception { - Log.d("kg", "---- requestTime from: " + singleIpHostAddress); + TrueLog.d(TAG, "---- requestTime from: " + singleIpHostAddress); return requestTime(singleIpHostAddress); } })// @@ -148,7 +145,7 @@ public long[] call() throws Exception { .doOnError(new Action1() { @Override public void call(Throwable throwable) { - Log.e("kg", "---- Error requesting time", throwable); + TrueLog.e(TAG, "---- Error requesting time", throwable); } })// .retry(_retryCount); @@ -165,9 +162,6 @@ private Func1, long[]> filterLeastRoundTripDelay() { return new Func1, long[]>() { @Override public long[] call(List responseTimeList) { - - Log.d("kg", "---- filterLeastRoundTrip: " + responseTimeList); - Collections.sort(responseTimeList, new Comparator() { @Override public int compare(long[] lhsParam, long[] rhsLongParam) { @@ -177,6 +171,8 @@ public int compare(long[] lhsParam, long[] rhsLongParam) { } }); + TrueLog.d(TAG, "---- filterLeastRoundTrip: " + responseTimeList); + return responseTimeList.get(0); } }; @@ -195,8 +191,7 @@ public int compare(long[] lhsParam, long[] rhsParam) { } }); - Log.d("kg", "---- bestResponses: " + bestResponses); - Log.d("kg", "---- bestResponse: " + Arrays.toString(bestResponses.get(bestResponses.size() / 2))); + TrueLog.d(TAG, "---- bestResponse: " + Arrays.toString(bestResponses.get(bestResponses.size() / 2))); return bestResponses.get(bestResponses.size() / 2); } diff --git a/library/src/main/java/com/instacart/library/truetime/TrueLog.java b/library/src/main/java/com/instacart/library/truetime/TrueLog.java index f9062e52..c7a421e6 100644 --- a/library/src/main/java/com/instacart/library/truetime/TrueLog.java +++ b/library/src/main/java/com/instacart/library/truetime/TrueLog.java @@ -2,7 +2,7 @@ import android.util.Log; -class TrueLog { +public class TrueLog { private static boolean LOGGING_ENABLED = true; From ff6452df1049e3d3fa7e60863d4da8a5191d1962 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Sat, 15 Oct 2016 00:37:38 -0700 Subject: [PATCH 7/9] ref: move TrueTimeRx to truetime package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ¯\_(ツ)_/¯ i don't know why i didn't start off doing this --- .../library/sample/Sample2Activity.java | 13 ++---------- .../{extensionrx => }/TrueTimeRx.java | 5 +---- .../instacart/library/truetime/TrueLog.java | 20 +++++++++---------- 3 files changed, 13 insertions(+), 25 deletions(-) rename library-extension-rx/src/main/java/com/instacart/library/truetime/{extensionrx => }/TrueTimeRx.java (97%) diff --git a/app/src/main/java/com/instacart/library/sample/Sample2Activity.java b/app/src/main/java/com/instacart/library/sample/Sample2Activity.java index 0310a6d3..1836e1e0 100644 --- a/app/src/main/java/com/instacart/library/sample/Sample2Activity.java +++ b/app/src/main/java/com/instacart/library/sample/Sample2Activity.java @@ -10,12 +10,10 @@ import butterknife.ButterKnife; import butterknife.OnClick; import com.instacart.library.truetime.TrueTime; -import com.instacart.library.truetime.extensionrx.TrueTimeRx; +import com.instacart.library.truetime.TrueTimeRx; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.Arrays; import java.util.Date; -import java.util.List; import java.util.Locale; import java.util.TimeZone; import rx.android.schedulers.AndroidSchedulers; @@ -41,13 +39,6 @@ protected void onCreate(Bundle savedInstanceState) { ButterKnife.bind(this); refreshBtn.setEnabled(false); - List ntpHosts = Arrays.asList("time.apple.com", - "0.north-america.pool.ntp.org", - "1.north-america.pool.ntp.org", - "2.north-america.pool.ntp.org", - "3.north-america.pool.ntp.org", - "0.us.pool.ntp.org", - "1.us.pool.ntp.org"); //TrueTimeRx.clearCachedInfo(this); TrueTimeRx.build() @@ -55,7 +46,7 @@ protected void onCreate(Bundle savedInstanceState) { .withRetryCount(100) .withSharedPreferences(this) .withLoggingEnabled(true) - .initializeRx(ntpHosts) + .initializeRx("time.apple.com") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1() { diff --git a/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java b/library-extension-rx/src/main/java/com/instacart/library/truetime/TrueTimeRx.java similarity index 97% rename from library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java rename to library-extension-rx/src/main/java/com/instacart/library/truetime/TrueTimeRx.java index c494b870..dff1ce47 100644 --- a/library-extension-rx/src/main/java/com/instacart/library/truetime/extensionrx/TrueTimeRx.java +++ b/library-extension-rx/src/main/java/com/instacart/library/truetime/TrueTimeRx.java @@ -1,9 +1,6 @@ -package com.instacart.library.truetime.extensionrx; +package com.instacart.library.truetime; import android.content.Context; -import com.instacart.library.truetime.SntpClient; -import com.instacart.library.truetime.TrueLog; -import com.instacart.library.truetime.TrueTime; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; diff --git a/library/src/main/java/com/instacart/library/truetime/TrueLog.java b/library/src/main/java/com/instacart/library/truetime/TrueLog.java index c7a421e6..55555a96 100644 --- a/library/src/main/java/com/instacart/library/truetime/TrueLog.java +++ b/library/src/main/java/com/instacart/library/truetime/TrueLog.java @@ -2,59 +2,59 @@ import android.util.Log; -public class TrueLog { +class TrueLog { private static boolean LOGGING_ENABLED = true; - public static void v(String tag, String msg) { + static void v(String tag, String msg) { if (LOGGING_ENABLED) { Log.v(tag, msg); } } - public static void d(String tag, String msg) { + static void d(String tag, String msg) { if (LOGGING_ENABLED) { Log.d(tag, msg); } } - public static void i(String tag, String msg) { + static void i(String tag, String msg) { if (LOGGING_ENABLED) { Log.i(tag, msg); } } - public static void w(String tag, String msg) { + static void w(String tag, String msg) { if (LOGGING_ENABLED) { Log.w(tag, msg); } } - public static void w(String tag, String msg, Throwable t) { + static void w(String tag, String msg, Throwable t) { if (LOGGING_ENABLED) { Log.w(tag, msg, t); } } - public static void e(String tag, String msg) { + static void e(String tag, String msg) { if (LOGGING_ENABLED) { Log.e(tag, msg); } } - public static void e(String tag, String msg, Throwable t) { + static void e(String tag, String msg, Throwable t) { if (LOGGING_ENABLED) { Log.e(tag, msg, t); } } - public static void wtf(String tag, String msg) { + static void wtf(String tag, String msg) { if (LOGGING_ENABLED) { Log.wtf(tag, msg); } } - public static void wtf(String tag, String msg, Throwable tr) { + static void wtf(String tag, String msg, Throwable tr) { if (LOGGING_ENABLED) { Log.wtf(tag, msg, tr); } From 8c8483cc185cdf47d4edd38682f9c948948f3817 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Sat, 15 Oct 2016 00:42:37 -0700 Subject: [PATCH 8/9] fix: cleanup encapsulation --- .../com/instacart/library/truetime/TrueTime.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/library/src/main/java/com/instacart/library/truetime/TrueTime.java b/library/src/main/java/com/instacart/library/truetime/TrueTime.java index 0e0c81fa..2fc7897d 100644 --- a/library/src/main/java/com/instacart/library/truetime/TrueTime.java +++ b/library/src/main/java/com/instacart/library/truetime/TrueTime.java @@ -11,7 +11,7 @@ public class TrueTime { private static final TrueTime INSTANCE = new TrueTime(); private static final DiskCacheClient DISK_CACHE_CLIENT = new DiskCacheClient(); - protected static final SntpClient SNTP_CLIENT = new SntpClient(); + private static final SntpClient SNTP_CLIENT = new SntpClient(); private static int _udpSocketTimeoutInMillis = 30_000; @@ -85,11 +85,11 @@ protected void initialize(String ntpHost) throws IOException { requestTime(ntpHost); } - protected long[] requestTime(String ntpHost) throws IOException { + long[] requestTime(String ntpHost) throws IOException { return SNTP_CLIENT.requestTime(ntpHost, _udpSocketTimeoutInMillis); } - protected synchronized static void saveTrueTimeInfoToDisk() { + synchronized static void saveTrueTimeInfoToDisk() { if (!SNTP_CLIENT.wasInitialized()) { TrueLog.i(TAG, "---- SNTP client not available. not caching TrueTime info in disk"); return; @@ -97,14 +97,10 @@ protected synchronized static void saveTrueTimeInfoToDisk() { DISK_CACHE_CLIENT.cacheTrueTimeInfo(SNTP_CLIENT); } - protected void cacheTrueTimeInfo(long[] response) { + void cacheTrueTimeInfo(long[] response) { SNTP_CLIENT.cacheTrueTimeInfo(response); } - protected long sntpTime(long[] response) { - return SNTP_CLIENT.sntpTime(response); - } - private static long _getCachedDeviceUptime() { long cachedDeviceUptime = SNTP_CLIENT.wasInitialized() ? SNTP_CLIENT.getCachedDeviceUptime() From 84032eb9bc9fe16de188f0c68e1fa06dbab3dc78 Mon Sep 17 00:00:00 2001 From: Kaushik Gopal Date: Sat, 15 Oct 2016 01:02:13 -0700 Subject: [PATCH 9/9] chore: update version and README --- README.md | 24 +++++++++++------------- library-extension-rx/build.gradle | 4 ++-- library/build.gradle | 4 ++-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index ad6c7598..1472ea51 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ *Make sure to check out our counterpart too: [TrueTime](https://github.com/instacart/TrueTime.swift), an SNTP library for Swift.* -SNTP client for Android. Calculate the date and time "now" impervious to manual changes to device clock time. +NTP client for Android. Calculate the date and time "now" impervious to manual changes to device clock time. In certain applications it becomes important to get the real or "true" date and time. On most devices, if the clock has been changed manually, then a `new Date()` instance gives you a time impacted by local settings. @@ -16,7 +16,7 @@ You can read more about the use case in our [blog post](https://tech.instacart.c It's pretty simple actually. We make a request to an NTP server that gives us the actual time. We then establish the delta between device uptime and uptime at the time of the network response. Each time "now" is requested subsequently, we account for that offset and return a corrected `Date` object. -Also, once we have this information it's valid until the next time you boot your device. This means if you enable the disk caching feature, after a signle successfull SNTP request you can use the information on disk directly without ever making another network request. This applies even across application kills which can happen frequently if your users have a memory starved device. +Also, once we have this information it's valid until the next time you boot your device. This means if you enable the disk caching feature, after a single successfull NTP request you can use the information on disk directly without ever making another network request. This applies even across application kills which can happen frequently if your users have a memory starved device. # Installation @@ -62,13 +62,11 @@ Date noReallyThisIsTheTrueDateAndTime = TrueTime.now(); ## Rx-ified Version -If you're down to using [RxJava](https://github.com/ReactiveX/RxJava) then there's a niftier `initialize()` api that takes in the pool of hosts you want to query. +If you're down to using [RxJava](https://github.com/ReactiveX/RxJava) then we go all the way and implement the full NTP. Use the nifty `initializeRx()` api which takes in an NTP pool server host. ```java -List ntpHosts = Arrays.asList("0.north-america.pool.ntp.org", - "1.north-america.pool.ntp.org"); TrueTimeRx.build() - .initialize(ntpHosts) + .initializeRx("0.north-america.pool.ntp.org") .subscribeOn(Schedulers.io()) .subscribe(date -> { Log.v(TAG, "TrueTime was initialized and we have a time: " + date); @@ -85,24 +83,24 @@ TrueTimeRx.now(); // return a Date object with the "true" time. ### What is nifty about the Rx version? -* it can take in multiple SNTP hosts to shoot out the UDP request -* those UDP requests are executed in parallel -* if one of the SNTP requests fail, we retry the failed request (alone) for a specified number of times -* as soon as we hear back from any of the hosts, we immediately take that and terminate the rest of the requests +* as against just SNTP, you get full NTP (read: far more accurate time) +* the NTP pool address you provide is resolved into multiple IP addresses +* we query each IP multiple times, guarding against checks, and taking the best response +* if any one of the requests fail, we retry that failed request (alone) for a specified number of times +* we collect all the responses and again filter for the best result as per the NTP spec ## Notes/tips: * Each `initialize` call makes an SNTP network request. TrueTime needs to be `initialize`d only once ever, per device boot. Use TrueTime's `withSharedPreferences` option to make use of this feature and avoid repeated network request calls. * Preferable use dependency injection (like [Dagger](http://square.github.io/dagger/)) and create a TrueTime @Singleton object -* TrueTime was built to be accurate "enough", hence the use of [SNTP](https://en.wikipedia.org/wiki/Network_Time_Protocol#SNTP). If you need exact millisecond accuracy then you probably want [NTP](https://www.meinbergglobal.com/english/faq/faq_37.htm) (i.e. SNTP + statistical analysis to ensure the reference time is exactly correct). TrueTime provides the building blocks for this. We welcome PRs if you think you can do this with TrueTime(Rx) pretty easily :). +* You can read up on Wikipedia the differences between [SNTP](https://en.wikipedia.org/wiki/Network_Time_Protocol#SNTP) and [NTP](https://www.meinbergglobal.com/english/faq/faq_37.htm). * TrueTime is also [available for iOS/Swift](https://github.com/instacart/truetime.swift) ## Exception handling: -* an `InvalidNtpServerResponseException` is thrown every time the server gets an invalid response (this can happen with the SNTP calls). +* an `InvalidNtpServerResponseException` is thrown every time the server gets an invalid response (this can happen with the individual SNTP calls). * If TrueTime fails to initialize (because of the above exception being throw), then an `IllegalStateException` is thrown if you try to call `TrueTime.now()` at a later point. - # License ``` diff --git a/library-extension-rx/build.gradle b/library-extension-rx/build.gradle index c5e9e0a7..4003dd65 100644 --- a/library-extension-rx/build.gradle +++ b/library-extension-rx/build.gradle @@ -8,8 +8,8 @@ android { defaultConfig { minSdkVersion 14 targetSdkVersion 23 - versionCode 5 - versionName "1.5" + versionCode 6 + versionName "2.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/library/build.gradle b/library/build.gradle index 333e7644..861ad0a6 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -8,8 +8,8 @@ android { defaultConfig { minSdkVersion 14 targetSdkVersion 23 - versionCode 5 - versionName "1.5" + versionCode 6 + versionName "2.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"