From e4e7b41c1c840a79082de969d340fa6b32eaa9f4 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 3 Sep 2025 14:56:08 +0900 Subject: [PATCH 1/2] 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 --- .licenseignore | 2 +- Package@swift-6.0.swift | 52 +++++++++++++++ Sources/Prometheus/NIOLock.swift | 78 +++++++++++----------- Sources/Prometheus/NIOLockedValueBox.swift | 57 +++++++++++++--- 4 files changed, 142 insertions(+), 47 deletions(-) create mode 100644 Package@swift-6.0.swift diff --git a/.licenseignore b/.licenseignore index d2535f5..9bafa9f 100644 --- a/.licenseignore +++ b/.licenseignore @@ -17,7 +17,7 @@ *.json Package.swift **/Package.swift -Package@-*.swift +Package@swift*.swift **/Package@-*.swift Package.resolved **/Package.resolved diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 0000000..d03fdaf --- /dev/null +++ b/Package@swift-6.0.swift @@ -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 +} diff --git a/Sources/Prometheus/NIOLock.swift b/Sources/Prometheus/NIOLock.swift index 3a80b6c..83b0f90 100644 --- a/Sources/Prometheus/NIOLock.swift +++ b/Sources/Prometheus/NIOLock.swift @@ -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: ManagedBuffer { @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: ManagedBuffer { @inlinable func withLockPrimitive(_ body: (UnsafeMutablePointer) throws -> T) rethrows -> T { try self.withUnsafeMutablePointerToElements { lockPtr in - return try body(lockPtr) + try body(lockPtr) } } @@ -189,23 +201,28 @@ final class LockStorage: ManagedBuffer { } } -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 /// 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(_ body: (UnsafeMutablePointer) 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(_ body: () throws -> T) rethrows -> T { + public func withLock(_ 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.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 - }() - ) -} diff --git a/Sources/Prometheus/NIOLockedValueBox.swift b/Sources/Prometheus/NIOLockedValueBox.swift index 9dcef47..86110d3 100644 --- a/Sources/Prometheus/NIOLockedValueBox.swift +++ b/Sources/Prometheus/NIOLockedValueBox.swift @@ -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 { +public struct NIOLockedValueBox { @usableFromInline internal let _storage: LockStorage /// 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(_ mutate: (inout Value) throws -> T) rethrows -> T { - return try self._storage.withLockedValue(mutate) + public func withLockedValue(_ 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 + + /// 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( + _ 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 {} From 987b1b5ae9df0ec06f12a6bc9bc11e5d9f222918 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 3 Sep 2025 17:34:34 +0900 Subject: [PATCH 2/2] don't accidentally make the locks public --- Sources/Prometheus/NIOLock.swift | 12 ++++++------ Sources/Prometheus/NIOLockedValueBox.swift | 17 +++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Sources/Prometheus/NIOLock.swift b/Sources/Prometheus/NIOLock.swift index 83b0f90..69bb58f 100644 --- a/Sources/Prometheus/NIOLock.swift +++ b/Sources/Prometheus/NIOLock.swift @@ -216,13 +216,13 @@ extension LockStorage: Sendable {} /// 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. -public struct NIOLock { +struct NIOLock { @usableFromInline internal let _storage: LockStorage /// Create a new lock. @inlinable - public init() { + init() { self._storage = .create(value: ()) } @@ -231,7 +231,7 @@ public struct NIOLock { /// Whenever possible, consider using `withLock` instead of this method and /// `unlock`, to simplify lock handling. @inlinable - public func lock() { + func lock() { self._storage.lock() } @@ -240,7 +240,7 @@ public struct NIOLock { /// Whenever possible, consider using `withLock` instead of this method and /// `lock`, to simplify lock handling. @inlinable - public func unlock() { + func unlock() { self._storage.unlock() } @@ -260,7 +260,7 @@ extension NIOLock { /// - Parameter body: The block to execute while holding the lock. /// - Returns: The value returned by the block. @inlinable - public func withLock(_ body: () throws -> T) rethrows -> T { + func withLock(_ body: () throws -> T) rethrows -> T { self.lock() defer { self.unlock() @@ -269,7 +269,7 @@ extension NIOLock { } @inlinable - public func withLockVoid(_ body: () throws -> Void) rethrows { + func withLockVoid(_ body: () throws -> Void) rethrows { try self.withLock(body) } } diff --git a/Sources/Prometheus/NIOLockedValueBox.swift b/Sources/Prometheus/NIOLockedValueBox.swift index 86110d3..129f963 100644 --- a/Sources/Prometheus/NIOLockedValueBox.swift +++ b/Sources/Prometheus/NIOLockedValueBox.swift @@ -34,20 +34,21 @@ /// 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. -public struct NIOLockedValueBox { +@usableFromInline +struct NIOLockedValueBox { @usableFromInline internal let _storage: LockStorage /// Initialize the `Value`. @inlinable - public init(_ value: Value) { + init(_ value: Value) { self._storage = .create(value: value) } /// Access the `Value`, allowing mutation of it. @inlinable - public func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { + func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { try self._storage.withLockedValue(mutate) } @@ -56,24 +57,24 @@ public struct NIOLockedValueBox { /// 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 { + var unsafe: Unsafe { Unsafe(_storage: self._storage) } /// Provides an unsafe view over the lock and its value. - public struct Unsafe { + struct Unsafe { @usableFromInline let _storage: LockStorage /// Manually acquire the lock. @inlinable - public func lock() { + func lock() { self._storage.lock() } /// Manually release the lock. @inlinable - public func unlock() { + func unlock() { self._storage.unlock() } @@ -82,7 +83,7 @@ public struct NIOLockedValueBox { /// - Parameter mutate: A closure with scoped access to the value. /// - Returns: The result of the `mutate` closure. @inlinable - public func withValueAssumingLockIsAcquired( + func withValueAssumingLockIsAcquired( _ mutate: (_ value: inout Value) throws -> Result ) rethrows -> Result { try self._storage.withUnsafeMutablePointerToHeader { value in