gunicorn/tests/test_dirty_hooks.py
Benoit Chesneau 77222b8017 feat: add dirty arbiters for long-running blocking operations
Introduce Dirty Arbiters - a separate process pool for executing
long-running, blocking operations (AI model loading, heavy computation)
without blocking HTTP workers. Inspired by Erlang's dirty schedulers.

Key features:
- Completely separate from HTTP workers - can be killed/restarted independently
- Stateful - loaded resources persist in dirty worker memory
- Message-passing IPC via Unix sockets with JSON serialization
- Explicit execute() API from HTTP workers
- Asyncio-based for clean concurrent handling

Architecture:
- DirtyArbiter: manages the dirty worker pool, routes requests
- DirtyWorker: executes functions, maintains state, handles requests
- DirtyClient: sync/async API for HTTP workers to call dirty apps
- DirtyProtocol: length-prefixed JSON messages over Unix sockets
- DirtyApp: base class for dirty applications

Configuration options:
- dirty_apps: list of import paths for dirty applications
- dirty_workers: number of dirty workers (default: 0)
- dirty_timeout: task timeout in seconds (default: 300)
- dirty_graceful_timeout: shutdown timeout (default: 30)

Lifecycle hooks:
- on_dirty_starting(arbiter)
- dirty_post_fork(arbiter, worker)
- dirty_worker_init(worker)
- dirty_worker_exit(arbiter, worker)

Includes comprehensive test suite with 164 tests covering:
- Protocol encoding/decoding
- Worker and arbiter lifecycle
- Client sync/async APIs
- Signal handling
- Error handling and timeouts
- Integration tests
2026-01-25 10:21:18 +01:00

110 lines
3.1 KiB
Python

#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""Tests for dirty arbiter hooks."""
import pytest
from gunicorn.config import Config
class TestDirtyHooksConfig:
"""Tests for dirty hook configuration settings."""
def test_on_dirty_starting_default(self):
"""Test on_dirty_starting default is a callable."""
cfg = Config()
assert callable(cfg.on_dirty_starting)
def test_on_dirty_starting_custom(self):
"""Test setting custom on_dirty_starting hook."""
hook_calls = []
def my_hook(arbiter):
hook_calls.append(arbiter)
cfg = Config()
cfg.set("on_dirty_starting", my_hook)
# Call the hook
cfg.on_dirty_starting("test_arbiter")
assert hook_calls == ["test_arbiter"]
def test_dirty_post_fork_default(self):
"""Test dirty_post_fork default is a callable."""
cfg = Config()
assert callable(cfg.dirty_post_fork)
def test_dirty_post_fork_custom(self):
"""Test setting custom dirty_post_fork hook."""
hook_calls = []
def my_hook(arbiter, worker):
hook_calls.append((arbiter, worker))
cfg = Config()
cfg.set("dirty_post_fork", my_hook)
# Call the hook
cfg.dirty_post_fork("test_arbiter", "test_worker")
assert hook_calls == [("test_arbiter", "test_worker")]
def test_dirty_worker_init_default(self):
"""Test dirty_worker_init default is a callable."""
cfg = Config()
assert callable(cfg.dirty_worker_init)
def test_dirty_worker_init_custom(self):
"""Test setting custom dirty_worker_init hook."""
hook_calls = []
def my_hook(worker):
hook_calls.append(worker)
cfg = Config()
cfg.set("dirty_worker_init", my_hook)
# Call the hook
cfg.dirty_worker_init("test_worker")
assert hook_calls == ["test_worker"]
def test_dirty_worker_exit_default(self):
"""Test dirty_worker_exit default is a callable."""
cfg = Config()
assert callable(cfg.dirty_worker_exit)
def test_dirty_worker_exit_custom(self):
"""Test setting custom dirty_worker_exit hook."""
hook_calls = []
def my_hook(arbiter, worker):
hook_calls.append((arbiter, worker))
cfg = Config()
cfg.set("dirty_worker_exit", my_hook)
# Call the hook
cfg.dirty_worker_exit("test_arbiter", "test_worker")
assert hook_calls == [("test_arbiter", "test_worker")]
class TestDirtyHooksValidation:
"""Tests for hook validation."""
def test_on_dirty_starting_requires_callable(self):
"""Test that on_dirty_starting requires a callable."""
cfg = Config()
with pytest.raises(TypeError):
cfg.set("on_dirty_starting", "not_a_callable")
def test_dirty_post_fork_requires_callable(self):
"""Test that dirty_post_fork requires a callable."""
cfg = Config()
with pytest.raises(TypeError):
cfg.set("dirty_post_fork", 123)