apache/beam · error · IllegalStateException

Expected state to be STARTED, but was COMPLETE_SUCCESS

Error message

Expected state to be STARTED, but was COMPLETE_SUCCESS

What it means

AttemptState.checkStarted() throws IllegalStateException when the attempt already finished successfully (COMPLETE_SUCCESS). A completed attempt can no longer be used to start or continue RPC work. This prevents double-completion and stale attempts from being reused after a successful operation.

Source

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

        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");
      }
    }
  }

  private abstract class BaseRpcAttempt implements RpcAttempt {
    private final Logger logger;
    final O11y o11y;
    final StatusCodeAwareBackoff backoff;
    final Sleeper sleeper;

    AttemptState state;
    Instant start;

    @SuppressWarnings(
        "initialization.fields.uninitialized") // allow transient fields to be managed by component
    // lifecycle

View on GitHub (pinned to 12126d8942)

Solutions

  1. Create a new attempt via qos.newAttempt() for every operation; never reuse a completed attempt.
  2. Call checkStarted() only once per attempt, immediately after start().
  3. Scope the Attempt to a single logical RPC and discard it after completion.
  4. Prefer the built-in Firestore connector transforms that handle attempt lifecycle internally.

Example fix

// before
RpcQos.Attempt attempt = qos.newAttempt();
attempt.start();
attempt.completeSuccessfully();
attempt.checkStarted(); // throws: already completed
// after
RpcQos.Attempt attempt = qos.newAttempt();
attempt.start();
attempt.completeSuccessfully();
attempt = qos.newAttempt(); // fresh attempt for the next operation
Defensive patterns

Strategy: try-catch

Validate before calling

// Java: scope each Attempt to exactly one operation
for (Write write : writes) {
  RpcQos.Attempt attempt = qos.newAttempt();
  attempt.start();
  attempt.checkStarted();
  // perform RPC
  attempt.completeSuccessfully();
}

Type guard

boolean canStartRpc(RpcQos.Attempt attempt, boolean alreadyCompleted) {
  return !alreadyCompleted;
}

Try / catch

try {
  attempt.checkStarted();
  // proceed
} catch (IllegalStateException e) {
  attempt = qos.newAttempt(); // replace completed attempt
  attempt.start();
}

Prevention

When it happens

Trigger: Calling checkStarted() after completeSuccessfully() — e.g. attempting to run another RPC on the same attempt, or re-entering a batch flow with a finished attempt object.

Common situations: Reusing an Attempt across multiple batched writes; loops that forget to create a new attempt per iteration; shared Attempt instances used by multiple pipeline steps.

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/ff3c04560a2fa418. Report an issue: GitHub.