code: private boolean handle(Throwable ex) {
          ex = Exceptions.unwrap(ex);
          if (this.secondEntry == null && isRetryable(ex)) {
              // This is the first attempt and we caught a retry-eligible exception; issue the query for the new entry.
              CompletableReadResultEntry newEntry = this.retryGetEntry.apply(getStreamSegmentOffset(), this.firstEntry.getRequestedReadLength(), this.redirectedSegmentId);
              if (!(newEntry instanceof RedirectedReadResultEntry)) {
                  // Request the content for the new entry (if that fails, we do not change any state).
                  newEntry.requestContent(this.timer == null ? DEFAULT_TIMEOUT : timer.getRemaining());
                  assert newEntry.getStreamSegmentOffset() == this.adjustedOffset : "new entry's StreamSegmentOffset does not match the adjusted offset of this entry";
                  assert newEntry.getRequestedReadLength() == this.firstEntry.getRequestedReadLength() : "new entry does not have the same RequestedReadLength";
  
                  // After all checks are done, update the internal state to use the new entry.
                  newEntry.setCompletionCallback(this.firstEntry.getCompletionCallback());
                  this.secondEntry = newEntry;
                  setOutcomeAfterSecondEntry();
                  return true;
              }
          }
  
          return false;
      }
docs: Handles an exception that was caught. If this is the first exception ever caught, and it is eligible for retries,
  then this method will invoke the retryGetEntry that was passed through the constructor to get a new entry.
  If that succeeds, the new entry is then used to serve up the result.
  <p>
  The new entry will only be accepted if it is not a RedirectedReadResultEntry (since that is likely to get us in the
  same situation again).
  
  @param ex      The exception to inspect.
  @return True if the exception was handled properly and the base entry swapped, false otherwise.