Skip to content

Commit 51c2066

Browse files
authored
Merge pull request #150 from optimizely/refactorDatafileHandler
(chore): Refactor datafile handler to download before rescheduling. Also, added download timeout.
2 parents 9b751ad + 8e7875a commit 51c2066

File tree

5 files changed

+171
-56
lines changed

5 files changed

+171
-56
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
/****************************************************************************
3+
* Copyright 2019, Optimizely, Inc. and contributors *
4+
* *
5+
* Licensed under the Apache License, Version 2.0 (the "License"); *
6+
* you may not use this file except in compliance with the License. *
7+
* You may obtain a copy of the License at *
8+
* *
9+
* http://www.apache.org/licenses/LICENSE-2.0 *
10+
* *
11+
* Unless required by applicable law or agreed to in writing, software *
12+
* distributed under the License is distributed on an "AS IS" BASIS, *
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
14+
* See the License for the specific language governing permissions and *
15+
* limitations under the License. *
16+
***************************************************************************/
17+
18+
19+
import Foundation
20+
21+
extension Date {
22+
func minutesPastSinceNow() -> Int {
23+
let calendar = Calendar.current
24+
return calendar.component(.minute, from: self)
25+
}
26+
}

OptimizelySDK/Implementation/DefaultDatafileHandler.swift

Lines changed: 111 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import Foundation
1919
class DefaultDatafileHandler : OPTDatafileHandler {
2020
static public var endPointStringFormat = "https://cdn.optimizely.com/datafiles/%@.json"
2121
lazy var logger = HandlerRegistryService.shared.injectLogger()
22-
var timers:[String:Timer] = [String:Timer]()
22+
var timers:AtomicProperty<[String:Timer]> = AtomicProperty(property: [String:Timer]())
2323
let dataStore = DataStoreUserDefaults()
2424

2525
required init() {
@@ -28,35 +28,34 @@ class DefaultDatafileHandler : OPTDatafileHandler {
2828

2929
func downloadDatafile(sdkKey: String) -> Data? {
3030

31-
let config = URLSessionConfiguration.ephemeral
32-
let session = URLSession(configuration: config)
33-
let str = String(format: DefaultDatafileHandler.endPointStringFormat, sdkKey)
34-
var result:Data?
31+
var datafile:Data?
3532
let group = DispatchGroup()
3633

3734
group.enter()
3835

39-
if let url = URL(string: str) {
40-
let task = session.downloadTask(with: url){ (url, response, error) in
41-
self.logger?.log(level: .debug, message: response.debugDescription)
42-
if let response = response as? HTTPURLResponse, response.statusCode == 200,
43-
let url = url, let projectConfig = try? Data(contentsOf: url) {
44-
result = projectConfig
45-
self.saveDatafile(sdkKey: sdkKey, dataFile: projectConfig)
46-
}
47-
group.leave()
36+
downloadDatafile(sdkKey: sdkKey) { (result) in
37+
switch result {
38+
case .success(let data):
39+
datafile = data
40+
case .failure(let error):
41+
self.logger?.log(level: .error, message: error.localizedDescription)
4842
}
49-
50-
task.resume()
51-
52-
group.wait()
53-
43+
group.leave()
5444
}
55-
return result
45+
46+
group.wait()
47+
48+
return datafile
5649
}
5750

58-
open func downloadDatafile(sdkKey: String, completionHandler: @escaping (Result<Data?, DatafileDownloadError>) -> Void) {
51+
open func downloadDatafile(sdkKey: String,
52+
resourceTimeoutInterval:Double? = nil,
53+
completionHandler: @escaping DatafileDownloadCompletionHandler) {
5954
let config = URLSessionConfiguration.ephemeral
55+
if let resourceTimeoutInterval = resourceTimeoutInterval,
56+
resourceTimeoutInterval > 0 {
57+
config.timeoutIntervalForResource = TimeInterval(resourceTimeoutInterval)
58+
}
6059
let session = URLSession(configuration: config)
6160
let str = String(format: DefaultDatafileHandler.endPointStringFormat, sdkKey)
6261
if let url = URL(string: str) {
@@ -106,49 +105,112 @@ class DefaultDatafileHandler : OPTDatafileHandler {
106105
}
107106

108107
func startPeriodicUpdates(sdkKey: String, updateInterval: Int, datafileChangeNotification:((Data)->Void)?) {
109-
if let _ = timers[sdkKey] {
110-
logger?.log(level: .info, message: "Timer already started for datafile updates")
111-
return
112-
}
108+
109+
let now = Date()
113110
if #available(iOS 10.0, tvOS 10.0, *) {
114111
DispatchQueue.main.async {
115-
let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(updateInterval), repeats: true) { (timer) in
116-
self.downloadDatafile(sdkKey: sdkKey) { (result) in
117-
if let datafileChangeNotification = datafileChangeNotification {
118-
switch result {
119-
case .success(let data):
120-
if let data = data {
121-
datafileChangeNotification(data)
122-
}
123-
case .failure( _): break
124-
// don't do anything.
125-
}
126-
}
127-
// background download saves to cache
128-
}
112+
if let timer = self.timers.property?[sdkKey], timer.isValid {
113+
return
129114
}
130-
self.timers[sdkKey] = timer
131-
115+
116+
let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(updateInterval), repeats: false) { (timer) in
117+
118+
self.performPerodicDownload(sdkKey: sdkKey,
119+
startTime: now,
120+
updateInterval: updateInterval,
121+
datafileChangeNotification: datafileChangeNotification)
122+
123+
timer.invalidate()
124+
}
125+
self.timers.performAtomic(atomicOperation: { (timers) in
126+
timers[sdkKey] = timer
127+
})
132128
}
133129
} else {
134130
// Fallback on earlier versions
131+
DispatchQueue.main.async {
132+
if let timer = self.timers.property?[sdkKey], timer.isValid {
133+
return
134+
}
135+
136+
let timer = Timer.scheduledTimer(timeInterval: TimeInterval(updateInterval), target: self, selector:#selector(self.timerFired(timer:)), userInfo: ["sdkKey": sdkKey, "startTime": Date(), "updateInterval":updateInterval, "datafileChangeNotification":datafileChangeNotification ?? { (data) in }], repeats: false)
137+
138+
self.timers.performAtomic(atomicOperation: { (timers) in
139+
timers[sdkKey] = timer
140+
})
141+
}
142+
135143
}
136144
}
137145

138-
func stopPeriodicUpdates(sdkKey: String) {
139-
if let timer = timers[sdkKey] {
140-
logger?.log(level: .info, message: "Stopping timer for datafile updates sdkKey:" + sdkKey)
146+
@objc
147+
func timerFired(timer:Timer) {
148+
if let info = timer.userInfo as? [String:Any],
149+
let sdkKey = info["sdkKey"] as? String,
150+
let updateInterval = info["updateInterval"] as? Int,
151+
let startDate = info["startDate"] as? Date,
152+
let datafileChangeNotification = info["datafileChangeNotification"] as? ((Data)->Void){
153+
self.performPerodicDownload(sdkKey: sdkKey, startTime: startDate, updateInterval: updateInterval, datafileChangeNotification: datafileChangeNotification)
154+
}
155+
timer.invalidate()
156+
157+
}
158+
159+
func hasPeriodUpdates(sdkKey: String) -> Bool {
160+
var restart = true
161+
self.timers.performAtomic(atomicOperation: { (timers) in
162+
if !timers.contains(where: { $0.key == sdkKey} ) {
163+
restart = false
164+
}
165+
})
166+
167+
return restart
168+
}
169+
170+
func performPerodicDownload(sdkKey: String,
171+
startTime:Date,
172+
updateInterval:Int,
173+
datafileChangeNotification:((Data)->Void)?) {
174+
self.downloadDatafile(sdkKey: sdkKey) { (result) in
175+
switch result {
176+
case .success(let data):
177+
if let data = data,
178+
let datafileChangeNotification = datafileChangeNotification {
179+
datafileChangeNotification(data)
180+
}
181+
case .failure(let error):
182+
self.logger?.log(level: .error, message: error.localizedDescription)
183+
}
141184

142-
timer.invalidate()
143-
timers.removeValue(forKey: sdkKey)
185+
if self.hasPeriodUpdates(sdkKey: sdkKey) {
186+
let minutesSinceFire = startTime.minutesPastSinceNow()
187+
var diff = updateInterval - minutesSinceFire
188+
if diff < 0 {
189+
diff = 0
190+
}
191+
self.startPeriodicUpdates(sdkKey: sdkKey, updateInterval: diff, datafileChangeNotification: datafileChangeNotification)
192+
}
144193
}
194+
}
195+
196+
func stopPeriodicUpdates(sdkKey: String) {
197+
timers.performAtomic { (timers) in
198+
if let timer = timers[sdkKey] {
199+
logger?.log(level: .info, message: "Stopping timer for datafile updates sdkKey: \(sdkKey)")
200+
201+
timer.invalidate()
202+
timers.removeValue(forKey: sdkKey)
203+
}
145204

205+
}
146206
}
147207

148208
func stopPeriodicUpdates() {
149-
for key in timers.keys {
150-
logger?.log(level: .info, message: "Stopping timer for all datafile updates")
151-
stopPeriodicUpdates(sdkKey: key)
209+
timers.performAtomic { (timers) in
210+
for key in timers.keys {
211+
logger?.log(level: .info, message: "Stopping timer for all datafile updates")
212+
stopPeriodicUpdates(sdkKey: key)
213+
}
152214
}
153215

154216
}
@@ -205,6 +267,4 @@ class DefaultDatafileHandler : OPTDatafileHandler {
205267
}
206268

207269
}
208-
209-
210270
}

OptimizelySDK/Optimizely/OptimizelyManager.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ open class OptimizelyManager: NSObject {
7979
///
8080
/// - Parameters:
8181
/// - completion: callback when initialization is completed
82-
public func initializeSDK(completion: ((OptimizelyResult<Data>) -> Void)?=nil) {
83-
fetchDatafileBackground { result in
82+
public func initializeSDK(resourceTimeout:Double? = nil,completion: ((OptimizelyResult<Data>) -> Void)?=nil) {
83+
fetchDatafileBackground(resourceTimeout:resourceTimeout) { result in
8484
switch result {
8585
case .failure:
8686
completion?(result)
@@ -176,12 +176,12 @@ open class OptimizelyManager: NSObject {
176176
}
177177
}
178178

179-
func fetchDatafileBackground(completion: ((OptimizelyResult<Data>) -> Void)?=nil) {
179+
func fetchDatafileBackground(resourceTimeout:Double? = nil, completion: ((OptimizelyResult<Data>) -> Void)?=nil) {
180180

181181
// TODO: fix downloadDatafile to throw OptimizelyError
182182
// those errors propagated instead of handling here
183183

184-
datafileHandler.downloadDatafile(sdkKey: self.sdkKey){ result in
184+
datafileHandler.downloadDatafile(sdkKey: self.sdkKey, resourceTimeoutInterval:resourceTimeout){ result in
185185
var fetchResult: OptimizelyResult<Data>
186186

187187
switch result {

0 commit comments

Comments
 (0)