Skip to content

Commit 54646bd

Browse files
authored
fix(aws-datastore): ignore foreign key error during sync (#1058)
1 parent 4dbe516 commit 54646bd

File tree

3 files changed

+106
-1
lines changed

3 files changed

+106
-1
lines changed

aws-datastore/src/main/java/com/amplifyframework/datastore/syncengine/Merger.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package com.amplifyframework.datastore.syncengine;
1717

18+
import android.database.sqlite.SQLiteConstraintException;
1819
import androidx.annotation.NonNull;
1920

2021
import com.amplifyframework.core.Amplify;
@@ -27,6 +28,7 @@
2728
import com.amplifyframework.datastore.appsync.ModelWithMetadata;
2829
import com.amplifyframework.datastore.storage.LocalStorageAdapter;
2930
import com.amplifyframework.datastore.storage.StorageItemChange;
31+
import com.amplifyframework.datastore.utils.ErrorInspector;
3032
import com.amplifyframework.hub.HubChannel;
3133
import com.amplifyframework.hub.HubEvent;
3234
import com.amplifyframework.logging.Logger;
@@ -109,6 +111,15 @@ <T extends Model> Completable merge(
109111
announceSuccessfulMerge(modelWithMetadata);
110112
LOG.debug("Remote model update was sync'd down into local storage: " + modelWithMetadata);
111113
})
114+
// Remote store may not always respect the foreign key constraint, so
115+
// swallow any error caused by foreign key constraint violation.
116+
.onErrorComplete(failure -> {
117+
if (!ErrorInspector.contains(failure, SQLiteConstraintException.class)) {
118+
return false;
119+
}
120+
LOG.warn("Failed to sync due to foreign key constraint violation: " + modelWithMetadata, failure);
121+
return true;
122+
})
112123
.doOnError(failure ->
113124
LOG.warn("Failed to sync remote model into local storage: " + modelWithMetadata, failure)
114125
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
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+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.datastore.utils;
17+
18+
import androidx.annotation.NonNull;
19+
import androidx.annotation.Nullable;
20+
21+
import java.util.Objects;
22+
23+
/**
24+
* Utility class to provide helpful functions to use on throwable instances.
25+
*/
26+
public final class ErrorInspector {
27+
private ErrorInspector() {}
28+
29+
/**
30+
* Returns true if an error object was caused by a specific throwable type.
31+
* @param error Error object to perform the check on.
32+
* @param causeType Class type of the error to look for in stacktrace.
33+
* @return true if an error object contains given cause.
34+
*/
35+
public static boolean contains(
36+
@Nullable Throwable error,
37+
@NonNull Class<? extends Throwable> causeType
38+
) {
39+
Objects.requireNonNull(causeType);
40+
if (error == null) {
41+
return false;
42+
}
43+
try {
44+
return causeType.isInstance(error) || contains(error.getCause(), causeType);
45+
} catch (Throwable unexpected) {
46+
// May encounter unexpected error during recursive search.
47+
// e.g. StackOverflowError, NoClassDefFoundError, etc.
48+
return false;
49+
}
50+
}
51+
}

aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/MergerTest.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
package com.amplifyframework.datastore.syncengine;
1717

18+
import android.database.sqlite.SQLiteConstraintException;
19+
1820
import com.amplifyframework.AmplifyException;
1921
import com.amplifyframework.core.model.ModelSchema;
2022
import com.amplifyframework.core.model.query.Where;
@@ -25,6 +27,7 @@
2527
import com.amplifyframework.datastore.appsync.ModelWithMetadata;
2628
import com.amplifyframework.datastore.storage.InMemoryStorageAdapter;
2729
import com.amplifyframework.datastore.storage.SynchronousStorageAdapter;
30+
import com.amplifyframework.testmodels.commentsblog.Blog;
2831
import com.amplifyframework.testmodels.commentsblog.BlogOwner;
2932
import com.amplifyframework.testutils.random.RandomString;
3033

@@ -41,6 +44,10 @@
4144

4245
import static org.junit.Assert.assertEquals;
4346
import static org.junit.Assert.assertTrue;
47+
import static org.mockito.ArgumentMatchers.any;
48+
import static org.mockito.ArgumentMatchers.eq;
49+
import static org.mockito.Mockito.doThrow;
50+
import static org.mockito.Mockito.spy;
4451

4552
/**
4653
* Tests the {@link Merger}.
@@ -49,6 +56,7 @@
4956
public final class MergerTest {
5057
private static final long REASONABLE_WAIT_TIME = TimeUnit.SECONDS.toMillis(2);
5158

59+
private InMemoryStorageAdapter inMemoryStorageAdapter;
5260
private SynchronousStorageAdapter storageAdapter;
5361
private MutationOutbox mutationOutbox;
5462
private Merger merger;
@@ -62,7 +70,7 @@ public final class MergerTest {
6270
*/
6371
@Before
6472
public void setup() {
65-
InMemoryStorageAdapter inMemoryStorageAdapter = InMemoryStorageAdapter.create();
73+
this.inMemoryStorageAdapter = spy(InMemoryStorageAdapter.create());
6674
this.storageAdapter = SynchronousStorageAdapter.delegatingTo(inMemoryStorageAdapter);
6775
this.mutationOutbox = new PersistentMutationOutbox(inMemoryStorageAdapter);
6876
VersionRepository versionRepository = new VersionRepository(inMemoryStorageAdapter);
@@ -347,4 +355,39 @@ public void itemWithoutVersionIsNotMerged() throws DataStoreException, Interrupt
347355
storageAdapter.query(ModelMetadata.class, Where.id(existingModel.getId()))
348356
);
349357
}
358+
359+
/**
360+
* Assume item A is dependent on item B, but the remote store has an
361+
* orphaned item A without item B. Then, we try to merge a save for a
362+
* item A. This should gracefully fail, with A not being in the local
363+
* store, at the end.
364+
* @throws DataStoreException On failure to query results for assertions
365+
* @throws InterruptedException If interrupted while awaiting terminal result in test observer
366+
*/
367+
@Test
368+
public void orphanedItemIsNotMerged() throws DataStoreException, InterruptedException {
369+
// Arrange: an item and its parent are not in the local store
370+
BlogOwner badOwner = BlogOwner.builder()
371+
.name("Raphael")
372+
.build();
373+
Blog orphanedBlog = Blog.builder()
374+
.name("How Not To Save Blogs")
375+
.owner(badOwner)
376+
.build();
377+
ModelMetadata metadata = new ModelMetadata(orphanedBlog.getId(), false, 1, Temporal.Timestamp.now());
378+
379+
// Enforce foreign key constraint on in-memory storage adapter
380+
doThrow(SQLiteConstraintException.class)
381+
.when(inMemoryStorageAdapter)
382+
.save(eq(orphanedBlog), any(), any(), any(), any());
383+
384+
// Act: merge a creation for an item
385+
TestObserver<Void> observer = merger.merge(new ModelWithMetadata<>(orphanedBlog, metadata)).test();
386+
assertTrue(observer.await(REASONABLE_WAIT_TIME, TimeUnit.MILLISECONDS));
387+
observer.assertNoErrors().assertComplete();
388+
389+
// Assert: orphaned model was not merged locally
390+
final List<Blog> blogsInStorage = storageAdapter.query(Blog.class);
391+
assertTrue(blogsInStorage.isEmpty());
392+
}
350393
}

0 commit comments

Comments
 (0)