From ec6af68013d75c1ca2aa56df7b0719b236831045 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 May 2026 16:01:02 +0530 Subject: [PATCH] fix: Remove hardcoded paths for slow prediction --- docs/design/gthread-slow-request-isolation.md | 16 ++++--------- gunicorn/config.py | 24 ------------------- gunicorn/workers/gthread.py | 5 +--- gunicorn/workers/gthread_routing.py | 18 ++++---------- tests/workers/test_gthread_routing.py | 6 ----- 5 files changed, 11 insertions(+), 58 deletions(-) diff --git a/docs/design/gthread-slow-request-isolation.md b/docs/design/gthread-slow-request-isolation.md index c674a294..cc62b8f7 100644 --- a/docs/design/gthread-slow-request-isolation.md +++ b/docs/design/gthread-slow-request-isolation.md @@ -163,9 +163,6 @@ New settings, mirroring `WorkerThreads` (`config.py:697`): - `slow_threads` — `S`, slow-lane worker count. Default `1`. - `slow_queue_maxsize` — bound on `slow_q`; overflow ⇒ `503`. Default e.g. `100` (`0` = unbounded). -- `slow_routes` — optional list of regex route patterns (matched with - `re.search` against the route key) operators know are slow, seeded into the - predictor so even the *first* request routes correctly. - `slow_lane_retry_after` — seconds for the `Retry-After` header on 503. A `slow_route_key` hook to customize the route key (e.g. collapse @@ -198,9 +195,8 @@ A small, self-contained, thread-safe object: - `update(route_key, duration)`: EWMA with decay so a route that becomes fast again eventually returns to the fast lane (avoids permanent misclassification after a one-off slow spike). Called on every completion. -- `is_slow(route_key)`: `True` if the route matches a seeded `slow_routes` - pattern, or its `ewma_seconds >= slow_request_threshold`. Unknown routes ⇒ - `False` (fast) by default. +- `is_slow(route_key)`: `True` if its `ewma_seconds >= slow_request_threshold`. + Unknown routes ⇒ `False` (fast) by default. - Optional hysteresis (separate promote/demote thresholds) to avoid flapping around the boundary. @@ -216,19 +212,17 @@ A small, self-contained, thread-safe object: request — we can't). This shortens the learning window when many requests to a brand-new slow route arrive at once: subsequent ones in the burst route to the slow lane after one threshold interval instead of after a full slow request. -3. **Seeding (eliminates the first-occurrence window for known offenders)**: - `slow_routes` patterns mark routes slow from the very first request. ## 6. Behavior under load (the cases that matter) -- **Flood of a known/seeded or previously-seen slow route**: every such request +- **Flood of a previously-seen slow route**: every such request is routed to the slow pool. The `F` fast threads are never given this work and keep serving fast traffic at full capacity. When the slow lane reaches `S + slow_queue_maxsize`, further slow requests get a fast `503` — backpressure is contained to the slow lane. - **Flood of a never-seen slow route**: the first occurrence(s) run in the fast lane; mid-flight learning (§5.4.2) flips the route to slow after one threshold - interval, so the flood is contained quickly. Seeding avoids even this window. + interval, so the flood is contained quickly. - **Mixed fast traffic, idle slow lane**: the `S` slow threads stay parked (no work stealing in this design — see §3), so fast throughput is `F`, not `F + S`. - **Misprediction (route marked slow but now fast)**: handled gracefully — it @@ -239,7 +233,7 @@ A small, self-contained, thread-safe object: Implemented: - `config.py` — `slow_request_threshold`, `slow_threads`, `slow_queue_maxsize`, - `slow_routes`, `slow_lane_retry_after`, plus `validate_pos_float`. + `slow_lane_retry_after`, plus `validate_pos_float`. - `gthread.py` `init_process`/`get_thread_pool` — build `fast_pool` and `slow_pool` (or the single legacy pool when disabled); `_shutdown_pools`. - `gthread.py` `enqueue_req` — route to the matching pool; `nr_slow` bound + diff --git a/gunicorn/config.py b/gunicorn/config.py index a5cfa4b9..0bbfd3fc 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -823,30 +823,6 @@ class SlowQueueMaxsize(Setting): """ -class SlowRoutes(Setting): - name = "slow_routes" - section = "Worker Processes" - cli = ["--slow-route"] - action = "append" - meta = "PATTERN" - validator = validate_list_string - default = [] - desc = """\ - Regular expression(s) matching routes that should always be treated as - slow, regardless of observed timings. - - Each pattern is matched (using ``re.search``) against the route key, - which is the request method and path joined by a space, e.g. - ``"POST /reports/generate"``. Seeding known-slow routes avoids the brief - window where a never-before-seen slow route is learned. - - Only used by the ``gthread`` worker when - :ref:`slow-request-threshold` is set. - - .. versionadded:: 23.1.0 - """ - - class SlowLaneRetryAfter(Setting): name = "slow_lane_retry_after" section = "Worker Processes" diff --git a/gunicorn/workers/gthread.py b/gunicorn/workers/gthread.py index a1ce27b6..c74e62f2 100644 --- a/gunicorn/workers/gthread.py +++ b/gunicorn/workers/gthread.py @@ -107,10 +107,7 @@ class ThreadWorker(base.Worker): def init_process(self): self.tpool = self.get_thread_pool() if self.routing_enabled: - self.predictor = SlowRoutePredictor( - self.slow_threshold, - seed_patterns=self.cfg.slow_routes, - ) + self.predictor = SlowRoutePredictor(self.slow_threshold) # a dedicated pool for the slow lane: slow requests can never # occupy the fast pool's (``self.tpool``) threads self.slow_pool = futures.ThreadPoolExecutor( diff --git a/gunicorn/workers/gthread_routing.py b/gunicorn/workers/gthread_routing.py index 885a29f7..b2ebf26a 100644 --- a/gunicorn/workers/gthread_routing.py +++ b/gunicorn/workers/gthread_routing.py @@ -6,12 +6,11 @@ The :class:`SlowRoutePredictor` decides, before a request is handed to a worker, whether its route is expected to be slow, based on previously observed -timings of the same route (method + path) plus operator-seeded patterns. The -gthread worker uses this to route slow requests to a dedicated thread pool so -they cannot starve fast requests. +timings of the same route (method + path). The gthread worker uses this to +route slow requests to a dedicated thread pool so they cannot starve fast +requests. """ -import re import threading from collections import OrderedDict @@ -22,26 +21,19 @@ class SlowRoutePredictor: Timings are tracked per route as an exponentially weighted moving average (EWMA) so that a route which becomes fast again decays back below the threshold. The table is bounded (LRU) to cap memory under high route - cardinality. Operator-seeded regex patterns always classify as slow. + cardinality. """ - def __init__(self, threshold, max_entries=1024, alpha=0.3, - seed_patterns=None): + def __init__(self, threshold, max_entries=1024, alpha=0.3): self.threshold = threshold self.alpha = alpha self.max_entries = max_entries self._stats = OrderedDict() self._lock = threading.Lock() - self._seed = [re.compile(p) for p in (seed_patterns or [])] - - def _seeded(self, key): - return any(p.search(key) for p in self._seed) def is_slow(self, key): if not key: return False - if self._seeded(key): - return True with self._lock: ewma = self._stats.get(key) if ewma is None: diff --git a/tests/workers/test_gthread_routing.py b/tests/workers/test_gthread_routing.py index d14ccfae..af933cc3 100644 --- a/tests/workers/test_gthread_routing.py +++ b/tests/workers/test_gthread_routing.py @@ -33,12 +33,6 @@ def test_predictor_observe_slow_marks_immediately(): assert p.is_slow("POST /report") is True -def test_predictor_seed_patterns(): - p = SlowRoutePredictor(threshold=1.0, seed_patterns=[r"^POST /reports/"]) - assert p.is_slow("POST /reports/generate") is True - assert p.is_slow("GET /reports/generate") is False - - def test_predictor_lru_bound(): p = SlowRoutePredictor(threshold=1.0, max_entries=10) for i in range(50):