diff --git a/Package.swift b/Package.swift index f85a1175b..cec829458 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ let package = Package( .target(name: "AtlasConnectivity", dependencies: ["MongoSwiftSync"]), .target(name: "TestsCommon", dependencies: ["MongoSwift", "Nimble"]), .testTarget(name: "BSONTests", dependencies: ["MongoSwift", "TestsCommon", "Nimble", "CLibMongoC"]), - .testTarget(name: "MongoSwiftTests", dependencies: ["MongoSwift", "TestsCommon", "Nimble", "NIO", "CLibMongoC"]), + .testTarget(name: "MongoSwiftTests", dependencies: ["MongoSwift", "TestsCommon", "Nimble", "NIO"]), .testTarget(name: "MongoSwiftSyncTests", dependencies: ["MongoSwiftSync", "TestsCommon", "Nimble", "MongoSwift"]), .target( name: "CLibMongoC", diff --git a/Sources/MongoSwift/ConnectionPool.swift b/Sources/MongoSwift/ConnectionPool.swift index fb5f97def..961b880cc 100644 --- a/Sources/MongoSwift/ConnectionPool.swift +++ b/Sources/MongoSwift/ConnectionPool.swift @@ -160,18 +160,19 @@ internal class ConnectionPool { /// started already. This method may block. internal func selectServer(forWrites: Bool, readPreference: ReadPreference? = nil) throws -> ServerDescription { try self.withConnection { conn in - var error = bson_error_t() - guard let desc = mongoc_client_select_server( - conn.clientHandle, - forWrites, - readPreference?.pointer, - &error - ) else { - throw extractMongoError(error: error) + try ReadPreference.withOptionalMongocReadPreference(from: readPreference) { rpPtr in + var error = bson_error_t() + guard let desc = mongoc_client_select_server( + conn.clientHandle, + forWrites, + rpPtr, + &error + ) else { + throw extractMongoError(error: error) + } + defer { mongoc_server_description_destroy(desc) } + return ServerDescription(desc) } - - defer { mongoc_server_description_destroy(desc) } - return ServerDescription(desc) } } diff --git a/Sources/MongoSwift/ConnectionString.swift b/Sources/MongoSwift/ConnectionString.swift index f44ad119e..e60548af0 100644 --- a/Sources/MongoSwift/ConnectionString.swift +++ b/Sources/MongoSwift/ConnectionString.swift @@ -47,7 +47,7 @@ internal class ConnectionString { /// The `ReadConcern` for this connection string. internal var readConcern: ReadConcern { get { - ReadConcern(from: mongoc_uri_get_read_concern(self._uri)) + ReadConcern(copying: mongoc_uri_get_read_concern(self._uri)) } set(rc) { rc.withMongocReadConcern { rcPtr in @@ -59,7 +59,7 @@ internal class ConnectionString { /// The `WriteConcern` for this connection string. internal var writeConcern: WriteConcern { get { - WriteConcern(from: mongoc_uri_get_write_concern(self._uri)) + WriteConcern(copying: mongoc_uri_get_write_concern(self._uri)) } set(wc) { wc.withMongocWriteConcern { wcPtr in @@ -74,7 +74,9 @@ internal class ConnectionString { ReadPreference(copying: mongoc_uri_get_read_prefs_t(self._uri)) } set(rp) { - mongoc_uri_set_read_prefs_t(self._uri, rp.pointer) + rp.withMongocReadPreference { rpPtr in + mongoc_uri_set_read_prefs_t(self._uri, rpPtr) + } } } diff --git a/Sources/MongoSwift/MongoClient.swift b/Sources/MongoSwift/MongoClient.swift index 3106ccc02..cce6ab3cc 100644 --- a/Sources/MongoSwift/MongoClient.swift +++ b/Sources/MongoSwift/MongoClient.swift @@ -1,3 +1,4 @@ +import CLibMongoC import Foundation import NIO import NIOConcurrencyHelpers @@ -609,6 +610,30 @@ public class MongoClient { ) throws -> T.OperationResult { try self.operationExecutor.execute(operation, using: connection, client: self, session: session).wait() } + + /// Internal method to check the `ReadConcern` that was ultimately set on this client. **This method may block + /// and is for testing purposes only**. + internal func getMongocReadConcern() throws -> ReadConcern? { + try self.connectionPool.withConnection { conn in + ReadConcern(copying: mongoc_client_get_read_concern(conn.clientHandle)) + } + } + + /// Internal method to check the `ReadPreference` that was ultimately set on this client. **This method may block + /// and is for testing purposes only**. + internal func getMongocReadPreference() throws -> ReadPreference { + try self.connectionPool.withConnection { conn in + ReadPreference(copying: mongoc_client_get_read_prefs(conn.clientHandle)) + } + } + + /// Internal method to check the `WriteConcern` that was ultimately set on this client. **This method may block + /// and is for testing purposes only**. + internal func getMongocWriteConcern() throws -> WriteConcern? { + try self.connectionPool.withConnection { conn in + WriteConcern(copying: mongoc_client_get_write_concern(conn.clientHandle)) + } + } } extension MongoClient: Equatable { diff --git a/Sources/MongoSwift/MongoCollection+BulkWrite.swift b/Sources/MongoSwift/MongoCollection+BulkWrite.swift index e00a7f693..91bab4482 100644 --- a/Sources/MongoSwift/MongoCollection+BulkWrite.swift +++ b/Sources/MongoSwift/MongoCollection+BulkWrite.swift @@ -257,7 +257,7 @@ internal struct BulkWriteOperation: Operation { // transactions do not support unacknowledged writes. writeConcernAcknowledged = true } else { - let writeConcern = WriteConcern(from: mongoc_bulk_operation_get_write_concern(bulk)) + let writeConcern = WriteConcern(copying: mongoc_bulk_operation_get_write_concern(bulk)) writeConcernAcknowledged = writeConcern.isAcknowledged } diff --git a/Sources/MongoSwift/MongoCollection.swift b/Sources/MongoSwift/MongoCollection.swift index 62c270aec..fda4f6364 100644 --- a/Sources/MongoSwift/MongoCollection.swift +++ b/Sources/MongoSwift/MongoCollection.swift @@ -139,9 +139,41 @@ public struct MongoCollection { if self.readPreference != self._client.readPreference { // there is no concept of an empty read preference so we will always have a value here. - mongoc_collection_set_read_prefs(collection, self.readPreference.pointer) + self.readPreference.withMongocReadPreference { rpPtr in + mongoc_collection_set_read_prefs(collection, rpPtr) + } } return try body(collection) } + + /// Internal method to check the `ReadConcern` that is set on `mongoc_collection_t`s via `withMongocCollection`. + /// **This method may block and is for testing purposes only**. + internal func getMongocReadConcern() throws -> ReadConcern? { + try self._client.connectionPool.withConnection { conn in + self.withMongocCollection(from: conn) { collPtr in + ReadConcern(copying: mongoc_collection_get_read_concern(collPtr)) + } + } + } + + /// Internal method to check the `ReadPreference` that is set on `mongoc_collection_t`s via `withMongocCollection`. + /// **This method may block and is for testing purposes only**. + internal func getMongocReadPreference() throws -> ReadPreference { + try self._client.connectionPool.withConnection { conn in + self.withMongocCollection(from: conn) { collPtr in + ReadPreference(copying: mongoc_collection_get_read_prefs(collPtr)) + } + } + } + + /// Internal method to check the `WriteConcern` that is set on `mongoc_collection_t`s via `withMongocCollection`. + /// **This method may block and is for testing purposes only**. + internal func getMongocWriteConcern() throws -> WriteConcern? { + try self._client.connectionPool.withConnection { conn in + self.withMongocCollection(from: conn) { collPtr in + WriteConcern(copying: mongoc_collection_get_write_concern(collPtr)) + } + } + } } diff --git a/Sources/MongoSwift/MongoDatabase.swift b/Sources/MongoSwift/MongoDatabase.swift index 24ee11d12..bcfd6ca12 100644 --- a/Sources/MongoSwift/MongoDatabase.swift +++ b/Sources/MongoSwift/MongoDatabase.swift @@ -491,9 +491,41 @@ public struct MongoDatabase { if self.readPreference != self._client.readPreference { // there is no concept of an empty read preference so we will always have a value here. - mongoc_database_set_read_prefs(db, self.readPreference.pointer) + self.readPreference.withMongocReadPreference { rpPtr in + mongoc_database_set_read_prefs(db, rpPtr) + } } return try body(db) } + + /// Internal method to check the `ReadConcern` that is set on `mongoc_database_t`s via `withMongocDatabase`. + /// **This method may block and is for testing purposes only**. + internal func getMongocReadConcern() throws -> ReadConcern? { + try self._client.connectionPool.withConnection { conn in + self.withMongocDatabase(from: conn) { dbPtr in + ReadConcern(copying: mongoc_database_get_read_concern(dbPtr)) + } + } + } + + /// Internal method to check the `ReadPreference` that is set on `mongoc_database_t`s via `withMongocDatabase`. + /// **This method may block and is for testing purposes only**. + internal func getMongocReadPreference() throws -> ReadPreference { + try self._client.connectionPool.withConnection { conn in + self.withMongocDatabase(from: conn) { dbPtr in + ReadPreference(copying: mongoc_database_get_read_prefs(dbPtr)) + } + } + } + + /// Internal method to check the `WriteConcern` that is set on `mongoc_database_t`s via `withMongocDatabase`. + /// **This method may block and is for testing purposes only**. + internal func getMongocWriteConcern() throws -> WriteConcern? { + try self._client.connectionPool.withConnection { conn in + self.withMongocDatabase(from: conn) { dbPtr in + WriteConcern(copying: mongoc_database_get_write_concern(dbPtr)) + } + } + } } diff --git a/Sources/MongoSwift/Operations/AggregateOperation.swift b/Sources/MongoSwift/Operations/AggregateOperation.swift index eaa21b964..84b91ee52 100644 --- a/Sources/MongoSwift/Operations/AggregateOperation.swift +++ b/Sources/MongoSwift/Operations/AggregateOperation.swift @@ -83,22 +83,23 @@ internal struct AggregateOperation: Operation { internal func execute(using connection: Connection, session: ClientSession?) throws -> MongoCursor { let opts = try encodeOptions(options: self.options, session: session) - let rp = self.options?.readPreference?.pointer let pipeline: Document = ["pipeline": .array(self.pipeline.map { .document($0) })] let result: OpaquePointer = self.collection.withMongocCollection(from: connection) { collPtr in pipeline.withBSONPointer { pipelinePtr in withOptionalBSONPointer(to: opts) { optsPtr in - guard let result = mongoc_collection_aggregate( - collPtr, - MONGOC_QUERY_NONE, - pipelinePtr, - optsPtr, - rp - ) else { - fatalError(failedToRetrieveCursorMessage) + ReadPreference.withOptionalMongocReadPreference(from: self.options?.readPreference) { rpPtr in + guard let result = mongoc_collection_aggregate( + collPtr, + MONGOC_QUERY_NONE, + pipelinePtr, + optsPtr, + rpPtr + ) else { + fatalError(failedToRetrieveCursorMessage) + } + return result } - return result } } } diff --git a/Sources/MongoSwift/Operations/CountDocumentsOperation.swift b/Sources/MongoSwift/Operations/CountDocumentsOperation.swift index fedbf61f6..3057c2cb3 100644 --- a/Sources/MongoSwift/Operations/CountDocumentsOperation.swift +++ b/Sources/MongoSwift/Operations/CountDocumentsOperation.swift @@ -63,12 +63,13 @@ internal struct CountDocumentsOperation: Operation { internal func execute(using connection: Connection, session: ClientSession?) throws -> Int { let opts = try encodeOptions(options: options, session: session) - let rp = self.options?.readPreference?.pointer var error = bson_error_t() let count = self.collection.withMongocCollection(from: connection) { collPtr in self.filter.withBSONPointer { filterPtr in withOptionalBSONPointer(to: opts) { optsPtr in - mongoc_collection_count_documents(collPtr, filterPtr, optsPtr, rp, nil, &error) + ReadPreference.withOptionalMongocReadPreference(from: self.options?.readPreference) { rpPtr in + mongoc_collection_count_documents(collPtr, filterPtr, optsPtr, rpPtr, nil, &error) + } } } } diff --git a/Sources/MongoSwift/Operations/DistinctOperation.swift b/Sources/MongoSwift/Operations/DistinctOperation.swift index 5cf3ee9fc..3afb705cc 100644 --- a/Sources/MongoSwift/Operations/DistinctOperation.swift +++ b/Sources/MongoSwift/Operations/DistinctOperation.swift @@ -56,14 +56,15 @@ internal struct DistinctOperation: Operation { ] let opts = try encodeOptions(options: self.options, session: session) - let rp = self.options?.readPreference?.pointer var reply = Document() var error = bson_error_t() let success = self.collection.withMongocCollection(from: connection) { collPtr in command.withBSONPointer { cmdPtr in - withOptionalBSONPointer(to: opts) { optsPtr in - reply.withMutableBSONPointer { replyPtr in - mongoc_collection_read_command_with_opts(collPtr, cmdPtr, rp, optsPtr, replyPtr, &error) + ReadPreference.withOptionalMongocReadPreference(from: self.options?.readPreference) { rpPtr in + withOptionalBSONPointer(to: opts) { optsPtr in + reply.withMutableBSONPointer { replyPtr in + mongoc_collection_read_command_with_opts(collPtr, cmdPtr, rpPtr, optsPtr, replyPtr, &error) + } } } } diff --git a/Sources/MongoSwift/Operations/EstimatedDocumentCountOperation.swift b/Sources/MongoSwift/Operations/EstimatedDocumentCountOperation.swift index e57bb94e8..33bce97a4 100644 --- a/Sources/MongoSwift/Operations/EstimatedDocumentCountOperation.swift +++ b/Sources/MongoSwift/Operations/EstimatedDocumentCountOperation.swift @@ -41,11 +41,12 @@ internal struct EstimatedDocumentCountOperation: Operation { internal func execute(using connection: Connection, session: ClientSession?) throws -> Int { let opts = try encodeOptions(options: options, session: session) - let rp = self.options?.readPreference?.pointer var error = bson_error_t() let count = self.collection.withMongocCollection(from: connection) { collPtr in withOptionalBSONPointer(to: opts) { optsPtr in - mongoc_collection_estimated_document_count(collPtr, optsPtr, rp, nil, &error) + ReadPreference.withOptionalMongocReadPreference(from: self.options?.readPreference) { rpPtr in + mongoc_collection_estimated_document_count(collPtr, optsPtr, rpPtr, nil, &error) + } } } diff --git a/Sources/MongoSwift/Operations/FindOperation.swift b/Sources/MongoSwift/Operations/FindOperation.swift index c164bd819..a70abcb41 100644 --- a/Sources/MongoSwift/Operations/FindOperation.swift +++ b/Sources/MongoSwift/Operations/FindOperation.swift @@ -305,15 +305,16 @@ internal struct FindOperation: Operation { session: ClientSession? ) throws -> MongoCursor { let opts = try encodeOptions(options: self.options, session: session) - let rp = self.options?.readPreference?.pointer let result: OpaquePointer = self.collection.withMongocCollection(from: connection) { collPtr in self.filter.withBSONPointer { filterPtr in withOptionalBSONPointer(to: opts) { optsPtr in - guard let result = mongoc_collection_find_with_opts(collPtr, filterPtr, optsPtr, rp) else { - fatalError(failedToRetrieveCursorMessage) + ReadPreference.withOptionalMongocReadPreference(from: self.options?.readPreference) { rpPtr in + guard let result = mongoc_collection_find_with_opts(collPtr, filterPtr, optsPtr, rpPtr) else { + fatalError(failedToRetrieveCursorMessage) + } + return result } - return result } } } diff --git a/Sources/MongoSwift/Operations/ListDatabasesOperation.swift b/Sources/MongoSwift/Operations/ListDatabasesOperation.swift index 5764db47b..cb248d495 100644 --- a/Sources/MongoSwift/Operations/ListDatabasesOperation.swift +++ b/Sources/MongoSwift/Operations/ListDatabasesOperation.swift @@ -57,7 +57,7 @@ internal struct ListDatabasesOperation: Operation { internal func execute(using connection: Connection, session: ClientSession?) throws -> ListDatabasesResults { // spec requires that this command be run against the primary. - let readPref = ReadPreference(.primary) + let readPref = ReadPreference.primary var cmd: Document = ["listDatabases": 1] if let filter = self.filter { cmd["filter"] = .document(filter) @@ -74,17 +74,19 @@ internal struct ListDatabasesOperation: Operation { var error = bson_error_t() let success = cmd.withBSONPointer { cmdPtr in - withOptionalBSONPointer(to: opts) { optsPtr in - reply.withMutableBSONPointer { replyPtr in - mongoc_client_read_command_with_opts( - connection.clientHandle, - "admin", - cmdPtr, - readPref.pointer, - optsPtr, - replyPtr, - &error - ) + readPref.withMongocReadPreference { rpPtr in + withOptionalBSONPointer(to: opts) { optsPtr in + reply.withMutableBSONPointer { replyPtr in + mongoc_client_read_command_with_opts( + connection.clientHandle, + "admin", + cmdPtr, + rpPtr, + optsPtr, + replyPtr, + &error + ) + } } } } diff --git a/Sources/MongoSwift/Operations/RunCommandOperation.swift b/Sources/MongoSwift/Operations/RunCommandOperation.swift index 1258fdbcb..7b943a407 100644 --- a/Sources/MongoSwift/Operations/RunCommandOperation.swift +++ b/Sources/MongoSwift/Operations/RunCommandOperation.swift @@ -43,15 +43,16 @@ internal struct RunCommandOperation: Operation { } internal func execute(using connection: Connection, session: ClientSession?) throws -> Document { - let rp = self.options?.readPreference?.pointer let opts = try encodeOptions(options: self.options, session: session) var reply = Document() var error = bson_error_t() - let success = self.command.withBSONPointer { cmdPtr in - withOptionalBSONPointer(to: opts) { optsPtr in - reply.withMutableBSONPointer { replyPtr in - self.database.withMongocDatabase(from: connection) { dbPtr in - mongoc_database_command_with_opts(dbPtr, cmdPtr, rp, optsPtr, replyPtr, &error) + let success = self.database.withMongocDatabase(from: connection) { dbPtr in + self.command.withBSONPointer { cmdPtr in + ReadPreference.withOptionalMongocReadPreference(from: self.options?.readPreference) { rpPtr in + withOptionalBSONPointer(to: opts) { optsPtr in + reply.withMutableBSONPointer { replyPtr in + mongoc_database_command_with_opts(dbPtr, cmdPtr, rpPtr, optsPtr, replyPtr, &error) + } } } } diff --git a/Sources/MongoSwift/Operations/StartTransactionOperation.swift b/Sources/MongoSwift/Operations/StartTransactionOperation.swift index e107d07cc..e87ce6838 100644 --- a/Sources/MongoSwift/Operations/StartTransactionOperation.swift +++ b/Sources/MongoSwift/Operations/StartTransactionOperation.swift @@ -49,8 +49,10 @@ internal func withMongocTransactionOpts( } } - if let rpPtr = options?.readPreference?.pointer { - mongoc_transaction_opts_set_read_prefs(optionsPtr, rpPtr) + if let rp = options?.readPreference { + rp.withMongocReadPreference { rpPtr in + mongoc_transaction_opts_set_read_prefs(optionsPtr, rpPtr) + } } if let maxCommitTimeMS = options?.maxCommitTimeMS { diff --git a/Sources/MongoSwift/ReadConcern.swift b/Sources/MongoSwift/ReadConcern.swift index 74f4e7b63..60a640f47 100644 --- a/Sources/MongoSwift/ReadConcern.swift +++ b/Sources/MongoSwift/ReadConcern.swift @@ -62,7 +62,7 @@ public struct ReadConcern: Codable { // Initializes a new `ReadConcern` with the same level as the provided `mongoc_read_concern_t`. // The caller is responsible for freeing the original `mongoc_read_concern_t`. - internal init(from readConcern: OpaquePointer) { + internal init(copying readConcern: OpaquePointer) { if let level = mongoc_read_concern_get_level(readConcern) { self.level = Level(rawValue: String(cString: level)) } diff --git a/Sources/MongoSwift/ReadPreference.swift b/Sources/MongoSwift/ReadPreference.swift index ba3281b83..05431c642 100644 --- a/Sources/MongoSwift/ReadPreference.swift +++ b/Sources/MongoSwift/ReadPreference.swift @@ -3,7 +3,7 @@ import CLibMongoC /// Represents a MongoDB read preference, indicating which member(s) of a replica set read operations should be /// directed to. /// - SeeAlso: https://docs.mongodb.com/manual/reference/read-preference/ -public struct ReadPreference { +public struct ReadPreference: Equatable { /// An enumeration of possible read preference modes. /// - SeeAlso: https://docs.mongodb.com/manual/core/read-preference/#read-preference-modes public enum Mode: String { @@ -56,24 +56,17 @@ public struct ReadPreference { /// The mode specified for this read preference. /// - SeeAlso: https://docs.mongodb.com/manual/core/read-preference/#read-preference-modes - public var mode: Mode { - self.mongocReadPreference.mode - } + public var mode: Mode /// Optionally specified ordered array of tag sets. If provided, a server will only be considered suitable if its /// tags are a superset of at least one of the tag sets. /// - SeeAlso: https://docs.mongodb.com/manual/core/read-preference-tags/#replica-set-read-preference-tag-sets - public var tagSets: [Document]? { - self.mongocReadPreference.tagSets - } + public var tagSets: [Document]? // swiftlint:disable line_length /// An optionally specified value indicating a maximum replication lag, or "staleness", for reads from secondaries. /// - SeeAlso: https://docs.mongodb.com/manual/core/read-preference-staleness/#replica-set-read-preference-max-staleness - public var maxStalenessSeconds: Int? { - self.mongocReadPreference.maxStalenessSeconds - } - + public var maxStalenessSeconds: Int? // swiftlint:enable line_length /// A `ReadPreference` with mode `primary`. This is the default mode. With this mode, all operations read from the @@ -193,104 +186,78 @@ public struct ReadPreference { try ReadPreference(.nearest, tagSets: tagSets, maxStalenessSeconds: maxStalenessSeconds) } - /// An equivalent libmongoc read preference used for libmongoc interop. NOTE: If we were ever to allow mutating the - /// properties of `ReadPreference` after initialization, we would need to implement copy-on-write semantics for - /// this type to prevent multiple `ReadPreference`s from being backed by the same `MongocReadPreference`. Since - /// this type is currently immutable it's ok that copies may share the same libmongoc type. - private let mongocReadPreference: MongocReadPreference - - /// Provides internal access to the underlying libmongoc object. - internal var pointer: OpaquePointer { - self.mongocReadPreference.readPref - } - /// Initializes a `ReadPreference` from a `Mode`. internal init(_ mode: Mode) { - self.mongocReadPreference = MongocReadPreference(mode) + self.mode = mode + self.tagSets = nil + self.maxStalenessSeconds = nil } - internal init(_ mode: Mode, tagSets: [Document]?, maxStalenessSeconds: Int?) throws { - self.mongocReadPreference = try MongocReadPreference( - mode: mode, - tagSets: tagSets, - maxStalenessSeconds: maxStalenessSeconds - ) + private init(_ mode: Mode, tagSets: [Document]?, maxStalenessSeconds: Int?) throws { + if let maxStaleness = maxStalenessSeconds { + guard maxStaleness >= MONGOC_SMALLEST_MAX_STALENESS_SECONDS else { + throw InvalidArgumentError( + message: "Expected maxStalenessSeconds to be >= " + + " \(MONGOC_SMALLEST_MAX_STALENESS_SECONDS), \(maxStaleness) given" + ) + } + } + self.mode = mode + self.tagSets = tagSets + self.maxStalenessSeconds = maxStalenessSeconds } /// Initializes a new `ReadPreference` by copying a `mongoc_read_prefs_t`. Does not free the original. internal init(copying pointer: OpaquePointer) { - self.mongocReadPreference = MongocReadPreference(copying: pointer) - } -} - -/// An extension of `ReadPreference` to make it `Equatable`. -extension ReadPreference: Equatable { - public static func == (lhs: ReadPreference, rhs: ReadPreference) -> Bool { - lhs.mode == rhs.mode && - lhs.tagSets == rhs.tagSets && - lhs.maxStalenessSeconds == rhs.maxStalenessSeconds - } -} - -/// A class wrapping a `mongoc_read_prefs_t`. -private class MongocReadPreference { - /// Pointer to underlying `mongoc_read_prefs_t`. - fileprivate let readPref: OpaquePointer + self.mode = Mode(mongocMode: mongoc_read_prefs_get_mode(pointer)) - fileprivate init(_ mode: ReadPreference.Mode) { - self.readPref = mongoc_read_prefs_new(mode.mongocMode) - } + guard let tagsPointer = mongoc_read_prefs_get_tags(pointer) else { + fatalError("Failed to retrieve read preference tags") + } + // we have to copy because libmongoc owns the pointer. + let wrappedTags = Document(copying: tagsPointer) + if !wrappedTags.isEmpty { + // swiftlint:disable:next force_unwrapping + self.tagSets = wrappedTags.values.map { $0.documentValue! } // libmongoc will always return array of docs + } - fileprivate init(copying pointer: OpaquePointer) { - self.readPref = mongoc_read_prefs_copy(pointer) + let maxStalenessValue = mongoc_read_prefs_get_max_staleness_seconds(pointer) + if maxStalenessValue != MONGOC_NO_MAX_STALENESS { + self.maxStalenessSeconds = Int(exactly: maxStalenessValue) + } } - fileprivate convenience init(mode: ReadPreference.Mode, tagSets: [Document]?, maxStalenessSeconds: Int?) throws { - self.init(mode) + /// Executes the provided closure using a pointer to an equivalent `mongoc_read_prefs_t`. The pointer is only valid + /// within the closure and must not escape. If you need to use the pointed-to struct after the closure completes + /// you must make a copy via `mongoc_read_prefs_copy`. + internal func withMongocReadPreference(body: (OpaquePointer) throws -> T) rethrows -> T { + // swiftlint:disable:next force_unwrapping + let rp = mongoc_read_prefs_new(self.mode.mongocMode)! // never returns nil + defer { mongoc_read_prefs_destroy(rp) } - if let tagSets = tagSets, !tagSets.isEmpty { + if let tagSets = self.tagSets, !tagSets.isEmpty { let tags = Document(tagSets.map { .document($0) }) tags.withBSONPointer { tagsPtr in - mongoc_read_prefs_set_tags(self.readPref, tagsPtr) + mongoc_read_prefs_set_tags(rp, tagsPtr) } } - if let maxStalenessSeconds = maxStalenessSeconds { - guard maxStalenessSeconds >= MONGOC_SMALLEST_MAX_STALENESS_SECONDS else { - throw InvalidArgumentError( - message: "Expected maxStalenessSeconds to be >= " + - " \(MONGOC_SMALLEST_MAX_STALENESS_SECONDS), \(maxStalenessSeconds) given" - ) - } - mongoc_read_prefs_set_max_staleness_seconds(self.readPref, Int64(maxStalenessSeconds)) + if let maxStaleness = self.maxStalenessSeconds { + mongoc_read_prefs_set_max_staleness_seconds(rp, Int64(maxStaleness)) } - } - fileprivate var mode: ReadPreference.Mode { - ReadPreference.Mode(mongocMode: mongoc_read_prefs_get_mode(self.readPref)) + return try body(rp) } - fileprivate var tagSets: [Document]? { - guard let bson = mongoc_read_prefs_get_tags(self.readPref) else { - fatalError("Failed to retrieve read preference tags") + /// If the provided ReadPreference is non-nil, executes the provided closure via `withMongocReadPreference`. + /// Otherwise, executes the provided closure, passing in nil as its single argument. + internal static func withOptionalMongocReadPreference( + from rp: ReadPreference?, + body: (OpaquePointer?) throws -> T + ) rethrows -> T { + guard let rp = rp else { + return try body(nil) } - // we have to copy because libmongoc owns the pointer. - let wrapped = Document(copying: bson) - - guard !wrapped.isEmpty else { - return nil - } - - // swiftlint:disable:next force_unwrapping - return wrapped.values.map { $0.documentValue! } // libmongoc will always give us an array of documents - } - - fileprivate var maxStalenessSeconds: Int? { - let maxStaleness = mongoc_read_prefs_get_max_staleness_seconds(self.readPref) - return maxStaleness == MONGOC_NO_MAX_STALENESS ? nil : Int(exactly: maxStaleness) - } - - deinit { - mongoc_read_prefs_destroy(readPref) + return try rp.withMongocReadPreference(body: body) } } diff --git a/Sources/MongoSwift/WriteConcern.swift b/Sources/MongoSwift/WriteConcern.swift index ba2279c3e..6a409877b 100644 --- a/Sources/MongoSwift/WriteConcern.swift +++ b/Sources/MongoSwift/WriteConcern.swift @@ -108,7 +108,7 @@ public struct WriteConcern: Codable { /// Initializes a new `WriteConcern` with the same values as the provided `mongoc_write_concern_t`. /// The caller is responsible for freeing the original `mongoc_write_concern_t`. - internal init(from writeConcern: OpaquePointer?) { + internal init(copying writeConcern: OpaquePointer) { if mongoc_write_concern_journal_is_set(writeConcern) { self.journal = mongoc_write_concern_get_journal(writeConcern) } else { diff --git a/Tests/BSONTests/CodecTests.swift b/Tests/BSONTests/CodecTests.swift index adcb9a37e..f6503444b 100644 --- a/Tests/BSONTests/CodecTests.swift +++ b/Tests/BSONTests/CodecTests.swift @@ -777,7 +777,7 @@ final class CodecTests: MongoSwiftTestCase { let rc = ReadConcern(.majority) let wc = try WriteConcern(wtimeoutMS: 123) - let rp = ReadPreference(.primary) + let rp = ReadPreference.primary let agg = AggregateOptions( allowDiskUse: true, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 84b2af157..41513ddbd 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -264,6 +264,7 @@ extension ReadConcernTests { ("testReadConcernType", testReadConcernType), ("testClientReadConcern", testClientReadConcern), ("testDatabaseReadConcern", testDatabaseReadConcern), + ("testRoundTripThroughLibmongoc", testRoundTripThroughLibmongoc), ] } @@ -278,7 +279,7 @@ extension ReadPreferenceTests { ("testMode", testMode), ("testTagSets", testTagSets), ("testMaxStalenessSeconds", testMaxStalenessSeconds), - ("testInitFromPointer", testInitFromPointer), + ("testRoundTripThroughLibmongoc", testRoundTripThroughLibmongoc), ("testEquatable", testEquatable), ("testClientReadPreference", testClientReadPreference), ("testDatabaseReadPreference", testDatabaseReadPreference), @@ -383,6 +384,7 @@ extension WriteConcernTests { ("testWriteConcernType", testWriteConcernType), ("testClientWriteConcern", testClientWriteConcern), ("testDatabaseWriteConcern", testDatabaseWriteConcern), + ("testRoundTripThroughLibmongoc", testRoundTripThroughLibmongoc), ] } diff --git a/Tests/MongoSwiftTests/ReadConcernTests.swift b/Tests/MongoSwiftTests/ReadConcernTests.swift index f26c4b070..b8ce71dc8 100644 --- a/Tests/MongoSwiftTests/ReadConcernTests.swift +++ b/Tests/MongoSwiftTests/ReadConcernTests.swift @@ -1,45 +1,20 @@ -import CLibMongoC @testable import MongoSwift import Nimble import TestsCommon /// Indicates that a type has a read concern property, as well as a way to get a read concern from an instance of the /// corresponding mongoc type. -protocol ReadConcernable { +private protocol ReadConcernable { var readConcern: ReadConcern? { get } func getMongocReadConcern() throws -> ReadConcern? } -extension MongoClient: ReadConcernable { - func getMongocReadConcern() throws -> ReadConcern? { - try self.connectionPool.withConnection { conn in - ReadConcern(from: mongoc_client_get_read_concern(conn.clientHandle)) - } - } -} - -extension MongoDatabase: ReadConcernable { - func getMongocReadConcern() throws -> ReadConcern? { - try self._client.connectionPool.withConnection { conn in - self.withMongocDatabase(from: conn) { dbPtr in - ReadConcern(from: mongoc_database_get_read_concern(dbPtr)) - } - } - } -} - -extension MongoCollection: ReadConcernable { - func getMongocReadConcern() throws -> ReadConcern? { - try self._client.connectionPool.withConnection { conn in - self.withMongocCollection(from: conn) { collPtr in - ReadConcern(from: mongoc_collection_get_read_concern(collPtr)) - } - } - } -} +extension MongoClient: ReadConcernable {} +extension MongoDatabase: ReadConcernable {} +extension MongoCollection: ReadConcernable {} /// Checks that a type T, as well as pointers to corresponding libmongoc instances, has the expected read concern. -func checkReadConcern( +private func checkReadConcern( _ instance: T, _ expected: ReadConcern, _ description: String @@ -202,4 +177,23 @@ final class ReadConcernTests: MongoSwiftTestCase { try checkReadConcern(coll5, majority, "collection retrieved with majority RC from \(dbDesc)") } } + + func testRoundTripThroughLibmongoc() throws { + let rcs: [ReadConcern] = [ + ReadConcern(), + ReadConcern(.local), + ReadConcern(.available), + ReadConcern(.majority), + ReadConcern(.linearizable), + ReadConcern(.snapshot), + ReadConcern(.other(level: "a")) + ] + + for original in rcs { + let copy = original.withMongocReadConcern { rcPtr in + ReadConcern(copying: rcPtr) + } + expect(copy).to(equal(original)) + } + } } diff --git a/Tests/MongoSwiftTests/ReadPreferenceTests.swift b/Tests/MongoSwiftTests/ReadPreferenceTests.swift index a1e793107..d7b50e71d 100644 --- a/Tests/MongoSwiftTests/ReadPreferenceTests.swift +++ b/Tests/MongoSwiftTests/ReadPreferenceTests.swift @@ -3,6 +3,27 @@ import Nimble import TestsCommon import XCTest +/// Indicates that a type has a read preference property, as well as a way to get a read preference from an instance +/// of the corresponding mongoc type. +private protocol ReadPreferenceable { + var readPreference: ReadPreference { get } + func getMongocReadPreference() throws -> ReadPreference +} + +extension MongoClient: ReadPreferenceable {} +extension MongoDatabase: ReadPreferenceable {} +extension MongoCollection: ReadPreferenceable {} + +/// Checks that a type T, as well as pointers to corresponding libmongoc instances, has the expected read preference. +private func checkReadPreference( + _ instance: T, + _ expected: ReadPreference, + _ description: String +) throws { + expect(instance.readPreference).to(equal(expected), description: description) + expect(try instance.getMongocReadPreference()).to(equal(expected)) +} + final class ReadPreferenceTests: MongoSwiftTestCase { override func setUp() { self.continueAfterFailure = false @@ -44,10 +65,23 @@ final class ReadPreferenceTests: MongoSwiftTestCase { .to(throwError(errorType: InvalidArgumentError.self)) } - func testInitFromPointer() { - let rpOrig = ReadPreference.primaryPreferred - let rpCopy = ReadPreference(copying: rpOrig.pointer) - expect(rpCopy).to(equal(rpOrig)) + func testRoundTripThroughLibmongoc() throws { + let rps: [ReadPreference] = [ + .primary, + .primaryPreferred, + .secondary, + .secondaryPreferred, + .nearest, + try .secondary(tagSets: [["dc": "east"], [:]]), + try .secondary(maxStalenessSeconds: 100) + ] + + for original in rps { + let copy = original.withMongocReadPreference { origPtr in + ReadPreference(copying: origPtr) + } + expect(copy).to(equal(original)) + } } func testEquatable() throws { @@ -76,30 +110,32 @@ final class ReadPreferenceTests: MongoSwiftTestCase { func testClientReadPreference() throws { try self.withTestClient { client in + let clientDesc = "client created with no RP provided" // expect that a client with an unset read preference has it default to primary - expect(client.readPreference).to(equal(.primary)) + try checkReadPreference(client, .primary, clientDesc) // expect that a database created from this client inherits its read preference let db1 = client.db(Self.testDatabase) - expect(db1.readPreference).to(equal(.primary)) + try checkReadPreference(db1, .primary, "db created with no RP provided from \(clientDesc)") // expect that a database can override the readPreference it inherited from a client let opts = DatabaseOptions(readPreference: .secondary) let db2 = client.db(Self.testDatabase, options: opts) - expect(db2.readPreference).to(equal(.secondary)) + try checkReadPreference(db2, .secondary, "db created with secondary RP from \(clientDesc)") } try self.withTestClient(options: ClientOptions(readPreference: .primaryPreferred)) { client in - expect(client.readPreference).to(equal(.primaryPreferred)) + let clientDesc = "client created with RP primaryPreferred" + try checkReadPreference(client, .primaryPreferred, clientDesc) // expect that a database created from this client inherits its read preference let db1 = client.db(Self.testDatabase) - expect(db1.readPreference).to(equal(.primaryPreferred)) + try checkReadPreference(db1, .primaryPreferred, "db created with no RP provided from \(clientDesc)") // expect that a database can override the readPreference it inherited from a client let opts = DatabaseOptions(readPreference: .secondary) let db2 = client.db(Self.testDatabase, options: opts) - expect(db2.readPreference).to(equal(.secondary)) + try checkReadPreference(db2, .secondary, "db created with secondary RP from \(clientDesc)") } } @@ -107,33 +143,35 @@ final class ReadPreferenceTests: MongoSwiftTestCase { try self.withTestClient { client in do { // expect that a database with an unset read preference defaults to primary + let dbDesc = "db created with no RP provided" let db = client.db(Self.testDatabase) - expect(db.readPreference).to(equal(.primary)) + try checkReadPreference(db, .primary, dbDesc) // expect that a collection inherits its database default read preference let coll1 = db.collection(self.getCollectionName(suffix: "1")) - expect(coll1.readPreference).to(equal(.primary)) + try checkReadPreference(coll1, .primary, "coll created with no RP provided from \(dbDesc)") // expect that a collection can override its inherited read preference let coll2 = db.collection( self.getCollectionName(suffix: "2"), options: CollectionOptions(readPreference: .secondary) ) - expect(coll2.readPreference).to(equal(.secondary)) + try checkReadPreference(coll2, .secondary, "coll created with secondary RP from \(dbDesc)") } do { // expect that a collection inherits its database read preference + let dbDesc = "db created with secondary RP" let db = client.db(Self.testDatabase, options: DatabaseOptions(readPreference: .secondary)) let coll1 = db.collection(self.getCollectionName(suffix: "1")) - expect(coll1.readPreference).to(equal(.secondary)) + try checkReadPreference(coll1, .secondary, "coll created with no RP provided from \(dbDesc)") // expect that a collection can override its database read preference let coll2 = db.collection( self.getCollectionName(suffix: "2"), options: CollectionOptions(readPreference: .primary) ) - expect(coll2.readPreference).to(equal(.primary)) + try checkReadPreference(coll2, .primary, "coll created with primary RP from \(dbDesc)") } } } diff --git a/Tests/MongoSwiftTests/WriteConcernTests.swift b/Tests/MongoSwiftTests/WriteConcernTests.swift index 9de08f915..954a3d98c 100644 --- a/Tests/MongoSwiftTests/WriteConcernTests.swift +++ b/Tests/MongoSwiftTests/WriteConcernTests.swift @@ -1,45 +1,20 @@ -import CLibMongoC @testable import MongoSwift import Nimble import TestsCommon /// Indicates that a type has a write concern property, as well as a way to get a write concern from an instance of the /// corresponding mongoc type. -protocol WriteConcernable { +private protocol WriteConcernable { var writeConcern: WriteConcern? { get } func getMongocWriteConcern() throws -> WriteConcern? } -extension MongoClient: WriteConcernable { - func getMongocWriteConcern() throws -> WriteConcern? { - try self.connectionPool.withConnection { conn in - WriteConcern(from: mongoc_client_get_write_concern(conn.clientHandle)) - } - } -} - -extension MongoDatabase: WriteConcernable { - func getMongocWriteConcern() throws -> WriteConcern? { - try self._client.connectionPool.withConnection { conn in - self.withMongocDatabase(from: conn) { dbPtr in - WriteConcern(from: mongoc_database_get_write_concern(dbPtr)) - } - } - } -} - -extension MongoCollection: WriteConcernable { - func getMongocWriteConcern() throws -> WriteConcern? { - try self._client.connectionPool.withConnection { conn in - self.withMongocCollection(from: conn) { collPtr in - WriteConcern(from: mongoc_collection_get_write_concern(collPtr)) - } - } - } -} +extension MongoClient: WriteConcernable {} +extension MongoDatabase: WriteConcernable {} +extension MongoCollection: WriteConcernable {} /// Checks that a type T, as well as pointers to corresponding libmongoc instances, has the expected write concern. -func checkWriteConcern( +private func checkWriteConcern( _ instance: T, _ expected: WriteConcern, _ description: String @@ -167,4 +142,22 @@ final class WriteConcernTests: MongoSwiftTestCase { try checkWriteConcern(coll4, w2, "collection retrieved with w:2 from \(dbDesc)") } } + + func testRoundTripThroughLibmongoc() throws { + let wcs: [WriteConcern] = [ + WriteConcern(), + try WriteConcern(w: .number(2)), + try WriteConcern(w: .tag("hi")), + try WriteConcern(w: .majority), + try WriteConcern(journal: true), + try WriteConcern(wtimeoutMS: 200) + ] + + for original in wcs { + let copy = original.withMongocWriteConcern { wcPtr in + WriteConcern(copying: wcPtr) + } + expect(copy).to(equal(original)) + } + } }