Skip to content

Commit a6270d7

Browse files
committed
feat: Add idempotency feature to detect duplicate requests due to network conditions
1 parent a08c5d1 commit a6270d7

File tree

3 files changed

+39
-0
lines changed

3 files changed

+39
-0
lines changed

parse/src/main/java/com/parse/ParseException.java

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ public class ParseException extends Exception {
102102
public static final int FILE_DELETE_ERROR = 153;
103103
/** Error code indicating that the application has exceeded its request limit. */
104104
public static final int REQUEST_LIMIT_EXCEEDED = 155;
105+
/** Error code indicating that the request was a duplicate and has been discarded due to idempotency rules. */
106+
public static final int DUPLICATE_REQUEST = 159;
105107
/** Error code indicating that the provided event name is invalid. */
106108
public static final int INVALID_EVENT_NAME = 160;
107109
/** Error code indicating that the username is missing or empty. */

parse/src/main/java/com/parse/ParseRESTCommand.java

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Collections;
2121
import java.util.Iterator;
2222
import java.util.Map;
23+
import java.util.UUID;
2324
import org.json.JSONArray;
2425
import org.json.JSONException;
2526
import org.json.JSONObject;
@@ -33,6 +34,7 @@ class ParseRESTCommand extends ParseRequest<JSONObject> {
3334
/* package */ static final String HEADER_APP_BUILD_VERSION = "X-Parse-App-Build-Version";
3435
/* package */ static final String HEADER_APP_DISPLAY_VERSION = "X-Parse-App-Display-Version";
3536
/* package */ static final String HEADER_OS_VERSION = "X-Parse-OS-Version";
37+
/* package */ static final String HEADER_REQUEST_ID = "X-Parse-Request-Id";
3638

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

5457
public ParseRESTCommand(
@@ -215,6 +218,7 @@ protected void addAdditionalHeaders(ParseHttpRequest.Builder requestBuilder) {
215218
if (masterKey != null) {
216219
requestBuilder.addHeader(HEADER_MASTER_KEY, masterKey);
217220
}
221+
requestBuilder.addHeader(HEADER_REQUEST_ID, requestId);
218222
}
219223

220224
@Override

parse/src/test/java/com/parse/ParseRESTCommandTest.java

+33
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010

1111
import static org.junit.Assert.assertEquals;
1212
import static org.junit.Assert.assertFalse;
13+
import static org.junit.Assert.assertNotNull;
1314
import static org.junit.Assert.assertNull;
1415
import static org.junit.Assert.assertTrue;
1516
import static org.mockito.ArgumentMatchers.any;
17+
import static org.mockito.ArgumentMatchers.argThat;
1618
import static org.mockito.Mockito.doNothing;
1719
import static org.mockito.Mockito.doThrow;
1820
import static org.mockito.Mockito.mock;
@@ -30,6 +32,8 @@
3032
import java.io.InputStream;
3133
import java.net.URL;
3234
import java.util.Collections;
35+
import java.util.concurrent.atomic.AtomicReference;
36+
3337
import org.json.JSONArray;
3438
import org.json.JSONObject;
3539
import org.junit.After;
@@ -552,4 +556,33 @@ public void testSaveObjectCommandUpdate() {
552556
ParsePlugins.reset();
553557
Parse.destroy();
554558
}
559+
560+
@Test
561+
public void testIdempotencyLogic() throws Exception {
562+
ParseHttpClient mockHttpClient = mock(ParseHttpClient.class);
563+
AtomicReference<String> requestIdAtomicReference = new AtomicReference<>();
564+
when(mockHttpClient.execute(
565+
argThat(
566+
argument -> {
567+
assertNotNull(
568+
argument.getHeader(ParseRESTCommand.HEADER_REQUEST_ID));
569+
if (requestIdAtomicReference.get() == null)
570+
requestIdAtomicReference.set(
571+
argument.getHeader(
572+
ParseRESTCommand.HEADER_REQUEST_ID));
573+
assertEquals(
574+
argument.getHeader(ParseRESTCommand.HEADER_REQUEST_ID),
575+
requestIdAtomicReference.get());
576+
return true;
577+
})))
578+
.thenThrow(new IOException());
579+
580+
ParseRESTCommand.server = new URL("http://parse.com");
581+
ParseRESTCommand command = new ParseRESTCommand.Builder().build();
582+
Task<Void> task = command.executeAsync(mockHttpClient).makeVoid();
583+
task.waitForCompletion();
584+
585+
verify(mockHttpClient, times(ParseRequest.DEFAULT_MAX_RETRIES + 1))
586+
.execute(any(ParseHttpRequest.class));
587+
}
555588
}

0 commit comments

Comments
 (0)