Skip to content

Commit b1c0282

Browse files
authored
fix: sync init flow changed to support dynamic update (#297)
1 parent c70fc12 commit b1c0282

13 files changed

+547
-98
lines changed

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,8 @@
327327
6E34A647231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E34A63D231ED28600BAE302 /* empty_datafile_new_account_id.json */; };
328328
6E34A648231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E34A63D231ED28600BAE302 /* empty_datafile_new_account_id.json */; };
329329
6E34A649231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E34A63D231ED28600BAE302 /* empty_datafile_new_account_id.json */; };
330+
6E5AB69323F6130D007A82B1 /* OptimizelyClientTests_Init_Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */; };
331+
6E5AB69423F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */; };
330332
6E614DD621E3F38A005982A1 /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E614DCD21E3F389005982A1 /* Optimizely.framework */; };
331333
6E636B912236C91F00AF3CEF /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */; };
332334
6E636BA02236C96700AF3CEF /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */; };
@@ -1268,6 +1270,8 @@
12681270
6E34A623231ED04900BAE302 /* empty_datafile_new_project_id.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = empty_datafile_new_project_id.json; sourceTree = "<group>"; };
12691271
6E34A624231ED04900BAE302 /* empty_datafile_new_revision.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = empty_datafile_new_revision.json; sourceTree = "<group>"; };
12701272
6E34A63D231ED28600BAE302 /* empty_datafile_new_account_id.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = empty_datafile_new_account_id.json; sourceTree = "<group>"; };
1273+
6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Sync.swift; sourceTree = "<group>"; };
1274+
6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Async.swift; sourceTree = "<group>"; };
12711275
6E614DCD21E3F389005982A1 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; };
12721276
6E614DD521E3F38A005982A1 /* OptimizelyTests-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
12731277
6E636B8C2236C91F00AF3CEF /* OptimizelyTests-APIs-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-APIs-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1911,6 +1915,8 @@
19111915
6E7519B922C5211100B2B157 /* OptimizelyTests-APIs */ = {
19121916
isa = PBXGroup;
19131917
children = (
1918+
6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */,
1919+
6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */,
19141920
6E7519BA22C5211100B2B157 /* OptimizelyClientTests_Evaluation.swift */,
19151921
6E7519BB22C5211100B2B157 /* OptimizelyClientTests_DatafileHandler.swift */,
19161922
6E7519BC22C5211100B2B157 /* OptimizelyErrorTests.swift */,
@@ -2848,9 +2854,11 @@
28482854
6E7517F022C520D400B2B157 /* DataStoreMemory.swift in Sources */,
28492855
6E9B11D922C548A200C22D81 /* OptimizelyClientTests_Invalid.swift in Sources */,
28502856
6E9B11D522C548A200C22D81 /* OptimizelyClientTests_Evaluation.swift in Sources */,
2857+
6E5AB69423F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift in Sources */,
28512858
6E9B11DA22C548A200C22D81 /* OptimizelyClientTests_ObjcAPIs.m in Sources */,
28522859
6E75179A22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */,
28532860
6E75182022C520D400B2B157 /* BatchEventBuilder.swift in Sources */,
2861+
6E5AB69323F6130D007A82B1 /* OptimizelyClientTests_Init_Sync.swift in Sources */,
28542862
6E75184422C520D400B2B157 /* Event.swift in Sources */,
28552863
6E75194022C520D500B2B157 /* OPTDecisionService.swift in Sources */,
28562864
6E7518E022C520D400B2B157 /* ConditionLeaf.swift in Sources */,

Sources/Implementation/DefaultDatafileHandler.swift

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
/****************************************************************************
2-
* Copyright 2019, 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-
***************************************************************************/
2+
* Copyright 2019-2020, 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+
***************************************************************************/
1616

1717
import Foundation
1818

@@ -84,7 +84,6 @@ class DefaultDatafileHandler: OPTDatafileHandler {
8484
}
8585

8686
return request
87-
8887
}
8988

9089
open func getResponseData(sdkKey: String, response: HTTPURLResponse, url: URL?) -> Data? {
@@ -101,7 +100,8 @@ class DefaultDatafileHandler: OPTDatafileHandler {
101100
}
102101

103102
open func downloadDatafile(sdkKey: String,
104-
resourceTimeoutInterval: Double? = nil,
103+
returnCacheIfNoChange: Bool,
104+
resourceTimeoutInterval: Double?,
105105
completionHandler: @escaping DatafileDownloadCompletionHandler) {
106106

107107
downloadQueue.async {
@@ -121,7 +121,16 @@ class DefaultDatafileHandler: OPTDatafileHandler {
121121
result = .success(data)
122122
} else if response.statusCode == 304 {
123123
self.logger.d("The datafile was not modified and won't be downloaded again")
124-
result = .success(nil)
124+
125+
if returnCacheIfNoChange {
126+
if let data = self.loadSavedDatafile(sdkKey: sdkKey) {
127+
result = .success(data)
128+
} else {
129+
result = .failure(.datafileLoadingFailed(sdkKey))
130+
}
131+
} else {
132+
result = .success(nil)
133+
}
125134
}
126135
}
127136

@@ -169,7 +178,6 @@ class DefaultDatafileHandler: OPTDatafileHandler {
169178
self.performPerodicDownload(sdkKey: sdkKey, startTime: startDate, updateInterval: updateInterval, datafileChangeNotification: datafileChangeNotification)
170179
}
171180
timer.invalidate()
172-
173181
}
174182

175183
func hasPeriodUpdates(sdkKey: String) -> Bool {
@@ -220,7 +228,6 @@ class DefaultDatafileHandler: OPTDatafileHandler {
220228
timer.timer?.invalidate()
221229
timers.removeValue(forKey: sdkKey)
222230
}
223-
224231
}
225232
}
226233

@@ -293,7 +300,6 @@ class DefaultDatafileHandler: OPTDatafileHandler {
293300
try? FileManager.default.removeItem(at: fileURL)
294301
}
295302
}
296-
297303
}
298304
}
299305

@@ -313,11 +319,10 @@ extension URLRequest {
313319
addValue(lastModified, forHTTPHeaderField: "If-Modified-Since")
314320
}
315321
}
316-
322+
317323
func getLastModified() -> String? {
318324
return value(forHTTPHeaderField: "If-Modified-Since")
319325
}
320-
321326
}
322327

323328
extension HTTPURLResponse {

Sources/Optimizely/OptimizelyClient+ObjC.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,30 @@ extension OptimizelyClient {
9999
/// - doFetchDatafileBackground: This is for debugging purposes when
100100
/// you don't want to download the datafile. In practice, you should allow the
101101
/// background thread to update the cache copy (optional)
102-
public func objcStart(datafile: Data, doFetchDatafileBackground: Bool = true) throws {
102+
public func objcStart(datafile: Data, doFetchDatafileBackground: Bool) throws {
103103
try self.start(datafile: datafile, doFetchDatafileBackground: doFetchDatafileBackground)
104104
}
105105

106+
@available(swift, obsoleted: 1.0)
107+
@objc(startWithDatafile:doUpdateConfigOnNewDatafile:doFetchDatafileBackground:error:)
108+
/// Start Optimizely SDK (Synchronous)
109+
///
110+
/// - Parameters:
111+
/// - datafile: This datafile will be used when cached copy is not available (fresh start)
112+
/// A cached copy from previous download is used if it's available.
113+
/// The datafile will be updated from the server in the background thread.
114+
/// - doUpdateConfigOnNewDatafile: When a new datafile is fetched from the server in the background thread,
115+
/// the SDK will be updated with the new datafile immediately if this value is set to true.
116+
/// When it's set to false (default), the new datafile is cached and will be used when the SDK is started again.
117+
/// - doFetchDatafileBackground: This is for debugging purposes when
118+
/// you don't want to download the datafile. In practice, you should allow the
119+
/// background thread to update the cache copy (optional)
120+
public func objcStart(datafile: Data, doUpdateConfigOnNewDatafile: Bool, doFetchDatafileBackground: Bool) throws {
121+
try self.start(datafile: datafile,
122+
doUpdateConfigOnNewDatafile: doUpdateConfigOnNewDatafile,
123+
doFetchDatafileBackground: doFetchDatafileBackground)
124+
}
125+
106126
@available(swift, obsoleted: 1.0)
107127
@objc(activateWithExperimentKey:userId:attributes:error:)
108128
/// Try to activate an experiment based on the experiment key and user ID with user attributes.

Sources/Optimizely/OptimizelyClient.swift

Lines changed: 67 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ open class OptimizelyClient: NSObject {
2424
// MARK: - Properties
2525

2626
var sdkKey: String
27+
2728
private var atomicConfig: AtomicProperty<ProjectConfig> = AtomicProperty<ProjectConfig>()
2829
var config: ProjectConfig? {
2930
get {
@@ -39,6 +40,14 @@ open class OptimizelyClient: NSObject {
3940
}
4041

4142
let eventLock = DispatchQueue(label: "com.optimizely.client")
43+
44+
private var isPeriodicPollingEnabled: Bool {
45+
if let handler = datafileHandler as? DefaultDatafileHandler {
46+
return handler.hasPeriodUpdates(sdkKey: sdkKey)
47+
} else {
48+
return false
49+
}
50+
}
4251

4352
// MARK: - Customizable Services
4453

@@ -54,8 +63,8 @@ open class OptimizelyClient: NSObject {
5463
return HandlerRegistryService.shared.injectDecisionService(sdkKey: self.sdkKey)!
5564
}
5665

57-
public var datafileHandler: OPTDatafileHandler {
58-
return HandlerRegistryService.shared.injectDatafileHandler(sdkKey: self.sdkKey)!
66+
public var datafileHandler: OPTDatafileHandler? {
67+
return HandlerRegistryService.shared.injectDatafileHandler(sdkKey: self.sdkKey)
5968
}
6069

6170
public var notificationCenter: OPTNotificationCenter? {
@@ -106,18 +115,22 @@ open class OptimizelyClient: NSObject {
106115
/// - resourceTimeout: timeout for datafile download (optional)
107116
/// - completion: callback when initialization is completed
108117
public func start(resourceTimeout: Double? = nil, completion: ((OptimizelyResult<Data>) -> Void)? = nil) {
109-
fetchDatafileBackground(resourceTimeout: resourceTimeout) { result in
118+
datafileHandler?.downloadDatafile(sdkKey: sdkKey, returnCacheIfNoChange: true) { result in
110119
switch result {
111-
case .failure:
112-
completion?(result)
113120
case .success(let datafile):
121+
guard let datafile = datafile else {
122+
completion?(.failure(.datafileLoadingFailed(self.sdkKey)))
123+
return
124+
}
125+
114126
do {
115127
try self.configSDK(datafile: datafile)
116-
117-
completion?(result)
128+
completion?(.success(datafile))
118129
} catch {
119130
completion?(.failure(error as! OptimizelyError))
120131
}
132+
case .failure(let error):
133+
completion?(.failure(error))
121134
}
122135
}
123136
}
@@ -139,80 +152,76 @@ open class OptimizelyClient: NSObject {
139152
/// - datafile: This datafile will be used when cached copy is not available (fresh start)
140153
/// A cached copy from previous download is used if it's available.
141154
/// The datafile will be updated from the server in the background thread.
155+
/// - doUpdateConfigOnNewDatafile: When a new datafile is fetched from the server in the background thread,
156+
/// the SDK will be updated with the new datafile immediately if this value is set to true.
157+
/// When it's set to false (default), the new datafile is cached and will be used when the SDK is started again.
142158
/// - doFetchDatafileBackground: This is for debugging purposes when
143159
/// you don't want to download the datafile. In practice, you should allow the
144160
/// background thread to update the cache copy (optional)
145-
public func start(datafile: Data, doFetchDatafileBackground: Bool = true) throws {
146-
let cachedDatafile = self.datafileHandler.loadSavedDatafile(sdkKey: self.sdkKey)
161+
public func start(datafile: Data,
162+
doUpdateConfigOnNewDatafile: Bool = false,
163+
doFetchDatafileBackground: Bool = true) throws {
164+
let cachedDatafile = datafileHandler?.loadSavedDatafile(sdkKey: self.sdkKey)
147165
let selectedDatafile = cachedDatafile ?? datafile
148166

149167
try configSDK(datafile: selectedDatafile)
150168

151169
// continue to fetch updated datafile from the server in background and cache it for next sessions
152-
if doFetchDatafileBackground { fetchDatafileBackground() }
170+
171+
if !doFetchDatafileBackground { return }
172+
173+
datafileHandler?.downloadDatafile(sdkKey: sdkKey, returnCacheIfNoChange: false) { result in
174+
// override to update always if periodic datafile polling is enabled
175+
// this is necessary for the case that the first cache download gets the updated datafile
176+
guard doUpdateConfigOnNewDatafile || self.isPeriodicPollingEnabled else { return }
177+
178+
if case .success(let data) = result, let datafile = data {
179+
// new datafile came in
180+
self.updateConfigFromBackgroundFetch(data: datafile)
181+
}
182+
}
153183
}
154184

155185
func configSDK(datafile: Data) throws {
156186
do {
157187
self.config = try ProjectConfig(datafile: datafile)
158-
159-
datafileHandler.startUpdates(sdkKey: self.sdkKey) { data in
160-
// new datafile came in...
161-
if let config = try? ProjectConfig(datafile: data) {
162-
do {
163-
if let users = self.config?.whitelistUsers {
164-
config.whitelistUsers = users
165-
}
166-
167-
self.config = config
168-
169-
// call reinit on the services we know we are reinitializing.
170-
171-
for component in HandlerRegistryService.shared.lookupComponents(sdkKey: self.sdkKey) ?? [] {
172-
HandlerRegistryService.shared.reInitializeComponent(service: component, sdkKey: self.sdkKey)
173-
}
174-
175-
}
176-
177-
self.sendDatafileChangeNotification(data: data)
178-
}
188+
189+
datafileHandler?.startUpdates(sdkKey: self.sdkKey) { data in
190+
// new datafile came in
191+
self.updateConfigFromBackgroundFetch(data: data)
179192
}
180-
} catch {
193+
} catch let error as OptimizelyError {
181194
// .datafileInvalid
182195
// .datafaileVersionInvalid
183196
// .datafaileLoadingFailed
197+
self.logger.e(error)
198+
throw error
199+
} catch {
200+
self.logger.e(error.localizedDescription)
184201
throw error
185202
}
186203
}
187204

188-
func fetchDatafileBackground(resourceTimeout: Double? = nil, completion: ((OptimizelyResult<Data>) -> Void)? = nil) {
205+
func updateConfigFromBackgroundFetch(data: Data) {
206+
guard let config = try? ProjectConfig(datafile: data) else {
207+
return
208+
}
189209

190-
datafileHandler.downloadDatafile(sdkKey: self.sdkKey, resourceTimeoutInterval: resourceTimeout) { result in
191-
var fetchResult: OptimizelyResult<Data>
192-
193-
switch result {
194-
case .failure(let error):
195-
fetchResult = .failure(error)
196-
case .success(let datafile):
197-
// we got a new datafile.
198-
if let datafile = datafile {
199-
fetchResult = .success(datafile)
200-
}
201-
// we got a success but no datafile 304. So, load the saved datafile.
202-
else if let data = self.datafileHandler.loadSavedDatafile(sdkKey: self.sdkKey) {
203-
fetchResult = .success(data)
204-
}
205-
// if that fails, we have a problem.
206-
else {
207-
fetchResult = .failure(.datafileLoadingFailed(self.sdkKey))
208-
}
209-
210-
}
211-
212-
completion?(fetchResult)
210+
if let users = self.config?.whitelistUsers {
211+
config.whitelistUsers = users
213212
}
213+
214+
self.config = config
215+
216+
// call reinit on the services we know we are reinitializing.
217+
218+
for component in HandlerRegistryService.shared.lookupComponents(sdkKey: self.sdkKey) ?? [] {
219+
HandlerRegistryService.shared.reInitializeComponent(service: component, sdkKey: self.sdkKey)
220+
}
221+
222+
self.sendDatafileChangeNotification(data: data)
214223
}
215-
224+
216225
/**
217226
* Use the activate method to start an experiment.
218227
*
@@ -836,7 +845,7 @@ extension OptimizelyClient {
836845
extension OptimizelyClient {
837846

838847
public func close() {
839-
datafileHandler.stopUpdates(sdkKey: sdkKey)
848+
datafileHandler?.stopUpdates(sdkKey: sdkKey)
840849
eventLock.sync {}
841850
eventDispatcher?.close()
842851
}

0 commit comments

Comments
 (0)