module-development-spec 99KB→57KB、harnessed-module-development 101KB→62KB、 crud-definition-spec 78KB→46KB、dspy-file-implementation-spec 51KB→26KB、 ahserver 50KB→26KB、sqlor-database-module 49KB→26KB、database-table-definition-spec 20KB→19KB 合计 448KB→263KB,压缩 41%,节省约 92k token
26 KiB
| name | description | version | author | tags | dependencies | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ahserver | ahserver - Asynchronous HTTP(S) web application framework based on aiohttp | 1.3.0 | yu moqing |
|
|
ahserver Web Application Framework
Overview
Async HTTP(S) server built on aiohttp. Features: auth/authorization, HTTPS (SSL/TLS), multi-DB connection pools (MySQL/PostgreSQL/Oracle/SQL Server), processors for .dspy/.tmpl/.md/.xlsxds/.sqlds/.wss, i18n, auto-stored file uploads, background tasks, RESTful APIs.
Installation
- Python 3.8+. From source:
cd /tmp/ahserver && pip install -e . - Python 3.12+ aioredis fix:
pip install packaging, then editaioredis/connection.py:11→from packaging.version import Version as StrictVersion;aioredis/exceptions.py:14→class TimeoutError(asyncio.TimeoutError, RedisError):
Project Structure
your-app/
├── ah.py # entry point
├── conf/config.json
├── i18n/
└── app/
├── index.html
├── api/status/index.dspy # → /api/status (directory pattern, 1.2.0+)
├── template.tmpl # Jinja2
└── data.sqlds
⚠️ 1.2.0+ requires directory/index.dspy for APIs — flat api/status.dspy no longer resolves to /api/status. See references/api-routing-patterns.md.
Configuration (conf/config.json)
Top-level keys: password_key, databases, website, langMapping.
website:paths: [["$[workdir]$/app", ""]],host: "0.0.0.0",port: 8080,coding: "utf-8", optionalssl: {crtfile, keyfile},indexes: ["index.html","index.ui","index.tmpl","index.dspy","index.md"],processors: [[".xlsxds","xlsxds"],[".sqlds","sqlds"],[".tmpl","tmpl"],[".dspy","dspy"],[".md","md"]].langMapping:{"zh-Hans-CN": "zh-cn", "en-US": "en"}.
⚠️ password_key must be a TOP-LEVEL field (same level as databases). sqlor reads it via getConfig().password_key to AES-decrypt DB passwords. Without it, self.unpassword() returns None and connection fails.
Database entries (all use coding: "utf8" + dbname; kwargs differ by driver)
| Driver | kwargs |
|---|---|
MySQL/MariaDB: mysql.connector |
user, db, password, host |
PostgreSQL: psycopg2 |
database, user, password, host, port |
Oracle: cx_Oracle |
user, host, dsn (e.g. 10.0.185.137:1521/SAMPLEDB) |
SQL Server: pymssql |
user, database, password, server, port (1433), charset (utf8) |
Passwords are AES-ECB base64 (see Password Encryption below).
Usage Examples
Basic Application Setup (ah.py) — startup hooks
| Version | API | Import |
|---|---|---|
| ≤1.0.x | RegisterCoroutine().register('ahapp_built', cb) |
ahserver.configuredServer |
| ≥1.2.0 | add_startup(cb) |
ahserver.configuredServer |
⚠️ RegisterCoroutine REMOVED in 1.2.0 → use add_startup. Callback signature changed: old (app) → new (*args, **kw).
from ahserver.webapp import webapp
from ahserver.serverenv import ServerEnv
from ahserver.configuredServer import add_startup
async def on_app_built(*args, **kw): # runs after auth middleware + processors are set up
asyncio.ensure_future(my_background_task())
def init():
env = ServerEnv()
env.get_module_dbname = lambda m: 'your_db_name'
add_startup(on_app_built)
if __name__ == '__main__':
webapp(init)
Authentication API
from ahserver.auth_api import AuthAPI
class MyAuthAPI(AuthAPI):
def needAuth(self, path): ... # True → require auth
async def getPermissionNeed(self, path): ... # permission name for path
async def checkUserPassword(self, user_id, password): ...
async def getUserPermissions(self, user): ... # list of permission names
# run: ConfiguredServer(MyAuthAPI).run()
Processor Types
.dspy (Dynamic Python Scripts)
Execution model: code runs directly in script scope; the framework captures the script's TOP-LEVEL return as the response.
⚠️ Setting a result variable alone does NOT work (framework sees NoneType). Do NOT wrap code in async def run() — its return goes nowhere at script scope. Always return at top level:
return json.dumps({'status': 'success', 'data': result}, ensure_ascii=False)
In-scope helpers: await get_user(), await redirect('/login'), uuid(), DBPools(); CRUD via async with db.sqlorContext('dbname') as sor: → await sor.C('tbl', ns), sor.R('tbl', ns), sor.U('tbl', ns), sor.D('tbl', {'id':...}), sor.sqlExe("SELECT ... WHERE id=${id}$", {'id':...}) (placeholders ${name}$), sor.sqlPaging(sql, {'search':..., 'page':1, 'pagerows':20, 'sort':'name'}).
File Upload
Multipart uploads auto-handled: files are saved by FileStorage BEFORE the .dspy runs; params_kw contains the relative web_path, not the file object.
from ahserver.filestorage import FileStorage
abs_path = FileStorage().realPath(params_kw.get('audio_file')) # '/66/34/59/64/file.mp3' → '/tmp/66/34/59/64/file.mp3'
See references/file-upload-patterns.md (examples, curl testing, debugging).
.tmpl / .sqlds / .xlsxds / .md
.tmpl: Jinja2 template rendered with request context (see Jinja2 filter pitfall below)..sqlds: SQL query exposed as data source;.xlsxds: Excel as structured data;.md: rendered as HTML.
.wss (WebSocket Endpoints)
Must contain async def myfunc(request, **kwargs). Behavior (websocketProcessor.py): reads client Sec-WebSocket-Protocol header as cookie → get_user() auth; injects ws_pool (WsPool) and ws_data (message text) into kwargs; runs myfunc per incoming TEXT message; ws_pool.sendto(data, id=None) pushes JSON.
async def myfunc(request, **kwargs):
data = json.loads(kwargs.get('ws_data') or '{}')
if data.get('cmd') == 'ping':
await kwargs['ws_pool'].sendto(json.dumps({'type': 'pong'}))
⚠️ Frontend must pass the cookie: new WebSocket(url, document.cookie) — without it auth fails.
Global Environment Variables
- Session (async):
get_user(),remember_user(userid, username='', userorgid=''),forget_user(),redirect(url),entire_url(url)(full URL with scheme/host/port),gethost()(client IP),path_call(path, **kw)(call other server resources). - Globals:
configValue(k)(e.g.configValue('.website.port')),DBPools()(sqlor pools),uuid(),curDatetime(),str2date(dstr),str2datetime(dstr),server_error(errcode)(400/401/403/404/500...). - Built-in modules:
time,datetime,random,json,ArgsConvert,DictObject.
CRUD with SQLor
All CRUD requires a table with id primary key. Pattern:
db = DBPools()
async with db.sqlorContext('dbname') as sor:
await sor.C('tbl', {'id': uuid(), 'field1': 'value1'}) # create
recs = await sor.R('tbl', params_kw.copy()) # read (client params)
await sor.U('tbl', params_kw.copy()) # update
await sor.D('tbl', {'id': params_kw.id}) # delete
recs = await sor.RP('tbl', ns) # paged → {'total': n, 'rows': [...]}; ns: page/sort/pagerows
Running Behind Nginx
Forward these headers:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Url $request_uri;
proxy_set_header X-Forwarded-Prepath "";
Performance
Middleware chain per request: real_ip_middleware → session_middleware → auth_api.checkAuth() → ProcessorResource._handle() → handler.
Per-request overhead (local SSD): Redis session GET 0.06ms (only if session_redis); info() log flush 0.01ms (production NFS/cloud: 10–100ms); _handle() closures + isHtml 0.22ms; network + file I/O 1.23ms. ⚠️ Production latency can be 10–50x higher (NFS/cloud disk flush, I/O contention, multiple log calls per request).
- Log flush (P0+P3 FIXED, commits
574ef00/5238a08,appPublic/log.py): file opened once + kept open;threading.Queue(maxsize=10000)+ daemon thread for non-blocking writes; onlyexception/criticalflush immediately, others every ~1s idle; queue-full drops oldest instead of blocking the loop; public API (info()etc.) unchanged. ⚠️ On local SSD flush ≈ 0.01ms — if prod disk is local SSD, log flush is NOT the bottleneck. - isHtml() (FIXED,
processorResource.py:398): now reads only first 512 bytes (f.read(512)+decode('utf-8', errors='ignore')) instead of the whole file (was reading e.g. 48KB bricks.js / 1MB echarts.min.js). - Static file fast path (APPLIED,
574ef00): at very top ofProcessorResource._handle()(before parse_request/closures/i18n/isHtml): ifrequest.path.lower()ends with a static ext (.js .css .png .jpg .jpeg .gif .ico .svg .woff .woff2 .ttf .eot .map .webp .bmp .mp3 .mp4 .webm .ogg .wav) →self.parse_request(request)thenreturn await super()._handle(request). ⚠️parse_requestMUST run first —super()needsself._preurlset by it. - Session on static files: with
session_redis, every request incl. static triggers a Redis GET. ConsiderEncryptedCookieStorageinstead ofRedisStoragefor static-heavy apps, or bypass session for known static paths.
timecost log format (auth_api.py)
timecost=client(IP) user_id access /path cost TOTAL, (AUTH_MS) — TOTAL = full handler execution; AUTH_MS = auth/permission check only. Diagnostic rule: AUTH_MS small (1–3ms) but total slow → bottleneck is in the handler / .dspy, NOT RBAC.
Unauthenticated static files
/bricks/3parties/, /bricks/css/, /bricks/*.js require any role permission in load_path.py; otherwise unauthenticated requests return need login (redirect, not 403). Check rbac/check_perm.py logs: userid=None, path='...' permission check failed,userroles=['anonymous', 'any'].
Pitfalls
⚠️ CRITICAL: StreamResponse breaks aiohttp_auth ticket reissue — active users get kicked out
aiohttp_auth.process_response() renews tickets only for web.Response instances; ahserver returns StreamResponse, so the check always fails and tickets are never renewed — users logged out after session_max_time (default 2h) regardless of activity. Fix in auth_api.py checkAuth after ret = await handler(request):
from aiohttp_auth.auth.ticket_auth import _REISSUE_KEY
if _REISSUE_KEY in request:
policy = request.get('aiohttp_auth.policy')
if policy and hasattr(policy, 'remember_ticket'):
await policy.remember_ticket(request, request[_REISSUE_KEY])
Diagnostic: users "kicked out while actively using the app" → verify this fix is present. See references/session-timeout-architecture.md.
⚠️ 1.2.0+ routing: use directory/index.dspy OR apply extensionless-URL fallback patch
≤1.0.x auto-resolved /api/status → /api/status.dspy; 1.2.0 removed auto-extension → extensionless URLs raise Exception: ... invalid path (HTTP 500). bricks/i18n.js hardcodes /i18n_getmsgs (no extension) → every Bricks app 500s on load without a fix.
Recommended: directory pattern (api/status/index.dspy, resolved natively via indexes config). Migration: cd app/api && for f in *.dspy; do mkdir -p ${f%.dspy} && mv $f ${f%.dspy}/index.dspy; done.
Fallback patch (commit 7a297e9, 2026-06-25) — only for hardcoded extensionless URLs:
processorResource.py_handle(): whenurl2file(path)returns None, tryurl2file(path + ext)for each registered processor extension; then afterurl2processor, ifrequest_filenameis set and processor has noreal_path, setprocessor.real_path = self.request_filename.baseProcessor.pyset_run_env(): computereal_pathonly if not already set (plainurl2file(request.path)returns None for extensionless URLs). Why two patches:BaseProcessor.__init__doesn't setreal_path; patch 1 sets it beforehandle(), patch 2 stopsset_run_envoverwriting it. Verify:curl -u admin:pass http://localhost:9090/i18n_getmsgs→ HTTP 200{"success": true, "msgs": {...}}; if 500 with logTypeError: expected str, bytes or os.PathLike object, not NoneType, patch 2 is missing. ❌ Not recommended: nginx rewrite (rewrite ^/api/(.*)$ /api/$1.dspy break;) — adds complexity, bypasses framework resolution.
⚠️ aligner service: own venv + numpy<2 + PYTHONPATH
Aligner (/data/ymq/aligner/) uses its own venv /data/ymq/aligner/py3/ (NOT /share/vllm-0.8.5); requires ctc_segmentation and numpy<2 (numpy>=2 breaks ctc_segmentation). Start with PYTHONPATH=/data/ymq/aligner/app:/data/ymq/aligner — aligner.py does from align import AlignEngine; without PYTHONPATH: ModuleNotFoundError: No module named 'align'. Listens on 8080, POST /api/align {audio_path, text}.
⚠️ demucs pitfalls
/share/vllm-0.8.5venv is root-owned →sudo /share/vllm-0.8.5/bin/pip install demucs; binary/share/vllm-0.8.5/bin/demucs; use-n htdemucs(4-stem) or-n htdemucs_ft(fine-tuned, better but slower).- demucs 4.0.1 + newer torchaudio:
torchaudio.save()→save_with_torchcodec()failsRuntimeError: Could not load libtorchcodec(needslibnvrtc.so.13, CUDA runtime, not on shared lib path). Separation completes 100% but save crashes. Fix: wrapper script monkey-patchingtorchaudio.savetosoundfile(sf.write(uri, wav.T if wav.shape[0]<=wav.shape[1] else wav, sample_rate)); requiressoundfile(present in/share/vllm-0.8.5); invoke:/share/vllm-0.8.5/bin/python /tmp/demucs_wrapper.py --two-stems=vocals -o <out> <in>.
⚠️ GPU services: independent venv + longtasks pattern, NEVER FastAPI
Rule: all GPU services on the media server (ymq@opencomputing.net) MUST use ahserver + longtasks, not FastAPI. Each service needs its OWN venv — never install into /share/vllm-0.8.5 (diffusers 0.35.2 + old transformers → HybridCache import error). CRITICAL: search existing code FIRST (media-server, aligner, songrate; session_search(query="longtasks ahserver deploy")) — reuse proven implementations, don't reinvent.
longtasks quirks: submit_task(payload) returns a dict {'task_id': '...'} (not a string) → task_id = result.get('task_id'); get_status(task_id) → PENDING|RUNNING|SUCCEEDED|FAILED.
Template (ah.py): subclass LongTasks.process_task(payload, workid=None) (payload may be JSON string — json.loads it); in on_app_built: if env.longtasks: schedule_once(0.1, env.longtasks.run); in init(): env.longtasks = MyTasks('redis://127.0.0.1:6379', 'myqueue', worker_cnt=1, stuck_seconds=3600).
Setup checklist: python3 -m venv ~/my-service/venv → pip install ahserver appPublic sqlor longtasks aiohttp → write ah.py + .dspy routes → conf/config.json (port, paths, processors) → nohup python ah.py > service.log 2>&1 &.
GitHub is blocked on the GPU server: SOCKS5 tunnel ssh -N -D 1086 ymq@proxy-server → clone with git -c http.proxy=socks5h://127.0.0.1:1086 -c https.proxy=socks5h://127.0.0.1:1086 clone <url> → scp -r to server.
See references/gpu-service-longtasks-pattern.md (wan22 example — DEPRECATED, replaced by wan27); references/ssh-access-map.md (passwordless hosts, server inventory).
⚠️ startswiths config: must use registerfunction, NOT path
{"leading": "/api/foo", "path": ...} → KeyError: 'registerfunction' on EVERY request (FunctionProcessor.path_call() unconditionally reads config_opts['registerfunction']; path belongs to a different routing mechanism). Correct:
"startswiths": [{"leading": "/idfile", "registerfunction": "idfile"}]
Prefer directory/index.dspy for new APIs; use startswiths + registerfunction only for Python-registered function handlers (RegisterFunction).
⚠️ idfile download endpoint: explicit import AND config required
/idfile?path=... → 500 "invalid path" unless BOTH: (1) from ahserver import filedownload in ah.py (registers idfile/download via RegisterFunction at import time; path_download() resolves path via FileStorage().realPath() → file_response); (2) startswiths entry {"leading": "/idfile", "registerfunction": "idfile"}.
⚠️ Chinese chars in path query MUST be URL-encoded (%E9%94%99%E9%A2%91, not 错频) — server rejects unencoded.
⚠️ appPublic.worker missing schedule functions (legacy ≤5.2.x / ahserver ≤1.0.8)
Only exports AsyncWorker — from appPublic.worker import schedule_once → ImportError. Fixed in appPublic ≥5.3.0 (pip install --upgrade apppublic). Workaround: define local get_event_loop() (= asyncio.get_event_loop()), schedule_once(delay, coro_func) and schedule_interval(interval, coro_func) as asyncio.ensure_future wrappers. Applies to longtasks and any appPublic.worker scheduling code; clear __pycache__ after patching.
⚠️ stream_response re-raises ClientConnectionResetError — noisy logs
ahserver/globalEnv.py stream_response() wraps ALL write errors in a generic Exception and re-raises, including ClientConnectionResetError (client disconnected mid-stream) → every client timeout during SSE floods logs with stack traces. Fix (commit 387726e): catch ClientConnectionResetError separately and break the streaming loop; other exceptions still logged (write error{e=}, {d=}) and re-raised.
⚠️ @routes decorator removed in 1.2.0+ — use app.router.add_route()
from ahserver.webapp import webapp, routes, add_startup → ImportError: cannot import name 'routes'; leftover @routes.get(...) → NameError. Fix: register inside on_app_built:
async def on_app_built(app):
app.router.add_route('GET', '/api/health', health)
Migration: grep -rn '@routes\.' /data/ymq/*/ah.py, remove decorator lines, register handlers in on_app_built.
⚠️ CRITICAL: @ inside comments can truncate files on production deploy
Incident 2026-05-26: auth_api.py comment # redis = await aioredis.from_url("redis://127.0.0.1:6379") preceded by @web.middleware text — the deployment process truncated the file at that line, losing ~30 lines (aiohttp_session.setup(app, storage), auth.setup(app, policy), app.middlewares.append(self.checkAuth)). Symptom: all requests 500, checkAuth never fires, no auth logs.
Prevention: never put @something patterns in comments (esp. strings containing @); reference URLs without the @user:pass portion. After deploying auth_api.py changes ALWAYS verify: grep -c "middlewares.append" auth_api.py ≥ 1; grep -E "middlewares.append|setup\(app|aiohttp_session.setup" auth_api.py → 3+ lines.
⚠️ Jinja2 in .ui/.tmpl: limited filter set
Non-standard filters (e.g. |ternary) → jinja2.exceptions.TemplateAssertionError: No filter named 'ternary'. Only standard Jinja2 filters + ahserver built-ins (entire_url(), get_user(), configValue()) are registered. Use if/else: {{get_user().nick_name if get_user() else '登录'}}. Rule: keep Jinja2 expressions simple in .ui JSON templates; complex conditionals handled client-side in JS.
⚠️ Never modify sage/conf/config.json during feature development
It is the PRODUCTION config for the Sage platform. Config changes (DB password, session settings) are handled manually by the user.
Verify auth_api.py after any edit
wc -l ahserver/auth_api.py ≈ 193 (not fewer); grep "middlewares.append" present; python3 -c "import ast; ast.parse(open('ahserver/auth_api.py').read())" parses.
Common Issues
Password Encryption
DB passwords in conf/config.json are AES-ECB base64 using password_key:
from appPublic.aes import aes_encode_b64
encoded = aes_encode_b64(getConfig().password_key, 'plaintext_password')
sqlor/sor.py calls self.unpassword() in __init__ (aes_decode_b64). ⚠️ Password MUST be valid AES-ECB base64 — plain text or RC4-encoded → ValueError: The length of the provided data is not a multiple of the block length. Quick encode: python3 -c "from appPublic.aes import aes_encode_b64; print(aes_encode_b64('YOUR_PASSWORD_KEY', 'test'))". Legacy RC4: python -m ahserver.dbpassword /path/to/app password123.
Debugging
Check console logs; format_exc() for detailed errors; request context via request._run_ns.
Performance: benchmark conclusions
May 2026 (local SSD, Redis session storage, clientinfo level, 48KB static file): pure aiohttp 1.23ms vs ahserver before fixes 1.13ms / after 3 fixes 1.10ms. → ahserver code overhead is negligible; a 17s production delay is NOT caused by ahserver. Likely causes: network/proxy layer (nginx reverse proxy, TLS handshake, keep-alive), DNS, connection pool exhaustion, deployment differences.
Diagnostics: (1) browser Network panel TTFB; (2) timing middleware (@web.middleware logging time.time() around handler); (3) compare with/without nginx in path; (4) timecost AUTH_MS rule above. See references/performance-benchmark-may2026.md, references/performance-diagnostics.md.
Multi-Service Deployment
Services are capability centers, not agents. Each service: exposes atomic capabilities via REST, accepts uploads/paths as input, returns results (paths/URLs/status), knows nothing about upstream/downstream steps. The orchestrating agent: calls services in sequence, handles state transitions/error recovery/retry, and is the file router — downloads from one service, uploads to the next. Services must NOT copy files directly between each other, even co-located (keeps pipelines distributable). See references/multi-service-media-platform.md (geo-block IP filtering, async longtasks vs sync lock split, port conventions, inter-service localhost calls); references/nginx-proxy-ssl-template.md (SSL non-standard port, X-Forwarded headers, Let's Encrypt DNS-01 where 80/443 unavailable).
Session & Cookie Timeout
Two-layer session system (aiohttp_session + aiohttp_auth ticket) — timeout/renewal semantics and sliding-window ticket renewal: see references/session-timeout-architecture.md.
Hot Reload
Built-in (ahserver/hotreload.py): watches file mtimes via stat(), multi-process safe with reuse_port=True (each worker runs its own FileWatcher; no Redis pub/sub, signals, or cross-process coordination).
- Auto (no config):
.dspy/.mdread from disk every request (no cache);.tmpl/.uivia Jinja2auto_reload(mtime check). - Needs hot_reload config:
conf/config.json(JsonConfig singleton; cleared on change, nextgetConfig()reloads) andi18n/*/msg.txt(MiniI18N singleton +ServerEnv.myi18n; both cleared).
"hot_reload": true // enable, default 2s interval
"hot_reload": {"enabled": true, "interval": 5} // custom interval
// omit or false → disabled (default)
GET /__hot_reload__(only when enabled) triggersinvalidate_all_caches(): rbac (UserPermissions.ur_caches + rp_caches → LRU.clear() + invalidate_rp_cache()), pricing (PricingProgram.pricing_data → clear()), uapi (UAPIData.apidata + org_users → clear()), llmage (_uapi_cache + _uapiio_cache → invalidate). Each module cleared independently (try/except — one failure doesn't block others).- Cache TTLs: rbac.ur_caches 5min, rbac.rp_caches 10min (DB events), pricing.pricing_data none (DB events), llmage._uapi_cache 5min (invalidate_uapi_cache()), uapi.UAPIData: NO TTL, NO auto-invalidation — use
/__hot_reload__. ⚠️ Multi-process caveat: withreuse_port=True/__hot_reload__only clears the single worker handling the request — hit multiple times or rely on the file-mtime watcher (works in all workers independently).
Version History
- 1.3.0: hot-reload module, file watching,
/__hot_reload__endpoint,invalidate_all_caches(). - 1.2.0 (BREAKING):
RegisterCoroutineREMOVED →add_startup(callback);.dspyauto-extension REMOVED (/api/statusno longer →/api/status.dspy; needs directory/index.dspy, startswiths, or the fallback patch); added uvloop + httptools;appPublic.workernow exportsschedule_once/schedule_interval/get_event_loop(was onlyAsyncWorkerin ≤5.2.x). - 1.0.8: added
server_error(errcode)global;request._run_nsfor global env access.
References
references/api-routing-patterns.md— directory/index.dspy, nginx proxy_pass trailing slash semantics, startswiths vs processor lookup, 1.0.x → 1.2.0 migration cheat sheet.references/api-testing-patterns.md— test ahserver APIs with Python urllib instead of curl (avoids shell escaping issues with Bearer tokens / JSON payloads).references/file-upload-patterns.md— upload examples, curl testing, debugging.references/session-timeout-architecture.md— two-layer session, timeout/renewal.references/performance-benchmark-may2026.md,references/performance-diagnostics.md— benchmark results, middleware chain breakdown.references/multi-service-media-platform.md,references/nginx-proxy-ssl-template.md,references/ssh-access-map.md— multi-instance nginx deployment, SSL template, SSH access map.references/vllm-integration-pattern.md— AsyncLLMEngine + stream_response for high-throughput inference; 8 GPUs × 2 instances; critical params--max-model-len/--max-num-seqs/--gpu-memory-utilization; pitfalls: heredoc variable expansion, ClientConnectionResetError, dead DB connections after OOM; transformers → vLLM migration.references/gpu-service-longtasks-pattern.md(wan22 example — DEPRECATED, replaced by wan27);references/wan22-deployment.md(DEPRECATED).