@@ -22,11 +22,15 @@ import Vapor
2222
2323actor DatabasePool {
2424 struct Database : Hashable {
25- var port : Int
25+ let index : Int
26+ let connectionDetails : ConnectionDetails
2627
27- var connectionDetails : ConnectionDetails {
28- . init( port: port)
28+ init ( index: Int ) {
29+ self . index = index
30+ self . connectionDetails = . init( index: index)
2931 }
32+
33+ var port : Int { connectionDetails. port }
3034 }
3135
3236 static let shared = DatabasePool ( maxCount: Environment . databasePoolSize)
@@ -40,24 +44,53 @@ actor DatabasePool {
4044 var availableDatabases : Set < Database > = . init( )
4145
4246 func setUp( ) async throws {
43- let start = Date ( )
44- print ( " ℹ️ \( #function) start " )
45- defer { print ( " ℹ️ \( #function) end " , Date ( ) . timeIntervalSince ( start) ) }
4647 // Call DotEnvFile.load once to ensure env variables are set
4748 await DotEnvFile . load ( for: . testing, fileio: . init( threadPool: . singleton) )
4849
49- // Re-use up to maxCount running dbs
5050 let runningDbs = try await runningDatabases ( )
51- for db in runningDbs. prefix ( maxCount) {
52- availableDatabases. insert ( db)
53- }
5451
55- print ( " ℹ️ availableDatabases " , availableDatabases. count)
56- for db in availableDatabases {
57- print ( " ℹ️ setting up db \( db. port) " )
58- try await db. setup ( for: . testing)
59- print ( " ℹ️ DONE setting up db \( db. port) " )
52+ if isRunningInCI ( ) {
53+ // In CI, running dbs are new and need to be set up
54+ try await withThrowingTaskGroup ( of: Database . self) { group in
55+ for db in runningDbs {
56+ group. addTask {
57+ try await db. setup ( for: . testing)
58+ return db
59+ }
60+ }
61+ for try await db in group {
62+ availableDatabases. insert ( db)
63+ }
64+ }
65+ } else {
66+ // Re-use up to maxCount running dbs
67+ for db in runningDbs. prefix ( maxCount) {
68+ availableDatabases. insert ( db)
69+ }
70+
71+ do { // Delete overprovisioned dbs
72+ let overprovisioned = runningDbs. dropFirst ( maxCount)
73+ try await tearDown ( databases: overprovisioned)
74+ }
75+
76+ do { // Create missing dbs
77+ let underprovisionedCount = max ( maxCount - availableDatabases. count, 0 )
78+ try await withThrowingTaskGroup ( of: Database . self) { group in
79+ for _ in ( 0 ..< underprovisionedCount) {
80+ group. addTask {
81+ let db = try await self . launchDB ( )
82+ try await db. setup ( for: . testing)
83+ return db
84+ }
85+ }
86+ for try await db in group {
87+ availableDatabases. insert ( db)
88+ }
89+ }
90+ }
6091 }
92+
93+ print ( " ℹ️ availableDatabases: " , availableDatabases. count)
6194 }
6295
6396 func tearDown( ) async throws {
@@ -81,9 +114,6 @@ actor DatabasePool {
81114 }
82115
83116 func withDatabase( _ operation: @Sendable ( Database) async throws -> Void ) async throws {
84- let start = Date ( )
85- print ( " ℹ️ \( #function) start " )
86- defer { print ( " ℹ️ \( #function) end " , Date ( ) . timeIntervalSince ( start) ) }
87117 let db = try await retainDatabase ( )
88118 do {
89119 try await operation ( db)
@@ -99,15 +129,15 @@ actor DatabasePool {
99129 // We don't have docker available in CI to probe for running dbs.
100130 // Instead, we have a hard-coded list of dbs we launch in the GH workflow
101131 // file and correspondingly, we hard-code their ports here.
102- return ( 6000 ..< 6008 ) . map ( Database . init)
132+ return ( 0 ..< 8 ) . map ( Database . init ( index : ) )
103133 } else {
104134 let stdout = try await ShellOut . shellOut ( to: . getContainerNames) . stdout
105135 return stdout
106136 . components ( separatedBy: " \n " )
107137 . filter { $0. starts ( with: " spi_test_ " ) }
108138 . map { String ( $0. dropFirst ( " spi_test_ " . count) ) }
109139 . compactMap ( Int . init)
110- . map ( Database . init ( port : ) )
140+ . map ( Database . init ( index : ) )
111141 }
112142 }
113143
@@ -144,57 +174,56 @@ actor DatabasePool {
144174 print ( " ⚠️ Launching DB on port \( port) (attempt: \( attempt) ) " )
145175 try await ShellOut . shellOut ( to: . launchDB( port: port) )
146176 }
147- return . init( port : port)
177+ return . init( index : port)
148178 }
149179
150180 private func removeDB( database: Database , maxAttempts: Int = 3 ) async throws {
151181 try await run ( maxAttempts: 3 ) { attempt in
152182 // print("⚠️ Removing DB on port \(database.port) (attempt: \(attempt))")
153- try await ShellOut . shellOut ( to: . removeDB( port: database. port ) )
183+ try await ShellOut . shellOut ( to: . removeDB( port: database. index ) )
154184 }
155185 }
156186}
157187
158188
159189extension DatabasePool . Database {
160190
161- struct ConnectionDetails {
191+ struct ConnectionDetails : Hashable {
162192 var host : String
163193 var port : Int
164194 var username : String
165195 var password : String
166196
167- init ( port : Int ) {
197+ init ( index : Int ) {
168198 // Ensure DATABASE_HOST is from a restricted set db hostnames and nothing else.
169199 // This is safeguard against accidental inheritance of setup in QueryPerformanceTests
170200 // and to ensure the database resetting cannot impact any other network hosts.
171201 if isRunningInCI ( ) {
172- self . host = " db- \( port) "
202+ self . host = " db- \( index) "
203+ self . port = 5432
173204 } else {
174205 self . host = Environment . get ( " DATABASE_HOST " ) !
175206 precondition ( [ " localhost " , " postgres " , " host.docker.internal " ] . contains ( host) ,
176207 " DATABASE_HOST must be a local db, was: \( host) " )
208+ self . port = index
177209 }
178- self . port = port
179210 self . username = Environment . get ( " DATABASE_USERNAME " ) !
180211 self . password = Environment . get ( " DATABASE_PASSWORD " ) !
181212 }
182213 }
183214
184215 func setup( for environment: Environment ) async throws {
185- let details = ConnectionDetails ( port: port)
186-
187216 // Create initial db snapshot
188- try await createSchema ( environment, details : details )
189- try await createSnapshot ( details : details )
217+ try await createSchema ( environment)
218+ try await createSnapshot ( )
190219 }
191220
192- func createSchema( _ environment: Environment , details : ConnectionDetails ) async throws {
221+ func createSchema( _ environment: Environment ) async throws {
193222 let start = Date ( )
194223 print ( " ℹ️ \( #function) start " )
195224 defer { print ( " ℹ️ \( #function) end " , Date ( ) . timeIntervalSince ( start) ) }
196225 do {
197- try await _withDatabase ( " postgres " , details: details , timeout: . seconds( 1 ) ) { // Connect to `postgres` db in order to reset the test db
226+ try await _withDatabase ( " postgres " , details: connectionDetails , timeout: . seconds( 5 ) ) { // Connect to `postgres` db in order to reset the test db
198227 let databaseName = Environment . get ( " DATABASE_NAME " ) !
199228 try await $0. query ( PostgresQuery ( unsafeSQL: " DROP DATABASE IF EXISTS \( databaseName) WITH (FORCE) " ) )
200229 try await $0. query ( PostgresQuery ( unsafeSQL: " CREATE DATABASE \( databaseName) " ) )
@@ -203,7 +232,7 @@ extension DatabasePool.Database {
203232 do { // Use autoMigrate to spin up the schema
204233 let app = try await Application . make ( environment)
205234 app. logger = . init( label: " noop " ) { _ in SwiftLogNoOpLogHandler ( ) }
206- try await configure ( app, databasePort: port)
235+ try await configure ( app, databasePort: connectionDetails . port)
207236 try await app. autoMigrate ( )
208237 try await app. asyncShutdown ( )
209238 }
@@ -213,14 +242,14 @@ extension DatabasePool.Database {
213242 }
214243 }
215244
216- func createSnapshot( details : ConnectionDetails ) async throws {
245+ func createSnapshot( ) async throws {
217246 let start = Date ( )
218247 print ( " ℹ️ \( #function) start " )
219248 defer { print ( " ℹ️ \( #function) end " , Date ( ) . timeIntervalSince ( start) ) }
220249 let original = Environment . get ( " DATABASE_NAME " ) !
221250 let snapshot = original + " _snapshot "
222251 do {
223- try await _withDatabase ( " postgres " , details: details , timeout: . seconds( 1 ) ) { client in
252+ try await _withDatabase ( " postgres " , details: connectionDetails , timeout: . seconds( 5 ) ) { client in
224253 try await client. query ( PostgresQuery ( unsafeSQL: " DROP DATABASE IF EXISTS \( snapshot) WITH (FORCE) " ) )
225254 try await client. query ( PostgresQuery ( unsafeSQL: " CREATE DATABASE \( snapshot) TEMPLATE \( original) " ) )
226255 }
0 commit comments