Skip to content

Commit d55ee3f

Browse files
authored
[FSSDK-9061] fix retries on odp event failures (#487)
- Add support for retries (up to 3 times) when ODP event dispatch fails on connection or server errors. ODP events are discarded after max retries. - Check reachability before dispatching to avoid discarding all events in the queue when network is down.
1 parent 8148e32 commit d55ee3f

File tree

7 files changed

+228
-59
lines changed

7 files changed

+228
-59
lines changed

.github/workflows/swift.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ jobs:
4747
4848
unittests:
4949
if: "${{ github.event.inputs.PREP == '' && github.event.inputs.RELEASE == '' }}"
50-
##uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@master
51-
uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@jae/empty-odp-action
50+
uses: optimizely/swift-sdk/.github/workflows/unit_tests.yml@master
5251

5352
prepare_for_release:
5453
runs-on: macos-12

Sources/Customization/DefaultEventDispatcher.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher {
207207
let config = URLSessionConfiguration.ephemeral
208208
return URLSession(configuration: config)
209209
}
210+
211+
// MARK: - Tests
212+
213+
open func close() {
214+
self.flushEvents()
215+
self.queueLock.sync {}
216+
}
210217

211218
}
212219

@@ -258,11 +265,4 @@ extension DefaultEventDispatcher {
258265
timer.property = nil
259266
}
260267

261-
// MARK: - Tests
262-
263-
open func close() {
264-
self.flushEvents()
265-
self.queueLock.sync {}
266-
}
267-
268268
}

Sources/ODP/OdpEventManager.swift

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ open class OdpEventManager {
2323

2424
var maxQueueSize = 100
2525
let maxBatchEvents = 10
26+
let maxFailureCount = 3
2627
let queueLock: DispatchQueue
2728
let eventQueue: DataStoreQueueStackImpl<OdpEvent>
2829

30+
let reachability = NetworkReachability(maxContiguousFails: 1)
2931
let logger = OPTLoggerFactory.getLogger()
3032

3133
/// OdpEventManager init
@@ -124,6 +126,12 @@ open class OdpEventManager {
124126
return
125127
}
126128

129+
// check if network is down to avoid that all existing events in the queue get discarded when network is down
130+
if reachability.shouldBlockNetworkAccess() {
131+
logger.e(.eventDispatchFailed("NetworkReachability down for ODP events"))
132+
return
133+
}
134+
127135
queueLock.async {
128136
func removeStoredEvents(num: Int) {
129137
if let removedItem = self.eventQueue.removeFirstItems(count: num), removedItem.count > 0 {
@@ -137,6 +145,7 @@ open class OdpEventManager {
137145
// sync group used to ensure that the sendEvent is synchronous.
138146
// used in flushEvents
139147
let sync = DispatchGroup()
148+
var failureCount = 0
140149

141150
while let events: [OdpEvent] = self.eventQueue.getFirstItems(count: self.maxBatchEvents) {
142151
let numEvents = events.count
@@ -151,31 +160,36 @@ open class OdpEventManager {
151160
self.apiMgr.sendOdpEvents(apiKey: odpApiKey,
152161
apiHost: odpApiHost,
153162
events: events) { error in
163+
if let error = error {
164+
self.logger.e(error.reason)
165+
}
166+
154167
odpError = error
155168
sync.leave() // our send is done.
156169
}
157170
sync.wait() // wait for send completed
158171

159-
if let error = odpError {
160-
self.logger.e(error.reason)
172+
// retry only for recoverable errors (connection failures or 5xx server errors)
173+
if let error = odpError, case .odpEventFailed(_, let canRetry) = error, canRetry {
174+
failureCount += 1
161175

162-
// retry only if needed (non-permanent)
163-
if case .odpEventFailed(_, let canRetry) = error {
164-
if canRetry {
165-
// keep the failed event queue so it can be re-sent later
166-
break
167-
} else {
168-
// permanent errors (400 response or invalid events, etc)
169-
// discard these events so that they do not block following valid events
170-
}
176+
if failureCount >= self.maxFailureCount {
177+
self.logger.e(.odpEventSendRetyFailed(failureCount))
178+
failureCount = 0
171179
}
180+
} else { // success or non-recoverable errors
181+
failureCount = 0
182+
}
183+
184+
if failureCount == 0 {
185+
removeStoredEvents(num: numEvents)
172186
}
173187

174-
removeStoredEvents(num: numEvents)
188+
self.reachability.updateNumContiguousFails(isError: odpError != nil)
175189
}
176190
}
177191
}
178-
192+
179193
func reset() {
180194
_ = eventQueue.removeFirstItems(count: self.maxQueueSize)
181195
}

Sources/Utils/LogMessage.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ enum LogMessage {
6363
case unrecognizedAttribute(_ key: String)
6464
case eventBatchFailed
6565
case eventSendRetyFailed(_ count: Int)
66+
case odpEventSendRetyFailed(_ count: Int)
6667
case failedToConvertMapToString
6768
case failedToAssignValue
6869
case valueForKeyNotFound(_ key: String)
@@ -123,8 +124,9 @@ extension LogMessage: CustomStringConvertible {
123124
case .unrecognizedAttribute(let key): message = "Unrecognized attribute (\(key)) provided. Pruning before sending event to Optimizely."
124125
case .eventBatchFailed: message = "Failed to batch events"
125126
case .eventSendRetyFailed(let count): message = "Event dispatch retries failed (\(count)) times"
127+
case .odpEventSendRetyFailed(let count): message = "ODP event dispatch retries failed (\(count)) times"
126128
case .failedToConvertMapToString: message = "Provided map could not be converted to string."
127-
case .failedToAssignValue: message = "Value for path could not be assigned to provided type."
129+
case .failedToAssignValue: message = "Value for path could not be assigned to provided type."
128130
case .valueForKeyNotFound(let key): message = "Value for JSON key (\(key)) not found."
129131
}
130132

0 commit comments

Comments
 (0)