Skip to content

Commit d5f84bc

Browse files
authored
Gunicorn settings class (#16)
* Add GunicornSettings class - Uses Pydantic BaseSettings class * Add tests for gunicorn conf * Minor tweaks to gunicorn module: - modify module docstring - remove pylint rule disable since it's not needed - add link to gunicorn settings docs
1 parent fd738ac commit d5f84bc

File tree

2 files changed

+94
-46
lines changed

2 files changed

+94
-46
lines changed

infra/gunicorn_conf.py

Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,63 @@
11
"""
2-
Conf file extracted from:
2+
Conf file adapted from:
33
https://github.com/tiangolo/uvicorn-gunicorn-docker/blob/2daa3e3873c837d5781feb4ff6a40a89f791f81b/docker-images/gunicorn_conf.py
44
"""
5-
# pylint: disable=invalid-name
65

76
import multiprocessing
8-
import os
7+
from typing import Optional
98

109
from prometheus_client import multiprocess
10+
from pydantic import BaseSettings, conint, root_validator, validator
1111

12-
workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1")
13-
max_workers_str = os.getenv("MAX_WORKERS")
14-
use_max_workers = None
15-
if max_workers_str:
16-
use_max_workers = int(max_workers_str)
17-
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None)
1812

19-
host = os.getenv("HOST", "0.0.0.0")
20-
port = os.getenv("PORT", "8000")
21-
bind_env = os.getenv("BIND", None)
22-
use_loglevel = os.getenv("LOG_LEVEL", "info")
23-
if bind_env:
24-
use_bind = bind_env
25-
else:
26-
use_bind = f"{host}:{port}"
13+
class GunicornSettings(BaseSettings):
14+
keep_alive: int = 5
15+
timeout: int = 120
16+
graceful_timeout: int = 120
17+
log_level: str = "info"
18+
host: str = "0.0.0.0"
19+
port: str = "8000"
20+
bind: Optional[str]
21+
access_log: str = "-"
22+
error_log: str = "-"
23+
workers_per_core: float = 1.0
24+
max_workers: Optional[int]
25+
web_concurrency: Optional[conint(gt=0)]
26+
workers: Optional[int]
2727

28-
cores = multiprocessing.cpu_count()
29-
workers_per_core = float(workers_per_core_str)
30-
default_web_concurrency = workers_per_core * cores
31-
if web_concurrency_str:
32-
web_concurrency = int(web_concurrency_str)
33-
assert web_concurrency > 0
34-
else:
35-
web_concurrency = max(int(default_web_concurrency), 2)
36-
if use_max_workers:
37-
web_concurrency = min(web_concurrency, use_max_workers)
38-
accesslog_var = os.getenv("ACCESS_LOG", "-")
39-
use_accesslog = accesslog_var or None
40-
errorlog_var = os.getenv("ERROR_LOG", "-")
41-
use_errorlog = errorlog_var or None
42-
graceful_timeout_str = os.getenv("GRACEFUL_TIMEOUT", "120")
43-
timeout_str = os.getenv("TIMEOUT", "120")
44-
keepalive_str = os.getenv("KEEP_ALIVE", "5")
28+
@validator("bind")
29+
def set_bind(cls, bind, values):
30+
return bind if bind else f"{values['host']}:{values['port']}"
31+
32+
@root_validator(skip_on_failure=True)
33+
def set_workers(cls, values):
34+
if values["workers"] is not None:
35+
return values
36+
elif values["web_concurrency"]:
37+
values["workers"] = values["web_concurrency"]
38+
else:
39+
cores = multiprocessing.cpu_count()
40+
default_workers = values["workers_per_core"] * cores
41+
workers = max(int(default_workers), 2)
42+
if values["max_workers"]:
43+
workers = min(workers, values["max_workers"])
44+
values["workers"] = workers
45+
return values
46+
47+
48+
gunicorn_settings = GunicornSettings()
4549

4650
# Gunicorn config variables
47-
loglevel = use_loglevel
48-
workers = web_concurrency
49-
bind = use_bind
50-
errorlog = use_errorlog
51+
# https://docs.gunicorn.org/en/stable/settings.html#settings
52+
loglevel = gunicorn_settings.log_level
53+
workers = gunicorn_settings.workers
54+
bind = gunicorn_settings.bind
55+
errorlog = gunicorn_settings.error_log
5156
worker_tmp_dir = "/dev/shm"
52-
accesslog = use_accesslog
53-
graceful_timeout = int(graceful_timeout_str)
54-
timeout = int(timeout_str)
55-
keepalive = int(keepalive_str)
57+
accesslog = gunicorn_settings.access_log
58+
graceful_timeout = gunicorn_settings.graceful_timeout
59+
timeout = gunicorn_settings.timeout
60+
keepalive = gunicorn_settings.keep_alive
5661

5762

5863
# For debugging and testing
@@ -66,10 +71,10 @@
6671
"errorlog": errorlog,
6772
"accesslog": accesslog,
6873
# Additional, non-gunicorn variables
69-
"workers_per_core": workers_per_core,
70-
"use_max_workers": use_max_workers,
71-
"host": host,
72-
"port": port,
74+
"workers_per_core": gunicorn_settings.workers_per_core,
75+
"use_max_workers": gunicorn_settings.max_workers,
76+
"host": gunicorn_settings.host,
77+
"port": gunicorn_settings.port,
7378
}
7479

7580

tests/unit/test_gunicorn_conf.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from pydantic import ValidationError
2+
import pytest
3+
from infra.gunicorn_conf import GunicornSettings
4+
5+
6+
def test_set_bind(monkeypatch):
7+
expected = "1.2.3.4:5678"
8+
monkeypatch.setenv("BIND", expected)
9+
settings = GunicornSettings()
10+
assert settings.bind == expected
11+
12+
13+
class TestSetWorkers:
14+
def test_workers_env_var(self, monkeypatch):
15+
expected = 3
16+
monkeypatch.setenv("WORKERS", str(expected))
17+
settings = GunicornSettings()
18+
assert settings.workers == expected
19+
20+
def test_web_concurency_env_var(self, monkeypatch):
21+
expected = 3
22+
monkeypatch.setenv("WEB_CONCURRENCY", str(expected))
23+
settings = GunicornSettings()
24+
assert settings.workers == expected
25+
26+
def test_web_concurency_env_var_out_of_range(self, monkeypatch):
27+
expected = 0
28+
monkeypatch.setenv("WEB_CONCURRENCY", str(expected))
29+
with pytest.raises(ValidationError):
30+
GunicornSettings()
31+
32+
def test_default_workers(self, monkeypatch):
33+
expected = 2
34+
monkeypatch.setattr("multiprocessing.cpu_count", lambda: 2)
35+
settings = GunicornSettings()
36+
assert settings.workers == expected
37+
38+
def test_max_workers(self, monkeypatch):
39+
expected = 1
40+
monkeypatch.setattr("multiprocessing.cpu_count", lambda: 2)
41+
monkeypatch.setenv("MAX_WORKERS", str(expected))
42+
settings = GunicornSettings()
43+
assert settings.workers == expected

0 commit comments

Comments
 (0)