apache/druid · error · IllegalStateException (ISE)

Null cursor factory found. Probably trying to issue a query

Error message

Null cursor factory found. Probably trying to issue a query against a segment being memory unmapped.

What it means

ScanQueryEngine.process obtains a CursorFactory from the segment via segment.as(CursorFactory.class). A null result means the segment no longer exposes cursors, which happens when its memory-mapped data is being unmapped (e.g. the segment was dropped or swapped out while the query ran). Druid throws ISE rather than returning partial results.

Source

Thrown at processing/src/main/java/org/apache/druid/query/scan/ScanQueryEngine.java:86

      final Segment segment,
      final ResponseContext responseContext,
      @Nullable final QueryMetrics<?> queryMetrics
  )
  {
    final Long numScannedRows = responseContext.getRowScanCount();
    if (numScannedRows != null && numScannedRows >= query.getScanRowsLimit() && query.getTimeOrder().equals(Order.NONE)) {
      return Sequences.empty();
    }
    if (segment.isTombstone()) {
      return Sequences.empty();
    }

    final boolean hasTimeout = query.context().hasTimeout();
    final Long timeoutAt = responseContext.getTimeoutTime();
    final CursorFactory cursorFactory = segment.as(CursorFactory.class);

    if (cursorFactory == null) {
      throw new ISE(
          "Null cursor factory found. Probably trying to issue a query against a segment being memory unmapped."
      );
    }

    final List<String> allColumns = new ArrayList<>();

    if (query.getColumns() != null && !query.getColumns().isEmpty()) {

      // Unless we're in legacy mode, allColumns equals query.getColumns() exactly. This is nice since it makes
      // the compactedList form easier to use.
      allColumns.addAll(query.getColumns());
    } else {
      final Set<String> availableColumns = Sets.newLinkedHashSet(
          Iterables.concat(
              cursorFactory.getRowSignature().getColumnNames(),
              Iterables.transform(
                  Arrays.asList(query.getVirtualColumns().getVirtualColumns()),
                  VirtualColumn::getOutputName

View on GitHub (pinned to 9b90983fd2)

Solutions

  1. Retry the query; the segment swap is usually transient
  2. Increase query concurrency handling / retry policy on the client
  3. Avoid force-dropping segments while queries run (stagger retention tasks)
  4. Increase historical process mmapped memory so segments are not evicted under load
Defensive patterns

Strategy: retry

Validate before calling

CursorFactory cf = segment.as(CursorFactory.class);
if (cf == null) { /* defer/requeue query */ }

Type guard

CursorFactory cursorFactoryOrNull(Segment s) { return s.as(CursorFactory.class); }

Try / catch

try {
  engine.process(query, segment, ...);
} catch (IllegalStateException e) {
  if (e.getMessage().contains("Null cursor factory")) { /* retry after short backoff */ }
  else throw e;
}

Prevention

When it happens

Trigger: Running a scan query against a ReferenceCountedSegment/segment whose adapter no longer supports CursorFactory because the underlying files were closed/unmapped concurrently (segment cleanup during query execution).

Common situations: Historical processes reloading or dropping segments while queries are in flight; segments evicted due to cache size limits; race after a segment swap during real-time hand-off.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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