355 lines
15 KiB
HTML
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 © 2024-2025 | MIT License</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|