Set up a Gunicorn Configuration File, and Test It

Gnoo-icorn?

If you use Gunicorn, it’s likely you have a configuration file. This is a Python module that contains settings as module-level variables. Here’s an example with some essential settings:

# Gunicorn configuration file
# https://docs.gunicorn.org/en/stable/configure.html#configuration-file
# https://docs.gunicorn.org/en/stable/settings.html
import multiprocessing

max_requests = 1000
max_requests_jitter = 50

log_file = "-"

workers = multiprocessing.cpu_count() * 2 + 1

These settings do the following things:

There are many more settings available.

When you’ve set up a config file you can test it with gunicorn --check-config:

$ gunicorn --check-config --config python:example.gunicorn example.wsgi

Here:

If all is well, this command exits with an exit code of 0. Otherwise, it prints an error message and exits with a non-zero value. For example, if there was a typo in the above file:

$ gunicorn --check-config --config python:example.gunicorn example.wsgi

Error: module 'multiprocessing' has no attribute 'cpu_coun'

$ echo $?  # print last command's exit code
1

So once you have a Gunicorn config file, it’s best to ensure you run gunicorn --check-config as part of your CI. Gunicorn may change or you may add broken logic in your config file, and you don’t want to be surprised only when you deploy.

You can verify by running the above command as an extra step in your CI system. This is a fine approach but it’s “yet another thing” and won’t be run locally by default.

An alternative that I prefer is to run the command within a test, combining it with other checks. This also has the bonus of including the config file in test coverage. Let’s look at how to write this test, with Django’s Test Framework and then pytest.

With Django’s Test Framework

Here’s the test:

import sys
from unittest import mock

from django.test import SimpleTestCase
from gunicorn.app.wsgiapp import run


class GunicornConfigTests(SimpleTestCase):
    def test_config(self):
        argv = [
            "gunicorn",
            "--check-config",
            "--config",
            "python:example.gunicorn",
            "example.wsgi",
        ]
        mock_argv = mock.patch.object(sys, "argv", argv)

        with self.assertRaises(SystemExit) as cm, mock_argv:
            run()

        exit_code = cm.exception.args[0]
        self.assertEqual(exit_code, 0)

What’s going on?

Cool beans.

If the test succeeds, we get the usual . in our test run. If it fails, Gunicorn’s output is visible above the failure:

$ ./manage.py test example.tests.test_gunicorn
Found 1 test(s).
System check identified no issues (0 silenced).

Error: module 'multiprocessing' has no attribute 'cpu_coun'
F
======================================================================
FAIL: test_config (example.tests.test_gunicorn.GunicornConfigTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/.../example/tests/test_gunicorn.py", line 21, in test_config
    self.assertEqual(exit_code, 0)
AssertionError: 1 != 0

----------------------------------------------------------------------
Ran 1 test in 0.006s

FAILED (failures=1)

Easy to debug!

With pytest

The pytest version is not that different:

from __future__ import annotations

import sys
from unittest import mock

import pytest
from gunicorn.app.wsgiapp import run

from db_buddy.test import SimpleTestCase


class GunicornConfigTests(SimpleTestCase):
    def test_config_imports(self):
        argv = [
            "gunicorn",
            "--check-config",
            "--config",
            "python:db_buddy.gunicorn",
            "db_buddy.wsgi",
        ]
        mock_argv = mock.patch.object(sys, "argv", argv)

        with pytest.raises(SystemExit) as excinfo, mock_argv:
            run()

        assert excinfo.value.args[0] == 0

The test is as before but with a couple bits of pytest-ness:

You could also write this as a function-based test, but I prefer to stick to Django TestCases.

Fin

May your Gunicorn be well-tuned,

—Adam


Newly updated: my book Boost Your Django DX now covers Django 5.0 and Python 3.12.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Tags: ,