DrValera commited on
Commit
b632be5
·
verified ·
1 Parent(s): da7025e

Added ping every 270 seconds to not fall asleep if the core space is still counting

Browse files
Files changed (1) hide show
  1. main.py +74 -10
main.py CHANGED
@@ -1,4 +1,4 @@
1
- import os, httpx
2
  from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form
3
  from fastapi.responses import JSONResponse, Response, StreamingResponse
4
  from fastapi.middleware.cors import CORSMiddleware
@@ -37,6 +37,11 @@ async def ratelimit_handler(request: Request, exc: RateLimitExceeded):
37
  SDK_MAX_BODY_BYTES = int(os.getenv("SDK_MAX_BODY_BYTES", "25000000")) # 25MB default for demo
38
  SDK_MAX_BODY_BYTES_extended = int(os.getenv("SDK_MAX_BODY_BYTES_extended", "125000000")) # 125MB default for prod
39
 
 
 
 
 
 
40
  # Early global body-size guard (runs before routes)
41
  @app.middleware("http")
42
  async def limit_body_size(request: Request, call_next):
@@ -74,6 +79,35 @@ def _filter_resp_headers(headers: dict) -> dict:
74
  allowed = {"content-disposition"}
75
  return {k: v for k, v in headers.items() if k.lower() in allowed}
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  def _extract_user_token(req: Request) -> str | None:
78
  """
79
  Read user's DATFID token from Authorization: Bearer <dt+...>.
@@ -89,6 +123,7 @@ async def _forward(path: str, method: str = "GET", json_body=None, user_token: s
89
  Forward request to the PRIVATE Space:
90
  - 'Authorization: Bearer <HF_TOKEN>' to pass HF private gate
91
  - 'X-API-Key: <dt+...>' so your private app can validate the user token
 
92
  """
93
  url = f"{UPSTREAM_URL}{path}"
94
  headers = {
@@ -98,9 +133,13 @@ async def _forward(path: str, method: str = "GET", json_body=None, user_token: s
98
  if user_token:
99
  headers["X-API-Key"] = user_token
100
 
101
- timeout = httpx.Timeout(600.0)
102
- async with httpx.AsyncClient(timeout=timeout) as client:
103
- r = await client.request(method, url, headers=headers, json=json_body)
 
 
 
 
104
 
105
  ct = r.headers.get("content-type", "")
106
 
@@ -121,8 +160,8 @@ async def _forward_stream(path: str, files=None, data=None, user_token: str | No
121
  if user_token:
122
  headers["X-API-Key"] = user_token
123
 
124
- timeout = httpx.Timeout(600.0)
125
-
126
  client = httpx.AsyncClient(timeout=timeout, follow_redirects=True)
127
 
128
  # Don't open the context yet; the iterator must own the context lifetime.
@@ -150,6 +189,7 @@ async def _forward_stream(path: str, files=None, data=None, user_token: str | No
150
  async for chunk in resp.aiter_raw():
151
  yield chunk
152
  finally:
 
153
  await client.aclose()
154
 
155
  response = StreamingResponse(
@@ -162,7 +202,7 @@ async def _forward_stream(path: str, files=None, data=None, user_token: str | No
162
 
163
 
164
  async def _forward_multipart_json(path: str, files=None, data=None, user_token: str | None = None, method: str = "POST"):
165
- """POST multipart (files + data) to upstream and return JSON response."""
166
  url = f"{UPSTREAM_URL}{path}"
167
  headers = {
168
  "Authorization": f"Bearer {HF_TOKEN}",
@@ -170,9 +210,13 @@ async def _forward_multipart_json(path: str, files=None, data=None, user_token:
170
  }
171
  if user_token:
172
  headers["X-API-Key"] = user_token
173
- timeout = httpx.Timeout(600.0)
174
- async with httpx.AsyncClient(timeout=timeout) as client:
175
- r = await client.request(method, url, headers=headers, files=files, data=data)
 
 
 
 
176
  ct = r.headers.get("content-type", "")
177
  if "application/json" in ct:
178
  try:
@@ -195,6 +239,8 @@ async def _forward_demo_stream(path: str, *, files: dict | None, data: dict | No
195
  "X-DATFID-Token": DATFID_DEMO_TOKEN,
196
  "Accept": "*/*",
197
  }
 
 
198
 
199
  timeout = httpx.Timeout(120.0)
200
  client = httpx.AsyncClient(timeout=timeout, follow_redirects=True)
@@ -219,6 +265,7 @@ async def _forward_demo_stream(path: str, *, files: dict | None, data: dict | No
219
  async for chunk in resp.aiter_raw():
220
  yield chunk
221
  finally:
 
222
  await client.aclose()
223
 
224
  response = StreamingResponse(
@@ -235,6 +282,23 @@ async def root(req: Request):
235
  user_token = _extract_user_token(req) # optional here
236
  return await _forward("/", "GET", user_token=user_token)
237
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  @app.get("/secure-ping/")
239
  async def secure_ping(req: Request):
240
  # Require user's DATFID token in Authorization: Bearer <dt+...>
 
1
+ import os, asyncio, httpx
2
  from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form
3
  from fastapi.responses import JSONResponse, Response, StreamingResponse
4
  from fastapi.middleware.cors import CORSMiddleware
 
37
  SDK_MAX_BODY_BYTES = int(os.getenv("SDK_MAX_BODY_BYTES", "25000000")) # 25MB default for demo
38
  SDK_MAX_BODY_BYTES_extended = int(os.getenv("SDK_MAX_BODY_BYTES_extended", "125000000")) # 125MB default for prod
39
 
40
+ # How long to wait for upstream (API) response; long runs may need 30+ min (1800+)
41
+ UPSTREAM_TIMEOUT = float(os.getenv("UPSTREAM_TIMEOUT", "1800"))
42
+ # While waiting for a response, ping upstream every N seconds so the backend Space does not sleep
43
+ PING_UPSTREAM_INTERVAL = float(os.getenv("PING_UPSTREAM_INTERVAL", "270"))
44
+
45
  # Early global body-size guard (runs before routes)
46
  @app.middleware("http")
47
  async def limit_body_size(request: Request, call_next):
 
79
  allowed = {"content-disposition"}
80
  return {k: v for k, v in headers.items() if k.lower() in allowed}
81
 
82
+ async def _ping_upstream_loop(ping_url: str, headers: dict, interval: float):
83
+ """Background task: every `interval` seconds, GET ping_url to keep the backend Space awake. Stops when cancelled."""
84
+ if interval <= 0:
85
+ return
86
+ while True:
87
+ await asyncio.sleep(interval)
88
+ try:
89
+ async with httpx.AsyncClient(timeout=10.0) as client:
90
+ await client.get(ping_url, headers=headers)
91
+ except asyncio.CancelledError:
92
+ break
93
+ except Exception:
94
+ pass # ignore ping errors
95
+
96
+ def _start_ping_task(ping_url: str, headers: dict):
97
+ """Start a background task that pings the given URL every PING_UPSTREAM_INTERVAL seconds. Returns the task (or None); cancel it when the main request finishes."""
98
+ if PING_UPSTREAM_INTERVAL <= 0:
99
+ return None
100
+ return asyncio.create_task(_ping_upstream_loop(ping_url, headers, PING_UPSTREAM_INTERVAL))
101
+
102
+ async def _cancel_ping_task(task: asyncio.Task | None):
103
+ if task is None:
104
+ return
105
+ task.cancel()
106
+ try:
107
+ await task
108
+ except asyncio.CancelledError:
109
+ pass
110
+
111
  def _extract_user_token(req: Request) -> str | None:
112
  """
113
  Read user's DATFID token from Authorization: Bearer <dt+...>.
 
123
  Forward request to the PRIVATE Space:
124
  - 'Authorization: Bearer <HF_TOKEN>' to pass HF private gate
125
  - 'X-API-Key: <dt+...>' so your private app can validate the user token
126
+ While waiting, pings upstream every PING_UPSTREAM_INTERVAL seconds so the backend Space does not sleep.
127
  """
128
  url = f"{UPSTREAM_URL}{path}"
129
  headers = {
 
133
  if user_token:
134
  headers["X-API-Key"] = user_token
135
 
136
+ ping_task = _start_ping_task(f"{UPSTREAM_URL}/", {"Authorization": f"Bearer {HF_TOKEN}"})
137
+ try:
138
+ timeout = httpx.Timeout(UPSTREAM_TIMEOUT)
139
+ async with httpx.AsyncClient(timeout=timeout) as client:
140
+ r = await client.request(method, url, headers=headers, json=json_body)
141
+ finally:
142
+ await _cancel_ping_task(ping_task)
143
 
144
  ct = r.headers.get("content-type", "")
145
 
 
160
  if user_token:
161
  headers["X-API-Key"] = user_token
162
 
163
+ ping_task = _start_ping_task(f"{UPSTREAM_URL}/", {"Authorization": f"Bearer {HF_TOKEN}"})
164
+ timeout = httpx.Timeout(UPSTREAM_TIMEOUT)
165
  client = httpx.AsyncClient(timeout=timeout, follow_redirects=True)
166
 
167
  # Don't open the context yet; the iterator must own the context lifetime.
 
189
  async for chunk in resp.aiter_raw():
190
  yield chunk
191
  finally:
192
+ await _cancel_ping_task(ping_task)
193
  await client.aclose()
194
 
195
  response = StreamingResponse(
 
202
 
203
 
204
  async def _forward_multipart_json(path: str, files=None, data=None, user_token: str | None = None, method: str = "POST"):
205
+ """POST multipart (files + data) to upstream and return JSON response. Pings upstream every PING_UPSTREAM_INTERVAL while waiting."""
206
  url = f"{UPSTREAM_URL}{path}"
207
  headers = {
208
  "Authorization": f"Bearer {HF_TOKEN}",
 
210
  }
211
  if user_token:
212
  headers["X-API-Key"] = user_token
213
+ ping_task = _start_ping_task(f"{UPSTREAM_URL}/", {"Authorization": f"Bearer {HF_TOKEN}"})
214
+ try:
215
+ timeout = httpx.Timeout(UPSTREAM_TIMEOUT)
216
+ async with httpx.AsyncClient(timeout=timeout) as client:
217
+ r = await client.request(method, url, headers=headers, files=files, data=data)
218
+ finally:
219
+ await _cancel_ping_task(ping_task)
220
  ct = r.headers.get("content-type", "")
221
  if "application/json" in ct:
222
  try:
 
239
  "X-DATFID-Token": DATFID_DEMO_TOKEN,
240
  "Accept": "*/*",
241
  }
242
+ ping_headers = {"Authorization": f"Bearer {HF_TOKEN}", "X-DATFID-Token": DATFID_DEMO_TOKEN}
243
+ ping_task = _start_ping_task(f"{DEMO_FORWARD_URL.rstrip('/')}/", ping_headers)
244
 
245
  timeout = httpx.Timeout(120.0)
246
  client = httpx.AsyncClient(timeout=timeout, follow_redirects=True)
 
265
  async for chunk in resp.aiter_raw():
266
  yield chunk
267
  finally:
268
+ await _cancel_ping_task(ping_task)
269
  await client.aclose()
270
 
271
  response = StreamingResponse(
 
282
  user_token = _extract_user_token(req) # optional here
283
  return await _forward("/", "GET", user_token=user_token)
284
 
285
+
286
+ @app.get("/keep-alive")
287
+ async def keep_alive():
288
+ """
289
+ Hit this from an external cron (e.g. every 4 min) to keep this Space and the backend awake.
290
+ Sleep timeout is configured on Hugging Face (Space settings), not in code; pinging more
291
+ often than that (e.g. every 4 min if HF sleep is 5 min) prevents both spaces from sleeping.
292
+ Pings the upstream API so both see activity. No auth; use UptimeRobot, cron-job.org, etc.
293
+ """
294
+ try:
295
+ url = f"{UPSTREAM_URL}/"
296
+ async with httpx.AsyncClient(timeout=10.0) as client:
297
+ r = await client.get(url, headers={"Authorization": f"Bearer {HF_TOKEN}"})
298
+ return JSONResponse(content={"ok": True, "upstream_status": r.status_code})
299
+ except Exception as e:
300
+ return JSONResponse(status_code=502, content={"ok": False, "error": str(e)[:200]})
301
+
302
  @app.get("/secure-ping/")
303
  async def secure_ping(req: Request):
304
  # Require user's DATFID token in Authorization: Bearer <dt+...>