From a6270d7efc98024f998d2572548852c00c8c4008 Mon Sep 17 00:00:00 2001 From: Abubkr Hago <24502430+abubkr-hago@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:13:01 +0400 Subject: [PATCH] feat: Add idempotency feature to detect duplicate requests due to network conditions --- .../main/java/com/parse/ParseException.java | 2 ++ .../main/java/com/parse/ParseRESTCommand.java | 4 +++ .../java/com/parse/ParseRESTCommandTest.java | 33 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/parse/src/main/java/com/parse/ParseException.java b/parse/src/main/java/com/parse/ParseException.java index 5dc3b5f90..972a8b4aa 100644 --- a/parse/src/main/java/com/parse/ParseException.java +++ b/parse/src/main/java/com/parse/ParseException.java @@ -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. */ diff --git a/parse/src/main/java/com/parse/ParseRESTCommand.java b/parse/src/main/java/com/parse/ParseRESTCommand.java index 60cd1c590..20ac6ae12 100644 --- a/parse/src/main/java/com/parse/ParseRESTCommand.java +++ b/parse/src/main/java/com/parse/ParseRESTCommand.java @@ -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; @@ -33,6 +34,7 @@ class ParseRESTCommand extends ParseRequest { /* 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"; @@ -49,6 +51,7 @@ class ParseRESTCommand extends ParseRequest { /* package */ String httpPath; private String installationId; private String operationSetUUID; + private final String requestId = UUID.randomUUID().toString(); private String localId; public ParseRESTCommand( @@ -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 diff --git a/parse/src/test/java/com/parse/ParseRESTCommandTest.java b/parse/src/test/java/com/parse/ParseRESTCommandTest.java index 9bfbb7f50..5d1b79c34 100644 --- a/parse/src/test/java/com/parse/ParseRESTCommandTest.java +++ b/parse/src/test/java/com/parse/ParseRESTCommandTest.java @@ -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; @@ -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; @@ -552,4 +556,33 @@ public void testSaveObjectCommandUpdate() { ParsePlugins.reset(); Parse.destroy(); } + + @Test + public void testIdempotencyLogic() throws Exception { + ParseHttpClient mockHttpClient = mock(ParseHttpClient.class); + AtomicReference 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 task = command.executeAsync(mockHttpClient).makeVoid(); + task.waitForCompletion(); + + verify(mockHttpClient, times(ParseRequest.DEFAULT_MAX_RETRIES + 1)) + .execute(any(ParseHttpRequest.class)); + } }