mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 18:21:30 +08:00
* feat(dirty): add stash - global shared state between workers
Add a simple key-value store (stash) that allows dirty workers to share
state through the arbiter. Tables are stored directly in arbiter memory
for fast access and simplicity.
Features:
- Auto-create tables on first access
- Dict-like interface via stash.table()
- Pattern matching for keys (glob patterns)
- Module-level API: stash.put(), stash.get(), stash.delete(), etc.
Usage:
from gunicorn.dirty import stash
stash.put("sessions", "user:1", {"name": "Alice"})
user = stash.get("sessions", "user:1")
# Or dict-like
sessions = stash.table("sessions")
sessions["user:1"] = {"name": "Alice"}
New files:
- gunicorn/dirty/stash.py - Client API and StashTable class
- Protocol additions for MSG_TYPE_STASH and STASH_OP_* codes
Note: Tables are ephemeral - lost if arbiter restarts.
* test(dirty): add tests for stash protocol and encoding
Test coverage for:
- Stash message creation and encoding
- Protocol constants (MSG_TYPE_STASH, STASH_OP_*)
- Error classes (StashError, StashTableNotFoundError, StashKeyNotFoundError)
- StashTable dict-like interface
- Edge cases: unicode, complex values, special patterns
* example(dirty): add stash usage example and integration tests
- Add SessionApp to dirty_app.py demonstrating stash usage
- Add /session/* endpoints to wsgi_app.py
- Add test_stash_integration.py with Docker tests
- Update docker-compose.yml with stash-test service
- Fix: Set GUNICORN_DIRTY_SOCKET in dirty arbiter for worker access
* docs(dirty): add stash documentation
226 lines
7.8 KiB
Python
226 lines
7.8 KiB
Python
#
|
|
# This file is part of gunicorn released under the MIT license.
|
|
# See the NOTICE for more information.
|
|
|
|
"""
|
|
Example WSGI Application that uses Dirty Workers
|
|
|
|
This demonstrates how HTTP workers can call dirty workers
|
|
for heavy operations like ML inference.
|
|
|
|
Run with:
|
|
cd examples/dirty_example
|
|
gunicorn wsgi_app:app -c gunicorn_conf.py
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from urllib.parse import parse_qs
|
|
|
|
|
|
def get_dirty_client():
|
|
"""Get the dirty client, with fallback for when dirty workers aren't enabled."""
|
|
try:
|
|
from gunicorn.dirty import get_dirty_client as _get_dirty_client
|
|
return _get_dirty_client()
|
|
except Exception as e:
|
|
return None
|
|
|
|
|
|
def app(environ, start_response):
|
|
"""WSGI application that demonstrates dirty worker integration."""
|
|
path = environ.get('PATH_INFO', '/')
|
|
method = environ.get('REQUEST_METHOD', 'GET')
|
|
|
|
# Parse query string
|
|
query = parse_qs(environ.get('QUERY_STRING', ''))
|
|
|
|
# Get dirty client
|
|
client = get_dirty_client()
|
|
|
|
try:
|
|
if path == '/':
|
|
result = {
|
|
"message": "Dirty Workers Demo",
|
|
"dirty_enabled": client is not None,
|
|
"pid": os.getpid(),
|
|
"endpoints": {
|
|
"/models": "List loaded models",
|
|
"/load?name=MODEL": "Load a model",
|
|
"/inference?model=NAME&data=INPUT": "Run inference",
|
|
"/unload?name=MODEL": "Unload a model",
|
|
"/fibonacci?n=NUMBER": "Compute fibonacci",
|
|
"/prime?n=NUMBER": "Check if prime",
|
|
"/stats": "Get dirty worker stats",
|
|
"/session/login?user_id=ID&name=NAME": "Login user (stash demo)",
|
|
"/session/get?user_id=ID": "Get session (stash demo)",
|
|
"/session/list": "List all sessions (stash demo)",
|
|
"/session/logout?user_id=ID": "Logout user (stash demo)",
|
|
"/session/stats": "Get stash stats (stash demo)",
|
|
}
|
|
}
|
|
|
|
elif path == '/models':
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:MLApp",
|
|
"list_models"
|
|
)
|
|
|
|
elif path == '/load':
|
|
name = query.get('name', ['model1'])[0]
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:MLApp",
|
|
"load_model",
|
|
name
|
|
)
|
|
|
|
elif path == '/inference':
|
|
model = query.get('model', ['default'])[0]
|
|
data = query.get('data', ['test input'])[0]
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:MLApp",
|
|
"inference",
|
|
model,
|
|
data
|
|
)
|
|
|
|
elif path == '/unload':
|
|
name = query.get('name', ['model1'])[0]
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:MLApp",
|
|
"unload_model",
|
|
name
|
|
)
|
|
|
|
elif path == '/fibonacci':
|
|
n = int(query.get('n', ['10'])[0])
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:ComputeApp",
|
|
"fibonacci",
|
|
n
|
|
)
|
|
|
|
elif path == '/prime':
|
|
n = int(query.get('n', ['17'])[0])
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:ComputeApp",
|
|
"prime_check",
|
|
n
|
|
)
|
|
|
|
elif path == '/stats':
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
ml_stats = client.execute(
|
|
"examples.dirty_example.dirty_app:MLApp",
|
|
"list_models"
|
|
)
|
|
compute_stats = client.execute(
|
|
"examples.dirty_example.dirty_app:ComputeApp",
|
|
"stats"
|
|
)
|
|
result = {
|
|
"ml_app": ml_stats,
|
|
"compute_app": compute_stats,
|
|
"http_worker_pid": os.getpid(),
|
|
}
|
|
|
|
# =====================================================================
|
|
# Session endpoints (stash demo)
|
|
# =====================================================================
|
|
elif path == '/session/login':
|
|
user_id = query.get('user_id', ['1'])[0]
|
|
name = query.get('name', ['Anonymous'])[0]
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:SessionApp",
|
|
"login",
|
|
user_id=user_id,
|
|
user_data={"name": name}
|
|
)
|
|
|
|
elif path == '/session/get':
|
|
user_id = query.get('user_id', ['1'])[0]
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:SessionApp",
|
|
"get_session",
|
|
user_id=user_id
|
|
)
|
|
|
|
elif path == '/session/list':
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:SessionApp",
|
|
"list_sessions"
|
|
)
|
|
|
|
elif path == '/session/logout':
|
|
user_id = query.get('user_id', ['1'])[0]
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:SessionApp",
|
|
"logout",
|
|
user_id=user_id
|
|
)
|
|
|
|
elif path == '/session/stats':
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:SessionApp",
|
|
"get_stats"
|
|
)
|
|
|
|
elif path == '/session/clear':
|
|
if client is None:
|
|
result = {"error": "Dirty workers not enabled"}
|
|
else:
|
|
result = client.execute(
|
|
"examples.dirty_example.dirty_app:SessionApp",
|
|
"clear_all"
|
|
)
|
|
|
|
else:
|
|
start_response('404 Not Found', [('Content-Type', 'application/json')])
|
|
return [json.dumps({"error": "Not found"}).encode()]
|
|
|
|
# Success response
|
|
start_response('200 OK', [('Content-Type', 'application/json')])
|
|
return [json.dumps(result, indent=2).encode()]
|
|
|
|
except Exception as e:
|
|
start_response('500 Internal Server Error', [('Content-Type', 'application/json')])
|
|
return [json.dumps({
|
|
"error": str(e),
|
|
"type": type(e).__name__
|
|
}).encode()]
|