Skip to content

Commit 3e6fbf8

Browse files
authored
fix(datastore): fix stop then start API call pattern (#2517)
* fix(datastore): enable forceSync/stop/clear on an already stopped state of StorageEngine to avoid crash * fix(datastore): address PR comments * chore(datastore): fix clearStart & stopStart Integ test
1 parent 74715df commit 3e6fbf8

File tree

5 files changed

+272
-5
lines changed

5 files changed

+272
-5
lines changed

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/AWSDataStorePlugin+DataStoreBaseBehavior.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,17 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior {
281281
dispatchedModelSynced.set(false)
282282
}
283283
if storageEngine == nil {
284-
285-
completion(.successfulVoid)
284+
queue.async {
285+
completion(.successfulVoid)
286+
}
286287
return
287288
}
288289

289290
storageEngine.stopSync { result in
290291
self.storageEngine = nil
291-
completion(result)
292+
self.queue.async {
293+
completion(result)
294+
}
292295
}
293296
}
294297
}
@@ -309,12 +312,16 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior {
309312
dispatchedModelSynced.set(false)
310313
}
311314
if storageEngine == nil {
312-
completion(.successfulVoid)
315+
queue.async {
316+
completion(.successfulVoid)
317+
}
313318
return
314319
}
315320
storageEngine.clear { result in
316321
self.storageEngine = nil
317-
completion(result)
322+
self.queue.async {
323+
completion(result)
324+
}
318325
}
319326
}
320327
}

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/AWSDataStorePlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin {
4646

4747
var storageEngine: StorageEngineBehavior!
4848
var storageEngineInitQueue = DispatchQueue(label: "AWSDataStorePlugin.storageEngineInitQueue")
49+
let queue = DispatchQueue(label: "AWSDataStorePlugin.queue", target: DispatchQueue.global())
4950
var storageEngineBehaviorFactory: StorageEngineBehaviorFactory
5051

5152
var iStorageEngineSink: Any?

AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginIntegrationTests/DataStoreEndToEndTests.swift

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Combine
1515
@testable import AmplifyTestCommon
1616
@testable import AWSDataStoreCategoryPlugin
1717

18+
// swiftlint:disable type_body_length
1819
@available(iOS 13.0, *)
1920
class DataStoreEndToEndTests: SyncEngineIntegrationTestBase {
2021

@@ -366,6 +367,177 @@ class DataStoreEndToEndTests: SyncEngineIntegrationTestBase {
366367

367368
}
368369

370+
/// Ensure DataStore.stop followed by DataStore.start is successful
371+
///
372+
/// - Given: DataStore has just configured, but not started
373+
/// - When:
374+
/// - DataStore.stop
375+
/// - Followed by DataStore.start in the completion of the stop
376+
/// - Then:
377+
/// - Datastore should be started successfully
378+
///
379+
func testConfigureAmplifyThenStopStart() throws {
380+
setUp(withModels: TestModelRegistration())
381+
try startAmplify()
382+
let stopStartSuccess = expectation(description: "stop then start successful")
383+
Amplify.DataStore.stop { result in
384+
switch result {
385+
case .success:
386+
Amplify.DataStore.start { result in
387+
switch result {
388+
case .success:
389+
stopStartSuccess.fulfill()
390+
case .failure(let error):
391+
XCTFail("\(error)")
392+
}
393+
}
394+
case .failure(let error):
395+
XCTFail("\(error)")
396+
}
397+
}
398+
wait(for: [stopStartSuccess], timeout: networkTimeout)
399+
400+
}
401+
402+
/// Ensure DataStore.clear followed by DataStore.start is successful
403+
///
404+
/// - Given: DataStore has just configured, but not started
405+
/// - When:
406+
/// - DataStore.clear
407+
/// - Followed by DataStore.start in the completion of the stop
408+
/// - Then:
409+
/// - Datastore should be started successfully
410+
///
411+
func testConfigureAmplifyThenClearStart() throws {
412+
setUp(withModels: TestModelRegistration())
413+
try startAmplify()
414+
let clearStartSuccess = expectation(description: "clear then start successful")
415+
Amplify.DataStore.clear { result in
416+
switch result {
417+
case .success:
418+
Amplify.DataStore.start { result in
419+
switch result {
420+
case .success:
421+
clearStartSuccess.fulfill()
422+
case .failure(let error):
423+
XCTFail("\(error)")
424+
}
425+
}
426+
case .failure(let error):
427+
XCTFail("\(error)")
428+
}
429+
}
430+
wait(for: [clearStartSuccess], timeout: networkTimeout)
431+
432+
}
433+
434+
/// Ensure DataStore.stop followed by DataStore.start on a DispatchQueue
435+
/// of a background thread is successful
436+
///
437+
/// - Given: DataStore has just configured, but not started
438+
/// - When:
439+
/// - DataStore.stop
440+
/// - Followed by DataStore.start on a background thread in the completion of the stop
441+
/// - Then:
442+
/// - Datastore should be started successfully
443+
///
444+
func testConfigureAmplifyThenStopStartonDispatch() throws {
445+
setUp(withModels: TestModelRegistration())
446+
try startAmplify()
447+
let stopStartSuccess = expectation(description: "stop then start successful")
448+
Amplify.DataStore.stop { result in
449+
switch result {
450+
case .success:
451+
DispatchQueue.global(qos: .background).async {
452+
Amplify.DataStore.start { result in
453+
switch result {
454+
case .success:
455+
stopStartSuccess.fulfill()
456+
case .failure(let error):
457+
XCTFail("\(error)")
458+
}
459+
}
460+
}
461+
case .failure(let error):
462+
XCTFail("\(error)")
463+
}
464+
}
465+
wait(for: [stopStartSuccess], timeout: networkTimeout)
466+
467+
}
468+
469+
/// Ensure DataStore.clear followed by DataStore.start on a DispatchQueue
470+
/// of a background thread is successful
471+
///
472+
/// - Given: DataStore has just configured, but not started
473+
/// - When:
474+
/// - DataStore.clear
475+
/// - Followed by DataStore.start on a background thread in the completion of the stop
476+
/// - Then:
477+
/// - Datastore should be started successfully
478+
///
479+
func testConfigureAmplifyThenClearStartOnDispatch() throws {
480+
setUp(withModels: TestModelRegistration())
481+
try startAmplify()
482+
let clearStartSuccess = expectation(description: "clear then start successful on DispatchQueue")
483+
DispatchQueue.global(qos: .background).async {
484+
Amplify.DataStore.clear { result in
485+
switch result {
486+
case .success:
487+
DispatchQueue.global(qos: .background).async {
488+
Amplify.DataStore.start { result in
489+
switch result {
490+
case .success:
491+
clearStartSuccess.fulfill()
492+
case .failure(let error):
493+
XCTFail("\(error)")
494+
}
495+
}
496+
}
497+
case .failure(let error):
498+
XCTFail("\(error)")
499+
}
500+
}
501+
}
502+
wait(for: [clearStartSuccess], timeout: networkTimeout)
503+
504+
}
505+
506+
/// Ensure DataStore.start followed by DataStore.stop & restarting with
507+
/// DataStore.start is successful
508+
///
509+
/// - Given: DataStore has has just configured, but not started
510+
/// - When:
511+
/// - DataStore.start
512+
/// - DataStore.stop
513+
/// - Followed by DataStore.start
514+
/// - Then:
515+
/// - Datastore should be started successfully
516+
///
517+
func testConfigureAmplifyThenStartStopStart() throws {
518+
setUp(withModels: TestModelRegistration())
519+
try startAmplify()
520+
let startStopSuccess = expectation(description: "start then stop successful")
521+
Amplify.DataStore.start { result in
522+
switch result {
523+
case .success:
524+
Amplify.DataStore.stop { result in
525+
switch result {
526+
case .success:
527+
self.startDataStore()
528+
startStopSuccess.fulfill()
529+
case .failure(let error):
530+
XCTFail("\(error)")
531+
}
532+
}
533+
case .failure(let error):
534+
XCTFail("\(error)")
535+
}
536+
}
537+
wait(for: [startStopSuccess], timeout: networkTimeout)
538+
539+
}
540+
369541
/// Ensure the DataStore is automatically started when querying for the first time
370542
///
371543
/// - Given: DataStore is configured but not started

AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginIntegrationTests/TestSupport/SyncEngineIntegrationTestBase.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ class SyncEngineIntegrationTestBase: DataStoreTestBase {
5151
}
5252
}
5353

54+
func startDataStore() {
55+
let started = expectation(description: "DataStore started")
56+
Amplify.DataStore.start { result in
57+
switch result {
58+
case .success:
59+
started.fulfill()
60+
case .failure(let error):
61+
XCTFail("\(error)")
62+
}
63+
}
64+
wait(for: [started], timeout: 2)
65+
}
66+
5467
func stopDataStore() {
5568
let stopped = expectation(description: "DataStore stopped")
5669
Amplify.DataStore.stop { result in

AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Core/AWSDataStorePluginTests.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,80 @@ class AWSDataStorePluginTests: XCTestCase {
6161
waitForExpectations(timeout: 1.0)
6262
}
6363

64+
func testStorageEngineStartsOnPluginStopStart() throws {
65+
let stopExpectation = expectation(description: "Stop Sync should not be called")
66+
stopExpectation.isInverted = true
67+
let startExpectation = expectation(description: "Start Sync should be called")
68+
69+
let storageEngine = MockStorageEngineBehavior()
70+
storageEngine.responders[.stopSync] = StopSyncResponder { _ in
71+
stopExpectation.fulfill()
72+
}
73+
74+
storageEngine.responders[.startSync] = StartSyncResponder { _ in
75+
startExpectation.fulfill()
76+
}
77+
78+
let storageEngineBehaviorFactory: StorageEngineBehaviorFactory = {_, _, _, _, _, _ throws in
79+
return storageEngine
80+
}
81+
let dataStorePublisher = DataStorePublisher()
82+
let plugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(),
83+
storageEngineBehaviorFactory: storageEngineBehaviorFactory,
84+
dataStorePublisher: dataStorePublisher,
85+
validAPIPluginKey: "MockAPICategoryPlugin",
86+
validAuthPluginKey: "MockAuthCategoryPlugin")
87+
88+
do {
89+
try plugin.configure(using: nil)
90+
XCTAssertNil(plugin.storageEngine)
91+
92+
plugin.stop(completion: { _ in
93+
plugin.start(completion: {_ in})
94+
})
95+
} catch {
96+
XCTFail("DataStore configuration should not fail with nil configuration. \(error)")
97+
}
98+
waitForExpectations(timeout: 1.0)
99+
}
100+
101+
func testStorageEngineStartsOnPluginClearStart() throws {
102+
let clearExpectation = expectation(description: "Clear should be called")
103+
let startExpectation = expectation(description: "Start Sync should be called")
104+
var currCount = 0
105+
106+
let storageEngine = MockStorageEngineBehavior()
107+
storageEngine.responders[.clear] = ClearResponder { _ in
108+
currCount = self.expect(clearExpectation, currCount, 1)
109+
}
110+
111+
storageEngine.responders[.startSync] = StartSyncResponder { _ in
112+
currCount = self.expect(startExpectation, currCount, 2)
113+
}
114+
115+
let storageEngineBehaviorFactory: StorageEngineBehaviorFactory = {_, _, _, _, _, _ throws in
116+
return storageEngine
117+
}
118+
let dataStorePublisher = DataStorePublisher()
119+
let plugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(),
120+
storageEngineBehaviorFactory: storageEngineBehaviorFactory,
121+
dataStorePublisher: dataStorePublisher,
122+
validAPIPluginKey: "MockAPICategoryPlugin",
123+
validAuthPluginKey: "MockAuthCategoryPlugin")
124+
125+
do {
126+
try plugin.configure(using: nil)
127+
XCTAssertNil(plugin.storageEngine)
128+
129+
plugin.clear(completion: { _ in
130+
plugin.start(completion: {_ in})
131+
})
132+
} catch {
133+
XCTFail("DataStore configuration should not fail with nil configuration. \(error)")
134+
}
135+
waitForExpectations(timeout: 1.0)
136+
}
137+
64138
func testStorageEngineStartsOnQuery() throws {
65139
let startExpectation = expectation(description: "Start Sync should be called with Query")
66140
let storageEngine = MockStorageEngineBehavior()

0 commit comments

Comments
 (0)