IDKHowToCodeFr commited on
Commit
471d73e
·
1 Parent(s): 9ef7f14

docs: humanize codebase, add architecture guide and clean docstrings

Browse files
Files changed (4) hide show
  1. ARCHITECTURE.md +188 -0
  2. README.md +38 -33
  3. main.py +13 -0
  4. prefetch.py +6 -0
ARCHITECTURE.md ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Architecture Guide
2
+
3
+ This document describes the design, execution lifecycle, and operational contracts of `hf-backend`.
4
+
5
+ ## System Overview
6
+
7
+ `hf-backend` is an aggregation gateway deployed on a Hugging Face Docker Space. Rather than hosting business logic directly, it mounts domain-specific routers from separate repositories under dedicated URL prefixes.
8
+
9
+ ```
10
+ Internet Request
11
+ │
12
+ ▼
13
+ [ Hugging Face Edge Proxy ] (:443)
14
+ │
15
+ ▼
16
+ [ Uvicorn Worker ] (:7860)
17
+ │
18
+ ▼
19
+ [ RequestLimitMiddleware ] (Body size cap + request timeout)
20
+ │
21
+ ├── /health ─────────► [ Host Health Monitor ]
22
+ ├── /semantic/* ─────► [ Mounted Subproject: Semantic-Comment-Analyze ]
23
+ └── /<prefix>/* ─────► [ Future Mounted Services ]
24
+ ```
25
+
26
+ The gateway handles shared infrastructure requirements:
27
+ - Dynamic discovery and mounting of standalone service packages.
28
+ - Early input validation (payload length enforcement before buffering).
29
+ - Global request timeouts preventing worker stalls.
30
+ - Zero-cold-start initialization through baked image weights and startup warmups.
31
+ - Centralized health checks reflecting mount status and model readiness.
32
+
33
+ ---
34
+
35
+ ## The Cold-Start Problem and Solution
36
+
37
+ Machine learning services on serverless or restart-prone container hosts typically suffer from two cold-start stages:
38
+
39
+ 1. **Storage delay**: Weights must be pulled from Hugging Face Hub over the network at first boot, taking anywhere from 30 seconds to several minutes.
40
+ 2. **Compute delay**: Models must be deserialized, moved to memory, and compiled or JIT-traced on the first inference call.
41
+
42
+ `hf-backend` eliminates both delays across container restarts.
43
+
44
+ ```
45
+ ┌─────────────────────────────────────────────────────────────┐
46
+ │ 1. BUILD TIME (Docker Build via BuildKit) │
47
+ │ │
48
+ │ pip install requirements.txt │
49
+ │ │ │
50
+ │ COPY registry.py prefetch.py │
51
+ │ │ │
52
+ │ RUN python prefetch.py │
53
+ │ └─► Imports mounted packages │
54
+ │ └─► Calls warmup() │
55
+ │ └─► Models download into /home/user/hf-cache │
56
+ │ │ │
57
+ │ ENV HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 │
58
+ │ │ │
59
+ │ COPY . . (App code changes never bust prefetch cache) │
60
+ └─────────────────────────────────────────────────────────────┘
61
+ │
62
+ ▼
63
+ ┌─────────────────────────────────────────────────────────────┐
64
+ │ 2. CONTAINER STARTUP (Lifespan Hook) │
65
+ │ │
66
+ │ Uvicorn starts process [1] │
67
+ │ │ │
68
+ │ load_routers() mounts endpoints from registry.py │
69
+ │ │ │
70
+ │ Lifespan executes: │
71
+ │ ├─► Iterates over loaded projects │
72
+ │ ├─► Dispatches warmup() in a background thread │
73
+ │ └─► Applies WARMUP_TIMEOUT_SECONDS per project │
74
+ │ │ │
75
+ │ All warmups finish / weights load into memory │
76
+ │ │ │
77
+ │ Server opens socket :7860 and accepts incoming traffic │
78
+ └─────────────────────────────────────────────────────────────┘
79
+ │
80
+ ▼
81
+ ┌─────────────────────────────────────────────────────────────┐
82
+ │ 3. SERVING RUNTIME │
83
+ │ │
84
+ │ First incoming inference request (e.g. /semantic/analyze) │
85
+ │ └─�� Models already resident in memory │
86
+ │ └─► Response latency < 100 ms │
87
+ └─────────────────────────────────────────────────────────────┘
88
+ ```
89
+
90
+ ### Layer Caching Details
91
+
92
+ The build uses a two-phase copy strategy:
93
+ 1. `requirements.txt` is installed first.
94
+ 2. `registry.py` and `prefetch.py` are copied, then `prefetch.py` executes. Model weights are stored in `/home/user/hf-cache`.
95
+ 3. `HF_HUB_OFFLINE=1` and `TRANSFORMERS_OFFLINE=1` are exported to lock down the filesystem at runtime.
96
+ 4. Application code (`main.py`, tests, documentation) is copied last.
97
+
98
+ When you modify routing logic or update documentation, Docker reuses the cached model layers. The Space rebuilds and restarts in seconds rather than downloading multi-gigabyte checkpoints repeatedly.
99
+
100
+ ---
101
+
102
+ ## Service Contracts
103
+
104
+ Every service mounted into `hf-backend` follows a standard contract.
105
+
106
+ ### 1. Package Structure
107
+ A subproject must be installable as a Python package (e.g., via git tag in `requirements.txt`) and expose a FastAPI `APIRouter` instance:
108
+
109
+ ```python
110
+ # target: "semantic_backend.router:router"
111
+ from fastapi import APIRouter
112
+
113
+ router = APIRouter()
114
+
115
+ @router.get("/health")
116
+ def project_health():
117
+ ...
118
+ ```
119
+
120
+ ### 2. Startup Warmup
121
+ If the top-level package defines a callable named `warmup()`, the host lifespan executes it at boot time:
122
+
123
+ ```python
124
+ # semantic_backend/__init__.py
125
+ def warmup():
126
+ from .nlp_engine import warmup as _warmup
127
+ return _warmup()
128
+ ```
129
+
130
+ Warmup conventions:
131
+ - Must load lazy checkpoints (embeddings, classification heads, tokenizers).
132
+ - Runs one dummy inference pass through the primary execution path.
133
+ - Runs synchronously; the host wraps execution in `asyncio.to_thread`.
134
+ - If a project does not define `warmup()`, the host records `"no warmup"` and continues without error.
135
+
136
+ ---
137
+
138
+ ## Guard Middleware
139
+
140
+ The host places `RequestLimitMiddleware` in front of all endpoints:
141
+
142
+ 1. **Streaming Body Guard**:
143
+ Inspects the `Content-Length` header upfront. If the declared size exceeds `MAX_BODY_SIZE_BYTES` (default 10 MB), the connection returns HTTP 413 immediately. For chunked or undeclared streams, chunks are counted on arrival, raising HTTP 413 before memory exhaustion occurs.
144
+
145
+ 2. **Request Timeout**:
146
+ Wraps downstream route evaluation with `asyncio.wait_for`. If route processing exceeds `REQUEST_TIMEOUT_SECONDS` (default 60.0s), the gateway returns an HTTP 504 Gateway Timeout.
147
+
148
+ ---
149
+
150
+ ## Health and Degradation Model
151
+
152
+ The root `/health` and `/` endpoints report the aggregate status of all registered projects:
153
+
154
+ ```json
155
+ {
156
+ "status": "healthy",
157
+ "loaded": {
158
+ "/semantic": "semantic_backend.router:router"
159
+ },
160
+ "failed": {},
161
+ "warm_status": {
162
+ "/semantic": "warmed"
163
+ },
164
+ "total_warmup_seconds": 6.484
165
+ }
166
+ ```
167
+
168
+ ### Evaluation Logic
169
+
170
+ ```
171
+ ┌──────────────────────────────────────────────┐
172
+ │ Any projects failed to mount? │
173
+ │ OR │───► "degraded"
174
+ │ Any project warmup failed / timed out? │
175
+ └──────────────────────────────────────────────┘
176
+ │ No
177
+ ▼
178
+ ┌──────────────────────────────────────────────┐
179
+ │ Are there any loaded projects? │───► No: "unhealthy"
180
+ └──────────────────────────────────────────────┘
181
+ │ Yes
182
+ ▼
183
+ "healthy"
184
+ ```
185
+
186
+ - A project without a `warmup()` reports `"no warmup"` and remains `"healthy"`.
187
+ - If a warmup throws an exception or exceeds `WARMUP_TIMEOUT_SECONDS`, the host logs the error, marks the project as `"failed: <reason>"`, and marks overall status as `"degraded"`.
188
+ - Surviving projects continue serving traffic even if an adjacent service is degraded.
README.md CHANGED
@@ -9,64 +9,65 @@ app_port: 7860
9
 
10
  # HF Backend
11
 
12
- FastAPI host running on a Hugging Face Docker Space. It mounts backend routers from separate project repositories under specific URL prefixes.
13
 
14
- ## How it works
15
 
16
- The host application does not define business logic. Each subproject lives in its own repository, exposes a router in `<package>/router.py`, and is installed as a Python package.
17
 
18
- The host imports each router dynamically from `registry.py` and mounts it under a prefix. If an individual router fails to import, the host logs the error, records the failure, and keeps running for the other services.
 
 
19
 
20
  ```
21
- /project-1/* -> HEARTFLOW_OS backend router
22
- /project-2/* -> Semantic Comment Analyzer backend router
23
- /health -> Host health status
24
  ```
25
 
26
- ## Adding a project
27
 
28
  Adding a new service takes two steps:
29
 
30
- 1. Add the prefix and router path to `registry.py`:
31
  ```python
32
  PROJECT_REGISTRY = {
33
- "/project-1": "heartflow_backend.router:router",
34
- "/project-2": "semantic_backend.router:router",
35
- "/project-3": "new_project.router:router",
36
  }
37
  ```
38
 
39
- 2. Add the pinned Git tag to `requirements.txt`:
40
  ```text
41
- git+https://github.com/<user>/<repo>.git@v1.0#subdirectory=backend
42
  ```
43
 
44
- Pushing to `main` syncs the change to Hugging Face and rebuilds the Space container.
45
 
46
  ## Configuration
47
 
48
- Set these in the Space settings under **Variables and secrets**:
49
 
50
- | Variable | Required | Default | Notes |
51
  | :--- | :--- | :--- | :--- |
52
  | `CORS_ALLOWED_ORIGINS` | No | None | Comma-separated allowed origins. Wildcards (`*`) are ignored. |
53
- | `DISABLE_PROJECTS` | No | None | Comma-separated prefixes to skip loading (e.g. `project-2`). |
54
- | `MAX_BODY_SIZE_BYTES` | No | 10485760 (10 MB) | Max request body size before returning 413. |
55
- | `REQUEST_TIMEOUT_SECONDS` | No | 60.0 | Request timeout before returning 504. |
56
- | `WARMUP_TIMEOUT_SECONDS` | No | 300.0 | Warmup timeout per project in worker thread before timing out. |
57
  | `GITHUB_TOKEN` | Only for private repos | None | GitHub token passed as a build secret to clone private dependencies. |
58
 
59
- GitHub repository secrets (used by GitHub Actions):
60
 
61
- | Secret | Notes |
62
  | :--- | :--- |
63
- | `HF_BACKEND_SPACE` | Hugging Face write token used to push code and trigger Space restart. |
64
- | `HF_SPACE_URL` | Optional. Health URL for the keep-alive workflow. |
65
 
66
  ## Endpoints
67
 
68
  ### `GET /health`
69
- Returns the status of the host and each registered project.
70
 
71
  ```json
72
  {
@@ -78,17 +79,21 @@ Returns the status of the host and each registered project.
78
  "warm_status": {
79
  "/semantic": "warmed"
80
  },
81
- "total_warmup_seconds": 6.35
82
  }
83
  ```
84
 
85
- If any service fails to load or warm up, `status` changes to `"degraded"` or `"unhealthy"` and the error is listed under `"failed"` or `"warm_status"`.
 
 
 
86
 
87
- Interactive documentation (`/docs`, `/redoc`, `/openapi.json`) is disabled.
88
 
89
- ## Release workflow
90
 
91
- 1. Test changes in the project repository using its `dev.py`.
92
  2. Tag the release (`git tag v1.0`, `git push origin v1.0`).
93
- 3. Update the tag in this repo's `requirements.txt`.
94
- 4. Push to `main`. The GitHub Actions workflow pushes to Hugging Face and restarts the Space.
 
 
9
 
10
  # HF Backend
11
 
12
+ FastAPI gateway running on a Hugging Face Docker Space. The gateway mounts backend routers from standalone project repositories under designated URL prefixes.
13
 
14
+ For in-depth lifecycle mechanics, prefetch caching, and degradation semantics, see [ARCHITECTURE.md](file:///c:/Users/manga/Projects/github/rachit-mangawa/hf-backend/ARCHITECTURE.md).
15
 
16
+ ## How It Works
17
 
18
+ The host application does not contain domain business logic. Each subproject is maintained in its own repository, exposes a router in `<package>/router.py`, and installs as a standard Python package.
19
+
20
+ The host imports each router from `registry.py` and mounts it under its configured prefix. If a router fails to import, the host logs the error, records the failure, and keeps running for the remaining services.
21
 
22
  ```
23
+ /project-1/* -> HEARTFLOW_OS router
24
+ /semantic/* -> Semantic Comment Analyzer router
25
+ /health -> Host health and readiness status
26
  ```
27
 
28
+ ## Adding a Service
29
 
30
  Adding a new service takes two steps:
31
 
32
+ 1. Register the URL prefix and target router in `registry.py`:
33
  ```python
34
  PROJECT_REGISTRY = {
35
+ "/semantic": "semantic_backend.router:router",
36
+ "/new-service": "new_service.router:router",
 
37
  }
38
  ```
39
 
40
+ 2. Pin the Git tag or release in `requirements.in`, then compile `requirements.txt`:
41
  ```text
42
+ git+https://github.com/<owner>/<repo>.git@v1.0#subdirectory=backend
43
  ```
44
 
45
+ Pushing to `main` deploys the changes to Hugging Face and rebuilds the Space container.
46
 
47
  ## Configuration
48
 
49
+ Set these variables in the Space settings under **Variables and secrets**:
50
 
51
+ | Variable | Required | Default | Description |
52
  | :--- | :--- | :--- | :--- |
53
  | `CORS_ALLOWED_ORIGINS` | No | None | Comma-separated allowed origins. Wildcards (`*`) are ignored. |
54
+ | `DISABLE_PROJECTS` | No | None | Comma-separated prefixes to skip loading (e.g. `semantic`). |
55
+ | `MAX_BODY_SIZE_BYTES` | No | 10485760 (10 MB) | Maximum request body size before returning HTTP 413. |
56
+ | `REQUEST_TIMEOUT_SECONDS` | No | 60.0 | Request timeout before returning HTTP 504. |
57
+ | `WARMUP_TIMEOUT_SECONDS` | No | 300.0 | Warmup timeout per project during container boot. |
58
  | `GITHUB_TOKEN` | Only for private repos | None | GitHub token passed as a build secret to clone private dependencies. |
59
 
60
+ Repository secrets for GitHub Actions:
61
 
62
+ | Secret | Description |
63
  | :--- | :--- |
64
+ | `HF_BACKEND_SPACE` | Hugging Face token with write permissions to push code and restart the Space. |
65
+ | `HF_SPACE_URL` | Optional health URL for the keep-alive workflow. |
66
 
67
  ## Endpoints
68
 
69
  ### `GET /health`
70
+ Returns the status of the host and each registered project:
71
 
72
  ```json
73
  {
 
79
  "warm_status": {
80
  "/semantic": "warmed"
81
  },
82
+ "total_warmup_seconds": 6.48
83
  }
84
  ```
85
 
86
+ Status outcomes:
87
+ - `healthy`: All registered projects mounted successfully and no warmup failed.
88
+ - `degraded`: One or more services failed to mount, or a warmup failed or timed out.
89
+ - `unhealthy`: No services loaded successfully.
90
 
91
+ Interactive documentation (`/docs`, `/redoc`, `/openapi.json`) is disabled for production hardening.
92
 
93
+ ## Release Workflow
94
 
95
+ 1. Test changes in the subproject repository.
96
  2. Tag the release (`git tag v1.0`, `git push origin v1.0`).
97
+ 3. Update the tag reference in `requirements.in` and recompile `requirements.txt`.
98
+ 4. Run `pytest` locally to verify routing and warmups.
99
+ 5. Push to `main`. The GitHub Actions workflow syncs the commit to Hugging Face and triggers a restart.
main.py CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  import asyncio
2
  import importlib
3
  import logging
@@ -5,9 +12,11 @@ import os
5
  import time
6
  from contextlib import asynccontextmanager
7
  from typing import Callable
 
8
  from fastapi import FastAPI, HTTPException, Request, Response
9
  from fastapi.middleware.cors import CORSMiddleware
10
  from starlette.middleware.base import BaseHTTPMiddleware
 
11
  from registry import PROJECT_REGISTRY
12
 
13
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
@@ -24,6 +33,8 @@ default_warmup_timeout = float(os.getenv("WARMUP_TIMEOUT_SECONDS", "300.0"))
24
 
25
 
26
  class RequestLimitMiddleware(BaseHTTPMiddleware):
 
 
27
  def __init__(self, app, max_size: int, timeout: float):
28
  super().__init__(app)
29
  self.max_size = max_size
@@ -55,6 +66,7 @@ class RequestLimitMiddleware(BaseHTTPMiddleware):
55
 
56
 
57
  async def run_warmup_for_project(prefix: str, target: str, timeout: float):
 
58
  logger.info(f"[{prefix}] Checking warmup for target '{target}'...")
59
  try:
60
  module_name = target.split(":")[0]
@@ -120,6 +132,7 @@ if allowed_origins:
120
 
121
 
122
  def load_routers():
 
123
  loaded_projects.clear()
124
  failed_projects.clear()
125
  warm_status.clear()
 
1
+ """HF Backend Gateway.
2
+
3
+ Aggregates subproject routers mounted on a Hugging Face Space. Handles
4
+ model warmup during lifespan startup, streaming payload size limits,
5
+ and centralized health tracking.
6
+ """
7
+
8
  import asyncio
9
  import importlib
10
  import logging
 
12
  import time
13
  from contextlib import asynccontextmanager
14
  from typing import Callable
15
+
16
  from fastapi import FastAPI, HTTPException, Request, Response
17
  from fastapi.middleware.cors import CORSMiddleware
18
  from starlette.middleware.base import BaseHTTPMiddleware
19
+
20
  from registry import PROJECT_REGISTRY
21
 
22
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
 
33
 
34
 
35
  class RequestLimitMiddleware(BaseHTTPMiddleware):
36
+ """Enforces incoming request payload limits and execution timeouts."""
37
+
38
  def __init__(self, app, max_size: int, timeout: float):
39
  super().__init__(app)
40
  self.max_size = max_size
 
66
 
67
 
68
  async def run_warmup_for_project(prefix: str, target: str, timeout: float):
69
+ """Executes a mounted package's warmup function in a worker thread."""
70
  logger.info(f"[{prefix}] Checking warmup for target '{target}'...")
71
  try:
72
  module_name = target.split(":")[0]
 
132
 
133
 
134
  def load_routers():
135
+ """Mounts routers dynamically from registry.py with isolated failure handling."""
136
  loaded_projects.clear()
137
  failed_projects.clear()
138
  warm_status.clear()
prefetch.py CHANGED
@@ -1,3 +1,9 @@
 
 
 
 
 
 
1
  import importlib
2
  import logging
3
  import sys
 
1
+ """Build-time model prefetch script.
2
+
3
+ Reads project routes from registry.py and invokes warmup() on each
4
+ top-level package to bake weights into the Docker image filesystem.
5
+ """
6
+
7
  import importlib
8
  import logging
9
  import sys