@@ -51,10 +51,38 @@ import Foundation
5151}
5252
5353
54+ /// Describes what is the last known status of a given API host.
55+ ///
56+ internal struct HostStatus {
57+ /// Whether the host is "up" or "down".
58+ /// "Up" means it answers normally, "down" means that it doesn't. This does not distinguish between the different
59+ /// kinds of retriable failures: it could be DNS resolution failure, no route to host, response timeout, or server
60+ /// error. A non-retriable failure (e.g. `400 Bad Request`) is not considered for the "down" state.
61+ ///
62+ var up : Bool
63+
64+ /// When the status was last modified.
65+ /// This is normally the moment when the client receives the response (or error).
66+ ///
67+ var lastModified : Date
68+ }
69+
70+
5471/// An abstract API client.
5572///
5673/// + Warning: Not meant to be used directly. See `Client` or `PlacesClient` instead.
5774///
75+ /// ## Stateful hosts
76+ ///
77+ /// In order to avoid hitting timeouts at every request when one or more hosts are not working properly (whatever the
78+ /// reason: DNS failure, no route to host, server down...), the client maintains a **known status** for each host.
79+ /// That status can be either *up*, *down* or *unknown*. Initially, all hosts are in the *unknown* state. Then a given
80+ /// host's status is updated whenever a request to it returns a response or an error.
81+ ///
82+ /// When a host is flagged as *down*, it will not be considered for subsequent requests. However, to avoid discarding
83+ /// hosts permanently, statuses are only remembered for a given timeframe, indicated by `hostStatusTimeout`. (You may
84+ /// adjust it as needed, although the default value `defaultHostStatusTimeout` should make sense for most applications.)
85+ ///
5886@objc public class AbstractClient : NSObject {
5987 // MARK: Properties
6088
@@ -125,6 +153,21 @@ import Foundation
125153 }
126154 }
127155
156+ /// The last known statuses of hosts.
157+ /// If a host is absent from this dictionary, it means its status is unknown.
158+ ///
159+ /// + Note: Hosts are never removed from this dictionary, which is a potential memory leak in theory, but does not
160+ /// matter in practice, because (1) the host arrays are provided at init time and seldom updated and (2) very
161+ /// short anyway.
162+ ///
163+ internal var hostStatuses : [ String : HostStatus ] = [ : ]
164+
165+ /// The timeout for host statuses.
166+ @objc public var hostStatusTimeout : TimeInterval = defaultHostStatusTimeout
167+
168+ /// GCD queue to synchronize access to `hostStatuses`.
169+ internal var hostStatusQueue = DispatchQueue ( label: " AbstractClient.hostStatusQueue " )
170+
128171 // NOTE: Not constant only for the sake of mocking during unit tests.
129172 var session : URLSession
130173
@@ -137,6 +180,11 @@ import Foundation
137180 /// Dispatch queue used to run completion handlers.
138181 internal var completionQueue = DispatchQueue . main
139182
183+ // MARK: Constant
184+
185+ /// The default timeout for host statuses.
186+ @objc public static let defaultHostStatusTimeout : TimeInterval = 5 * 60
187+
140188 // MARK: Initialization
141189
142190 internal init ( appID: String ? , apiKey: String ? , readHosts: [ String ] , writeHosts: [ String ] ) {
@@ -241,7 +289,39 @@ import Foundation
241289 /// Create a request with this client's settings.
242290 func newRequest( method: HTTPMethod , path: String , body: JSONObject ? , hostnames: [ String ] , isSearchQuery: Bool = false , completion: CompletionHandler ? = nil ) -> Request {
243291 let currentTimeout = isSearchQuery ? searchTimeout : timeout
244- let request = Request ( session : session , method: method, hosts: hostnames, firstHostIndex: 0 , path: path, headers: headers, jsonBody: body, timeout: currentTimeout, completion: completion)
292+ let request = Request ( client : self , method: method, hosts: hostnames, firstHostIndex: 0 , path: path, headers: headers, jsonBody: body, timeout: currentTimeout, completion: completion)
245293 return request
246294 }
295+
296+ /// Filter a list of hosts according to the currently known statuses, keeping only those that are up or unknown.
297+ ///
298+ /// - parameter hosts: The list of hosts to filter.
299+ /// - returns: A filtered list of hosts, or the original list if the result of filtering would be empty.
300+ ///
301+ func upOrUnknownHosts( _ hosts: [ String ] ) -> [ String ] {
302+ assert ( !hosts. isEmpty)
303+ let now = Date ( )
304+ let filteredHosts = hostStatusQueue. sync {
305+ return hosts. filter { ( host) -> Bool in
306+ if let status = self . hostStatuses [ host] { // known status
307+ return status. up || now. timeIntervalSince ( status. lastModified) >= self . hostStatusTimeout // include if up or obsolete
308+ } else { // unknown status
309+ return true // always include
310+ }
311+ }
312+ }
313+ // Avoid returning an empty list.
314+ return filteredHosts. isEmpty ? hosts : filteredHosts
315+ }
316+
317+ /// Update the status for a given host.
318+ ///
319+ /// - parameter host: The name of the host to update.
320+ /// - parameter up: Whether the host is currently up (true) or down (false).
321+ ///
322+ func updateHostStatus( host: String , up: Bool ) {
323+ hostStatusQueue. sync {
324+ self . hostStatuses [ host] = HostStatus ( up: up, lastModified: Date ( ) )
325+ }
326+ }
247327}
0 commit comments