From 1dd1fd9959837438896bd4aa39f8de5c426a23b7 Mon Sep 17 00:00:00 2001 From: Bommas Date: Wed, 23 Jan 2019 17:47:25 -0800 Subject: [PATCH] Support for Mutation Queue handling and bug fixes. (#106) * Bug fix to handle scenario where mutation is canceled in its own callback. * Fixed bug in setting mutation timeout and added support for checking if mutation queue is empty and for clearing it. * Updated Core SDK dependency, Changelog and made version bump changes * Changed method name to "isMutationQueueEmpty" --- CHANGELOG.md | 10 + .../com/apollographql/android/Version.kt | 2 +- .../AWSAppSyncQueryInstrumentationTest.java | 198 ++++++++++++++++++ .../appsync/AWSAppSyncClient.java | 33 ++- .../AppSyncOfflineMutationManager.java | 11 +- .../InMemoryOfflineMutationManager.java | 14 +- .../PersistentOfflineMutationManager.java | 5 + gradle.properties | 4 +- 8 files changed, 265 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f15c78f5..9ec9e92d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log - AWS AppSync SDK for Android +## [Release 2.7.6](https://github.com/awslabs/aws-mobile-appsync-sdk-android/releases/tag/release_v2.7.6) + +### Misc. Updates +* `AWSAppSync` now depends on `AWSCore` version `2.11.0` instead of `2.10.1`. +* Added support to check if mutation queue is empty and to clear mutation queue. See [issue #96](https://github.com/awslabs/aws-mobile-appsync-sdk-android/issues/96), and [issue #101](https://github.com/awslabs/aws-mobile-appsync-sdk-android/issues/101) + +### Bug Fixes +* Fixed bug in mutationQueue Execution set method. [issue #105](https://github.com/awslabs/aws-mobile-appsync-sdk-android/issues/105) +* Fixed bug in mutation processing logic to handle case where cancel is called in the mutation callback. See [issue #102](https://github.com/awslabs/aws-mobile-appsync-sdk-android/issues/102) + ## [Release 2.7.5](https://github.com/awslabs/aws-mobile-appsync-sdk-android/releases/tag/release_v2.7.5) ### Misc. Updates diff --git a/aws-android-sdk-appsync-compiler/src/generated/kotlin/com/apollographql/android/Version.kt b/aws-android-sdk-appsync-compiler/src/generated/kotlin/com/apollographql/android/Version.kt index fc11be2d..e2eac5cb 100644 --- a/aws-android-sdk-appsync-compiler/src/generated/kotlin/com/apollographql/android/Version.kt +++ b/aws-android-sdk-appsync-compiler/src/generated/kotlin/com/apollographql/android/Version.kt @@ -1,3 +1,3 @@ // Generated file. Do not edit! package com.apollographql.android -val VERSION = "2.7.5" +val VERSION = "2.7.6" diff --git a/aws-android-sdk-appsync-tests/src/androidTest/java/com/amazonaws/mobileconnectors/appsync/AWSAppSyncQueryInstrumentationTest.java b/aws-android-sdk-appsync-tests/src/androidTest/java/com/amazonaws/mobileconnectors/appsync/AWSAppSyncQueryInstrumentationTest.java index 5b1f000e..690a1691 100644 --- a/aws-android-sdk-appsync-tests/src/androidTest/java/com/amazonaws/mobileconnectors/appsync/AWSAppSyncQueryInstrumentationTest.java +++ b/aws-android-sdk-appsync-tests/src/androidTest/java/com/amazonaws/mobileconnectors/appsync/AWSAppSyncQueryInstrumentationTest.java @@ -1008,6 +1008,204 @@ public void testUpdateWithInvalidID() { } + @Test + public void testCancelMutationWithinCallback() { + + AWSAppSyncClient awsAppSyncClient = AppSyncTestSetupHelper.createAppSyncClientWithIAM(); + assertNotNull(awsAppSyncClient); + final CountDownLatch add2CountDownlatch = new CountDownLatch(1); + + AddPostMutation.Data expected = new AddPostMutation.Data(new AddPostMutation.CreatePost( + "Post", + "", + "", + "", + "", + "", + null, + null, + 0 + )); + + CreatePostInput createPostInput1 = CreatePostInput.builder() + .title("L.A. Woman") + .author("Doors" + System.currentTimeMillis()) + .url("Doors.com") + .content("City at night") + .ups(new Integer(1)) + .downs(new Integer(0)) + .build(); + + AddPostMutation addPostMutation1 = AddPostMutation.builder().input(createPostInput1).build(); + final AppSyncMutationCall call1 = awsAppSyncClient.mutate(addPostMutation1, expected); + + CreatePostInput createPostInput2 = CreatePostInput.builder() + .title("Break On Through") + .author("Doors" + System.currentTimeMillis()) + .url("Doors.com") + .content("To the other side") + .ups(new Integer(1)) + .downs(new Integer(0)) + .build(); + AddPostMutation addPostMutation2 = AddPostMutation.builder().input(createPostInput2).build(); + final AppSyncMutationCall call2 = awsAppSyncClient.mutate(addPostMutation2, expected); + + + call1.enqueue(new GraphQLCall.Callback() { + @Override + public void onResponse(@Nonnull final Response response) { + call1.cancel(); + } + + @Override + public void onFailure(@Nonnull final ApolloException e) { + e.printStackTrace(); + assertTrue("OnError received for first mutation. Unexpected", false); + } + }); + + call2.enqueue(new GraphQLCall.Callback() { + @Override + public void onResponse(@Nonnull final Response response) { + call2.cancel(); + add2CountDownlatch.countDown(); + } + + @Override + public void onFailure(@Nonnull final ApolloException e) { + e.printStackTrace(); + assertTrue("OnError received for Second mutation. Unexpected", false); + add2CountDownlatch.countDown(); + } + }); + + try { + add2CountDownlatch.await(60, TimeUnit.SECONDS); + } catch (InterruptedException iex) { + iex.printStackTrace(); + } + + } + + @Test + public void testMutationQueueEmpty() { + AWSAppSyncClient awsAppSyncClient = AppSyncTestSetupHelper.createAppSyncClientWithIAM(true,2*1000); + assertNotNull(awsAppSyncClient); + assertTrue(awsAppSyncClient.isMutationQueueEmpty()); + + final CountDownLatch addCountdownlatch = new CountDownLatch(1); + + AddPostMutation.Data expected = new AddPostMutation.Data(new AddPostMutation.CreatePost( + "Post", + "", + "", + "", + "", + "", + null, + null, + 0 + )); + + CreatePostInput createPostInput = CreatePostInput.builder() + .title("Lonely Day") + .author("SOAD" + System.currentTimeMillis()) + .url("SOAD.com") + .content("Such a lonely day") + .ups(new Integer(1)) + .downs(new Integer(0)) + .build(); + + AddPostMutation addPostMutation = AddPostMutation.builder().input(createPostInput).build(); + final AppSyncMutationCall call = awsAppSyncClient.mutate(addPostMutation, expected); + + call.enqueue(new GraphQLCall.Callback() { + @Override + public void onResponse(@Nonnull final Response response) { + call.cancel(); + addCountdownlatch.countDown(); + } + + @Override + public void onFailure(@Nonnull final ApolloException e) { + e.printStackTrace(); + assertTrue("OnError received for Second mutation. Unexpected", false); + addCountdownlatch.countDown(); + } + }); + assertFalse(awsAppSyncClient.isMutationQueueEmpty()); + try { + addCountdownlatch.await(60, TimeUnit.SECONDS); + } catch (InterruptedException iex) { + iex.printStackTrace(); + } + assertTrue(awsAppSyncClient.isMutationQueueEmpty()); + + } + + @Test + public void testMutationQueueClear() { + AWSAppSyncClient awsAppSyncClient = AppSyncTestSetupHelper.createAppSyncClientWithIAM(true,2*1000); + assertNotNull(awsAppSyncClient); + assertTrue(awsAppSyncClient.isMutationQueueEmpty()); + + final int lastMutation = 10; + final CountDownLatch addCountdownlatch = new CountDownLatch(1); + + for (int i = 0; i < lastMutation; i++ ) { + final int currentIteration = i; + AddPostMutation.Data expected = new AddPostMutation.Data(new AddPostMutation.CreatePost( + "Post", + "", + "", + "", + "", + "", + null, + null, + 0 + )); + + CreatePostInput createPostInput = CreatePostInput.builder() + .title("Lonely Day") + .author("SOAD" + System.currentTimeMillis()) + .url("SOAD.com") + .content("Such a lonely day") + .ups(new Integer(1)) + .downs(new Integer(0)) + .build(); + + AddPostMutation addPostMutation = AddPostMutation.builder().input(createPostInput).build(); + AppSyncMutationCall call = awsAppSyncClient.mutate(addPostMutation, expected); + + call.enqueue(new GraphQLCall.Callback() { + @Override + public void onResponse(@Nonnull final Response response) { + if (currentIteration == lastMutation) { + addCountdownlatch.countDown(); + } + } + + @Override + public void onFailure(@Nonnull final ApolloException e) { + if (currentIteration == lastMutation) { + addCountdownlatch.countDown(); + } + } + }); + } + assertFalse(awsAppSyncClient.isMutationQueueEmpty()); + awsAppSyncClient.clearMutationQueue(); + assertTrue(awsAppSyncClient.isMutationQueueEmpty()); + + try { + assertFalse(addCountdownlatch.await(60, TimeUnit.SECONDS)); + } catch (InterruptedException iex) { + iex.printStackTrace(); + } + + } + private void queryPosts(AWSAppSyncClient awsAppSyncClient, final ResponseFetcher responseFetcher) { diff --git a/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/AWSAppSyncClient.java b/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/AWSAppSyncClient.java index 703aa6d9..757eb736 100644 --- a/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/AWSAppSyncClient.java +++ b/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/AWSAppSyncClient.java @@ -83,6 +83,7 @@ public class AWSAppSyncClient { //Map that houses retried mutations. private Map mutationsToRetryAfterConflictResolution; + private AppSyncOfflineMutationManager mAppSyncOfflineMutationManager = null; private enum AuthMode { API_KEY("API_KEY"), @@ -172,16 +173,18 @@ private AWSAppSyncClient(AWSAppSyncClient.Builder builder) { builder.mPersistentMutationsCallback, builder.mS3ObjectManager); - //Create the Apollo Client and setup the interceptor chain. + mAppSyncOfflineMutationManager = new AppSyncOfflineMutationManager(builder.mContext, + builder.customTypeAdapters, + sqlCacheOperations, + networkInvoker); + + //Create the Apollo Client and setup the interceptor chain. ApolloClient.Builder clientBuilder = ApolloClient.builder() .serverUrl(builder.mServerUrl) .normalizedCache(builder.mNormalizedCacheFactory, builder.mResolver) .addApplicationInterceptor(optimisticUpdateInterceptor) .addApplicationInterceptor(new AppSyncOfflineMutationInterceptor( - new AppSyncOfflineMutationManager(builder.mContext, - builder.customTypeAdapters, - sqlCacheOperations, - networkInvoker), + mAppSyncOfflineMutationManager, false, builder.mContext, mutationsToRetryAfterConflictResolution, @@ -427,7 +430,7 @@ public Builder subscriptionsAutoReconnect( boolean subscriptionsAutoReconnect) { * @return */ public Builder mutationQueueExecutionTimeout(long mutationQueueExecutionTimeout) { - mutationQueueExecutionTimeout = mutationQueueExecutionTimeout; + mMutationQueueExecutionTimeout = mutationQueueExecutionTimeout; return this; } @@ -698,4 +701,22 @@ public Cancelable sync( return this.sync(baseQuery, baseQueryCallback, subscription, subscriptionCallback, null, null, 0 ); } + /** + * Used to check if the mutation queue is empty. + * @return true if queue is empty, false otherwise. + */ + public boolean isMutationQueueEmpty() { + if (mAppSyncOfflineMutationManager != null ) { + return mAppSyncOfflineMutationManager.mutationQueueEmpty(); + } + return true; + } + + /** + * Clear the mutation queue. A Mutation that is currently in progress will continue to execute until finished. + * + */ + public void clearMutationQueue() { + mAppSyncOfflineMutationManager.clearMutationQueue(); + } } diff --git a/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/AppSyncOfflineMutationManager.java b/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/AppSyncOfflineMutationManager.java index 6dcc562b..ae28bf43 100644 --- a/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/AppSyncOfflineMutationManager.java +++ b/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/AppSyncOfflineMutationManager.java @@ -217,7 +217,7 @@ public void processNextInQueueMutation() { void setInProgressMutationAsCompleted(String recordIdentifier) { persistentOfflineMutationManager.removePersistentMutationObject(recordIdentifier); - inMemoryOfflineMutationManager.removeFirstInQueue(); + inMemoryOfflineMutationManager.removeFromQueue(recordIdentifier); queueHandler.setMutationInProgressStatusToFalse(); queueHandler.clearInMemoryOfflineMutationObjectBeingExecuted(); queueHandler.clearPersistentOfflineMutationObjectBeingExecuted(); @@ -362,5 +362,14 @@ String getClientStateFromMutation(Mutation mutation) { } return clientState; } + + boolean mutationQueueEmpty() { + return ( persistentOfflineMutationManager.isQueueEmpty() && inMemoryOfflineMutationManager.isQueueEmpty()); + } + + void clearMutationQueue(){ + inMemoryOfflineMutationManager.clearMutationQueue(); + persistentOfflineMutationManager.clearMutationQueue(); + } } diff --git a/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/InMemoryOfflineMutationManager.java b/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/InMemoryOfflineMutationManager.java index 354954e2..79cae61c 100644 --- a/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/InMemoryOfflineMutationManager.java +++ b/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/InMemoryOfflineMutationManager.java @@ -52,10 +52,13 @@ public void addMutationObjectInQueue(InMemoryOfflineMutationObject object) { } } - public InMemoryOfflineMutationObject removeFirstInQueue() { + public InMemoryOfflineMutationObject removeFromQueue(String recordIdentifer) { synchronized ( lock ) { if (!inMemoryOfflineMutationObjects.isEmpty() ) { - return inMemoryOfflineMutationObjects.remove(0); + InMemoryOfflineMutationObject mutationObject = inMemoryOfflineMutationObjects.get(0); + if (mutationObject != null && recordIdentifer.equals(mutationObject.recordIdentifier)) { + return inMemoryOfflineMutationObjects.remove(0); + } } } return null; @@ -107,4 +110,11 @@ InMemoryOfflineMutationObject getMutationObject(Mutation m) { } return null; } + + void clearMutationQueue() { + synchronized (lock) { + inMemoryOfflineMutationObjects.clear(); + cancelledMutations.clear(); + } + } } diff --git a/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/PersistentOfflineMutationManager.java b/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/PersistentOfflineMutationManager.java index 91504029..768d1199 100644 --- a/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/PersistentOfflineMutationManager.java +++ b/aws-android-sdk-appsync/src/main/java/com/amazonaws/mobileconnectors/appsync/PersistentOfflineMutationManager.java @@ -133,4 +133,9 @@ synchronized void removeTimedoutMutation(PersistentOfflineMutationObject p ) { synchronized Set getTimedoutMutations() { return timedOutMutations; } + + synchronized void clearMutationQueue() { + mutationSqlCacheOperations.clearCurrentCache(); + persistentOfflineMutationObjectList.clear(); + } } diff --git a/gradle.properties b/gradle.properties index f309b5b8..efe76b94 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,8 +13,8 @@ org.gradle.jvmargs=-Xmx1536m org.gradle.parallel=true GROUP=com.amazonaws -VERSION_NAME=2.7.5 -AWS_CORE_SDK_VERSION=2.10.1 +VERSION_NAME=2.7.6 +AWS_CORE_SDK_VERSION=2.11.0 POM_URL=https://github.com/awslabs/aws-mobile-appsync-sdk-android POM_SCM_URL=https://github.com/awslabs/aws-mobile-appsync-sdk-android