Hello all, I want to offer a quick update. tl;dr: Jeff's analysis is correct. The executor is legacy code at this point, slated for deletion, and increasingly unused.
We have been carefully replacing the legacy I/O, timer, and async execution implementations with a new public
EventEngine API and its default implementations. The new thread pools do still auto-scale as needed - albeit with different heuristics, which are evolving as we benchmark - but threads are now reclaimed if/when gRPC calms down from a burst of activity that caused the pool to grow. Also, I believe the executor did not rate limit thread creation when closure queues reached their max depths, but the default EventEngine implementations do rate limit thread creation (currently capped at 1 new thread per second, but that's an implementation detail which may change ... some benchmarks have shown it to be a pretty effective rate). Beginning around gRPC v.148, you should see an increasing number of "event_engine" threads, and a decreasing number of executor threads. Ultimately we aim to unify all async activity into a single auto-scaling thread pool under the EventEngine.
And since the EventEngine is a public API, any integrators that want complete control over thread behavior can implement their own EventEngine and plug it in to gRPC. gRPC will (eventually) use a provided engine for all async execution, timers, and I/O. Implementing an engine is not a small task, but it is an option people have been requesting for years. Otherwise, the default threading behavior provided by gRPC is tuned for performance - if starting a thread helps gRPC move faster, then that's what it will do.
Hope this helps!
-aj