Skip to content

Commit 25440f1

Browse files
authored
feature(Reachability): add reachability checking for network connections (#439)
- Add network reachability for datafile fetch and event dispatching. - Reachability is checked once the first request fails.
1 parent 60d5123 commit 25440f1

File tree

9 files changed

+524
-49
lines changed

9 files changed

+524
-49
lines changed

DemoSwiftApp/DemoSwiftApp.xcodeproj/project.pbxproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@
560560
TargetAttributes = {
561561
252D7DEC21C8800800134A7A = {
562562
CreatedOnToolsVersion = 10.1;
563+
DevelopmentTeam = BDMC9C2X5M;
563564
ProvisioningStyle = Automatic;
564565
SystemCapabilities = {
565566
com.apple.BackgroundModes = {
@@ -953,6 +954,7 @@
953954
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
954955
CODE_SIGN_IDENTITY = "iPhone Developer";
955956
CODE_SIGN_STYLE = Automatic;
957+
DEVELOPMENT_TEAM = BDMC9C2X5M;
956958
GCC_C_LANGUAGE_STANDARD = gnu11;
957959
GCC_PREPROCESSOR_DEFINITIONS = (
958960
"DEBUG=1",
@@ -983,6 +985,7 @@
983985
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
984986
CODE_SIGN_IDENTITY = "iPhone Developer";
985987
CODE_SIGN_STYLE = Automatic;
988+
DEVELOPMENT_TEAM = BDMC9C2X5M;
986989
GCC_C_LANGUAGE_STANDARD = gnu11;
987990
INFOPLIST_FILE = "$(SRCROOT)/DemoSwiftiOS/Info.plist";
988991
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 40 additions & 0 deletions
Large diffs are not rendered by default.

Sources/Customization/DefaultDatafileHandler.swift

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ open class DefaultDatafileHandler: OPTDatafileHandler {
3636

3737
// and our download queue to speed things up.
3838
let downloadQueue = DispatchQueue(label: "DefaultDatafileHandlerQueue")
39+
40+
// network reachability
41+
let reachability = NetworkReachability(maxContiguousFails: 1)
3942

4043
public required init() {}
4144

@@ -47,45 +50,59 @@ open class DefaultDatafileHandler: OPTDatafileHandler {
4750
completionHandler: @escaping DatafileDownloadCompletionHandler) {
4851

4952
downloadQueue.async {
53+
54+
func returnCached(_ result: OptimizelyResult<Data?>? = nil) -> OptimizelyResult<Data?> {
55+
if let data = self.loadSavedDatafile(sdkKey: sdkKey) {
56+
return .success(data)
57+
} else {
58+
return result ?? .failure(.datafileLoadingFailed(sdkKey))
59+
}
60+
}
61+
62+
if self.reachability.shouldBlockNetworkAccess() {
63+
let optError = OptimizelyError.datafileDownloadFailed("NetworkReachability down")
64+
self.logger.e(optError)
65+
66+
let result = OptimizelyResult<Data?>.failure(optError)
67+
completionHandler(returnCached(result))
68+
return
69+
}
70+
5071
let session = self.getSession(resourceTimeoutInterval: resourceTimeoutInterval)
5172

5273
guard let request = self.getRequest(sdkKey: sdkKey) else { return }
5374

5475
let task = session.downloadTask(with: request) { (url, response, error) in
55-
var result = OptimizelyResult<Data?>.failure(.datafileLoadingFailed(sdkKey))
56-
57-
let returnCached = {
58-
if let data = self.loadSavedDatafile(sdkKey: sdkKey) {
59-
result = .success(data)
60-
}
61-
}
76+
var result = OptimizelyResult<Data?>.failure(.generic)
6277

6378
if error != nil {
64-
self.logger.e(error.debugDescription)
65-
result = .failure(.datafileDownloadFailed(error.debugDescription))
66-
returnCached() // error recovery
79+
let optError = OptimizelyError.datafileDownloadFailed(error.debugDescription)
80+
self.logger.e(optError)
81+
result = returnCached(.failure(optError)) // error recovery
6782
} else if let response = response as? HTTPURLResponse {
6883
switch response.statusCode {
6984
case 200:
7085
if let data = self.getResponseData(sdkKey: sdkKey, response: response, url: url) {
7186
result = .success(data)
7287
} else {
73-
returnCached() // error recovery
88+
result = returnCached() // error recovery
7489
}
7590
case 304:
7691
self.logger.d("The datafile was not modified and won't be downloaded again")
7792

7893
if returnCacheIfNoChange {
79-
returnCached()
94+
result = returnCached()
8095
} else {
8196
result = .success(nil)
8297
}
8398
default:
8499
self.logger.i("got response code \(response.statusCode)")
85-
returnCached() // error recovery
100+
result = returnCached() // error recovery
86101
}
87102
}
88103

104+
self.reachability.updateNumContiguousFails(isError: (error != nil))
105+
89106
completionHandler(result)
90107
}
91108

@@ -244,6 +261,24 @@ extension DefaultDatafileHandler {
244261
updateInterval: Int,
245262
datafileChangeNotification: ((Data) -> Void)?) {
246263
let beginDownloading = Date()
264+
265+
let scheduleNextUpdate: () -> Void = {
266+
guard self.hasPeriodicInterval(sdkKey: sdkKey) else { return }
267+
268+
// adjust the next fire time so that events will be fired at fixed interval regardless of the download latency
269+
// if latency is too big (or returning from background mode), fire the next event immediately once
270+
271+
var interval = self.timers.property?[sdkKey]?.interval ?? updateInterval
272+
let delay = Int(Date().timeIntervalSince(beginDownloading))
273+
interval -= delay
274+
if interval < 0 {
275+
interval = 0
276+
}
277+
278+
self.logger.d("next datafile download is \(interval) seconds \(Date())")
279+
self.startPeriodicUpdates(sdkKey: sdkKey, updateInterval: interval, datafileChangeNotification: datafileChangeNotification)
280+
}
281+
247282
self.downloadDatafile(sdkKey: sdkKey) { (result) in
248283
switch result {
249284
case .success(let data):
@@ -255,20 +290,7 @@ extension DefaultDatafileHandler {
255290
self.logger.e(error.reason)
256291
}
257292

258-
if self.hasPeriodicInterval(sdkKey: sdkKey) {
259-
// adjust the next fire time so that events will be fired at fixed interval regardless of the download latency
260-
// if latency is too big (or returning from background mode), fire the next event immediately once
261-
262-
var interval = self.timers.property?[sdkKey]?.interval ?? updateInterval
263-
let delay = Int(Date().timeIntervalSince(beginDownloading))
264-
interval -= delay
265-
if interval < 0 {
266-
interval = 0
267-
}
268-
269-
self.logger.d("next datafile download is \(interval) seconds \(Date())")
270-
self.startPeriodicUpdates(sdkKey: sdkKey, updateInterval: interval, datafileChangeNotification: datafileChangeNotification)
271-
}
293+
scheduleNextUpdate()
272294
}
273295
}
274296

Sources/Customization/DefaultEventDispatcher.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher {
5151
var timer = AtomicProperty<Timer>()
5252
var observers = [NSObjectProtocol]()
5353

54+
// network reachability
55+
let reachability = NetworkReachability(maxContiguousFails: 1)
56+
5457
public init(batchSize: Int = DefaultValues.batchSize,
5558
backingStore: DataStoreType = .file,
5659
dataStoreName: String = "OPTEventQueue",
@@ -143,8 +146,8 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher {
143146
}
144147

145148
// we've exhuasted our failure count. Give up and try the next time a event
146-
// is queued or someone calls flush.
147-
if failureCount > DefaultValues.maxFailureCount {
149+
// is queued or someone calls flush (changed to >= so that retried exactly "maxFailureCount" times).
150+
if failureCount >= DefaultValues.maxFailureCount {
148151
self.logger.e(.eventSendRetyFailed(failureCount))
149152
break
150153
}
@@ -174,8 +177,13 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher {
174177
}
175178

176179
open func sendEvent(event: EventForDispatch, completionHandler: @escaping DispatchCompletionHandler) {
177-
let config = URLSessionConfiguration.ephemeral
178-
let session = URLSession(configuration: config)
180+
181+
if self.reachability.shouldBlockNetworkAccess() {
182+
completionHandler(.failure(.eventDispatchFailed("NetworkReachability down")))
183+
return
184+
}
185+
186+
let session = getSession()
179187
var request = URLRequest(url: event.url)
180188
request.httpMethod = "POST"
181189
request.httpBody = event.body
@@ -191,11 +199,18 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher {
191199
self.logger.d("Event Sent")
192200
completionHandler(.success(event.body))
193201
}
202+
203+
self.reachability.updateNumContiguousFails(isError: (error != nil))
194204
}
195205

196206
task.resume()
197207
}
198208

209+
func getSession() -> URLSession {
210+
let config = URLSessionConfiguration.ephemeral
211+
return URLSession(configuration: config)
212+
}
213+
199214
}
200215

201216
// MARK: - internals
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//
2+
// Copyright 2021, Optimizely, Inc. and contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import Foundation
18+
import Network
19+
20+
class NetworkReachability {
21+
22+
var monitor: AnyObject?
23+
let queue = DispatchQueue(label: "reachability")
24+
25+
// the number of contiguous download failures (reachability)
26+
var numContiguousFails = 0
27+
// the maximum number of contiguous network connection failures allowed before reachability checking
28+
var maxContiguousFails: Int
29+
static let defaultMaxContiguousFails = 1
30+
31+
#if targetEnvironment(simulator)
32+
private var connected = false // initially false for testing support
33+
#else
34+
private var connected = true // initially true for safety in production
35+
#endif
36+
37+
var isConnected: Bool {
38+
get {
39+
var result = false
40+
queue.sync {
41+
result = connected
42+
}
43+
return result
44+
}
45+
// for test support only
46+
set {
47+
queue.sync {
48+
connected = newValue
49+
}
50+
}
51+
}
52+
53+
init(maxContiguousFails: Int? = nil) {
54+
self.maxContiguousFails = maxContiguousFails ?? NetworkReachability.defaultMaxContiguousFails
55+
56+
if #available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *) {
57+
58+
// NOTE: test with real devices only (simulator not updating properly)
59+
60+
self.monitor = NWPathMonitor()
61+
62+
(monitor as! NWPathMonitor).pathUpdateHandler = { [weak self] (path: NWPath) -> Void in
63+
// "Reachability path: satisfied (Path is satisfied), interface: en0, ipv4, ipv6, dns, expensive, constrained"
64+
// "Reachability path: unsatisfied (No network route)"
65+
// print("Reachability path: \(path)")
66+
67+
// this task runs in sync queue. set private variable (instead of isConnected to avoid deadlock)
68+
self?.connected = (path.status == .satisfied)
69+
}
70+
71+
(monitor as! NWPathMonitor).start(queue: queue)
72+
}
73+
}
74+
75+
func stop() {
76+
if #available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *) {
77+
guard let monitor = monitor as? NWPathMonitor else { return }
78+
79+
monitor.pathUpdateHandler = nil
80+
monitor.cancel()
81+
}
82+
}
83+
84+
func updateNumContiguousFails(isError: Bool) {
85+
numContiguousFails = isError ? (numContiguousFails + 1) : 0
86+
}
87+
88+
/// Skip network access when reachability is down (optimization for iOS12+ only)
89+
/// - Returns: true when network access should be blocked
90+
func shouldBlockNetworkAccess() -> Bool {
91+
if numContiguousFails < maxContiguousFails { return false }
92+
93+
if #available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *) {
94+
return !isConnected
95+
} else {
96+
return false
97+
}
98+
}
99+
100+
}

0 commit comments

Comments
 (0)