Commit
This commit is contained in:
263
Carthage/Checkouts/TrueTime.swift/Sources/NTPClient.swift
vendored
Normal file
263
Carthage/Checkouts/TrueTime.swift/Sources/NTPClient.swift
vendored
Normal file
@ -0,0 +1,263 @@
|
||||
//
|
||||
// NTPClient.swift
|
||||
// TrueTime
|
||||
//
|
||||
// Created by Michael Sanders on 10/12/16.
|
||||
// Copyright © 2016 Instacart. All rights reserved.
|
||||
//
|
||||
|
||||
struct NTPConfig {
|
||||
let timeout: TimeInterval
|
||||
let maxRetries: Int
|
||||
let maxConnections: Int
|
||||
let maxServers: Int
|
||||
let numberOfSamples: Int
|
||||
let pollInterval: TimeInterval
|
||||
}
|
||||
|
||||
final class NTPClient {
|
||||
let config: NTPConfig
|
||||
init(config: NTPConfig) {
|
||||
self.config = config
|
||||
}
|
||||
|
||||
func start(pool: [String], port: Int) {
|
||||
precondition(!pool.isEmpty, "Must include at least one pool URL")
|
||||
queue.async {
|
||||
precondition(self.reachability.callback == nil, "Already started")
|
||||
self.pool = pool
|
||||
self.port = port
|
||||
self.reachability.callbackQueue = self.queue
|
||||
self.reachability.callback = self.updateReachability
|
||||
self.reachability.startMonitoring()
|
||||
self.startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
func pause() {
|
||||
queue.async {
|
||||
self.cancelTimer()
|
||||
self.reachability.stopMonitoring()
|
||||
self.reachability.callback = nil
|
||||
self.stopQueue()
|
||||
}
|
||||
}
|
||||
|
||||
func fetchIfNeeded(queue callbackQueue: DispatchQueue,
|
||||
first: ReferenceTimeCallback?,
|
||||
completion: ReferenceTimeCallback?) {
|
||||
queue.async {
|
||||
precondition(self.reachability.callback != nil,
|
||||
"Must start client before retrieving time")
|
||||
if let time = self.referenceTime {
|
||||
callbackQueue.async { first?(.success(time)) }
|
||||
} else if let first = first {
|
||||
self.startCallbacks.append((callbackQueue, first))
|
||||
}
|
||||
|
||||
if let time = self.referenceTime, self.finished {
|
||||
callbackQueue.async { completion?(.success(time)) }
|
||||
} else {
|
||||
if let completion = completion {
|
||||
self.completionCallbacks.append((callbackQueue, completion))
|
||||
}
|
||||
self.updateReachability(status: self.reachability.status ?? .notReachable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let referenceTimeLock: GCDLock<ReferenceTime?> = GCDLock(value: nil)
|
||||
var referenceTime: ReferenceTime? {
|
||||
get { return referenceTimeLock.read() }
|
||||
set { referenceTimeLock.write(newValue) }
|
||||
}
|
||||
|
||||
fileprivate func debugLog(_ message: @autoclosure () -> String) {
|
||||
#if DEBUG_LOGGING
|
||||
logger?(message())
|
||||
#endif
|
||||
}
|
||||
|
||||
var logger: LogCallback? = defaultLogger
|
||||
private let queue = DispatchQueue(label: "com.instacart.ntp.client")
|
||||
private let reachability = Reachability()
|
||||
private var completionCallbacks: [(DispatchQueue, ReferenceTimeCallback)] = []
|
||||
private var connections: [NTPConnection] = []
|
||||
private var finished: Bool = false
|
||||
private var invalidated: Bool = false
|
||||
private var startCallbacks: [(DispatchQueue, ReferenceTimeCallback)] = []
|
||||
private var startTime: TimeInterval?
|
||||
private var timer: DispatchSourceTimer?
|
||||
private var port: Int = 123
|
||||
private var pool: [String] = [] {
|
||||
didSet { invalidate() }
|
||||
}
|
||||
}
|
||||
|
||||
private extension NTPClient {
|
||||
var started: Bool { return startTime != nil }
|
||||
func updateReachability(status: ReachabilityStatus) {
|
||||
switch status {
|
||||
case .notReachable:
|
||||
debugLog("Network unreachable")
|
||||
cancelTimer()
|
||||
finish(.failure(NSError(trueTimeError: .offline)))
|
||||
case .reachableViaWWAN, .reachableViaWiFi:
|
||||
debugLog("Network reachable")
|
||||
startTimer()
|
||||
startPool(pool: pool, port: port)
|
||||
}
|
||||
}
|
||||
|
||||
func startTimer() {
|
||||
cancelTimer()
|
||||
if let referenceTime = referenceTime {
|
||||
let remainingInterval = max(0, config.pollInterval - referenceTime.underlyingValue.uptimeInterval)
|
||||
timer = DispatchSource.makeTimerSource(flags: [], queue: queue)
|
||||
timer?.setEventHandler(handler: invalidate)
|
||||
timer?.schedule(deadline: .now() + remainingInterval)
|
||||
timer?.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func cancelTimer() {
|
||||
timer?.cancel()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
func startPool(pool: [String], port: Int) {
|
||||
guard !started && !finished else {
|
||||
debugLog("Already \(started ? "started" : "finished")")
|
||||
return
|
||||
}
|
||||
|
||||
startTime = CFAbsoluteTimeGetCurrent()
|
||||
debugLog("Resolving pool: \(pool)")
|
||||
HostResolver.resolve(hosts: pool.map { ($0, port) },
|
||||
timeout: config.timeout,
|
||||
logger: logger,
|
||||
callbackQueue: queue) { resolver, result in
|
||||
guard self.started && !self.finished else {
|
||||
self.debugLog("Got DNS response after queue stopped: \(resolver), \(result)")
|
||||
return
|
||||
}
|
||||
guard pool == self.pool, port == self.port else {
|
||||
self.debugLog("Got DNS response after pool URLs changed: \(resolver), \(result)")
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case let .success(addresses): self.query(addresses: addresses, host: resolver.host)
|
||||
case let .failure(error): self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopQueue() {
|
||||
debugLog("Stopping queue")
|
||||
startTime = nil
|
||||
connections.forEach { $0.close(waitUntilFinished: true) }
|
||||
connections = []
|
||||
}
|
||||
|
||||
func invalidate() {
|
||||
stopQueue()
|
||||
finished = false
|
||||
if let referenceTime = referenceTime,
|
||||
reachability.status != .notReachable && !pool.isEmpty {
|
||||
debugLog("Invalidated time \(referenceTime.debugDescription)")
|
||||
startPool(pool: pool, port: port)
|
||||
}
|
||||
}
|
||||
|
||||
func query(addresses: [SocketAddress], host: String) {
|
||||
var results: [String: [FrozenNetworkTimeResult]] = [:]
|
||||
connections = NTPConnection.query(addresses: addresses,
|
||||
config: config,
|
||||
logger: logger,
|
||||
callbackQueue: queue) { connection, result in
|
||||
guard self.started && !self.finished else {
|
||||
self.debugLog("Got NTP response after queue stopped: \(result)")
|
||||
return
|
||||
}
|
||||
|
||||
let host = connection.address.host
|
||||
results[host] = (results[host] ?? []) + [result]
|
||||
|
||||
let responses = Array(results.values)
|
||||
let sampleSize = responses.map { $0.count }.reduce(0, +)
|
||||
let expectedCount = addresses.count * self.config.numberOfSamples
|
||||
let atEnd = sampleSize == expectedCount
|
||||
let times = responses.map { results in
|
||||
results.compactMap { try? $0.get() }
|
||||
}
|
||||
|
||||
self.debugLog("Got \(sampleSize) out of \(expectedCount)")
|
||||
if let time = bestTime(fromResponses: times) {
|
||||
let time = FrozenNetworkTime(networkTime: time, sampleSize: sampleSize, host: host)
|
||||
self.debugLog("\(atEnd ? "Final" : "Best") time: \(time), " +
|
||||
"δ: \(time.serverResponse.delay), " +
|
||||
"θ: \(time.serverResponse.offset)")
|
||||
|
||||
let referenceTime = self.referenceTime ?? ReferenceTime(time)
|
||||
if self.referenceTime == nil {
|
||||
self.referenceTime = referenceTime
|
||||
} else {
|
||||
referenceTime.underlyingValue = time
|
||||
}
|
||||
|
||||
if atEnd {
|
||||
self.finish(.success(referenceTime))
|
||||
} else {
|
||||
self.updateProgress(.success(referenceTime))
|
||||
}
|
||||
} else if atEnd {
|
||||
let error: NSError
|
||||
if case let .failure(failure) = result {
|
||||
error = failure as NSError
|
||||
} else {
|
||||
error = NSError(trueTimeError: .noValidPacket)
|
||||
}
|
||||
|
||||
self.finish(ReferenceTimeResult.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateProgress(_ result: ReferenceTimeResult) {
|
||||
let endTime = CFAbsoluteTimeGetCurrent()
|
||||
let hasStartCallbacks = !startCallbacks.isEmpty
|
||||
startCallbacks.forEach { queue, callback in
|
||||
queue.async {
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
startCallbacks = []
|
||||
if hasStartCallbacks {
|
||||
logDuration(endTime, to: "get first result")
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(Notification(name: .TrueTimeUpdated, object: self, userInfo: nil))
|
||||
}
|
||||
|
||||
func finish(_ result: ReferenceTimeResult) {
|
||||
let endTime = CFAbsoluteTimeGetCurrent()
|
||||
updateProgress(result)
|
||||
completionCallbacks.forEach { queue, callback in
|
||||
queue.async {
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
completionCallbacks = []
|
||||
logDuration(endTime, to: "get last result")
|
||||
finished = (try? result.get()) != nil
|
||||
stopQueue()
|
||||
startTimer()
|
||||
}
|
||||
|
||||
func logDuration(_ endTime: CFAbsoluteTime, to description: String) {
|
||||
if let startTime = startTime {
|
||||
debugLog("Took \(endTime - startTime)s to \(description)")
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user