From a56fb840bed99950eb972b35cc2ff67a237ec890 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 3 Apr 2025 15:25:23 -0400 Subject: [PATCH 01/35] enable swift 6.0 language mode --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index f878e6f40..98221e24a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 import Foundation import PackageDescription @@ -56,7 +56,7 @@ let package = Package( ], dependencies: { var dependencies: [Package.Dependency] = [ - .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.48.0"), + .package(url: "https://github.com/awslabs/aws-crt-swift.git", branch: "swift6-support"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ] let isDocCEnabled = ProcessInfo.processInfo.environment["AWS_SWIFT_SDK_ENABLE_DOCC"] != nil From 8761073b4534a90c1323bb3d0fe14a186dbf9637 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 3 Apr 2025 15:42:36 -0400 Subject: [PATCH 02/35] try modifying SwiftDependency --- .../software/amazon/smithy/swift/codegen/SwiftDependency.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt index e1981aed8..50c078791 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt @@ -55,7 +55,7 @@ class SwiftDependency( val CRT = SwiftDependency( "AwsCommonRuntimeKit", - null, + "swift6-support", "0.31.0", "https://github.com/awslabs/aws-crt-swift", "", From dad05853ed24a8560e8c0a778c0a2a3bffe2dc2e Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 3 Apr 2025 16:02:20 -0400 Subject: [PATCH 03/35] make WaiterError's Output: Sendable --- Sources/SmithyWaitersAPI/WaiterErrors.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SmithyWaitersAPI/WaiterErrors.swift b/Sources/SmithyWaitersAPI/WaiterErrors.swift index 736fdc758..e4a769f68 100644 --- a/Sources/SmithyWaitersAPI/WaiterErrors.swift +++ b/Sources/SmithyWaitersAPI/WaiterErrors.swift @@ -11,7 +11,7 @@ public struct WaiterConfigurationError: Error { } /// Thrown when a waiter matches a failure acceptor on an attempt to call the waited operation. -public struct WaiterFailureError: Error { +public struct WaiterFailureError: Error { /// The number of attempts at the operation that were made while waiting. public let attempts: Int From dc98b6d54604c832692c2dc262fe46872a7b0c04 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 3 Apr 2025 16:09:21 -0400 Subject: [PATCH 04/35] more sendable --- Sources/SmithyWaitersAPI/WaiterRetryer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SmithyWaitersAPI/WaiterRetryer.swift b/Sources/SmithyWaitersAPI/WaiterRetryer.swift index 4f8cdae4f..0ebd625cd 100644 --- a/Sources/SmithyWaitersAPI/WaiterRetryer.swift +++ b/Sources/SmithyWaitersAPI/WaiterRetryer.swift @@ -11,7 +11,7 @@ import Foundation /// Called by the `Waiter` to make the initial request, then called again for an additional request when retry /// is needed. /// A custom closure may be injected at initialization to allow `WaiterRetryer` to be used as a mock. -struct WaiterRetryer { +struct WaiterRetryer { typealias WaitThenRequest = (WaiterScheduler, Input, WaiterConfiguration, (Input) async throws -> Output) async throws -> WaiterOutcome? From f759bdb9af193b487e1409d170cecdb68118fa95 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 3 Apr 2025 16:12:09 -0400 Subject: [PATCH 05/35] more sendable --- Sources/SmithyWaitersAPI/Waiter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SmithyWaitersAPI/Waiter.swift b/Sources/SmithyWaitersAPI/Waiter.swift index c25ee8444..790888591 100644 --- a/Sources/SmithyWaitersAPI/Waiter.swift +++ b/Sources/SmithyWaitersAPI/Waiter.swift @@ -11,7 +11,7 @@ import Foundation /// the desired state will never be reached. /// Intended to be a generic type for use when waiting on any Smithy `operation`. /// May be reused for multiple waits, including concurrent operations. -public class Waiter { +public class Waiter { /// The configuration this waiter was created with. public let config: WaiterConfiguration From 8ed31c2f260d89a0f2dd5c2d55f15b7203f46bd8 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 3 Apr 2025 16:51:18 -0400 Subject: [PATCH 06/35] try removing version string --- .../software/amazon/smithy/swift/codegen/SwiftDependency.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt index 50c078791..5ffc3a2f0 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt @@ -56,7 +56,7 @@ class SwiftDependency( SwiftDependency( "AwsCommonRuntimeKit", "swift6-support", - "0.31.0", + "", "https://github.com/awslabs/aws-crt-swift", "", "aws-crt-swift", From 1d9380703da7f553f36cf4e556ae9fd5d0917589 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 9 Apr 2025 17:29:27 -0400 Subject: [PATCH 07/35] swift 6 compatible, all tests passing --- Package.swift | 5 +- .../DefaultSDKRuntimeConfiguration.swift | 2 +- .../Config/IdempotencyTokenGenerator.swift | 2 +- .../Endpoints/CRTResolvedEndpoint.swift | 2 +- .../Endpoints/DefaultEndpointResolver.swift | 3 +- .../EndpointsAuthSchemeResolver.swift | 62 ++- .../Networking/Http/CRT/CRTClientEngine.swift | 460 ++++++++++-------- .../Http/CRT/CRTClientEngineConfig.swift | 2 +- .../Http/CRT/CRTClientTLSOptions.swift | 2 +- .../URLSession/URLSessionHTTPClient.swift | 14 +- .../URLSession/URLSessionTLSOptions.swift | 2 +- Sources/Smithy/Attribute.swift | 10 +- Sources/Smithy/ByteStream.swift | 2 +- Sources/Smithy/Context.swift | 8 +- Sources/Smithy/Stream.swift | 3 +- Sources/Smithy/URI.swift | 2 +- Sources/Smithy/URIQueryItem.swift | 2 +- Sources/Smithy/URIScheme.swift | 2 +- .../ChecksumAlgorithm.swift | 2 +- .../MessageEncoder.swift | 2 +- .../MessageDecoderStream.swift | 6 +- .../MessageSigner.swift | 2 +- Sources/SmithyHTTPAPI/Context+HTTP.swift | 2 +- Sources/SmithyHTTPAPI/Endpoint.swift | 37 +- .../SmithyHTTPAPI/EndpointPropertyValue.swift | 62 +++ Sources/SmithyHTTPAPI/HTTPMethodType.swift | 2 +- Sources/SmithyHTTPAPI/HTTPRequest.swift | 2 + Sources/SmithyHTTPAuth/SigV4Signer.swift | 2 +- .../Signers/BearerTokenSigner.swift | 2 +- Sources/SmithyHTTPAuthAPI/AuthScheme.swift | 2 +- .../AuthSchemeResolver.swift | 2 +- .../SelectedAuthScheme.swift | 4 +- Sources/SmithyHTTPAuthAPI/Signer.swift | 2 +- .../AWSSignatureType.swift | 2 +- .../AWSSignedBodyHeader.swift | 2 +- .../SigningAlgorithm.swift | 2 +- Sources/SmithyIdentityAPI/FlowType.swift | 2 +- Sources/SmithyIdentityAPI/Identity.swift | 2 +- .../SmithyIdentityAPI/IdentityResolver.swift | 2 +- .../SmithyTestUtil/ProtocolTestClient.swift | 2 +- .../EndpointsAuthSchemeResolverTests.swift | 7 +- .../InterceptorTests/InterceptorTests.swift | 2 +- .../LoggingTests/SwiftLoggerTests.swift | 2 +- .../NetworkingTests/EndpointTests.swift | 1 - .../MockAWSCredentialIdentityResolver.swift | 4 +- .../SmithyJSONTests/FloatingPointTests.swift | 22 +- .../swift/codegen/EndpointTestGenerator.kt | 16 +- .../swiftmodules/SmithyHTTPAPITypes.kt | 1 + 48 files changed, 497 insertions(+), 288 deletions(-) create mode 100644 Sources/SmithyHTTPAPI/EndpointPropertyValue.swift diff --git a/Package.swift b/Package.swift index 98221e24a..5edaa6852 100644 --- a/Package.swift +++ b/Package.swift @@ -157,7 +157,10 @@ let package = Package( ), .target( name: "SmithyHTTPAPI", - dependencies: ["Smithy"] + dependencies: [ + "Smithy", + .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift") + ] ), .target( name: "SmithyHTTPClient", diff --git a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift index e50540a04..3c3a3a805 100644 --- a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift +++ b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift @@ -131,7 +131,7 @@ public class DefaultAuthSchemeResolverParameters: AuthSchemeResolverParameters { } } -public class DefaultAuthSchemeResolver: AuthSchemeResolver { +public class DefaultAuthSchemeResolver: AuthSchemeResolver, @unchecked Sendable { public func resolveAuthScheme(params: AuthSchemeResolverParameters) throws -> [AuthOption] { return [] } diff --git a/Sources/ClientRuntime/Config/IdempotencyTokenGenerator.swift b/Sources/ClientRuntime/Config/IdempotencyTokenGenerator.swift index 8510cd742..ea790169a 100644 --- a/Sources/ClientRuntime/Config/IdempotencyTokenGenerator.swift +++ b/Sources/ClientRuntime/Config/IdempotencyTokenGenerator.swift @@ -3,6 +3,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -public protocol IdempotencyTokenGenerator { +public protocol IdempotencyTokenGenerator: Sendable { func generateToken() -> String } diff --git a/Sources/ClientRuntime/Endpoints/CRTResolvedEndpoint.swift b/Sources/ClientRuntime/Endpoints/CRTResolvedEndpoint.swift index 4f6fbe6df..d718d09b7 100644 --- a/Sources/ClientRuntime/Endpoints/CRTResolvedEndpoint.swift +++ b/Sources/ClientRuntime/Endpoints/CRTResolvedEndpoint.swift @@ -34,7 +34,7 @@ public class CRTResolvedEndpoint { } } - public func getProperties() -> [String: AnyHashable]? { + public func getProperties() -> [String: EndpointProperty]? { switch crtResolvedEndpoint { case let .endpoint(_, _, properties): return properties diff --git a/Sources/ClientRuntime/Endpoints/DefaultEndpointResolver.swift b/Sources/ClientRuntime/Endpoints/DefaultEndpointResolver.swift index e33fea035..68423c024 100644 --- a/Sources/ClientRuntime/Endpoints/DefaultEndpointResolver.swift +++ b/Sources/ClientRuntime/Endpoints/DefaultEndpointResolver.swift @@ -7,6 +7,7 @@ import struct SmithyHTTPAPI.Endpoint import struct SmithyHTTPAPI.Headers +import enum SmithyHTTPAPI.EndpointPropertyValue public struct DefaultEndpointResolver { @@ -32,6 +33,6 @@ public struct DefaultEndpointResolver let headers = crtResolvedEndpoint.getHeaders() ?? [:] let properties = crtResolvedEndpoint.getProperties() ?? [:] - return try Endpoint(urlString: url, headers: Headers(headers), properties: properties) + return try Endpoint(urlString: url, headers: Headers(headers), endpointProperties: properties) } } diff --git a/Sources/ClientRuntime/Endpoints/EndpointsAuthSchemeResolver.swift b/Sources/ClientRuntime/Endpoints/EndpointsAuthSchemeResolver.swift index fb604bb38..7fab1ce0c 100644 --- a/Sources/ClientRuntime/Endpoints/EndpointsAuthSchemeResolver.swift +++ b/Sources/ClientRuntime/Endpoints/EndpointsAuthSchemeResolver.swift @@ -5,6 +5,8 @@ // SPDX-License-Identifier: Apache-2.0 // +import enum SmithyHTTPAPI.EndpointPropertyValue + /// Supported authentication schemes public enum EndpointsAuthScheme: Equatable { case sigV4(SigV4Parameters) @@ -25,7 +27,8 @@ extension EndpointsAuthScheme { /// Initialize an AuthScheme from a dictionary /// - Parameter dictionary: Dictionary containing the auth scheme public init(from dictionary: [String: Any]) throws { - guard let name = dictionary["name"] as? String else { + guard let endpointProperty = dictionary["name"] as? SmithyHTTPAPI.EndpointPropertyValue, + case let .string(name) = endpointProperty else { throw EndpointError.authScheme("Invalid auth scheme") } switch name { @@ -60,9 +63,29 @@ extension EndpointsAuthScheme.SigV4Parameters { /// Initialize a SigV4AuthScheme from a dictionary /// - Parameter dictionary: Dictionary containing the auth scheme init(from dictionary: [String: Any]) throws { - self.signingName = dictionary["signingName"] as? String - self.signingRegion = dictionary["signingRegion"] as? String - self.disableDoubleEncoding = dictionary["disableDoubleEncoding"] as? Bool + // For signingName (expected to be a string) + if let value = dictionary["signingName"] as? EndpointPropertyValue, + case let .string(name) = value { + self.signingName = name + } else { + self.signingName = nil + } + + // For signingRegion (expected to be a string) + if let value = dictionary["signingRegion"] as? EndpointPropertyValue, + case let .string(region) = value { + self.signingRegion = region + } else { + self.signingRegion = nil + } + + // For disableDoubleEncoding (expected to be a bool) + if let value = dictionary["disableDoubleEncoding"] as? EndpointPropertyValue, + case let .bool(flag) = value { + self.disableDoubleEncoding = flag + } else { + self.disableDoubleEncoding = nil + } } } @@ -86,9 +109,34 @@ extension EndpointsAuthScheme.SigV4AParameters { /// Initialize a SigV4AAuthScheme from a dictionary /// - Parameter dictionary: Dictionary containing the auth scheme init(from dictionary: [String: Any]) throws { - self.signingName = dictionary["signingName"] as? String - self.signingRegionSet = dictionary["signingRegionSet"] as? [String] - self.disableDoubleEncoding = dictionary["disableDoubleEncoding"] as? Bool + // Extract signingName + if let value = dictionary["signingName"] as? EndpointPropertyValue, + case let .string(name) = value { + self.signingName = name + } else { + self.signingName = nil + } + + // Extract signingRegionSet as an array of String + if let value = dictionary["signingRegionSet"] as? EndpointPropertyValue, + case let .array(regions) = value { + self.signingRegionSet = regions.compactMap { element in + if case let .string(region) = element { + return region + } + return nil + } + } else { + self.signingRegionSet = nil + } + + // Extract disableDoubleEncoding + if let value = dictionary["disableDoubleEncoding"] as? EndpointPropertyValue, + case let .bool(flag) = value { + self.disableDoubleEncoding = flag + } else { + self.disableDoubleEncoding = nil + } } } diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 4040c698e..66bad34f4 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -74,6 +74,16 @@ public class CRTClientEngine: HTTPClient { self.socketTimeout = config.socketTimeout } + init(windowSize: Int, maxConnectionsPerEndpoint: Int, telemetry: HttpTelemetry, connectTimeoutMs: UInt32? = nil, crtTLSOptions: CRTClientTLSOptions? = nil, socketTimeout: UInt32? = nil) { + self.windowSize = windowSize + self.maxConnectionsPerEndpoint = maxConnectionsPerEndpoint + self.telemetry = telemetry + self.logger = self.telemetry.loggerProvider.getLogger(name: "SerialExecutor") + self.connectTimeoutMs = connectTimeoutMs + self.crtTLSOptions = crtTLSOptions + self.socketTimeout = socketTimeout + } + func getOrCreateConnectionPool(endpoint: Endpoint) throws -> HTTPClientConnectionManager { let poolID = ConnectionPoolID(endpoint: endpoint) guard let connectionPool = connectionPools[poolID] else { @@ -182,203 +192,61 @@ public class CRTClientEngine: HTTPClient { self.windowSize = config.windowSize self.telemetry = config.telemetry self.logger = self.telemetry.loggerProvider.getLogger(name: "CRTClientEngine") - self.serialExecutor = SerialExecutor(config: config) + + self.serialExecutor = SerialExecutor( + windowSize: config.windowSize, + maxConnectionsPerEndpoint: config.maxConnectionsPerEndpoint, + telemetry: config.telemetry, + connectTimeoutMs: config.connectTimeoutMs, + crtTLSOptions: config.crtTlsOptions, + socketTimeout: config.socketTimeout + ) } - // swiftlint:disable function_body_length public func send(request: HTTPRequest) async throws -> HTTPResponse { let telemetryContext = telemetry.contextManager.current() let tracer = telemetry.tracerProvider.getTracer( scope: telemetry.tracerScope, - attributes: telemetry.tracerAttributes) - do { - // START - smithy.client.http.requests.queued_duration - let queuedStart = Date().timeIntervalSinceReferenceDate - let span = tracer.createSpan( - name: telemetry.spanName, - initialAttributes: telemetry.spanAttributes, - spanKind: SpanKind.internal, - parentContext: telemetryContext) - defer { - span.end() - } - let connectionMgr = try await serialExecutor.getOrCreateConnectionPool(endpoint: request.endpoint) - - // START - smithy.client.http.connections.acquire_duration - let acquireConnectionStart = Date().timeIntervalSinceReferenceDate - let connection = try await connectionMgr.acquireConnection() - let acquireConnectionEnd = Date().timeIntervalSinceReferenceDate - telemetry.connectionsAcquireDuration.record( - value: acquireConnectionEnd - acquireConnectionStart, - attributes: Attributes(), - context: telemetryContext) - // END - smithy.client.http.connections.acquire_duration - let queuedEnd = acquireConnectionEnd - telemetry.requestsQueuedDuration.record( - value: queuedEnd - queuedStart, - attributes: Attributes(), - context: telemetryContext) - // END - smithy.client.http.requests.queued_duration - - let connectionsLimit = serialExecutor.maxConnectionsPerEndpoint - let connectionMgrMetrics = connectionMgr.fetchMetrics() - telemetry.updateHTTPMetricsUsage { httpMetricsUsage in - // TICK - smithy.client.http.connections.limit - httpMetricsUsage.connectionsLimit = connectionsLimit - - // TICK - smithy.client.http.connections.usage - httpMetricsUsage.idleConnections = connectionMgrMetrics.availableConcurrency - httpMetricsUsage.acquiredConnections = connectionMgrMetrics.leasedConcurrency - - // TICK - smithy.client.http.requests.usage - httpMetricsUsage.inflightRequests = connectionMgrMetrics.leasedConcurrency - httpMetricsUsage.queuedRequests = connectionMgrMetrics.pendingConcurrencyAcquires - } - - do { - // DURATION - smithy.client.http.connections.uptime - let connectionUptimeStart = acquireConnectionEnd - defer { - telemetry.connectionsUptime.record( - value: Date().timeIntervalSinceReferenceDate - connectionUptimeStart, - attributes: Attributes(), - context: telemetryContext) - } - // swiftlint:disable:next line_length - self.logger.debug("Connection was acquired to: \(String(describing: request.destination.url?.absoluteString))") - switch connection.httpVersion { - case .version_1_1: - self.logger.debug("Using HTTP/1.1 connection") - let crtRequest = try request.toHttpRequest() - return try await withCheckedThrowingContinuation { (continuation: StreamContinuation) in - let wrappedContinuation = ContinuationWrapper(continuation) - let requestOptions = makeHttpRequestStreamOptions( - request: crtRequest, - continuation: wrappedContinuation, - serverAddress: CRTClientEngine.makeServerAddress(request: request) - ) - do { - let stream = try connection.makeRequest(requestOptions: requestOptions) - try stream.activate() - if request.isChunked { - Task { - do { - guard let http1Stream = stream as? HTTP1Stream else { - throw StreamError.notSupported( - "HTTP1Stream should be used with an HTTP/1.1 connection!" - ) - } - let body = request.body - // swiftlint:disable line_length - guard case .stream(let stream) = body, stream.isEligibleForChunkedStreaming else { - throw ByteStreamError.invalidStreamTypeForChunkedBody( - "The stream is not eligible for chunked streaming or is not a stream type!" - ) - } - // swiftlint:enable line_length - - guard let chunkedStream = stream as? ChunkedStream else { - throw ByteStreamError.streamDoesNotConformToChunkedStream( - "Stream does not conform to ChunkedStream! Type is \(stream)." - ) - } - - var hasMoreChunks = true - var currentChunkBodyIsEmpty = false - while hasMoreChunks { - // Process the first chunk and determine if there are more to send - hasMoreChunks = try await chunkedStream.chunkedReader.processNextChunk() - currentChunkBodyIsEmpty = chunkedStream - .chunkedReader - .getCurrentChunkBody() - .isEmpty - - if !hasMoreChunks || currentChunkBodyIsEmpty { - // Send the final chunk - let finalChunk = try await chunkedStream.chunkedReader.getFinalChunk() - // TICK - smithy.client.http.bytes_sent - try await http1Stream.writeChunk(chunk: finalChunk, endOfStream: true) - var bytesSentAttributes = Attributes() - bytesSentAttributes.set( - key: HttpMetricsAttributesKeys.serverAddress, - value: CRTClientEngine.makeServerAddress(request: request)) - telemetry.bytesSent.add( - value: finalChunk.count, - attributes: bytesSentAttributes, - context: telemetry.contextManager.current()) - hasMoreChunks = false - } else { - let currentChunk = chunkedStream.chunkedReader.getCurrentChunk() - // TICK - smithy.client.http.bytes_sent - try await http1Stream.writeChunk( - chunk: currentChunk, - endOfStream: false - ) - var bytesSentAttributes = Attributes() - bytesSentAttributes.set( - key: HttpMetricsAttributesKeys.serverAddress, - value: CRTClientEngine.makeServerAddress(request: request)) - telemetry.bytesSent.add( - value: currentChunk.count, - attributes: bytesSentAttributes, - context: telemetry.contextManager.current()) - } - } - } catch { - logger.error(error.localizedDescription) - wrappedContinuation.safeResume(error: error) - } - } - } - } catch { - logger.error(error.localizedDescription) - wrappedContinuation.safeResume(error: error) - } - } - case .version_2: - self.logger.debug("Using HTTP/2 connection") - let crtRequest = try request.toHttp2Request() - return try await withCheckedThrowingContinuation { (continuation: StreamContinuation) in - let wrappedContinuation = ContinuationWrapper(continuation) - let requestOptions = makeHttpRequestStreamOptions( - request: crtRequest, - continuation: wrappedContinuation, - http2ManualDataWrites: true, - serverAddress: CRTClientEngine.makeServerAddress(request: request) - ) - let stream: HTTP2Stream - do { - // swiftlint:disable:next force_cast - stream = try connection.makeRequest(requestOptions: requestOptions) as! HTTP2Stream - try stream.activate() - } catch { - logger.error(error.localizedDescription) - wrappedContinuation.safeResume(error: error) - return - } + attributes: telemetry.tracerAttributes + ) + let queuedStart = Date().timeIntervalSinceReferenceDate + let span = tracer.createSpan( + name: telemetry.spanName, + initialAttributes: telemetry.spanAttributes, + spanKind: .internal, + parentContext: telemetryContext + ) + defer { + span.end() + } - // At this point, continuation is resumed when the initial headers are received - // it is now safe to write the body - // writing is done in a separate task to avoid blocking the continuation - Task { [logger] in - do { - // TICK - smithy.client.http.bytes_sent - try await stream.write( - body: request.body, - telemetry: telemetry, - serverAddress: CRTClientEngine.makeServerAddress(request: request)) - } catch { - logger.error(error.localizedDescription) - } - } - } - case .unknown: - fatalError("Unknown HTTP version") - } - } + let connectionMgr = try await serialExecutor.getOrCreateConnectionPool(endpoint: request.endpoint) + let acquireConnectionStart = Date().timeIntervalSinceReferenceDate + let connection = try await connectionMgr.acquireConnection() + let acquireConnectionEnd = Date().timeIntervalSinceReferenceDate + + telemetry.connectionsAcquireDuration.record( + value: acquireConnectionEnd - acquireConnectionStart, + attributes: Attributes(), + context: telemetryContext + ) + telemetry.requestsQueuedDuration.record( + value: acquireConnectionEnd - queuedStart, + attributes: Attributes(), + context: telemetryContext + ) + + updateHTTPMetrics(connectionMgr.fetchMetrics()) + + switch connection.httpVersion { + case .version_1_1: + return try await handleHTTP1Request(connection: connection, request: request, telemetryContext: telemetryContext) + case .version_2: + return try await handleHTTP2Request(connection: connection, request: request, telemetryContext: telemetryContext) + case .unknown: + fatalError("Unknown HTTP version") } } - // swiftlint:enable function_body_length // Forces an Http2 request that uses CRT's `HTTP2StreamManager`. // This may be removed or improved as part of SRA work and CRT adapting to SRA for HTTP. @@ -411,7 +279,10 @@ public class CRTClientEngine: HTTPClient { http2ManualDataWrites: true, serverAddress: CRTClientEngine.makeServerAddress(request: request) ) - Task { [logger] in + let telemetry = self.telemetry + let logger = self.logger + let serialExecutor = self.serialExecutor + Task { @Sendable in let stream: HTTP2Stream var acquireConnectionEnd: TimeInterval do { @@ -432,7 +303,7 @@ public class CRTClientEngine: HTTPClient { // END - smithy.client.http.requests.queued_duration } catch { logger.error(error.localizedDescription) - wrappedContinuation.safeResume(error: error) + await wrappedContinuation.safeResume(error: error) return } @@ -477,6 +348,146 @@ public class CRTClientEngine: HTTPClient { } } + private func updateHTTPMetrics(_ metrics: HTTPClientConnectionManagerMetrics) { + let connectionsLimit = serialExecutor.maxConnectionsPerEndpoint + telemetry.updateHTTPMetricsUsage { httpMetricsUsage in + httpMetricsUsage.connectionsLimit = connectionsLimit + httpMetricsUsage.idleConnections = metrics.availableConcurrency + httpMetricsUsage.acquiredConnections = metrics.leasedConcurrency + httpMetricsUsage.inflightRequests = metrics.leasedConcurrency + httpMetricsUsage.queuedRequests = metrics.pendingConcurrencyAcquires + } + } + + private func handleHTTP1Request( + connection: HTTPClientConnection, + request: HTTPRequest, + telemetryContext: TelemetryContext + ) async throws -> HTTPResponse { + logger.debug("Using HTTP/1.1 connection") + let crtRequest = try request.toHttpRequest() + let serverAddress = Self.makeServerAddress(request: request) + + return try await withCheckedThrowingContinuation { [weak self] continuation in + guard let self = self else { + continuation.resume(throwing: StreamError.connectionReleased("Engine released")) + return + } + + let wrappedContinuation = ContinuationWrapper(continuation) + + do { + let requestOptions = self.makeHttpRequestStreamOptions( + request: crtRequest, + continuation: wrappedContinuation, + serverAddress: serverAddress + ) + + let stream = try connection.makeRequest(requestOptions: requestOptions) + try stream.activate() + + if request.isChunked { + let logger = self.logger + let telemetry = self.telemetry + + Task { @Sendable in + do { + + try await ClientRuntime.sendChunkedBody( + using: stream, + request: request, + logger: logger, + telemetry: telemetry, + serverAddress: serverAddress + ) + } catch { + logger.error("Chunked streaming error: \(error.localizedDescription)") + await wrappedContinuation.safeResume(error: error) + } + } + } + } catch { + self.logger.error("Request setup error: \(error.localizedDescription)") + Task { + await wrappedContinuation.safeResume(error: error) + } + } + } + } + + private func handleHTTP2Request(connection: HTTPClientConnection, request: HTTPRequest, telemetryContext: TelemetryContext) async throws -> HTTPResponse { + self.logger.debug("Using HTTP/2 connection") + let crtRequest = try request.toHttp2Request() + return try await withCheckedThrowingContinuation { (continuation: StreamContinuation) in + let wrappedContinuation = ContinuationWrapper(continuation) + let requestOptions = self.makeHttpRequestStreamOptions( + request: crtRequest, + continuation: wrappedContinuation, + http2ManualDataWrites: true, + serverAddress: CRTClientEngine.makeServerAddress(request: request) + ) + do { + guard let stream = try connection.makeRequest(requestOptions: requestOptions) as? HTTP2Stream else { + throw StreamError.notSupported("HTTP2Stream expected for HTTP/2 connection!") + } + try stream.activate() + + let telemetry = self.telemetry + let logger = self.logger + + Task { @Sendable in + do { + try await stream.write( + body: request.body, + telemetry: telemetry, + serverAddress: CRTClientEngine.makeServerAddress(request: request) + ) + } catch { + logger.error(error.localizedDescription) + } + } + } catch { + self.logger.error(error.localizedDescription) + Task { + await wrappedContinuation.safeResume(error: error) + } + return + } + } + } + + private func sendChunkedBody(using stream: Any, request: HTTPRequest) async throws { + guard let http1Stream = stream as? HTTP1Stream else { + throw StreamError.notSupported("HTTP1Stream should be used with an HTTP/1.1 connection!") + } + guard case .stream(let bodyStream) = request.body, bodyStream.isEligibleForChunkedStreaming else { + throw ByteStreamError.invalidStreamTypeForChunkedBody("The stream is not eligible for chunked streaming or is not a stream type!") + } + guard let chunkedStream = bodyStream as? ChunkedStream else { + throw ByteStreamError.streamDoesNotConformToChunkedStream("Stream does not conform to ChunkedStream! Type is \\(bodyStream).") + } + + var hasMoreChunks = true + while hasMoreChunks { + hasMoreChunks = try await chunkedStream.chunkedReader.processNextChunk() + let currentChunk = chunkedStream.chunkedReader.getCurrentChunk() + let currentChunkBodyIsEmpty = chunkedStream.chunkedReader.getCurrentChunkBody().isEmpty + if !hasMoreChunks || currentChunkBodyIsEmpty { + let finalChunk = try await chunkedStream.chunkedReader.getFinalChunk() + try await http1Stream.writeChunk(chunk: finalChunk, endOfStream: true) + var bytesSentAttributes = Attributes() + bytesSentAttributes.set(key: HttpMetricsAttributesKeys.serverAddress, value: CRTClientEngine.makeServerAddress(request: request)) + telemetry.bytesSent.add(value: finalChunk.count, attributes: bytesSentAttributes, context: telemetry.contextManager.current()) + break + } else { + try await http1Stream.writeChunk(chunk: currentChunk, endOfStream: false) + var bytesSentAttributes = Attributes() + bytesSentAttributes.set(key: HttpMetricsAttributesKeys.serverAddress, value: CRTClientEngine.makeServerAddress(request: request)) + telemetry.bytesSent.add(value: currentChunk.count, attributes: bytesSentAttributes, context: telemetry.contextManager.current()) + } + } + } + /// Creates a `HTTPRequestOptions` object that can be used to make a HTTP request /// - Parameters: /// - request: The `HTTPRequestBase` object that contains the request information @@ -516,7 +527,9 @@ public class CRTClientEngine: HTTPClient { // resume the continuation as soon as we have all the initial headers // this allows callers to start reading the response as it comes in // instead of waiting for the entire response to be received - continuation.safeResume(response: response) + Task { + await continuation.safeResume(response: response) + } } onIncomingBody: { bodyChunk in self.logger.debug("Body chunk received") do { @@ -538,7 +551,9 @@ public class CRTClientEngine: HTTPClient { response.statusCode = makeStatusCode(statusCode) case .failure(let error): self.logger.error("Response encountered an error: \(error)") - continuation.safeResume(error: error) + Task { + await continuation.safeResume(error: error) + } } // closing the stream is required to signal to the caller that the response is complete @@ -561,7 +576,7 @@ public class CRTClientEngine: HTTPClient { } } - class ContinuationWrapper { + actor ContinuationWrapper { private var continuation: StreamContinuation? public init(_ continuation: StreamContinuation) { @@ -570,12 +585,67 @@ public class CRTClientEngine: HTTPClient { public func safeResume(response: HTTPResponse) { continuation?.resume(returning: response) - self.continuation = nil + continuation = nil } public func safeResume(error: Error) { continuation?.resume(throwing: error) - self.continuation = nil + continuation = nil + } + } +} + +private func sendChunkedBody( + using stream: HTTPStream, + request: HTTPRequest, + logger: LogAgent, + telemetry: HttpTelemetry, + serverAddress: String +) async throws { + guard let http1Stream = stream as? HTTP1Stream else { + throw StreamError.notSupported("HTTP1Stream should be used with an HTTP/1.1 connection!") + } + + let body = request.body + guard case .stream(let stream) = body, stream.isEligibleForChunkedStreaming else { + throw ByteStreamError.invalidStreamTypeForChunkedBody( + "The stream is not eligible for chunked streaming or is not a stream type!" + ) + } + + guard let chunkedStream = stream as? ChunkedStream else { + throw ByteStreamError.streamDoesNotConformToChunkedStream( + "Stream does not conform to ChunkedStream! Type is \(stream)." + ) + } + + var hasMoreChunks = true + while hasMoreChunks { + hasMoreChunks = try await chunkedStream.chunkedReader.processNextChunk() + let currentChunkBodyIsEmpty = chunkedStream.chunkedReader.getCurrentChunkBody().isEmpty + + if !hasMoreChunks || currentChunkBodyIsEmpty { + let finalChunk = try await chunkedStream.chunkedReader.getFinalChunk() + try await http1Stream.writeChunk(chunk: finalChunk, endOfStream: true) + + var bytesSentAttributes = Attributes() + bytesSentAttributes.set(key: HttpMetricsAttributesKeys.serverAddress, value: serverAddress) + telemetry.bytesSent.add( + value: finalChunk.count, + attributes: bytesSentAttributes, + context: telemetry.contextManager.current() + ) + } else { + let currentChunk = chunkedStream.chunkedReader.getCurrentChunk() + try await http1Stream.writeChunk(chunk: currentChunk, endOfStream: false) + + var bytesSentAttributes = Attributes() + bytesSentAttributes.set(key: HttpMetricsAttributesKeys.serverAddress, value: serverAddress) + telemetry.bytesSent.add( + value: currentChunk.count, + attributes: bytesSentAttributes, + context: telemetry.contextManager.current() + ) } } } diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngineConfig.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngineConfig.swift index 87130b5aa..25b13d4b8 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngineConfig.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngineConfig.swift @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0. */ -struct CRTClientEngineConfig { +struct CRTClientEngineConfig: Sendable { /// Max connections the manager can contain per endpoint let maxConnectionsPerEndpoint: Int diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientTLSOptions.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientTLSOptions.swift index 623e243a7..8439830ff 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientTLSOptions.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientTLSOptions.swift @@ -8,7 +8,7 @@ import AwsCommonRuntimeKit import Smithy -public struct CRTClientTLSOptions: TLSConfiguration { +public struct CRTClientTLSOptions: TLSConfiguration, Sendable { /// Optional PEM certificate filename public var certificate: String? diff --git a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift index 6f49023f2..86a2ee1db 100644 --- a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift +++ b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift @@ -310,7 +310,7 @@ public final class URLSessionHTTPClient: HTTPClient { func urlSession( _ session: URLSession, task: URLSessionTask, - needNewBodyStream completionHandler: @escaping (InputStream?) -> Void + needNewBodyStream completionHandler: @escaping @Sendable (InputStream?) -> Void ) { storage.modify(task) { connection in guard let streamBridge = connection.streamBridge else { completionHandler(nil); return } @@ -362,6 +362,11 @@ public final class URLSessionHTTPClient: HTTPClient { // - The stream bridge is closed. // This ensures that resources are freed and stream readers/writers are continued. storage.modify(task) { connection in + let streamBridge = connection.streamBridge + let shouldRemove = { [self] in + storage.remove(task) + } + if let error = connection.error ?? error { if connection.hasBeenResumed { connection.responseStream.closeWithError(error) @@ -376,12 +381,9 @@ public final class URLSessionHTTPClient: HTTPClient { connection.responseStream.close() } - // Close the stream bridge so that its resources are deallocated Task { - await connection.streamBridge?.close() - - // Task is complete & no longer needed. Remove it from storage. - storage.remove(task) + await streamBridge?.close() + shouldRemove() } } } diff --git a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionTLSOptions.swift b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionTLSOptions.swift index a0553632a..ef89dd9ed 100644 --- a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionTLSOptions.swift +++ b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionTLSOptions.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct URLSessionTLSOptions: TLSConfiguration { +public struct URLSessionTLSOptions: TLSConfiguration, Sendable { /// Filename of the turst certificate file in main bundle (.cer) public var certificate: String? diff --git a/Sources/Smithy/Attribute.swift b/Sources/Smithy/Attribute.swift index 42cb8ea59..a082b54a2 100644 --- a/Sources/Smithy/Attribute.swift +++ b/Sources/Smithy/Attribute.swift @@ -19,21 +19,21 @@ public struct AttributeKey: Sendable { } /// Type safe property bag -public struct Attributes { - private var attributes = [String: Any]() +public struct Attributes: Sendable { + private var attributes = [String: any Sendable]() public var size: Int { attributes.count } public init() {} - public func get(key: AttributeKey) -> T? { + public func get(key: AttributeKey) -> T? { attributes[key.name] as? T } - public func contains(key: AttributeKey) -> Bool { + public func contains(key: AttributeKey) -> Bool { get(key: key) != nil } - public mutating func set(key: AttributeKey, value: T?) { + public mutating func set(key: AttributeKey, value: T?) { attributes[key.name] = value } diff --git a/Sources/Smithy/ByteStream.swift b/Sources/Smithy/ByteStream.swift index d6d00abf3..9a13b3338 100644 --- a/Sources/Smithy/ByteStream.swift +++ b/Sources/Smithy/ByteStream.swift @@ -73,7 +73,7 @@ extension ByteStream: CustomDebugStringConvertible { } } -public enum ByteStreamError: Error { +public enum ByteStreamError: Error, Sendable { case streamNotSeeakble case invalidStreamTypeForChunkedBody(String) case streamClosedOrEmpty diff --git a/Sources/Smithy/Context.swift b/Sources/Smithy/Context.swift index 175f92261..cd58b3217 100644 --- a/Sources/Smithy/Context.swift +++ b/Sources/Smithy/Context.swift @@ -34,15 +34,15 @@ public class Context { extension Context { - public func get(key: AttributeKey) -> T? { + public func get(key: AttributeKey) -> T? { accessAttributes().get(key: key) } - public func contains(key: AttributeKey) -> Bool { + public func contains(key: AttributeKey) -> Bool { accessAttributes().contains(key: key) } - public func set(key: AttributeKey, value: T?) { + public func set(key: AttributeKey, value: T?) { accessAttributes { attributes in attributes.set(key: key, value: value) } @@ -68,7 +68,7 @@ public class ContextBuilder { // @discardableResult attribute we won't get warnings if we // don't end up doing any chaining. @discardableResult - public func with(key: AttributeKey, value: T) -> Self { + public func with(key: AttributeKey, value: T) -> Self { self.attributes.set(key: key, value: value) return self } diff --git a/Sources/Smithy/Stream.swift b/Sources/Smithy/Stream.swift index c16fda8f7..43a351812 100644 --- a/Sources/Smithy/Stream.swift +++ b/Sources/Smithy/Stream.swift @@ -60,9 +60,10 @@ public protocol WriteableStream: AnyObject, Sendable { public protocol Stream: ReadableStream, WriteableStream { } -public enum StreamError: Error { +public enum StreamError: Error, Sendable { case invalidOffset(String) case notSupported(String) + case connectionReleased(String) } extension Stream { diff --git a/Sources/Smithy/URI.swift b/Sources/Smithy/URI.swift index b038764f8..325d12f83 100644 --- a/Sources/Smithy/URI.swift +++ b/Sources/Smithy/URI.swift @@ -7,7 +7,7 @@ import Foundation /// A representation of the RFC 3986 Uniform Resource Identifier /// Note: URIBuilder returns an URI instance with all components percent encoded -public struct URI: Hashable { +public struct URI: Hashable, Sendable { public let scheme: URIScheme public let path: String public let host: String diff --git a/Sources/Smithy/URIQueryItem.swift b/Sources/Smithy/URIQueryItem.swift index f2ed3d904..dc5c29727 100644 --- a/Sources/Smithy/URIQueryItem.swift +++ b/Sources/Smithy/URIQueryItem.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct URIQueryItem: Hashable { +public struct URIQueryItem: Hashable, Sendable { public var name: String public var value: String? diff --git a/Sources/Smithy/URIScheme.swift b/Sources/Smithy/URIScheme.swift index c476a94a7..793f6afea 100644 --- a/Sources/Smithy/URIScheme.swift +++ b/Sources/Smithy/URIScheme.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public enum URIScheme: String, CaseIterable { +public enum URIScheme: String, CaseIterable, Sendable { case http case https diff --git a/Sources/SmithyChecksumsAPI/ChecksumAlgorithm.swift b/Sources/SmithyChecksumsAPI/ChecksumAlgorithm.swift index 1251b2d0e..771ba2883 100644 --- a/Sources/SmithyChecksumsAPI/ChecksumAlgorithm.swift +++ b/Sources/SmithyChecksumsAPI/ChecksumAlgorithm.swift @@ -6,7 +6,7 @@ // /// The list of checksum algorithms contained by this enum may expand in future. -public enum ChecksumAlgorithm { +public enum ChecksumAlgorithm: Sendable { case crc32, crc32c, crc64nvme, sha1, sha256, md5 public func toString() -> String { diff --git a/Sources/SmithyEventStreamsAPI/MessageEncoder.swift b/Sources/SmithyEventStreamsAPI/MessageEncoder.swift index fe122f0bf..b8a4e90d9 100644 --- a/Sources/SmithyEventStreamsAPI/MessageEncoder.swift +++ b/Sources/SmithyEventStreamsAPI/MessageEncoder.swift @@ -9,7 +9,7 @@ import struct Foundation.Data /// Encodes a `Message` into a `Data` object /// to be sent over the wire. -public protocol MessageEncoder { +public protocol MessageEncoder: Sendable { /// Encodes a `Message` into a `Data` object func encode(message: Message) throws -> Data diff --git a/Sources/SmithyEventStreamsAuthAPI/MessageDecoderStream.swift b/Sources/SmithyEventStreamsAuthAPI/MessageDecoderStream.swift index 3ddb52c6b..417cf99aa 100644 --- a/Sources/SmithyEventStreamsAuthAPI/MessageDecoderStream.swift +++ b/Sources/SmithyEventStreamsAuthAPI/MessageDecoderStream.swift @@ -7,14 +7,14 @@ /// Stream adapter that decodes input data into `Message` objects. public protocol MessageDecoderStream: AsyncSequence where Event == Element { - associatedtype Event + associatedtype Event: Sendable } -extension MessageDecoderStream { +extension MessageDecoderStream where Self: Sendable { /// Returns an `AsyncThrowingStream` that decodes input data into `Event` objects. public func toAsyncStream() -> AsyncThrowingStream where Event == Element { let stream = AsyncThrowingStream { continuation in - Task { + Task { [self] in // explicitly capture self do { for try await event in self { continuation.yield(event) diff --git a/Sources/SmithyEventStreamsAuthAPI/MessageSigner.swift b/Sources/SmithyEventStreamsAuthAPI/MessageSigner.swift index e3d535263..6d1f6a067 100644 --- a/Sources/SmithyEventStreamsAuthAPI/MessageSigner.swift +++ b/Sources/SmithyEventStreamsAuthAPI/MessageSigner.swift @@ -8,7 +8,7 @@ import SmithyEventStreamsAPI /// Protocol for signing messages. -public protocol MessageSigner { +public protocol MessageSigner: Sendable { /// Signs a message. /// - Parameter message: The message to sign. /// - Returns: The signed message. diff --git a/Sources/SmithyHTTPAPI/Context+HTTP.swift b/Sources/SmithyHTTPAPI/Context+HTTP.swift index 3d40df606..2c5e48934 100644 --- a/Sources/SmithyHTTPAPI/Context+HTTP.swift +++ b/Sources/SmithyHTTPAPI/Context+HTTP.swift @@ -97,7 +97,7 @@ extension ContextBuilder { // @discardableResult attribute we won't get warnings if we // don't end up doing any chaining. @discardableResult - public func with(key: AttributeKey, value: T) -> Self { + public func with(key: AttributeKey, value: T) -> Self { self.attributes.set(key: key, value: value) return self } diff --git a/Sources/SmithyHTTPAPI/Endpoint.swift b/Sources/SmithyHTTPAPI/Endpoint.swift index 5f001dc02..2757f1290 100644 --- a/Sources/SmithyHTTPAPI/Endpoint.swift +++ b/Sources/SmithyHTTPAPI/Endpoint.swift @@ -7,8 +7,9 @@ import Foundation import Smithy +import enum AwsCommonRuntimeKit.EndpointProperty -public struct Endpoint: Hashable { +public struct Endpoint: Sendable, Equatable { public let uri: URI public let headers: Headers public var protocolType: URIScheme? { uri.scheme } @@ -17,11 +18,11 @@ public struct Endpoint: Hashable { public var host: String { uri.host } public var port: Int16? { uri.port } public var url: URL? { uri.url } - private let properties: [String: AnyHashable] + private let properties: [String: EndpointPropertyValue] public init(urlString: String, headers: Headers = Headers(), - properties: [String: AnyHashable] = [:]) throws { + properties: [String: EndpointPropertyValue] = [:]) throws { guard let url = URLComponents(string: urlString)?.url else { throw ClientError.unknownError("invalid url \(urlString)") } @@ -31,7 +32,7 @@ public struct Endpoint: Hashable { public init(url: URL, headers: Headers = Headers(), - properties: [String: AnyHashable] = [:]) throws { + properties: [String: EndpointPropertyValue] = [:]) throws { guard let host = url.host else { throw ClientError.unknownError("invalid host \(String(describing: url.host))") @@ -72,24 +73,44 @@ public struct Endpoint: Hashable { public init(uri: URI, headers: Headers = Headers(), - properties: [String: AnyHashable] = [:]) { + properties: [String: EndpointPropertyValue] = [:]) { self.uri = uri self.headers = headers self.properties = properties } } +extension Endpoint { + public init(urlString: String, + headers: Headers = Headers(), + endpointProperties: [String: EndpointProperty]) throws { + guard let url = URLComponents(string: urlString)?.url else { + throw ClientError.unknownError("invalid url \(urlString)") + } + + let properties = endpointProperties.mapValues(EndpointPropertyValue.init) + try self.init(url: url, headers: headers, properties: properties) + } +} + extension Endpoint { /// Returns list of auth schemes /// This is an internal API and subject to change without notice /// - Returns: list of auth schemes if present - public func authSchemes() -> [[String: Any]]? { - guard let schemes = properties[AuthSchemeKeys.authSchemes] as? [[String: Any]] else { + public func authSchemes() -> [[String: EndpointPropertyValue]]? { + guard let value = properties[AuthSchemeKeys.authSchemes], + case let .array(schemeArray) = value else { return nil } - return schemes + return schemeArray.compactMap { scheme in + guard case let .dictionary(dict) = scheme else { + return nil + } + + return dict + } } } diff --git a/Sources/SmithyHTTPAPI/EndpointPropertyValue.swift b/Sources/SmithyHTTPAPI/EndpointPropertyValue.swift new file mode 100644 index 000000000..3d6a9dd31 --- /dev/null +++ b/Sources/SmithyHTTPAPI/EndpointPropertyValue.swift @@ -0,0 +1,62 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Smithy +import enum AwsCommonRuntimeKit.EndpointProperty + +public enum EndpointPropertyValue: Sendable, Equatable { + case bool(Bool) + case string(String) + indirect case array([EndpointPropertyValue]) + indirect case dictionary([String: EndpointPropertyValue]) +} + +public extension EndpointPropertyValue { + // Convenience for string literals + init(_ string: String) { self = .string(string) } + init(_ bool: Bool) { self = .bool(bool) } +} + +extension EndpointPropertyValue { + init(_ endpointProperty: EndpointProperty) { + switch endpointProperty { + case .bool(let value): + self = .bool(value) + case .string(let value): + self = .string(value) + case .array(let values): + self = .array(values.map(EndpointPropertyValue.init)) + case .dictionary(let dict): + self = .dictionary(dict.mapValues(EndpointPropertyValue.init)) + } + } +} + +extension EndpointPropertyValue: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self = .string(value) + } +} + +extension EndpointPropertyValue: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: Bool) { + self = .bool(value) + } +} + +extension EndpointPropertyValue: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: EndpointPropertyValue...) { + self = .array(elements) + } +} + +extension EndpointPropertyValue: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, EndpointPropertyValue)...) { + self = .dictionary(Dictionary(uniqueKeysWithValues: elements)) + } +} diff --git a/Sources/SmithyHTTPAPI/HTTPMethodType.swift b/Sources/SmithyHTTPAPI/HTTPMethodType.swift index 5e8647e7c..0be774515 100644 --- a/Sources/SmithyHTTPAPI/HTTPMethodType.swift +++ b/Sources/SmithyHTTPAPI/HTTPMethodType.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public enum HTTPMethodType: String { +public enum HTTPMethodType: String, Sendable { case connect = "CONNECT" case delete = "DELETE" case get = "GET" diff --git a/Sources/SmithyHTTPAPI/HTTPRequest.swift b/Sources/SmithyHTTPAPI/HTTPRequest.swift index da1009fc5..fd3f5978a 100644 --- a/Sources/SmithyHTTPAPI/HTTPRequest.swift +++ b/Sources/SmithyHTTPAPI/HTTPRequest.swift @@ -274,3 +274,5 @@ extension HTTPRequestBuilder { return String(signatureSequence) } } + +extension HTTPRequest: @unchecked Sendable {} diff --git a/Sources/SmithyHTTPAuth/SigV4Signer.swift b/Sources/SmithyHTTPAuth/SigV4Signer.swift index 65cd4a9b5..38fa755ee 100644 --- a/Sources/SmithyHTTPAuth/SigV4Signer.swift +++ b/Sources/SmithyHTTPAuth/SigV4Signer.swift @@ -30,7 +30,7 @@ import struct Foundation.TimeInterval import struct Foundation.URL import SmithyHTTPClient -public class SigV4Signer: SmithyHTTPAuthAPI.Signer { +public class SigV4Signer: SmithyHTTPAuthAPI.Signer, @unchecked Sendable { public init() {} public func signRequest( diff --git a/Sources/SmithyHTTPAuth/Signers/BearerTokenSigner.swift b/Sources/SmithyHTTPAuth/Signers/BearerTokenSigner.swift index fa3a4ab28..d7e9fdaf8 100644 --- a/Sources/SmithyHTTPAuth/Signers/BearerTokenSigner.swift +++ b/Sources/SmithyHTTPAuth/Signers/BearerTokenSigner.swift @@ -14,7 +14,7 @@ import struct SmithyIdentity.BearerTokenIdentity /// The signer for HTTP bearer auth. /// Adds the Authorization header to the request using the resolved bearer token identity as its value. -public class BearerTokenSigner: Signer { +public class BearerTokenSigner: Signer, @unchecked Sendable { public init() {} public func signRequest( diff --git a/Sources/SmithyHTTPAuthAPI/AuthScheme.swift b/Sources/SmithyHTTPAuthAPI/AuthScheme.swift index ab1c54faf..a88e11d20 100644 --- a/Sources/SmithyHTTPAuthAPI/AuthScheme.swift +++ b/Sources/SmithyHTTPAuthAPI/AuthScheme.swift @@ -9,7 +9,7 @@ import class Smithy.Context import protocol SmithyIdentityAPI.IdentityResolver import struct Smithy.Attributes -public protocol AuthScheme { +public protocol AuthScheme: Sendable { var schemeID: String { get } var signer: Signer { get } diff --git a/Sources/SmithyHTTPAuthAPI/AuthSchemeResolver.swift b/Sources/SmithyHTTPAuthAPI/AuthSchemeResolver.swift index fdb78ad47..c65ac856c 100644 --- a/Sources/SmithyHTTPAuthAPI/AuthSchemeResolver.swift +++ b/Sources/SmithyHTTPAuthAPI/AuthSchemeResolver.swift @@ -7,7 +7,7 @@ import class Smithy.Context -public protocol AuthSchemeResolver { +public protocol AuthSchemeResolver: Sendable { func resolveAuthScheme(params: AuthSchemeResolverParameters) throws -> [AuthOption] func constructParameters(context: Context) throws -> AuthSchemeResolverParameters } diff --git a/Sources/SmithyHTTPAuthAPI/SelectedAuthScheme.swift b/Sources/SmithyHTTPAuthAPI/SelectedAuthScheme.swift index a2b9f0b58..33e163b6c 100644 --- a/Sources/SmithyHTTPAuthAPI/SelectedAuthScheme.swift +++ b/Sources/SmithyHTTPAuthAPI/SelectedAuthScheme.swift @@ -9,7 +9,7 @@ import protocol SmithyIdentityAPI.Identity import struct Smithy.Attributes import struct Smithy.AttributeKey -public struct SelectedAuthScheme { +public struct SelectedAuthScheme: Sendable { public let schemeID: String public let identity: Identity? public let signingProperties: Attributes? @@ -24,7 +24,7 @@ public struct SelectedAuthScheme { } extension SelectedAuthScheme { - public func getCopyWithUpdatedSigningProperty(key: AttributeKey, value: T) -> SelectedAuthScheme { + public func getCopyWithUpdatedSigningProperty(key: AttributeKey, value: T) -> SelectedAuthScheme { var updatedSigningProperties = self.signingProperties updatedSigningProperties?.set(key: key, value: value) return SelectedAuthScheme( diff --git a/Sources/SmithyHTTPAuthAPI/Signer.swift b/Sources/SmithyHTTPAuthAPI/Signer.swift index e5527e73a..0675d3765 100644 --- a/Sources/SmithyHTTPAuthAPI/Signer.swift +++ b/Sources/SmithyHTTPAuthAPI/Signer.swift @@ -9,7 +9,7 @@ import class SmithyHTTPAPI.HTTPRequestBuilder import protocol SmithyIdentityAPI.Identity import struct Smithy.Attributes -public protocol Signer { +public protocol Signer: Sendable { func signRequest( requestBuilder: HTTPRequestBuilder, diff --git a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignatureType.swift b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignatureType.swift index 8aabd0d95..960abbfeb 100644 --- a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignatureType.swift +++ b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignatureType.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public enum AWSSignatureType { +public enum AWSSignatureType: Sendable { case requestHeaders case requestQueryParams case requestChunk diff --git a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignedBodyHeader.swift b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignedBodyHeader.swift index dfb4151b9..131d7e7f7 100644 --- a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignedBodyHeader.swift +++ b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignedBodyHeader.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public enum AWSSignedBodyHeader { +public enum AWSSignedBodyHeader: Sendable { case none case contentSha256 } diff --git a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/SigningAlgorithm.swift b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/SigningAlgorithm.swift index 098272da3..130879dc2 100644 --- a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/SigningAlgorithm.swift +++ b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/SigningAlgorithm.swift @@ -7,7 +7,7 @@ /// Type of signing algorithm /// String raw value used for serialization and deserialization -public enum SigningAlgorithm: String { +public enum SigningAlgorithm: String, Sendable { /// Signature Version 4 case sigv4 /// Signature Version 4 Asymmetric diff --git a/Sources/SmithyIdentityAPI/FlowType.swift b/Sources/SmithyIdentityAPI/FlowType.swift index fe5a19448..c8943ed0f 100644 --- a/Sources/SmithyIdentityAPI/FlowType.swift +++ b/Sources/SmithyIdentityAPI/FlowType.swift @@ -6,6 +6,6 @@ // /// The type of flow the middleware context is being constructed for -public enum FlowType { +public enum FlowType: Sendable { case NORMAL, PRESIGN_REQUEST, PRESIGN_URL } diff --git a/Sources/SmithyIdentityAPI/Identity.swift b/Sources/SmithyIdentityAPI/Identity.swift index 621634e9f..0144f72a1 100644 --- a/Sources/SmithyIdentityAPI/Identity.swift +++ b/Sources/SmithyIdentityAPI/Identity.swift @@ -8,6 +8,6 @@ import struct Foundation.Date // Base protocol for all identity types -public protocol Identity { +public protocol Identity: Sendable { var expiration: Date? { get } } diff --git a/Sources/SmithyIdentityAPI/IdentityResolver.swift b/Sources/SmithyIdentityAPI/IdentityResolver.swift index 09b5bb3c5..53052f045 100644 --- a/Sources/SmithyIdentityAPI/IdentityResolver.swift +++ b/Sources/SmithyIdentityAPI/IdentityResolver.swift @@ -8,7 +8,7 @@ import struct Smithy.Attributes // Base protocol for all identity provider types -public protocol IdentityResolver { +public protocol IdentityResolver: Sendable { associatedtype IdentityT: Identity func getIdentity(identityProperties: Attributes?) async throws -> IdentityT diff --git a/Sources/SmithyTestUtil/ProtocolTestClient.swift b/Sources/SmithyTestUtil/ProtocolTestClient.swift index d55f2402e..c3d591fb8 100644 --- a/Sources/SmithyTestUtil/ProtocolTestClient.swift +++ b/Sources/SmithyTestUtil/ProtocolTestClient.swift @@ -22,7 +22,7 @@ extension ProtocolTestClient: HTTPClient { } } -public class ProtocolTestIdempotencyTokenGenerator: ClientRuntime.IdempotencyTokenGenerator { +public class ProtocolTestIdempotencyTokenGenerator: ClientRuntime.IdempotencyTokenGenerator, @unchecked Sendable { public init() {} public func generateToken() -> String { diff --git a/Tests/ClientRuntimeTests/Endpoints/EndpointsAuthSchemeResolverTests.swift b/Tests/ClientRuntimeTests/Endpoints/EndpointsAuthSchemeResolverTests.swift index f14d82e0a..c7f2edc0b 100644 --- a/Tests/ClientRuntimeTests/Endpoints/EndpointsAuthSchemeResolverTests.swift +++ b/Tests/ClientRuntimeTests/Endpoints/EndpointsAuthSchemeResolverTests.swift @@ -7,6 +7,7 @@ import XCTest @testable import ClientRuntime +import enum SmithyHTTPAPI.EndpointPropertyValue final class EndpointsAuthSchemeResolverTests: XCTestCase { func testParsing() throws { @@ -15,7 +16,7 @@ final class EndpointsAuthSchemeResolverTests: XCTestCase { "name": "sigv4", "signingName": "s3", "disableDoubleEncoding": true - ] as [String: AnyHashable] + ] as [String: EndpointPropertyValue] let actualSigV4 = try EndpointsAuthScheme(from: sigV4) if case let .sigV4(data) = actualSigV4 { @@ -33,7 +34,7 @@ final class EndpointsAuthSchemeResolverTests: XCTestCase { "name": "sigv4a", "signingName": "s3", "disableDoubleEncoding": true - ] as [String: AnyHashable] + ] as [String: EndpointPropertyValue] let actualSigV4a = try EndpointsAuthScheme(from: sigV4A) if case let .sigV4A(data) = actualSigV4a { @@ -70,4 +71,4 @@ final class EndpointsAuthSchemeResolverTests: XCTestCase { XCTAssertThrowsError(try resolver.resolve(authSchemes: [none, sigV4A])) } -} \ No newline at end of file +} diff --git a/Tests/ClientRuntimeTests/InterceptorTests/InterceptorTests.swift b/Tests/ClientRuntimeTests/InterceptorTests/InterceptorTests.swift index 3d1e48f5d..ee931e4ea 100644 --- a/Tests/ClientRuntimeTests/InterceptorTests/InterceptorTests.swift +++ b/Tests/ClientRuntimeTests/InterceptorTests/InterceptorTests.swift @@ -20,7 +20,7 @@ class InterceptorTests: XCTestCase { public var property: String? } - struct AddAttributeInterceptor: Interceptor { + struct AddAttributeInterceptor: Interceptor { private let key: AttributeKey private let value: T diff --git a/Tests/ClientRuntimeTests/LoggingTests/SwiftLoggerTests.swift b/Tests/ClientRuntimeTests/LoggingTests/SwiftLoggerTests.swift index a9b3ae478..53deae67c 100644 --- a/Tests/ClientRuntimeTests/LoggingTests/SwiftLoggerTests.swift +++ b/Tests/ClientRuntimeTests/LoggingTests/SwiftLoggerTests.swift @@ -74,7 +74,7 @@ final class SwiftLoggerTests: XCTestCase { } -private class TestLogHandler: LogHandler { +private class TestLogHandler: LogHandler, @unchecked Sendable { let label: String var invocations = [TestLogInvocation]() diff --git a/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift index 41b3f1a34..fd58365ac 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift @@ -28,7 +28,6 @@ class EndpointTests: XCTestCase { let endpoint1 = try Endpoint(url: url) let endpoint2 = try Endpoint(url: url) XCTAssertEqual(endpoint1, endpoint2) - XCTAssertEqual(endpoint1.hashValue, endpoint2.hashValue) } func test_path_percentEncodedInput() throws { diff --git a/Tests/SmithyIdentityTests/MockAWSCredentialIdentityResolver.swift b/Tests/SmithyIdentityTests/MockAWSCredentialIdentityResolver.swift index 451562b1e..bebdfb89c 100644 --- a/Tests/SmithyIdentityTests/MockAWSCredentialIdentityResolver.swift +++ b/Tests/SmithyIdentityTests/MockAWSCredentialIdentityResolver.swift @@ -10,9 +10,9 @@ import struct Smithy.Attributes import struct SmithyIdentity.AWSCredentialIdentity struct MockAWSCredentialIdentityResolver: AWSCredentialIdentityResolver { - let _getIdentity: () async throws -> AWSCredentialIdentity + let _getIdentity: @Sendable () async throws -> AWSCredentialIdentity - init(_ _getIdentity: @escaping () async throws -> AWSCredentialIdentity) { + init(_ _getIdentity: @escaping @Sendable () async throws -> AWSCredentialIdentity) { self._getIdentity = _getIdentity } diff --git a/Tests/SmithyJSONTests/FloatingPointTests.swift b/Tests/SmithyJSONTests/FloatingPointTests.swift index 693cd3830..2389a0e28 100644 --- a/Tests/SmithyJSONTests/FloatingPointTests.swift +++ b/Tests/SmithyJSONTests/FloatingPointTests.swift @@ -15,32 +15,30 @@ import XCTest // This set of tests is intended to verify that Doubles and Floats can be read accurately from JSON. final class FloatingPointTests: XCTestCase { - - static var originalFloats = [Float]() - static var floatData = Data() - static var originalDoubles = [Double]() - static var doubleData = Data() + var originalFloats = [Float]() + var floatData = Data() + var originalDoubles = [Double]() + var doubleData = Data() // Renders 250 floats & doubles to JSON arrays. Numbers are random so they will // likely use all available decimal places. - static override func setUp() { + override func setUp() { + super.setUp() originalFloats = (1..<250).map { _ in Float.random(in: 0.0...1.0) } originalDoubles = (1..<250).map { _ in Double.random(in: 0.0...1.0) } floatData = try! JSONEncoder().encode(originalFloats) doubleData = try! JSONEncoder().encode(originalDoubles) } - // Read the floats from JSON using JSONReader, and compare the values to the original. func test_floatRead() throws { - let reader = try Reader.from(data: Self.floatData) + let reader = try Reader.from(data: floatData) let floats = try reader.readList(memberReadingClosure: ReadingClosures.readFloat(from:), memberNodeInfo: "", isFlattened: false) - XCTAssert(floats == Self.originalFloats) + XCTAssert(floats == originalFloats) } - // Read the doubles from JSON using JSONReader, and compare the values to the original. func test_doubleRead() throws { - let reader = try Reader.from(data: Self.doubleData) + let reader = try Reader.from(data: doubleData) let doubles = try reader.readList(memberReadingClosure: ReadingClosures.readDouble(from:), memberNodeInfo: "", isFlattened: false) - XCTAssert(doubles == Self.originalDoubles) + XCTAssert(doubles == originalDoubles) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt index 058d23542..6905f0c4a 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt @@ -99,10 +99,10 @@ class EndpointTestGenerator( testCase.expect.endpoint.ifPresent { endpoint -> writer.write("let actual = try resolver.resolve(params: endpointParams)").write("") - // [String: AnyHashable] can't be constructed from a dictionary literal + // [String: EndpointPropertyValue] can't be constructed from a dictionary literal // first create a string JSON string literal - // then convert to [String: AnyHashable] using JSONSerialization.jsonObject(with:) - writer.openBlock("let properties: [String: AnyHashable] = ", "") { + // then convert to [String: EndpointPropertyValue] using JSONSerialization.jsonObject(with:) + writer.openBlock("let properties: [String: \$N] = ", "", SmithyHTTPAPITypes.EndpointPropertyValue) { generateProperties(writer, endpoint.properties) } @@ -156,7 +156,7 @@ class EndpointTestGenerator( writer: SwiftWriter, value: Value, delimeter: String, - castToAnyHashable: Boolean, + castToEndpointPropertyValue: Boolean, ) { when (value) { is StringValue -> { @@ -176,11 +176,11 @@ class EndpointTestGenerator( } is ArrayValue -> { - val castStmt = if (castToAnyHashable) " as [AnyHashable]$delimeter" else delimeter + val castStmt = if (castToEndpointPropertyValue) " as [EndpointPropertyValue]$delimeter" else delimeter writer.openBlock("[", "]$castStmt") { value.values.forEachIndexed { idx, item -> writer.call { - generateValue(writer, item, if (idx < value.values.count() - 1) "," else "", castToAnyHashable) + generateValue(writer, item, if (idx < value.values.count() - 1) "," else "", castToEndpointPropertyValue) } } } @@ -190,11 +190,11 @@ class EndpointTestGenerator( if (value.value.isEmpty()) { writer.writeInline("[:]") } else { - writer.openBlock("[", "] as [String: AnyHashable]$delimeter") { + writer.openBlock("[", "] as [String: EndpointPropertyValue]$delimeter") { value.value.map { it.key to it.value }.forEachIndexed { idx, (first, second) -> writer.writeInline("\$S: ", first.name) writer.call { - generateValue(writer, second, if (idx < value.value.count() - 1) "," else "", castToAnyHashable) + generateValue(writer, second, if (idx < value.value.count() - 1) "," else "", castToEndpointPropertyValue) } } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyHTTPAPITypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyHTTPAPITypes.kt index 8d2788a82..b55648e11 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyHTTPAPITypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SmithyHTTPAPITypes.kt @@ -12,6 +12,7 @@ import software.amazon.smithy.swift.codegen.SwiftDependency */ object SmithyHTTPAPITypes { val Endpoint = runtimeSymbol("Endpoint", SwiftDeclaration.STRUCT) + val EndpointPropertyValue = runtimeSymbol("EndpointPropertyValue", SwiftDeclaration.ENUM) val HttpClient = runtimeSymbol("HTTPClient", SwiftDeclaration.PROTOCOL) val Header = runtimeSymbol("Header", SwiftDeclaration.STRUCT) val Headers = runtimeSymbol("Headers", SwiftDeclaration.STRUCT) From ebf4060aa4f9be174763d8155ad37e28e2dfec74 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 9 Apr 2025 17:42:12 -0400 Subject: [PATCH 08/35] swiftlint --- .../Networking/Http/CRT/CRTClientEngine.swift | 57 +++++++++++++++---- .../Http/CRT/HTTP2Stream+ByteStream.swift | 3 +- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 66bad34f4..6444e076c 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -74,7 +74,14 @@ public class CRTClientEngine: HTTPClient { self.socketTimeout = config.socketTimeout } - init(windowSize: Int, maxConnectionsPerEndpoint: Int, telemetry: HttpTelemetry, connectTimeoutMs: UInt32? = nil, crtTLSOptions: CRTClientTLSOptions? = nil, socketTimeout: UInt32? = nil) { + init( + windowSize: Int, + maxConnectionsPerEndpoint: Int, + telemetry: HttpTelemetry, + connectTimeoutMs: UInt32? = nil, + crtTLSOptions: CRTClientTLSOptions? = nil, + socketTimeout: UInt32? = nil + ) { self.windowSize = windowSize self.maxConnectionsPerEndpoint = maxConnectionsPerEndpoint self.telemetry = telemetry @@ -240,9 +247,17 @@ public class CRTClientEngine: HTTPClient { switch connection.httpVersion { case .version_1_1: - return try await handleHTTP1Request(connection: connection, request: request, telemetryContext: telemetryContext) + return try await handleHTTP1Request( + connection: connection, + request: request, + telemetryContext: telemetryContext + ) case .version_2: - return try await handleHTTP2Request(connection: connection, request: request, telemetryContext: telemetryContext) + return try await handleHTTP2Request( + connection: connection, + request: request, + telemetryContext: telemetryContext + ) case .unknown: fatalError("Unknown HTTP version") } @@ -415,7 +430,11 @@ public class CRTClientEngine: HTTPClient { } } - private func handleHTTP2Request(connection: HTTPClientConnection, request: HTTPRequest, telemetryContext: TelemetryContext) async throws -> HTTPResponse { + private func handleHTTP2Request( + connection: HTTPClientConnection, + request: HTTPRequest, + telemetryContext: TelemetryContext + ) async throws -> HTTPResponse { self.logger.debug("Using HTTP/2 connection") let crtRequest = try request.toHttp2Request() return try await withCheckedThrowingContinuation { (continuation: StreamContinuation) in @@ -461,10 +480,14 @@ public class CRTClientEngine: HTTPClient { throw StreamError.notSupported("HTTP1Stream should be used with an HTTP/1.1 connection!") } guard case .stream(let bodyStream) = request.body, bodyStream.isEligibleForChunkedStreaming else { - throw ByteStreamError.invalidStreamTypeForChunkedBody("The stream is not eligible for chunked streaming or is not a stream type!") + throw ByteStreamError.invalidStreamTypeForChunkedBody( + "The stream is not eligible for chunked streaming or is not a stream type!" + ) } guard let chunkedStream = bodyStream as? ChunkedStream else { - throw ByteStreamError.streamDoesNotConformToChunkedStream("Stream does not conform to ChunkedStream! Type is \\(bodyStream).") + throw ByteStreamError.streamDoesNotConformToChunkedStream( + "Stream does not conform to ChunkedStream! Type is \\(bodyStream)." + ) } var hasMoreChunks = true @@ -476,14 +499,28 @@ public class CRTClientEngine: HTTPClient { let finalChunk = try await chunkedStream.chunkedReader.getFinalChunk() try await http1Stream.writeChunk(chunk: finalChunk, endOfStream: true) var bytesSentAttributes = Attributes() - bytesSentAttributes.set(key: HttpMetricsAttributesKeys.serverAddress, value: CRTClientEngine.makeServerAddress(request: request)) - telemetry.bytesSent.add(value: finalChunk.count, attributes: bytesSentAttributes, context: telemetry.contextManager.current()) + bytesSentAttributes.set( + key: HttpMetricsAttributesKeys.serverAddress, + value: CRTClientEngine.makeServerAddress(request: request) + ) + telemetry.bytesSent.add( + value: finalChunk.count, + attributes: bytesSentAttributes, + context: telemetry.contextManager.current() + ) break } else { try await http1Stream.writeChunk(chunk: currentChunk, endOfStream: false) var bytesSentAttributes = Attributes() - bytesSentAttributes.set(key: HttpMetricsAttributesKeys.serverAddress, value: CRTClientEngine.makeServerAddress(request: request)) - telemetry.bytesSent.add(value: currentChunk.count, attributes: bytesSentAttributes, context: telemetry.contextManager.current()) + bytesSentAttributes.set( + key: HttpMetricsAttributesKeys.serverAddress, + value: CRTClientEngine.makeServerAddress(request: request) + ) + telemetry.bytesSent.add( + value: currentChunk.count, + attributes: bytesSentAttributes, + context: telemetry.contextManager.current() + ) } } } diff --git a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift index f5e8ec020..f2a7cd79d 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift @@ -17,7 +17,8 @@ extension HTTP2Stream { } /// Writes the ByteStream to the stream asynchronously - /// There is no recommended size for the data to write. The data will be written in chunks of `manualWriteBufferSize` bytes. + /// There is no recommended size for the data to write. + /// The data will be written in chunks of `manualWriteBufferSize` bytes. /// - Parameter body: The body to write /// - Throws: Throws an error if the write fails func write(body: ByteStream, telemetry: HttpTelemetry, serverAddress: String) async throws { From b1491d3be307a3d189ab1b1a567c02fbe1799f9a Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 14 Apr 2025 11:25:07 -0400 Subject: [PATCH 09/35] more sendables --- .../DefaultMessageDecoder.swift | 2 +- .../DefaultMessageDecoderStream.swift | 2 +- .../SmithyEventStreamsAPI/MessageDecoder.swift | 2 +- .../SmithyEventStreamsAPI/UnmarshalClosure.swift | 2 +- Sources/SmithyHTTPAuth/AWSSigningConfig.swift | 6 +++--- .../SigningConfigFields/AWSSignedBodyValue.swift | 2 +- .../SigningConfigFields/SigningFlags.swift | 2 +- .../smithy/swift/codegen/EndpointTestGenerator.kt | 15 ++++++--------- .../codegen/endpoints/EndpointParamsGenerator.kt | 2 +- 9 files changed, 16 insertions(+), 19 deletions(-) diff --git a/Sources/SmithyEventStreams/DefaultMessageDecoder.swift b/Sources/SmithyEventStreams/DefaultMessageDecoder.swift index 0777a3420..8a7549a01 100644 --- a/Sources/SmithyEventStreams/DefaultMessageDecoder.swift +++ b/Sources/SmithyEventStreams/DefaultMessageDecoder.swift @@ -16,7 +16,7 @@ import struct Foundation.Data /// AWS implementation of `MessageDecoder` for decoding event stream messages /// Note: This is class because struct does not allow using `self` in closure /// and we need to use `self` to access `messageBuffer` per CRT API. -public class DefaultMessageDecoder: MessageDecoder { +public class DefaultMessageDecoder: MessageDecoder, @unchecked Sendable { private var decoder: EventStreamMessageDecoder? private var messageBuffer: [Message] = [] private var error: Error? diff --git a/Sources/SmithyEventStreams/DefaultMessageDecoderStream.swift b/Sources/SmithyEventStreams/DefaultMessageDecoderStream.swift index e00872098..fed4efcb2 100644 --- a/Sources/SmithyEventStreams/DefaultMessageDecoderStream.swift +++ b/Sources/SmithyEventStreams/DefaultMessageDecoderStream.swift @@ -11,7 +11,7 @@ import SmithyEventStreamsAuthAPI import struct Foundation.Data /// Stream adapter that decodes input data into `EventStream.Message` objects. -public struct DefaultMessageDecoderStream: MessageDecoderStream { +public struct DefaultMessageDecoderStream: MessageDecoderStream, Sendable { public typealias Element = Event let stream: ReadableStream diff --git a/Sources/SmithyEventStreamsAPI/MessageDecoder.swift b/Sources/SmithyEventStreamsAPI/MessageDecoder.swift index 3b06d24f8..d2104df30 100644 --- a/Sources/SmithyEventStreamsAPI/MessageDecoder.swift +++ b/Sources/SmithyEventStreamsAPI/MessageDecoder.swift @@ -8,7 +8,7 @@ import struct Foundation.Data /// Decodes a `Data` object into a `Message` object. -public protocol MessageDecoder { +public protocol MessageDecoder: Sendable { /// Feeds data into the decoder, which may or may not result in a message. /// - Parameter data: The data to feed into the decoder. diff --git a/Sources/SmithyEventStreamsAPI/UnmarshalClosure.swift b/Sources/SmithyEventStreamsAPI/UnmarshalClosure.swift index de5c4888c..7ba9a821a 100644 --- a/Sources/SmithyEventStreamsAPI/UnmarshalClosure.swift +++ b/Sources/SmithyEventStreamsAPI/UnmarshalClosure.swift @@ -5,4 +5,4 @@ // SPDX-License-Identifier: Apache-2.0 // -public typealias UnmarshalClosure = (Message) throws -> T +public typealias UnmarshalClosure = @Sendable (Message) throws -> T diff --git a/Sources/SmithyHTTPAuth/AWSSigningConfig.swift b/Sources/SmithyHTTPAuth/AWSSigningConfig.swift index 4a48c6872..4765ca3f7 100644 --- a/Sources/SmithyHTTPAuth/AWSSigningConfig.swift +++ b/Sources/SmithyHTTPAuth/AWSSigningConfig.swift @@ -17,7 +17,7 @@ import struct Foundation.TimeInterval import struct SmithyHTTPAuthAPI.SigningFlags import struct SmithyIdentity.AWSCredentialIdentity -public struct AWSSigningConfig { +public struct AWSSigningConfig: Sendable { public let credentials: AWSCredentialIdentity? public let awsCredentialIdentityResolver: (any AWSCredentialIdentityResolver)? public let expiration: TimeInterval @@ -27,7 +27,7 @@ public struct AWSSigningConfig { public let date: Date public let service: String public let region: String - public let shouldSignHeader: ((String) -> Bool)? + public let shouldSignHeader: (@Sendable (String) -> Bool)? public let signatureType: AWSSignatureType public let signingAlgorithm: SigningAlgorithm @@ -41,7 +41,7 @@ public struct AWSSigningConfig { date: Date, service: String, region: String, - shouldSignHeader: ((String) -> Bool)? = nil, + shouldSignHeader: (@Sendable (String) -> Bool)? = nil, signatureType: AWSSignatureType, signingAlgorithm: SigningAlgorithm ) { diff --git a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignedBodyValue.swift b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignedBodyValue.swift index d4110fca8..6389998ee 100644 --- a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignedBodyValue.swift +++ b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/AWSSignedBodyValue.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public enum AWSSignedBodyValue { +public enum AWSSignedBodyValue: Sendable { case empty case emptySha256 case unsignedPayload diff --git a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/SigningFlags.swift b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/SigningFlags.swift index ea1c431d4..59f1e028f 100644 --- a/Sources/SmithyHTTPAuthAPI/SigningConfigFields/SigningFlags.swift +++ b/Sources/SmithyHTTPAuthAPI/SigningConfigFields/SigningFlags.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct SigningFlags { +public struct SigningFlags: Sendable { public let useDoubleURIEncode: Bool public let shouldNormalizeURIPath: Bool public let omitSessionToken: Bool diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt index 6905f0c4a..a2c41b839 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt @@ -75,8 +75,7 @@ class EndpointTestGenerator( generateValue( writer, value, - if (idx < applicableParams.count() - 1) "," else "", - false, + if (idx < applicableParams.count() - 1) "," else "" ) } } @@ -142,7 +141,7 @@ class EndpointTestGenerator( val value = Value.fromNode(second) writer.writeInline("\$S: ", first) writer.call { - generateValue(writer, value, if (idx < properties.values.count() - 1) "," else "", true) + generateValue(writer, value, if (idx < properties.values.count() - 1) "," else "") } } } @@ -156,7 +155,6 @@ class EndpointTestGenerator( writer: SwiftWriter, value: Value, delimeter: String, - castToEndpointPropertyValue: Boolean, ) { when (value) { is StringValue -> { @@ -176,11 +174,10 @@ class EndpointTestGenerator( } is ArrayValue -> { - val castStmt = if (castToEndpointPropertyValue) " as [EndpointPropertyValue]$delimeter" else delimeter - writer.openBlock("[", "]$castStmt") { + writer.openBlock("[", "]$delimeter") { value.values.forEachIndexed { idx, item -> writer.call { - generateValue(writer, item, if (idx < value.values.count() - 1) "," else "", castToEndpointPropertyValue) + generateValue(writer, item, if (idx < value.values.count() - 1) "," else "") } } } @@ -190,11 +187,11 @@ class EndpointTestGenerator( if (value.value.isEmpty()) { writer.writeInline("[:]") } else { - writer.openBlock("[", "] as [String: EndpointPropertyValue]$delimeter") { + writer.openBlock("[", "]$delimeter") { value.value.map { it.key to it.value }.forEachIndexed { idx, (first, second) -> writer.writeInline("\$S: ", first.name) writer.call { - generateValue(writer, second, if (idx < value.value.count() - 1) "," else "", castToEndpointPropertyValue) + generateValue(writer, second, if (idx < value.value.count() - 1) "," else "") } } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt index 03cb1c353..fc69ce3fb 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/endpoints/EndpointParamsGenerator.kt @@ -41,7 +41,7 @@ class EndpointParamsGenerator( writer: SwiftWriter, endpointRuleSet: EndpointRuleSet?, ) { - writer.openBlock("public struct EndpointParams {", "}") { + writer.openBlock("public struct EndpointParams: Sendable {", "}") { endpointRuleSet?.parameters?.toList()?.sortedBy { it.name.toString() }?.let { sortedParameters -> renderMembers(writer, sortedParameters) writer.write("") From 082c5f1f67210a2a3c870a06f9adfafa905f8989 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 14 Apr 2025 15:23:03 -0400 Subject: [PATCH 10/35] temp add Package.resolved --- Package.resolved | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 000000000..4478765c5 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,33 @@ +{ + "originHash" : "5fb2136fb45769f2b13e0fc74c80cb130312f142ca1864e908cebdabac2066a1", + "pins" : [ + { + "identity" : "aws-crt-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/awslabs/aws-crt-swift.git", + "state" : { + "branch" : "swift6-support", + "revision" : "9b16f05a202fa1f4d75ce20fdf8ebbbff5ce7e04" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", + "version" : "1.6.3" + } + } + ], + "version" : 3 +} From 43b13d13b78bf3b0c2b19256151717ca00373fa5 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 14 Apr 2025 16:32:07 -0400 Subject: [PATCH 11/35] remove xcbeautify --- .github/workflows/continuous-integration.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 4f9e575e4..5f081498b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -77,9 +77,7 @@ jobs: NSUnbufferedIO=YES xcodebuild \ -scheme smithy-swift-Package \ -destination '${{ matrix.destination }}' \ - test 2>&1 \ - | xcbeautify - + test 2>&1 apple-downstream: if: github.repository == 'smithy-lang/smithy-swift' || github.event_name == 'pull_request' runs-on: ${{ matrix.runner }} From 0d41fdc643aa649c7e799f23189fa6c0e9ccc50b Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 14 Apr 2025 16:45:32 -0400 Subject: [PATCH 12/35] bump minimum xcode to 15.3 --- .github/workflows/continuous-integration.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 5f081498b..f3b06b91b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -21,7 +21,7 @@ jobs: - macos-14 - macos-15 xcode: - - Xcode_15.2 + - Xcode_15.3 - Xcode_16.1 destination: - 'platform=iOS Simulator,OS=17.2,name=iPhone 15' @@ -37,7 +37,7 @@ jobs: xcode: Xcode_16.1 # Don't run new macOS with old Xcode - runner: macos-15 - xcode: Xcode_15.2 + xcode: Xcode_15.3 # Don't run old simulators with new Xcode - destination: 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)' xcode: Xcode_16.1 @@ -47,11 +47,11 @@ jobs: xcode: Xcode_16.1 # Don't run new simulators with old Xcode - destination: 'platform=tvOS Simulator,OS=18.1,name=Apple TV 4K (3rd generation) (at 1080p)' - xcode: Xcode_15.2 + xcode: Xcode_15.3 - destination: 'platform=iOS Simulator,OS=18.1,name=iPhone 16' - xcode: Xcode_15.2 + xcode: Xcode_15.3 - destination: 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' - xcode: Xcode_15.2 + xcode: Xcode_15.3 steps: - name: Configure Xcode run: | @@ -89,7 +89,7 @@ jobs: - macos-14 - macos-15 xcode: - - Xcode_15.2 + - Xcode_15.3 - Xcode_16.1 destination: - 'platform=iOS Simulator,OS=17.2,name=iPhone 15' @@ -105,7 +105,7 @@ jobs: xcode: Xcode_16.1 # Don't run new macOS with old Xcode - runner: macos-15 - xcode: Xcode_15.2 + xcode: Xcode_15.3 # Don't run old simulators with new Xcode - destination: 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)' xcode: Xcode_16.1 @@ -115,11 +115,11 @@ jobs: xcode: Xcode_16.1 # Don't run new simulators with old Xcode - destination: 'platform=tvOS Simulator,OS=18.1,name=Apple TV 4K (3rd generation) (at 1080p)' - xcode: Xcode_15.2 + xcode: Xcode_15.3 - destination: 'platform=iOS Simulator,OS=18.1,name=iPhone 16' - xcode: Xcode_15.2 + xcode: Xcode_15.3 - destination: 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' - xcode: Xcode_15.2 + xcode: Xcode_15.3 steps: - name: Configure Xcode run: | From b672e526931cb42d3e53f63c27263fa7e5afe993 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 14 Apr 2025 17:53:33 -0400 Subject: [PATCH 13/35] add back xcbeautify --- .github/workflows/continuous-integration.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index f3b06b91b..216da07ab 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -77,7 +77,8 @@ jobs: NSUnbufferedIO=YES xcodebuild \ -scheme smithy-swift-Package \ -destination '${{ matrix.destination }}' \ - test 2>&1 + test 2>&1 \ + | xcbeautify apple-downstream: if: github.repository == 'smithy-lang/smithy-swift' || github.event_name == 'pull_request' runs-on: ${{ matrix.runner }} From a7afa38a3011fb0df50f33a8a56ad5a67da03719 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 15 Apr 2025 12:38:12 -0400 Subject: [PATCH 14/35] add Event: Sendable and remove 5.9 focal --- .github/workflows/continuous-integration.yml | 2 -- Sources/SmithyEventStreams/DefaultMessageDecoderStream.swift | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 216da07ab..c075835ae 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -176,7 +176,6 @@ jobs: fail-fast: false matrix: swift: - - 5.9-focal - 6.0-jammy container: image: swift:${{ matrix.swift }} @@ -206,7 +205,6 @@ jobs: fail-fast: false matrix: swift: - - 5.9-focal - 6.0-jammy container: image: swift:${{ matrix.swift }} diff --git a/Sources/SmithyEventStreams/DefaultMessageDecoderStream.swift b/Sources/SmithyEventStreams/DefaultMessageDecoderStream.swift index fed4efcb2..a95d6b2ca 100644 --- a/Sources/SmithyEventStreams/DefaultMessageDecoderStream.swift +++ b/Sources/SmithyEventStreams/DefaultMessageDecoderStream.swift @@ -11,7 +11,7 @@ import SmithyEventStreamsAuthAPI import struct Foundation.Data /// Stream adapter that decodes input data into `EventStream.Message` objects. -public struct DefaultMessageDecoderStream: MessageDecoderStream, Sendable { +public struct DefaultMessageDecoderStream: MessageDecoderStream, Sendable { public typealias Element = Event let stream: ReadableStream From a8da6ea4b0ec06f4e7e68f27f8a4b4ea6d5395d4 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 15 Apr 2025 12:40:48 -0400 Subject: [PATCH 15/35] remove xcbeautify --- .github/workflows/continuous-integration.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index c075835ae..b2c42a35e 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -77,8 +77,7 @@ jobs: NSUnbufferedIO=YES xcodebuild \ -scheme smithy-swift-Package \ -destination '${{ matrix.destination }}' \ - test 2>&1 \ - | xcbeautify + test 2>&1 apple-downstream: if: github.repository == 'smithy-lang/smithy-swift' || github.event_name == 'pull_request' runs-on: ${{ matrix.runner }} From 829882dd6f83e2d673b6ccabe613357898b3c089 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 15 Apr 2025 13:37:55 -0400 Subject: [PATCH 16/35] remove xcode 15.3 for now --- .github/workflows/continuous-integration.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b2c42a35e..ab820d725 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -21,7 +21,6 @@ jobs: - macos-14 - macos-15 xcode: - - Xcode_15.3 - Xcode_16.1 destination: - 'platform=iOS Simulator,OS=17.2,name=iPhone 15' @@ -35,9 +34,6 @@ jobs: # Don't run old macOS with new Xcode - runner: macos-14 xcode: Xcode_16.1 - # Don't run new macOS with old Xcode - - runner: macos-15 - xcode: Xcode_15.3 # Don't run old simulators with new Xcode - destination: 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)' xcode: Xcode_16.1 @@ -45,13 +41,6 @@ jobs: xcode: Xcode_16.1 - destination: 'platform=visionOS Simulator,OS=1.0,name=Apple Vision Pro' xcode: Xcode_16.1 - # Don't run new simulators with old Xcode - - destination: 'platform=tvOS Simulator,OS=18.1,name=Apple TV 4K (3rd generation) (at 1080p)' - xcode: Xcode_15.3 - - destination: 'platform=iOS Simulator,OS=18.1,name=iPhone 16' - xcode: Xcode_15.3 - - destination: 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' - xcode: Xcode_15.3 steps: - name: Configure Xcode run: | @@ -89,7 +78,6 @@ jobs: - macos-14 - macos-15 xcode: - - Xcode_15.3 - Xcode_16.1 destination: - 'platform=iOS Simulator,OS=17.2,name=iPhone 15' From 7a7e74524ab1b6fb02ef1d9ebb7d430415051d5a Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 15 Apr 2025 13:43:32 -0400 Subject: [PATCH 17/35] remove 15.3 and allow xcode 16.1 to run with macos 14 --- .github/workflows/continuous-integration.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ab820d725..3b4911330 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -31,9 +31,6 @@ jobs: - 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' - 'platform=macOS' exclude: - # Don't run old macOS with new Xcode - - runner: macos-14 - xcode: Xcode_16.1 # Don't run old simulators with new Xcode - destination: 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)' xcode: Xcode_16.1 @@ -88,12 +85,6 @@ jobs: - 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' - 'platform=macOS' exclude: - # Don't run old macOS with new Xcode - - runner: macos-14 - xcode: Xcode_16.1 - # Don't run new macOS with old Xcode - - runner: macos-15 - xcode: Xcode_15.3 # Don't run old simulators with new Xcode - destination: 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)' xcode: Xcode_16.1 @@ -101,13 +92,6 @@ jobs: xcode: Xcode_16.1 - destination: 'platform=visionOS Simulator,OS=1.0,name=Apple Vision Pro' xcode: Xcode_16.1 - # Don't run new simulators with old Xcode - - destination: 'platform=tvOS Simulator,OS=18.1,name=Apple TV 4K (3rd generation) (at 1080p)' - xcode: Xcode_15.3 - - destination: 'platform=iOS Simulator,OS=18.1,name=iPhone 16' - xcode: Xcode_15.3 - - destination: 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' - xcode: Xcode_15.3 steps: - name: Configure Xcode run: | From 67a9febafb480e4099b12c633d6d4909a9a5e984 Mon Sep 17 00:00:00 2001 From: Chan <55515281+sichanyoo@users.noreply.github.com> Date: Tue, 15 Apr 2025 11:04:26 -0700 Subject: [PATCH 18/35] fix: Fix silent failure with CRT HTTP client when response stream has an error (#921) Co-authored-by: Sichan Yoo --- .../Networking/Http/CRT/CRTClientEngine.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 4040c698e..011338110 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -536,14 +536,11 @@ public class CRTClientEngine: HTTPClient { switch result { case .success(let statusCode): response.statusCode = makeStatusCode(statusCode) + stream.close() case .failure(let error): self.logger.error("Response encountered an error: \(error)") - continuation.safeResume(error: error) + stream.closeWithError(error) } - - // closing the stream is required to signal to the caller that the response is complete - // and no more data will be received in this stream - stream.close() } requestOptions.http2ManualDataWrites = http2ManualDataWrites From 0e274f5b138c2c4c8463c61f1cc28e4109940aef Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 16 Apr 2025 16:33:39 -0400 Subject: [PATCH 19/35] update package.resolved --- Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.resolved b/Package.resolved index 4478765c5..b4415276b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -7,7 +7,7 @@ "location" : "https://github.com/awslabs/aws-crt-swift.git", "state" : { "branch" : "swift6-support", - "revision" : "9b16f05a202fa1f4d75ce20fdf8ebbbff5ce7e04" + "revision" : "f7ea06a571f64c12b501d3a3ad772eee94f547a5" } }, { From ea89587951f9950361f5ddd53c805596cae5b9f0 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 17 Apr 2025 11:23:28 -0400 Subject: [PATCH 20/35] ktlint --- .../amazon/smithy/swift/codegen/EndpointTestGenerator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt index a2c41b839..398316bf6 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/EndpointTestGenerator.kt @@ -75,7 +75,7 @@ class EndpointTestGenerator( generateValue( writer, value, - if (idx < applicableParams.count() - 1) "," else "" + if (idx < applicableParams.count() - 1) "," else "", ) } } From 6162f2cf10e2edb88741bbb04e31026bec8238e4 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 17 Apr 2025 15:15:07 -0400 Subject: [PATCH 21/35] Clean up CRTClientEngine & fix failing test --- .../Networking/Http/CRT/CRTClientEngine.swift | 81 +++++-------------- 1 file changed, 19 insertions(+), 62 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 1301bead7..0995c24d6 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -408,7 +408,7 @@ public class CRTClientEngine: HTTPClient { Task { @Sendable in do { - try await ClientRuntime.sendChunkedBody( + try await CRTClientEngine.sendChunkedBody( using: stream, request: request, logger: logger, @@ -475,18 +475,27 @@ public class CRTClientEngine: HTTPClient { } } - private func sendChunkedBody(using stream: Any, request: HTTPRequest) async throws { + private static func sendChunkedBody( + using stream: HTTPStream, + request: HTTPRequest, + logger: LogAgent, + telemetry: HttpTelemetry, + serverAddress: String + ) async throws { guard let http1Stream = stream as? HTTP1Stream else { throw StreamError.notSupported("HTTP1Stream should be used with an HTTP/1.1 connection!") } - guard case .stream(let bodyStream) = request.body, bodyStream.isEligibleForChunkedStreaming else { + + let body = request.body + guard case .stream(let stream) = body, stream.isEligibleForChunkedStreaming else { throw ByteStreamError.invalidStreamTypeForChunkedBody( "The stream is not eligible for chunked streaming or is not a stream type!" ) } - guard let chunkedStream = bodyStream as? ChunkedStream else { + + guard let chunkedStream = stream as? ChunkedStream else { throw ByteStreamError.streamDoesNotConformToChunkedStream( - "Stream does not conform to ChunkedStream! Type is \\(bodyStream)." + "Stream does not conform to ChunkedStream! Type is \(stream)." ) } @@ -495,13 +504,15 @@ public class CRTClientEngine: HTTPClient { hasMoreChunks = try await chunkedStream.chunkedReader.processNextChunk() let currentChunk = chunkedStream.chunkedReader.getCurrentChunk() let currentChunkBodyIsEmpty = chunkedStream.chunkedReader.getCurrentChunkBody().isEmpty + if !hasMoreChunks || currentChunkBodyIsEmpty { let finalChunk = try await chunkedStream.chunkedReader.getFinalChunk() try await http1Stream.writeChunk(chunk: finalChunk, endOfStream: true) + var bytesSentAttributes = Attributes() bytesSentAttributes.set( key: HttpMetricsAttributesKeys.serverAddress, - value: CRTClientEngine.makeServerAddress(request: request) + value: serverAddress ) telemetry.bytesSent.add( value: finalChunk.count, @@ -511,10 +522,11 @@ public class CRTClientEngine: HTTPClient { break } else { try await http1Stream.writeChunk(chunk: currentChunk, endOfStream: false) + var bytesSentAttributes = Attributes() bytesSentAttributes.set( key: HttpMetricsAttributesKeys.serverAddress, - value: CRTClientEngine.makeServerAddress(request: request) + value: serverAddress ) telemetry.bytesSent.add( value: currentChunk.count, @@ -626,58 +638,3 @@ public class CRTClientEngine: HTTPClient { } } } - -private func sendChunkedBody( - using stream: HTTPStream, - request: HTTPRequest, - logger: LogAgent, - telemetry: HttpTelemetry, - serverAddress: String -) async throws { - guard let http1Stream = stream as? HTTP1Stream else { - throw StreamError.notSupported("HTTP1Stream should be used with an HTTP/1.1 connection!") - } - - let body = request.body - guard case .stream(let stream) = body, stream.isEligibleForChunkedStreaming else { - throw ByteStreamError.invalidStreamTypeForChunkedBody( - "The stream is not eligible for chunked streaming or is not a stream type!" - ) - } - - guard let chunkedStream = stream as? ChunkedStream else { - throw ByteStreamError.streamDoesNotConformToChunkedStream( - "Stream does not conform to ChunkedStream! Type is \(stream)." - ) - } - - var hasMoreChunks = true - while hasMoreChunks { - hasMoreChunks = try await chunkedStream.chunkedReader.processNextChunk() - let currentChunkBodyIsEmpty = chunkedStream.chunkedReader.getCurrentChunkBody().isEmpty - - if !hasMoreChunks || currentChunkBodyIsEmpty { - let finalChunk = try await chunkedStream.chunkedReader.getFinalChunk() - try await http1Stream.writeChunk(chunk: finalChunk, endOfStream: true) - - var bytesSentAttributes = Attributes() - bytesSentAttributes.set(key: HttpMetricsAttributesKeys.serverAddress, value: serverAddress) - telemetry.bytesSent.add( - value: finalChunk.count, - attributes: bytesSentAttributes, - context: telemetry.contextManager.current() - ) - } else { - let currentChunk = chunkedStream.chunkedReader.getCurrentChunk() - try await http1Stream.writeChunk(chunk: currentChunk, endOfStream: false) - - var bytesSentAttributes = Attributes() - bytesSentAttributes.set(key: HttpMetricsAttributesKeys.serverAddress, value: serverAddress) - telemetry.bytesSent.add( - value: currentChunk.count, - attributes: bytesSentAttributes, - context: telemetry.contextManager.current() - ) - } - } -} From a6a80403f5999f06dbd1a4cecea8fc32988bd87a Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 18 Apr 2025 14:33:20 -0400 Subject: [PATCH 22/35] change ContinuationWrapper from actor to synchronous final class using locks --- .../Networking/Http/CRT/CRTClientEngine.swift | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 0995c24d6..0151249fd 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -318,7 +318,7 @@ public class CRTClientEngine: HTTPClient { // END - smithy.client.http.requests.queued_duration } catch { logger.error(error.localizedDescription) - await wrappedContinuation.safeResume(error: error) + wrappedContinuation.resume(throwing: error) return } @@ -417,15 +417,13 @@ public class CRTClientEngine: HTTPClient { ) } catch { logger.error("Chunked streaming error: \(error.localizedDescription)") - await wrappedContinuation.safeResume(error: error) + wrappedContinuation.resume(throwing: error) } } } } catch { self.logger.error("Request setup error: \(error.localizedDescription)") - Task { - await wrappedContinuation.safeResume(error: error) - } + wrappedContinuation.resume(throwing: error) } } } @@ -467,9 +465,7 @@ public class CRTClientEngine: HTTPClient { } } catch { self.logger.error(error.localizedDescription) - Task { - await wrappedContinuation.safeResume(error: error) - } + wrappedContinuation.resume(throwing: error) return } } @@ -550,7 +546,7 @@ public class CRTClientEngine: HTTPClient { /// - Returns: A `HTTPRequestOptions` object that can be used to make a HTTP request private func makeHttpRequestStreamOptions( request: HTTPRequestBase, - continuation: ContinuationWrapper, + continuation: ContinuationWrapper, http2ManualDataWrites: Bool = false, serverAddress: String ) -> HTTPRequestOptions { @@ -576,9 +572,7 @@ public class CRTClientEngine: HTTPClient { // resume the continuation as soon as we have all the initial headers // this allows callers to start reading the response as it comes in // instead of waiting for the entire response to be received - Task { - await continuation.safeResume(response: response) - } + continuation.resume(returning: response) } onIncomingBody: { bodyChunk in self.logger.debug("Body chunk received") do { @@ -620,21 +614,30 @@ public class CRTClientEngine: HTTPClient { } } - actor ContinuationWrapper { - private var continuation: StreamContinuation? + final class ContinuationWrapper: @unchecked Sendable { + + private var continuation: CheckedContinuation? + private let lock = NSLock() + + init(_ cont: CheckedContinuation) { + self.continuation = cont + } - public init(_ continuation: StreamContinuation) { - self.continuation = continuation + func resume(returning value: Response) { + take()?.resume(returning: value) } - public func safeResume(response: HTTPResponse) { - continuation?.resume(returning: response) - continuation = nil + func resume(throwing error: Error) { + take()?.resume(throwing: error) } - public func safeResume(error: Error) { - continuation?.resume(throwing: error) - continuation = nil + /// Fetch the continuation exactly once. Returns the current value under + /// a lock, then sets it to `nil` so later callers get nothing. + private func take() -> CheckedContinuation? { + lock.lock() + defer { lock.unlock() } + defer { continuation = nil } + return continuation } } } From f22919f51541a31db36f9e9c3025e57766a5c62d Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 21 Apr 2025 11:30:37 -0400 Subject: [PATCH 23/35] remove unnecessary SerialExecutor constructor --- .../Networking/Http/CRT/CRTClientEngine.swift | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 0151249fd..11ed4c183 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -74,23 +74,6 @@ public class CRTClientEngine: HTTPClient { self.socketTimeout = config.socketTimeout } - init( - windowSize: Int, - maxConnectionsPerEndpoint: Int, - telemetry: HttpTelemetry, - connectTimeoutMs: UInt32? = nil, - crtTLSOptions: CRTClientTLSOptions? = nil, - socketTimeout: UInt32? = nil - ) { - self.windowSize = windowSize - self.maxConnectionsPerEndpoint = maxConnectionsPerEndpoint - self.telemetry = telemetry - self.logger = self.telemetry.loggerProvider.getLogger(name: "SerialExecutor") - self.connectTimeoutMs = connectTimeoutMs - self.crtTLSOptions = crtTLSOptions - self.socketTimeout = socketTimeout - } - func getOrCreateConnectionPool(endpoint: Endpoint) throws -> HTTPClientConnectionManager { let poolID = ConnectionPoolID(endpoint: endpoint) guard let connectionPool = connectionPools[poolID] else { @@ -199,15 +182,7 @@ public class CRTClientEngine: HTTPClient { self.windowSize = config.windowSize self.telemetry = config.telemetry self.logger = self.telemetry.loggerProvider.getLogger(name: "CRTClientEngine") - - self.serialExecutor = SerialExecutor( - windowSize: config.windowSize, - maxConnectionsPerEndpoint: config.maxConnectionsPerEndpoint, - telemetry: config.telemetry, - connectTimeoutMs: config.connectTimeoutMs, - crtTLSOptions: config.crtTlsOptions, - socketTimeout: config.socketTimeout - ) + self.serialExecutor = SerialExecutor(config: config) } public func send(request: HTTPRequest) async throws -> HTTPResponse { From bb3bf8da7220c45d1740ae0d09afe157166833d0 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 21 Apr 2025 15:10:06 -0400 Subject: [PATCH 24/35] DefaultAuthSchemeResolver Sendable --- .../ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift | 2 +- .../ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift | 1 + Sources/Smithy/Context.swift | 2 +- Sources/SmithyEventStreamsAPI/Header.swift | 4 ++-- Sources/SmithyEventStreamsAPI/Message.swift | 2 +- Sources/SmithyEventStreamsAuthAPI/MessageDataSigner.swift | 4 ++-- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift index 3c3a3a805..9d347b24f 100644 --- a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift +++ b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift @@ -131,7 +131,7 @@ public class DefaultAuthSchemeResolverParameters: AuthSchemeResolverParameters { } } -public class DefaultAuthSchemeResolver: AuthSchemeResolver, @unchecked Sendable { +public final class DefaultAuthSchemeResolver: AuthSchemeResolver, Sendable { public func resolveAuthScheme(params: AuthSchemeResolverParameters) throws -> [AuthOption] { return [] } diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 11ed4c183..8f195e65e 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -589,6 +589,7 @@ public class CRTClientEngine: HTTPClient { } } + // Guaranteed to be safe in concurrent environments due to use of locks final class ContinuationWrapper: @unchecked Sendable { private var continuation: CheckedContinuation? diff --git a/Sources/Smithy/Context.swift b/Sources/Smithy/Context.swift index cd58b3217..ed1f98317 100644 --- a/Sources/Smithy/Context.swift +++ b/Sources/Smithy/Context.swift @@ -7,7 +7,7 @@ import class Foundation.NSRecursiveLock -public class Context { +public final class Context: @unchecked Sendable { private var _attributes: Attributes private let lock = NSRecursiveLock() diff --git a/Sources/SmithyEventStreamsAPI/Header.swift b/Sources/SmithyEventStreamsAPI/Header.swift index ff6e42a03..d57951e1e 100644 --- a/Sources/SmithyEventStreamsAPI/Header.swift +++ b/Sources/SmithyEventStreamsAPI/Header.swift @@ -9,7 +9,7 @@ import Foundation /// A header in an event stream message. /// Headers are used to convey metadata about the message. -public struct Header: Equatable { +public struct Header: Equatable, Sendable { /// The name of the header. public let name: String @@ -25,7 +25,7 @@ public struct Header: Equatable { /// The value of a header in an event stream message. /// Encoders and decoders may use this to determine how to encode or decode the header. -public enum HeaderValue: Equatable { +public enum HeaderValue: Equatable, Sendable { case bool(Bool) case byte(Int8) case int16(Int16) diff --git a/Sources/SmithyEventStreamsAPI/Message.swift b/Sources/SmithyEventStreamsAPI/Message.swift index ddf94b88d..8ad27dfcd 100644 --- a/Sources/SmithyEventStreamsAPI/Message.swift +++ b/Sources/SmithyEventStreamsAPI/Message.swift @@ -8,7 +8,7 @@ import struct Foundation.Data /// A message in an event stream that can be sent or received. -public struct Message: Equatable { +public struct Message: Equatable, Sendable { /// The headers associated with the message. public let headers: [Header] diff --git a/Sources/SmithyEventStreamsAuthAPI/MessageDataSigner.swift b/Sources/SmithyEventStreamsAuthAPI/MessageDataSigner.swift index ad715581f..b87606d7a 100644 --- a/Sources/SmithyEventStreamsAuthAPI/MessageDataSigner.swift +++ b/Sources/SmithyEventStreamsAuthAPI/MessageDataSigner.swift @@ -10,7 +10,7 @@ import struct SmithyEventStreamsAPI.Message import struct Foundation.Data /// Protocol for signing messages. -public protocol MessageDataSigner { +public protocol MessageDataSigner: Sendable { /// Signs a message. /// - Parameters: @@ -25,7 +25,7 @@ public protocol MessageDataSigner { ) async throws -> SigningResult } -public struct SigningResult { +public struct SigningResult: Sendable { public let output: T public let signature: String From 2310612a46d4615c08c85e966f3bf60ed0a34375 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 28 Apr 2025 16:05:49 -0400 Subject: [PATCH 25/35] set smithy tools version to 5.9 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 5edaa6852..d25b70ca2 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.0 +// swift-tools-version:5.9 import Foundation import PackageDescription From 4735c1d07b3d1a981a0b2de503e66b0b762f88a3 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 28 Apr 2025 16:44:02 -0400 Subject: [PATCH 26/35] revert CI changes --- .github/workflows/continuous-integration.yml | 34 +++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b6f8aed01..b512cbdea 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -21,6 +21,7 @@ jobs: - macos-14 - macos-15 xcode: + - Xcode_15.2 - Xcode_16.1 destination: - 'platform=iOS Simulator,OS=17.2,name=iPhone 15' @@ -31,6 +32,12 @@ jobs: - 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' - 'platform=macOS' exclude: + # Don't run old macOS with new Xcode + - runner: macos-14 + xcode: Xcode_16.1 + # Don't run new macOS with old Xcode + - runner: macos-15 + xcode: Xcode_15.2 # Don't run old simulators with new Xcode - destination: 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)' xcode: Xcode_16.1 @@ -38,6 +45,13 @@ jobs: xcode: Xcode_16.1 - destination: 'platform=visionOS Simulator,OS=1.0,name=Apple Vision Pro' xcode: Xcode_16.1 + # Don't run new simulators with old Xcode + - destination: 'platform=tvOS Simulator,OS=18.1,name=Apple TV 4K (3rd generation) (at 1080p)' + xcode: Xcode_15.2 + - destination: 'platform=iOS Simulator,OS=18.1,name=iPhone 16' + xcode: Xcode_15.2 + - destination: 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' + xcode: Xcode_15.2 steps: - name: Configure Xcode run: | @@ -63,7 +77,9 @@ jobs: NSUnbufferedIO=YES xcodebuild \ -scheme smithy-swift-Package \ -destination '${{ matrix.destination }}' \ - test 2>&1 + test 2>&1 \ + | xcbeautify + apple-downstream: if: github.repository == 'smithy-lang/smithy-swift' || github.event_name == 'pull_request' runs-on: ${{ matrix.runner }} @@ -75,6 +91,7 @@ jobs: - macos-14 - macos-15 xcode: + - Xcode_15.2 - Xcode_16.1 destination: - 'platform=iOS Simulator,OS=17.2,name=iPhone 15' @@ -85,6 +102,12 @@ jobs: - 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' - 'platform=macOS' exclude: + # Don't run old macOS with new Xcode + - runner: macos-14 + xcode: Xcode_16.1 + # Don't run new macOS with old Xcode + - runner: macos-15 + xcode: Xcode_15.2 # Don't run old simulators with new Xcode - destination: 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)' xcode: Xcode_16.1 @@ -92,6 +115,13 @@ jobs: xcode: Xcode_16.1 - destination: 'platform=visionOS Simulator,OS=1.0,name=Apple Vision Pro' xcode: Xcode_16.1 + # Don't run new simulators with old Xcode + - destination: 'platform=tvOS Simulator,OS=18.1,name=Apple TV 4K (3rd generation) (at 1080p)' + xcode: Xcode_15.2 + - destination: 'platform=iOS Simulator,OS=18.1,name=iPhone 16' + xcode: Xcode_15.2 + - destination: 'platform=visionOS Simulator,OS=2.1,name=Apple Vision Pro' + xcode: Xcode_15.2 steps: - name: Configure Xcode run: | @@ -150,6 +180,7 @@ jobs: - ubuntu-24.04 - ubuntu-24.04-arm swift: + - 5.9-focal - 6.0-jammy container: image: swift:${{ matrix.swift }} @@ -182,6 +213,7 @@ jobs: - ubuntu-24.04 - ubuntu-24.04-arm swift: + - 5.9-focal - 6.0-jammy container: image: swift:${{ matrix.swift }} From c638616cfef334efb42ec49a9d2b9aa344357914 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 29 Apr 2025 17:28:56 -0400 Subject: [PATCH 27/35] use crt 0.58.1 --- Package.resolved | 33 ------------------- Package.swift | 2 +- Sources/SmithyStreams/FileStream.swift | 21 ++++++++++++ .../SmithyStreams/StreamableHttpBody.swift | 8 +++++ .../smithy/swift/codegen/SwiftDependency.kt | 4 +-- 5 files changed, 32 insertions(+), 36 deletions(-) delete mode 100644 Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index b4415276b..000000000 --- a/Package.resolved +++ /dev/null @@ -1,33 +0,0 @@ -{ - "originHash" : "5fb2136fb45769f2b13e0fc74c80cb130312f142ca1864e908cebdabac2066a1", - "pins" : [ - { - "identity" : "aws-crt-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/awslabs/aws-crt-swift.git", - "state" : { - "branch" : "swift6-support", - "revision" : "f7ea06a571f64c12b501d3a3ad772eee94f547a5" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "3d8596ed08bd13520157f0355e35caed215ffbfa", - "version" : "1.6.3" - } - } - ], - "version" : 3 -} diff --git a/Package.swift b/Package.swift index d25b70ca2..79d3e9121 100644 --- a/Package.swift +++ b/Package.swift @@ -56,7 +56,7 @@ let package = Package( ], dependencies: { var dependencies: [Package.Dependency] = [ - .package(url: "https://github.com/awslabs/aws-crt-swift.git", branch: "swift6-support"), + .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.52.1"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), ] let isDocCEnabled = ProcessInfo.processInfo.environment["AWS_SWIFT_SDK_ENABLE_DOCC"] != nil diff --git a/Sources/SmithyStreams/FileStream.swift b/Sources/SmithyStreams/FileStream.swift index 25908c53c..c9431fadf 100644 --- a/Sources/SmithyStreams/FileStream.swift +++ b/Sources/SmithyStreams/FileStream.swift @@ -140,3 +140,24 @@ public final class FileStream: Stream, @unchecked Sendable { close() } } + +fileprivate extension FileHandle { + func length() throws -> UInt64 { + let length: UInt64 + let savedPos: UInt64 + if #available(macOS 11, tvOS 13.4, iOS 13.4, watchOS 6.2, *) { + savedPos = try offset() + try seekToEnd() + length = try offset() + } else { + savedPos = offsetInFile + seekToEndOfFile() + length = offsetInFile + } + guard length != savedPos else { + return length + } + try self.seek(toOffset: savedPos) + return length + } +} diff --git a/Sources/SmithyStreams/StreamableHttpBody.swift b/Sources/SmithyStreams/StreamableHttpBody.swift index e0479819d..e94f6dc54 100644 --- a/Sources/SmithyStreams/StreamableHttpBody.swift +++ b/Sources/SmithyStreams/StreamableHttpBody.swift @@ -109,4 +109,12 @@ public class StreamableHttpBody: IStreamable { return nil } } + + public func isEndOfStream() -> Bool { + do { + return try self.position >= self.length() + } catch { + return false + } + } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt index 5ffc3a2f0..12a624de6 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt @@ -55,8 +55,8 @@ class SwiftDependency( val CRT = SwiftDependency( "AwsCommonRuntimeKit", - "swift6-support", - "", + null, + "0.58.2", "https://github.com/awslabs/aws-crt-swift", "", "aws-crt-swift", From 31dca2710e35f2f7cd7db8dac92e2ce39877924a Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 29 Apr 2025 17:40:09 -0400 Subject: [PATCH 28/35] version correction --- .../software/amazon/smithy/swift/codegen/SwiftDependency.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt index 12a624de6..631d33338 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt @@ -56,7 +56,7 @@ class SwiftDependency( SwiftDependency( "AwsCommonRuntimeKit", null, - "0.58.2", + "0.52.1", "https://github.com/awslabs/aws-crt-swift", "", "aws-crt-swift", From 72ed616fe6ffe11af6bcc426394649f40a051759 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 30 Apr 2025 10:58:04 -0400 Subject: [PATCH 29/35] address PR comments --- .../ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift | 6 +++--- Sources/SmithyHTTPAPI/HTTPRequest.swift | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index d1b21ec0e..e48d44b17 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -611,9 +611,9 @@ public class CRTClientEngine: HTTPClient { /// a lock, then sets it to `nil` so later callers get nothing. private func take() -> CheckedContinuation? { lock.lock() - defer { lock.unlock() } - defer { continuation = nil } - return continuation + defer { continuation = nil; lock.unlock() } + let pendingContinuation = continuation + return pendingContinuation } } } diff --git a/Sources/SmithyHTTPAPI/HTTPRequest.swift b/Sources/SmithyHTTPAPI/HTTPRequest.swift index 3482403e4..224fea91f 100644 --- a/Sources/SmithyHTTPAPI/HTTPRequest.swift +++ b/Sources/SmithyHTTPAPI/HTTPRequest.swift @@ -25,7 +25,8 @@ import struct Foundation.URLRequest // we need to maintain a reference to this same request while we add headers // in the CRT engine so that is why it's a class -public final class HTTPRequest: RequestMessage { +// developer must ensure HTTPRequest’s internal state remains thread-safe +public final class HTTPRequest: RequestMessage, @unchecked Sendable { public var body: ByteStream public let destination: URI public var headers: Headers @@ -274,5 +275,3 @@ extension HTTPRequestBuilder { return String(signatureSequence) } } - -extension HTTPRequest: @unchecked Sendable {} From f104d7e7dda59ee39b341e2f6c74df76415da70c Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 1 May 2025 13:16:30 -0400 Subject: [PATCH 30/35] try adding CI to edit swift-tools version --- .github/workflows/codegen.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml index fe22c5f20..25eab9c47 100644 --- a/.github/workflows/codegen.yml +++ b/.github/workflows/codegen.yml @@ -38,3 +38,9 @@ jobs: run: | git diff test -z "$(git diff)" + + - name: Test on swift-tools-version 6.0 + run: | + perl -pi -e 's{^// swift-tools-version:\d+\.\d+}{// swift-tools-version:6.0}' Package.swift + swift build && swift test + git restore Package.swift \ No newline at end of file From 958a5e32c1a3450ae5908465f8ea1d4fb177fab2 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 1 May 2025 13:28:30 -0400 Subject: [PATCH 31/35] try adding new swift6 compat workflow --- .github/workflows/codegen.yml | 6 ---- .github/workflows/swift6-compatibility.yml | 33 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/swift6-compatibility.yml diff --git a/.github/workflows/codegen.yml b/.github/workflows/codegen.yml index 25eab9c47..fe22c5f20 100644 --- a/.github/workflows/codegen.yml +++ b/.github/workflows/codegen.yml @@ -38,9 +38,3 @@ jobs: run: | git diff test -z "$(git diff)" - - - name: Test on swift-tools-version 6.0 - run: | - perl -pi -e 's{^// swift-tools-version:\d+\.\d+}{// swift-tools-version:6.0}' Package.swift - swift build && swift test - git restore Package.swift \ No newline at end of file diff --git a/.github/workflows/swift6-compatibility.yml b/.github/workflows/swift6-compatibility.yml new file mode 100644 index 000000000..c08e65af9 --- /dev/null +++ b/.github/workflows/swift6-compatibility.yml @@ -0,0 +1,33 @@ +name: Swift6 Compatibility + +on: + push: + branches: [ main ] + pull_request: + workflow_dispatch: + +env: + AWS_SWIFT_SDK_USE_LOCAL_DEPS: 1 + +jobs: + swift6-compatibility: + if: github.repository == 'smithy-lang/smithy-swift' || github.event_name == 'pull_request' + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + runner: + - macos-15 + xcode: + - Xcode_16.1 + destination: + - 'platform=macOS' + steps: + - name: Checkout smithy-swift + uses: actions/checkout@v4 + - name: Setup common tools + uses: ./.github/actions/setup-common-tools-composite-action + - name: Test on swift-tools-version 6.0 + run: | + perl -pi -e 's{^// swift-tools-version:\d+\.\d+}{// swift-tools-version:6.0}' Package.swift + swift build && swift test From df7c76783554c1565fc6093ae6c595108edd0cbf Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 1 May 2025 13:32:31 -0400 Subject: [PATCH 32/35] intentionally remove Sendable to check build --- .github/workflows/swift6-compatibility.yml | 3 ++- Sources/SmithyHTTPAPI/HTTPRequest.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift6-compatibility.yml b/.github/workflows/swift6-compatibility.yml index c08e65af9..8811e204a 100644 --- a/.github/workflows/swift6-compatibility.yml +++ b/.github/workflows/swift6-compatibility.yml @@ -10,7 +10,7 @@ env: AWS_SWIFT_SDK_USE_LOCAL_DEPS: 1 jobs: - swift6-compatibility: + bump-swift-tools-version: if: github.repository == 'smithy-lang/smithy-swift' || github.event_name == 'pull_request' runs-on: ${{ matrix.runner }} strategy: @@ -30,4 +30,5 @@ jobs: - name: Test on swift-tools-version 6.0 run: | perl -pi -e 's{^// swift-tools-version:\d+\.\d+}{// swift-tools-version:6.0}' Package.swift + set -euo pipefail swift build && swift test diff --git a/Sources/SmithyHTTPAPI/HTTPRequest.swift b/Sources/SmithyHTTPAPI/HTTPRequest.swift index 224fea91f..b2eea3025 100644 --- a/Sources/SmithyHTTPAPI/HTTPRequest.swift +++ b/Sources/SmithyHTTPAPI/HTTPRequest.swift @@ -26,7 +26,7 @@ import struct Foundation.URLRequest // we need to maintain a reference to this same request while we add headers // in the CRT engine so that is why it's a class // developer must ensure HTTPRequest’s internal state remains thread-safe -public final class HTTPRequest: RequestMessage, @unchecked Sendable { +public final class HTTPRequest: RequestMessage { public var body: ByteStream public let destination: URI public var headers: Headers From 2cb970285e76cceb6372e23b11c177b83317baf4 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 1 May 2025 13:35:53 -0400 Subject: [PATCH 33/35] add back Sendable --- .github/workflows/swift6-compatibility.yml | 4 ++-- Sources/SmithyHTTPAPI/HTTPRequest.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/swift6-compatibility.yml b/.github/workflows/swift6-compatibility.yml index 8811e204a..756dffb5f 100644 --- a/.github/workflows/swift6-compatibility.yml +++ b/.github/workflows/swift6-compatibility.yml @@ -27,8 +27,8 @@ jobs: uses: actions/checkout@v4 - name: Setup common tools uses: ./.github/actions/setup-common-tools-composite-action - - name: Test on swift-tools-version 6.0 + - name: Build on swift-tools-version 6.0 run: | perl -pi -e 's{^// swift-tools-version:\d+\.\d+}{// swift-tools-version:6.0}' Package.swift set -euo pipefail - swift build && swift test + swift build diff --git a/Sources/SmithyHTTPAPI/HTTPRequest.swift b/Sources/SmithyHTTPAPI/HTTPRequest.swift index b2eea3025..224fea91f 100644 --- a/Sources/SmithyHTTPAPI/HTTPRequest.swift +++ b/Sources/SmithyHTTPAPI/HTTPRequest.swift @@ -26,7 +26,7 @@ import struct Foundation.URLRequest // we need to maintain a reference to this same request while we add headers // in the CRT engine so that is why it's a class // developer must ensure HTTPRequest’s internal state remains thread-safe -public final class HTTPRequest: RequestMessage { +public final class HTTPRequest: RequestMessage, @unchecked Sendable { public var body: ByteStream public let destination: URI public var headers: Headers From 0fca3663cc8dd25ade0c47c42d697ebf1c6683ee Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 1 May 2025 14:35:40 -0400 Subject: [PATCH 34/35] purposefully remove Sendable from HTTPResponse --- Sources/SmithyHTTPAPI/HTTPResponse.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SmithyHTTPAPI/HTTPResponse.swift b/Sources/SmithyHTTPAPI/HTTPResponse.swift index 20e430c1e..21bd260c0 100644 --- a/Sources/SmithyHTTPAPI/HTTPResponse.swift +++ b/Sources/SmithyHTTPAPI/HTTPResponse.swift @@ -10,7 +10,7 @@ import protocol Smithy.Stream import enum Smithy.ByteStream import class Foundation.NSRecursiveLock -public final class HTTPResponse: ResponseMessage, @unchecked Sendable { +public final class HTTPResponse: ResponseMessage { private var lock = NSRecursiveLock() private var _headers: Headers From 4515eb6107268887017309ab6da1e9a23c0e47b9 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 1 May 2025 14:37:06 -0400 Subject: [PATCH 35/35] add back Sendable to HTTPResponse --- Sources/SmithyHTTPAPI/HTTPResponse.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SmithyHTTPAPI/HTTPResponse.swift b/Sources/SmithyHTTPAPI/HTTPResponse.swift index 21bd260c0..20e430c1e 100644 --- a/Sources/SmithyHTTPAPI/HTTPResponse.swift +++ b/Sources/SmithyHTTPAPI/HTTPResponse.swift @@ -10,7 +10,7 @@ import protocol Smithy.Stream import enum Smithy.ByteStream import class Foundation.NSRecursiveLock -public final class HTTPResponse: ResponseMessage { +public final class HTTPResponse: ResponseMessage, @unchecked Sendable { private var lock = NSRecursiveLock() private var _headers: Headers