mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 18:21:30 +08:00
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
110 lines
3.1 KiB
Python
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)
|