Skip to content

Commit 41c319b

Browse files
Merge pull request #85 from ably/3-decide-how-threading-will-work
[ECO-5377] Finalise and document threading approach
2 parents 8aedccc + 5a63313 commit 41c319b

26 files changed

+1645
-1012
lines changed

AblyLiveObjects.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 3 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CONTRIBUTING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ The `Public…` classes all follow the same pattern and are not very interesting
7070

7171
`ObjectLifetimesTests.swift` contains tests of the behaviour described in this section.
7272

73+
### Threading
74+
75+
Since this is an extension of ably-cocoa, we follow the same threading approach:
76+
77+
1. The public API can be interacted with from any thread, including synchronous methods such as getters
78+
2. Callbacks passed to the public API are invoked on the same queue as used by the `ARTRealtime` instance (the `dispatchQueue` client option)
79+
3. Synchronisation of mutable state is performed using the same internal serial dispatch queue as is used by the `ARTRealtime` instance (the `internalDispatchQueue` client option)
80+
81+
We follow the same naming convention as in ably-cocoa whereby if a method's name contains `nosync` then it must be called on the internal dispatch queue. This allows us to avoid deadlocks that would result from attempting to call `DispatchQueue.sync { … }` when already on the internal queue.
82+
83+
We have an extension on `DispatchQueue`, `ably_syncNoDeadlock(execute:)`, which behaves the same as `sync(execute:)` but with a runtime precondition that we are not already on the queue; favour our extension in order to avoid deadlock.
84+
7385
### Testing guidelines
7486

7587
#### Attributing tests to a spec point

Package.resolved

Lines changed: 3 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ let package = Package(
2020
dependencies: [
2121
.package(
2222
url: "https://github.com/ably/ably-cocoa",
23-
from: "1.2.44",
23+
// TODO: Unpin before next release
24+
revision: "8dde3e841aa1f861176c1341cf44e92014b95857",
2425
),
2526
.package(
2627
url: "https://github.com/ably/ably-cocoa-plugin-support",
27-
from: "0.1.0",
28+
// TODO: Unpin before next release
29+
revision: "2ce1058ed4430cc3563dcead0299e92a81d2774b",
2830
),
2931
.package(
3032
url: "https://github.com/apple/swift-argument-parser",

Sources/AblyLiveObjects/Internal/CoreSDK.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ internal protocol CoreSDK: AnyObject, Sendable {
1414
func testsOnly_overridePublish(with newImplementation: @escaping ([OutboundObjectMessage]) async throws(InternalError) -> Void)
1515

1616
/// Returns the current state of the Realtime channel that this wraps.
17-
var channelState: _AblyPluginSupportPrivate.RealtimeChannelState { get }
17+
var nosync_channelState: _AblyPluginSupportPrivate.RealtimeChannelState { get }
1818
}
1919

2020
internal final class DefaultCoreSDK: CoreSDK {
@@ -81,8 +81,8 @@ internal final class DefaultCoreSDK: CoreSDK {
8181
}
8282
}
8383

84-
internal var channelState: _AblyPluginSupportPrivate.RealtimeChannelState {
85-
pluginAPI.state(for: channel)
84+
internal var nosync_channelState: _AblyPluginSupportPrivate.RealtimeChannelState {
85+
pluginAPI.nosync_state(for: channel)
8686
}
8787
}
8888

@@ -96,11 +96,11 @@ internal extension CoreSDK {
9696
/// - invalidStates: Array of channel states that are considered invalid for the operation
9797
/// - operationDescription: A description of the operation being performed, used in error messages
9898
/// - Throws: `ARTErrorInfo` with code 90001 and statusCode 400 if the channel is in any of the invalid states
99-
func validateChannelState(
99+
func nosync_validateChannelState(
100100
notIn invalidStates: [_AblyPluginSupportPrivate.RealtimeChannelState],
101101
operationDescription: String,
102102
) throws(ARTErrorInfo) {
103-
let currentChannelState = channelState
103+
let currentChannelState = nosync_channelState
104104
if invalidStates.contains(currentChannelState) {
105105
throw LiveObjectsError.objectsOperationFailedInvalidChannelState(
106106
operationDescription: operationDescription,

Sources/AblyLiveObjects/Internal/DefaultInternalPlugin.swift

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ internal final class DefaultInternalPlugin: NSObject, _AblyPluginSupportPrivate.
2121
/// Retrieves the `RealtimeObjects` for this channel.
2222
///
2323
/// We expect this value to have been previously set by ``prepare(_:)``.
24-
internal static func realtimeObjects(for channel: _AblyPluginSupportPrivate.RealtimeChannel, pluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol) -> InternalDefaultRealtimeObjects {
25-
guard let pluginData = pluginAPI.pluginDataValue(forKey: pluginDataKey, channel: channel) else {
24+
internal static func nosync_realtimeObjects(for channel: _AblyPluginSupportPrivate.RealtimeChannel, pluginAPI: _AblyPluginSupportPrivate.PluginAPIProtocol) -> InternalDefaultRealtimeObjects {
25+
guard let pluginData = pluginAPI.nosync_pluginDataValue(forKey: pluginDataKey, channel: channel) else {
2626
// InternalPlugin.prepare was not called
2727
fatalError("To access LiveObjects functionality, you must pass the LiveObjects plugin in the client options when creating the ARTRealtime instance: `clientOptions.plugins = [.liveObjects: AblyLiveObjects.Plugin.self]`")
2828
}
@@ -34,25 +34,27 @@ internal final class DefaultInternalPlugin: NSObject, _AblyPluginSupportPrivate.
3434
// MARK: - LiveObjectsInternalPluginProtocol
3535

3636
// Populates the channel's `objects` property.
37-
internal func prepare(_ channel: _AblyPluginSupportPrivate.RealtimeChannel, client: _AblyPluginSupportPrivate.RealtimeClient) {
37+
internal func nosync_prepare(_ channel: _AblyPluginSupportPrivate.RealtimeChannel, client: _AblyPluginSupportPrivate.RealtimeClient) {
3838
let pluginLogger = pluginAPI.logger(for: channel)
39+
let internalQueue = pluginAPI.internalQueue(for: client)
3940
let callbackQueue = pluginAPI.callbackQueue(for: client)
4041
let options = ARTClientOptions.castPluginPublicClientOptions(pluginAPI.options(for: client))
4142

4243
let logger = DefaultLogger(pluginLogger: pluginLogger, pluginAPI: pluginAPI)
4344
logger.log("LiveObjects.DefaultInternalPlugin received prepare(_:)", level: .debug)
4445
let liveObjects = InternalDefaultRealtimeObjects(
4546
logger: logger,
47+
internalQueue: internalQueue,
4648
userCallbackQueue: callbackQueue,
4749
clock: DefaultSimpleClock(),
4850
garbageCollectionOptions: options.garbageCollectionOptions ?? .init(),
4951
)
50-
pluginAPI.setPluginDataValue(liveObjects, forKey: Self.pluginDataKey, channel: channel)
52+
pluginAPI.nosync_setPluginDataValue(liveObjects, forKey: Self.pluginDataKey, channel: channel)
5153
}
5254

5355
/// Retrieves the internally-typed `objects` property for the channel.
54-
private func realtimeObjects(for channel: _AblyPluginSupportPrivate.RealtimeChannel) -> InternalDefaultRealtimeObjects {
55-
Self.realtimeObjects(for: channel, pluginAPI: pluginAPI)
56+
private func nosync_realtimeObjects(for channel: _AblyPluginSupportPrivate.RealtimeChannel) -> InternalDefaultRealtimeObjects {
57+
Self.nosync_realtimeObjects(for: channel, pluginAPI: pluginAPI)
5658
}
5759

5860
/// A class that wraps an object message.
@@ -102,30 +104,30 @@ internal final class DefaultInternalPlugin: NSObject, _AblyPluginSupportPrivate.
102104
return wireObjectMessage.toWireObject.toPluginSupportDataDictionary
103105
}
104106

105-
internal func onChannelAttached(_ channel: _AblyPluginSupportPrivate.RealtimeChannel, hasObjects: Bool) {
106-
realtimeObjects(for: channel).onChannelAttached(hasObjects: hasObjects)
107+
internal func nosync_onChannelAttached(_ channel: _AblyPluginSupportPrivate.RealtimeChannel, hasObjects: Bool) {
108+
nosync_realtimeObjects(for: channel).nosync_onChannelAttached(hasObjects: hasObjects)
107109
}
108110

109-
internal func handleObjectProtocolMessage(withObjectMessages publicObjectMessages: [any _AblyPluginSupportPrivate.ObjectMessageProtocol], channel: _AblyPluginSupportPrivate.RealtimeChannel) {
111+
internal func nosync_handleObjectProtocolMessage(withObjectMessages publicObjectMessages: [any _AblyPluginSupportPrivate.ObjectMessageProtocol], channel: _AblyPluginSupportPrivate.RealtimeChannel) {
110112
guard let inboundObjectMessageBoxes = publicObjectMessages as? [ObjectMessageBox<InboundObjectMessage>] else {
111113
preconditionFailure("Expected to receive the same InboundObjectMessage type as we emit")
112114
}
113115

114116
let objectMessages = inboundObjectMessageBoxes.map(\.objectMessage)
115117

116-
realtimeObjects(for: channel).handleObjectProtocolMessage(
118+
nosync_realtimeObjects(for: channel).nosync_handleObjectProtocolMessage(
117119
objectMessages: objectMessages,
118120
)
119121
}
120122

121-
internal func handleObjectSyncProtocolMessage(withObjectMessages publicObjectMessages: [any _AblyPluginSupportPrivate.ObjectMessageProtocol], protocolMessageChannelSerial: String?, channel: _AblyPluginSupportPrivate.RealtimeChannel) {
123+
internal func nosync_handleObjectSyncProtocolMessage(withObjectMessages publicObjectMessages: [any _AblyPluginSupportPrivate.ObjectMessageProtocol], protocolMessageChannelSerial: String?, channel: _AblyPluginSupportPrivate.RealtimeChannel) {
122124
guard let inboundObjectMessageBoxes = publicObjectMessages as? [ObjectMessageBox<InboundObjectMessage>] else {
123125
preconditionFailure("Expected to receive the same InboundObjectMessage type as we emit")
124126
}
125127

126128
let objectMessages = inboundObjectMessageBoxes.map(\.objectMessage)
127129

128-
realtimeObjects(for: channel).handleObjectSyncProtocolMessage(
130+
nosync_realtimeObjects(for: channel).nosync_handleObjectSyncProtocolMessage(
129131
objectMessages: objectMessages,
130132
protocolMessageChannelSerial: protocolMessageChannelSerial,
131133
)
@@ -145,10 +147,13 @@ internal final class DefaultInternalPlugin: NSObject, _AblyPluginSupportPrivate.
145147
let internalQueue = pluginAPI.internalQueue(for: client)
146148

147149
internalQueue.async {
148-
pluginAPI.sendObject(
150+
pluginAPI.nosync_sendObject(
149151
withObjectMessages: objectMessageBoxes,
150152
channel: channel,
151153
) { error in
154+
// We don't currently rely on this documented behaviour of `nosync_sendObject` but we may do later, so assert it to be sure it's happening.
155+
dispatchPrecondition(condition: .onQueue(internalQueue))
156+
152157
if let error {
153158
continuation.resume(returning: .failure(error.toInternalError()))
154159
} else {

0 commit comments

Comments
 (0)