Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUMM-1609 Use NTP server offset #607

Merged
merged 3 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions Sources/Datadog/Core/System/Time/DateCorrector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ internal class DateCorrector: DateCorrectorType {
self.serverDateProvider = serverDateProvider
serverDateProvider.synchronize(
with: DateCorrector.datadogNTPServers.randomElement()!, // swiftlint:disable:this force_unwrapping
completion: { serverTime in
let deviceTime = deviceDateProvider.currentDate()
if let serverTime = serverTime {
let difference = (serverTime.timeIntervalSince(deviceTime) * 1_000).rounded() / 1_000
completion: { offset in
if let offset = offset {
let difference = (offset * 1_000).rounded() / 1_000
userLogger.info(
"""
NTP time synchronization completed.
Server time will be used for signing events (current server time is \(serverTime); \(difference)s difference with device time).
Server time will be used for signing events (\(difference)s difference with device time).
"""
)
} else {
let deviceTime = deviceDateProvider.currentDate()
userLogger.warn(
"""
NTP time synchronization failed.
Expand All @@ -61,13 +61,10 @@ internal class DateCorrector: DateCorrectorType {
}

var currentCorrection: DateCorrection {
if let serverTime = serverDateProvider.currentDate() {
let deviceTime = deviceDateProvider.currentDate()
return DateCorrection(
serverTimeOffset: serverTime.timeIntervalSince(deviceTime)
)
} else {
guard let offset = serverDateProvider.offset else {
return DateCorrection(serverTimeOffset: 0)
}

return DateCorrection(serverTimeOffset: offset)
}
}
47 changes: 36 additions & 11 deletions Sources/Datadog/Core/System/Time/ServerDateProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,46 @@ import Kronos
/// Abstract the monotonic clock synchronized with the server using NTP.
internal protocol ServerDateProvider {
/// Start the clock synchronisation with NTP server.
/// Calls the `completion` by passing it the server time when the synchronization succeeds or`nil` if it fails.
func synchronize(with ntpPool: String, completion: @escaping (Date?) -> Void)
/// Returns the server time or `nil` if not yet determined.
/// This time gets more precise while synchronization is pending.
func currentDate() -> Date?
/// Calls the `completion` by passing it the server time offset when the synchronization succeeds or`nil` if it fails.
func synchronize(with pool: String, completion: @escaping (TimeInterval?) -> Void)
/// Returns the server time offset or `nil` if not yet determined.
/// This offset gets more precise while synchronization is pending.
var offset: TimeInterval? { get }
}

internal class NTPServerDateProvider: ServerDateProvider {
func synchronize(with ntpPool: String, completion: @escaping (Date?) -> Void) {
Clock.sync(from: ntpPool, completion: { serverTime, _ in
completion(serverTime)
})
/// Server offset publisher.
private let publisher: ValuePublisher<TimeInterval?> = ValuePublisher(initialValue: nil)

/// Returns the server time offset or `nil` if not yet determined.
/// This offset gets more precise while synchronization is pending.
var offset: TimeInterval? {
return publisher.currentValue
}

func currentDate() -> Date? {
return Clock.now
func synchronize(with pool: String, completion: @escaping (TimeInterval?) -> Void) {
Clock.sync(
from: pool,
first: { [weak self] _, offset in
self?.publisher.publishAsync(offset)
},
completion: { [weak self] now, offset in
// Kronos only notifies for the first and last samples.
// In case, the last sample does not return an offset, we calculate the offset
// from the returned `now` parameter. The `now` parameter in this callback
// is `Clock.now`, so it is possible to have `now` but not `offset`.
if let offset = offset {
self?.publisher.publishAsync(offset)
} else if let now = now {
self?.publisher.publishAsync(now.timeIntervalSinceNow)
}

completion(self?.publisher.currentValue)
}
)

// `Kronos.sync` first loads the previous state from the `UserDefaults` if any.
// We can invoke `Clock.now` to retrieve the stored offset.
publisher.publishAsync(Clock.now?.timeIntervalSinceNow)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,15 @@ import XCTest

private class ServerDateProviderMock: ServerDateProvider {
private(set) var synchronizedNTPPool: String? = nil
var serverTime: Date? = nil
var offset: TimeInterval? = nil

init(using serverTime: Date? = nil) {
self.serverTime = serverTime
init(using offset: TimeInterval? = nil) {
self.offset = offset
}

func synchronize(with ntpPool: String, completion: @escaping (Date?) -> Void) {
synchronizedNTPPool = ntpPool
completion(self.serverTime)
}

func currentDate() -> Date? {
return serverTime
func synchronize(with pool: String, completion: @escaping (TimeInterval?) -> Void) {
synchronizedNTPPool = pool
completion(self.offset)
}
}

Expand Down Expand Up @@ -57,8 +53,8 @@ class DateCorrectorTests: XCTestCase {
}

func testWhenNTPSynchronizationSucceeds_itPrintsInfoMessage() throws {
let serverDateProvider = ServerDateProviderMock(using: .mockDecember15th2019At10AMUTC())
let deviceDateProvider = RelativeDateProvider(using: .mockDecember15th2019At10AMUTC(addingTimeInterval: 1))
let serverDateProvider = ServerDateProviderMock(using: -1)
let deviceDateProvider = RelativeDateProvider(using: .mockRandomInThePast())

// When
_ = DateCorrector(deviceDateProvider: deviceDateProvider, serverDateProvider: serverDateProvider)
Expand All @@ -70,7 +66,7 @@ class DateCorrectorTests: XCTestCase {
log.message,
"""
NTP time synchronization completed.
Server time will be used for signing events (current server time is 2019-12-15 10:00:00 +0000; -1.0s difference with device time).
Server time will be used for signing events (-1.0s difference with device time).
"""
)
}
Expand Down Expand Up @@ -110,9 +106,7 @@ class DateCorrectorTests: XCTestCase {
let serverDateProvider = ServerDateProviderMock(using: .mockRandomInThePast())
let deviceDateProvider = RelativeDateProvider(using: .mockRandomInThePast())

func currentTimeDifference() -> TimeInterval {
serverDateProvider.currentDate()!.timeIntervalSince(deviceDateProvider.currentDate())
}
var serverOffset: TimeInterval { serverDateProvider.offset! }

// When
let corrector = DateCorrector(deviceDateProvider: deviceDateProvider, serverDateProvider: serverDateProvider)
Expand All @@ -121,7 +115,7 @@ class DateCorrectorTests: XCTestCase {
XCTAssertTrue(
datesEqual(
corrector.currentCorrection.applying(to: deviceDateProvider.currentDate()),
serverDateProvider.currentDate()!
deviceDateProvider.currentDate().addingTimeInterval(serverOffset)
),
"The device current time should be corrected to the server time."
)
Expand All @@ -130,16 +124,16 @@ class DateCorrectorTests: XCTestCase {
XCTAssertTrue(
datesEqual(
corrector.currentCorrection.applying(to: randomDeviceTime),
randomDeviceTime.addingTimeInterval(currentTimeDifference())
randomDeviceTime.addingTimeInterval(serverOffset)
),
"Any device time should be corrected by the server-to-device time difference."
)

serverDateProvider.serverTime = .mockRandomInThePast()
serverDateProvider.offset = .mockRandomInThePast()
XCTAssertTrue(
datesEqual(
corrector.currentCorrection.applying(to: randomDeviceTime),
randomDeviceTime.addingTimeInterval(currentTimeDifference())
randomDeviceTime.addingTimeInterval(serverOffset)
),
"When the server time goes on, any next correction should include new server-to-device time difference."
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ extension Date: AnyMockable {
}

static func mockRandomInThePast() -> Date {
return Date(timeIntervalSinceReferenceDate: TimeInterval.random(in: 0..<Date().timeIntervalSinceReferenceDate))
return Date(timeIntervalSinceReferenceDate: TimeInterval.mockRandomInThePast())
}

static func mockSpecificUTCGregorianDate(year: Int, month: Int, day: Int, hour: Int, minute: Int = 0, second: Int = 0) -> Date {
Expand Down Expand Up @@ -286,7 +286,7 @@ extension Float: AnyMockable {
}

extension Double: AnyMockable, RandomMockable {
static func mockAny() -> Float {
static func mockAny() -> Double {
return 0
}

Expand All @@ -300,11 +300,11 @@ extension Double: AnyMockable, RandomMockable {
}

extension TimeInterval {
static func mockAny() -> TimeInterval {
return 0
}

static let distantFuture = TimeInterval(integerLiteral: .max)

static func mockRandomInThePast() -> TimeInterval {
return random(in: 0..<Date().timeIntervalSinceReferenceDate)
}
}

struct ErrorMock: Error, CustomStringConvertible {
Expand Down