Skip to content

Commit bcfa46b

Browse files
authored
SWIFT-360 Allow specification of an event handler instead of NotificationCenter for APM (#409)
1 parent 6ebad16 commit bcfa46b

18 files changed

+1063
-834
lines changed

Sources/MongoSwift/APM.swift

Lines changed: 431 additions & 276 deletions
Large diffs are not rendered by default.

Sources/MongoSwift/MongoClient.swift

Lines changed: 95 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,6 @@ import NIOConcurrencyHelpers
44

55
/// Options to use when creating a `MongoClient`.
66
public struct ClientOptions: CodingStrategyProvider, Decodable {
7-
/**
8-
* Indicates whether this client should publish command monitoring events. If true, the following event types will
9-
* be published, under the listed names (which are defined as static properties of `Notification.Name`):
10-
* - `CommandStartedEvent`: `.commandStarted`
11-
* - `CommandSucceededEvent`: `.commandSucceeded`
12-
* - `CommandFailedEvent`: `.commandFailed`
13-
*/
14-
public var commandMonitoring: Bool = false
15-
167
// swiftlint:disable redundant_optional_initialization
178

189
/// Specifies the `DataCodingStrategy` to use for BSON encoding/decoding operations performed by this client and any
@@ -23,10 +14,6 @@ public struct ClientOptions: CodingStrategyProvider, Decodable {
2314
/// databases or collections that derive from it.
2415
public var dateCodingStrategy: DateCodingStrategy? = nil
2516

26-
/// If command and/or server monitoring is enabled, indicates the `NotificationCenter` events are posted to. If one
27-
/// is not specified, the application's default `NotificationCenter` will be used.
28-
public var notificationCenter: NotificationCenter?
29-
3017
/// Specifies a ReadConcern to use for the client.
3118
public var readConcern: ReadConcern?
3219

@@ -39,21 +26,6 @@ public struct ClientOptions: CodingStrategyProvider, Decodable {
3926
/// Determines whether the client should retry supported write operations (on by default).
4027
public var retryWrites: Bool?
4128

42-
/**
43-
* Indicates whether this client should publish command monitoring events. If true, the following event types will
44-
* be published, under the listed names (which are defined as static properties of `Notification.Name`):
45-
* - `ServerOpeningEvent`: `.serverOpening`
46-
* - `ServerClosedEvent`: `.serverClosed`
47-
* - `ServerDescriptionChangedEvent`: `.serverDescriptionChanged`
48-
* - `TopologyOpeningEvent`: `.topologyOpening`
49-
* - `TopologyClosedEvent`: `.topologyClosed`
50-
* - `TopologyDescriptionChangedEvent`: `.topologyDescriptionChanged`
51-
* - `ServerHeartbeatStartedEvent`: `serverHeartbeatStarted`
52-
* - `ServerHeartbeatSucceededEvent`: `serverHeartbeatSucceeded`
53-
* - `ServerHeartbeatFailedEvent`: `serverHeartbeatFailed`
54-
*/
55-
public var serverMonitoring: Bool = false
56-
5729
/**
5830
* `MongoSwift.MongoClient` provides an asynchronous API by running all blocking operations off of their
5931
* originating threads in a thread pool. `MongoSwiftSync.MongoClient` is implemented as a wrapper of the async
@@ -81,29 +53,23 @@ public struct ClientOptions: CodingStrategyProvider, Decodable {
8153

8254
/// Convenience initializer allowing any/all to be omitted or optional.
8355
public init(
84-
commandMonitoring: Bool = false,
8556
dataCodingStrategy: DataCodingStrategy? = nil,
8657
dateCodingStrategy: DateCodingStrategy? = nil,
87-
notificationCenter: NotificationCenter? = nil,
8858
readConcern: ReadConcern? = nil,
8959
readPreference: ReadPreference? = nil,
9060
retryReads: Bool? = nil,
9161
retryWrites: Bool? = nil,
92-
serverMonitoring: Bool = false,
9362
threadPoolSize: Int = MongoClient.defaultThreadPoolSize,
9463
tlsOptions: TLSOptions? = nil,
9564
uuidCodingStrategy: UUIDCodingStrategy? = nil,
9665
writeConcern: WriteConcern? = nil
9766
) {
98-
self.commandMonitoring = commandMonitoring
9967
self.dataCodingStrategy = dataCodingStrategy
10068
self.dateCodingStrategy = dateCodingStrategy
101-
self.notificationCenter = notificationCenter
10269
self.readConcern = readConcern
10370
self.readPreference = readPreference
10471
self.retryWrites = retryWrites
10572
self.retryReads = retryReads
106-
self.serverMonitoring = serverMonitoring
10773
self.threadPoolSize = threadPoolSize
10874
self.tlsOptions = tlsOptions
10975
self.uuidCodingStrategy = uuidCodingStrategy
@@ -199,8 +165,11 @@ public class MongoClient {
199165
/// Indicates whether this client has been closed.
200166
internal private(set) var isClosed = false
201167

202-
/// If command and/or server monitoring is enabled, stores the NotificationCenter events are posted to.
203-
internal let notificationCenter: NotificationCenter
168+
/// Handlers for command monitoring events.
169+
internal var commandEventHandlers: [CommandEventHandler]
170+
171+
/// Handlers for SDAM monitoring events.
172+
internal var sdamEventHandlers: [SDAMEventHandler]
204173

205174
/// Counter for generating client _ids.
206175
internal static var clientIdGenerator = NIOAtomic<Int>.makeAtomic(value: 0)
@@ -275,13 +244,9 @@ public class MongoClient {
275244
self.readPreference = connString.readPreference
276245
self.encoder = BSONEncoder(options: options)
277246
self.decoder = BSONDecoder(options: options)
278-
self.notificationCenter = options?.notificationCenter ?? NotificationCenter.default
279-
280-
self.connectionPool.initializeMonitoring(
281-
commandMonitoring: options?.commandMonitoring ?? false,
282-
serverMonitoring: options?.serverMonitoring ?? false,
283-
client: self
284-
)
247+
self.sdamEventHandlers = []
248+
self.commandEventHandlers = []
249+
self.connectionPool.initializeMonitoring(client: self)
285250
}
286251

287252
deinit {
@@ -583,6 +548,46 @@ public class MongoClient {
583548
return self.operationExecutor.execute(operation, client: self, session: session)
584549
}
585550

551+
/**
552+
* Attach a `CommandEventHandler` that will receive `CommandEvent`s emitted by this client.
553+
*
554+
* Note: the client stores a weak reference to this handler, so it must be kept alive separately in order for it
555+
* to continue to receive events.
556+
*/
557+
public func addCommandEventHandler<T: CommandEventHandler>(_ handler: T) {
558+
self.commandEventHandlers.append(WeakEventHandler<T>(referencing: handler))
559+
}
560+
561+
/**
562+
* Attach a callback that will receive `CommandEvent`s emitted by this client.
563+
*
564+
* Note: if the provided callback captures this client, it must do so weakly. Otherwise, it will constitute a
565+
* strong reference cycle and potentially result in memory leaks.
566+
*/
567+
public func addCommandEventHandler(_ handlerFunc: @escaping (CommandEvent) -> Void) {
568+
self.commandEventHandlers.append(CallbackEventHandler(handlerFunc))
569+
}
570+
571+
/**
572+
* Attach an `SDAMEventHandler` that will receive `CommandEvent`s emitted by this client.
573+
*
574+
* Note: the client stores a weak reference to this handler, so it must be kept alive separately in order for it
575+
* to continue to receive events.
576+
*/
577+
public func addSDAMEventHandler<T: SDAMEventHandler>(_ handler: T) {
578+
self.sdamEventHandlers.append(WeakEventHandler(referencing: handler))
579+
}
580+
581+
/**
582+
* Attach a callback that will receive `SDAMEvent`s emitted by this client.
583+
*
584+
* Note: if the provided callback captures this client, it must do so weakly. Otherwise, it will constitute a
585+
* strong reference cycle and potentially result in memory leaks.
586+
*/
587+
public func addSDAMEventHandler(_ handlerFunc: @escaping (SDAMEvent) -> Void) {
588+
self.sdamEventHandlers.append(CallbackEventHandler(handlerFunc))
589+
}
590+
586591
/// Executes an `Operation` using this `MongoClient` and an optionally provided session.
587592
internal func executeOperation<T: Operation>(
588593
_ operation: T,
@@ -598,3 +603,50 @@ extension MongoClient: Equatable {
598603
return lhs._id == rhs._id
599604
}
600605
}
606+
607+
/// Event handler constructed from a callback.
608+
/// Stores a strong reference to the provided callback.
609+
private class CallbackEventHandler<EventType> {
610+
private let handlerFunc: (EventType) -> Void
611+
612+
fileprivate init(_ handlerFunc: @escaping (EventType) -> Void) {
613+
self.handlerFunc = handlerFunc
614+
}
615+
}
616+
617+
/// Extension to make `CallbackEventHandler` an `SDAMEventHandler` when the event type is an `SDAMEvent`.
618+
extension CallbackEventHandler: SDAMEventHandler where EventType == SDAMEvent {
619+
fileprivate func handleSDAMEvent(_ event: SDAMEvent) {
620+
self.handlerFunc(event)
621+
}
622+
}
623+
624+
/// Extension to make `CallbackEventHandler` a `CommandEventHandler` when the event type is a `CommandEvent`.
625+
extension CallbackEventHandler: CommandEventHandler where EventType == CommandEvent {
626+
fileprivate func handleCommandEvent(_ event: CommandEvent) {
627+
self.handlerFunc(event)
628+
}
629+
}
630+
631+
/// Event handler that stores a weak reference to the underlying handler.
632+
private class WeakEventHandler<T: AnyObject> {
633+
private weak var handler: T?
634+
635+
fileprivate init(referencing handler: T) {
636+
self.handler = handler
637+
}
638+
}
639+
640+
/// Extension to make `WeakEventHandler` a `CommandEventHandler` when the referenced handler is a `CommandEventHandler`.
641+
extension WeakEventHandler: CommandEventHandler where T: CommandEventHandler {
642+
fileprivate func handleCommandEvent(_ event: CommandEvent) {
643+
self.handler?.handleCommandEvent(event)
644+
}
645+
}
646+
647+
/// Extension to make `WeakEventHandler` an `SDAMEventHandler` when the referenced handler is an `SDAMEventHandler`.
648+
extension WeakEventHandler: SDAMEventHandler where T: SDAMEventHandler {
649+
fileprivate func handleSDAMEvent(_ event: SDAMEvent) {
650+
self.handler?.handleSDAMEvent(event)
651+
}
652+
}

Sources/MongoSwiftSync/Exports.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
@_exported import struct MongoSwift.CollectionSpecificationInfo
3434
@_exported import enum MongoSwift.CollectionType
3535
@_exported import struct MongoSwift.CommandError
36+
@_exported import enum MongoSwift.CommandEvent
3637
@_exported import struct MongoSwift.CommandFailedEvent
3738
@_exported import struct MongoSwift.CommandStartedEvent
3839
@_exported import struct MongoSwift.CommandSucceededEvent
@@ -86,6 +87,7 @@
8687
@_exported import struct MongoSwift.ResumeToken
8788
@_exported import enum MongoSwift.ReturnDocument
8889
@_exported import struct MongoSwift.RunCommandOptions
90+
@_exported import enum MongoSwift.SDAMEvent
8991
@_exported import struct MongoSwift.ServerClosedEvent
9092
@_exported import struct MongoSwift.ServerDescription
9193
@_exported import struct MongoSwift.ServerDescriptionChangedEvent
@@ -114,15 +116,11 @@
114116

115117
// Protocols are not included in the types list, so we list them separately here.
116118
@_exported import protocol MongoSwift.CodingStrategyProvider
119+
@_exported import protocol MongoSwift.CommandEventHandler
117120
@_exported import protocol MongoSwift.LabeledError
118-
@_exported import protocol MongoSwift.MongoCommandEvent
119121
@_exported import protocol MongoSwift.MongoError
120-
@_exported import protocol MongoSwift.MongoEvent
121-
@_exported import protocol MongoSwift.MongoSDAMEvent
122-
@_exported import protocol MongoSwift.MongoServerHeartbeatEvent
123-
@_exported import protocol MongoSwift.MongoServerUpdateEvent
124-
@_exported import protocol MongoSwift.MongoTopologyUpdateEvent
125122
@_exported import protocol MongoSwift.RuntimeError
123+
@_exported import protocol MongoSwift.SDAMEventHandler
126124
@_exported import protocol MongoSwift.ServerError
127125
@_exported import protocol MongoSwift.UserError
128126

Sources/MongoSwiftSync/MongoClient.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,46 @@ public class MongoClient {
281281

282282
return ChangeStream(wrapping: asyncStream, client: self)
283283
}
284+
285+
/**
286+
* Attach a `CommandEventHandler` that will receive `CommandEvent`s emitted by this client.
287+
*
288+
* Note: the client stores a weak reference to this handler, so it must be kept alive separately in order for it
289+
* to continue to receive events.
290+
*/
291+
public func addCommandEventHandler<T: CommandEventHandler>(_ handler: T) {
292+
self.asyncClient.addCommandEventHandler(handler)
293+
}
294+
295+
/**
296+
* Attach a callback that will receive `CommandEvent`s emitted by this client.
297+
*
298+
* Note: if the provided callback captures this client, it must do so weakly. Otherwise, it will constitute a
299+
* strong reference cycle and potentially result in memory leaks.
300+
*/
301+
public func addCommandEventHandler(_ handlerFunc: @escaping (CommandEvent) -> Void) {
302+
self.asyncClient.addCommandEventHandler(handlerFunc)
303+
}
304+
305+
/**
306+
* Attach an `SDAMEventHandler` that will receive `SDAMEvent`s emitted by this client.
307+
*
308+
* Note: the client stores a weak reference to this handler, so it must be kept alive separately in order for it
309+
* to continue to receive events.
310+
*/
311+
public func addSDAMEventHandler<T: SDAMEventHandler>(_ handler: T) {
312+
self.asyncClient.addSDAMEventHandler(handler)
313+
}
314+
315+
/**
316+
* Attach a callback that will receive `SDAMEvent`s emitted by this client.
317+
*
318+
* Note: if the provided callback captures this client, it must do so weakly. Otherwise, it will constitute a
319+
* strong reference cycle and potentially result in memory leaks.
320+
*/
321+
public func addSDAMEventHandler(_ handlerFunc: @escaping (SDAMEvent) -> Void) {
322+
self.asyncClient.addSDAMEventHandler(handlerFunc)
323+
}
284324
}
285325

286326
extension MongoClient: Equatable {

Sources/TestsCommon/APMUtils.swift

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import Foundation
2+
import MongoSwift
3+
4+
/// A command event handler that caches the events it encounters.
5+
/// Note: it will only cache events that occur while closures passed to `captureEvents` are executing.
6+
public class TestCommandMonitor: CommandEventHandler {
7+
private var monitoring: Bool
8+
private var events: [CommandEvent]
9+
10+
public init() {
11+
self.events = []
12+
self.monitoring = false
13+
}
14+
15+
public func handleCommandEvent(_ event: CommandEvent) {
16+
guard self.monitoring else {
17+
return
18+
}
19+
self.events.append(event)
20+
}
21+
22+
/// Retrieve all the command started events seen so far, clearing the event cache.
23+
public func commandStartedEvents(withNames namesFilter: [String]? = nil) -> [CommandStartedEvent] {
24+
return self.events(withNames: namesFilter).compactMap { $0.commandStartedValue }
25+
}
26+
27+
/// Retrieve all the command started events seen so far, clearing the event cache.
28+
public func commandSucceededEvents(withNames namesFilter: [String]? = nil) -> [CommandSucceededEvent] {
29+
return self.events(withNames: namesFilter).compactMap { $0.commandSucceededValue }
30+
}
31+
32+
/// Retrieve all the events seen so far that match the optionally provided filters, clearing the event cache.
33+
public func events(
34+
withEventTypes typeFilter: [CommandEvent.EventType]? = nil,
35+
withNames nameFilter: [String]? = nil
36+
) -> [CommandEvent] {
37+
defer { self.events.removeAll() }
38+
return self.events.compactMap { event in
39+
if let typeFilter = typeFilter {
40+
guard typeFilter.contains(event.type) else {
41+
return nil
42+
}
43+
}
44+
if let nameFilter = nameFilter {
45+
guard nameFilter.contains(event.commandName) else {
46+
return nil
47+
}
48+
}
49+
return event
50+
}
51+
}
52+
53+
/// Capture events that occur while the the provided closure executes.
54+
public func captureEvents<T>(_ f: () throws -> T) rethrows -> T {
55+
self.monitoring = true
56+
defer { self.monitoring = false }
57+
return try f()
58+
}
59+
}
60+
61+
extension CommandEvent {
62+
public enum EventType {
63+
case commandStarted
64+
case commandSucceeded
65+
case commandFailed
66+
}
67+
68+
/// The "type" of this event. Used for filtering events by their type.
69+
public var type: EventType {
70+
switch self {
71+
case .started:
72+
return .commandStarted
73+
case .failed:
74+
return .commandFailed
75+
case .succeeded:
76+
return .commandSucceeded
77+
}
78+
}
79+
80+
/// Returns this event as a `CommandStartedEvent` if it is one, nil otherwise.
81+
public var commandStartedValue: CommandStartedEvent? {
82+
guard case let .started(event) = self else {
83+
return nil
84+
}
85+
return event
86+
}
87+
88+
/// Returns this event as a `CommandSucceededEvent` if it is one, nil otherwise.
89+
public var commandSucceededValue: CommandSucceededEvent? {
90+
guard case let .succeeded(event) = self else {
91+
return nil
92+
}
93+
return event
94+
}
95+
96+
/// Returns this event as a `CommandFailedEvent` if it is one, nil otherwise.
97+
public var commandFailedValue: CommandFailedEvent? {
98+
guard case let .failed(event) = self else {
99+
return nil
100+
}
101+
return event
102+
}
103+
}

0 commit comments

Comments
 (0)