Skip to content

Commit eaa9a50

Browse files
author
Clément Le Provost
authored
[offline] Use network reachability to decide whether to perform online requests (#168)
* [offline] Use network reachability to decide whether to perform online requests The system’s behavior has apparently changed. Network requests used to fail fast when the device was offline. Now, they just hit the timeout, which of course is an entirely different story. Therefore we use `SCNetworkReachability` to test network connectivity and launch online requests based on the result. * Use network reachability also in the online client All online requests will now fail fast if reachability says that the device is offline, unless the developer disables this behavior by setting `AbstractClient.useReachability` to `false`. NOTE: As this feature uses the System Configuration framework, it is not available on watchOS. The behavior remains unchanged on that platform.
1 parent 198a0be commit eaa9a50

File tree

5 files changed

+124
-2
lines changed

5 files changed

+124
-2
lines changed

AlgoliaSearch.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
BC0A015B1C9BEDB200CD4A7C /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0A01591C9BEDB200CD4A7C /* Error.swift */; };
4040
BC0A01641C9C19CD00CD4A7C /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0A01631C9C19CD00CD4A7C /* QueryTests.swift */; };
4141
BC0A01651C9C19CD00CD4A7C /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC0A01631C9C19CD00CD4A7C /* QueryTests.swift */; };
42+
BC15FAC11E01C041005E3B56 /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC15FAC01E01C041005E3B56 /* NetworkReachability.swift */; };
4243
BC23A6F41D63541500DF9034 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC4A7F381CB5308100AF1DCB /* AsyncOperation.swift */; };
4344
BC23A6F51D63541500DF9034 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D67400E1B4AA22600B326EC /* Cache.swift */; };
4445
BC23A6F61D63541500DF9034 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D7495A01A8E277400B0263F /* Client.swift */; };
@@ -50,6 +51,9 @@
5051
BC23A6FC1D63541500DF9034 /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D7495A61A8E499B00B0263F /* Query.swift */; };
5152
BC23A6FD1D63541500DF9034 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC01E66C1CA43CEE0067670B /* Request.swift */; };
5253
BC23A6FE1D63541B00DF9034 /* BrowseIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC09F7761CAE743900ABB395 /* BrowseIterator.swift */; };
54+
BC3DC7521E02DD7900862F5B /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC15FAC01E01C041005E3B56 /* NetworkReachability.swift */; };
55+
BC3DC7531E02DD7A00862F5B /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC15FAC01E01C041005E3B56 /* NetworkReachability.swift */; };
56+
BC3DC7541E02DD7B00862F5B /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC15FAC01E01C041005E3B56 /* NetworkReachability.swift */; };
5357
BC4A7F391CB5308100AF1DCB /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC4A7F381CB5308100AF1DCB /* AsyncOperation.swift */; };
5458
BC4A7F3A1CB5308100AF1DCB /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC4A7F381CB5308100AF1DCB /* AsyncOperation.swift */; };
5559
BC4A7F3C1CB5373E00AF1DCB /* CancelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC4A7F3B1CB5373E00AF1DCB /* CancelTests.swift */; };
@@ -207,6 +211,7 @@
207211
BC0A01611C9BEDE000CD4A7C /* MirrorSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirrorSettings.swift; sourceTree = "<group>"; };
208212
BC0A01621C9BEDE000CD4A7C /* OfflineClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineClient.swift; sourceTree = "<group>"; };
209213
BC0A01631C9C19CD00CD4A7C /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = "<group>"; };
214+
BC15FAC01E01C041005E3B56 /* NetworkReachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkReachability.swift; sourceTree = "<group>"; };
210215
BC23A6EC1D63539A00DF9034 /* AlgoliaSearch.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AlgoliaSearch.framework; sourceTree = BUILT_PRODUCTS_DIR; };
211216
BC4A7F381CB5308100AF1DCB /* AsyncOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = "<group>"; };
212217
BC4A7F3B1CB5373E00AF1DCB /* CancelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancelTests.swift; sourceTree = "<group>"; };
@@ -360,6 +365,7 @@
360365
5DECA2D91A960BC5001A6088 /* Index.swift */,
361366
BC68BC561CB69776007F9655 /* IndexQuery.swift */,
362367
5D09E1DB1AC0773A00B799A6 /* Network.swift */,
368+
BC15FAC01E01C041005E3B56 /* NetworkReachability.swift */,
363369
BC4DDEFB1DD11527004D9A6E /* PlacesClient.swift */,
364370
BC4DDF071DD12BCC004D9A6E /* PlacesQuery.swift */,
365371
5D7495A61A8E499B00B0263F /* Query.swift */,
@@ -877,6 +883,7 @@
877883
5D67400F1B4AA22600B326EC /* Cache.swift in Sources */,
878884
BC4DDF081DD12BCC004D9A6E /* PlacesQuery.swift in Sources */,
879885
BC4DDEF61DD10EBA004D9A6E /* Types.swift in Sources */,
886+
BC3DC7541E02DD7B00862F5B /* NetworkReachability.swift in Sources */,
880887
BC4A7F391CB5308100AF1DCB /* AsyncOperation.swift in Sources */,
881888
5DECA2DA1A960BC5001A6088 /* Index.swift in Sources */,
882889
BC4DDEFC1DD11527004D9A6E /* PlacesClient.swift in Sources */,
@@ -920,6 +927,7 @@
920927
5D6740101B4AA22600B326EC /* Cache.swift in Sources */,
921928
BC4DDF091DD12BCC004D9A6E /* PlacesQuery.swift in Sources */,
922929
BC4DDEF71DD10EBA004D9A6E /* Types.swift in Sources */,
930+
BC3DC7531E02DD7A00862F5B /* NetworkReachability.swift in Sources */,
923931
BC4A7F3A1CB5308100AF1DCB /* AsyncOperation.swift in Sources */,
924932
5DB2515C1AAD9F3300945339 /* Helpers.swift in Sources */,
925933
BC4DDEFD1DD11527004D9A6E /* PlacesClient.swift in Sources */,
@@ -982,6 +990,7 @@
982990
isa = PBXSourcesBuildPhase;
983991
buildActionMask = 2147483647;
984992
files = (
993+
BC15FAC11E01C041005E3B56 /* NetworkReachability.swift in Sources */,
985994
BC4BD2821D54E48900170ECC /* Error.swift in Sources */,
986995
BCD57D261D89970D00C5DE68 /* MultipleQueryEmulator.swift in Sources */,
987996
BC4BD2831D54E48900170ECC /* IndexQuery.swift in Sources */,
@@ -1016,6 +1025,7 @@
10161025
BCD1F5621CC61D020006E227 /* Cache.swift in Sources */,
10171026
BC4DDF0A1DD12BCC004D9A6E /* PlacesQuery.swift in Sources */,
10181027
BC4DDEF81DD10EBA004D9A6E /* Types.swift in Sources */,
1028+
BC3DC7521E02DD7900862F5B /* NetworkReachability.swift in Sources */,
10191029
BCD1F56C1CC61D2D0006E227 /* BrowseIterator.swift in Sources */,
10201030
BCD1F5681CC61D160006E227 /* Network.swift in Sources */,
10211031
BC4DDEFE1DD11527004D9A6E /* PlacesClient.swift in Sources */,

Source/AbstractClient.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,24 @@ internal struct HostStatus {
185185
/// The default timeout for host statuses.
186186
@objc public static let defaultHostStatusTimeout: TimeInterval = 5 * 60
187187

188+
#if !os(watchOS)
189+
190+
/// Network reachability tester.
191+
internal let reachability = NetworkReachability()
192+
193+
/// Whether to use network reachability to decide if online requests should be attempted.
194+
///
195+
/// - When `true` (default), if the network reachability (as reported by the System Configuration framework) is
196+
/// down, online requests will not be attempted and report to fail immediately.
197+
/// - When `false`, online requests will always be attempted (if the strategy involves them), even if the network
198+
/// does not seem to be reachable.
199+
///
200+
/// + Note: Not available on watchOS (the System Configuration framework is not available there).
201+
///
202+
@objc public var useReachability: Bool = true
203+
204+
#endif // !os(watchOS)
205+
188206
// MARK: Initialization
189207

190208
internal init(appID: String?, apiKey: String?, readHosts: [String], writeHosts: [String]) {
@@ -324,4 +342,17 @@ internal struct HostStatus {
324342
self.hostStatuses[host] = HostStatus(up: up, lastModified: Date())
325343
}
326344
}
345+
346+
#if !os(watchOS)
347+
348+
/// Decide whether a network request should be attempted in the current conditions.
349+
///
350+
/// - returns: `true` if a network request should be attempted, `false` if the client should fail fast with a
351+
/// network error.
352+
///
353+
func shouldMakeNetworkCall() -> Bool {
354+
return !useReachability || reachability.isReachable()
355+
}
356+
357+
#endif // !os(watchOS)
327358
}

Source/NetworkReachability.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// Copyright (c) 2016 Algolia
3+
// http://www.algolia.com/
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in
13+
// all copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
// THE SOFTWARE.
22+
//
23+
24+
import Foundation
25+
import SystemConfiguration
26+
27+
28+
/// Tests network reachability.
29+
///
30+
class NetworkReachability {
31+
// MARK: Properties
32+
33+
/// Reachability handle used to test connectivity.
34+
private var reachability: SCNetworkReachability
35+
36+
// MARK: Initialization
37+
38+
init() {
39+
// Create reachability handle to an all-zeroes address.
40+
var zeroAddress = NetworkReachability.zeroAddress
41+
reachability = withUnsafePointer(to: &zeroAddress) {
42+
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
43+
SCNetworkReachabilityCreateWithAddress(nil, $0)
44+
}
45+
}!
46+
}
47+
48+
/// Test if network connectivity is currently available.
49+
///
50+
/// - returns: true if network connectivity is available, false otherwise.
51+
///
52+
func isReachable() -> Bool {
53+
var flags: SCNetworkReachabilityFlags = []
54+
if !SCNetworkReachabilityGetFlags(reachability, &flags) {
55+
return false
56+
}
57+
58+
let reachable = flags.contains(.reachable)
59+
let connectionRequired = flags.contains(.connectionRequired)
60+
return reachable && !connectionRequired
61+
}
62+
63+
// MARK: Constants
64+
65+
/// An all zeroes IP address.
66+
static let zeroAddress: sockaddr_in = {
67+
var address = sockaddr_in()
68+
address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
69+
address.sin_family = sa_family_t(AF_INET)
70+
return address
71+
}()
72+
}

Source/Offline/MirroredIndex.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,8 @@ import Foundation
533533
// WARNING: All callbacks must run sequentially; we cannot afford race conditions between them.
534534
// Since most methods use the main thread for callbacks, we have to use it as well.
535535

536-
// If the strategy is "offline only", well, go offline straight away.
537-
if index.requestStrategy == .offlineOnly {
536+
// If the strategy is "offline only" or if connectivity is unavailable, go offline straight away.
537+
if index.requestStrategy == .offlineOnly || !index.client.shouldMakeNetworkCall() {
538538
startOffline()
539539
}
540540
// Otherwise, always launch an online request.

Source/Request.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,15 @@ internal class Request: AsyncOperationWithCompletion {
128128
if _cancelled {
129129
return
130130
}
131+
132+
// If there is no network reachability, don't even attempt to perform the request.
133+
#if !os(watchOS)
134+
if !client.shouldMakeNetworkCall() {
135+
self.callCompletion(content: nil, error: NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet, userInfo: nil))
136+
return
137+
}
138+
#endif // !os(watchOS)
139+
131140
let currentHostIndex = nextHostIndex
132141
let request = createRequest(currentHostIndex)
133142
nextHostIndex = (nextHostIndex + 1) % hosts.count

0 commit comments

Comments
 (0)