diff --git a/Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift b/Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift index 819da6d..2abc701 100644 --- a/Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift +++ b/Sources/BreezeDynamoDBService/BreezeDynamoDBService.swift @@ -20,15 +20,16 @@ import Logging /// Defines the interface for a Breeze DynamoDB service. /// /// Provides methods to access the database manager and to gracefully shutdown the service. -public protocol BreezeDynamoDBServing: Actor { - func dbManager() async -> BreezeDynamoDBManaging - func gracefulShutdown() throws +public protocol BreezeDynamoDBServing: Service { + var dbManager: BreezeDynamoDBManaging { get } + func onGracefulShutdown() async throws + func syncShutdown() throws } /// Provides methods to access the DynamoDB database manager and to gracefully shutdown the service. -public actor BreezeDynamoDBService: BreezeDynamoDBServing { +public struct BreezeDynamoDBService: BreezeDynamoDBServing { - private let dbManager: BreezeDynamoDBManaging + public let dbManager: BreezeDynamoDBManaging private let logger: Logger private let awsClient: AWSClient private let httpClient: HTTPClient @@ -46,7 +47,7 @@ public actor BreezeDynamoDBService: BreezeDynamoDBServing { httpConfig: BreezeHTTPClientConfig, logger: Logger, DBManagingType: BreezeDynamoDBManaging.Type = BreezeDynamoDBManager.self - ) async { + ) { logger.info("Init DynamoDBService with config...") logger.info("region: \(config.region)") logger.info("tableName: \(config.tableName)") @@ -76,10 +77,9 @@ public actor BreezeDynamoDBService: BreezeDynamoDBServing { logger.info("DBManager is ready.") } - /// Returns the BreezeDynamoDBManaging instance. - public func dbManager() async -> BreezeDynamoDBManaging { - logger.info("Starting DynamoDBService...") - return self.dbManager + public func run() async throws { + try await gracefulShutdown() + try await onGracefulShutdown() } /// Gracefully shutdown the service and its components. @@ -89,21 +89,19 @@ public actor BreezeDynamoDBService: BreezeDynamoDBServing { /// It also logs the shutdown process. /// This method is idempotent; /// - Important: This method must be called at leat once to ensure that resources are released properly. If the method is not called, it will lead to a crash. - public func gracefulShutdown() throws { - guard !isShutdown else { return } - isShutdown = true + public func onGracefulShutdown() async throws { logger.info("Stopping DynamoDBService...") - try awsClient.syncShutdown() + try await awsClient.shutdown() logger.info("DynamoDBService is stopped.") logger.info("Stopping HTTPClient...") - try httpClient.syncShutdown() + try await httpClient.shutdown() logger.info("HTTPClient is stopped.") } - deinit { - guard !isShutdown else { return } - try? awsClient.syncShutdown() - try? httpClient.syncShutdown() + /// Sync shutdown + public func syncShutdown() throws { + try awsClient.syncShutdown() + try httpClient.syncShutdown() } } diff --git a/Sources/BreezeLambdaAPI/BreezeLambdaAPI.swift b/Sources/BreezeLambdaAPI/BreezeLambdaAPI.swift index a13c55f..78317a2 100644 --- a/Sources/BreezeLambdaAPI/BreezeLambdaAPI.swift +++ b/Sources/BreezeLambdaAPI/BreezeLambdaAPI.swift @@ -43,6 +43,7 @@ public actor BreezeLambdaAPI: Service { let timeout: TimeAmount private let serviceGroup: ServiceGroup private let apiConfig: any APIConfiguring + private let dynamoDBService: BreezeDynamoDBService /// Initializes the BreezeLambdaAPI with the provided API configuration. /// - Parameter apiConfig: An object conforming to `APIConfiguring` that provides the necessary configuration for the Breeze API. @@ -63,19 +64,18 @@ public actor BreezeLambdaAPI: Service { logger: logger ) let operation = try apiConfig.operation() - let dynamoDBService = await BreezeDynamoDBService( + self.dynamoDBService = BreezeDynamoDBService( config: config, httpConfig: httpConfig, logger: logger ) - let breezeLambdaService = BreezeLambdaService( - dynamoDBService: dynamoDBService, - operation: operation, - logger: logger - ) + let dbManager = dynamoDBService.dbManager + let breezeApi = BreezeLambdaHandler(dbManager: dbManager, operation: operation) + let runtime = LambdaRuntime(body: breezeApi.handle) self.serviceGroup = ServiceGroup( - services: [breezeLambdaService], - gracefulShutdownSignals: [.sigterm, .sigint], + services: [runtime, dynamoDBService], + gracefulShutdownSignals: [.sigint], + cancellationSignals: [.sigterm], logger: logger ) } catch { @@ -90,7 +90,12 @@ public actor BreezeLambdaAPI: Service { /// The internal ServiceGroup will handle the lifecycle of the BreezeLambdaAPI, including starting and stopping the service gracefully. public func run() async throws { logger.info("Starting BreezeLambdaAPI...") - try await serviceGroup.run() + do { + try await serviceGroup.run() + } catch { + try dynamoDBService.syncShutdown() + throw error + } logger.info("BreezeLambdaAPI is stopped successfully") } } diff --git a/Sources/BreezeLambdaAPI/BreezeLambdaService.swift b/Sources/BreezeLambdaAPI/BreezeLambdaService.swift deleted file mode 100644 index 94b5c5d..0000000 --- a/Sources/BreezeLambdaAPI/BreezeLambdaService.swift +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2024 (c) Andrea Scuderi - https://github.com/swift-serverless -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import ServiceLifecycle -import AsyncHTTPClient -import NIOCore -import BreezeDynamoDBService -import AWSLambdaRuntime -import AWSLambdaEvents -import Logging -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -/// Service for processing AWS API Gateway events with BreezeCodable models. -/// -/// `BreezeLambdaService` is a key component in the serverless architecture that: -/// - Acts as a bridge between AWS Lambda runtime and DynamoDB operations -/// - Processes incoming API Gateway events through a type-safe interface -/// - Manages the lifecycle of AWS Lambda handlers for BreezeCodable models -/// - Coordinates graceful shutdown procedures to ensure clean resource release -/// -/// The service leverages Swift concurrency features through the actor model to ensure -/// thread-safe access to shared resources while processing multiple Lambda invocations. -/// It delegates the actual processing of events to a specialized `BreezeLambdaHandler` -/// which performs the database operations via the injected `BreezeDynamoDBService`. -/// -/// This service is designed to be initialized and run as part of a `ServiceGroup` -/// within the AWS Lambda execution environment. -actor BreezeLambdaService: Service { - - /// Database service that provides access to the underlying DynamoDB operations. - /// - /// This service is responsible for all database interactions and connection management. - let dynamoDBService: BreezeDynamoDBServing - - /// Operation type that determines the behavior of this service instance. - /// - /// Defines whether this Lambda will perform create, read, update, delete, or list operation - let operation: BreezeOperation - - /// Logger instance for tracking service lifecycle events and errors. - /// - /// Used throughout the service to provide consistent logging patterns. - let logger: Logger - - /// Initializes a new instance of `BreezeLambdaService`. - /// - Parameters: - /// - dynamoDBService: Service providing DynamoDB operations and connection management - /// - operation: The specific CRUD operation this Lambda instance will perform - /// - logger: Logger instance for service monitoring and debugging - init(dynamoDBService: BreezeDynamoDBServing, operation: BreezeOperation, logger: Logger) { - self.dynamoDBService = dynamoDBService - self.operation = operation - self.logger = logger - } - - /// Handler instance that processes business logic for the configured operation. - /// - /// Lazily initialized during the `run()` method to ensure proper service startup sequence. - var breezeApi: BreezeLambdaHandler? - - /// Handler method that processes incoming AWS Lambda events. - func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { - guard let breezeApi else { throw BreezeLambdaAPIError.invalidHandler } - return try await breezeApi.handle(event, context: context) - } - - /// Runs the service allowing graceful shutdown. - /// - /// - Throws: An error if the service fails to initialize or run. - func run() async throws { - let dbManager = await dynamoDBService.dbManager() - let breezeApi = BreezeLambdaHandler(dbManager: dbManager, operation: self.operation) - self.breezeApi = breezeApi - logger.info("Starting BreezeLambdaService...") - let runtime = LambdaRuntime(body: handler) - try await runTaskWithCancellationOnGracefulShutdown { - do { - try await runtime.run() - } catch { - self.logger.error("\(error.localizedDescription)") - throw error - } - } onGracefulShutdown: { - self.logger.info("Gracefully stoping BreezeLambdaService ...") - try await self.dynamoDBService.gracefulShutdown() - self.logger.info("BreezeLambdaService is stopped.") - } - } - - /// Runs a task with cancellation on graceful shutdown. - /// - /// - Note: It's required to allow a full process shutdown without leaving tasks hanging. - private func runTaskWithCancellationOnGracefulShutdown( - operation: @escaping @Sendable () async throws -> Void, - onGracefulShutdown: () async throws -> Void - ) async throws { - let (cancelOrGracefulShutdown, cancelOrGracefulShutdownContinuation) = AsyncStream.makeStream() - let task = Task { - try await withTaskCancellationOrGracefulShutdownHandler { - try await operation() - } onCancelOrGracefulShutdown: { - cancelOrGracefulShutdownContinuation.yield() - cancelOrGracefulShutdownContinuation.finish() - } - } - for await _ in cancelOrGracefulShutdown { - try await onGracefulShutdown() - task.cancel() - } - } -} diff --git a/Tests/BreezeDynamoDBServiceTests/BreezeDynamoDBServiceTests.swift b/Tests/BreezeDynamoDBServiceTests/BreezeDynamoDBServiceTests.swift index 1c0509c..1cec714 100644 --- a/Tests/BreezeDynamoDBServiceTests/BreezeDynamoDBServiceTests.swift +++ b/Tests/BreezeDynamoDBServiceTests/BreezeDynamoDBServiceTests.swift @@ -23,16 +23,19 @@ struct BreezeDynamoDBServiceTests { @Test func testInitPrepareBreezeDynamoDBManager() async throws { let sut = await makeBreezeDynamoDBConfig() - let manager = await sut.dbManager() + let manager = +sut.dbManager #expect(manager is BreezeDynamoDBManager, "Expected BreezeDynamoDBManager instance") - try await sut.gracefulShutdown() + try await sut.onGracefulShutdown() } @Test func testGracefulShutdownCanBeCalledMultipleTimes() async throws { let sut = await makeBreezeDynamoDBConfig() - try await sut.gracefulShutdown() - try await sut.gracefulShutdown() + try await sut.onGracefulShutdown() + await #expect(throws: Error.self) { + try await sut.onGracefulShutdown() + } } @Test @@ -44,15 +47,15 @@ struct BreezeDynamoDBServiceTests { ) let logger = Logger(label: "BreezeDynamoDBServiceTests") let httpConfig = BreezeHTTPClientConfig(timeout: .seconds(10), logger: logger) - let sut = await BreezeDynamoDBService( + let sut = BreezeDynamoDBService( config: config, httpConfig: httpConfig, logger: logger, DBManagingType: BreezeDynamoDBManagerMock.self ) - let manager = await sut.dbManager() + let manager = sut.dbManager #expect(manager is BreezeDynamoDBManagerMock, "Expected BreezeDynamoDBManager instance") - try await sut.gracefulShutdown() + try await sut.onGracefulShutdown() } private func makeBreezeDynamoDBConfig() async -> BreezeDynamoDBService { @@ -63,7 +66,7 @@ struct BreezeDynamoDBServiceTests { ) let logger = Logger(label: "BreezeDynamoDBServiceTests") let httpConfig = BreezeHTTPClientConfig(timeout: .seconds(10), logger: logger) - return await BreezeDynamoDBService( + return BreezeDynamoDBService( config: config, httpConfig: httpConfig, logger: logger, diff --git a/Tests/BreezeLambdaAPITests/BreezeLambdaAPIServiceTests.swift b/Tests/BreezeLambdaAPITests/BreezeLambdaAPITests.swift similarity index 88% rename from Tests/BreezeLambdaAPITests/BreezeLambdaAPIServiceTests.swift rename to Tests/BreezeLambdaAPITests/BreezeLambdaAPITests.swift index bd55e44..cf3fffa 100644 --- a/Tests/BreezeLambdaAPITests/BreezeLambdaAPIServiceTests.swift +++ b/Tests/BreezeLambdaAPITests/BreezeLambdaAPITests.swift @@ -31,12 +31,12 @@ struct APIConfiguration: APIConfiguring { } @Suite -struct BreezeLambdaAPIServiceTests { +struct BreezeLambdaAPITests { let logger = Logger(label: "BreezeHTTPClientServiceTests") @Test - func test_breezeLambdaAPIService_whenValidEnvironment() async throws { + func test_breezeLambdaAPI_whenValidEnvironment() async throws { try await testGracefulShutdown { gracefulShutdownTestTrigger in let (gracefulStream, continuation) = AsyncStream.makeStream() @@ -49,8 +49,12 @@ struct BreezeLambdaAPIServiceTests { } group.addTask { try await withGracefulShutdownHandler { - try await sut.run() - print("BreezeLambdaAPIService started successfully") + do { + try await sut.run() + print("BreezeLambdaAPIService started successfully") + } catch { + + } } onGracefulShutdown: { logger.info("On Graceful Shutdown") continuation.yield() @@ -66,7 +70,7 @@ struct BreezeLambdaAPIServiceTests { } @Test - func test_breezeLambdaAPIService_whenInvalidEnvironment() async throws { + func test_breezeLambdaAPI_whenInvalidEnvironment() async throws { await #expect(throws: BreezeLambdaAPIError.self) { try await testGracefulShutdown { gracefulShutdownTestTrigger in try await withThrowingTaskGroup(of: Void.self) { group in