public <T> T run(Operation<T> f) {
    TaskState retryState = state.nestedState(stateKey);
    TaskState operationState = retryState.nestedState(OPERATION);

    T result;

    try {
      result = f.perform(operationState);
    } catch (TaskExecutionException e) {
      throw e;
    } catch (Exception e) {
      String formattedErrorMessage = String.format(errorMessage, errorMessageParameters);

      if (!retry(e)) {
        logger.warn("{}: giving up", formattedErrorMessage, e);
        throw new TaskExecutionException(e, TaskExecutionException.buildExceptionErrorConfig(e));
      }

      int retryIteration = retryState.params().get(RETRY, int.class, 0);
      retryState.params().set(RETRY, retryIteration + 1);
      int interval =
          (int)
              Math.min(
                  retryInterval.min().getSeconds() * Math.pow(2, retryIteration),
                  retryInterval.max().getSeconds());
      logger.warn("{}: retrying in {} seconds", formattedErrorMessage, interval, e);
      throw state.pollingTaskExecutionException(interval);
    }

    // Clear retry state
    retryState.params().remove(RETRY);

    return result;
  }