Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add idempotency feature to detect duplicate requests due to network conditions #1208

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions parse/src/main/java/com/parse/ParseException.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ public class ParseException extends Exception {
public static final int FILE_DELETE_ERROR = 153;
/** Error code indicating that the application has exceeded its request limit. */
public static final int REQUEST_LIMIT_EXCEEDED = 155;
/**
* Error code indicating that the request was a duplicate and has been discarded due to
* idempotency rules.
*/
public static final int DUPLICATE_REQUEST = 159;
/** Error code indicating that the provided event name is invalid. */
public static final int INVALID_EVENT_NAME = 160;
/** Error code indicating that the username is missing or empty. */
Expand Down
4 changes: 4 additions & 0 deletions parse/src/main/java/com/parse/ParseRESTCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
Expand All @@ -33,6 +34,7 @@ class ParseRESTCommand extends ParseRequest<JSONObject> {
/* package */ static final String HEADER_APP_BUILD_VERSION = "X-Parse-App-Build-Version";
/* package */ static final String HEADER_APP_DISPLAY_VERSION = "X-Parse-App-Display-Version";
/* package */ static final String HEADER_OS_VERSION = "X-Parse-OS-Version";
/* package */ static final String HEADER_REQUEST_ID = "X-Parse-Request-Id";

/* package */ static final String HEADER_INSTALLATION_ID = "X-Parse-Installation-Id";
/* package */ static final String USER_AGENT = "User-Agent";
Expand All @@ -49,6 +51,7 @@ class ParseRESTCommand extends ParseRequest<JSONObject> {
/* package */ String httpPath;
private String installationId;
private String operationSetUUID;
private final String requestId = UUID.randomUUID().toString();
private String localId;

public ParseRESTCommand(
Expand Down Expand Up @@ -215,6 +218,7 @@ protected void addAdditionalHeaders(ParseHttpRequest.Builder requestBuilder) {
if (masterKey != null) {
requestBuilder.addHeader(HEADER_MASTER_KEY, masterKey);
}
requestBuilder.addHeader(HEADER_REQUEST_ID, requestId);
}

@Override
Expand Down
32 changes: 32 additions & 0 deletions parse/src/test/java/com/parse/ParseRESTCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
Expand All @@ -30,6 +32,7 @@
import java.io.InputStream;
import java.net.URL;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.After;
Expand Down Expand Up @@ -552,4 +555,33 @@ public void testSaveObjectCommandUpdate() {
ParsePlugins.reset();
Parse.destroy();
}

@Test
public void testIdempotencyLogic() throws Exception {
ParseHttpClient mockHttpClient = mock(ParseHttpClient.class);
AtomicReference<String> requestIdAtomicReference = new AtomicReference<>();
when(mockHttpClient.execute(
argThat(
argument -> {
assertNotNull(
argument.getHeader(ParseRESTCommand.HEADER_REQUEST_ID));
if (requestIdAtomicReference.get() == null)
requestIdAtomicReference.set(
argument.getHeader(
ParseRESTCommand.HEADER_REQUEST_ID));
assertEquals(
argument.getHeader(ParseRESTCommand.HEADER_REQUEST_ID),
requestIdAtomicReference.get());
return true;
})))
.thenThrow(new IOException());

ParseRESTCommand.server = new URL("http://parse.com");
ParseRESTCommand command = new ParseRESTCommand.Builder().build();
Task<Void> task = command.executeAsync(mockHttpClient).makeVoid();
task.waitForCompletion();

verify(mockHttpClient, times(ParseRequest.DEFAULT_MAX_RETRIES + 1))
.execute(any(ParseHttpRequest.class));
}
}
Loading