Files
Schedule-ICTIS/Carthage/Checkouts/TrueTime.swift/Sources/NTPClient.swift
Vladimir Dubovik 5abafda21b Commit
2025-06-13 11:59:16 +03:00

264 lines
9.1 KiB
Swift

//
// 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)")
}
}
}