title: Seeing unexpected behavior with async and retry spring boot
body: <p>I have a use case to call multiple REST APIS in parallel. But catch here is, they need an access token to be passed in header of each request. So, I ended up making this class level variable. There's also retry to happen in case of timeout or expired token. In case of expired token, we call loadToken() method to get new token and retry the same call. we have a max retries of 3 times.</p>
  <p>Issue:</p>
  <ol>
  <li>One issue I see is loadToken() can be called by multiple threads at same time and there could be a race condition here.</li>
  <li>In prd, I am seeing very weird behaviour. Even for 500 errors, the retry is kicking loadToken() method which should not happen. Only loadToken() should trigger in case of expired token.</li>
  </ol>
  <p>Any thoughts/ suggestions here to improve this code. Thanks for any help.</p>
  <p>Code looks like this:</p>
  <pre class="lang-java prettyprint-override"><code>@Component
  public class MyDaoImpl implements MyDao {
      
      @Autowired
      private RestTemplate restTemplate;
      
      private String token;
  
      @PostConstruct
      public void loadToken() throws CustomException {
          HttpHeaders headers = new HttpHeaders();
          headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
          MultiValueMap&lt;String, String&gt; map= new LinkedMultiValueMap&lt;&gt;();
          HttpEntity&lt;MultiValueMap&lt;String, String&gt;&gt; request = new HttpEntity&lt;&gt;(map, headers);
          try {
              ResponseEntity&lt;TokenResponse&gt; exchange = restTemplate.exchange(tokenUrl, HttpMethod.POST, request, TokenResponse.class);
              token = exchange.getBody()!=null &amp;&amp; StringUtils.isNotBlank(exchange.getBody().getToken()) ? exchange.getBody().getToken() : null;
          } catch (Exception e) {
              throw e; 
          }
      }
  
      @Override
      @Async(&quot;threadPoolExecutor&quot;)
      @Retryable(maxAttemptsExpression = &quot;#{${retry.max.attempts:3}}&quot;, value = {UnAuthorizedException.class, GatewayTimeoutException.class}, backoff = @Backoff(delayExpression = &quot;#{${retry.backoff.period:1000}}&quot;))
      public CompletableFuture&lt;String&gt; getResponse1(String string) throws CustomException {
          HttpHeaders headers = new HttpHeaders();
          headers.setBearerAuth(getAccessToken());
          HttpEntity&lt;String&gt; request = new HttpEntity&lt;&gt;(headers);
          try {
              ResponseEntity&lt;byte[]&gt; exchange = restTemplate.exchange(url, HttpMethod.GET, request, byte[].class);
              if(exchange.getBody()==null) return CompletableFuture.completedFuture(null);
              return CompletableFuture.completedFuture(Base64.getEncoder().encodeToString(exchange.getBody()));
          } catch (HttpClientErrorException | HttpServerErrorException e) {
              if(e.getStatusCode()==HttpStatus.UNAUTHORIZED) {
                  loadToken();
                  throw new UnAuthorizedException(&quot;Expired Token&quot;, e);
              }
          } catch (Exception e) {
              if(StringUtils.isNotEmpty(e.getMessage()) &amp;&amp; (e.getMessage().contains(&quot;I/O error on &quot;) || e.getMessage().contains(&quot;timeout&quot;))) {
                  throw new GatewayTimeoutException(&quot;Gateway_Timeout&quot;, e);
              }
          }
          return CompletableFuture.completedFuture(null);
      }
  
      @Override
      @Async(&quot;threadPoolExecutor&quot;)
      @Retryable(maxAttemptsExpression = &quot;#{${retry.max.attempts:3}}&quot;, value = {UnAuthorizedException.class, GatewayTimeoutException.class}, backoff = @Backoff(delayExpression = &quot;#{${retry.backoff.period:1000}}&quot;))
      public CompletableFuture&lt;SomeResponse&gt; getResponse2(String string) throws CustomException {
          HttpHeaders headers = new HttpHeaders();
          headers.setBearerAuth(getAccessToken());
          HttpEntity&lt;String&gt; request = new HttpEntity&lt;&gt;(headers);
          try {
              final ResponseEntity&lt;Response&gt; exchange = restTemplate.exchange(someurl, HttpMethod.GET, request, Response.class);
              return CompletableFuture.completedFuture(exchange.getBody());
          } catch (HttpClientErrorException | HttpServerErrorException e) {
              if(e.getStatusCode()==HttpStatus.UNAUTHORIZED) {
                  loadToken();
                  throw new UnAuthorizedException(&quot;Expired Token&quot;, e);
              }
          } catch (Exception e) {
              if(StringUtils.isNotEmpty(e.getMessage()) &amp;&amp; (e.getMessage().contains(&quot;I/O error on &quot;) || e.getMessage().contains(&quot;timeout&quot;))) {
                  throw new GatewayTimeoutException(&quot;Gateway_Timeout&quot;, e);
              }
          }
          return CompletableFuture.completedFuture(null);
      }
  
      @Override
      public String getAccessToken() throws JbException {
          if(StringUtils.isBlank(token)) {
              loadToken();
          }
          return token;
      }
  
      @Override
      @Async(&quot;threadPoolExecutor&quot;)
      @Retryable(maxAttemptsExpression = &quot;#{${retry.max.attempts:3}}&quot;, value = {UnAuthorizedException.class, GatewayTimeoutException.class}, backoff = @Backoff(delayExpression = &quot;#{${retry.backoff.period:1000}}&quot;))
      public CompletableFuture&lt;List&lt;Response3&gt;&gt; getResponse3(String string) throws CustomException {
          HttpHeaders headers = new HttpHeaders();
          headers.setContentType(MediaType.APPLICATION_JSON);
          headers.setBearerAuth(getAccessToken());
          HttpEntity&lt;String&gt; request = new HttpEntity&lt;&gt;(&quot;{}&quot;, headers);
          try {
              ResponseEntity&lt;List&lt;CobrandCard&gt;&gt; exchange = restTemplate.exchange(someUrl2, HttpMethod.POST, request, new ParameterizedTypeReference&lt;List&lt;Response3&gt;&gt;() {});
              return CompletableFuture.completedFuture(exchange.getBody());
          } catch (HttpClientErrorException | HttpServerErrorException e) {
              if(e.getStatusCode()==HttpStatus.UNAUTHORIZED) {
                  loadToken();
                  throw new UnAuthorizedException(&quot;Expired Token&quot;, e);
              }
          } catch (Exception e) {
              if(StringUtils.isNotEmpty(e.getMessage()) &amp;&amp; (e.getMessage().contains(&quot;I/O error on &quot;) || e.getMessage().contains(&quot;timeout&quot;))) {
                  throw new GatewayTimeoutException(&quot;Gateway_Timeout&quot;, e);
              }
          }
          return CompletableFuture.completedFuture(null);
      }
  
      @Override
      @Async(&quot;threadPoolExecutor&quot;)
      @Retryable(maxAttemptsExpression = &quot;#{${retry.max.attempts:3}}&quot;, value = {UnAuthorizedException.class, GatewayTimeoutException.class}, backoff = @Backoff(delayExpression = &quot;#{${retry.backoff.period:1000}}&quot;))
      public CompletableFuture&lt;List&lt;Response4&gt;&gt; getTier(String string) throws CustomException {
          HttpHeaders headers = new HttpHeaders();
          headers.setContentType(MediaType.APPLICATION_JSON);
          headers.setBearerAuth(getAccessToken());
          HttpEntity&lt;String&gt; request = new HttpEntity&lt;&gt;(headers);
          try {
              ResponseEntity&lt;List&lt;Response4&gt;&gt; exchange = restTemplate.exchange(finalUrl, HttpMethod.GET, request, new ParameterizedTypeReference&lt;List&lt;Response4&gt;&gt;() {});
              return CompletableFuture.completedFuture(exchange.getBody());
          } catch (HttpClientErrorException | HttpServerErrorException e) {
              if(e.getStatusCode()==HttpStatus.UNAUTHORIZED) {
                  loadToken();
                  throw new UnAuthorizedException(&quot;Expired Token&quot;, e);
              }
          } catch (Exception e) {
              if(StringUtils.isNotEmpty(e.getMessage()) &amp;&amp; (e.getMessage().contains(&quot;I/O error on &quot;) || e.getMessage().contains(&quot;timeout&quot;))) {
                  throw new GatewayTimeoutException(&quot;Gateway_Timeout&quot;, e);
              }
          }
          return CompletableFuture.completedFuture(null);
      }
  
  }
  </code></pre>