A workload group can now cap the number of search requests a single
node admits for it concurrently. Overflow is rejected with a 429
instead of queueing up behind the resource-based cancellation path.
Throttle config is a new nested "throttling" object on a workload
group, modeled as a Settings bag like the existing "settings" field,
so a per-key null clears one field and an absent key keeps its current
value. "attribute" selects the dimension the limit is keyed by: group,
or username/role for one bucket per principal, resolved from the
security plugin's principal.* attributes at enforcement time.
"node_limit" is the per-node concurrent allowance for each bucket.
The caller's principal for username/role keying is carried on the
coordinator's task object rather than in a ThreadContext request
header, so a client cannot supply it, it is never serialized onto shard
requests or to remote clusters in a cross-cluster search, and the
concurrent _msearch sub-requests that share one thread context each get
their own value.
Admission runs in TransportSearchAction before onRequestStart, so a
rejection never leaves the request-operations gauges incremented. The
permit is chained onto the response listener and released exactly
once, including on the transformRequest failure path. Rejections are
counted in a new total_throttled field on _wlm/stats.
A group in monitor resiliency mode observes only: a breach is logged
at DEBUG and the request is admitted, matching how MONITOR is dormant
on the cancellation path, and it is not counted. The throttle path
fails open, so an unattributable request, an unregistered group, or an
unexpected error skips throttling rather than failing a valid search.
Validation rejects a limit with no attribute, an unknown throttling
key, a negative or int-overflowing limit, and a config whose effective
ceiling is 0. Setting "throttling" to null disables throttling.
The throttling field is gated behind 3.9.0 on the wire, separately
from the already-released settings gate, so mixed-version clusters
stay wire-compatible.
The cluster-wide shared tier is not included here; shared_limit is not
an accepted key, so it cannot be set with no effect.
Scroll continuations draw on the same budget: exempting them would make
node_limit evadable by appending ?scroll= to a query. Each _msearch
sub-search takes its own permit, so a throttled sub-search reports 429
inside the enclosing 200 response.
Query rewriting can issue a nested coordinator search on the same node --
a terms lookup with a subquery does -- while the outer request already
holds its bucket's permit, and the nested request inherits the same
workload group id, so it resolves to the same bucket. Charging it a
second permit made the request compete with itself: with node_limit=N,
N such requests all got a 429 at exactly the configured concurrency.
Admission now skips a request whose bucket an ancestor task already
holds. To make that visible, searches issued by the rewrite phase are
parented on the task that triggered the rewrite, via new overloads of
SearchService and IndicesService getRewriteContext. That parenting is
applied only when the request actually holds a permit, so a search in a
group without throttling issues rewrite requests exactly as before; a
throttled request's rewrite searches do now report a parent action to
system-generated search pipeline selection, where before they reported
none.
Creating or updating a throttling config is rejected when the cluster
still has a pre-3.9 node, instead of returning 200 and silently dropping
the field on the wire, and when the attribute keys on a principal but no
principal attribute provider is installed, which could never enforce.
Both are checked in the cluster-manager transport actions rather than in
a state applier, because throwing while applying state wedges the
cluster-manager.
Signed-off-by: Emily Guo <35637792+LilyCaroline17@users.noreply.github.com>
Signed-off-by: David Zane <davizane@amazon.com>
Co-authored-by: Emily Guo <35637792+LilyCaroline17@users.noreply.github.com>
Description
Adds per-node request throttling to workload management: a workload group can cap how many search requests one node admits for it concurrently, rejecting overflow with a
429.Config is a new nested
throttlingobject — aSettingsbag like the existingsettingsfield, so a per-keynullclears one field and an absent key keeps its current value:attribute— the dimension the limit is keyed by:group(one bucket for the group), orusername/role.node_limit— per-node concurrent in-flight allowance per bucket._search,_count,_msearchsub-searches and_search/scrollcontinuations all draw on the same budget. A nested coordinator search issued by the rewrite phase (a terms lookup with a subquery) is not charged a second permit for a bucket its own request already holds. Rejections surface astotal_throttledon_wlm/statsand aTOTAL_THROTTLEDcolumn on_list/wlm_stats.How the limit is enforced. Each node throttles in isolation. There is no cross-node coordination, no shared counter, and no extra network hop on the request path — a node decides admission from its own in-memory state. Each node keeps a
ConcurrentHashMapof bucket key to in-flight count, incremented on admission and decremented when the request finishes; a bucket's entry is dropped once it drains to zero, so idle buckets cost nothing and cardinality follows live traffic rather than configuration.node_limitis therefore a per-node ceiling, not a cluster-wide one: a 3-node cluster withnode_limit: 10admits up to 10 concurrent requests for the group per node, so up to 30 in aggregate. Sizing it means reasoning about one node's capacity. (A cluster-wide pool is the follow-up described at the end.)The bucket key is
<workload_group_id>:<attribute>:<attribute_value>, where the value depends on the attribute:group— the literalgroup, so every request tagged to the workload group shares one bucket per node.username/role— the caller's value for that subfield, read from the principal the security plugin's extractor supplied, so each principal gets its own bucket per node. When a caller has several values for the subfield (a user in many roles), the request is charged to the lexicographically smallest one, so the same caller always lands in the same bucket rather than drawing a fresh allowance per request.Two behaviors to know before sizing a limit:
_msearchsub-search takes its own permit, so a multi-search with more sub-requests thannode_limitthrottles itself — a429on the individual item inside an enclosing200."throttling": {}disables throttling, exactly as"settings": {}clears settings. Use"throttling": nullto disable explicitly; omit the field to leave it untouched.First of two parts: the cluster-wide shared tier is excluded, and
shared_limitis not an accepted key, so it cannot be set with no effect.Related Issues
Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.