@@ -16,7 +16,7 @@ final class ConnectionPoolTests: XCTestCase {
1616 configuration: config,
1717 idGenerator: ConnectionIDGenerator ( ) ,
1818 requestType: ConnectionRequest< MockConnection> . self ,
19- keepAliveBehavior: MockPingPongBehavior ( keepAliveFrequency: nil ) ,
19+ keepAliveBehavior: MockPingPongBehavior ( keepAliveFrequency: nil , connectionType : MockConnection . self ) ,
2020 observabilityDelegate: NoOpConnectionPoolMetrics ( connectionIDType: MockConnection . ID. self) ,
2121 clock: ContinuousClock ( )
2222 ) {
@@ -74,7 +74,7 @@ final class ConnectionPoolTests: XCTestCase {
7474 configuration: config,
7575 idGenerator: ConnectionIDGenerator ( ) ,
7676 requestType: ConnectionRequest< MockConnection> . self ,
77- keepAliveBehavior: MockPingPongBehavior ( keepAliveFrequency: nil ) ,
77+ keepAliveBehavior: MockPingPongBehavior ( keepAliveFrequency: nil , connectionType : MockConnection . self ) ,
7878 observabilityDelegate: NoOpConnectionPoolMetrics ( connectionIDType: MockConnection . ID. self) ,
7979 clock: clock
8080 ) {
@@ -119,7 +119,7 @@ final class ConnectionPoolTests: XCTestCase {
119119 configuration: config,
120120 idGenerator: ConnectionIDGenerator ( ) ,
121121 requestType: ConnectionRequest< MockConnection> . self ,
122- keepAliveBehavior: MockPingPongBehavior ( keepAliveFrequency: nil ) ,
122+ keepAliveBehavior: MockPingPongBehavior ( keepAliveFrequency: nil , connectionType : MockConnection . self ) ,
123123 observabilityDelegate: NoOpConnectionPoolMetrics ( connectionIDType: MockConnection . ID. self) ,
124124 clock: clock
125125 ) {
@@ -135,7 +135,7 @@ final class ConnectionPoolTests: XCTestCase {
135135 throw ConnectionCreationError ( )
136136 }
137137
138- await clock. timerScheduled ( )
138+ await clock. nextTimerScheduled ( )
139139
140140 taskGroup. cancelAll ( )
141141 }
@@ -156,7 +156,7 @@ final class ConnectionPoolTests: XCTestCase {
156156 configuration: config,
157157 idGenerator: ConnectionIDGenerator ( ) ,
158158 requestType: ConnectionRequest< MockConnection> . self ,
159- keepAliveBehavior: MockPingPongBehavior ( keepAliveFrequency: nil ) ,
159+ keepAliveBehavior: MockPingPongBehavior ( keepAliveFrequency: nil , connectionType : MockConnection . self ) ,
160160 observabilityDelegate: NoOpConnectionPoolMetrics ( connectionIDType: MockConnection . ID. self) ,
161161 clock: ContinuousClock ( )
162162 ) {
@@ -220,6 +220,154 @@ final class ConnectionPoolTests: XCTestCase {
220220 XCTAssert ( hasFinished. load ( ordering: . relaxed) )
221221 XCTAssertEqual ( factory. runningConnections. count, 0 )
222222 }
223+
224+ func testKeepAliveWorks( ) async throws {
225+ let clock = MockClock ( )
226+ let factory = MockConnectionFactory < MockClock > ( )
227+ let keepAliveDuration = Duration . seconds ( 30 )
228+ let keepAlive = MockPingPongBehavior ( keepAliveFrequency: keepAliveDuration, connectionType: MockConnection . self)
229+
230+ var mutableConfig = ConnectionPoolConfiguration ( )
231+ mutableConfig. minimumConnectionCount = 0
232+ mutableConfig. maximumConnectionSoftLimit = 1
233+ mutableConfig. maximumConnectionHardLimit = 1
234+ let config = mutableConfig
235+
236+ let pool = ConnectionPool (
237+ configuration: config,
238+ idGenerator: ConnectionIDGenerator ( ) ,
239+ requestType: ConnectionRequest< MockConnection> . self ,
240+ keepAliveBehavior: keepAlive,
241+ observabilityDelegate: NoOpConnectionPoolMetrics ( connectionIDType: MockConnection . ID. self) ,
242+ clock: clock
243+ ) {
244+ try await factory. makeConnection ( id: $0, for: $1)
245+ }
246+
247+ try await withThrowingTaskGroup ( of: Void . self) { taskGroup in
248+ taskGroup. addTask {
249+ await pool. run ( )
250+ }
251+
252+ async let lease1ConnectionAsync = pool. leaseConnection ( )
253+
254+ let connection = await factory. nextConnectAttempt { connectionID in
255+ return 1
256+ }
257+
258+ let lease1Connection = try await lease1ConnectionAsync
259+ XCTAssert ( connection === lease1Connection)
260+
261+ pool. releaseConnection ( lease1Connection)
262+
263+ // keep alive 1
264+
265+ // validate that a keep alive timer and an idle timeout timer is scheduled
266+ var expectedInstants : Set < MockClock . Instant > = [ . init( keepAliveDuration) , . init( config. idleTimeout) ]
267+ let deadline1 = await clock. nextTimerScheduled ( )
268+ print ( deadline1)
269+ XCTAssertNotNil ( expectedInstants. remove ( deadline1) )
270+ let deadline2 = await clock. nextTimerScheduled ( )
271+ print ( deadline2)
272+ XCTAssertNotNil ( expectedInstants. remove ( deadline2) )
273+ XCTAssert ( expectedInstants. isEmpty)
274+
275+ // move clock forward to keep alive
276+ let newTime = clock. now. advanced ( by: keepAliveDuration)
277+ clock. advance ( to: newTime)
278+ print ( " clock advanced to: \( newTime) " )
279+
280+ await keepAlive. nextKeepAlive { keepAliveConnection in
281+ defer { print ( " keep alive 1 has run " ) }
282+ XCTAssertTrue ( keepAliveConnection === lease1Connection)
283+ return true
284+ }
285+
286+ // keep alive 2
287+
288+ let deadline3 = await clock. nextTimerScheduled ( )
289+ XCTAssertEqual ( deadline3, clock. now. advanced ( by: keepAliveDuration) )
290+ print ( deadline3)
291+
292+ // race keep alive vs timeout
293+ clock. advance ( to: clock. now. advanced ( by: keepAliveDuration) )
294+
295+ taskGroup. cancelAll ( )
296+
297+ for connection in factory. runningConnections {
298+ connection. closeIfClosing ( )
299+ }
300+ }
301+ }
302+
303+ func testKeepAliveWorksRacesAgainstShutdown( ) async throws {
304+ let clock = MockClock ( )
305+ let factory = MockConnectionFactory < MockClock > ( )
306+ let keepAliveDuration = Duration . seconds ( 30 )
307+ let keepAlive = MockPingPongBehavior ( keepAliveFrequency: keepAliveDuration, connectionType: MockConnection . self)
308+
309+ var mutableConfig = ConnectionPoolConfiguration ( )
310+ mutableConfig. minimumConnectionCount = 0
311+ mutableConfig. maximumConnectionSoftLimit = 1
312+ mutableConfig. maximumConnectionHardLimit = 1
313+ let config = mutableConfig
314+
315+ let pool = ConnectionPool (
316+ configuration: config,
317+ idGenerator: ConnectionIDGenerator ( ) ,
318+ requestType: ConnectionRequest< MockConnection> . self ,
319+ keepAliveBehavior: keepAlive,
320+ observabilityDelegate: NoOpConnectionPoolMetrics ( connectionIDType: MockConnection . ID. self) ,
321+ clock: clock
322+ ) {
323+ try await factory. makeConnection ( id: $0, for: $1)
324+ }
325+
326+ try await withThrowingTaskGroup ( of: Void . self) { taskGroup in
327+ taskGroup. addTask {
328+ await pool. run ( )
329+ }
330+
331+ async let lease1ConnectionAsync = pool. leaseConnection ( )
332+
333+ let connection = await factory. nextConnectAttempt { connectionID in
334+ return 1
335+ }
336+
337+ let lease1Connection = try await lease1ConnectionAsync
338+ XCTAssert ( connection === lease1Connection)
339+
340+ pool. releaseConnection ( lease1Connection)
341+
342+ // keep alive 1
343+
344+ // validate that a keep alive timer and an idle timeout timer is scheduled
345+ var expectedInstants : Set < MockClock . Instant > = [ . init( keepAliveDuration) , . init( config. idleTimeout) ]
346+ let deadline1 = await clock. nextTimerScheduled ( )
347+ print ( deadline1)
348+ XCTAssertNotNil ( expectedInstants. remove ( deadline1) )
349+ let deadline2 = await clock. nextTimerScheduled ( )
350+ print ( deadline2)
351+ XCTAssertNotNil ( expectedInstants. remove ( deadline2) )
352+ XCTAssert ( expectedInstants. isEmpty)
353+
354+ clock. advance ( to: clock. now. advanced ( by: keepAliveDuration) )
355+
356+ await keepAlive. nextKeepAlive { keepAliveConnection in
357+ defer { print ( " keep alive 1 has run " ) }
358+ XCTAssertTrue ( keepAliveConnection === lease1Connection)
359+ return true
360+ }
361+
362+ taskGroup. cancelAll ( )
363+ print ( " cancelled " )
364+
365+ for connection in factory. runningConnections {
366+ connection. closeIfClosing ( )
367+ }
368+ }
369+ }
370+
223371}
224372
225373
0 commit comments