Skip to content

Commit

Permalink
Add EventLoop.now API for getting the current time (#3015)
Browse files Browse the repository at this point in the history
### Motivation

Code that tries to work with `NIODeadline` can be challenging to test
using `EmbeddedEventLoop` and `NIOAsyncTestingEventLoop` because they
have their own, fake clock.

These testing event loops do implement `scheduleTask(in:_)` to submit
work in the future, relative to the event loop clock, but there is no
way to get the event loop's current notion of "now", as a `NIODeadline`,
and users sometimes find that `NIODeadline.now`, which returns the time
of the real clock, to be surprising.

### Modifications

Add `EventLoop.now`  to get the current time of the event loop clock.

### Result

New APIs to support writing code that's easier to test.
  • Loading branch information
simonjbeaumont authored Dec 6, 2024
1 parent 74f7674 commit c3a8d18
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 3 deletions.
8 changes: 8 additions & 0 deletions Sources/NIOCore/EventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ public protocol EventLoop: EventLoopGroup {
@preconcurrency
func submit<T>(_ task: @escaping @Sendable () throws -> T) -> EventLoopFuture<T>

/// The current time of the event loop.
var now: NIODeadline { get }

/// Schedule a `task` that is executed by this `EventLoop` at the given time.
///
/// - Parameters:
Expand Down Expand Up @@ -394,6 +397,11 @@ public protocol EventLoop: EventLoopGroup {
func cancelScheduledCallback(_ scheduledCallback: NIOScheduledCallback)
}

extension EventLoop {
/// Default implementation of `now`: Returns `NIODeadline.now()`.
public var now: NIODeadline { .now() }
}

extension EventLoop {
/// Default implementation of `makeSucceededVoidFuture`: Return a fresh future (which will allocate).
public func makeSucceededVoidFuture() -> EventLoopFuture<Void> {
Expand Down
4 changes: 3 additions & 1 deletion Sources/NIOEmbedded/AsyncTestingEventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ public final class NIOAsyncTestingEventLoop: EventLoop, @unchecked Sendable {
/// The current "time" for this event loop. This is an amount in nanoseconds.
/// As we need to access this from any thread, we store this as an atomic.
private let _now = ManagedAtomic<UInt64>(0)
internal var now: NIODeadline {

/// The current "time" for this event loop. This is an amount in nanoseconds.
public var now: NIODeadline {
NIODeadline.uptimeNanoseconds(self._now.load(ordering: .relaxed))
}

Expand Down
3 changes: 2 additions & 1 deletion Sources/NIOEmbedded/Embedded.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ extension EmbeddedScheduledTask: Comparable {
/// responsible for ensuring they never call into the `EmbeddedEventLoop` in an
/// unsynchronized fashion.
public final class EmbeddedEventLoop: EventLoop, CustomStringConvertible {
private var _now: NIODeadline = .uptimeNanoseconds(0)
/// The current "time" for this event loop. This is an amount in nanoseconds.
internal var _now: NIODeadline = .uptimeNanoseconds(0)
public var now: NIODeadline { _now }

private enum State { case open, closing, closed }
private var state: State = .open
Expand Down
6 changes: 6 additions & 0 deletions Sources/NIOPosix/SelectableEventLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,12 @@ internal final class SelectableEventLoop: EventLoop {
thread.isCurrent
}

/// - see: `EventLoop.now`
@usableFromInline
internal var now: NIODeadline {
.now()
}

/// - see: `EventLoop.scheduleTask(deadline:_:)`
@inlinable
internal func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
Expand Down
10 changes: 10 additions & 0 deletions Tests/NIOEmbeddedTests/AsyncTestingEventLoopTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -619,4 +619,14 @@ final class NIOAsyncTestingEventLoopTests: XCTestCase {
await eventLoop.advanceTime(by: .seconds(1))
XCTAssertEqual(counter.load(ordering: .relaxed), 3)
}

func testCurrentTime() async {
let eventLoop = NIOAsyncTestingEventLoop()

await eventLoop.advanceTime(to: .uptimeNanoseconds(42))
XCTAssertEqual(eventLoop.now, .uptimeNanoseconds(42))

await eventLoop.advanceTime(by: .nanoseconds(42))
XCTAssertEqual(eventLoop.now, .uptimeNanoseconds(84))
}
}
33 changes: 32 additions & 1 deletion Tests/NIOEmbeddedTests/EmbeddedEventLoopTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ public final class EmbeddedEventLoopTest: XCTestCase {
try eventLoop.syncShutdownGracefully()
childTasks.append(
scheduleRecursiveTask(
at: eventLoop._now + childTaskStartDelay,
at: eventLoop.now + childTaskStartDelay,
andChildTaskAfter: childTaskStartDelay
)
)
Expand Down Expand Up @@ -497,4 +497,35 @@ public final class EmbeddedEventLoopTest: XCTestCase {
eventLoop.advanceTime(by: .seconds(1))
XCTAssertEqual(counter, 3)
}

func testCurrentTime() {
let eventLoop = EmbeddedEventLoop()

eventLoop.advanceTime(to: .uptimeNanoseconds(42))
XCTAssertEqual(eventLoop.now, .uptimeNanoseconds(42))

eventLoop.advanceTime(by: .nanoseconds(42))
XCTAssertEqual(eventLoop.now, .uptimeNanoseconds(84))
}

func testScheduleRepeatedTask() {
let eventLoop = EmbeddedEventLoop()

var counter = 0
eventLoop.scheduleRepeatedTask(initialDelay: .seconds(1), delay: .seconds(1)) { repeatedTask in
guard counter < 10 else {
repeatedTask.cancel()
return
}
counter += 1
}

XCTAssertEqual(counter, 0)

eventLoop.advanceTime(by: .seconds(1))
XCTAssertEqual(counter, 1)

eventLoop.advanceTime(by: .seconds(9))
XCTAssertEqual(counter, 10)
}
}
8 changes: 8 additions & 0 deletions Tests/NIOPosixTests/EventLoopTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1962,6 +1962,10 @@ private class EventLoopWithPreSucceededFuture: EventLoop {
preconditionFailure("not implemented")
}

var now: NIODeadline {
preconditionFailure("not implemented")
}

@discardableResult
func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
preconditionFailure("not implemented")
Expand Down Expand Up @@ -2013,6 +2017,10 @@ private class EventLoopWithoutPreSucceededFuture: EventLoop {
preconditionFailure("not implemented")
}

var now: NIODeadline {
preconditionFailure("not implemented")
}

@discardableResult
func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
preconditionFailure("not implemented")
Expand Down

0 comments on commit c3a8d18

Please sign in to comment.