Skip to content

Commit 434baa9

Browse files
authored
Basics: make ObservabilityScope conform to Sendable (#6089)
New `actor HTTPClient` can't take `ObservabilityScope` as an argument to its functions as it's not `Sendable`. This is the only major API difference between `HTTPClient` and `LegacyHTTPClient`, which we'd like to resolve. Marked `SwiftToolObservabilityHandler.OutputHandler`, `ThreadSafeBox`, `ThreadSafeArrayStore`, and `ThreadSafeKeyValueStore` as `@unchecked Sendable`. Made `ObservabilityMetadata: Sendable` by requiring its keys and values to be sendable. Added additional `Sendable` requirement on `DiagnosticsHandler`. This allows passing `ObservabilityScope` instances to `HTTPClient/execute`.
1 parent 0d175a1 commit 434baa9

File tree

5 files changed

+40
-128
lines changed

5 files changed

+40
-128
lines changed

Sources/Basics/HTTPClient/HTTPClient.swift

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ public actor HTTPClient {
5151
///
5252
/// - Parameters:
5353
/// - request: The ``HTTPClientRequest`` to perform.
54+
/// - observabilityScope: the observability scope to emit diagnostics on.
5455
/// - progress: A closure to handle response download progress.
5556
/// - Returns: A response value returned by underlying ``HTTPClient.Implementation``.
5657
public func execute(
5758
_ request: Request,
59+
observabilityScope: ObservabilityScope? = nil,
5860
progress: ProgressHandler? = nil
5961
) async throws -> Response {
6062
// merge configuration
@@ -85,16 +87,18 @@ public actor HTTPClient {
8587
request.headers.add(name: "Authorization", value: authorization)
8688
}
8789

88-
return try await executeWithStrategies(request: request, progress: progress, requestNumber: 0)
90+
return try await executeWithStrategies(request: request, requestNumber: 0, observabilityScope, progress)
8991
}
9092

9193
private func executeWithStrategies(
9294
request: Request,
93-
progress: ProgressHandler?,
94-
requestNumber: Int
95+
requestNumber: Int,
96+
_ observabilityScope: ObservabilityScope?,
97+
_ progress: ProgressHandler?
9598
) async throws -> Response {
9699
// apply circuit breaker if necessary
97100
if self.shouldCircuitBreak(request: request) {
101+
observabilityScope?.emit(warning: "Circuit breaker triggered for \(request.url)")
98102
throw HTTPClientError.circuitBreakerTriggered
99103
}
100104

@@ -115,17 +119,19 @@ public actor HTTPClient {
115119
self.recordErrorIfNecessary(response: response, request: request)
116120

117121
// handle retry strategy
118-
if let retryDelayInNanoseconds = self.calculateRetry(
122+
if let retryDelay = self.calculateRetry(
119123
response: response,
120124
request: request,
121125
requestNumber: requestNumber
122-
)?.nanoseconds() {
126+
), let retryDelayInNanoseconds = retryDelay.nanoseconds() {
127+
observabilityScope?.emit(warning: "\(request.url) failed, retrying in \(retryDelay)")
123128
try await Task.sleep(nanoseconds: UInt64(retryDelayInNanoseconds))
124129

125130
return try await self.executeWithStrategies(
126131
request: request,
127-
progress: progress,
128-
requestNumber: requestNumber + 1
132+
requestNumber: requestNumber + 1,
133+
observabilityScope,
134+
progress
129135
)
130136
}
131137
// check for valid response codes

Sources/Basics/Observability.swift

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ public class ObservabilitySystem {
3636
}
3737

3838
/// Create an ObservabilitySystem with a single diagnostics handler.
39-
public convenience init(_ handler: @escaping (ObservabilityScope, Diagnostic) -> Void) {
39+
public convenience init(_ handler: @escaping @Sendable (ObservabilityScope, Diagnostic) -> Void) {
4040
self.init(SingleDiagnosticsHandler(handler))
4141
}
4242

4343
private struct SingleDiagnosticsHandler: ObservabilityHandlerProvider, DiagnosticsHandler {
44-
var diagnosticsHandler: DiagnosticsHandler { self }
44+
var diagnosticsHandler: DiagnosticsHandler { self }
4545

46-
let underlying: (ObservabilityScope, Diagnostic) -> Void
46+
let underlying: @Sendable (ObservabilityScope, Diagnostic) -> Void
4747

48-
init(_ underlying: @escaping (ObservabilityScope, Diagnostic) -> Void) {
48+
init(_ underlying: @escaping @Sendable (ObservabilityScope, Diagnostic) -> Void) {
4949
self.underlying = underlying
5050
}
5151

@@ -61,12 +61,12 @@ public protocol ObservabilityHandlerProvider {
6161

6262
// MARK: - ObservabilityScope
6363

64-
public final class ObservabilityScope: DiagnosticsEmitterProtocol, CustomStringConvertible {
64+
public final class ObservabilityScope: DiagnosticsEmitterProtocol, Sendable, CustomStringConvertible {
6565
public let description: String
6666
private let parent: ObservabilityScope?
6767
private let metadata: ObservabilityMetadata?
6868

69-
private var diagnosticsHandler: DiagnosticsHandlerWrapper
69+
private let diagnosticsHandler: DiagnosticsHandlerWrapper
7070

7171
fileprivate init(
7272
description: String,
@@ -150,7 +150,7 @@ public final class ObservabilityScope: DiagnosticsEmitterProtocol, CustomStringC
150150

151151
// MARK: - Diagnostics
152152

153-
public protocol DiagnosticsHandler {
153+
public protocol DiagnosticsHandler: Sendable {
154154
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic)
155155
}
156156

@@ -252,7 +252,7 @@ public struct DiagnosticsEmitter: DiagnosticsEmitterProtocol {
252252
}
253253
}
254254

255-
public struct Diagnostic: CustomStringConvertible {
255+
public struct Diagnostic: Sendable, CustomStringConvertible {
256256
public let severity: Severity
257257
public let message: String
258258
public internal (set) var metadata: ObservabilityMetadata?
@@ -324,7 +324,7 @@ public struct Diagnostic: CustomStringConvertible {
324324
Self(severity: .debug, message: message.description, metadata: metadata)
325325
}
326326

327-
public enum Severity: Comparable {
327+
public enum Severity: Comparable, Sendable {
328328
case error
329329
case warning
330330
case info
@@ -380,10 +380,10 @@ public struct Diagnostic: CustomStringConvertible {
380380
// FIXME: we currently require that Value conforms to CustomStringConvertible which sucks
381381
// ideally Value would conform to Equatable but that has generic requirement
382382
// luckily, this is about to change so we can clean this up soon
383-
public struct ObservabilityMetadata: CustomDebugStringConvertible {
383+
public struct ObservabilityMetadata: Sendable, CustomDebugStringConvertible {
384384
public typealias Key = ObservabilityMetadataKey
385385

386-
private var _storage = [AnyKey: Any]()
386+
private var _storage = [AnyKey: Sendable]()
387387

388388
public init() {}
389389

@@ -414,7 +414,7 @@ public struct ObservabilityMetadata: CustomDebugStringConvertible {
414414
///
415415
/// - Parameter body: The closure to be invoked for each item stored in this `ObservabilityMetadata`,
416416
/// passing the type-erased key and the associated value.
417-
public func forEach(_ body: (AnyKey, Any) throws -> Void) rethrows {
417+
public func forEach(_ body: (AnyKey, Sendable) throws -> Void) rethrows {
418418
try self._storage.forEach { key, value in
419419
try body(key, value)
420420
}
@@ -473,7 +473,7 @@ public struct ObservabilityMetadata: CustomDebugStringConvertible {
473473
}
474474

475475
/// A type-erased `ObservabilityMetadataKey` used when iterating through the `ObservabilityMetadata` using its `forEach` method.
476-
public struct AnyKey {
476+
public struct AnyKey: Sendable {
477477
/// The key's type represented erased to an `Any.Type`.
478478
public let keyType: Any.Type
479479

@@ -485,7 +485,7 @@ public struct ObservabilityMetadata: CustomDebugStringConvertible {
485485

486486
public protocol ObservabilityMetadataKey {
487487
/// The type of value uniquely identified by this key.
488-
associatedtype Value
488+
associatedtype Value: Sendable
489489
}
490490

491491
extension ObservabilityMetadata.AnyKey: Hashable {
@@ -569,11 +569,11 @@ extension ObservabilityMetadata {
569569
}
570570
}
571571

572-
private enum LegacyLocationKey: Key {
572+
private enum LegacyLocationKey: Key, Sendable {
573573
typealias Value = DiagnosticLocationWrapper
574574
}
575575

576-
public struct DiagnosticLocationWrapper: CustomStringConvertible {
576+
public struct DiagnosticLocationWrapper: Sendable, CustomStringConvertible {
577577
let underlying: DiagnosticLocation
578578

579579
public init (_ underlying: DiagnosticLocation) {
@@ -597,11 +597,11 @@ extension ObservabilityMetadata {
597597
}
598598
}
599599

600-
private enum LegacyDataKey: Key {
600+
private enum LegacyDataKey: Key, Sendable {
601601
typealias Value = DiagnosticDataWrapper
602602
}
603603

604-
struct DiagnosticDataWrapper: CustomStringConvertible {
604+
struct DiagnosticDataWrapper: Sendable, CustomStringConvertible {
605605
let underlying: DiagnosticData
606606

607607
public init (_ underlying: DiagnosticData) {

Sources/Basics/SendableTimeInterval.swift

Lines changed: 4 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -11,106 +11,9 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import enum Dispatch.DispatchTimeInterval
14-
import struct Foundation.TimeInterval
1514

16-
public enum SendableTimeInterval: Equatable, Sendable {
17-
case seconds(Int)
18-
case milliseconds(Int)
19-
case microseconds(Int)
20-
case nanoseconds(Int)
21-
case never
15+
extension DispatchTimeInterval: @unchecked Sendable {}
2216

23-
public func timeInterval() -> TimeInterval? {
24-
switch self {
25-
case .seconds(let value):
26-
return Double(value)
27-
case .milliseconds(let value):
28-
return Double(value) / 1000
29-
case .microseconds(let value):
30-
return Double(value) / 1_000_000
31-
case .nanoseconds(let value):
32-
return Double(value) / 1_000_000_000
33-
default:
34-
return nil
35-
}
36-
}
37-
38-
public func nanoseconds() -> Int? {
39-
switch self {
40-
case .seconds(let value):
41-
return value.multipliedReportingOverflow(by: 1_000_000_000).partialValue
42-
case .milliseconds(let value):
43-
return value.multipliedReportingOverflow(by: 1_000_000).partialValue
44-
case .microseconds(let value):
45-
return value.multipliedReportingOverflow(by: 1000).partialValue
46-
case .nanoseconds(let value):
47-
return value
48-
default:
49-
return nil
50-
}
51-
}
52-
53-
public func milliseconds() -> Int? {
54-
switch self {
55-
case .seconds(let value):
56-
return value.multipliedReportingOverflow(by: 1000).partialValue
57-
case .milliseconds(let value):
58-
return value
59-
case .microseconds(let value):
60-
return Int(Double(value) / 1000)
61-
case .nanoseconds(let value):
62-
return Int(Double(value) / 1_000_000)
63-
default:
64-
return nil
65-
}
66-
}
67-
68-
public func seconds() -> Int? {
69-
switch self {
70-
case .seconds(let value):
71-
return value
72-
case .milliseconds(let value):
73-
return Int(Double(value) / 1000)
74-
case .microseconds(let value):
75-
return Int(Double(value) / 1_000_000)
76-
case .nanoseconds(let value):
77-
return Int(Double(value) / 1_000_000_000)
78-
default:
79-
return nil
80-
}
81-
}
82-
83-
public var descriptionInSeconds: String {
84-
switch self {
85-
case .seconds(let value):
86-
return "\(value)s"
87-
case .milliseconds(let value):
88-
return String(format: "%.2f", Double(value)/Double(1000)) + "s"
89-
case .microseconds(let value):
90-
return String(format: "%.2f", Double(value)/Double(1_000_000)) + "s"
91-
case .nanoseconds(let value):
92-
return String(format: "%.2f", Double(value)/Double(1_000_000_000)) + "s"
93-
case .never:
94-
return "n/a"
95-
}
96-
}
97-
}
98-
99-
public extension SendableTimeInterval {
100-
init(_ interval: DispatchTimeInterval) {
101-
switch interval {
102-
case let .seconds(s):
103-
self = .seconds(s)
104-
case let .milliseconds(ms):
105-
self = .milliseconds(ms)
106-
case let .microseconds(us):
107-
self = .microseconds(us)
108-
case let .nanoseconds(ns):
109-
self = .nanoseconds(ns)
110-
case .never:
111-
self = .never
112-
@unknown default:
113-
fatalError("unknown case of `DispatchTimeInterval`")
114-
}
115-
}
116-
}
17+
/// This typealias hides `DispatchTimeInterval` as an implementation detail until we can use `Swift.Duration`, as the
18+
/// latter requires macOS 13.
19+
public typealias SendableTimeInterval = DispatchTimeInterval

Sources/CoreCommands/SwiftToolObservabilityHandler.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public struct SwiftToolObservabilityHandler: ObservabilityHandlerProvider {
5757
self.outputHandler.wait(timeout: timeout)
5858
}
5959

60-
struct OutputHandler: DiagnosticsHandler {
60+
struct OutputHandler {
6161
private let logLevel: Diagnostic.Severity
6262
internal let outputStream: ThreadSafeOutputByteStream
6363
private let writer: InteractiveWriter
@@ -159,6 +159,9 @@ public struct SwiftToolObservabilityHandler: ObservabilityHandlerProvider {
159159
}
160160
}
161161

162+
extension SwiftToolObservabilityHandler.OutputHandler: @unchecked Sendable {}
163+
extension SwiftToolObservabilityHandler.OutputHandler: DiagnosticsHandler {}
164+
162165
/// This type is used to write on the underlying stream.
163166
///
164167
/// If underlying stream is a not tty, the string will be written in without any

Sources/SPMBuildCore/PluginInvocation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,7 @@ extension ObservabilityMetadata {
10091009
}
10101010
}
10111011

1012-
public struct FileLocation: Equatable, CustomStringConvertible {
1012+
public struct FileLocation: Equatable, CustomStringConvertible, Sendable {
10131013
public let file: AbsolutePath
10141014
public let line: Int?
10151015

0 commit comments

Comments
 (0)