apache/druid · error · IllegalStateException

Invalid operation - KinesisRecordSupplier has already been c

Error message

Invalid operation - KinesisRecordSupplier has already been closed

What it means

checkIfClosed() is invoked before operations that require a live supplier. Once close() has run (closed=true), any further use throws this ISE. It guards the scheduled executor and shard iterators, which no longer exist after close.

Source

Thrown at extensions-core/kinesis-indexing-service/src/main/java/org/apache/druid/indexing/kinesis/KinesisRecordSupplier.java:1068

    }

    String shardIterator = kinesis.getShardIterator(iteratorRequestBuilder.build()).shardIterator();

    return kinesis.getRecords(
        GetRecordsRequest.builder()
            .shardIterator(shardIterator)
            .limit(1)
            .build()
    );
  }

  /**
   * Explode if {@link #close()} has been called on the supplier.
   */
  private void checkIfClosed()
  {
    if (closed) {
      throw new ISE("Invalid operation - KinesisRecordSupplier has already been closed");
    }
  }

  /**
   * This method must be called before a seek operation ({@link #seek}, {@link #seekToLatest}, or
   * {@link #seekToEarliest}).
   * <p>
   * When called, it will nuke the {@link #scheduledExec} that is shared by all {@link PartitionResource}, filters
   * records from the buffer for partitions which will have a seek operation performed, and stops background fetch for
   * each {@link PartitionResource} to prepare for the seek. If background fetch is not currently running, the
   * {@link #scheduledExec} will not be re-created.
   */
  private void filterBufferAndResetBackgroundFetch(Set<StreamPartition<String>> partitions) throws InterruptedException
  {
    checkIfClosed();
    if (backgroundFetchEnabled && partitionsFetchStarted.compareAndSet(true, false)) {
      scheduledExec.shutdown();

View on GitHub (pinned to 9b90983fd2)

Solutions

  1. Do not use the supplier after close(); restructure code so close() is the last operation (try-with-resources)
  2. Guard consumer loops with an isClosed/stop flag checked before each poll
  3. Create a new KinesisRecordSupplier instance if you need to read again after closing
  4. Synchronize close() and read paths if multiple threads share the supplier

Example fix

// before
supplier.close();
supplier.poll(1000);
// after
supplier.close();
// create a fresh supplier for any further reads
KinesisRecordSupplier newSupplier = new KinesisRecordSupplier(kinesis, ...);
Defensive patterns

Strategy: type-guard

Validate before calling

if (supplier.isClosed()) { return; } // skip work after close

Type guard

static boolean usable(KinesisRecordSupplier s) { return !s.isClosed(); }

Try / catch

try {
  records = supplier.poll(timeoutMs);
} catch (ISE e) {
  if (e.getMessage().contains("has already been closed")) {
    return Collections.emptyList(); // graceful shutdown path
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling poll, seek, seekToEarliest, seekToLatest, or other checkIfClosed-guarded methods after close() has been called on the KinesisRecordSupplier instance.

Common situations: Using a supplier after task shutdown hooks ran; accidentally closing the supplier in a finally block while a late poll still executes; sharing one supplier across threads where one closes while another reads.

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/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/3ea5cbeeef9cd066. Report an issue: GitHub.