Skip to content

Commit

Permalink
Async nonblocking fileio (#597)
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler authored Mar 30, 2024
1 parent fb597d5 commit 8383e7b
Show file tree
Hide file tree
Showing 8 changed files with 59 additions and 80 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ let package = Package(
.library(name: "SotoSignerV4", targets: ["SotoSignerV4"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.63.0"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"4.0.0"),
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.1"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.0"),
.package(url: "https://github.com/apple/swift-metrics.git", "1.0.0"..<"3.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.42.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.7.2"),
.package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.13.1"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.19.0"),
Expand Down
2 changes: 0 additions & 2 deletions Sources/SotoCore/AWSClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ public final class AWSClient: Sendable {
public let httpClient: HTTPClient
/// Keeps a record of how we obtained the HTTP client
let httpClientProvider: HTTPClientProvider
/// EventLoopGroup used by AWSClient
public var eventLoopGroup: EventLoopGroup { return self.httpClient.eventLoopGroup }
/// Logger used for non-request based output
let clientLogger: Logger
/// client options
Expand Down
2 changes: 0 additions & 2 deletions Sources/SotoCore/AWSService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ extension AWSService {
public var region: Region { return config.region }
/// The url to use in requests
public var endpoint: String { return config.endpoint }
/// The EventLoopGroup service is using
public var eventLoopGroup: EventLoopGroup { return client.eventLoopGroup }

/// Return new version of Service with edited parameters
/// - Parameters:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import Logging
import NIOConcurrencyHelpers
import NIOCore
import NIOPosix
import SotoSignerV4

final class ConfigFileCredentialProvider: CredentialProviderSelector {
Expand Down Expand Up @@ -60,14 +61,15 @@ final class ConfigFileCredentialProvider: CredentialProviderSelector {
configFilePath: String,
for profile: String,
context: CredentialProviderFactory.Context,
endpoint: String?
endpoint: String?,
threadPool: NIOThreadPool = .singleton
) async throws -> CredentialProvider {
let sharedCredentials = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: credentialsFilePath,
configFilePath: configFilePath,
profile: profile,
context: context
).get()
threadPool: threadPool
)
return try self.credentialProvider(from: sharedCredentials, context: context, endpoint: endpoint)
}

Expand Down
67 changes: 28 additions & 39 deletions Sources/SotoCore/Credential/ConfigFileLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,36 +94,31 @@ enum ConfigFileLoader {
credentialsFilePath: String,
configFilePath: String,
profile: String,
context: CredentialProviderFactory.Context
) -> EventLoopFuture<SharedCredentials> {
let threadPool = NIOThreadPool(numberOfThreads: 1)
threadPool.start()
threadPool: NIOThreadPool = .singleton
) async throws -> SharedCredentials {
let fileIO = NonBlockingFileIO(threadPool: threadPool)

// Load credentials file
return self.loadFile(path: credentialsFilePath, on: context.httpClient.eventLoopGroup.any(), using: fileIO)
.flatMap { credentialsByteBuffer in
// Load profile config file
return self.loadFile(path: configFilePath, on: context.httpClient.eventLoopGroup.any(), using: fileIO)
.map {
(credentialsByteBuffer, $0)
}
.flatMapError { _ in
// Recover from error if profile config file does not exist
context.httpClient.eventLoopGroup.any().makeSucceededFuture((credentialsByteBuffer, nil))
}
}
.flatMapErrorThrowing { _ in
// Throw `.noProvider` error if credential file cannot be loaded
throw CredentialProviderError.noProvider
}
.flatMapThrowing { credentialsByteBuffer, configByteBuffer in
return try self.parseSharedCredentials(from: credentialsByteBuffer, configByteBuffer: configByteBuffer, for: profile)
}
.always { _ in
// shutdown the threadpool async
threadPool.shutdownGracefully { _ in }
}
let credentialsByteBuffer: ByteBuffer
do {
// Load credentials file
credentialsByteBuffer = try await self.loadFile(
path: credentialsFilePath,
fileIO: fileIO
)
} catch {
// Throw `.noProvider` error if credential file cannot be loaded
throw CredentialProviderError.noProvider
}
let configByteBuffer: ByteBuffer?
do {
// Load profile config file
configByteBuffer = try await self.loadFile(
path: configFilePath,
fileIO: fileIO
)
} catch {
configByteBuffer = nil
}
return try self.parseSharedCredentials(from: credentialsByteBuffer, configByteBuffer: configByteBuffer, for: profile)
}

/// Load a file from disk without blocking the current thread
Expand All @@ -132,17 +127,11 @@ enum ConfigFileLoader {
/// - eventLoop: event loop to run everything on
/// - fileIO: non-blocking file IO
/// - Returns: Event loop future with file contents in a byte-buffer
static func loadFile(path: String, on eventLoop: EventLoop, using fileIO: NonBlockingFileIO) -> EventLoopFuture<ByteBuffer> {
static func loadFile(path: String, fileIO: NonBlockingFileIO) async throws -> ByteBuffer {
let path = self.expandTildeInFilePath(path)

return fileIO.openFile(path: path, eventLoop: eventLoop)
.flatMap { handle, region in
fileIO.read(fileRegion: region, allocator: ByteBufferAllocator(), eventLoop: eventLoop).and(value: handle)
}
.flatMapThrowing { byteBuffer, handle in
try handle.close()
return byteBuffer
}
return try await fileIO.withFileRegion(path: path) { fileRegion in
try await fileIO.read(fileHandle: fileRegion.fileHandle, byteCount: fileRegion.readableBytes, allocator: ByteBufferAllocator())
}
}

// MARK: - Byte Buffer parsing (INIParser)
Expand Down
1 change: 1 addition & 0 deletions Tests/SotoCoreTests/Concurrency/ExpiringValueTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ final class ExpiringValueTests: XCTestCase {
return (1, Date())
}
await Task.yield()
await Task.yield()
// test it return current value
XCTAssertEqual(value, 0)
// test it kicked off a task
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,8 @@ class ConfigFileCredentialProviderTests: XCTestCase {
let sharedCredentials = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: filename,
configFilePath: "/dev/null",
profile: profile,
context: context
).get()
profile: profile
)

switch sharedCredentials {
case .assumeRole(let aRoleArn, _, _, let sourceCredentialProvider):
Expand Down
52 changes: 22 additions & 30 deletions Tests/SotoCoreTests/Credential/ConfigFileLoaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class ConfigFileLoadersTests: XCTestCase {
"""

let credentialsPath = try save(content: credentialsFile, prefix: #function)
let (context, httpClient) = try makeContext()
let (_, httpClient) = try makeContext()

defer {
try? FileManager.default.removeItem(atPath: credentialsPath)
Expand All @@ -55,9 +55,8 @@ class ConfigFileLoadersTests: XCTestCase {
let sharedCredentials = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: credentialsPath,
configFilePath: "/dev/null",
profile: profile,
context: context
).get()
profile: profile
)

switch sharedCredentials {
case .staticCredential(let credentials):
Expand Down Expand Up @@ -102,9 +101,8 @@ class ConfigFileLoadersTests: XCTestCase {
let sharedCredentials = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: credentialsPath,
configFilePath: configPath,
profile: profile,
context: context
).get()
profile: profile
)

switch sharedCredentials {
case .assumeRole(let aRoleArn, let aSessionName, let region, let sourceCredentialProvider):
Expand Down Expand Up @@ -139,9 +137,8 @@ class ConfigFileLoadersTests: XCTestCase {
let sharedCredentials = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: credentialsPath,
configFilePath: "non-existing-file-path",
profile: profile,
context: context
).get()
profile: profile
)

switch sharedCredentials {
case .assumeRole(let aRoleArn, _, _, let source):
Expand All @@ -164,7 +161,7 @@ class ConfigFileLoadersTests: XCTestCase {
"""

let credentialsPath = try save(content: credentialsFile, prefix: #function)
let (context, httpClient) = try makeContext()
let (_, httpClient) = try makeContext()

defer {
try? FileManager.default.removeItem(atPath: credentialsPath)
Expand All @@ -175,9 +172,8 @@ class ConfigFileLoadersTests: XCTestCase {
_ = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: credentialsPath,
configFilePath: "/dev/null",
profile: profile,
context: context
).get()
profile: profile
)
} catch ConfigFileLoader.ConfigFileError.missingAccessKeyId {
// Pass
} catch {
Expand All @@ -194,7 +190,7 @@ class ConfigFileLoadersTests: XCTestCase {
"""

let credentialsPath = try save(content: credentialsFile, prefix: #function)
let (context, httpClient) = try makeContext()
let (_, httpClient) = try makeContext()

defer {
try? FileManager.default.removeItem(atPath: credentialsPath)
Expand All @@ -205,9 +201,8 @@ class ConfigFileLoadersTests: XCTestCase {
_ = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: credentialsPath,
configFilePath: "/dev/null",
profile: profile,
context: context
).get()
profile: profile
)
} catch ConfigFileLoader.ConfigFileError.missingSecretAccessKey {
// Pass
} catch {
Expand All @@ -229,7 +224,7 @@ class ConfigFileLoadersTests: XCTestCase {
"""

let credentialsPath = try save(content: credentialsFile, prefix: #function)
let (context, httpClient) = try makeContext()
let (_, httpClient) = try makeContext()

defer {
try? FileManager.default.removeItem(atPath: credentialsPath)
Expand All @@ -240,9 +235,8 @@ class ConfigFileLoadersTests: XCTestCase {
_ = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: credentialsPath,
configFilePath: "/dev/null",
profile: profile,
context: context
).get()
profile: profile
)
} catch ConfigFileLoader.ConfigFileError.missingAccessKeyId {
// Pass
} catch {
Expand All @@ -264,7 +258,7 @@ class ConfigFileLoadersTests: XCTestCase {
"""

let credentialsPath = try save(content: credentialsFile, prefix: #function)
let (context, httpClient) = try makeContext()
let (_, httpClient) = try makeContext()

defer {
try? FileManager.default.removeItem(atPath: credentialsPath)
Expand All @@ -275,9 +269,8 @@ class ConfigFileLoadersTests: XCTestCase {
_ = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: credentialsPath,
configFilePath: "/dev/null",
profile: profile,
context: context
).get()
profile: profile
)
} catch ConfigFileLoader.ConfigFileError.missingSecretAccessKey {
// Pass
} catch {
Expand All @@ -294,7 +287,7 @@ class ConfigFileLoadersTests: XCTestCase {
"""

let credentialsPath = try save(content: credentialsFile, prefix: #function)
let (context, httpClient) = try makeContext()
let (_, httpClient) = try makeContext()

defer {
try? FileManager.default.removeItem(atPath: credentialsPath)
Expand All @@ -305,9 +298,8 @@ class ConfigFileLoadersTests: XCTestCase {
_ = try await ConfigFileLoader.loadSharedCredentials(
credentialsFilePath: credentialsPath,
configFilePath: "/dev/null",
profile: profile,
context: context
).get()
profile: profile
)
} catch ConfigFileLoader.ConfigFileError.invalidCredentialFile {
// Pass
} catch {
Expand Down

0 comments on commit 8383e7b

Please sign in to comment.