967 lines
38 KiB
Swift
967 lines
38 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
import XCTest
|
|
@testable import NIO
|
|
import Dispatch
|
|
import NIOConcurrencyHelpers
|
|
|
|
public final class EventLoopTest : XCTestCase {
|
|
|
|
public func testSchedule() throws {
|
|
let nanos: NIODeadline = .now()
|
|
let amount: TimeAmount = .seconds(1)
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
let value = try eventLoopGroup.next().scheduleTask(in: amount) {
|
|
true
|
|
}.futureResult.wait()
|
|
|
|
XCTAssertTrue(NIODeadline.now() - nanos >= amount)
|
|
XCTAssertTrue(value)
|
|
}
|
|
|
|
public func testScheduleWithDelay() throws {
|
|
let smallAmount: TimeAmount = .milliseconds(100)
|
|
let longAmount: TimeAmount = .seconds(1)
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
// First, we create a server and client channel, but don't connect them.
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: eventLoopGroup)
|
|
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
|
|
.bind(host: "127.0.0.1", port: 0).wait())
|
|
let clientBootstrap = ClientBootstrap(group: eventLoopGroup)
|
|
|
|
// Now, schedule two tasks: one that takes a while, one that doesn't.
|
|
let nanos: NIODeadline = .now()
|
|
let longFuture = eventLoopGroup.next().scheduleTask(in: longAmount) {
|
|
true
|
|
}.futureResult
|
|
|
|
XCTAssertTrue(try assertNoThrowWithValue(try eventLoopGroup.next().scheduleTask(in: smallAmount) {
|
|
true
|
|
}.futureResult.wait()))
|
|
|
|
// Ok, the short one has happened. Now we should try connecting them. This connect should happen
|
|
// faster than the final task firing.
|
|
_ = try assertNoThrowWithValue(clientBootstrap.connect(to: serverChannel.localAddress!).wait()) as Channel
|
|
XCTAssertTrue(NIODeadline.now() - nanos < longAmount)
|
|
|
|
// Now wait for the long-delayed task.
|
|
XCTAssertTrue(try assertNoThrowWithValue(try longFuture.wait()))
|
|
// Now we're ok.
|
|
XCTAssertTrue(NIODeadline.now() - nanos >= longAmount)
|
|
}
|
|
|
|
public func testScheduleCancelled() throws {
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
let ran = NIOAtomic<Bool>.makeAtomic(value: false)
|
|
let scheduled = eventLoopGroup.next().scheduleTask(in: .seconds(2)) {
|
|
ran.store(true)
|
|
}
|
|
|
|
scheduled.cancel()
|
|
|
|
let nanos = NIODeadline.now()
|
|
let amount: TimeAmount = .seconds(2)
|
|
let value = try eventLoopGroup.next().scheduleTask(in: amount) {
|
|
true
|
|
}.futureResult.wait()
|
|
|
|
XCTAssertTrue(NIODeadline.now() - nanos >= amount)
|
|
XCTAssertTrue(value)
|
|
XCTAssertFalse(ran.load())
|
|
}
|
|
|
|
public func testScheduleRepeatedTask() throws {
|
|
let nanos: NIODeadline = .now()
|
|
let initialDelay: TimeAmount = .milliseconds(5)
|
|
let delay: TimeAmount = .milliseconds(10)
|
|
let count = 5
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
let expect = expectation(description: "Is cancelling RepatedTask")
|
|
let counter = NIOAtomic<Int>.makeAtomic(value: 0)
|
|
let loop = eventLoopGroup.next()
|
|
loop.scheduleRepeatedTask(initialDelay: initialDelay, delay: delay) { repeatedTask -> Void in
|
|
XCTAssertTrue(loop.inEventLoop)
|
|
let initialValue = counter.load()
|
|
_ = counter.add(1)
|
|
if initialValue == 0 {
|
|
XCTAssertTrue(NIODeadline.now() - nanos >= initialDelay)
|
|
} else if initialValue == count {
|
|
expect.fulfill()
|
|
repeatedTask.cancel()
|
|
}
|
|
}
|
|
|
|
waitForExpectations(timeout: 1) { error in
|
|
XCTAssertNil(error)
|
|
XCTAssertEqual(counter.load(), count + 1)
|
|
XCTAssertTrue(NIODeadline.now() - nanos >= initialDelay + Int64(count) * delay)
|
|
}
|
|
}
|
|
|
|
public func testScheduledTaskThatIsImmediatelyCancelledNeverFires() throws {
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
let loop = eventLoopGroup.next()
|
|
loop.execute {
|
|
let task = loop.scheduleTask(in: .milliseconds(0)) {
|
|
XCTFail()
|
|
}
|
|
task.cancel()
|
|
}
|
|
Thread.sleep(until: .init(timeIntervalSinceNow: 0.1))
|
|
}
|
|
|
|
public func testRepeatedTaskThatIsImmediatelyCancelledNeverFires() throws {
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
let loop = eventLoopGroup.next()
|
|
loop.execute {
|
|
let task = loop.scheduleRepeatedTask(initialDelay: .milliseconds(0), delay: .milliseconds(0)) { task in
|
|
XCTFail()
|
|
}
|
|
task.cancel()
|
|
}
|
|
Thread.sleep(until: .init(timeIntervalSinceNow: 0.1))
|
|
}
|
|
|
|
public func testScheduleRepeatedTaskCancelFromDifferentThread() throws {
|
|
let nanos: NIODeadline = .now()
|
|
let initialDelay: TimeAmount = .milliseconds(5)
|
|
let delay: TimeAmount = .milliseconds(0) // this will actually force the race from issue #554 to happen frequently
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
let expect = expectation(description: "Is cancelling RepatedTask")
|
|
let group = DispatchGroup()
|
|
let loop = eventLoopGroup.next()
|
|
group.enter()
|
|
var isAllowedToFire = true // read/write only on `loop`
|
|
var hasFired = false // read/write only on `loop`
|
|
let repeatedTask = loop.scheduleRepeatedTask(initialDelay: initialDelay, delay: delay) { (_: RepeatedTask) -> Void in
|
|
XCTAssertTrue(loop.inEventLoop)
|
|
if !hasFired {
|
|
// we can only do this once as we can only leave the DispatchGroup once but we might lose a race and
|
|
// the timer might fire more than once (until `shouldNoLongerFire` becomes true).
|
|
hasFired = true
|
|
group.leave()
|
|
expect.fulfill()
|
|
}
|
|
XCTAssertTrue(isAllowedToFire)
|
|
}
|
|
group.notify(queue: DispatchQueue.global()) {
|
|
repeatedTask.cancel()
|
|
loop.execute {
|
|
// only now do we know that the `cancel` must have gone through
|
|
isAllowedToFire = false
|
|
}
|
|
}
|
|
|
|
waitForExpectations(timeout: 1) { error in
|
|
XCTAssertNil(error)
|
|
XCTAssertTrue(NIODeadline.now() - nanos >= initialDelay)
|
|
}
|
|
}
|
|
|
|
public func testScheduleRepeatedTaskToNotRetainRepeatedTask() throws {
|
|
let initialDelay: TimeAmount = .milliseconds(5)
|
|
let delay: TimeAmount = .milliseconds(10)
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
|
|
weak var weakRepeated: RepeatedTask?
|
|
try { () -> Void in
|
|
let repeated = eventLoopGroup.next().scheduleRepeatedTask(initialDelay: initialDelay, delay: delay) { (_: RepeatedTask) -> Void in }
|
|
weakRepeated = repeated
|
|
XCTAssertNotNil(weakRepeated)
|
|
repeated.cancel()
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}()
|
|
assert(weakRepeated == nil, within: .seconds(1))
|
|
}
|
|
|
|
public func testScheduleRepeatedTaskToNotRetainEventLoop() throws {
|
|
weak var weakEventLoop: EventLoop? = nil
|
|
try {
|
|
let initialDelay: TimeAmount = .milliseconds(5)
|
|
let delay: TimeAmount = .milliseconds(10)
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
weakEventLoop = eventLoopGroup.next()
|
|
XCTAssertNotNil(weakEventLoop)
|
|
|
|
eventLoopGroup.next().scheduleRepeatedTask(initialDelay: initialDelay, delay: delay) { (_: RepeatedTask) -> Void in }
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}()
|
|
assert(weakEventLoop == nil, within: .seconds(1))
|
|
}
|
|
|
|
func testScheduledRepeatedAsyncTask() {
|
|
let eventLoop = EmbeddedEventLoop()
|
|
var counter = 0
|
|
let repeatedTask = eventLoop.scheduleRepeatedAsyncTask(initialDelay: .milliseconds(10),
|
|
delay: .milliseconds(10)) { (_: RepeatedTask) in
|
|
counter += 1
|
|
let p = eventLoop.makePromise(of: Void.self)
|
|
eventLoop.scheduleTask(in: .milliseconds(10)) {
|
|
p.succeed(())
|
|
}
|
|
return p.futureResult
|
|
}
|
|
for _ in 0..<10 {
|
|
// just running shouldn't do anything
|
|
eventLoop.run()
|
|
}
|
|
// t == 0: nothing
|
|
XCTAssertEqual(0, counter)
|
|
|
|
// t == 5: nothing
|
|
eventLoop.advanceTime(by: .milliseconds(5))
|
|
eventLoop.run()
|
|
XCTAssertEqual(0, counter)
|
|
|
|
// t == 10: once
|
|
eventLoop.advanceTime(by: .milliseconds(5))
|
|
eventLoop.run()
|
|
XCTAssertEqual(1, counter)
|
|
|
|
// t == 15: still once
|
|
eventLoop.advanceTime(by: .milliseconds(5))
|
|
eventLoop.run()
|
|
XCTAssertEqual(1, counter)
|
|
|
|
// t == 20: still once (because the task takes 10ms to execute)
|
|
eventLoop.advanceTime(by: .milliseconds(5))
|
|
eventLoop.run()
|
|
XCTAssertEqual(1, counter)
|
|
|
|
// t == 25: still once (because the task takes 10ms to execute)
|
|
eventLoop.advanceTime(by: .milliseconds(5))
|
|
eventLoop.run()
|
|
XCTAssertEqual(1, counter)
|
|
|
|
// t == 30: twice
|
|
eventLoop.advanceTime(by: .milliseconds(5))
|
|
eventLoop.run()
|
|
XCTAssertEqual(2, counter)
|
|
|
|
// t == 40: twice
|
|
eventLoop.advanceTime(by: .milliseconds(10))
|
|
eventLoop.run()
|
|
XCTAssertEqual(2, counter)
|
|
|
|
// t == 50: three times
|
|
eventLoop.advanceTime(by: .milliseconds(10))
|
|
eventLoop.run()
|
|
XCTAssertEqual(3, counter)
|
|
|
|
// t == 60: three times
|
|
eventLoop.advanceTime(by: .milliseconds(10))
|
|
eventLoop.run()
|
|
XCTAssertEqual(3, counter)
|
|
|
|
// t == 89: four times
|
|
eventLoop.advanceTime(by: .milliseconds(29))
|
|
eventLoop.run()
|
|
XCTAssertEqual(4, counter)
|
|
|
|
// t == 90: five times
|
|
eventLoop.advanceTime(by: .milliseconds(1))
|
|
eventLoop.run()
|
|
XCTAssertEqual(5, counter)
|
|
|
|
repeatedTask.cancel()
|
|
|
|
eventLoop.run()
|
|
XCTAssertEqual(5, counter)
|
|
|
|
eventLoop.advanceTime(by: .hours(10))
|
|
eventLoop.run()
|
|
XCTAssertEqual(5, counter)
|
|
}
|
|
|
|
public func testEventLoopGroupMakeIterator() throws {
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
var counter = 0
|
|
var innerCounter = 0
|
|
eventLoopGroup.makeIterator().forEach { loop in
|
|
counter += 1
|
|
loop.makeIterator().forEach { _ in
|
|
innerCounter += 1
|
|
}
|
|
}
|
|
|
|
XCTAssertEqual(counter, System.coreCount)
|
|
XCTAssertEqual(innerCounter, System.coreCount)
|
|
}
|
|
|
|
public func testEventLoopMakeIterator() throws {
|
|
let eventLoop = EmbeddedEventLoop()
|
|
let iterator = eventLoop.makeIterator()
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoop.syncShutdownGracefully())
|
|
}
|
|
|
|
var counter = 0
|
|
iterator.forEach { loop in
|
|
XCTAssertTrue(loop === eventLoop)
|
|
counter += 1
|
|
}
|
|
|
|
XCTAssertEqual(counter, 1)
|
|
}
|
|
|
|
public func testMultipleShutdown() throws {
|
|
// This test catches a regression that causes it to intermittently fail: it reveals bugs in synchronous shutdown.
|
|
// Do not ignore intermittent failures in this test!
|
|
let threads = 8
|
|
let numBytes = 256
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: threads)
|
|
|
|
// Create a server channel.
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
|
|
.bind(host: "127.0.0.1", port: 0).wait())
|
|
|
|
// We now want to connect to it. To try to slow this stuff down, we're going to use a multiple of the number
|
|
// of event loops.
|
|
for _ in 0..<(threads * 5) {
|
|
let clientChannel = try assertNoThrowWithValue(ClientBootstrap(group: group)
|
|
.connect(to: serverChannel.localAddress!)
|
|
.wait())
|
|
|
|
var buffer = clientChannel.allocator.buffer(capacity: numBytes)
|
|
for i in 0..<numBytes {
|
|
buffer.writeInteger(UInt8(i % 256))
|
|
}
|
|
|
|
try clientChannel.writeAndFlush(NIOAny(buffer)).wait()
|
|
}
|
|
|
|
// We should now shut down gracefully.
|
|
try group.syncShutdownGracefully()
|
|
}
|
|
|
|
public func testShuttingDownFailsRegistration() throws {
|
|
// This test catches a regression where the selectable event loop would allow a socket registration while
|
|
// it was nominally "shutting down". To do this, we take advantage of the fact that the event loop attempts
|
|
// to cleanly shut down all the channels before it actually closes. We add a custom channel that we can use
|
|
// to wedge the event loop in the "shutting down" state, ensuring that we have plenty of time to attempt the
|
|
// registration.
|
|
class WedgeOpenHandler: ChannelDuplexHandler {
|
|
typealias InboundIn = Any
|
|
typealias OutboundIn = Any
|
|
typealias OutboundOut = Any
|
|
|
|
private let promiseRegisterCallback: (EventLoopPromise<Void>) -> Void
|
|
|
|
var closePromise: EventLoopPromise<Void>? = nil
|
|
private let channelActivePromise: EventLoopPromise<Void>?
|
|
|
|
init(channelActivePromise: EventLoopPromise<Void>? = nil, _ promiseRegisterCallback: @escaping (EventLoopPromise<Void>) -> Void) {
|
|
self.promiseRegisterCallback = promiseRegisterCallback
|
|
self.channelActivePromise = channelActivePromise
|
|
}
|
|
|
|
func channelActive(context: ChannelHandlerContext) {
|
|
self.channelActivePromise?.succeed(())
|
|
}
|
|
|
|
func close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise<Void>?) {
|
|
guard self.closePromise == nil else {
|
|
XCTFail("Attempted to create duplicate close promise")
|
|
return
|
|
}
|
|
XCTAssertTrue(context.channel.isActive)
|
|
self.closePromise = context.eventLoop.makePromise()
|
|
self.closePromise!.futureResult.whenSuccess {
|
|
context.close(mode: mode, promise: promise)
|
|
}
|
|
promiseRegisterCallback(self.closePromise!)
|
|
}
|
|
}
|
|
|
|
let promiseQueue = DispatchQueue(label: "promiseQueue")
|
|
var promises: [EventLoopPromise<Void>] = []
|
|
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
do {
|
|
try group.syncShutdownGracefully()
|
|
} catch EventLoopError.shutdown {
|
|
// Fine, that's expected if the test completed.
|
|
} catch {
|
|
XCTFail("Unexpected error on close: \(error)")
|
|
}
|
|
}
|
|
let loop = group.next() as! SelectableEventLoop
|
|
|
|
let serverChannelUp = group.next().makePromise(of: Void.self)
|
|
let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.childChannelInitializer { channel in
|
|
channel.pipeline.addHandler(WedgeOpenHandler(channelActivePromise: serverChannelUp) { promise in
|
|
promiseQueue.sync { promises.append(promise) }
|
|
})
|
|
}
|
|
.bind(host: "127.0.0.1", port: 0).wait())
|
|
defer {
|
|
XCTAssertFalse(serverChannel.isActive)
|
|
}
|
|
let connectPromise = loop.makePromise(of: Void.self)
|
|
|
|
// We're going to create and register a channel, but not actually attempt to do anything with it.
|
|
let wedgeHandler = WedgeOpenHandler { promise in
|
|
promiseQueue.sync { promises.append(promise) }
|
|
}
|
|
let channel = try SocketChannel(eventLoop: loop, protocolFamily: AF_INET)
|
|
try channel.eventLoop.submit {
|
|
channel.pipeline.addHandler(wedgeHandler).flatMap {
|
|
channel.register()
|
|
}.flatMap {
|
|
// connecting here to stop epoll from throwing EPOLLHUP at us
|
|
channel.connect(to: serverChannel.localAddress!)
|
|
}.cascade(to: connectPromise)
|
|
}.wait()
|
|
|
|
// Wait for the connect to complete.
|
|
XCTAssertNoThrow(try connectPromise.futureResult.wait())
|
|
|
|
XCTAssertNoThrow(try serverChannelUp.futureResult.wait())
|
|
|
|
let g = DispatchGroup()
|
|
let q = DispatchQueue(label: "\(#file)/\(#line)")
|
|
g.enter()
|
|
// Now we're going to start closing the event loop. This should not immediately succeed.
|
|
loop.initiateClose(queue: q) { result in
|
|
func workaroundSR9815() {
|
|
XCTAssertNoThrow(try result.get())
|
|
}
|
|
workaroundSR9815()
|
|
g.leave()
|
|
}
|
|
|
|
// Now we're going to attempt to register a new channel. This should immediately fail.
|
|
let newChannel = try SocketChannel(eventLoop: loop, protocolFamily: AF_INET)
|
|
|
|
do {
|
|
try newChannel.register().wait()
|
|
XCTFail("Register did not throw")
|
|
} catch EventLoopError.shutdown {
|
|
// All good
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
}
|
|
|
|
// Confirm that the loop still hasn't closed.
|
|
XCTAssertEqual(.timedOut, g.wait(timeout: .now()))
|
|
|
|
// Now let it close.
|
|
promiseQueue.sync {
|
|
promises.forEach { $0.succeed(()) }
|
|
}
|
|
XCTAssertNoThrow(g.wait())
|
|
}
|
|
|
|
public func testEventLoopThreads() throws {
|
|
var counter = 0
|
|
let body: ThreadInitializer = { t in
|
|
counter += 1
|
|
}
|
|
let threads: [ThreadInitializer] = [body, body]
|
|
|
|
let group = MultiThreadedEventLoopGroup(threadInitializers: threads)
|
|
|
|
XCTAssertEqual(2, counter)
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
|
|
public func testEventLoopPinned() throws {
|
|
#if os(Linux)
|
|
let body: ThreadInitializer = { t in
|
|
let set = LinuxCPUSet(0)
|
|
t.affinity = set
|
|
XCTAssertEqual(set, t.affinity)
|
|
}
|
|
let threads: [ThreadInitializer] = [body, body]
|
|
|
|
let group = MultiThreadedEventLoopGroup(threadInitializers: threads)
|
|
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
#endif
|
|
}
|
|
|
|
public func testEventLoopPinnedCPUIdsConstructor() throws {
|
|
#if os(Linux)
|
|
let group = MultiThreadedEventLoopGroup(pinnedCPUIds: [0])
|
|
let eventLoop = group.next()
|
|
let set = try eventLoop.submit {
|
|
NIOThread.current.affinity
|
|
}.wait()
|
|
|
|
XCTAssertEqual(LinuxCPUSet(0), set)
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
#endif
|
|
}
|
|
|
|
public func testCurrentEventLoop() throws {
|
|
class EventLoopHolder {
|
|
weak var loop: EventLoop?
|
|
init(_ loop: EventLoop) {
|
|
self.loop = loop
|
|
}
|
|
}
|
|
|
|
func assertCurrentEventLoop0() throws -> EventLoopHolder {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 2)
|
|
|
|
let loop1 = group.next()
|
|
let currentLoop1 = try loop1.submit {
|
|
MultiThreadedEventLoopGroup.currentEventLoop
|
|
}.wait()
|
|
XCTAssertTrue(loop1 === currentLoop1)
|
|
|
|
let loop2 = group.next()
|
|
let currentLoop2 = try loop2.submit {
|
|
MultiThreadedEventLoopGroup.currentEventLoop
|
|
}.wait()
|
|
XCTAssertTrue(loop2 === currentLoop2)
|
|
XCTAssertFalse(loop1 === loop2)
|
|
|
|
let holder = EventLoopHolder(loop2)
|
|
XCTAssertNotNil(holder.loop)
|
|
XCTAssertNil(MultiThreadedEventLoopGroup.currentEventLoop)
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
return holder
|
|
}
|
|
|
|
let holder = try assertCurrentEventLoop0()
|
|
|
|
// We loop as the Thread used by SelectableEventLoop may not be gone yet.
|
|
// In the next major version we should ensure to join all threads and so be sure all are gone when
|
|
// syncShutdownGracefully returned.
|
|
var tries = 0
|
|
while holder.loop != nil {
|
|
XCTAssertTrue(tries < 5, "Reference to EventLoop still alive after 5 seconds")
|
|
sleep(1)
|
|
tries += 1
|
|
}
|
|
}
|
|
|
|
public func testShutdownWhileScheduledTasksNotReady() throws {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
let eventLoop = group.next()
|
|
_ = eventLoop.scheduleTask(in: .hours(1)) { }
|
|
try group.syncShutdownGracefully()
|
|
}
|
|
|
|
public func testCloseFutureNotifiedBeforeUnblock() throws {
|
|
class AssertHandler: ChannelInboundHandler {
|
|
typealias InboundIn = Any
|
|
|
|
let groupIsShutdown = NIOAtomic.makeAtomic(value: false)
|
|
let removed = NIOAtomic.makeAtomic(value: false)
|
|
|
|
public func handlerRemoved(context: ChannelHandlerContext) {
|
|
XCTAssertFalse(groupIsShutdown.load())
|
|
XCTAssertTrue(removed.compareAndExchange(expected: false, desired: true))
|
|
}
|
|
}
|
|
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
let eventLoop = group.next()
|
|
let assertHandler = AssertHandler()
|
|
let serverSocket = try assertNoThrowWithValue(ServerBootstrap(group: group)
|
|
.bind(host: "localhost", port: 0).wait())
|
|
let channel = try assertNoThrowWithValue(SocketChannel(eventLoop: eventLoop as! SelectableEventLoop,
|
|
protocolFamily: serverSocket.localAddress!.protocolFamily))
|
|
XCTAssertNoThrow(try channel.pipeline.addHandler(assertHandler).wait() as Void)
|
|
XCTAssertNoThrow(try channel.eventLoop.flatSubmit {
|
|
channel.register().flatMap {
|
|
channel.connect(to: serverSocket.localAddress!)
|
|
}
|
|
}.wait() as Void)
|
|
let closeFutureFulfilledEventually = NIOAtomic<Bool>.makeAtomic(value: false)
|
|
XCTAssertFalse(channel.closeFuture.isFulfilled)
|
|
channel.closeFuture.whenSuccess {
|
|
XCTAssertTrue(closeFutureFulfilledEventually.compareAndExchange(expected: false, desired: true))
|
|
}
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
XCTAssertTrue(assertHandler.groupIsShutdown.compareAndExchange(expected: false, desired: true))
|
|
XCTAssertTrue(assertHandler.removed.load())
|
|
XCTAssertFalse(channel.isActive)
|
|
XCTAssertTrue(closeFutureFulfilledEventually.load())
|
|
}
|
|
|
|
public func testScheduleMultipleTasks() throws {
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
var array = Array<(Int, NIODeadline)>()
|
|
let scheduled1 = eventLoopGroup.next().scheduleTask(in: .milliseconds(500)) {
|
|
array.append((1, .now()))
|
|
}
|
|
|
|
let scheduled2 = eventLoopGroup.next().scheduleTask(in: .milliseconds(100)) {
|
|
array.append((2, .now()))
|
|
}
|
|
|
|
let scheduled3 = eventLoopGroup.next().scheduleTask(in: .milliseconds(1000)) {
|
|
array.append((3, .now()))
|
|
}
|
|
|
|
var result = try eventLoopGroup.next().scheduleTask(in: .milliseconds(1000)) {
|
|
array
|
|
}.futureResult.wait()
|
|
|
|
XCTAssertTrue(scheduled1.futureResult.isFulfilled)
|
|
XCTAssertTrue(scheduled2.futureResult.isFulfilled)
|
|
XCTAssertTrue(scheduled3.futureResult.isFulfilled)
|
|
|
|
let first = result.removeFirst()
|
|
XCTAssertEqual(2, first.0)
|
|
let second = result.removeFirst()
|
|
XCTAssertEqual(1, second.0)
|
|
let third = result.removeFirst()
|
|
XCTAssertEqual(3, third.0)
|
|
|
|
XCTAssertTrue(first.1 < second.1)
|
|
XCTAssertTrue(second.1 < third.1)
|
|
|
|
XCTAssertTrue(result.isEmpty)
|
|
|
|
}
|
|
|
|
public func testRepeatedTaskThatIsImmediatelyCancelledNotifies() throws {
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
let loop = eventLoopGroup.next()
|
|
let promise1: EventLoopPromise<Void> = loop.makePromise()
|
|
let promise2: EventLoopPromise<Void> = loop.makePromise()
|
|
let expect1 = XCTestExpectation(description: "Initializer promise was fulfilled")
|
|
let expect2 = XCTestExpectation(description: "Cancellation-specific promise was fulfilled")
|
|
promise1.futureResult.whenSuccess { expect1.fulfill() }
|
|
promise2.futureResult.whenSuccess { expect2.fulfill() }
|
|
loop.execute {
|
|
let task = loop.scheduleRepeatedTask(initialDelay: .milliseconds(0), delay: .milliseconds(0), notifying: promise1) { task in
|
|
XCTFail()
|
|
}
|
|
task.cancel(promise: promise2)
|
|
}
|
|
Thread.sleep(until: .init(timeIntervalSinceNow: 0.1))
|
|
let res = XCTWaiter.wait(for: [expect1, expect2], timeout: 1.0)
|
|
XCTAssertEqual(res, .completed)
|
|
}
|
|
|
|
public func testRepeatedTaskThatIsCancelledAfterRunningAtLeastTwiceNotifies() throws {
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
let loop = eventLoopGroup.next()
|
|
let promise1: EventLoopPromise<Void> = loop.makePromise()
|
|
let promise2: EventLoopPromise<Void> = loop.makePromise()
|
|
let expectRuns = XCTestExpectation(description: "Repeated task has run")
|
|
expectRuns.expectedFulfillmentCount = 2
|
|
let task = loop.scheduleRepeatedTask(initialDelay: .milliseconds(0), delay: .milliseconds(10), notifying: promise1) { task in
|
|
expectRuns.fulfill()
|
|
}
|
|
XCTAssertEqual(XCTWaiter.wait(for: [expectRuns], timeout: 0.05), .completed)
|
|
let expect1 = XCTestExpectation(description: "Initializer promise was fulfilled")
|
|
let expect2 = XCTestExpectation(description: "Cancellation-specific promise was fulfilled")
|
|
promise1.futureResult.whenSuccess { expect1.fulfill() }
|
|
promise2.futureResult.whenSuccess { expect2.fulfill() }
|
|
task.cancel(promise: promise2)
|
|
XCTAssertEqual(XCTWaiter.wait(for: [expect1, expect2], timeout: 1.0), .completed)
|
|
}
|
|
|
|
public func testRepeatedTaskThatCancelsItselfNotifiesOnlyWhenFinished() throws {
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
let loop = eventLoopGroup.next()
|
|
let promise1: EventLoopPromise<Void> = loop.makePromise()
|
|
let promise2: EventLoopPromise<Void> = loop.makePromise()
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
loop.scheduleRepeatedTask(initialDelay: .milliseconds(0), delay: .milliseconds(0), notifying: promise1) { task -> Void in
|
|
task.cancel(promise: promise2)
|
|
semaphore.wait()
|
|
}
|
|
let expectFail1 = XCTestExpectation(description: "Initializer promise was wrongly fulfilled")
|
|
let expectFail2 = XCTestExpectation(description: "Cancellation-specific promise was wrongly fulfilled")
|
|
let expect1 = XCTestExpectation(description: "Initializer promise was fulfilled")
|
|
let expect2 = XCTestExpectation(description: "Cancellation-specific promise was fulfilled")
|
|
promise1.futureResult.whenSuccess { expectFail1.fulfill(); expect1.fulfill() }
|
|
promise2.futureResult.whenSuccess { expectFail2.fulfill(); expect2.fulfill() }
|
|
XCTAssertEqual(XCTWaiter.wait(for: [expectFail1, expectFail2], timeout: 0.5), .timedOut)
|
|
semaphore.signal()
|
|
XCTAssertEqual(XCTWaiter.wait(for: [expect1, expect2], timeout: 0.5), .completed)
|
|
}
|
|
|
|
func testAndAllCompleteWithZeroFutures() {
|
|
let eventLoop = EmbeddedEventLoop()
|
|
let done = DispatchWorkItem {}
|
|
EventLoopFuture<Void>.andAllComplete([], on: eventLoop).whenComplete { (result: Result<Void, Error>) in
|
|
_ = result.mapError { error -> Error in
|
|
XCTFail("unexpected error \(error)")
|
|
return error
|
|
}
|
|
done.perform()
|
|
}
|
|
done.wait()
|
|
}
|
|
|
|
func testAndAllSucceedWithZeroFutures() {
|
|
let eventLoop = EmbeddedEventLoop()
|
|
let done = DispatchWorkItem {}
|
|
EventLoopFuture<Void>.andAllSucceed([], on: eventLoop).whenComplete { result in
|
|
_ = result.mapError { error -> Error in
|
|
XCTFail("unexpected error \(error)")
|
|
return error
|
|
}
|
|
done.perform()
|
|
}
|
|
done.wait()
|
|
}
|
|
|
|
func testCancelledScheduledTasksDoNotHoldOnToRunClosure() {
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
|
|
class Thing {}
|
|
|
|
weak var weakThing: Thing? = nil
|
|
|
|
func make() -> Scheduled<Never> {
|
|
let aThing = Thing()
|
|
weakThing = aThing
|
|
return group.next().scheduleTask(in: .hours(1)) {
|
|
preconditionFailure("this should definitely not run: \(aThing)")
|
|
}
|
|
}
|
|
|
|
let scheduled = make()
|
|
scheduled.cancel()
|
|
assert(weakThing == nil, within: .seconds(1))
|
|
XCTAssertThrowsError(try scheduled.futureResult.wait()) { error in
|
|
XCTAssertEqual(EventLoopError.cancelled, error as? EventLoopError)
|
|
}
|
|
}
|
|
|
|
func testIllegalCloseOfEventLoopFails() {
|
|
// Vapor 3 closes EventLoops directly which is illegal and makes the `shutdownGracefully` of the owning
|
|
// MultiThreadedEventLoopGroup never succeed.
|
|
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try group.syncShutdownGracefully())
|
|
}
|
|
XCTAssertThrowsError(try group.next().syncShutdownGracefully()) { error in
|
|
switch error {
|
|
case EventLoopError.unsupportedOperation:
|
|
() // expected
|
|
default:
|
|
XCTFail("illegal shutdown threw wrong error \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testSubtractingDeadlineFromPastAndFuturesDeadlinesWorks() {
|
|
let older = NIODeadline.now()
|
|
Thread.sleep(until: Date().addingTimeInterval(0.02))
|
|
let newer = NIODeadline.now()
|
|
|
|
XCTAssertLessThan(older - newer, .nanoseconds(0))
|
|
XCTAssertGreaterThan(newer - older, .nanoseconds(0))
|
|
}
|
|
|
|
func testCallingSyncShutdownGracefullyMultipleTimesShouldNotHang() throws {
|
|
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4)
|
|
try elg.syncShutdownGracefully()
|
|
try elg.syncShutdownGracefully()
|
|
try elg.syncShutdownGracefully()
|
|
}
|
|
|
|
func testCallingShutdownGracefullyMultipleTimesShouldExecuteAllCallbacks() throws {
|
|
let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4)
|
|
let condition: ConditionLock<Int> = ConditionLock(value: 0)
|
|
elg.shutdownGracefully { _ in
|
|
if condition.lock(whenValue: 0, timeoutSeconds: 1) {
|
|
condition.unlock(withValue: 1)
|
|
}
|
|
}
|
|
elg.shutdownGracefully { _ in
|
|
if condition.lock(whenValue: 1, timeoutSeconds: 1) {
|
|
condition.unlock(withValue: 2)
|
|
}
|
|
}
|
|
elg.shutdownGracefully { _ in
|
|
if condition.lock(whenValue: 2, timeoutSeconds: 1) {
|
|
condition.unlock(withValue: 3)
|
|
}
|
|
}
|
|
|
|
guard condition.lock(whenValue: 3, timeoutSeconds: 1) else {
|
|
XCTFail("Not all shutdown callbacks have been executed")
|
|
return
|
|
}
|
|
condition.unlock()
|
|
}
|
|
|
|
func testEdgeCasesNIODeadlineMinusNIODeadline() {
|
|
let smallestPossibleDeadline = NIODeadline.uptimeNanoseconds(.min)
|
|
let largestPossibleDeadline = NIODeadline.uptimeNanoseconds(.max)
|
|
let distantFuture = NIODeadline.distantFuture
|
|
let distantPast = NIODeadline.distantPast
|
|
let zeroDeadline = NIODeadline.uptimeNanoseconds(0)
|
|
let nowDeadline = NIODeadline.now()
|
|
|
|
let allDeadlines = [smallestPossibleDeadline, largestPossibleDeadline, distantPast, distantFuture,
|
|
zeroDeadline, nowDeadline]
|
|
|
|
for deadline1 in allDeadlines {
|
|
for deadline2 in allDeadlines {
|
|
if deadline1 > deadline2 {
|
|
XCTAssertGreaterThan(deadline1 - deadline2, TimeAmount.nanoseconds(0))
|
|
} else if deadline1 < deadline2 {
|
|
XCTAssertLessThan(deadline1 - deadline2, TimeAmount.nanoseconds(0))
|
|
} else {
|
|
// they're equal.
|
|
XCTAssertEqual(deadline1 - deadline2, TimeAmount.nanoseconds(0))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func testEdgeCasesNIODeadlinePlusTimeAmount() {
|
|
let smallestPossibleTimeAmount = TimeAmount.nanoseconds(.min)
|
|
let largestPossibleTimeAmount = TimeAmount.nanoseconds(.max)
|
|
let zeroTimeAmount = TimeAmount.nanoseconds(0)
|
|
|
|
let smallestPossibleDeadline = NIODeadline.uptimeNanoseconds(.min)
|
|
let largestPossibleDeadline = NIODeadline.uptimeNanoseconds(.max)
|
|
let distantFuture = NIODeadline.distantFuture
|
|
let distantPast = NIODeadline.distantPast
|
|
let zeroDeadline = NIODeadline.uptimeNanoseconds(0)
|
|
let nowDeadline = NIODeadline.now()
|
|
|
|
for timeAmount in [smallestPossibleTimeAmount, largestPossibleTimeAmount, zeroTimeAmount] {
|
|
for deadline in [smallestPossibleDeadline, largestPossibleDeadline, distantPast, distantFuture,
|
|
zeroDeadline, nowDeadline] {
|
|
let (partial, overflow) = Int64(deadline.uptimeNanoseconds).addingReportingOverflow(timeAmount.nanoseconds)
|
|
let expectedValue: UInt64
|
|
if overflow {
|
|
XCTAssertGreaterThanOrEqual(timeAmount.nanoseconds, 0)
|
|
XCTAssertGreaterThanOrEqual(deadline.uptimeNanoseconds, 0)
|
|
// we cap at distantFuture torwards +inf
|
|
expectedValue = NIODeadline.distantFuture.uptimeNanoseconds
|
|
} else if partial < 0 {
|
|
// we cap at 0 towards -inf
|
|
expectedValue = 0
|
|
} else {
|
|
// otherwise we have a result
|
|
expectedValue = .init(partial)
|
|
}
|
|
XCTAssertEqual((deadline + timeAmount).uptimeNanoseconds, expectedValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testEdgeCasesNIODeadlineMinusTimeAmount() {
|
|
let smallestPossibleTimeAmount = TimeAmount.nanoseconds(.min)
|
|
let largestPossibleTimeAmount = TimeAmount.nanoseconds(.max)
|
|
let zeroTimeAmount = TimeAmount.nanoseconds(0)
|
|
|
|
let smallestPossibleDeadline = NIODeadline.uptimeNanoseconds(.min)
|
|
let largestPossibleDeadline = NIODeadline.uptimeNanoseconds(.max)
|
|
let distantFuture = NIODeadline.distantFuture
|
|
let distantPast = NIODeadline.distantPast
|
|
let zeroDeadline = NIODeadline.uptimeNanoseconds(0)
|
|
let nowDeadline = NIODeadline.now()
|
|
|
|
for timeAmount in [smallestPossibleTimeAmount, largestPossibleTimeAmount, zeroTimeAmount] {
|
|
for deadline in [smallestPossibleDeadline, largestPossibleDeadline, distantPast, distantFuture,
|
|
zeroDeadline, nowDeadline] {
|
|
let (partial, overflow) = Int64(deadline.uptimeNanoseconds).subtractingReportingOverflow(timeAmount.nanoseconds)
|
|
let expectedValue: UInt64
|
|
if overflow {
|
|
XCTAssertLessThan(timeAmount.nanoseconds, 0)
|
|
XCTAssertGreaterThanOrEqual(deadline.uptimeNanoseconds, 0)
|
|
// we cap at distantFuture torwards +inf
|
|
expectedValue = NIODeadline.distantFuture.uptimeNanoseconds
|
|
} else if partial < 0 {
|
|
// we cap at 0 towards -inf
|
|
expectedValue = 0
|
|
} else {
|
|
// otherwise we have a result
|
|
expectedValue = .init(partial)
|
|
}
|
|
XCTAssertEqual((deadline - timeAmount).uptimeNanoseconds, expectedValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testSuccessfulFlatSubmit() {
|
|
let eventLoop = EmbeddedEventLoop()
|
|
let future = eventLoop.flatSubmit {
|
|
eventLoop.makeSucceededFuture(1)
|
|
}
|
|
eventLoop.run()
|
|
XCTAssertNoThrow(XCTAssertEqual(1, try future.wait()))
|
|
}
|
|
|
|
func testFailingFlatSubmit() {
|
|
enum TestError: Error { case failed }
|
|
|
|
let eventLoop = EmbeddedEventLoop()
|
|
let future = eventLoop.flatSubmit { () -> EventLoopFuture<Int> in
|
|
eventLoop.makeFailedFuture(TestError.failed)
|
|
}
|
|
eventLoop.run()
|
|
XCTAssertThrowsError(try future.wait()) { error in
|
|
XCTAssertEqual(.failed, error as? TestError)
|
|
}
|
|
}
|
|
}
|