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 #1190

Closed
wants to merge 3 commits into from
Closed
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
2 changes: 2 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,8 @@ 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
5 changes: 5 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,8 @@
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 @@ -35,6 +37,7 @@ class ParseRESTCommand extends ParseRequest<JSONObject> {
/* package */ static final String HEADER_OS_VERSION = "X-Parse-OS-Version";

/* package */ static final String HEADER_INSTALLATION_ID = "X-Parse-Installation-Id";
/* package */ static final String HEADER_REQUEST_ID = "X-Parse-Request-Id";
/* package */ static final String USER_AGENT = "User-Agent";
private static final String HEADER_SESSION_TOKEN = "X-Parse-Session-Token";
private static final String HEADER_MASTER_KEY = "X-Parse-Master-Key";
This conversation was marked as resolved.
Show resolved Hide resolved
Expand All @@ -49,6 +52,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 +219,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
22 changes: 11 additions & 11 deletions parse/src/test/java/com/parse/ParseRESTUserCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void testGetCurrentUserCommand() {
@Test
public void testLogInUserCommand() throws Exception {
ParseRESTUserCommand command =
ParseRESTUserCommand.logInUserCommand("userName", "password", true);
ParseRESTUserCommand.logInUserCommand("userName", "password", true);

assertEquals("login", command.httpPath);
assertEquals(ParseHttpRequest.Method.GET, command.method);
Expand All @@ -68,7 +68,7 @@ public void testLogInUserCommand() throws Exception {
@Test
public void testResetPasswordResetCommand() throws Exception {
ParseRESTUserCommand command =
ParseRESTUserCommand.resetPasswordResetCommand("test@parse.com");
ParseRESTUserCommand.resetPasswordResetCommand("test@parse.com");

assertEquals("requestPasswordReset", command.httpPath);
assertEquals(ParseHttpRequest.Method.POST, command.method);
Expand All @@ -82,7 +82,7 @@ public void testSignUpUserCommand() throws Exception {
JSONObject parameters = new JSONObject();
parameters.put("key", "value");
ParseRESTUserCommand command =
ParseRESTUserCommand.signUpUserCommand(parameters, "sessionToken", true);
ParseRESTUserCommand.signUpUserCommand(parameters, "sessionToken", true);

assertEquals("users", command.httpPath);
assertEquals(ParseHttpRequest.Method.POST, command.method);
Expand All @@ -96,7 +96,7 @@ public void testServiceLogInUserCommandWithParameters() throws Exception {
JSONObject parameters = new JSONObject();
parameters.put("key", "value");
ParseRESTUserCommand command =
ParseRESTUserCommand.serviceLogInUserCommand(parameters, "sessionToken", true);
ParseRESTUserCommand.serviceLogInUserCommand(parameters, "sessionToken", true);

assertEquals("users", command.httpPath);
assertEquals(ParseHttpRequest.Method.POST, command.method);
Expand All @@ -110,7 +110,7 @@ public void testServiceLogInUserCommandWithAuthType() throws Exception {
Map<String, String> facebookAuthData = new HashMap<>();
facebookAuthData.put("token", "test");
ParseRESTUserCommand command =
ParseRESTUserCommand.serviceLogInUserCommand("facebook", facebookAuthData, true);
ParseRESTUserCommand.serviceLogInUserCommand("facebook", facebookAuthData, true);

assertEquals("users", command.httpPath);
assertEquals(ParseHttpRequest.Method.POST, command.method);
Expand All @@ -132,7 +132,7 @@ public void testAddAdditionalHeaders() throws Exception {
JSONObject parameters = new JSONObject();
parameters.put("key", "value");
ParseRESTUserCommand command =
ParseRESTUserCommand.signUpUserCommand(parameters, "sessionToken", true);
ParseRESTUserCommand.signUpUserCommand(parameters, "sessionToken", true);

ParseHttpRequest.Builder requestBuilder = new ParseHttpRequest.Builder();
command.addAdditionalHeaders(requestBuilder);
Expand All @@ -153,11 +153,11 @@ public void testOnResponseAsync() {
int statusCode = 200;

ParseHttpResponse response =
new ParseHttpResponse.Builder()
.setContent(new ByteArrayInputStream(content.getBytes()))
.setContentType(contentType)
.setStatusCode(statusCode)
.build();
new ParseHttpResponse.Builder()
.setContent(new ByteArrayInputStream(content.getBytes()))
.setContentType(contentType)
.setStatusCode(statusCode)
.build();
command.onResponseAsync(response, null);

assertEquals(200, command.getStatusCode());
Expand Down
24 changes: 24 additions & 0 deletions parse/src/test/java/com/parse/ParseRequestTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
Expand All @@ -24,8 +26,11 @@
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
Expand Down Expand Up @@ -121,6 +126,25 @@ public void testDownloadProgress() throws Exception {
assertProgressCompletedSuccessfully(downloadProgressCallback);
}

@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(5)).execute(any(ParseHttpRequest.class));
}

private static class TestProgressCallback implements ProgressCallback {
final List<Integer> history = new LinkedList<>();

Expand Down
Loading