9292
9393public class ShopifySdk {
9494
95+ private static final String MINIMUM_REQUEST_RETRY_DELAY_CANNOT_BE_LARGER_THAN_MAXIMUM_REQUEST_RETRY_DELAY_MESSAGE = "Maximum request retry delay must be larger than minimum request retry delay." ;
96+
97+ private static final String INVALID_MAXIMUM_REQUEST_RETRY_TIMEOUT_MESSAGE = "Maximum request retry timeout cannot be set lower than 2 seconds." ;
98+
99+ private static final String INVALID_MAXIMUM_REQUEST_RETRY_DELAY_MESSAGE = "Maximum request retry delay cannot be set lower than 2 seconds." ;
100+
101+ private static final String INVALID_MINIMUM_REQUEST_RETRY_DELAY_MESSAGE = "Minimum request retry delay cannot be set lower than 1 second." ;
102+
95103 private static final Logger LOGGER = LoggerFactory .getLogger (ShopifySdk .class );
96104
97105 private static final String HTTPS = "https://" ;
@@ -141,13 +149,17 @@ public class ShopifySdk {
141149
142150 private static final String SHOP_RETRIEVED_MESSAGE = "Starting to make calls for Shopify store with ID of {} and name of {}" ;
143151 private static final String COULD_NOT_BE_SAVED_SHOPIFY_ERROR_MESSAGE = "could not successfully be saved" ;
144- private static final String RETRY_ATTEMPT_MESSAGE = "Waited {} seconds since first retry attempt. This is attempt {}. Please review the following failed request information.\n Request Location of {}\n Response Status Code of {}\n Response Headers of:\n {}\n Response Body of:\n {}" ;
145152 private static final String RETRY_FAILED_MESSAGE = "Request retry has failed." ;
146153 private static final String DEPRECATED_SHOPIFY_CALL_ERROR_MESSAGE = "Shopify call is deprecated. Please take note of the X-Shopify-API-Deprecated-Reason and correct the call.\n Request Location of {}\n Response Status Code of {}\n Response Headers of:\n {}" ;
147154 static final String GENERAL_ACCESS_TOKEN_EXCEPTION_MESSAGE = "There was a problem generating access token using shop subdomain of %s and authorization code of %s." ;
148155
149- private static final int ONE_MINUTE_IN_MILLISECONDS = 60000 ;
150- private static final int FIVE_MINUTES_IN_MILLISECONDS = 300000 ;
156+ private static final Long TWO_SECONDS_IN_MILLISECONDS = 2000L ;
157+ private static final Long ONE_SECOND_IN_MILLISECONDS = 1000L ;
158+ private static final Long DEFAULT_MAXIMUM_REQUEST_RETRY_TIMEOUT_IN_MILLISECONDS = 180000L ;
159+ private static final Long DEFAULT_MAXIMUM_REQUEST_RETRY_RANDOM_DELAY_IN_MILLISECONDS = 5000L ;
160+ private static final Long DEFAULT_MINIMUM_REQUEST_RETRY_RANDOM_DELAY_IN_MILLISECONDS = 1000L ;
161+ private static final long DEFAULT_READ_TIMEOUT_IN_MILLISECONDS = 15000L ;
162+ private static final long DEFAULT_CONNECTION_TIMEOUT_IN_MILLISECONDS = 60000L ;
151163
152164 private String shopSubdomain ;
153165 private String apiUrl ;
@@ -156,15 +168,76 @@ public class ShopifySdk {
156168 private String authorizationToken ;
157169 private WebTarget webTarget ;
158170 private String accessToken ;
171+ private long minimumRequestRetryRandomDelayMilliseconds ;
172+ private long maximumRequestRetryRandomDelayMilliseconds ;
173+ private long maximumRequestRetryTimeoutMilliseconds ;
159174
160175 private static final Client CLIENT = buildClient ();
161176
162- public static interface BuildStep {
177+ public static interface OptionalsStep {
178+
179+ /**
180+ * The Shopify SDK uses random waits in between retry attempts. Minimum duration
181+ * time to wait before retrying a failed request. Value must also be less than
182+ * {@link #withMaximumRequestRetryRandomDelay(int, TimeUnit) Maximum Request
183+ * Retry Random Delay}.<br>
184+ * Default value is: 1 second.
185+ *
186+ * @param duration
187+ * @param timeUnit
188+ * @return {@link OptionalsStep}
189+ */
190+ OptionalsStep withMinimumRequestRetryRandomDelay (int duration , TimeUnit timeUnit );
191+
192+ /**
193+ * The Shopify SDK uses random waits in between retry attempts. Maximum duration
194+ * time to wait before retrying a failed request. Value must also be more than
195+ * {@link #withMinimumRequestRetryRandomDelay(int, TimeUnit) Minimum Request
196+ * Retry Random Delay}.<br>
197+ * Default value is: 5 seconds.
198+ *
199+ * @param duration
200+ * @param timeUnit
201+ * @return {@link OptionalsStep}
202+ */
203+ OptionalsStep withMaximumRequestRetryRandomDelay (int duration , TimeUnit timeUnit );
204+
205+ /**
206+ * Maximum duration time to keep attempting requests <br>
207+ * Default value is: 3 minutes.
208+ *
209+ * @param duration
210+ * @param timeUnit
211+ * @return {@link OptionalsStep}
212+ */
213+ OptionalsStep withMaximumRequestRetryTimeout (int duration , TimeUnit timeUnit );
214+
215+ /**
216+ * The duration to wait when connecting to Shopify's API. <br>
217+ * Default value is: 1 minute.
218+ *
219+ * @param duration
220+ * @param timeUnit
221+ * @return {@link OptionalsStep}
222+ */
223+ OptionalsStep withConnectionTimeout (int duration , TimeUnit timeUnit );
224+
225+ /**
226+ * The duration to attempt to read a response from Shopify's API. <br>
227+ * Default value is: 15 seconds.
228+ *
229+ * @param duration
230+ * @param timeUnit
231+ * @return {@link OptionalsStep}
232+ */
233+ OptionalsStep withReadTimeout (int duration , TimeUnit timeUnit );
234+
163235 ShopifySdk build ();
236+
164237 }
165238
166239 public static interface AuthorizationTokenStep {
167- BuildStep withAuthorizationToken (final String authorizationToken );
240+ OptionalsStep withAuthorizationToken (final String authorizationToken );
168241
169242 }
170243
@@ -174,7 +247,7 @@ public static interface ClientSecretStep {
174247 }
175248
176249 public static interface AccessTokenStep {
177- BuildStep withAccessToken (final String accessToken );
250+ OptionalsStep withAccessToken (final String accessToken );
178251
179252 ClientSecretStep withClientId (final String clientId );
180253 }
@@ -197,26 +270,56 @@ protected ShopifySdk(final Steps steps) {
197270 this .clientSecret = steps .clientSecret ;
198271 this .authorizationToken = steps .authorizationToken ;
199272 this .apiUrl = steps .apiUrl ;
273+ this .minimumRequestRetryRandomDelayMilliseconds = steps .minimumRequestRetryRandomDelayMilliseconds ;
274+ this .maximumRequestRetryRandomDelayMilliseconds = steps .maximumRequestRetryRandomDelayMilliseconds ;
275+ this .maximumRequestRetryTimeoutMilliseconds = steps .maximumRequestRetryTimeoutMilliseconds ;
276+
277+ CLIENT .property (ClientProperties .CONNECT_TIMEOUT , Math .toIntExact (steps .connectionTimeoutMilliseconds ));
278+ CLIENT .property (ClientProperties .READ_TIMEOUT , Math .toIntExact (steps .readTimeoutMilliseconds ));
279+ validateConstructionOfShopifySdk ();
200280 }
201281
202282 }
203283
284+ private void validateConstructionOfShopifySdk () {
285+ if (this .minimumRequestRetryRandomDelayMilliseconds < ONE_SECOND_IN_MILLISECONDS ) {
286+ throw new IllegalArgumentException (INVALID_MINIMUM_REQUEST_RETRY_DELAY_MESSAGE );
287+ }
288+ if (this .maximumRequestRetryRandomDelayMilliseconds < TWO_SECONDS_IN_MILLISECONDS ) {
289+ throw new IllegalArgumentException (INVALID_MAXIMUM_REQUEST_RETRY_DELAY_MESSAGE );
290+ }
291+ if (this .maximumRequestRetryTimeoutMilliseconds < TWO_SECONDS_IN_MILLISECONDS ) {
292+ throw new IllegalArgumentException (INVALID_MAXIMUM_REQUEST_RETRY_TIMEOUT_MESSAGE );
293+ }
294+
295+ if (minimumRequestRetryRandomDelayMilliseconds > maximumRequestRetryRandomDelayMilliseconds ) {
296+ throw new IllegalArgumentException (
297+ MINIMUM_REQUEST_RETRY_DELAY_CANNOT_BE_LARGER_THAN_MAXIMUM_REQUEST_RETRY_DELAY_MESSAGE );
298+ }
299+ }
300+
204301 protected static class Steps
205- implements SubdomainStep , ClientSecretStep , AuthorizationTokenStep , AccessTokenStep , BuildStep {
302+ implements SubdomainStep , ClientSecretStep , AuthorizationTokenStep , AccessTokenStep , OptionalsStep {
303+
206304 private String subdomain ;
207305 private String accessToken ;
208306 private String clientId ;
209307 private String clientSecret ;
210308 private String authorizationToken ;
211309 private String apiUrl ;
310+ private long minimumRequestRetryRandomDelayMilliseconds = DEFAULT_MINIMUM_REQUEST_RETRY_RANDOM_DELAY_IN_MILLISECONDS ;
311+ private long maximumRequestRetryRandomDelayMilliseconds = DEFAULT_MAXIMUM_REQUEST_RETRY_RANDOM_DELAY_IN_MILLISECONDS ;
312+ private long maximumRequestRetryTimeoutMilliseconds = DEFAULT_MAXIMUM_REQUEST_RETRY_TIMEOUT_IN_MILLISECONDS ;
313+ private long connectionTimeoutMilliseconds = DEFAULT_CONNECTION_TIMEOUT_IN_MILLISECONDS ;
314+ private long readTimeoutMilliseconds = DEFAULT_READ_TIMEOUT_IN_MILLISECONDS ;
212315
213316 @ Override
214317 public ShopifySdk build () {
215318 return new ShopifySdk (this );
216319 }
217320
218321 @ Override
219- public BuildStep withAccessToken (final String accessToken ) {
322+ public OptionalsStep withAccessToken (final String accessToken ) {
220323 this .accessToken = accessToken ;
221324 return this ;
222325 }
@@ -240,7 +343,7 @@ public ClientSecretStep withClientId(final String clientId) {
240343 }
241344
242345 @ Override
243- public BuildStep withAuthorizationToken (final String authorizationToken ) {
346+ public OptionalsStep withAuthorizationToken (final String authorizationToken ) {
244347 this .authorizationToken = authorizationToken ;
245348 return this ;
246349 }
@@ -251,6 +354,36 @@ public AuthorizationTokenStep withClientSecret(final String clientSecret) {
251354 return this ;
252355 }
253356
357+ @ Override
358+ public OptionalsStep withMinimumRequestRetryRandomDelay (final int duration , final TimeUnit timeUnit ) {
359+ this .minimumRequestRetryRandomDelayMilliseconds = timeUnit .toMillis (duration );
360+ return this ;
361+ }
362+
363+ @ Override
364+ public OptionalsStep withMaximumRequestRetryRandomDelay (final int duration , final TimeUnit timeUnit ) {
365+ this .maximumRequestRetryRandomDelayMilliseconds = timeUnit .toMillis (duration );
366+ return this ;
367+ }
368+
369+ @ Override
370+ public OptionalsStep withMaximumRequestRetryTimeout (final int duration , final TimeUnit timeUnit ) {
371+ this .maximumRequestRetryTimeoutMilliseconds = timeUnit .toMillis (duration );
372+ return this ;
373+ }
374+
375+ @ Override
376+ public OptionalsStep withConnectionTimeout (final int duration , final TimeUnit timeUnit ) {
377+ this .connectionTimeoutMilliseconds = timeUnit .toMillis (duration );
378+ return this ;
379+ }
380+
381+ @ Override
382+ public OptionalsStep withReadTimeout (final int duration , final TimeUnit timeUnit ) {
383+ this .readTimeoutMilliseconds = timeUnit .toMillis (duration );
384+ return this ;
385+ }
386+
254387 }
255388
256389 public boolean revokeOAuthToken () {
@@ -462,6 +595,7 @@ public ShopifyFulfillment createFulfillment(
462595 final ShopifyFulfillmentCreationRequest shopifyFulfillmentCreationRequest ) {
463596 final ShopifyFulfillmentRoot shopifyFulfillmentRoot = new ShopifyFulfillmentRoot ();
464597 final ShopifyFulfillment shopifyFulfillment = shopifyFulfillmentCreationRequest .getRequest ();
598+
465599 shopifyFulfillmentRoot .setFulfillment (shopifyFulfillment );
466600 final Response response = post (
467601 getWebTarget ().path (ORDERS ).path (shopifyFulfillment .getOrderId ()).path (FULFILLMENTS ),
@@ -703,10 +837,11 @@ private Response invokeResponseCallable(final Callable<Response> responseCallabl
703837 }
704838
705839 private Retryer <Response > buildResponseRetyer () {
706-
707- return RetryerBuilder .<Response > newBuilder ().retryIfResult (ShopifySdk ::shouldRetryResponse )
708- .withWaitStrategy (WaitStrategies .randomWait (2 , TimeUnit .SECONDS , 30 , TimeUnit .SECONDS ))
709- .withStopStrategy (StopStrategies .stopAfterDelay (15 , TimeUnit .MINUTES ))
840+ return RetryerBuilder .<Response >newBuilder ().retryIfResult (ShopifySdk ::shouldRetryResponse ).retryIfException ()
841+ .withWaitStrategy (WaitStrategies .randomWait (minimumRequestRetryRandomDelayMilliseconds ,
842+ TimeUnit .MILLISECONDS , maximumRequestRetryRandomDelayMilliseconds , TimeUnit .MILLISECONDS ))
843+ .withStopStrategy (
844+ StopStrategies .stopAfterDelay (maximumRequestRetryTimeoutMilliseconds , TimeUnit .MILLISECONDS ))
710845 .withRetryListener (new ShopifySdkRetryListener ()).build ();
711846 }
712847
@@ -777,9 +912,8 @@ private static Client buildClient() {
777912 final ObjectMapper mapper = buildMapper ();
778913 final JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider ();
779914 provider .setMapper (mapper );
780- return ClientBuilder .newClient ().property (ClientProperties .CONNECT_TIMEOUT , ONE_MINUTE_IN_MILLISECONDS )
781- .register (JacksonFeature .class ).property (ClientProperties .READ_TIMEOUT , FIVE_MINUTES_IN_MILLISECONDS )
782- .register (provider );
915+
916+ return ClientBuilder .newClient ().register (JacksonFeature .class ).register (provider );
783917 }
784918
785919 static ObjectMapper buildMapper () {
@@ -797,21 +931,33 @@ static ObjectMapper buildMapper() {
797931
798932 private static class ShopifySdkRetryListener implements RetryListener {
799933
934+ private static final String RETRY_EXCEPTION_ATTEMPT_MESSAGE = "An exception occurred while making an API call to shopify on attempt number {} and {} seconds since first attempt with exception {}" ;
935+ private static final String RETRY_INVALID_RESPONSE_ATTEMPT_MESSAGE = "Waited {} seconds since first retry attempt. This is attempt {}. Please review the following failed request information.\n Request Location of {}\n Response Status Code of {}\n Response Headers of:\n {}\n Response Body of:\n {}" ;
936+
800937 @ Override
801938 public <V > void onRetry (final Attempt <V > attempt ) {
802939 if (LOGGER .isWarnEnabled () && attempt .hasResult ()) {
803940 final Response response = (Response ) attempt .getResult ();
804941 response .bufferEntity ();
805942 if (shouldRetryResponse (response ) && !hasExceededRateLimit (response )) {
806- final long delaySinceFirstAttemptInMilliseconds = attempt .getDelaySinceFirstAttempt ();
807- final long delaySinceFirstAttemptInSeconds = TimeUnit .SECONDS
808- .convert (delaySinceFirstAttemptInMilliseconds , TimeUnit .MILLISECONDS );
809- LOGGER .warn (RETRY_ATTEMPT_MESSAGE , delaySinceFirstAttemptInSeconds , attempt .getAttemptNumber (),
810- response .getLocation (), response .getStatus (), response .getStringHeaders (),
811- response .readEntity (String .class ));
943+ final long delaySinceFirstAttemptInSeconds = convertMillisecondsToSeconds (
944+ attempt .getDelaySinceFirstAttempt ());
945+ LOGGER .warn (RETRY_INVALID_RESPONSE_ATTEMPT_MESSAGE , delaySinceFirstAttemptInSeconds ,
946+ attempt .getAttemptNumber (), response .getLocation (), response .getStatus (),
947+ response .getStringHeaders (), response .readEntity (String .class ));
812948 }
949+ } else if (LOGGER .isWarnEnabled () && attempt .hasException ()) {
950+
951+ final long delaySinceFirstAttemptInSeconds = convertMillisecondsToSeconds (
952+ attempt .getDelaySinceFirstAttempt ());
953+ LOGGER .warn (RETRY_EXCEPTION_ATTEMPT_MESSAGE , attempt .getAttemptNumber (),
954+ delaySinceFirstAttemptInSeconds , attempt .getExceptionCause ());
813955 }
814956 }
957+
958+ private long convertMillisecondsToSeconds (final long milliiseconds ) {
959+ return TimeUnit .SECONDS .convert (milliiseconds , TimeUnit .MILLISECONDS );
960+ }
815961 }
816962
817963}
0 commit comments