Skip to content

Commit 3ff7417

Browse files
committed
simplify reconnection logic by always reconnecting from Monitor
1 parent 660dd34 commit 3ff7417

File tree

4 files changed

+63
-128
lines changed

4 files changed

+63
-128
lines changed

Sources/SwiftOCA/OCP.1/Ocp1Connection+Connect.swift

Lines changed: 23 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import OpenCombine
2727
import SystemPackage
2828

2929
private extension Ocp1Error {
30-
var connectionState: Ocp1ConnectionState? {
30+
var ocp1ConnectionState: Ocp1ConnectionState? {
3131
switch self {
3232
case .notConnected:
3333
.notConnected
@@ -75,7 +75,7 @@ private extension Errno {
7575

7676
private extension Error {
7777
var ocp1ConnectionState: Ocp1ConnectionState {
78-
(self as? Ocp1Error)?.connectionState ?? .connectionFailed
78+
(self as? Ocp1Error)?.ocp1ConnectionState ?? .connectionFailed
7979
}
8080

8181
var _isRecoverableConnectionError: Bool {
@@ -109,7 +109,8 @@ private extension Ocp1ConnectionState {
109109
extension Ocp1Connection {
110110
/// start receiveMessages/keepAlive monitor task
111111
private func _startMonitor() {
112-
let monitor = Monitor(self)
112+
connectionID &+= 1
113+
let monitor = Monitor(self, id: connectionID)
113114
monitorTask = Task {
114115
try await monitor.run()
115116
}
@@ -160,18 +161,20 @@ extension Ocp1Connection {
160161
}
161162
}
162163

163-
func updateConnectionState(_ connectionState: Ocp1ConnectionState) {
164+
private func _updateConnectionState(_ connectionState: Ocp1ConnectionState) {
164165
logger.trace("_updateConnectionState: \(_connectionState.value) => \(connectionState)")
165-
if connectionState == .connected {
166-
logger.info("connected to \(self)")
167-
}
168166
_connectionState.send(connectionState)
169167
}
170168

169+
func markConnectionConnected() {
170+
logger.info("connected to \(self)")
171+
_updateConnectionState(.connected)
172+
}
173+
171174
private func _didConnectDevice() async throws {
172175
if !isDatagram {
173176
// otherwise, set connected state when we receive first keepAlive PDU
174-
updateConnectionState(.connected)
177+
markConnectionConnected()
175178
}
176179

177180
_startMonitor()
@@ -190,13 +193,13 @@ extension Ocp1Connection {
190193
}
191194

192195
public func connect() async throws {
193-
updateConnectionState(.connecting)
196+
_updateConnectionState(.connecting)
194197

195198
do {
196199
try await _connectDeviceWithTimeout()
197200
} catch {
198201
logger.debug("connection failed: \(error)")
199-
updateConnectionState(error.ocp1ConnectionState)
202+
_updateConnectionState(error.ocp1ConnectionState)
200203
throw error
201204
}
202205

@@ -243,7 +246,7 @@ extension Ocp1Connection {
243246
public func disconnect() async throws {
244247
await removeSubscriptions()
245248

246-
updateConnectionState(.notConnected)
249+
_updateConnectionState(.notConnected)
247250

248251
let clearObjectCache = !options.flags.contains(.retainObjectCacheAfterDisconnect)
249252
try await _disconnectDevice(clearObjectCache: clearObjectCache)
@@ -253,32 +256,8 @@ extension Ocp1Connection {
253256
// MARK: - reconnection handling
254257

255258
extension Ocp1Connection {
256-
enum ReconnectionPolicy {
257-
/// do not try to automatically reconnect on connection failure
258-
case noReconnect
259-
/// try to reconnect in the keepAlive monitor task
260-
case reconnectInMonitor
261-
/// try to reconnect before sending the next message
262-
case reconnectOnSend
263-
}
264-
265-
///
266-
/// Re-connection logic is as follows:
267-
///
268-
/// * If the connection has a heartbeat, then automatic reconnection is only
269-
/// managed in the / heartbeat task
270-
///
271-
/// * If the connection does not have a heartbeat, than automatic
272-
/// reconnection is managed when / sending a PDU
273-
///
274-
private var _reconnectionPolicy: ReconnectionPolicy {
275-
if !options.flags.contains(.automaticReconnect) {
276-
.noReconnect
277-
} else if heartbeatTime == .zero {
278-
.reconnectOnSend
279-
} else {
280-
.reconnectInMonitor
281-
}
259+
private var _automaticReconnect: Bool {
260+
options.flags.contains(.automaticReconnect)
282261
}
283262

284263
/// reconnect to the OCA device with exponential backoff, updating
@@ -287,7 +266,7 @@ extension Ocp1Connection {
287266
var lastError: Error?
288267
var backoff: Duration = options.reconnectPauseInterval
289268

290-
updateConnectionState(.reconnecting)
269+
_updateConnectionState(.reconnecting)
291270

292271
logger
293272
.trace(
@@ -313,69 +292,22 @@ extension Ocp1Connection {
313292

314293
if let lastError {
315294
logger.debug("reconnection abandoned: \(lastError)")
316-
updateConnectionState(lastError.ocp1ConnectionState)
295+
_updateConnectionState(lastError.ocp1ConnectionState)
317296
throw lastError
318297
} else if !isDatagram && !isConnected {
319298
logger.trace("reconnection abandoned after too many tries")
320-
updateConnectionState(.notConnected)
299+
_updateConnectionState(.notConnected)
321300
throw Ocp1Error.notConnected
322301
}
323302
}
324303

325-
private var _needsReconnectOnSend: Bool {
326-
guard _reconnectionPolicy == .reconnectOnSend else { return false }
304+
func onMonitorError(id: Int, _ error: Error) async throws {
305+
_updateConnectionState(error.ocp1ConnectionState)
327306

328-
switch _connectionState.value {
329-
case .notConnected:
330-
fallthrough
331-
case .connectionTimedOut:
332-
fallthrough
333-
case .connectionFailed:
334-
return true
335-
default:
336-
return false
337-
}
338-
}
339-
340-
func willSendMessage() async throws {
341-
guard _needsReconnectOnSend else { return }
342-
try await reconnectDeviceWithBackoff()
343-
}
344-
345-
func didSendMessage(error: Ocp1Error? = nil) async throws {
346-
if error == nil {
347-
lastMessageSentTime = Monitor.now
348-
}
349-
350-
if _reconnectionPolicy != .reconnectInMonitor, let error,
351-
let connectionState = error.connectionState
352-
{
353-
logger
354-
.debug(
355-
"failed to send message: error \(error), new connection state \(connectionState); disconnecting"
356-
)
357-
if isConnected {
358-
updateConnectionState(connectionState)
359-
try await _disconnectDeviceAfterConnectionFailure()
360-
}
361-
}
362-
}
363-
364-
private func _onMonitorError(_ error: Error) async throws {
365-
updateConnectionState(error.ocp1ConnectionState)
366-
367-
if error._isRecoverableConnectionError, _reconnectionPolicy == .reconnectInMonitor {
368-
logger.trace("expiring connection after receiving error \(error)")
307+
if _automaticReconnect, error._isRecoverableConnectionError {
308+
logger.trace("monitor task \(id) disconnecting: \(error)")
369309
try await _disconnectDeviceAfterConnectionFailure()
370310
Task.detached { try await self.reconnectDeviceWithBackoff() }
371311
}
372312
}
373-
374-
func onKeepAliveMonitorError(_ error: Error) async throws {
375-
try await _onMonitorError(error)
376-
}
377-
378-
func onReceiveMessageMonitorError(_ error: Error) async throws {
379-
try await _onMonitorError(error)
380-
}
381313
}

Sources/SwiftOCA/OCP.1/Ocp1Connection+Messages.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,9 @@ extension Ocp1Connection {
2323
) async throws {
2424
let messagePduData = try Self.encodeOcp1MessagePdu(messages, type: messageType)
2525

26-
try await willSendMessage()
27-
28-
do {
29-
guard try await write(messagePduData) == messagePduData.count else {
30-
throw Ocp1Error.pduSendingFailed
31-
}
32-
} catch let error as Ocp1Error {
33-
try await didSendMessage(error: error)
34-
throw error
26+
guard try await write(messagePduData) == messagePduData.count else {
27+
throw Ocp1Error.pduSendingFailed
3528
}
36-
37-
try await didSendMessage()
3829
}
3930

4031
private func sendMessage(

Sources/SwiftOCA/OCP.1/Ocp1Connection.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public enum Ocp1ConnectionState: OcaUint8, Codable, Sendable {
141141

142142
public struct Ocp1ConnectionStatistics: Sendable {
143143
public let connectionState: Ocp1ConnectionState
144+
public let connectionID: Int
144145
public var isConnected: Bool { connectionState == .connected }
145146
public let requestCount: Int
146147
public let outstandingRequests: [OcaUint32]
@@ -200,6 +201,7 @@ open class Ocp1Connection: CustomStringConvertible, ObservableObject {
200201

201202
var subscriptions = [OcaEvent: EventSubscriptions]()
202203
var logger = Logger(label: "com.padl.SwiftOCA")
204+
var connectionID = 0
203205

204206
private var nextCommandHandle = CommandHandleBase
205207

@@ -214,6 +216,7 @@ open class Ocp1Connection: CustomStringConvertible, ObservableObject {
214216
public var statistics: Ocp1ConnectionStatistics {
215217
Ocp1ConnectionStatistics(
216218
connectionState: _connectionState.value,
219+
connectionID: connectionID,
217220
requestCount: Int(nextCommandHandle - CommandHandleBase),
218221
outstandingRequests: monitor?.outstandingRequests ?? [],
219222
cachedObjectCount: objects.count,
@@ -225,19 +228,21 @@ open class Ocp1Connection: CustomStringConvertible, ObservableObject {
225228
}
226229

227230
/// Monitor structure for matching requests and responses
228-
final class Monitor: @unchecked Sendable {
231+
final class Monitor: @unchecked Sendable, CustomStringConvertible {
229232
typealias Continuation = CheckedContinuation<Ocp1Response, Error>
230233

231234
private let _connection: Weak<Ocp1Connection>
235+
let _connectionID: Int
232236
private let _continuations = ManagedCriticalState<[OcaUint32: Continuation]>([:])
233237
private var _lastMessageReceivedTime = ManagedAtomic<UInt64>(0)
234238

235239
static var now: UInt64 {
236240
UInt64(time(nil))
237241
}
238242

239-
init(_ connection: Ocp1Connection) {
243+
init(_ connection: Ocp1Connection, id: Int) {
240244
_connection = Weak(connection)
245+
_connectionID = id
241246
updateLastMessageReceivedTime()
242247
}
243248

@@ -302,6 +307,13 @@ open class Ocp1Connection: CustomStringConvertible, ObservableObject {
302307
var lastMessageReceivedTime: UInt64 {
303308
_lastMessageReceivedTime.load(ordering: .relaxed)
304309
}
310+
311+
var description: String {
312+
let connectionString: String = if let connection { connection.description }
313+
else { "<null>" }
314+
315+
return "\(connectionString)[\(_connectionID)]"
316+
}
305317
}
306318

307319
/// actor for monitoring response and matching them with requests

Sources/SwiftOCA/OCP.1/Ocp1ConnectionMonitor.swift

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ extension Ocp1Connection.Monitor {
9191
@OcaConnection
9292
private func _markDatagramConnectionConnected(_ connection: Ocp1Connection) async {
9393
if connection.isDatagram, connection.isConnecting {
94-
connection.updateConnectionState(.connected)
94+
connection.markConnectionConnected()
9595
}
9696
}
9797

@@ -136,33 +136,33 @@ extension Ocp1Connection.Monitor {
136136
}
137137

138138
func receiveMessages(_ connection: Ocp1Connection) async throws {
139-
try await withThrowingTaskGroup(of: Void.self) { @OcaConnection group in
140-
group.addTask { [self] in
141-
repeat {
142-
try Task.checkCancellation()
143-
do {
144-
try await receiveMessage(connection)
145-
} catch Ocp1Error.unknownPduType {
146-
} catch Ocp1Error.invalidHandle {
147-
} catch {
148-
guard await !connection.isConnecting else { continue }
149-
try await connection.onReceiveMessageMonitorError(error)
150-
throw error
151-
}
152-
} while true
153-
}
154-
if connection.heartbeatTime > .zero {
155-
group.addTask(priority: .background) { [self] in
156-
do {
139+
do {
140+
try await withThrowingTaskGroup(of: Void.self) { @OcaConnection group in
141+
group.addTask { [self] in
142+
repeat {
143+
try Task.checkCancellation()
144+
do {
145+
try await receiveMessage(connection)
146+
} catch Ocp1Error.unknownPduType {
147+
} catch Ocp1Error.invalidHandle {}
148+
} while true
149+
}
150+
if connection.heartbeatTime > .zero {
151+
group.addTask(priority: .background) { [self] in
157152
try await keepAlive(connection)
158-
} catch {
159-
try await connection.onKeepAliveMonitorError(error)
160-
throw error
161153
}
162154
}
155+
try await group.next()
156+
group.cancelAll()
157+
}
158+
} catch {
159+
// if we're not already in the middle of connecting or re-connecting,
160+
// possibly trigger a reconnect depending on the autoReconnect policy
161+
// and the nature of the error
162+
if await !connection.isConnecting {
163+
try? await connection.onMonitorError(id: _connectionID, error)
163164
}
164-
try await group.next()
165-
group.cancelAll()
165+
throw error
166166
}
167167
}
168168
}

0 commit comments

Comments
 (0)