From 6979fdc39818d00663534d0abf2e1849236d0ca3 Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 25 Jul 2022 22:59:04 +1200 Subject: [PATCH 1/8] Added async await for DataLoader --- Sources/DataLoader/DataLoader.swift | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/Sources/DataLoader/DataLoader.swift b/Sources/DataLoader/DataLoader.swift index f0d7e81..6618f5f 100644 --- a/Sources/DataLoader/DataLoader.swift +++ b/Sources/DataLoader/DataLoader.swift @@ -217,3 +217,51 @@ final public class DataLoader { } } } + +#if compiler(>=5.5) && canImport(_Concurrency) + +/// Batch load function using async await +public typealias ConcurrentBatchLoadFunction = @Sendable (_ keys: [Key]) async throws -> [DataLoaderFutureValue] + +public extension DataLoader { + @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + convenience init( + on eventLoop: EventLoop, + options: DataLoaderOptions = DataLoaderOptions(), + throwing asyncThrowingLoadFunction: @escaping ConcurrentBatchLoadFunction + ) { + self.init(options: options, batchLoadFunction: { keys in + let promise = eventLoop.next().makePromise(of: [DataLoaderFutureValue].self) + promise.completeWithTask { + try await asyncThrowingLoadFunction(keys) + } + return promise.futureResult + }) + } + + /// Asynchronously loads a key, returning the value represented by that key. + @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + func load(key: Key, on eventLoopGroup: EventLoopGroup) async throws -> Value { + try await load(key: key, on: eventLoopGroup).get() + } + + /// Asynchronously loads multiple keys, promising an array of values: + /// + /// ``` + /// let aAndB = try await myLoader.loadMany(keys: [ "a", "b" ], on: eventLoopGroup) + /// ``` + /// + /// This is equivalent to the more verbose: + /// + /// ``` + /// async let a = myLoader.load(key: "a", on: eventLoopGroup) + /// async let b = myLoader.load(key: "b", on: eventLoopGroup) + /// let aAndB = try await a + b + /// ``` + @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + func loadMany(keys: [Key], on eventLoopGroup: EventLoopGroup) async throws -> [Value] { + try await loadMany(keys: keys, on: eventLoopGroup).get() + } +} + +#endif \ No newline at end of file From 1f16983811d83aeae2fb87d162cd791a641d802f Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 25 Jul 2022 22:59:26 +1200 Subject: [PATCH 2/8] Added async await test that is not redudant --- .../DataLoaderAsyncTests.swift | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 Tests/DataLoaderTests/DataLoaderAsyncTests.swift diff --git a/Tests/DataLoaderTests/DataLoaderAsyncTests.swift b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift new file mode 100644 index 0000000..930a745 --- /dev/null +++ b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift @@ -0,0 +1,106 @@ +import XCTest +import NIO + +@testable import DataLoader + +#if compiler(>=5.5) && canImport(_Concurrency) + +/// Primary API +@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) +final class DataLoaderAsyncTests: XCTestCase { + + /// Builds a really really simple data loader with async await + func testReallyReallySimpleDataLoader() async throws { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) + } + + let identityLoader = DataLoader( + on: eventLoopGroup.next(), + options: DataLoaderOptions(batchingEnabled: false) + ) { keys async in + let task = Task { + keys.map { DataLoaderFutureValue.success($0) } + } + return await task.value + } + + let value = try await identityLoader.load(key: 1, on: eventLoopGroup) + + XCTAssertEqual(value, 1) + } + + /// Supports loading multiple keys in one call + func testLoadingMultipleKeys() async throws { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) + } + + let identityLoader = DataLoader(on: eventLoopGroup.next()) { keys in + let task = Task { + keys.map { DataLoaderFutureValue.success($0) } + } + return await task.value + } + + let values = try await identityLoader.loadMany(keys: [1, 2], on: eventLoopGroup) + + XCTAssertEqual(values, [1,2]) + + let empty = try await identityLoader.loadMany(keys: [], on: eventLoopGroup) + + XCTAssertTrue(empty.isEmpty) + } + + // Batches multiple requests + func testMultipleRequests() async throws { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { + XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) + } + + actor LoadCalls { + var loadCalls = [[Int]]() + + func append(_ calls: [Int]) { + loadCalls.append(calls) + } + + static let shared: LoadCalls = .init() + } + + let identityLoader = DataLoader( + on: eventLoopGroup.next(), + options: DataLoaderOptions( + batchingEnabled: true, + executionPeriod: nil + ) + ) { keys in + await LoadCalls.shared.append(keys) + let task = Task { + keys.map { DataLoaderFutureValue.success($0) } + } + return await task.value + } + + async let value1 = identityLoader.load(key: 1, on: eventLoopGroup) + async let value2 = identityLoader.load(key: 2, on: eventLoopGroup) + + /// Have to wait for a split second because Tasks may not be executed before this statement + try await Task.sleep(nanoseconds: 500_000) + + XCTAssertNoThrow(try identityLoader.execute()) + + let result1 = try await value1 + XCTAssertEqual(result1, 1) + let result2 = try await value2 + XCTAssertEqual(result2, 2) + + let loadCalls = await LoadCalls.shared.loadCalls + XCTAssertEqual(loadCalls, [[1,2]]) + } +} + +#endif \ No newline at end of file From 127ec68a539b5d1bce58dac152d37fe9e1536b70 Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 25 Jul 2022 23:22:06 +1200 Subject: [PATCH 3/8] Fixed delay to be more reliable --- Tests/DataLoaderTests/DataLoaderAsyncTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/DataLoaderTests/DataLoaderAsyncTests.swift b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift index 930a745..902d7bb 100644 --- a/Tests/DataLoaderTests/DataLoaderAsyncTests.swift +++ b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift @@ -89,7 +89,7 @@ final class DataLoaderAsyncTests: XCTestCase { async let value2 = identityLoader.load(key: 2, on: eventLoopGroup) /// Have to wait for a split second because Tasks may not be executed before this statement - try await Task.sleep(nanoseconds: 500_000) + try await Task.sleep(nanoseconds: 500_000_000) XCTAssertNoThrow(try identityLoader.execute()) From b35b680dc79e74f7956954e8e1e475e8d628daf4 Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 25 Jul 2022 23:31:39 +1200 Subject: [PATCH 4/8] Opted for Task instead of async-let --- Tests/DataLoaderTests/DataLoaderAsyncTests.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Tests/DataLoaderTests/DataLoaderAsyncTests.swift b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift index 902d7bb..b6f28e7 100644 --- a/Tests/DataLoaderTests/DataLoaderAsyncTests.swift +++ b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift @@ -85,17 +85,24 @@ final class DataLoaderAsyncTests: XCTestCase { return await task.value } - async let value1 = identityLoader.load(key: 1, on: eventLoopGroup) - async let value2 = identityLoader.load(key: 2, on: eventLoopGroup) + // Normally async-let is a better option that using explicit Task, but the test machine fails to use async let multiple times already + // async let value1 = identityLoader.load(key: 1, on: eventLoopGroup) + // async let value2 = identityLoader.load(key: 2, on: eventLoopGroup) + let value1 = Task { + try await identityLoader.load(key: 1, on: eventLoopGroup) + } + let value2 = Task { + try await identityLoader.load(key: 2, on: eventLoopGroup) + } /// Have to wait for a split second because Tasks may not be executed before this statement try await Task.sleep(nanoseconds: 500_000_000) XCTAssertNoThrow(try identityLoader.execute()) - let result1 = try await value1 + let result1 = try await value1.value XCTAssertEqual(result1, 1) - let result2 = try await value2 + let result2 = try await value2.value XCTAssertEqual(result2, 2) let loadCalls = await LoadCalls.shared.loadCalls From b79b0d7d5886c26809284ee1afb3b068ea32cbbd Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 25 Jul 2022 23:53:01 +1200 Subject: [PATCH 5/8] Updated swift-actions and Moved actor from functions --- .github/workflows/test.yml | 2 +- .../DataLoaderAsyncTests.swift | 49 ++++++++++--------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 253488a..7ee3ec0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: os: [macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v2 - - uses: fwal/setup-swift@v1 + - uses: swift-actions/setup-swift@v1 - name: Build run: swift build - name: Run tests diff --git a/Tests/DataLoaderTests/DataLoaderAsyncTests.swift b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift index b6f28e7..1efd872 100644 --- a/Tests/DataLoaderTests/DataLoaderAsyncTests.swift +++ b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift @@ -5,6 +5,24 @@ import NIO #if compiler(>=5.5) && canImport(_Concurrency) +@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) +actor Concurrent { + var wrappedValue: T + + func nonmutating(_ action: (T) throws -> Returned) async rethrows -> Returned { + try action(wrappedValue) + } + + func mutating(_ action: (inout T) throws -> Returned) async rethrows -> Returned { + try action(&wrappedValue) + } + + init(_ value: T) { + self.wrappedValue = value + } +} + + /// Primary API @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) final class DataLoaderAsyncTests: XCTestCase { @@ -61,15 +79,7 @@ final class DataLoaderAsyncTests: XCTestCase { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - actor LoadCalls { - var loadCalls = [[Int]]() - - func append(_ calls: [Int]) { - loadCalls.append(calls) - } - - static let shared: LoadCalls = .init() - } + let loadCalls = Concurrent<[[Int]]>([]) let identityLoader = DataLoader( on: eventLoopGroup.next(), @@ -78,35 +88,28 @@ final class DataLoaderAsyncTests: XCTestCase { executionPeriod: nil ) ) { keys in - await LoadCalls.shared.append(keys) + await loadCalls.mutating { $0.append(keys) } let task = Task { keys.map { DataLoaderFutureValue.success($0) } } return await task.value } - // Normally async-let is a better option that using explicit Task, but the test machine fails to use async let multiple times already - // async let value1 = identityLoader.load(key: 1, on: eventLoopGroup) - // async let value2 = identityLoader.load(key: 2, on: eventLoopGroup) - let value1 = Task { - try await identityLoader.load(key: 1, on: eventLoopGroup) - } - let value2 = Task { - try await identityLoader.load(key: 2, on: eventLoopGroup) - } + async let value1 = identityLoader.load(key: 1, on: eventLoopGroup) + async let value2 = identityLoader.load(key: 2, on: eventLoopGroup) /// Have to wait for a split second because Tasks may not be executed before this statement try await Task.sleep(nanoseconds: 500_000_000) XCTAssertNoThrow(try identityLoader.execute()) - let result1 = try await value1.value + let result1 = try await value1 XCTAssertEqual(result1, 1) - let result2 = try await value2.value + let result2 = try await value2 XCTAssertEqual(result2, 2) - let loadCalls = await LoadCalls.shared.loadCalls - XCTAssertEqual(loadCalls, [[1,2]]) + let calls = await loadCalls.wrappedValue + XCTAssertEqual(calls, [[1,2]]) } } From d637ec57aa3395a8d0615556a697897fe9de9539 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 26 Jul 2022 08:21:33 +1200 Subject: [PATCH 6/8] Updated actions on macOS to use setup-xcode --- .github/workflows/test.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ee3ec0..c49e11a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,12 +6,12 @@ on: pull_request: branches: [ master ] jobs: - build: + linux-build: name: Build and test on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: - os: [macos-latest, ubuntu-latest] + os: [ubuntu-latest] steps: - uses: actions/checkout@v2 - uses: swift-actions/setup-swift@v1 @@ -19,3 +19,16 @@ jobs: run: swift build - name: Run tests run: swift test + macos-build: + name: Build and test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest] + steps: + - uses: actions/checkout@v2 + - uses: maxim-lobanov/setup-xcode@v1 + - name: Build + run: swift build + - name: Run tests + run: swift test From 35238f1232eab77af2df051a30c91abba998e772 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 26 Jul 2022 08:45:54 +1200 Subject: [PATCH 7/8] Updated workflow to use latest Xcode --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c49e11a..74704e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,8 @@ jobs: steps: - uses: actions/checkout@v2 - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable - name: Build run: swift build - name: Run tests From 6a9ddee3f08c1ed9a6d6fb9bc6ada9db94bc53a5 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 26 Jul 2022 08:50:22 +1200 Subject: [PATCH 8/8] Fixed test to compensate for non-ordered Task --- Tests/DataLoaderTests/DataLoaderAsyncTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/DataLoaderTests/DataLoaderAsyncTests.swift b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift index 1efd872..1f67f05 100644 --- a/Tests/DataLoaderTests/DataLoaderAsyncTests.swift +++ b/Tests/DataLoaderTests/DataLoaderAsyncTests.swift @@ -109,7 +109,8 @@ final class DataLoaderAsyncTests: XCTestCase { XCTAssertEqual(result2, 2) let calls = await loadCalls.wrappedValue - XCTAssertEqual(calls, [[1,2]]) + XCTAssertEqual(calls.count, 1) + XCTAssertEqual(calls.map { $0.sorted() }, [[1, 2]]) } }