mark syncShutdownGracefully noasync (#2381)

mark syncShutdownGracefully noasync

Motivation:

The code as-is blocks the calling thread.

Modifications:

* mark `EventLoopGroup.syncShutdownGracefully()` and `NIOThreadPool.syncShutdownGracefully()` noasync on Swift > 5.7
* offer NIOThreadPool.shutdownGracefully()
* add renamed to syncShutdownGracefully()
This commit is contained in:
Rick Newton-Rogers 2023-03-02 14:20:23 +00:00 committed by GitHub
parent e81bd62fb6
commit 5f5fa9a2b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 177 additions and 4 deletions

View File

@ -1234,10 +1234,21 @@ extension EventLoopGroup {
self.shutdownGracefully(queue: .global(), callback)
}
#endif
public func syncShutdownGracefully() throws {
self._preconditionSafeToSyncShutdown(file: #fileID, line: #line)
#if swift(>=5.7)
@available(*, noasync, message: "this can end up blocking the calling thread", renamed: "shutdownGracefully()")
public func syncShutdownGracefully() throws {
try self._syncShutdownGracefully()
}
#else
public func syncShutdownGracefully() throws {
try self._syncShutdownGracefully()
}
#endif
private func _syncShutdownGracefully() throws {
self._preconditionSafeToSyncShutdown(file: #fileID, line: #line)
let errorStorageLock = NIOLock()
var errorStorage: Error? = nil
let continuation = DispatchWorkItem {}

View File

@ -310,7 +310,33 @@ extension NIOThreadPool {
}
#endif
/// Shuts down the thread pool gracefully.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
@inlinable
public func shutdownGracefully() async throws {
return try await withCheckedThrowingContinuation { cont in
self.shutdownGracefully { error in
if let error = error {
cont.resume(throwing: error)
} else {
cont.resume()
}
}
}
}
#if swift(>=5.7)
@available(*, noasync, message: "this can end up blocking the calling thread", renamed: "shutdownGracefully()")
public func syncShutdownGracefully() throws {
try self._syncShutdownGracefully()
}
#else
public func syncShutdownGracefully() throws {
try self._syncShutdownGracefully()
}
#endif
private func _syncShutdownGracefully() throws {
let errorStorageLock = NIOLock()
var errorStorage: Swift.Error? = nil
let continuation = DispatchWorkItem {}

View File

@ -2,7 +2,7 @@
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
// Copyright (c) 2017-2023 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
@ -29,6 +29,7 @@ extension NIOThreadPoolTest {
return [
("testThreadNamesAreSetUp", testThreadNamesAreSetUp),
("testThreadPoolStartsMultipleTimes", testThreadPoolStartsMultipleTimes),
("testAsyncShutdownWorks", testAsyncShutdownWorks),
]
}
}

View File

@ -16,6 +16,7 @@ import XCTest
@testable import NIOPosix
import Dispatch
import NIOConcurrencyHelpers
import NIOEmbedded
class NIOThreadPoolTest: XCTestCase {
func testThreadNamesAreSetUp() {
@ -108,4 +109,20 @@ class NIOThreadPoolTest: XCTestCase {
XCTAssertEqual(threadOne, threadTwo)
}
}
func testAsyncShutdownWorks() throws {
guard #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) else { throw XCTSkip() }
XCTAsyncTest {
let threadPool = NIOThreadPool(numberOfThreads: 17)
let eventLoop = NIOAsyncTestingEventLoop()
threadPool.start()
try await threadPool.shutdownGracefully()
let future = threadPool.runIfActive(eventLoop: eventLoop) {
XCTFail("This shouldn't run because the pool is shutdown.")
}
await XCTAssertThrowsError(try await future.get())
}
}
}

View File

@ -0,0 +1,118 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2022 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
//
//===----------------------------------------------------------------------===//
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
/*
* Copyright 2021, gRPC Authors All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import XCTest
extension XCTestCase {
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
/// Cross-platform XCTest support for async-await tests.
///
/// Currently the Linux implementation of XCTest doesn't have async-await support.
/// Until it does, we make use of this shim which uses a detached `Task` along with
/// `XCTest.wait(for:timeout:)` to wrap the operation.
///
/// - NOTE: Support for Linux is tracked by https://bugs.swift.org/browse/SR-14403.
/// - NOTE: Implementation currently in progress: https://github.com/apple/swift-corelibs-xctest/pull/326
func XCTAsyncTest(
expectationDescription: String = "Async operation",
timeout: TimeInterval = 30,
file: StaticString = #filePath,
line: UInt = #line,
function: StaticString = #function,
operation: @escaping @Sendable () async throws -> Void
) {
let expectation = self.expectation(description: expectationDescription)
Task {
do {
try await operation()
} catch {
XCTFail("Error thrown while executing \(function): \(error)", file: file, line: line)
Thread.callStackSymbols.forEach { print($0) }
}
expectation.fulfill()
}
self.wait(for: [expectation], timeout: timeout)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
internal func XCTAssertThrowsError<T>(
_ expression: @autoclosure () async throws -> T,
file: StaticString = #filePath,
line: UInt = #line,
verify: (Error) -> Void = { _ in }
) async {
do {
_ = try await expression()
XCTFail("Expression did not throw error", file: file, line: line)
} catch {
verify(error)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
internal func XCTAssertNoThrow<T>(
_ expression: @autoclosure () async throws -> T,
file: StaticString = #file,
line: UInt = #line
) async {
do {
_ = try await expression()
} catch {
XCTFail("Expression did throw error", file: file, line: line)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
internal func XCTAssertNoThrowWithResult<Result>(
_ expression: @autoclosure () async throws -> Result,
file: StaticString = #filePath,
line: UInt = #line
) async -> Result? {
do {
return try await expression()
} catch {
XCTFail("Expression did throw: \(error)", file: file, line: line)
}
return nil
}