swift-nio/Sources/NIO/SelectableEventLoop.swift

578 lines
24 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2019 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 Dispatch
import NIOConcurrencyHelpers
/// Execute the given closure and ensure we release all auto pools if needed.
@inlinable
internal func withAutoReleasePool<T>(_ execute: () throws -> T) rethrows -> T {
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
return try autoreleasepool {
try execute()
}
#else
return try execute()
#endif
}
/// `EventLoop` implementation that uses a `Selector` to get notified once there is more I/O or tasks to process.
/// The whole processing of I/O and tasks is done by a `NIOThread` that is tied to the `SelectableEventLoop`. This `NIOThread`
/// is guaranteed to never change!
@usableFromInline
internal final class SelectableEventLoop: EventLoop {
/// The different state in the lifecycle of an `EventLoop` seen from _outside_ the `EventLoop`.
private enum ExternalState {
/// `EventLoop` is open and so can process more work.
case open
/// `EventLoop` is currently in the process of closing.
case closing
/// `EventLoop` is closed.
case closed
/// `EventLoop` is closed and is currently trying to reclaim resources (such as the EventLoop thread).
case reclaimingResources
/// `EventLoop` is closed and all the resources (such as the EventLoop thread) have been reclaimed.
case resourcesReclaimed
}
/// The different state in the lifecycle of an `EventLoop` seen from _inside_ the `EventLoop`.
private enum InternalState {
case runningAndAcceptingNewRegistrations
case runningButNotAcceptingNewRegistrations
case noLongerRunning
case exitingThread
}
/* private but tests */ internal let _selector: NIO.Selector<NIORegistration>
private let thread: NIOThread
@usableFromInline
internal var _scheduledTasks = PriorityQueue<ScheduledTask>(ascending: true)
private var tasksCopy = ContiguousArray<() -> Void>()
private let canBeShutdownIndividually: Bool
@usableFromInline
internal let _tasksLock = Lock()
private let _externalStateLock = Lock()
private var externalStateLock: Lock {
// The assert is here to check that we never try to read the external state on the EventLoop unless we're
// shutting down.
assert(!self.inEventLoop || self.internalState != .runningAndAcceptingNewRegistrations,
"lifecycle lock taken whilst up and running and in EventLoop")
return self._externalStateLock
}
private var internalState: InternalState = .runningAndAcceptingNewRegistrations // protected by the EventLoop thread
private var externalState: ExternalState = .open // protected by externalStateLock
private let _iovecs: UnsafeMutablePointer<IOVector>
private let _storageRefs: UnsafeMutablePointer<Unmanaged<AnyObject>>
let iovecs: UnsafeMutableBufferPointer<IOVector>
let storageRefs: UnsafeMutableBufferPointer<Unmanaged<AnyObject>>
// Used for gathering UDP writes.
private let _msgs: UnsafeMutablePointer<MMsgHdr>
private let _addresses: UnsafeMutablePointer<sockaddr_storage>
let msgs: UnsafeMutableBufferPointer<MMsgHdr>
let addresses: UnsafeMutableBufferPointer<sockaddr_storage>
// Used for UDP control messages.
private(set) var controlMessageStorage: UnsafeControlMessageStorage
/// Creates a new `SelectableEventLoop` instance that is tied to the given `pthread_t`.
private let promiseCreationStoreLock = Lock()
private var _promiseCreationStore: [UInt: (file: StaticString, line: UInt)] = [:]
@usableFromInline
internal func promiseCreationStoreAdd<T>(future: EventLoopFuture<T>, file: StaticString, line: UInt) {
precondition(_isDebugAssertConfiguration())
self.promiseCreationStoreLock.withLock {
self._promiseCreationStore[self.obfuscatePointerValue(future)] = (file: file, line: line)
}
}
internal func promiseCreationStoreRemove<T>(future: EventLoopFuture<T>) -> (file: StaticString, line: UInt) {
precondition(_isDebugAssertConfiguration())
return self.promiseCreationStoreLock.withLock {
self._promiseCreationStore.removeValue(forKey: self.obfuscatePointerValue(future))!
}
}
@usableFromInline
internal var _validInternalStateToScheduleTasks: Bool {
switch self.internalState {
case .exitingThread:
return false
case .runningAndAcceptingNewRegistrations, .runningButNotAcceptingNewRegistrations, .noLongerRunning:
return true
}
}
// access with `externalStateLock` held
private var validExternalStateToScheduleTasks: Bool {
switch self.externalState {
case .open, .closing:
return true
case .closed, .reclaimingResources, .resourcesReclaimed:
return false
}
}
internal var testsOnly_validExternalStateToScheduleTasks: Bool {
return self.externalStateLock.withLock {
return self.validExternalStateToScheduleTasks
}
}
internal init(thread: NIOThread, selector: NIO.Selector<NIORegistration>, canBeShutdownIndividually: Bool) {
self._selector = selector
self.thread = thread
self._iovecs = UnsafeMutablePointer.allocate(capacity: Socket.writevLimitIOVectors)
self._storageRefs = UnsafeMutablePointer.allocate(capacity: Socket.writevLimitIOVectors)
self.iovecs = UnsafeMutableBufferPointer(start: self._iovecs, count: Socket.writevLimitIOVectors)
self.storageRefs = UnsafeMutableBufferPointer(start: self._storageRefs, count: Socket.writevLimitIOVectors)
self._msgs = UnsafeMutablePointer.allocate(capacity: Socket.writevLimitIOVectors)
self._addresses = UnsafeMutablePointer.allocate(capacity: Socket.writevLimitIOVectors)
self.msgs = UnsafeMutableBufferPointer(start: _msgs, count: Socket.writevLimitIOVectors)
self.addresses = UnsafeMutableBufferPointer(start: _addresses, count: Socket.writevLimitIOVectors)
self.controlMessageStorage = UnsafeControlMessageStorage.allocate(msghdrCount: Socket.writevLimitIOVectors)
// We will process 4096 tasks per while loop.
self.tasksCopy.reserveCapacity(4096)
self.canBeShutdownIndividually = canBeShutdownIndividually
}
deinit {
assert(self.internalState == .exitingThread,
"illegal internal state on deinit: \(self.internalState)")
assert(self.externalState == .resourcesReclaimed,
"illegal external state on shutdown: \(self.externalState)")
_iovecs.deallocate()
_storageRefs.deallocate()
_msgs.deallocate()
_addresses.deallocate()
self.controlMessageStorage.deallocate()
}
/// Is this `SelectableEventLoop` still open (ie. not shutting down or shut down)
internal var isOpen: Bool {
self.assertInEventLoop()
switch self.internalState {
case .noLongerRunning, .runningButNotAcceptingNewRegistrations, .exitingThread:
return false
case .runningAndAcceptingNewRegistrations:
return true
}
}
/// Register the given `SelectableChannel` with this `SelectableEventLoop`. After this point all I/O for the `SelectableChannel` will be processed by this `SelectableEventLoop` until it
/// is deregistered by calling `deregister`.
internal func register<C: SelectableChannel>(channel: C) throws {
self.assertInEventLoop()
// Don't allow registration when we're closed.
guard self.isOpen else {
throw EventLoopError.shutdown
}
try channel.register(selector: self._selector, interested: channel.interestedEvent)
}
/// Deregister the given `SelectableChannel` from this `SelectableEventLoop`.
internal func deregister<C: SelectableChannel>(channel: C, mode: CloseMode = .all) throws {
self.assertInEventLoop()
guard self.isOpen else {
// It's possible the EventLoop was closed before we were able to call deregister, so just return in this case as there is no harm.
return
}
try channel.deregister(selector: self._selector, mode: mode)
}
/// Register the given `SelectableChannel` with this `SelectableEventLoop`. This should be done whenever `channel.interestedEvents` has changed and it should be taken into account when
/// waiting for new I/O for the given `SelectableChannel`.
internal func reregister<C: SelectableChannel>(channel: C) throws {
self.assertInEventLoop()
try channel.reregister(selector: self._selector, interested: channel.interestedEvent)
}
/// - see: `EventLoop.inEventLoop`
@usableFromInline
internal var inEventLoop: Bool {
return thread.isCurrent
}
/// - see: `EventLoop.scheduleTask(deadline:_:)`
@inlinable
internal func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
let promise: EventLoopPromise<T> = self.makePromise()
let task = ScheduledTask({
do {
promise.succeed(try task())
} catch let err {
promise.fail(err)
}
}, { error in
promise.fail(error)
}, deadline)
let scheduled = Scheduled(promise: promise, cancellationTask: {
self._tasksLock.withLockVoid {
self._scheduledTasks.remove(task)
}
// We don't need to wake up the selector here, the scheduled task will never be picked up. Waking up the
// selector would mean that we may be able to recalculate the shutdown to a later date. The cost of not
// doing the recalculation is one potentially unnecessary wakeup which is exactly what we're
// saving here. So in the worst case, we didn't do a performance optimisation, in the best case, we saved
// one wakeup.
})
do {
try self._schedule0(task)
} catch {
scheduled._promise.fail(error)
}
return scheduled
}
/// - see: `EventLoop.scheduleTask(in:_:)`
@inlinable
internal func scheduleTask<T>(in: TimeAmount, _ task: @escaping () throws -> T) -> Scheduled<T> {
return scheduleTask(deadline: .now() + `in`, task)
}
// - see: `EventLoop.execute`
@inlinable
internal func execute(_ task: @escaping () -> Void) {
// nothing we can do if we fail enqueuing here.
try? self._schedule0(ScheduledTask(task, { error in
// do nothing
}, .now()))
}
/// Add the `ScheduledTask` to be executed.
@usableFromInline
internal func _schedule0(_ task: ScheduledTask) throws {
if self.inEventLoop {
precondition(self._validInternalStateToScheduleTasks,
"BUG IN NIO (please report): EventLoop is shutdown, yet we're on the EventLoop.")
self._tasksLock.withLockVoid {
self._scheduledTasks.push(task)
}
} else {
self.externalStateLock.withLockVoid {
guard self.validExternalStateToScheduleTasks else {
print("ERROR: Cannot schedule tasks on an EventLoop that has already shut down. " +
"This will be upgraded to a forced crash in future SwiftNIO versions.")
return
}
self._tasksLock.withLockVoid {
self._scheduledTasks.push(task)
}
}
// We only need to wake up the selector if we're not in the EventLoop. If we're in the EventLoop already, we're
// either doing IO tasks (which happens before checking the scheduled tasks) or we're running a scheduled task
// already which means that we'll check at least once more if there are other scheduled tasks runnable.
try self._wakeupSelector()
}
}
/// Wake the `Selector` which means `Selector.whenReady(...)` will unblock.
@usableFromInline
internal func _wakeupSelector() throws {
try _selector.wakeup()
}
/// Handle the given `SelectorEventSet` for the `SelectableChannel`.
internal final func handleEvent<C: SelectableChannel>(_ ev: SelectorEventSet, channel: C) {
guard channel.isOpen else {
return
}
// process resets first as they'll just cause the writes to fail anyway.
if ev.contains(.reset) {
channel.reset()
} else {
if ev.contains(.writeEOF) {
channel.writeEOF()
guard channel.isOpen else {
return
}
} else if ev.contains(.write) {
channel.writable()
guard channel.isOpen else {
return
}
}
if ev.contains(.readEOF) {
channel.readEOF()
} else if ev.contains(.read) {
channel.readable()
}
}
}
private func currentSelectorStrategy(nextReadyTask: ScheduledTask?) -> SelectorStrategy {
guard let sched = nextReadyTask else {
// No tasks to handle so just block. If any tasks were added in the meantime wakeup(...) was called and so this
// will directly unblock.
return .block
}
let nextReady = sched.readyIn(.now())
if nextReady <= .nanoseconds(0) {
// Something is ready to be processed just do a non-blocking select of events.
return .now
} else {
return .blockUntilTimeout(nextReady)
}
}
private func obfuscatePointerValue<T>(_ future: EventLoopFuture<T>) -> UInt {
// Note:
// 1. 0xbf15ca5d is randomly picked such that it fits into both 32 and 64 bit address spaces
// 2. XOR with 0xbf15ca5d so that Memory Graph Debugger and other memory debugging tools
// won't see it as a reference.
return UInt(bitPattern: ObjectIdentifier(future)) ^ 0xbf15ca5d
}
/// Start processing I/O and tasks for this `SelectableEventLoop`. This method will continue running (and so block) until the `SelectableEventLoop` is closed.
internal func run() throws {
self.preconditionInEventLoop()
defer {
var scheduledTasksCopy = ContiguousArray<ScheduledTask>()
var iterations = 0
repeat { // We may need to do multiple rounds of this because failing tasks may lead to more work.
scheduledTasksCopy.removeAll(keepingCapacity: true)
self._tasksLock.withLockVoid {
// reserve the correct capacity so we don't need to realloc later on.
scheduledTasksCopy.reserveCapacity(self._scheduledTasks.count)
while let sched = self._scheduledTasks.pop() {
scheduledTasksCopy.append(sched)
}
}
// Fail all the scheduled tasks.
for task in scheduledTasksCopy {
task.fail(EventLoopError.shutdown)
}
iterations += 1
} while scheduledTasksCopy.count > 0 && iterations < 1000
precondition(scheduledTasksCopy.count == 0, "EventLoop \(self) didn't quiesce after 1000 ticks.")
assert(self.internalState == .noLongerRunning, "illegal state: \(self.internalState)")
self.internalState = .exitingThread
}
var nextReadyTask: ScheduledTask? = nil
self._tasksLock.withLock {
if let firstTask = self._scheduledTasks.peek() {
// The reason this is necessary is a very interesting race:
// In theory (and with `makeEventLoopFromCallingThread` even in practise), we could publish an
// `EventLoop` reference _before_ the EL thread has entered the `run` function.
// If that is the case, we need to schedule the first wakeup at the ready time for this task that was
// enqueued really early on, so let's do that :).
nextReadyTask = firstTask
}
}
while self.internalState != .noLongerRunning && self.internalState != .exitingThread {
// Block until there are events to handle or the selector was woken up
/* for macOS: in case any calls we make to Foundation put objects into an autoreleasepool */
try withAutoReleasePool {
try self._selector.whenReady(strategy: currentSelectorStrategy(nextReadyTask: nextReadyTask)) { ev in
switch ev.registration {
case .serverSocketChannel(let chan, _):
self.handleEvent(ev.io, channel: chan)
case .socketChannel(let chan, _):
self.handleEvent(ev.io, channel: chan)
case .datagramChannel(let chan, _):
self.handleEvent(ev.io, channel: chan)
case .pipeChannel(let chan, let direction, _):
var ev = ev
if ev.io.contains(.reset) {
// .reset needs special treatment here because we're dealing with two separate pipes instead
// of one socket. So we turn .reset input .readEOF/.writeEOF.
ev.io.subtract([.reset])
ev.io.formUnion([direction == .input ? .readEOF : .writeEOF])
}
self.handleEvent(ev.io, channel: chan)
}
}
}
// We need to ensure we process all tasks, even if a task added another task again
while true {
// TODO: Better locking
self._tasksLock.withLockVoid {
if !self._scheduledTasks.isEmpty {
// We only fetch the time one time as this may be expensive and is generally good enough as if we miss anything we will just do a non-blocking select again anyway.
let now: NIODeadline = .now()
// Make a copy of the tasks so we can execute these while not holding the lock anymore
while tasksCopy.count < tasksCopy.capacity, let task = self._scheduledTasks.peek() {
if task.readyIn(now) <= .nanoseconds(0) {
self._scheduledTasks.pop()
self.tasksCopy.append(task.task)
} else {
nextReadyTask = task
break
}
}
} else {
// Reset nextReadyTask to nil which means we will do a blocking select.
nextReadyTask = nil
}
}
// all pending tasks are set to occur in the future, so we can stop looping.
if self.tasksCopy.isEmpty {
break
}
// Execute all the tasks that were summited
for task in self.tasksCopy {
/* for macOS: in case any calls we make to Foundation put objects into an autoreleasepool */
withAutoReleasePool {
task()
}
}
// Drop everything (but keep the capacity) so we can fill it again on the next iteration.
self.tasksCopy.removeAll(keepingCapacity: true)
}
}
// This EventLoop was closed so also close the underlying selector.
try self._selector.close()
}
internal func initiateClose(queue: DispatchQueue, completionHandler: @escaping (Result<Void, Error>) -> Void) {
func doClose() {
self.assertInEventLoop()
// There should only ever be one call into this function so we need to be up and running, ...
assert(self.internalState == .runningAndAcceptingNewRegistrations)
self.internalState = .runningButNotAcceptingNewRegistrations
self.externalStateLock.withLock {
// ... but before this call happened, the lifecycle state should have been changed on some other thread.
assert(self.externalState == .closing)
}
self._selector.closeGently(eventLoop: self).whenComplete { result in
self.assertInEventLoop()
assert(self.internalState == .runningButNotAcceptingNewRegistrations)
self.internalState = .noLongerRunning
self.execute {} // force a new event loop tick, so the event loop definitely stops looping very soon.
self.externalStateLock.withLock {
assert(self.externalState == .closing)
self.externalState = .closed
}
queue.async {
completionHandler(result)
}
}
}
if self.inEventLoop {
queue.async {
self.initiateClose(queue: queue, completionHandler: completionHandler)
}
} else {
let goAhead = self.externalStateLock.withLock { () -> Bool in
if self.externalState == .open {
self.externalState = .closing
return true
} else {
return false
}
}
guard goAhead else {
queue.async {
completionHandler(Result.failure(EventLoopError.shutdown))
}
return
}
self.execute {
doClose()
}
}
}
internal func syncFinaliseClose(joinThread: Bool) {
// This may not be true in the future but today we need to join all ELs that can't be shut down individually.
assert(joinThread != self.canBeShutdownIndividually)
let goAhead = self.externalStateLock.withLock { () -> Bool in
switch self.externalState {
case .closed:
self.externalState = .reclaimingResources
return true
case .resourcesReclaimed, .reclaimingResources:
return false
default:
preconditionFailure("illegal lifecycle state in syncFinaliseClose: \(self.externalState)")
}
}
guard goAhead else {
return
}
if joinThread {
self.thread.join()
}
self.externalStateLock.withLock {
precondition(self.externalState == .reclaimingResources)
self.externalState = .resourcesReclaimed
}
}
@usableFromInline
func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
if self.canBeShutdownIndividually {
self.initiateClose(queue: queue) { result in
self.syncFinaliseClose(joinThread: false) // This thread was taken over by somebody else
switch result {
case .success:
callback(nil)
case .failure(let error):
callback(error)
}
}
} else {
// This function is never called legally because the only possibly owner of an `SelectableEventLoop` is
// `MultiThreadedEventLoopGroup` which calls `initiateClose` followed by `syncFinaliseClose`.
queue.async {
callback(EventLoopError.unsupportedOperation)
}
}
}
}
extension SelectableEventLoop: CustomStringConvertible, CustomDebugStringConvertible {
@usableFromInline
var description: String {
return "SelectableEventLoop { selector = \(self._selector), thread = \(self.thread) }"
}
@usableFromInline
var debugDescription: String {
return self._tasksLock.withLock {
return "SelectableEventLoop { selector = \(self._selector), thread = \(self.thread), scheduledTasks = \(self._scheduledTasks.description) }"
}
}
}