--- name: ahserver description: "ahserver - Asynchronous HTTP(S) web application framework based on aiohttp" version: "1.3.0" author: "yu moqing" tags: ["aiohttp", "web-framework", "async", "python", "http-server"] dependencies: - uvloop - httptools - redis - asyncio - aiofiles - aiodns - aiohttp==3.10.10 - aiohttp_session - aiohttp_auth_autz - aiohttp-middlewares - aiohttp-cors - aiomysql - aioredis - psycopg2-binary - aiopg - jinja2 - ujson - openpyxl - pillow - py-natpmp --- # 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 edit `aioredis/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"`, optional `ssl: {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)`. ```python 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 ```python 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: ```python 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. ```python 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. ```python 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: ```python 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; only `exception`/`critical` flush 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 of `ProcessorResource._handle()` (before parse_request/closures/i18n/isHtml): if `request.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)` then `return await super()._handle(request)`. ⚠️ `parse_request` MUST run first — `super()` needs `self._preurl` set by it. - **Session on static files:** with `session_redis`, every request incl. static triggers a Redis GET. Consider `EncryptedCookieStorage` instead of `RedisStorage` for 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)`: ```python 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: 1. `processorResource.py` `_handle()`: when `url2file(path)` returns None, try `url2file(path + ext)` for each registered processor extension; then after `url2processor`, if `request_filename` is set and processor has no `real_path`, set `processor.real_path = self.request_filename`. 2. `baseProcessor.py` `set_run_env()`: compute `real_path` only if not already set (plain `url2file(request.path)` returns None for extensionless URLs). Why two patches: `BaseProcessor.__init__` doesn't set `real_path`; patch 1 sets it before `handle()`, patch 2 stops `set_run_env` overwriting it. Verify: `curl -u admin:pass http://localhost:9090/i18n_getmsgs` → HTTP 200 `{"success": true, "msgs": {...}}`; if 500 with log `TypeError: 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.5` venv 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()` fails `RuntimeError: Could not load libtorchcodec` (needs `libnvrtc.so.13`, CUDA runtime, not on shared lib path). Separation completes 100% but save crashes. Fix: wrapper script monkey-patching `torchaudio.save` to `soundfile` (`sf.write(uri, wav.T if wav.shape[0]<=wav.shape[1] else wav, sample_rate)`); requires `soundfile` (present in `/share/vllm-0.8.5`); invoke: `/share/vllm-0.8.5/bin/python /tmp/demucs_wrapper.py --two-stems=vocals -o `. ### ⚠️ 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 ` → `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: ```json "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`: ```python 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`: ```python 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`/`.md` read from disk every request (no cache); `.tmpl`/`.ui` via Jinja2 `auto_reload` (mtime check). - **Needs hot_reload config:** `conf/config.json` (JsonConfig singleton; cleared on change, next `getConfig()` reloads) and `i18n/*/msg.txt` (MiniI18N singleton + `ServerEnv.myi18n`; both cleared). ```json "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) triggers `invalidate_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: with `reuse_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):** `RegisterCoroutine` REMOVED → `add_startup(callback)`; `.dspy` auto-extension REMOVED (`/api/status` no longer → `/api/status.dspy`; needs directory/index.dspy, startswiths, or the fallback patch); added uvloop + httptools; `appPublic.worker` now exports `schedule_once`/`schedule_interval`/`get_event_loop` (was only `AsyncWorker` in ≤5.2.x). - **1.0.8:** added `server_error(errcode)` global; `request._run_ns` for 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).