gunicorn/tests/test_dirty_errors.py
Benoit Chesneau 8559854b4f feat(dirty): add per-app worker allocation for memory optimization
Allow dirty apps to specify how many workers should load them, enabling
significant memory savings for heavy applications like ML models.

- Add `workers` class attribute to DirtyApp (None = all workers)
- Add `parse_dirty_app_spec()` to parse "module:Class:N" format
- Add `DirtyNoWorkersAvailableError` for app-specific error handling
- Update DirtyArbiter with per-app worker tracking and routing
- Maintain backward compatibility when no dirty_apps configured

Example: 8 workers x 10GB model = 80GB RAM needed
With workers=2: 2 x 10GB = 20GB RAM (75% savings)

Configuration formats:
- Class attribute: `workers = 2` on DirtyApp subclass
- Config format: `module:class:N` (e.g., `myapp.ml:HugeModel:2`)
2026-02-01 02:40:09 +01:00

77 lines
2.6 KiB
Python

#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""Tests for dirty errors module."""
import pytest
from gunicorn.dirty.errors import (
DirtyError,
DirtyNoWorkersAvailableError,
)
class TestDirtyNoWorkersAvailableError:
"""Tests for DirtyNoWorkersAvailableError exception."""
def test_error_contains_app_path(self):
"""Error includes the app_path."""
error = DirtyNoWorkersAvailableError("myapp:Model")
assert error.app_path == "myapp:Model"
assert "myapp:Model" in str(error)
assert "No workers available" in str(error)
def test_error_with_custom_message(self):
"""Error can have a custom message."""
error = DirtyNoWorkersAvailableError(
"myapp:Model",
message="Custom: no workers for heavy model"
)
assert error.app_path == "myapp:Model"
assert "Custom: no workers" in str(error)
def test_error_serialization_roundtrip(self):
"""Error survives to_dict/from_dict cycle."""
original = DirtyNoWorkersAvailableError("myapp.ml:HugeModel")
# Serialize
data = original.to_dict()
assert data["error_type"] == "DirtyNoWorkersAvailableError"
assert data["details"]["app_path"] == "myapp.ml:HugeModel"
# Deserialize
restored = DirtyError.from_dict(data)
assert isinstance(restored, DirtyNoWorkersAvailableError)
assert restored.app_path == "myapp.ml:HugeModel"
assert "No workers available" in str(restored)
def test_error_is_dirty_error_subclass(self):
"""DirtyNoWorkersAvailableError is a DirtyError subclass."""
error = DirtyNoWorkersAvailableError("app:Class")
assert isinstance(error, DirtyError)
def test_web_app_can_catch_specific_error(self):
"""Web app can catch DirtyNoWorkersAvailableError specifically."""
def simulate_execute():
raise DirtyNoWorkersAvailableError("myapp:HeavyModel")
# Catch specific error
try:
simulate_execute()
assert False, "Should have raised"
except DirtyNoWorkersAvailableError as e:
assert e.app_path == "myapp:HeavyModel"
def test_can_catch_as_base_error(self):
"""Can catch DirtyNoWorkersAvailableError as DirtyError."""
def simulate_execute():
raise DirtyNoWorkersAvailableError("myapp:Model")
try:
simulate_execute()
assert False, "Should have raised"
except DirtyError as e:
# Should catch it as the base class
assert hasattr(e, "app_path")