docs.pyserve.org/guides/process-orchestration.html
Илья Глазунов 1f25033d2d initial commit
2025-12-05 12:57:41 +03:00

355 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Process Orchestration - pyserve</title>
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div id="container">
<div id="header">
<h1>pyserve</h1>
<div class="tagline">async http server</div>
</div>
<div class="breadcrumb">
<a href="../index.html">pyserve</a> » <a href="/guides/">Guides</a> » Process Orchestration
</div>
<div id="content">
<h2>Process Orchestration</h2>
<p>Process Orchestration is pyserve's flagship feature for running multiple Python web
applications with full process isolation. Each application runs in its own subprocess
with independent lifecycle, health monitoring, and automatic restart on failure.</p>
<h3>Overview</h3>
<p>Unlike <a href="asgi-mount.html">ASGI Mounting</a> which runs apps in-process,
Process Orchestration provides:</p>
<ul class="indent">
<li><strong>Process Isolation</strong> — Each app runs in a separate Python process</li>
<li><strong>Health Monitoring</strong> — Automatic health checks with configurable intervals</li>
<li><strong>Auto-restart</strong> — Failed processes restart with exponential backoff</li>
<li><strong>Multi-worker Support</strong> — Configure multiple uvicorn workers per app</li>
<li><strong>Dynamic Port Allocation</strong> — Automatic port assignment (9000-9999)</li>
<li><strong>WSGI Support</strong> — Flask/Django apps via automatic wrapping</li>
<li><strong>Request Tracing</strong> — X-Request-ID propagation through proxied requests</li>
</ul>
<h3>Architecture</h3>
<pre>
PyServe Gateway (:8000)
┌────────────────┼────────────────┐
▼ ▼ ▼
FastAPI Flask Starlette
:9001 :9002 :9003
/api/* /admin/* /ws/*
</pre>
<p>PyServe acts as a gateway, routing requests to the appropriate subprocess based on URL path.</p>
<h3>Basic Configuration</h3>
<pre><span class="directive">server:</span>
<span class="directive">host:</span> <span class="value">0.0.0.0</span>
<span class="directive">port:</span> <span class="value">8000</span>
<span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
- <span class="directive">name:</span> <span class="value">admin</span>
<span class="directive">path:</span> <span class="value">/admin</span>
<span class="directive">app_path:</span> <span class="value">myapp.admin:app</span></pre>
<h3>App Configuration Options</h3>
<dl>
<dt>name</dt>
<dd>Unique identifier for the application (required)</dd>
<dt>path</dt>
<dd>URL path prefix for routing requests (required)</dd>
<dt>app_path</dt>
<dd>Python import path. Format: <code>module:attribute</code> (required)</dd>
<dt>app_type</dt>
<dd>Application type: <code>asgi</code> or <code>wsgi</code>. Default: <code>asgi</code></dd>
<dt>workers</dt>
<dd>Number of uvicorn workers. Default: <code>1</code></dd>
<dt>port</dt>
<dd>Fixed port number. Default: auto-allocated from port_range</dd>
<dt>factory</dt>
<dd>If <code>true</code>, app_path points to a factory function. Default: <code>false</code></dd>
<dt>env</dt>
<dd>Environment variables to pass to the subprocess</dd>
<dt>module_path</dt>
<dd>Path to add to <code>sys.path</code> for module resolution</dd>
</dl>
<h3>Health Check Options</h3>
<dl>
<dt>health_check_enabled</dt>
<dd>Enable health monitoring. Default: <code>true</code></dd>
<dt>health_check_path</dt>
<dd>Endpoint to check for health. Default: <code>/health</code></dd>
<dt>health_check_interval</dt>
<dd>Interval between health checks in seconds. Default: <code>10.0</code></dd>
<dt>health_check_timeout</dt>
<dd>Timeout for health check requests. Default: <code>5.0</code></dd>
<dt>health_check_retries</dt>
<dd>Failed checks before restart. Default: <code>3</code></dd>
</dl>
<h3>Restart Options</h3>
<dl>
<dt>max_restart_count</dt>
<dd>Maximum restart attempts before giving up. Default: <code>5</code></dd>
<dt>restart_delay</dt>
<dd>Initial delay between restarts in seconds. Default: <code>1.0</code></dd>
<dt>shutdown_timeout</dt>
<dd>Timeout for graceful shutdown. Default: <code>30.0</code></dd>
</dl>
<h3>Global Configuration</h3>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">port_range:</span> <span class="value">[9000, 9999]</span>
<span class="directive">health_check_enabled:</span> <span class="value">true</span>
<span class="directive">proxy_timeout:</span> <span class="value">60.0</span>
<span class="directive">logging:</span>
<span class="directive">httpx_level:</span> <span class="value">warning</span>
<span class="directive">proxy_logs:</span> <span class="value">true</span>
<span class="directive">health_check_logs:</span> <span class="value">false</span>
<span class="directive">apps:</span>
<span class="comment"># ...</span></pre>
<dl>
<dt>port_range</dt>
<dd>Range for dynamic port allocation. Default: <code>[9000, 9999]</code></dd>
<dt>proxy_timeout</dt>
<dd>Timeout for proxied requests in seconds. Default: <code>60.0</code></dd>
<dt>logging.httpx_level</dt>
<dd>Log level for HTTP client (debug/info/warning/error). Default: <code>warning</code></dd>
<dt>logging.proxy_logs</dt>
<dd>Log proxied requests with latency. Default: <code>true</code></dd>
<dt>logging.health_check_logs</dt>
<dd>Log health check results. Default: <code>false</code></dd>
</dl>
<h3>FastAPI Example</h3>
<pre><span class="comment"># myapp/api.py</span>
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/users")
async def get_users():
return [{"id": 1, "name": "Alice"}]</pre>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
<span class="directive">workers:</span> <span class="value">4</span>
<span class="directive">health_check_path:</span> <span class="value">/health</span></pre>
<p>Requests to <code>/api/users</code> are proxied to the FastAPI process as <code>/users</code>.</p>
<h3>Flask Example (WSGI)</h3>
<pre><span class="comment"># myapp/admin.py</span>
from flask import Flask
app = Flask(__name__)
@app.route("/health")
def health():
return {"status": "ok"}
@app.route("/dashboard")
def dashboard():
return {"page": "dashboard"}</pre>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">admin</span>
<span class="directive">path:</span> <span class="value">/admin</span>
<span class="directive">app_path:</span> <span class="value">myapp.admin:app</span>
<span class="directive">app_type:</span> <span class="value">wsgi</span>
<span class="directive">workers:</span> <span class="value">2</span></pre>
<div class="note">
<strong>Note:</strong> WSGI support requires <code>a2wsgi</code> package:
<code>pip install a2wsgi</code>
</div>
<h3>Factory Pattern</h3>
<pre><span class="comment"># myapp/api.py</span>
from fastapi import FastAPI
def create_app(debug: bool = False) -> FastAPI:
app = FastAPI(debug=debug)
@app.get("/health")
async def health():
return {"status": "ok", "debug": debug}
return app</pre>
<pre><span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">myapp.api:create_app</span>
<span class="directive">factory:</span> <span class="value">true</span></pre>
<h3>Environment Variables</h3>
<p>Pass environment variables to subprocesses:</p>
<pre><span class="directive">apps:</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">myapp.api:app</span>
<span class="directive">env:</span>
<span class="directive">DATABASE_URL:</span> <span class="value">"postgresql://localhost/mydb"</span>
<span class="directive">REDIS_URL:</span> <span class="value">"redis://localhost:6379"</span>
<span class="directive">DEBUG:</span> <span class="value">"false"</span></pre>
<h3>Multiple Applications</h3>
<pre><span class="directive">extensions:</span>
- <span class="directive">type:</span> <span class="value">process_orchestration</span>
<span class="directive">config:</span>
<span class="directive">port_range:</span> <span class="value">[9000, 9999]</span>
<span class="directive">apps:</span>
<span class="comment"># FastAPI REST API</span>
- <span class="directive">name:</span> <span class="value">api</span>
<span class="directive">path:</span> <span class="value">/api</span>
<span class="directive">app_path:</span> <span class="value">apps.api:app</span>
<span class="directive">workers:</span> <span class="value">4</span>
<span class="comment"># Flask Admin Panel</span>
- <span class="directive">name:</span> <span class="value">admin</span>
<span class="directive">path:</span> <span class="value">/admin</span>
<span class="directive">app_path:</span> <span class="value">apps.admin:app</span>
<span class="directive">app_type:</span> <span class="value">wsgi</span>
<span class="directive">workers:</span> <span class="value">2</span>
<span class="comment"># Starlette WebSocket Handler</span>
- <span class="directive">name:</span> <span class="value">websocket</span>
<span class="directive">path:</span> <span class="value">/ws</span>
<span class="directive">app_path:</span> <span class="value">apps.websocket:app</span>
<span class="directive">workers:</span> <span class="value">1</span></pre>
<h3>Request Tracing</h3>
<p>PyServe automatically generates and propagates <code>X-Request-ID</code> headers:</p>
<ul class="indent">
<li>If a request has <code>X-Request-ID</code>, it's preserved</li>
<li>Otherwise, a UUID is generated</li>
<li>The ID is passed to subprocesses and included in response headers</li>
<li>All logs include the request ID for tracing</li>
</ul>
<h3>Process Orchestration vs ASGI Mount</h3>
<table class="dirindex">
<tr>
<th>Feature</th>
<th>Process Orchestration</th>
<th>ASGI Mount</th>
</tr>
<tr>
<td>Isolation</td>
<td>Full process isolation</td>
<td>Shared process</td>
</tr>
<tr>
<td>Memory</td>
<td>Separate per app</td>
<td>Shared</td>
</tr>
<tr>
<td>Crash Impact</td>
<td>Only that app restarts</td>
<td>All apps affected</td>
</tr>
<tr>
<td>Health Checks</td>
<td>Yes, with auto-restart</td>
<td>No</td>
</tr>
<tr>
<td>Multi-worker</td>
<td>Yes, per app</td>
<td>No</td>
</tr>
<tr>
<td>Latency</td>
<td>HTTP proxy overhead</td>
<td>In-process (faster)</td>
</tr>
<tr>
<td>Use Case</td>
<td>Production, isolation needed</td>
<td>Development, simple setups</td>
</tr>
</table>
<div class="note">
<strong>When to use Process Orchestration:</strong>
<ul class="indent">
<li>Running multiple apps that shouldn't affect each other</li>
<li>Need automatic restart on failure</li>
<li>Different resource requirements per app</li>
<li>Production deployments</li>
</ul>
<strong>When to use ASGI Mount:</strong>
<ul class="indent">
<li>Development and testing</li>
<li>Simple setups with trusted apps</li>
<li>Minimal latency requirements</li>
</ul>
</div>
<div class="note">
<strong>See Also:</strong>
<ul class="indent">
<li><a href="asgi-mount.html">ASGI Mounting Guide</a> — In-process app mounting</li>
<li><a href="../reference/extensions.html">Extensions Reference</a> — All extension types</li>
<li><a href="configuration.html">Configuration Guide</a> — Full configuration reference</li>
</ul>
</div>
</div>
<div id="footer">
<p>pyserve &copy; 2024-2025 | MIT License</p>
</div>
</div>
</body>
</html>