ESQL: avoid monopolizing the coordinator thread during large LOAD_ALL expansion
Description
Follow-up from #159914 / #160053. The cancellation half of #159914 is done: the ExpandUnmappedFieldsPostProcessor expansion loops now poll the task's cancellation supplier every 1024 rows and throw TaskCancelledException, so a long LOAD_ALL expansion aborts promptly and releases its pages. The issue also floated "maybe we should even yield the thread?" — this issue tracks that second, orthogonal concern.
The concern
The expansion is a coordinator-side, single-threaded scan over every row of every result page (parsing each row's _unmapped_fields JSON). It runs synchronously inside the post-compute SubscribableListener continuation in EsqlSession, i.e. on whichever thread completes the compute result (ComputeListener) — the coordinator compute pool, or potentially a transport worker, depending on completion order. It is not explicitly re-dispatched onto a bounded query pool before expansion begins.
For a wide LOAD_ALL result this loop can run for seconds. Under concurrency that means one query can hold a worker thread for the whole expansion, starving other queued work — and if the completing thread is a transport_worker, it would block I/O, which is worse. The periodic cancellation checks let a cancelled query release the thread, but a non-cancelled large expansion still monopolizes it.
Thread.yield() is not the fix
The issue's phrasing ("yield the thread") should not be taken literally. Thread.yield() is an unreliable JVM scheduling hint: it does not hand a pooled thread back to other queued tasks and does nothing for threadpool starvation. It should be avoided.
Proposed approach
Chunked re-dispatch: process a bounded batch of rows, then re-fork the remainder onto the appropriate executor (e.g. an AbstractRunnable that re-queues itself), so other queued tasks interleave. This turns expand into an iterative/async loop and keeps any single query from holding a worker thread for its whole expansion. Concretely:
- Confirm the executor the expansion continuation actually runs on today (coordinator compute pool vs. transport worker) to size the risk and pick the right target executor.
- Restructure
ExpandUnmappedFieldsPostProcessor.expand(thecollectFieldNamesandrewritePagesscans) to yield back to the executor every N rows, preserving the existing per-row cancellation checks and page-release semantics.
Why the priority is not urgent
- #160053 already makes long expansions promptly cancellable, so a hogged thread can be reclaimed.
LOAD_ALLtruncates its output to ~10k rows regardless ofLIMIT(observed: a plain query honoredLIMIT 50000,LOAD_ALLcapped at 10k), so per-query expansion is bounded — in manual testing it stayed sub-second.- The circuit breaker bounds memory; the pathological wide/sparse case is more a memory / async-storage concern, tracked in #160278.
Notes
- Original recommendation was periodic cancellation checks (done, #160053) plus a "maybe yield" aside (this issue).
- Related: #159914 (parent), #160053 (cancellation fix), #160278 (async storage / circuit-breaking for wide LOAD_ALL).
Source: elastic/elasticsearch