gunicorn/tests/support_dirty_app.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

96 lines
2.0 KiB
Python

#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""Support module for dirty app tests."""
from gunicorn.dirty.app import DirtyApp
class TestDirtyApp(DirtyApp):
"""A simple dirty app for testing."""
def __init__(self):
self.initialized = False
self.closed = False
self.data = {}
def init(self):
self.initialized = True
self.data['init_called'] = True
def store(self, key, value):
self.data[key] = value
return {"stored": True, "key": key}
def retrieve(self, key):
return self.data.get(key)
def compute(self, a, b, operation="add"):
if operation == "add":
return a + b
elif operation == "multiply":
return a * b
else:
raise ValueError(f"Unknown operation: {operation}")
def close(self):
self.closed = True
self.data.clear()
class BrokenInitApp(DirtyApp):
"""A dirty app that fails during init."""
def init(self):
raise RuntimeError("Init failed!")
class BrokenInstantiationApp(DirtyApp):
"""A dirty app that fails during instantiation."""
def __init__(self):
raise RuntimeError("Cannot instantiate!")
class NotAClass:
"""Not a class, just an instance for testing."""
pass
not_a_class = NotAClass()
class MissingCallApp:
"""An invalid dirty app missing __call__."""
def init(self):
pass
def close(self):
pass
class SlowDirtyApp(DirtyApp):
"""A dirty app with slow methods for timeout testing."""
def __init__(self):
self.initialized = False
self.closed = False
def init(self):
self.initialized = True
def slow_action(self, delay=1.0):
"""An action that takes time to complete."""
import time
time.sleep(delay)
return {"delayed": True, "duration": delay}
def fast_action(self):
"""A fast action for comparison."""
return {"fast": True}
def close(self):
self.closed = True