apache/druid · error · IllegalArgumentException

Invalid maxAttempts[ ] in retry policy

Error message

Invalid maxAttempts[%d] in retry policy

What it means

ServiceClientImpl's constructor rejects a ServiceRetryPolicy whose maxAttempts is 0, since a client that never attempts any request is useless. maxAttempts must be at least 1; anything else throws an IllegalArgumentException.

Solutions

  1. Set retryPolicy.maxAttempts() to at least 1 (e.g., ServiceRetryPolicy.basic() or maxAttempts(3)).
  2. If 0 means 'no retries', convert it: maxAttempts = retries + 1.
  3. Add validation at config load time so 0 is rejected or coerced before reaching ServiceClientImpl.

Example fix

// before
ServiceRetryPolicy policy = new ServiceRetryPolicy(0, backoffFactory);
// after
ServiceRetryPolicy policy = new ServiceRetryPolicy(3, backoffFactory); // or retries + 1
Defensive patterns

Strategy: validation

Validate before calling

if (retryPolicy.maxAttempts() < 1) {
  retryPolicy = new ServiceRetryPolicy(1, retryPolicy.backoffFactory()); // or retries + 1
}

Try / catch

try { return new ServiceClientImpl(httpClient, locator, retryPolicy, connectExec); } catch (IllegalArgumentException e) { if (e.getMessage().startsWith("Invalid maxAttempts")) { return new ServiceClientImpl(httpClient, locator, retryPolicy.withMaxAttempts(3), connectExec); } throw e; }

Prevention

When it happens

Trigger: Building a ServiceClientImpl with a ServiceRetryPolicy built as maxAttempts(0), often from config or a computation that produced 0 (e.g., default minus one, unset counter).

Common situations: Config parsing yielding 0 retries and being passed directly as maxAttempts; confusion between 'retry count' and 'attempt count' semantics when constructing the policy.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/a825f33d9530a070. Report an issue: GitHub.

Appendix: source

Thrown at server/src/main/java/org/apache/druid/rpc/ServiceClientImpl.java:81

  // Populated when we receive a redirect. The location here has no base path; it only identifies a preferred server.
  private final AtomicReference<ServiceLocation> preferredLocationNoPath = new AtomicReference<>();

  public ServiceClientImpl(
      final String serviceName,
      final HttpClient httpClient,
      final ServiceLocator serviceLocator,
      final ServiceRetryPolicy retryPolicy,
      final ScheduledExecutorService connectExec
  )
  {
    this.serviceName = Preconditions.checkNotNull(serviceName, "serviceName");
    this.httpClient = Preconditions.checkNotNull(httpClient, "httpClient");
    this.serviceLocator = Preconditions.checkNotNull(serviceLocator, "serviceLocator");
    this.retryPolicy = Preconditions.checkNotNull(retryPolicy, "retryPolicy");
    this.connectExec = Preconditions.checkNotNull(connectExec, "connectExec");

    if (retryPolicy.maxAttempts() == 0) {
      throw new IAE("Invalid maxAttempts[%d] in retry policy", retryPolicy.maxAttempts());
    }
  }

  @VisibleForTesting
  public static long computeBackoffMs(final ServiceRetryPolicy retryPolicy, final long attemptNumber)
  {
    return Math.max(
        retryPolicy.minWaitMillis(),
        Math.min(retryPolicy.maxWaitMillis(), (long) (Math.pow(2, attemptNumber) * retryPolicy.minWaitMillis()))
    );
  }

  @Override
  public <IntermediateType, FinalType> ListenableFuture<FinalType> asyncRequest(
      final RequestBuilder requestBuilder,
      final HttpResponseHandler<IntermediateType, FinalType> handler
  )
  {

View on GitHub (pinned to 9b90983fd2)