Skip to content

Commit

Permalink
feat: Add idempotency feature to detect duplicate requests due to net…
Browse files Browse the repository at this point in the history
…work conditions
  • Loading branch information
abubkr-hago committed Aug 15, 2024
1 parent a08c5d1 commit a6270d7
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 0 deletions.
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
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
33 changes: 33 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,8 @@
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 +556,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));
}
}

0 comments on commit a6270d7

Please sign in to comment.