Repository: govuk-one-login/ipv-cri-lib
Pull request #552: OJ-3087: Add retry mechanism and add retires to SessionService#getSessionByAccessToken
Author: login: surajkumar
Created: 2025-03-24T12:54:11Z | Merged: 2025-03-26T09:42:22Z

## Proposed changes

### What changed
* Added a generic class to allow for retry logic
* Added retries to SessionService#getSessionByAccessToken

### Why did it change
We use a GSI in DynamoDB, which provides eventual consistency, meaning reads from the GSI may not immediately reflect the latest record. To address this, we implemented a retry mechanism in getSessionByAccessToken, retrying up to 3 times within 1 second to ensure the record is retrieved.

**Background on GSI**:
DynamoDB provides two consistency models for reads: eventually consistent and strongly consistent. When you read data from a GSI, the default behaviour is eventual consistency, meaning that after a write operation (e.g., an update, insert, or delete), the GSI may not immediately reflect the latest changes. This happens because GSIs are updated asynchronously in the background, and there can be a small delay (typically less than a second, but not guaranteed) before the GSI is fully consistent with the base table.

DynamoDB does not support strongly consistent reads for GSIs, only the base table supports this option.

**Why not use resilience4j**
There is a library called [resilience4j-retry](https://resilience4j.readme.io/docs/retry) which may have done a better job and allow us for greater flexibility. This library was not used to back this PR because it requires a minimum of Java 17 and we are currently on Java 11. 

## Usage
### 1. Setup a config
```java
RetryConfig config = new RetryConfig.Builder().build();
```
This uses the following defaults:
* The code should run 3x at most (maxAttempts)
* The code will wait for 1 second between attempts (delayBetweenAttempts)

To further configure this, you can add `exponentiallyRetry` to retry exponentially and also an `abortCondition` if you want to stop retrying on a specific Exception. 

### 2. Create a Retryable<T>

```java
Retryable<String> retryable = () -> {
   return "Hello, World!";
};
```

If an exception is thrown within the `retryable`, the system will "re-run" the logic up to the `maxAttempts` or if the `abortCondition` is satisfied. 

### 3. Execute the Retryable
```java
String result = RetryManager.execute(config, retryable);
```
Note: On failed execution, the final exception will be thrown to the caller. 

### Issue tracking
- [OJ-3087](https://govukverify.atlassian.net/browse/OJ-3087)


[OJ-3087]: https://govukverify.atlassian.net/browse/OJ-3087?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 7fc16505..98fdeda7 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,10 @@
 # Credential Issuer common libraries Release Notes
 
+## 4.2.0
+
+    - Adds a generic RetryManager that allows for retry logic
+    - Updated SessionService#getSessionByAccessToken to retry on failure, 3x upto 1 second to fix GSI eventual consistentency
+
 ## 4.1.0
 
     - Adds null and blank check to the sessionID passed into validateSession, now throws SessionNotFoundException instead of a DynamoDBException
diff --git a/build.gradle b/build.gradle
index 2dcac3d0..75532540 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,7 +11,7 @@ plugins {
 }
 
 // please update RELEASE_NOTES.md when you update this version
-def buildVersion = "4.1.0"
+def buildVersion = "4.2.0"
 
 defaultTasks 'clean', 'spotlessApply', 'build'
 
diff --git a/src/main/java/uk/gov/di/ipv/cri/common/library/exception/RetryException.java b/src/main/java/uk/gov/di/ipv/cri/common/library/exception/RetryException.java
new file mode 100644
index 00000000..ad9041c7
--- /dev/null
+++ b/src/main/java/uk/gov/di/ipv/cri/common/library/exception/RetryException.java
@@ -0,0 +1,12 @@
+package uk.gov.di.ipv.cri.common.library.exception;
+
+public class RetryException extends RuntimeException {
+
+    public RetryException() {
+        super();
+    }
+
+    public RetryException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/src/main/java/uk/gov/di/ipv/cri/common/library/service/SessionService.java b/src/main/java/uk/gov/di/ipv/cri/common/library/service/SessionService.java
index a9fe187a..1fb7aed7 100644
--- a/src/main/java/uk/gov/di/ipv/cri/common/library/service/SessionService.java
+++ b/src/main/java/uk/gov/di/ipv/cri/common/library/service/SessionService.java
@@ -12,6 +12,9 @@
 import uk.gov.di.ipv.cri.common.library.persistence.DataStore;
 import uk.gov.di.ipv.cri.common.library.persistence.item.SessionItem;
 import uk.gov.di.ipv.cri.common.library.util.ListUtil;
+import uk.gov.di.ipv.cri.common.library.util.retry.RetryConfig;
+import uk.gov.di.ipv.cri.common.library.util.retry.RetryManager;
+import uk.gov.di.ipv.cri.common.library.util.retry.Retryable;
 
 import java.time.Clock;
 import java.util.Optional;
@@ -126,25 +129,42 @@ public SessionItem getSession(String sessionId) {
 
     public SessionItem getSessionByAccessToken(AccessToken accessToken)
             throws SessionExpiredException, AccessTokenExpiredException, SessionNotFoundException {
-        SessionItem sessionItem;
 
-        try {
-            sessionItem =
-                    ListUtil.getOneItemOrThrowError(
-                            dataStore.getItemByIndex(
-                                    SessionItem.ACCESS_TOKEN_INDEX,
-                                    accessToken.toAuthorizationHeader()));
-        } catch (IllegalArgumentException e) {
-            if (e.getMessage().contains("No items found")) {
-                throw new SessionNotFoundException("no session found with that access token");
-            } else {
-                throw new SessionNotFoundException(
-                        "more than one session found with that access token");
-            }
-        }
+        RetryConfig retryConfig =
+                new RetryConfig.Builder()
+                        .delayBetweenAttempts(100)
+                        .maxAttempts(3)
+                        .exponentiallyRetry(true)
+                        .build();
+
+        Retryable<SessionItem> retryable =
+                () -> {
+                    SessionItem sessionItem;
+
+                    try {
+                        sessionItem =
+                                ListUtil.getOneItemOrThrowError(
+                                        dataStore.getItemByIndex(
+                                                SessionItem.ACCESS_TOKEN_INDEX,
+                                                accessToken.toAuthorizationHeader()));
+                    } catch (IllegalArgumentException e) {
+                        if (e.getMessage().contains("No items found")) {
+                            throw new SessionNotFoundException(
+                                    "no session found with that access token");
+                        } else {
+                            throw new SessionNotFoundException(
+                                    "more than one session found with that access token");
+                        }
+                    }
+
+                    // Re-fetch our session directly to avoid problems with projections
+                    sessionItem = validateSessionId(String.valueOf(sessionItem.getSessionId()));
+
+                    return sessionItem;
+                };
+
+        SessionItem sessionItem = RetryManager.execute(retryConfig, retryable);
 
-        // Re-fetch our session directly to avoid problems with projections
-        sessionItem = validateSessionId(String.valueOf(sessionItem.getSessionId()));
         if (sessionItem.getAccessTokenExpiryDate() < clock.instant().getEpochSecond()) {
             throw new AccessTokenExpiredException("access code expired");
         }
diff --git a/src/main/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryConfig.java b/src/main/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryConfig.java
new file mode 100644
index 00000000..a6893bcc
--- /dev/null
+++ b/src/main/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryConfig.java
@@ -0,0 +1,70 @@
+package uk.gov.di.ipv.cri.common.library.util.retry;
+
+import java.util.function.Predicate;
+
+public class RetryConfig {
+    private final int maxAttempts;
+    private final long delayBetweenAttempts;
+    private final boolean exponentiallyRetry;
+    private final Predicate<Exception> abortCondition;
+
+    private RetryConfig(Builder builder) {
+        this.maxAttempts = builder.maxAttempts;
+        this.delayBetweenAttempts = builder.delayBetweenAttempts;
+        this.exponentiallyRetry = builder.exponentiallyRetry;
+        this.abortCondition = builder.abortCondition;
+    }
+
+    public int getMaxAttempts() {
+        return maxAttempts;
+    }
+
+    public long getDelayBetweenAttempts() {
+        return delayBetweenAttempts;
+    }
+
+    public boolean isExponentiallyRetry() {
+        return exponentiallyRetry;
+    }
+
+    public Predicate<Exception> getAbortCondition() {
+        return abortCondition;
+    }
+
+    public static class Builder {
+        private int maxAttempts;
+        private long delayBetweenAttempts;
+        private boolean exponentiallyRetry;
+        private Predicate<Exception> abortCondition;
+
+        public Builder() {
+            this.maxAttempts = 3;
+            this.delayBetweenAttempts = 1000;
+            this.exponentiallyRetry = false;
+        }
+
+        public Builder maxAttempts(int maxAttempts) {
+            this.maxAttempts = maxAttempts;
+            return this;
+        }
+
+        public Builder delayBetweenAttempts(long delayBetweenAttemptsInMs) {
+            this.delayBetweenAttempts = delayBetweenAttemptsInMs;
+            return this;
+        }
+
+        public Builder exponentiallyRetry(boolean exponentiallyRetry) {
+            this.exponentiallyRetry = exponentiallyRetry;
+            return this;
+        }
+
+        public Builder abortOn(Predicate<Exception> condition) {
+            this.abortCondition = condition;
+            return this;
+        }
+
+        public RetryConfig build() {
+            return new RetryConfig(this);
+        }
+    }
+}
diff --git a/src/main/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryManager.java b/src/main/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryManager.java
new file mode 100644
index 00000000..5b94de8a
--- /dev/null
+++ b/src/main/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryManager.java
@@ -0,0 +1,65 @@
+package uk.gov.di.ipv.cri.common.library.util.retry;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import uk.gov.di.ipv.cri.common.library.exception.RetryException;
+
+public class RetryManager {
+    private static final Logger LOGGER = LogManager.getLogger(RetryManager.class);
+
+    private RetryManager() {
+        throw new IllegalStateException("Static class");
+    }
+
+    public static <T> T execute(RetryConfig retryConfig, Retryable<T> retryable) {
+        for (int attempt = 0; attempt < retryConfig.getMaxAttempts(); attempt++) {
+            try {
+                if (attempt > 0) {
+                    long start = System.currentTimeMillis();
+                    long sleepDuration = calculateSleepDuration(retryConfig, attempt);
+                    LOGGER.info(
+                            "Sleeping for {}ms at {}", sleepDuration, System.currentTimeMillis());
+                    Thread.sleep(sleepDuration); // NOSONAR
+                    long elapsed = System.currentTimeMillis() - start;
+                    LOGGER.info("Slept for {}ms", elapsed);
+                }
+
+                T result = retryable.execute();
+
+                if (attempt > 0) {
+                    LOGGER.info("Retry succeeded on attempt {}", attempt);
+                }
+
+                return result;
+
+            } catch (InterruptedException ex) {
+                Thread.currentThread().interrupt();
+                throw new RetryException(ex);
+            } catch (Exception e) {
+                LOGGER.error("Retry attempt {} failed", attempt, e);
+                if (shouldAbortRetry(retryConfig, e, attempt)) {
+                    throw e;
+                }
+            }
+        }
+
+        throw new RetryException();
+    }
+
+    private static boolean shouldAbortRetry(RetryConfig retryConfig, Exception e, int attempt) {
+        return attempt == retryConfig.getMaxAttempts() - 1
+                || (retryConfig.getAbortCondition() != null
+                        && retryConfig.getAbortCondition().test(e));
+    }
+
+    private static long calculateSleepDuration(RetryConfig retryConfig, int attempt) {
+        if (attempt == 0) {
+            return 0;
+        }
+        long duration = retryConfig.getDelayBetweenAttempts();
+        if (retryConfig.isExponentiallyRetry()) {
+            duration = (long) (duration * Math.pow(2, attempt));
+        }
+        return duration;
+    }
+}
diff --git a/src/main/java/uk/gov/di/ipv/cri/common/library/util/retry/Retryable.java b/src/main/java/uk/gov/di/ipv/cri/common/library/util/retry/Retryable.java
new file mode 100644
index 00000000..da40ab51
--- /dev/null
+++ b/src/main/java/uk/gov/di/ipv/cri/common/library/util/retry/Retryable.java
@@ -0,0 +1,5 @@
+package uk.gov.di.ipv.cri.common.library.util.retry;
+
+public interface Retryable<T> {
+    T execute();
+}
diff --git a/src/test/java/uk/gov/di/ipv/cri/common/library/service/SessionServiceTest.java b/src/test/java/uk/gov/di/ipv/cri/common/library/service/SessionServiceTest.java
index 8310f4ba..31c919d6 100644
--- a/src/test/java/uk/gov/di/ipv/cri/common/library/service/SessionServiceTest.java
+++ b/src/test/java/uk/gov/di/ipv/cri/common/library/service/SessionServiceTest.java
@@ -27,6 +27,7 @@
 import java.time.Instant;
 import java.time.ZoneId;
 import java.time.temporal.ChronoUnit;
+import java.util.Collections;
 import java.util.List;
 import java.util.UUID;
 
@@ -193,6 +194,33 @@ void shouldThrowSessionNotFoundWhenSessionIdBlank() {
         verify(mockDataStore, never()).getItem(any());
     }
 
+    @Test
+    void shouldRetryWhenSessionNotFound() {
+        SessionItem item = new SessionItem();
+        AccessToken accessToken = new BearerAccessToken();
+        UUID uuid = UUID.randomUUID();
+
+        long tomorrow = Instant.now().plus(1, ChronoUnit.DAYS).getEpochSecond();
+
+        item.setSessionId(uuid);
+        item.setAuthorizationCode(uuid.toString());
+        item.setAccessToken(accessToken.toAuthorizationHeader());
+        item.setExpiryDate(tomorrow);
+        item.setAuthorizationCodeExpiryDate(tomorrow);
+        item.setAccessTokenExpiryDate(tomorrow);
+
+        when(mockDataStore.getItemByIndex(
+                        SessionItem.ACCESS_TOKEN_INDEX, accessToken.toAuthorizationHeader()))
+                .thenReturn(Collections.emptyList())
+                .thenReturn(Collections.emptyList())
+                .thenReturn(List.of(item));
+
+        when(mockDataStore.getItem(item.getSessionId().toString())).thenReturn(item);
+
+        SessionItem sessionItem = sessionService.getSessionByAccessToken(accessToken);
+        assertThat(sessionItem.getSessionId(), equalTo(item.getSessionId()));
+    }
+
     @Test
     void shouldThrowAuthorizationCodeExpiredException() {
         String authorizationCode = UUID.randomUUID().toString();
diff --git a/src/test/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryManagerTest.java b/src/test/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryManagerTest.java
new file mode 100644
index 00000000..08fa8d64
--- /dev/null
+++ b/src/test/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryManagerTest.java
@@ -0,0 +1,91 @@
+package uk.gov.di.ipv.cri.common.library.util.retry;
+
+import org.junit.jupiter.api.Test;
+import uk.gov.di.ipv.cri.common.library.exception.AccessTokenExpiredException;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class RetryManagerTest {
+
+    @Test
+    void shouldRetryThreeTimes() {
+        String output = "Hello, World!";
+
+        RetryConfig config = new RetryConfig.Builder().maxAttempts(3).build();
+
+        AtomicInteger attempts = new AtomicInteger();
+
+        Retryable<String> retryable =
+                () -> {
+                    if (attempts.getAndIncrement() < 2) {
+                        throw new RuntimeException();
+                    }
+                    return output;
+                };
+
+        String result = RetryManager.execute(config, retryable);
+        assertEquals(output, result);
+        assertEquals(3, attempts.get());
+    }
+
+    @Test
+    void shouldAbortOnExpectedException() {
+        RetryConfig config =
+                new RetryConfig.Builder()
+                        .maxAttempts(3)
+                        .abortOn(AccessTokenExpiredException.class::isInstance)
+                        .build();
+
+        AtomicInteger attempts = new AtomicInteger();
+
+        Retryable<String> retryable =
+                () -> {
+                    if (attempts.getAndIncrement() == 1) {
+                        throw new AccessTokenExpiredException("dummy exception");
+                    } else {
+                        throw new RuntimeException();
+                    }
+                };
+
+        assertThrows(
+                AccessTokenExpiredException.class, () -> RetryManager.execute(config, retryable));
+        assertEquals(2, attempts.get());
+    }
+
+    @Test
+    void shouldRetryExponentially() {
+        String output = "Hello, World!";
+
+        long duration = 1000;
+        long expectedExponentiallyRetry = duration + (duration * 2) + (duration * 3);
+
+        long start = System.currentTimeMillis();
+
+        RetryConfig config =
+                new RetryConfig.Builder()
+                        .maxAttempts(3)
+                        .delayBetweenAttempts(duration)
+                        .exponentiallyRetry(true)
+                        .build();
+
+        AtomicInteger attempts = new AtomicInteger();
+
+        Retryable<String> retryable =
+                () -> {
+                    if (attempts.getAndIncrement() < 2) {
+                        throw new RuntimeException("Retry attempt: " + attempts.get());
+                    }
+                    return output;
+                };
+
+        String result = RetryManager.execute(config, retryable);
+
+        assertEquals(output, result);
+        assertEquals(3, attempts.get());
+        assertTrue((System.currentTimeMillis() - start) >= expectedExponentiallyRetry);
+    }
+}


Reviews:
user: login: jkunle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T13:26:52Z
user: login: jkunle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T13:27:34Z
user: login: smsgds
body: Have approved, look forward to seeing if this helps some of the random session errors.

Just a note the default RetryConfig is still using 1000ms but not an issue.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T17:52:30Z
user: login: Caitlin-cooling
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T09:04:46Z
user: login: Caitlin-cooling
body: Nice! Thanks for the really detailed PR description
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T09:08:03Z
user: login: surajkumar
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T09:52:29Z

Review comments:
diff_hunk: @@ -0,0 +1,89 @@
+package uk.gov.di.ipv.cri.common.library.util.retry;
+
+import org.junit.jupiter.api.Test;
+import uk.gov.di.ipv.cri.common.library.exception.AccessTokenExpiredException;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
path: src/test/java/uk/gov/di/ipv/cri/common/library/util/retry/RetryManagerTest.java
user: login: jkunle
body: Wild card import
created_at: 2025-03-24T13:26:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,6 +193,33 @@ void shouldThrowSessionNotFoundWhenSessionIdBlank() {
         verify(mockDataStore, never()).getItem(any());
     }
 
+    @Test
+    void shouldRetryWhenSessionNotFound() {
+        SessionItem item = new SessionItem();
+        AccessToken accessToken = new BearerAccessToken();
+        UUID uuid = UUID.randomUUID();
+
+        long tomorrow = Instant.now().plus(1, ChronoUnit.DAYS).getEpochSecond();
+
+        item.setSessionId(uuid);
+        item.setAuthorizationCode(uuid.toString());
+        item.setAccessToken(accessToken.toAuthorizationHeader());
+        item.setExpiryDate(tomorrow);
+        item.setAuthorizationCodeExpiryDate(tomorrow);
+        item.setAccessTokenExpiryDate(tomorrow);
+
+        when(mockDataStore.getItemByIndex(
+                        SessionItem.ACCESS_TOKEN_INDEX, accessToken.toAuthorizationHeader()))
+                .thenReturn(List.of())
path: src/test/java/uk/gov/di/ipv/cri/common/library/service/SessionServiceTest.java
user: login: jkunle
body: Collection.emptylist
created_at: 2025-03-24T13:27:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,25 +129,42 @@ public SessionItem getSession(String sessionId) {
 
     public SessionItem getSessionByAccessToken(AccessToken accessToken)
             throws SessionExpiredException, AccessTokenExpiredException, SessionNotFoundException {
-        SessionItem sessionItem;
 
-        try {
-            sessionItem =
-                    ListUtil.getOneItemOrThrowError(
-                            dataStore.getItemByIndex(
-                                    SessionItem.ACCESS_TOKEN_INDEX,
-                                    accessToken.toAuthorizationHeader()));
-        } catch (IllegalArgumentException e) {
-            if (e.getMessage().contains("No items found")) {
-                throw new SessionNotFoundException("no session found with that access token");
-            } else {
-                throw new SessionNotFoundException(
-                        "more than one session found with that access token");
-            }
-        }
+        RetryConfig retryConfig =
+                new RetryConfig.Builder()
+                        .delayBetweenAttempts(100)
+                        .maxAttempts(3)
+                        .exponentiallyRetry(true)
+                        .build();
+
+        Retryable<SessionItem> retryable =
+                () -> {
+                    SessionItem sessionItem;
+
+                    try {
+                        sessionItem =
+                                ListUtil.getOneItemOrThrowError(
+                                        dataStore.getItemByIndex(
+                                                SessionItem.ACCESS_TOKEN_INDEX,
+                                                accessToken.toAuthorizationHeader()));
+                    } catch (IllegalArgumentException e) {
+                        if (e.getMessage().contains("No items found")) {
+                            throw new SessionNotFoundException(
+                                    "no session found with that access token");
path: src/main/java/uk/gov/di/ipv/cri/common/library/service/SessionService.java
user: login: Caitlin-cooling
body: What do you think about putting some logger.errors in here?
created_at: 2025-03-25T09:04:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -126,25 +129,42 @@ public SessionItem getSession(String sessionId) {
 
     public SessionItem getSessionByAccessToken(AccessToken accessToken)
             throws SessionExpiredException, AccessTokenExpiredException, SessionNotFoundException {
-        SessionItem sessionItem;
 
-        try {
-            sessionItem =
-                    ListUtil.getOneItemOrThrowError(
-                            dataStore.getItemByIndex(
-                                    SessionItem.ACCESS_TOKEN_INDEX,
-                                    accessToken.toAuthorizationHeader()));
-        } catch (IllegalArgumentException e) {
-            if (e.getMessage().contains("No items found")) {
-                throw new SessionNotFoundException("no session found with that access token");
-            } else {
-                throw new SessionNotFoundException(
-                        "more than one session found with that access token");
-            }
-        }
+        RetryConfig retryConfig =
+                new RetryConfig.Builder()
+                        .delayBetweenAttempts(100)
+                        .maxAttempts(3)
+                        .exponentiallyRetry(true)
+                        .build();
+
+        Retryable<SessionItem> retryable =
+                () -> {
+                    SessionItem sessionItem;
+
+                    try {
+                        sessionItem =
+                                ListUtil.getOneItemOrThrowError(
+                                        dataStore.getItemByIndex(
+                                                SessionItem.ACCESS_TOKEN_INDEX,
+                                                accessToken.toAuthorizationHeader()));
+                    } catch (IllegalArgumentException e) {
+                        if (e.getMessage().contains("No items found")) {
+                            throw new SessionNotFoundException(
+                                    "no session found with that access token");
path: src/main/java/uk/gov/di/ipv/cri/common/library/service/SessionService.java
user: login: surajkumar
body: It'll bubble down: `LOGGER.error("Retry attempt {} failed", attempt, e);` to this line 
created_at: 2025-03-25T09:52:29Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-24T15:19:35Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ipv-cri-lib&pullRequest=552) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=ipv-cri-lib&pullRequest=552&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ipv-cri-lib&pullRequest=552&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ipv-cri-lib&pullRequest=552&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [84.1% Coverage on New Code](https://sonarcloud.io/component_measures?id=ipv-cri-lib&pullRequest=552&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ipv-cri-lib&pullRequest=552&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ipv-cri-lib&pullRequest=552)


author_association: NONE
user: login: surajkumar
created_at: 2025-03-25T09:54:03Z
body: @smsgds - the one defined in the RetryConfig is just a default however we use the following config:

```
RetryConfig retryConfig =
                new RetryConfig.Builder()
                        .delayBetweenAttempts(100)
                        .maxAttempts(3)
                        .exponentiallyRetry(true)
                        .build();
```

This is defined in the SessionService. Hope this satisfies your concern. 
author_association: MEMBER