apache/beam · error · IllegalStateException

Expected state to be PENDING or STARTED, but was COMPLETE_ER

Error message

Expected state to be PENDING or STARTED, but was COMPLETE_ERROR

What it means

AttemptState.checkActive() throws IllegalStateException when the attempt already ended with COMPLETE_ERROR. Once an attempt has been marked failed via completeWithError(), it can no longer be treated as active. This enforces the Qos attempt state machine so failed attempts cannot consume budget or proceed.

Source

Thrown at sdks/java/io/google-cloud-platform/src/main/java/org/apache/beam/sdk/io/gcp/firestore/RpcQosImpl.java:192

        Sum.ofLongs());
  }

  private enum AttemptState {
    PENDING,
    STARTED,
    COMPLETE_SUCCESS,
    COMPLETE_ERROR;

    public void checkActive() {
      switch (this) {
        case PENDING:
        case STARTED:
          return;
        case COMPLETE_SUCCESS:
          throw new IllegalStateException(
              "Expected state to be PENDING or STARTED, but was COMPLETE_SUCCESS");
        case COMPLETE_ERROR:
          throw new IllegalStateException(
              "Expected state to be PENDING or STARTED, but was COMPLETE_ERROR");
      }
    }

    public void checkStarted() {
      switch (this) {
        case STARTED:
          return;
        case PENDING:
          throw new IllegalStateException("Expected state to be STARTED, but was PENDING");
        case COMPLETE_SUCCESS:
          throw new IllegalStateException("Expected state to be STARTED, but was COMPLETE_SUCCESS");
        case COMPLETE_ERROR:
          throw new IllegalStateException("Expected state to be STARTED, but was COMPLETE_ERROR");
      }
    }
  }

View on GitHub (pinned to 12126d8942)

Solutions

  1. Create a fresh attempt with qos.newAttempt() before retrying after an error.
  2. Call checkActive()/checkStarted() only at the beginning of an attempt's lifecycle, before any completion call.
  3. Ensure completeWithError() is called exactly once and the attempt is abandoned afterward.
  4. Serialize access to the Attempt if it is shared across threads.

Example fix

// before
try {
  attempt.checkActive();
  doRpc();
} catch (Exception e) {
  attempt.completeWithError(e);
}
attempt.checkActive(); // throws: attempt already failed
// after
try {
  attempt.checkActive();
  doRpc();
} catch (Exception e) {
  attempt.completeWithError(e);
}
attempt = qos.newAttempt(); // new attempt for the retry
attempt.checkActive();
Defensive patterns

Strategy: retry

Validate before calling

// Java: only retry with a fresh attempt after completeWithError()
try {
  attempt.checkActive();
} catch (IllegalStateException e) {
  attempt = qos.newAttempt();
}

Type guard

boolean attemptActive(RpcQos.Attempt attempt) {
  try { attempt.checkActive(); return true; }
  catch (IllegalStateException e) { return false; }
}

Try / catch

try {
  attempt.checkActive();
  doRpc();
} catch (Exception rpcError) {
  attempt.completeWithError(rpcError);
  attempt = qos.newAttempt(); // fresh attempt for retry
  attempt.start();
}

Prevention

When it happens

Trigger: Calling checkActive() on an attempt after an RPC failed and completeWithError() was invoked — e.g. retry logic that continues to use the same failed attempt instead of creating a new one.

Common situations: Retry loops that catch an exception but keep using the old Attempt object; rethrow paths that double-complete an attempt; concurrent completion and check from different threads.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/93568635424b500a. Report an issue: GitHub.