From 2b20a0dd7e8590c59016d43494dc9e775e4eca50 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Wed, 15 Jul 2026 11:56:28 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(scalingo)=20isolate=20the=20metric?= =?UTF-8?q?s=20endpoint=20in=20its=20own=20gunicorn=20pool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeployCenter calls the usage metrics endpoint synchronously while answering our entitlements requests. Served by the same sync workers, this callback could queue behind the very requests blocked waiting for DeployCenter, freezing the whole pool until timeouts fire. A dedicated single-worker gunicorn behind nginx guarantees the callback always finds a free worker, making the circular wait impossible. The main pool is sized to WEB_CONCURRENCY minus the reserved worker so the container keeps the worker count its size was provisioned for, and both values are logged at boot to ease diagnosing worker starvation. --- bin/scalingo_run_web | 20 +++++++++++++++++++- src/nginx/servers.conf.erb | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/bin/scalingo_run_web b/bin/scalingo_run_web index 80044165c..67ff17e8e 100644 --- a/bin/scalingo_run_web +++ b/bin/scalingo_run_web @@ -3,8 +3,26 @@ # Trigger collabora configuration python manage.py trigger_wopi_configuration +# Keep the total worker count at WEB_CONCURRENCY: the metrics pool below +# takes one worker, the main pool gets the rest (at least one). +MAIN_WORKERS=$((${WEB_CONCURRENCY:-2} - 1)) +if [ "$MAIN_WORKERS" -lt 1 ]; then + MAIN_WORKERS=1 +fi +echo "WEB_CONCURRENCY=${WEB_CONCURRENCY:-unset} MAIN_WORKERS=$MAIN_WORKERS" + # Start the Django backend -gunicorn -b :8000 drive.wsgi:application --log-file - & +gunicorn -b :8000 -w "$MAIN_WORKERS" drive.wsgi:application --log-file - & + +# Start a dedicated Django backend pool for the usage metrics endpoint. +# DeployCenter calls it synchronously while Drive itself is blocked waiting +# on a DeployCenter entitlements response: served by the main pool, this +# callback would queue behind the very workers waiting for it (deadlock +# until timeout). An isolated pool guarantees it always gets a worker. +# Its access log is enabled with a distinctive prefix so the platform +# logs show which requests were served by this pool. +gunicorn -b :8001 -w 1 drive.wsgi:application --log-file - \ + --access-logfile - --access-logformat 'metrics-pool %(h)s "%(r)s" %(s)s' & # Start the Nginx server bin/run & diff --git a/src/nginx/servers.conf.erb b/src/nginx/servers.conf.erb index 277257674..1eda916cf 100644 --- a/src/nginx/servers.conf.erb +++ b/src/nginx/servers.conf.erb @@ -5,6 +5,13 @@ upstream backend_server { server localhost:8000 fail_timeout=0; } +# Dedicated gunicorn pool for the usage metrics endpoint, so DeployCenter's +# synchronous callback never competes with user requests blocked on +# DeployCenter itself (see bin/scalingo_run_web). +upstream metrics_server { + server localhost:8001 fail_timeout=0; +} + server { listen <%= ENV["PORT"] %>; @@ -14,6 +21,18 @@ server { error_page 404 /404.html; + # Usage metrics, isolated from the main backend pool. Must stay above + # the generic api/external_api block: nginx picks the first matching + # regex location. + location ~ ^/external_api/v[0-9.]+/metrics/ { + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_redirect off; + proxy_pass http://metrics_server; + } + # Django rest framework and external API location ~ ^/(api|external_api)/ { proxy_set_header X-Forwarded-Proto https;