11import CLibMongoC
2+ import Foundation
3+ import NIOConcurrencyHelpers
24
35/// A connection to the database.
46internal class Connection {
@@ -13,27 +15,60 @@ internal class Connection {
1315 }
1416
1517 deinit {
16- switch self . pool. state {
17- case let . open( pool) :
18- mongoc_client_pool_push ( pool, self . clientHandle)
19- case . closed:
20- assertionFailure ( " ConnectionPool was already closed " )
18+ do {
19+ try self . pool. checkIn ( self )
20+ } catch {
21+ assertionFailure ( " Failed to check connection back in: \( error) " )
2122 }
2223 }
2324}
2425
26+ extension NSCondition {
27+ fileprivate func withLock< T> ( _ body: ( ) throws -> T ) rethrows -> T {
28+ self . lock ( )
29+ defer { self . unlock ( ) }
30+ return try body ( )
31+ }
32+ }
33+
2534/// A pool of one or more connections.
2635internal class ConnectionPool {
2736 /// Represents the state of a `ConnectionPool`.
2837 internal enum State {
2938 /// Indicates that the `ConnectionPool` is open and using the associated pointer to a `mongoc_client_pool_t`.
3039 case open( pool: OpaquePointer )
40+ /// Indicates that the `ConnectionPool` is in the process of closing. Connections can be checked back in, but
41+ /// no new connections can be checked out.
42+ case closing( pool: OpaquePointer )
3143 /// Indicates that the `ConnectionPool` has been closed and contains no connections.
3244 case closed
3345 }
3446
3547 /// The state of this `ConnectionPool`.
36- internal private( set) var state : State
48+ private var state : State
49+ /// The number of connections currently checked out of the pool.
50+ private var connCount = 0
51+ /// Lock over `state` and `connCount`.
52+ private let stateLock = NSCondition ( )
53+
54+ /// Internal helper for testing purposes that returns whether the pool is in the `closing` state.
55+ internal var isClosing : Bool {
56+ self . stateLock. withLock {
57+ guard case . closing = self . state else {
58+ return false
59+ }
60+ return true
61+ }
62+ }
63+
64+ /// Internal helper for testing purposes that returns the number of connections currently checked out from the pool.
65+ internal var checkedOutConnections : Int {
66+ self . stateLock. withLock {
67+ self . connCount
68+ }
69+ }
70+
71+ internal static let PoolClosedError = LogicError ( message: " ConnectionPool was already closed " )
3772
3873 /// Initializes the pool using the provided `ConnectionString` and options.
3974 internal init ( from connString: ConnectionString , options: ClientOptions ? ) throws {
@@ -56,7 +91,7 @@ internal class ConnectionPool {
5691
5792 self . state = . open( pool: pool)
5893 if let options = options {
59- try self . setTLSOptions ( options)
94+ self . setTLSOptions ( options)
6095 }
6196 }
6297
@@ -67,38 +102,79 @@ internal class ConnectionPool {
67102 }
68103 }
69104
70- /// Closes the pool, cleaning up underlying resources. This method blocks as it sends `endSessions` to the server.
71- internal func shutdown( ) {
72- switch self . state {
73- case let . open( pool) :
74- mongoc_client_pool_destroy ( pool)
75- case . closed:
76- return
105+ /// Closes the pool, cleaning up underlying resources. **This method blocks until all connections are returned to
106+ /// the pool.**
107+ internal func close( ) throws {
108+ try self . stateLock. withLock {
109+ switch self . state {
110+ case let . open( pool) :
111+ self . state = . closing( pool: pool)
112+ case . closing, . closed:
113+ throw Self . PoolClosedError
114+ }
115+
116+ while self . connCount > 0 {
117+ // wait for signal from checkIn().
118+ self . stateLock. wait ( )
119+ }
120+
121+ switch self . state {
122+ case . open, . closed:
123+ throw InternalError ( message: " ConnectionPool in unexpected state \( self . state) during close() " )
124+ case let . closing( pool) :
125+ mongoc_client_pool_destroy ( pool)
126+ self . state = . closed
127+ }
77128 }
78- self . state = . closed
79129 }
80130
81131 /// Checks out a connection. This connection will return itself to the pool when its reference count drops to 0.
82- /// This method will block until a connection is available.
132+ /// This method will block until a connection is available. Throws an error if the pool is in the process of
133+ /// closing or has finished closing.
83134 internal func checkOut( ) throws -> Connection {
84- switch self . state {
85- case let . open( pool) :
86- return Connection ( clientHandle: mongoc_client_pool_pop ( pool) , pool: self )
87- case . closed:
88- throw InternalError ( message: " ConnectionPool was already closed " )
135+ try self . stateLock. withLock {
136+ switch self . state {
137+ case let . open( pool) :
138+ self . connCount += 1
139+ return Connection ( clientHandle: mongoc_client_pool_pop ( pool) , pool: self )
140+ case . closing, . closed:
141+ throw Self . PoolClosedError
142+ }
89143 }
90144 }
91145
92- /// Checks out a connection from the pool, or returns `nil` if none are currently available.
146+ /// Checks out a connection from the pool, or returns `nil` if none are currently available. Throws an error if the
147+ /// pool is not open. This method may block waiting on the state lock as well as libmongoc's locks and thus must be
148+ // run within the thread pool.
93149 internal func tryCheckOut( ) throws -> Connection ? {
94- switch self . state {
95- case let . open( pool) :
96- guard let handle = mongoc_client_pool_try_pop ( pool) else {
97- return nil
150+ try self . stateLock. withLock {
151+ switch self . state {
152+ case let . open( pool) :
153+ guard let handle = mongoc_client_pool_try_pop ( pool) else {
154+ return nil
155+ }
156+ self . connCount += 1
157+ return Connection ( clientHandle: handle, pool: self )
158+ case . closing, . closed:
159+ throw Self . PoolClosedError
160+ }
161+ }
162+ }
163+
164+ /// Checks a connection into the pool. Accepts the connection if the pool is still open or in the process of
165+ /// closing; throws an error if the pool has already finished closing. This method may block waiting on the state
166+ /// lock as well as libmongoc's locks, and thus must be run within the thread pool.
167+ fileprivate func checkIn( _ connection: Connection ) throws {
168+ try self . stateLock. withLock {
169+ switch self . state {
170+ case let . open( pool) , let . closing( pool) :
171+ mongoc_client_pool_push ( pool, connection. clientHandle)
172+ self . connCount -= 1
173+ // signal to close() that we are updating the count.
174+ self . stateLock. signal ( )
175+ case . closed:
176+ throw Self . PoolClosedError
98177 }
99- return Connection ( clientHandle: handle, pool: self )
100- case . closed:
101- throw InternalError ( message: " ConnectionPool was already closed " )
102178 }
103179 }
104180
@@ -109,9 +185,9 @@ internal class ConnectionPool {
109185 return try body ( connection)
110186 }
111187
112- // Sets TLS/SSL options that the user passes in through the client level. This must be called from
113- // the ConnectionPool init before the pool is used .
114- private func setTLSOptions( _ options: ClientOptions ) throws {
188+ // Sets TLS/SSL options that the user passes in through the client level. ** This must only be called from
189+ // the ConnectionPool initializer** .
190+ private func setTLSOptions( _ options: ClientOptions ) {
115191 // return early so we don't set an empty options struct on the libmongoc pool. doing so will make libmongoc
116192 // attempt to use TLS for connections.
117193 guard options. tls == true ||
@@ -147,11 +223,14 @@ internal class ConnectionPool {
147223 if let invalidHosts = options. tlsAllowInvalidHostnames {
148224 opts. allow_invalid_hostname = invalidHosts
149225 }
226+
227+ // lock isn't needed as this is called before pool is in use.
150228 switch self . state {
151229 case let . open( pool) :
152230 mongoc_client_pool_set_ssl_opts ( pool, & opts)
153- case . closed:
154- throw InternalError ( message: " ConnectionPool was already closed " )
231+ case . closing, . closed:
232+ // if we get here, we must have called this method outside of `ConnectionPool.init`.
233+ fatalError ( " ConnectionPool in unexpected state \( self . state) while setting TLS options " )
155234 }
156235 }
157236
@@ -187,6 +266,22 @@ internal class ConnectionPool {
187266 return ConnectionString ( copying: uri)
188267 }
189268 }
269+
270+ /// Sets the provided APM callbacks on this pool, using the provided client as the "context" value. **This method
271+ /// may only be called before any connections are checked out of the pool.** Ideally this code would just live in
272+ /// `ConnectionPool.init`. However, the client we accept here has to be fully initialized before we can pass it
273+ /// as the context. In order for it to be fully initialized its pool must exist already.
274+ internal func setAPMCallbacks( callbacks: OpaquePointer , client: MongoClient ) {
275+ // lock isn't needed as this is called before pool is in use.
276+ switch self . state {
277+ case let . open( pool) :
278+ mongoc_client_pool_set_apm_callbacks ( pool, callbacks, Unmanaged . passUnretained ( client) . toOpaque ( ) )
279+ case . closing, . closed:
280+ // this method is called via `initializeMonitoring()`, which is called from `MongoClient.init`.
281+ // unless we have a bug it's impossible that the pool is already closed.
282+ fatalError ( " ConnectionPool in unexpected state \( self . state) while setting APM callbacks " )
283+ }
284+ }
190285}
191286
192287extension String {
0 commit comments