diff --git a/Package.resolved b/Package.resolved index e76e062..e999eda 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e50c3719c50b624936c95eb0ed73d05ef04316baae0ce0a39d0040afbc061d2b", + "originHash" : "4fcf6655c10aa8b2d812570ea3b119de335eea4c9b6d66e846abf32a9765b181", "pins" : [ { "identity" : "feather-database", @@ -19,15 +19,6 @@ "version" : "1.12.3" } }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms.git", - "state" : { - "revision" : "6c050d5ef8e1aa6342528460db614e9770d7f804", - "version" : "1.1.1" - } - }, { "identity" : "swift-atomics", "kind" : "remoteSourceControl", @@ -64,15 +55,6 @@ "version" : "2.94.0" } }, - { - "identity" : "swift-service-lifecycle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-server/swift-service-lifecycle", - "state" : { - "revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348", - "version" : "2.9.1" - } - }, { "identity" : "swift-system", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 4a683fb..a623bab 100644 --- a/Package.swift +++ b/Package.swift @@ -82,6 +82,13 @@ let package = Package( name: "FeatherSQLiteDatabaseTests", dependencies: [ .target(name: "FeatherSQLiteDatabase"), + .product( + name: "ServiceLifecycleTestKit", + package: "swift-service-lifecycle", + condition: .when( + traits: ["ServiceLifecycleSupport"] + ) + ), ], swiftSettings: defaultSwiftSettings ), diff --git a/README.md b/README.md index 85a7372..6abb512 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ SQLite driver implementation for the abstract [Feather Database](https://github.com/feather-framework/feather-database) Swift API package. -[![Release: 1.0.0-beta.6](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E6-F05138)](https://github.com/feather-framework/feather-sqlite-database/releases/tag/1.0.0-beta.6) +[ + ![Release: 1.0.0-beta.7](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E7-F05138) +]( + https://github.com/feather-framework/feather-sqlite-database/releases/tag/1.0.0-beta.7 +) ## Features @@ -66,7 +70,11 @@ Available traits: API documentation is available at the link below: -[![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138)](https://feather-framework.github.io/feather-sqlite-database/) +[ + ![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138) +]( + https://feather-framework.github.io/feather-sqlite-database/ +) Here is a brief example: diff --git a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift index 8e21b83..d79ec54 100644 --- a/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift +++ b/Sources/FeatherSQLiteDatabase/SQLiteDatabaseService.swift @@ -32,9 +32,16 @@ public struct SQLiteDatabaseService: Service { /// /// - Throws: Rethrows any error produced while starting the SQLite client. public func run() async throws { - try await client.run() - try? await gracefulShutdown() - await client.shutdown() + do { + try await client.run() + try? await gracefulShutdown() + await client.shutdown() + } + catch { + await client.shutdown() + throw error + } + } } diff --git a/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift b/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift index ab8180f..f11ac60 100644 --- a/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift +++ b/Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift @@ -202,17 +202,7 @@ actor SQLiteConnectionPool { ) } catch { - do { - try await connection.close() - } - catch { - configuration.logger.warning( - "Failed to close SQLite connection after setup error", - metadata: [ - "error": "\(error)" - ] - ) - } + await closeConnection(connection) throw error } return connection @@ -221,10 +211,13 @@ actor SQLiteConnectionPool { private func closeConnection( _ connection: SQLiteConnection ) async { - do { - try await connection.close() - } - catch { + let result = + await Task.detached { + try await connection.close() + } + .result + + if case .failure(let error) = result { configuration.logger.warning( "Failed to close SQLite connection", metadata: [ diff --git a/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift b/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift index 2acdbce..55c1cb3 100644 --- a/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift +++ b/Tests/FeatherSQLiteDatabaseTests/FeatherSQLiteDatabaseTestSuite.swift @@ -8,10 +8,14 @@ import FeatherDatabase import Logging import SQLiteNIO -import SQLiteNIOExtras import Testing @testable import FeatherSQLiteDatabase +@testable import SQLiteNIOExtras + +#if ServiceLifecycleSupport +import ServiceLifecycleTestKit +#endif @Suite struct FeatherSQLiteDatabaseTestSuite { @@ -1180,5 +1184,179 @@ extension FeatherSQLiteDatabaseTestSuite { await serviceGroup.triggerGracefulShutdown() } } + + @Test + func serviceLifecycleCancellationShutsDownClient() async throws { + var logger = Logger(label: "test") + logger.logLevel = .info + + let configuration = SQLiteClient.Configuration( + storage: .memory, + logger: logger + ) + let client = SQLiteClient(configuration: configuration) + let service = SQLiteDatabaseService(client) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await service.run() + } + + try await Task.sleep(for: .milliseconds(100)) + group.cancelAll() + + do { + while let _ = try await group.next() {} + } + catch { + // Cancellation is expected; the shutdown is asserted below. + } + } + + do { + try await client.withConnection { _ in } + Issue.record("Expected shutdown to reject new connections.") + } + catch { + #expect(error is SQLiteConnectionPoolError) + } + } + + @Test + func serviceLifecycleGracefulShutdownShutsDownClient() async throws { + var logger = Logger(label: "test") + logger.logLevel = .info + + let configuration = SQLiteClient.Configuration( + storage: .memory, + logger: logger + ) + let client = SQLiteClient(configuration: configuration) + let service = SQLiteDatabaseService(client) + let serviceGroup = ServiceGroup( + services: [service], + logger: logger + ) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await serviceGroup.run() + } + + try await Task.sleep(for: .milliseconds(100)) + await serviceGroup.triggerGracefulShutdown() + + do { + while let _ = try await group.next() {} + } + catch { + Issue.record(error) + } + } + + do { + try await client.withConnection { _ in } + Issue.record("Expected shutdown to reject new connections.") + } + catch { + #expect(error is SQLiteConnectionPoolError) + } + } + + @Test + func cancellationErrorTrigger() async throws { + var logger = Logger(label: "test") + logger.logLevel = .info + + let configuration = SQLiteClient.Configuration( + storage: .memory, + logger: logger + ) + let client = SQLiteClient(configuration: configuration) + let database = SQLiteDatabaseClient( + client: client, + logger: logger + ) + + enum MigrationError: Error { + case generic + } + + struct MigrationService: Service { + let database: any DatabaseClient + + func run() async throws { + let result = try await database.withConnection { connection in + try await connection.run( + query: #""" + SELECT sqlite_version() AS "version" + """# + ) { try await $0.collect().first } + } + let version = try result? + .decode( + column: "version", + as: String.self + ) + #expect(version?.split(separator: ".").count == 3) + + throw MigrationError.generic + } + } + + let serviceGroup = ServiceGroup( + configuration: .init( + services: [ + .init( + service: SQLiteDatabaseService(client) + ), + .init( + service: MigrationService(database: database), + successTerminationBehavior: .gracefullyShutdownGroup, + failureTerminationBehavior: .cancelGroup + ), + ], + logger: logger + ) + ) + + do { + try await serviceGroup.run() + Issue.record("Service group should fail.") + } + catch let error as MigrationError { + #expect(error == .generic) + } + catch { + Issue.record("Service group should throw a generic Migration error") + } + } + + @Test + func serviceGracefulShutdown() async throws { + var logger = Logger(label: "test") + logger.logLevel = .info + + let configuration = SQLiteClient.Configuration( + storage: .memory, + logger: logger + ) + let client = SQLiteClient(configuration: configuration) + let service = SQLiteDatabaseService(client) + + try await testGracefulShutdown { trigger in + try await withThrowingTaskGroup { group in + let serviceGroup = ServiceGroup( + services: [service], + logger: logger + ) + group.addTask { try await serviceGroup.run() } + + trigger.triggerGracefulShutdown() + + try await group.waitForAll() + } + } + } } #endif