Spaces:
Running
Running
Deploy 7de0c2ab99518aefca3cce1496d6b74cae82705f
Browse files- app/main.py +10 -0
- app/services/cron_scheduler.py +52 -9
- start.sh +5 -0
app/main.py
CHANGED
|
@@ -86,7 +86,17 @@ async def lifespan(app: FastAPI):
|
|
| 86 |
asyncio.create_task(_self_ping())
|
| 87 |
else:
|
| 88 |
logger.info("self-ping disabled (SELF_PING_ENABLED=false) — not scheduling")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
cron_scheduler = start_cron_scheduler()
|
|
|
|
|
|
|
| 90 |
yield
|
| 91 |
if cron_scheduler is not None:
|
| 92 |
cron_scheduler.shutdown(wait=False)
|
|
|
|
| 86 |
asyncio.create_task(_self_ping())
|
| 87 |
else:
|
| 88 |
logger.info("self-ping disabled (SELF_PING_ENABLED=false) — not scheduling")
|
| 89 |
+
if settings.cron_enabled:
|
| 90 |
+
logger.info(
|
| 91 |
+
"scheduling frontend credit crons -> %s/api/cron + %s/api/v1/cron (daily 00:00)",
|
| 92 |
+
(settings.frontend_base_url or "").rstrip("/") or "(FRONTEND_BASE_URL missing)",
|
| 93 |
+
(settings.frontend_base_url or "").rstrip("/") or "(FRONTEND_BASE_URL missing)",
|
| 94 |
+
)
|
| 95 |
+
else:
|
| 96 |
+
logger.info("frontend credit crons disabled (CRON_ENABLED=false) — not scheduling /api/cron nor /api/v1/cron")
|
| 97 |
cron_scheduler = start_cron_scheduler()
|
| 98 |
+
if cron_scheduler is None:
|
| 99 |
+
logger.warning("frontend credit cron scheduler NOT running — /api/cron and /api/v1/cron will not fire (see reason above)")
|
| 100 |
yield
|
| 101 |
if cron_scheduler is not None:
|
| 102 |
cron_scheduler.shutdown(wait=False)
|
app/services/cron_scheduler.py
CHANGED
|
@@ -46,14 +46,28 @@ def resolve_timezone() -> Any:
|
|
| 46 |
def _targets() -> list[tuple[str, str]]:
|
| 47 |
base = (settings.frontend_base_url or "").rstrip("/")
|
| 48 |
if not base:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
return []
|
| 50 |
out = []
|
| 51 |
for path, secret_field in ROUTES:
|
| 52 |
token = (getattr(settings, secret_field, "") or "").strip()
|
| 53 |
if not token:
|
| 54 |
-
logger.warning(
|
|
|
|
|
|
|
|
|
|
| 55 |
continue
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
return out
|
| 58 |
|
| 59 |
|
|
@@ -64,10 +78,13 @@ async def _fire_one(url: str, token: str) -> dict[str, Any]:
|
|
| 64 |
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 65 |
resp = await client.post(url, headers={"Authorization": f"Bearer {token}"})
|
| 66 |
ok = resp.status_code == 200
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
| 68 |
return {"url": url, "status": resp.status_code, "ok": ok}
|
| 69 |
except Exception as exc: # noqa: BLE001
|
| 70 |
-
logger.warning("cron fire failed", extra={"url": url, "error": str(exc)})
|
| 71 |
return {"url": url, "status": None, "ok": False, "error": str(exc)}
|
| 72 |
|
| 73 |
|
|
@@ -77,21 +94,44 @@ async def fire_frontend_crons() -> list[dict[str, Any]]:
|
|
| 77 |
|
| 78 |
targets = _targets()
|
| 79 |
if not targets:
|
| 80 |
-
logger.warning("cron tick: nothing configured, skipping")
|
| 81 |
return []
|
|
|
|
| 82 |
results = await asyncio.gather(*(_fire_one(url, token) for url, token in targets))
|
|
|
|
|
|
|
| 83 |
return list(results)
|
| 84 |
|
| 85 |
|
| 86 |
def start_cron_scheduler() -> AsyncIOScheduler | None:
|
| 87 |
"""Start the daily 00:00 scheduler. Returns None when disabled/unconfigured."""
|
| 88 |
if not settings.cron_enabled:
|
| 89 |
-
logger.info("
|
| 90 |
return None
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
return None
|
| 94 |
tz = resolve_timezone()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
scheduler = AsyncIOScheduler(timezone=tz)
|
| 96 |
scheduler.add_job(
|
| 97 |
fire_frontend_crons,
|
|
@@ -103,5 +143,8 @@ def start_cron_scheduler() -> AsyncIOScheduler | None:
|
|
| 103 |
)
|
| 104 |
scheduler.start()
|
| 105 |
nxt = scheduler.get_job("midnight-frontend-crons").next_run_time
|
| 106 |
-
logger.info(
|
|
|
|
|
|
|
|
|
|
| 107 |
return scheduler
|
|
|
|
| 46 |
def _targets() -> list[tuple[str, str]]:
|
| 47 |
base = (settings.frontend_base_url or "").rstrip("/")
|
| 48 |
if not base:
|
| 49 |
+
logger.warning(
|
| 50 |
+
"frontend credit cron disabled: FRONTEND_BASE_URL not set — "
|
| 51 |
+
"skipping both /api/cron and /api/v1/cron"
|
| 52 |
+
)
|
| 53 |
return []
|
| 54 |
out = []
|
| 55 |
for path, secret_field in ROUTES:
|
| 56 |
token = (getattr(settings, secret_field, "") or "").strip()
|
| 57 |
if not token:
|
| 58 |
+
logger.warning(
|
| 59 |
+
"frontend credit cron disabled for route (secret missing, skipping)",
|
| 60 |
+
extra={"route": path, "secret": secret_field},
|
| 61 |
+
)
|
| 62 |
continue
|
| 63 |
+
url = f"{base}{path}"
|
| 64 |
+
logger.info(
|
| 65 |
+
"frontend credit cron enabled: %s -> %s (daily 00:00 auth=%s)",
|
| 66 |
+
path,
|
| 67 |
+
url,
|
| 68 |
+
"Bearer ***",
|
| 69 |
+
)
|
| 70 |
+
out.append((url, token))
|
| 71 |
return out
|
| 72 |
|
| 73 |
|
|
|
|
| 78 |
async with httpx.AsyncClient(timeout=120.0) as client:
|
| 79 |
resp = await client.post(url, headers={"Authorization": f"Bearer {token}"})
|
| 80 |
ok = resp.status_code == 200
|
| 81 |
+
if ok:
|
| 82 |
+
logger.info("frontend credit cron fired ok", extra={"url": url, "status": resp.status_code})
|
| 83 |
+
else:
|
| 84 |
+
logger.warning("frontend credit cron fired non-200", extra={"url": url, "status": resp.status_code})
|
| 85 |
return {"url": url, "status": resp.status_code, "ok": ok}
|
| 86 |
except Exception as exc: # noqa: BLE001
|
| 87 |
+
logger.warning("frontend credit cron fire failed", extra={"url": url, "error": str(exc)})
|
| 88 |
return {"url": url, "status": None, "ok": False, "error": str(exc)}
|
| 89 |
|
| 90 |
|
|
|
|
| 94 |
|
| 95 |
targets = _targets()
|
| 96 |
if not targets:
|
| 97 |
+
logger.warning("frontend credit cron tick: nothing configured, skipping both /api/cron and /api/v1/cron")
|
| 98 |
return []
|
| 99 |
+
logger.info("frontend credit cron tick: firing %d route(s) (/api/cron, /api/v1/cron)", len(targets))
|
| 100 |
results = await asyncio.gather(*(_fire_one(url, token) for url, token in targets))
|
| 101 |
+
ok_count = sum(1 for r in results if r.get("ok"))
|
| 102 |
+
logger.info("frontend credit cron tick done: %d/%d ok", ok_count, len(results))
|
| 103 |
return list(results)
|
| 104 |
|
| 105 |
|
| 106 |
def start_cron_scheduler() -> AsyncIOScheduler | None:
|
| 107 |
"""Start the daily 00:00 scheduler. Returns None when disabled/unconfigured."""
|
| 108 |
if not settings.cron_enabled:
|
| 109 |
+
logger.info("frontend credit cron disabled (CRON_ENABLED=false) — not scheduling /api/cron nor /api/v1/cron")
|
| 110 |
return None
|
| 111 |
+
base = (settings.frontend_base_url or "").strip()
|
| 112 |
+
if not base:
|
| 113 |
+
logger.warning(
|
| 114 |
+
"frontend credit cron disabled: FRONTEND_BASE_URL not set — "
|
| 115 |
+
"not scheduling /api/cron nor /api/v1/cron"
|
| 116 |
+
)
|
| 117 |
return None
|
| 118 |
tz = resolve_timezone()
|
| 119 |
+
# Per-route boot log (mirrors self-ping style): on/off for each cron.
|
| 120 |
+
for path, secret_field in ROUTES:
|
| 121 |
+
token = (getattr(settings, secret_field, "") or "").strip()
|
| 122 |
+
url = f"{base.rstrip('/')}{path}"
|
| 123 |
+
if token:
|
| 124 |
+
logger.info(
|
| 125 |
+
"scheduling frontend credit cron -> %s (daily 00:00 auth=%s)",
|
| 126 |
+
url,
|
| 127 |
+
"Bearer ***",
|
| 128 |
+
)
|
| 129 |
+
else:
|
| 130 |
+
logger.warning(
|
| 131 |
+
"frontend credit cron NOT scheduled -> %s (%s not set)",
|
| 132 |
+
url,
|
| 133 |
+
secret_field.upper(),
|
| 134 |
+
)
|
| 135 |
scheduler = AsyncIOScheduler(timezone=tz)
|
| 136 |
scheduler.add_job(
|
| 137 |
fire_frontend_crons,
|
|
|
|
| 143 |
)
|
| 144 |
scheduler.start()
|
| 145 |
nxt = scheduler.get_job("midnight-frontend-crons").next_run_time
|
| 146 |
+
logger.info(
|
| 147 |
+
"frontend credit cron scheduler started: daily 00:00 -> /api/cron + /api/v1/cron",
|
| 148 |
+
extra={"timezone": str(tz), "next_run": str(nxt)},
|
| 149 |
+
)
|
| 150 |
return scheduler
|
start.sh
CHANGED
|
@@ -91,5 +91,10 @@ python -c "import fastapi, uvicorn, httpx, pydantic" 2>/dev/null \
|
|
| 91 |
HOST="${HOST:-0.0.0.0}"
|
| 92 |
PORT="${PORT:-7860}"
|
| 93 |
log "Self-ping: SELF_PING_ENABLED=${SELF_PING_ENABLED:-true} SELF_PING_URL=${SELF_PING_URL:-'(derived from SPACE_ID)'} interval=${SELF_PING_INTERVAL:-600}s"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
log "Starting uvicorn: host=$HOST port=$PORT workers=1 app=app.main:app"
|
| 95 |
exec python -m uvicorn app.main:app --host "$HOST" --port "$PORT" --workers 1
|
|
|
|
| 91 |
HOST="${HOST:-0.0.0.0}"
|
| 92 |
PORT="${PORT:-7860}"
|
| 93 |
log "Self-ping: SELF_PING_ENABLED=${SELF_PING_ENABLED:-true} SELF_PING_URL=${SELF_PING_URL:-'(derived from SPACE_ID)'} interval=${SELF_PING_INTERVAL:-600}s"
|
| 94 |
+
_cron_base="${FRONTEND_BASE_URL:-'(missing)'}"
|
| 95 |
+
_cron_secret_state="missing"; [ -n "${CRON_SECRET:-}" ] && _cron_secret_state="set"
|
| 96 |
+
_cron_v1_state="missing"; [ -n "${EXTERNAL_API_CRON_TOKEN:-}" ] && _cron_v1_state="set"
|
| 97 |
+
log "Frontend credit crons: CRON_ENABLED=${CRON_ENABLED:-true} CRON_TIMEZONE=${CRON_TIMEZONE:-'(system)'} FRONTEND_BASE_URL=${_cron_base} /api/cron(${_cron_secret_state}) /api/v1/cron(${_cron_v1_state}) daily=00:00"
|
| 98 |
+
unset _cron_base _cron_secret_state _cron_v1_state
|
| 99 |
log "Starting uvicorn: host=$HOST port=$PORT workers=1 app=app.main:app"
|
| 100 |
exec python -m uvicorn app.main:app --host "$HOST" --port "$PORT" --workers 1
|