Concurrency: enable 6.0 language mode / concurrency safety

Swift 6 language mode enables complete concurrency checking and will
error when races are found.

Also fix a few types to be 6 language mode compatible.

fix licenseignore to ignore Package-swift...

fix formatting

fix docs in NIOLockedValueBox
This commit is contained in:
Konrad Malawski
2025-09-03 14:56:08 +09:00
parent e9ca4f7432
commit e4e7b41c1c
4 changed files with 142 additions and 47 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
*.json
Package.swift
**/Package.swift
Package@-*.swift
Package@swift*.swift
**/Package@-*.swift
Package.resolved
**/Package.resolved
+52
View File
@@ -0,0 +1,52 @@
// swift-tools-version:6.0
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftPrometheus open source project
//
// Copyright (c) 2018-2025 SwiftPrometheus project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftPrometheus project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import PackageDescription
let package = Package(
name: "swift-prometheus",
platforms: [.macOS(.v13), .iOS(.v16), .watchOS(.v9), .tvOS(.v16)],
products: [
.library(
name: "Prometheus",
targets: ["Prometheus"]
)
],
dependencies: [
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
.package(url: "https://github.com/apple/swift-metrics.git", from: "2.4.1"),
],
targets: [
.target(
name: "Prometheus",
dependencies: [
.product(name: "Atomics", package: "swift-atomics"),
.product(name: "CoreMetrics", package: "swift-metrics"),
]
),
.testTarget(
name: "PrometheusTests",
dependencies: [
"Prometheus"
]
),
]
)
for target in package.targets {
var settings = target.swiftSettings ?? []
settings.append(.enableExperimentalFeature("StrictConcurrency=complete"))
target.swiftSettings = settings
}
+40 -38
View File
@@ -31,9 +31,16 @@ import Darwin
import ucrt
import WinSDK
#elseif canImport(Glibc)
import Glibc
@preconcurrency import Glibc
#elseif canImport(Musl)
import Musl
@preconcurrency import Musl
#elseif canImport(Bionic)
@preconcurrency import Bionic
#elseif canImport(WASILibc)
@preconcurrency import WASILibc
#if canImport(wasi_pthread)
import wasi_pthread
#endif
#else
#error("The concurrency NIOLock module was unable to identify your C library.")
#endif
@@ -47,7 +54,7 @@ typealias LockPrimitive = pthread_mutex_t
#endif
@usableFromInline
enum LockOperations {}
enum LockOperations: Sendable {}
extension LockOperations {
@inlinable
@@ -56,12 +63,15 @@ extension LockOperations {
#if os(Windows)
InitializeSRWLock(mutex)
#else
#elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded))
var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)
debugOnly {
pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK))
}
assert(
{
pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK))
return true
}()
)
let err = pthread_mutex_init(mutex, &attr)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
@@ -74,7 +84,7 @@ extension LockOperations {
#if os(Windows)
// SRWLOCK does not need to be free'd
#else
#elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded))
let err = pthread_mutex_destroy(mutex)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
@@ -86,7 +96,7 @@ extension LockOperations {
#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
#elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded))
let err = pthread_mutex_lock(mutex)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
@@ -98,7 +108,7 @@ extension LockOperations {
#if os(Windows)
ReleaseSRWLockExclusive(mutex)
#else
#elseif (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && _runtime(_multithreaded))
let err = pthread_mutex_unlock(mutex)
precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
#endif
@@ -139,9 +149,11 @@ final class LockStorage<Value>: ManagedBuffer<Value, LockPrimitive> {
@inlinable
static func create(value: Value) -> Self {
let buffer = Self.create(minimumCapacity: 1) { _ in
return value
value
}
// Avoid 'unsafeDowncast' as there is a miscompilation on 5.10.
// Intentionally using a force cast here to avoid a miss compiliation in 5.10.
// This is as fast as an unsafeDownCast since ManagedBuffer is inlined and the optimizer
// can eliminate the upcast/downcast pair
let storage = buffer as! Self
storage.withUnsafeMutablePointers { _, lockPtr in
@@ -175,7 +187,7 @@ final class LockStorage<Value>: ManagedBuffer<Value, LockPrimitive> {
@inlinable
func withLockPrimitive<T>(_ body: (UnsafeMutablePointer<LockPrimitive>) throws -> T) rethrows -> T {
try self.withUnsafeMutablePointerToElements { lockPtr in
return try body(lockPtr)
try body(lockPtr)
}
}
@@ -189,23 +201,28 @@ final class LockStorage<Value>: ManagedBuffer<Value, LockPrimitive> {
}
}
extension LockStorage: @unchecked Sendable {}
// This compiler guard is here becaue `ManagedBuffer` is already declaring
// Sendable unavailability after 6.1, which `LockStorage` inherits.
#if compiler(<6.2)
@available(*, unavailable)
extension LockStorage: Sendable {}
#endif
/// A threading lock based on `libpthread` instead of `libdispatch`.
///
/// - note: ``NIOLock`` has reference semantics.
/// - Note: ``NIOLock`` has reference semantics.
///
/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
/// of lock is safe to use with `libpthread`-based threading models, such as the
/// one used by NIO. On Windows, the lock is based on the substantially similar
/// `SRWLOCK` type.
struct NIOLock {
public struct NIOLock {
@usableFromInline
internal let _storage: LockStorage<Void>
/// Create a new lock.
@inlinable
init() {
public init() {
self._storage = .create(value: ())
}
@@ -214,7 +231,7 @@ struct NIOLock {
/// Whenever possible, consider using `withLock` instead of this method and
/// `unlock`, to simplify lock handling.
@inlinable
func lock() {
public func lock() {
self._storage.lock()
}
@@ -223,13 +240,13 @@ struct NIOLock {
/// Whenever possible, consider using `withLock` instead of this method and
/// `lock`, to simplify lock handling.
@inlinable
func unlock() {
public func unlock() {
self._storage.unlock()
}
@inlinable
internal func withLockPrimitive<T>(_ body: (UnsafeMutablePointer<LockPrimitive>) throws -> T) rethrows -> T {
return try self._storage.withLockPrimitive(body)
try self._storage.withLockPrimitive(body)
}
}
@@ -243,7 +260,7 @@ extension NIOLock {
/// - Parameter body: The block to execute while holding the lock.
/// - Returns: The value returned by the block.
@inlinable
func withLock<T>(_ body: () throws -> T) rethrows -> T {
public func withLock<T>(_ body: () throws -> T) rethrows -> T {
self.lock()
defer {
self.unlock()
@@ -252,12 +269,12 @@ extension NIOLock {
}
@inlinable
func withLockVoid(_ body: () throws -> Void) rethrows {
public func withLockVoid(_ body: () throws -> Void) rethrows {
try self.withLock(body)
}
}
extension NIOLock: Sendable {}
extension NIOLock: @unchecked Sendable {}
extension UnsafeMutablePointer {
@inlinable
@@ -265,18 +282,3 @@ extension UnsafeMutablePointer {
assert(UInt(bitPattern: self) % UInt(MemoryLayout<Pointee>.alignment) == 0)
}
}
/// A utility function that runs the body code only in debug builds, without
/// emitting compiler warnings.
///
/// This is currently the only way to do this in Swift: see
/// https://forums.swift.org/t/support-debug-only-code/11037 for a discussion.
@inlinable
internal func debugOnly(_ body: () -> Void) {
assert(
{
body()
return true
}()
)
}
+49 -8
View File
@@ -27,30 +27,71 @@
/// Provides locked access to `Value`.
///
/// - note: ``NIOLockedValueBox`` has reference semantics and holds the `Value`
/// - Note: ``NIOLockedValueBox`` has reference semantics and holds the `Value`
/// alongside a lock behind a reference.
///
/// This is no different than creating a ``Lock`` and protecting all
/// This is no different than creating a `Lock` and protecting all
/// accesses to a value using the lock. But it's easy to forget to actually
/// acquire/release the lock in the correct place. ``NIOLockedValueBox`` makes
/// that much easier.
@usableFromInline
struct NIOLockedValueBox<Value> {
public struct NIOLockedValueBox<Value> {
@usableFromInline
internal let _storage: LockStorage<Value>
/// Initialize the `Value`.
@inlinable
init(_ value: Value) {
public init(_ value: Value) {
self._storage = .create(value: value)
}
/// Access the `Value`, allowing mutation of it.
@inlinable
func withLockedValue<T>(_ mutate: (inout Value) throws -> T) rethrows -> T {
return try self._storage.withLockedValue(mutate)
public func withLockedValue<T>(_ mutate: (inout Value) throws -> T) rethrows -> T {
try self._storage.withLockedValue(mutate)
}
/// Provides an unsafe view over the lock and its value.
///
/// This can be beneficial when you require fine grained control over the lock in some
/// situations but don't want lose the benefits of ``withLockedValue(_:)`` in others by
/// switching to ``NIOLock``.
public var unsafe: Unsafe {
Unsafe(_storage: self._storage)
}
/// Provides an unsafe view over the lock and its value.
public struct Unsafe {
@usableFromInline
let _storage: LockStorage<Value>
/// Manually acquire the lock.
@inlinable
public func lock() {
self._storage.lock()
}
/// Manually release the lock.
@inlinable
public func unlock() {
self._storage.unlock()
}
/// Mutate the value, assuming the lock has been acquired manually.
///
/// - Parameter mutate: A closure with scoped access to the value.
/// - Returns: The result of the `mutate` closure.
@inlinable
public func withValueAssumingLockIsAcquired<Result>(
_ mutate: (_ value: inout Value) throws -> Result
) rethrows -> Result {
try self._storage.withUnsafeMutablePointerToHeader { value in
try mutate(&value.pointee)
}
}
}
}
extension NIOLockedValueBox: Sendable where Value: Sendable {}
extension NIOLockedValueBox: @unchecked Sendable where Value: Sendable {}
extension NIOLockedValueBox.Unsafe: @unchecked Sendable where Value: Sendable {}