From 3b190c3d76adf0c732b1eabcd0b3d4c3880bd61e Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Mon, 7 Jul 2025 12:09:33 +0200 Subject: [PATCH 01/10] add a test for cancellable --- .../LambdaRuntimeTests.swift | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index cd519d76..430fe70a 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -75,6 +75,53 @@ struct LambdaRuntimeTests { taskGroup.cancelAll() } } + @Test("run() must be cancellable") + func testLambdaRuntimeCancellable() async throws { + + let logger = Logger(label: "LambdaRuntimeTests.RuntimeCancellable") + // create a runtime + let runtime = LambdaRuntime( + handler: MockHandler(), + eventLoop: Lambda.defaultEventLoop, + logger: logger + ) + + // Running the runtime with structured concurrency + // Task group returns when all tasks are completed. + // Even cancelled tasks must cooperatlivly complete + await #expect(throws: Never.self) { + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + taskGroup.addTask { + logger.trace("--- launching runtime ----") + try await runtime.run() + } + + // Add a timeout task to the group + taskGroup.addTask { + logger.trace("--- launching timeout task ----") + try await Task.sleep(for: .seconds(5)) + if Task.isCancelled { return } + logger.trace("--- throwing timeout error ----") + throw TestError.timeout // Fail the test if the timeout triggers + } + + do { + // Wait for the runtime to start + logger.trace("--- waiting for runtime to start ----") + try await Task.sleep(for: .seconds(1)) + + // Cancel all tasks, this should not throw an error + // and should allow the runtime to complete gracefully + logger.trace("--- cancel all tasks ----") + taskGroup.cancelAll() // Cancel all tasks + } catch { + logger.error("--- catch an error: \(error)") + throw error // Propagate the error to fail the test + } + } + } + + } } struct MockHandler: StreamingLambdaHandler { @@ -86,3 +133,15 @@ struct MockHandler: StreamingLambdaHandler { } } + +// Define a custom error for timeout +enum TestError: Error, CustomStringConvertible { + case timeout + + var description: String { + switch self { + case .timeout: + return "Test timed out waiting for the task to complete." + } + } +} From abdda820ce786f37f0f782da041f9b391eecb4b0 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Mon, 7 Jul 2025 12:09:47 +0200 Subject: [PATCH 02/10] move service lifecycle test in a separate file --- .../LambdaRuntime+ServiceLifeCycle.swift | 32 +++++++++++++++++++ .../LambdaRuntimeClientTests.swift | 25 --------------- 2 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift new file mode 100644 index 00000000..90e28e83 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift @@ -0,0 +1,32 @@ +#if ServiceLifecycleSupport +@testable import AWSLambdaRuntime +import ServiceLifecycle +import Testing +import Logging + +@Suite +struct LambdaRuntimeServiceLifecycleTests { + @Test + func testLambdaRuntimeGracefulShutdown() async throws { + let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + "Hello \(event)" + } + + let serviceGroup = ServiceGroup( + services: [runtime], + gracefulShutdownSignals: [.sigterm, .sigint], + logger: Logger(label: "TestLambdaRuntimeGracefulShutdown") + ) + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await serviceGroup.run() + } + // wait a small amount to ensure we are waiting for continuation + try await Task.sleep(for: .milliseconds(100)) + + await serviceGroup.triggerGracefulShutdown() + } + } +} +#endif diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift index cc901461..33ebde3f 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift @@ -15,7 +15,6 @@ import Logging import NIOCore import NIOPosix -import ServiceLifecycle import Testing import struct Foundation.UUID @@ -140,28 +139,4 @@ struct LambdaRuntimeClientTests { } } } - #if ServiceLifecycleSupport - @Test - func testLambdaRuntimeGracefulShutdown() async throws { - let runtime = LambdaRuntime { - (event: String, context: LambdaContext) in - "Hello \(event)" - } - - let serviceGroup = ServiceGroup( - services: [runtime], - gracefulShutdownSignals: [.sigterm, .sigint], - logger: Logger(label: "TestLambdaRuntimeGracefulShutdown") - ) - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await serviceGroup.run() - } - // wait a small amount to ensure we are waiting for continuation - try await Task.sleep(for: .milliseconds(100)) - - await serviceGroup.triggerGracefulShutdown() - } - } - #endif } From 4d3479a9f61fb13d500d7744f1c17a1d3a5a2ca6 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Mon, 7 Jul 2025 12:13:12 +0200 Subject: [PATCH 03/10] license header --- .../LambdaRuntime+ServiceLifeCycle.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift index 90e28e83..7103ea8d 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntime+ServiceLifeCycle.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + #if ServiceLifecycleSupport @testable import AWSLambdaRuntime import ServiceLifecycle From dcbda48766456b4328a81a370fa0a3ce309d23e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 12 Jul 2025 13:51:26 +0200 Subject: [PATCH 04/10] add more details error message + warning --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index 147f56d6..5f66df6f 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -82,7 +82,7 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb // The handler can be non-sendable, we want to ensure we only ever have one copy of it let handler = try? self.handlerStorage.get() guard let handler else { - throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) + throw LambdaRuntimeError(code: .handlerCanOnlyBeGetOnce) } // are we running inside an AWS Lambda runtime environment ? From 07a05dfd0db9025cefab9b3f532c18b983026db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 12 Jul 2025 13:53:42 +0200 Subject: [PATCH 05/10] add error code --- Sources/AWSLambdaRuntime/LambdaRuntimeError.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift index 499ed600..a9c0cbca 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift @@ -34,6 +34,7 @@ package struct LambdaRuntimeError: Error { case missingLambdaRuntimeAPIEnvironmentVariable case runtimeCanOnlyBeStartedOnce + case handlerCanOnlyBeGetOnce case invalidPort } From bbe834f3a5d4fa06ab48ddb68233a953059343e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 12 Jul 2025 13:55:40 +0200 Subject: [PATCH 06/10] add delay on test for cleaning up --- Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index 8c893eb8..ada533e7 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -61,6 +61,9 @@ struct LambdaRuntimeTests { taskGroup.cancelAll() } + // wait a small amount to ensure everything is cancelled and cleanup + try await Task.sleep(for: .seconds(0.5)) + // Running the second runtime should work now try await withThrowingTaskGroup(of: Void.self) { taskGroup in taskGroup.addTask { From 70dc8c13ecb23c1b1e9f0fcc748b16c2e6cc04d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 12 Jul 2025 14:01:53 +0200 Subject: [PATCH 07/10] increase test delay --- Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index ada533e7..efe5173c 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -50,7 +50,7 @@ struct LambdaRuntimeTests { } // wait a small amount to ensure runtime1 task is started - try await Task.sleep(for: .seconds(0.5)) + try await Task.sleep(for: .seconds(1)) // Running the second runtime should trigger LambdaRuntimeError await #expect(throws: LambdaRuntimeError.self) { @@ -62,7 +62,7 @@ struct LambdaRuntimeTests { } // wait a small amount to ensure everything is cancelled and cleanup - try await Task.sleep(for: .seconds(0.5)) + try await Task.sleep(for: .seconds(1)) // Running the second runtime should work now try await withThrowingTaskGroup(of: Void.self) { taskGroup in From b1527ffbad73c2905ea52725158133ff6aa66660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 12 Jul 2025 14:06:38 +0200 Subject: [PATCH 08/10] increase test delay --- Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index efe5173c..b3bdee80 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -50,7 +50,8 @@ struct LambdaRuntimeTests { } // wait a small amount to ensure runtime1 task is started - try await Task.sleep(for: .seconds(1)) + // on GH Actions, it might take a bit longer to start the runtime + try await Task.sleep(for: .seconds(2)) // Running the second runtime should trigger LambdaRuntimeError await #expect(throws: LambdaRuntimeError.self) { From de5e31cb2752dbad9271294054e7da1504b417a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sun, 13 Jul 2025 08:54:33 +0200 Subject: [PATCH 09/10] change some test names for clarity --- Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift | 2 +- Tests/AWSLambdaRuntimeTests/PoolTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift index 33ebde3f..ba24b2cc 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift @@ -89,7 +89,7 @@ struct LambdaRuntimeClientTests { } @Test - func testCancellation() async throws { + func testRuntimeClientCancellation() async throws { struct HappyBehavior: LambdaServerBehavior { let requestId = UUID().uuidString let event = "hello" diff --git a/Tests/AWSLambdaRuntimeTests/PoolTests.swift b/Tests/AWSLambdaRuntimeTests/PoolTests.swift index cfeea6a2..15d54a73 100644 --- a/Tests/AWSLambdaRuntimeTests/PoolTests.swift +++ b/Tests/AWSLambdaRuntimeTests/PoolTests.swift @@ -37,7 +37,7 @@ struct PoolTests { } @Test - func testCancellation() async throws { + func testPoolCancellation() async throws { let pool = LambdaHTTPServer.Pool() // Create a task that will be cancelled From 14e80d990af964bf0b7c90b54e8e84f0321350f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sun, 13 Jul 2025 08:54:45 +0200 Subject: [PATCH 10/10] put second runtime in a task group --- Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index b3bdee80..3f37bf09 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -54,8 +54,11 @@ struct LambdaRuntimeTests { try await Task.sleep(for: .seconds(2)) // Running the second runtime should trigger LambdaRuntimeError - await #expect(throws: LambdaRuntimeError.self) { - try await runtime2.run() + // start the first runtime + taskGroup.addTask { + await #expect(throws: LambdaRuntimeError.self) { + try await runtime2.run() + } } // cancel runtime 1 / task 1