apache/druid · error · IllegalStateException

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

Inside the timeboundary runner's IteratorMaker.make(), the segment's cursor factory is checked for null before computing min/max times; a null cursor factory means the segment's underlying storage has already been released. Druid throws this IllegalStateException because querying a segment whose memory has been unmapped (e.g. a swappable/mmapped segment evicted while a query is in flight) cannot proceed safely.

Source

Thrown at processing/src/main/java/org/apache/druid/query/timeboundary/TimeBoundaryQueryRunnerFactory.java:129

        final QueryPlus<Result<TimeBoundaryResultValue>> queryPlus,
        final ResponseContext responseContext
    )
    {
      Query<Result<TimeBoundaryResultValue>> input = queryPlus.getQuery();
      if (!(input instanceof TimeBoundaryQuery)) {
        throw new ISE("Got a [%s] which isn't a %s", input.getClass(), TimeBoundaryQuery.class);
      }

      final TimeBoundaryQuery query = (TimeBoundaryQuery) input;

      return new BaseSequence<>(
          new BaseSequence.IteratorMaker<>()
          {
            @Override
            public Iterator<Result<TimeBoundaryResultValue>> make()
            {
              if (cursorFactory == null) {
                throw new ISE(
                    "Null cursor factory found. Probably trying to issue a query against a segment being memory unmapped."
                );
              }

              DateTime minTime = null;
              DateTime maxTime = null;

              if (canUseTimeBoundaryInspector(query, timeBoundaryInspector)) {
                if (query.needsMinTime()) {
                  minTime = timeBoundaryInspector.getMinTime();
                }

                if (query.needsMaxTime()) {
                  maxTime = timeBoundaryInspector.getMaxTime();
                }
              } else {
                final Pair<DateTime, DateTime> timeBoundary = getTimeBoundary(query, cursorFactory);
                minTime = timeBoundary.left();

View on GitHub (pinned to 9b90983fd2)

Solutions

  1. Retry the query — the broker will typically route to a server still serving the segment
  2. Check historical node logs/memory to ensure segments aren't being dropped mid-query (review load/drop rules and kill tasks)
  3. Increase memory or reduce segment eviction pressure on historicals
  4. Ensure query timeouts and retries are configured so transient unmapping races are retried rather than surfaced to users

Example fix

// before
// no retry; a dropped mid-flight segment surfaces ISE to the user
List<Result<TimeBoundaryResultValue>> r = queryRunner.run(QueryPlus.wrap(tbQuery), ctx).toList();
// after
try {
  r = queryRunner.run(QueryPlus.wrap(tbQuery), ctx).toList();
} catch (IllegalStateException e) {
  if (e.getMessage() != null && e.getMessage().contains("Null cursor factory")) {
    r = retryQuery(tbQuery); // broker re-routes to a live segment
  } else {
    throw e;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// Before issuing: check the segment is still served by this server
if (segment == null || segment.asStorageAdapter() == null) {
  throw new IllegalStateException("Segment no longer available for query");
}

Type guard

boolean isSegmentQueryable(Segment segment) {
  try {
    return segment != null && segment.asStorageAdapter() != null;
  } catch (Exception e) {
    return false;
  }
}

Try / catch

try {
  return runner.run(QueryPlus.wrap(query), ctx).toList();
} catch (IllegalStateException e) {
  if (e.getMessage() != null && e.getMessage().contains("Null cursor factory")) {
    return retryWithBackoff(query, 3); // broker will re-route to a live segment
  }
  throw e;
}

Prevention

When it happens

Trigger: Issuing a timeBoundary query against a segment whose CursorFactory is null, which happens when the segment's data is memory-unmapped (unloaded/evicted) between runner construction and iterator creation — typically on historical nodes with aggressive segment drop or during realtime hand-off.

Common situations: Segments dropped or swapped out while a query runs (e.g. kill tasks, load rules dropping data, broker retries landing on a historical that just unloaded the segment); low-memory historicals evicting mmapped segments; race between a query in flight and segment cleanup.

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