Skip to content

Commit 910b2f8

Browse files
committed
修复补全小错误
1 parent ee0549b commit 910b2f8

File tree

6 files changed

+122
-65
lines changed

6 files changed

+122
-65
lines changed

backend/app/api/public/images.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from datetime import datetime, timezone
55
from typing import Any
66

7-
from fastapi import APIRouter, Query, Request
7+
from fastapi import APIRouter, BackgroundTasks, Query, Request
88
from fastapi.responses import JSONResponse
99

1010
from app.core.errors import ApiError, ErrorCode
@@ -250,6 +250,7 @@ async def proxy_image(
250250
request: Request,
251251
image_id: int,
252252
ext: str,
253+
background_tasks: BackgroundTasks,
253254
pixiv_cat: int = 0,
254255
pximg_mirror_host: str | None = None,
255256
):
@@ -305,15 +306,9 @@ async def proxy_image(
305306
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
306307
mirror_host = mirror_host_override or str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
307308

308-
def _spawn_best_effort(coro) -> None:
309-
async def _run() -> None:
310-
try:
311-
await coro
312-
except Exception:
313-
pass
314-
309+
async def _best_effort(fn, *args, timeout_s: float = 1.5, **kwargs) -> None: # type: ignore[no-untyped-def]
315310
try:
316-
asyncio.create_task(_run())
311+
await asyncio.wait_for(fn(*args, **kwargs), timeout=float(timeout_s))
317312
except Exception:
318313
pass
319314

@@ -340,12 +335,21 @@ async def _run() -> None:
340335
range_header=request.headers.get("Range"),
341336
)
342337
if should_mark_ok:
343-
_spawn_best_effort(mark_image_ok(engine, image_id=int(image.id), now=now))
338+
background_tasks.add_task(_best_effort, mark_image_ok, engine, image_id=int(image.id), now=now, timeout_s=1.5)
344339
if needs_hydrate:
345-
try:
346-
await enqueue_opportunistic_hydrate_metadata(engine, illust_id=int(image.illust_id), reason="image_proxy")
347-
except Exception:
348-
pass
340+
background_tasks.add_task(
341+
_best_effort,
342+
enqueue_opportunistic_hydrate_metadata,
343+
engine,
344+
illust_id=int(image.illust_id),
345+
reason="image_proxy",
346+
timeout_s=2.5,
347+
)
348+
try:
349+
if getattr(resp, "background", None) is None:
350+
resp.background = background_tasks
351+
except Exception:
352+
pass
349353
return resp
350354
except ApiError as exc:
351355
if exc.code in {
@@ -354,13 +358,13 @@ async def _run() -> None:
354358
ErrorCode.UPSTREAM_404,
355359
ErrorCode.UPSTREAM_RATE_LIMIT,
356360
}:
357-
_spawn_best_effort(
358-
mark_image_failure(
359-
engine,
360-
image_id=int(image.id),
361-
now=now,
362-
error_code=exc.code.value,
363-
error_message=exc.message,
364-
)
361+
await _best_effort(
362+
mark_image_failure,
363+
engine,
364+
image_id=int(image.id),
365+
now=now,
366+
error_code=exc.code.value,
367+
error_message=exc.message,
368+
timeout_s=1.5,
365369
)
366370
raise

backend/app/api/public/random.py

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from datetime import datetime, timedelta, timezone
88
from typing import Any
99

10-
from fastapi import APIRouter, Query, Request
10+
from fastapi import APIRouter, BackgroundTasks, Query, Request
1111
from fastapi.responses import RedirectResponse
1212

1313
from app.core.errors import ApiError, ErrorCode
@@ -109,6 +109,7 @@ def _quality_score(image: Any, *, weights: dict[str, float] | None = None) -> fl
109109
@router.get("/random")
110110
async def random_image(
111111
request: Request,
112+
background_tasks: BackgroundTasks,
112113
format: str = "image",
113114
redirect: int = 0,
114115
attempts: int | None = None,
@@ -350,15 +351,9 @@ def _no_match_error() -> ApiError:
350351
use_pixiv_cat = bool(runtime.image_proxy_use_pixiv_cat) or int(pixiv_cat) == 1
351352
mirror_host = mirror_host_override or str(getattr(runtime, "image_proxy_pximg_mirror_host", "") or "").strip() or "i.pixiv.cat"
352353

353-
def _spawn_best_effort(coro) -> None:
354-
async def _run() -> None:
355-
try:
356-
await coro
357-
except Exception:
358-
pass
359-
354+
async def _best_effort(fn, *args, timeout_s: float = 1.5, **kwargs) -> None: # type: ignore[no-untyped-def]
360355
try:
361-
asyncio.create_task(_run())
356+
await asyncio.wait_for(fn(*args, **kwargs), timeout=float(timeout_s))
362357
except Exception:
363358
pass
364359

@@ -713,10 +708,14 @@ def _needs_opportunistic_hydrate(image: Any) -> bool:
713708
tags = await get_tag_names_for_image(session, image_id=image.id)
714709

715710
if _needs_opportunistic_hydrate(image):
716-
try:
717-
await enqueue_opportunistic_hydrate_metadata(engine, illust_id=int(image.illust_id), reason="random")
718-
except Exception:
719-
pass
711+
background_tasks.add_task(
712+
_best_effort,
713+
enqueue_opportunistic_hydrate_metadata,
714+
engine,
715+
illust_id=int(image.illust_id),
716+
reason="random",
717+
timeout_s=2.5,
718+
)
720719

721720
if format == "image" and redirect == 1:
722721
qp: list[tuple[str, str]] = []
@@ -725,11 +724,17 @@ def _needs_opportunistic_hydrate(image: Any) -> bool:
725724
if mirror_host_override is not None:
726725
qp.append(("pximg_mirror_host", str(mirror_host_override)))
727726
qs = ("?" + "&".join([f"{k}={v}" for k, v in qp])) if qp else ""
728-
return RedirectResponse(
727+
resp = RedirectResponse(
729728
url=f"/i/{image.id}.{image.ext}{qs}",
730729
status_code=302,
731730
headers={"Cache-Control": "no-store"},
732731
)
732+
try:
733+
if getattr(resp, "background", None) is None:
734+
resp.background = background_tasks
735+
except Exception:
736+
pass
737+
return resp
733738

734739
origin_url = None if runtime.hide_origin_url_in_public_json else image.original_url
735740

@@ -856,16 +861,21 @@ def _needs_opportunistic_hydrate(image: Any) -> bool:
856861
range_header=request.headers.get("Range"),
857862
)
858863
if should_mark_ok:
859-
_spawn_best_effort(mark_image_ok(engine, image_id=image_id, now=iso_utc_ms()))
864+
background_tasks.add_task(_best_effort, mark_image_ok, engine, image_id=image_id, now=iso_utc_ms(), timeout_s=1.5)
860865
if needs_hydrate:
861-
try:
862-
await enqueue_opportunistic_hydrate_metadata(
863-
engine,
864-
illust_id=illust_id_for_hydrate,
865-
reason="random",
866-
)
867-
except Exception:
868-
pass
866+
background_tasks.add_task(
867+
_best_effort,
868+
enqueue_opportunistic_hydrate_metadata,
869+
engine,
870+
illust_id=illust_id_for_hydrate,
871+
reason="random",
872+
timeout_s=2.5,
873+
)
874+
try:
875+
if getattr(resp, "background", None) is None:
876+
resp.background = background_tasks
877+
except Exception:
878+
pass
869879
return resp
870880
except ApiError as exc:
871881
if exc.code in {
@@ -874,14 +884,14 @@ def _needs_opportunistic_hydrate(image: Any) -> bool:
874884
ErrorCode.UPSTREAM_404,
875885
ErrorCode.UPSTREAM_RATE_LIMIT,
876886
}:
877-
_spawn_best_effort(
878-
mark_image_failure(
879-
engine,
880-
image_id=image_id,
881-
now=iso_utc_ms(),
882-
error_code=exc.code.value,
883-
error_message=exc.message,
884-
)
887+
await _best_effort(
888+
mark_image_failure,
889+
engine,
890+
image_id=image_id,
891+
now=iso_utc_ms(),
892+
error_code=exc.code.value,
893+
error_message=exc.message,
894+
timeout_s=1.5,
885895
)
886896
tried_ids.add(image_id)
887897
last_error = exc

backend/app/core/pixiv_urls.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ class PixivOriginalUrl:
1212
ext: str
1313

1414

15-
_PIXIV_P_RE = re.compile(r"(?P<illust_id>\d+)_p(?P<page_index>\d+)(?:_master1200)?\.(?P<ext>[A-Za-z0-9]+)$")
15+
_PIXIV_P_RE = re.compile(
16+
r"(?P<illust_id>\d+)_p(?P<page_index>\d+)(?:_(?:master|square|custom)\d+)?\.(?P<ext>[A-Za-z0-9]+)$"
17+
)
1618
_PIXIV_UGOIRA_RE = re.compile(r"(?P<illust_id>\d+)_ugoira(?P<page_index>\d+)\.(?P<ext>[A-Za-z0-9]+)$")
19+
_PIXIV_UGOIRA_ZIP_RE = re.compile(r"(?P<illust_id>\d+)_ugoira(?:\d+x\d+)?\.(?P<ext>[A-Za-z0-9]+)$")
1720

18-
ALLOWED_IMAGE_EXTS = {"jpg", "jpeg", "png", "gif", "webp"}
21+
ALLOWED_IMAGE_EXTS = {"jpg", "jpeg", "png", "gif", "webp", "zip"}
22+
23+
ALLOWED_PXIMG_MIRROR_HOSTS = {"i.pixiv.cat", "i.pixiv.re", "i.pixiv.nl"}
1924

2025

2126
def parse_pixiv_original_url(url: str) -> PixivOriginalUrl:
@@ -28,15 +33,16 @@ def parse_pixiv_original_url(url: str) -> PixivOriginalUrl:
2833
raise ValueError("unsupported scheme")
2934

3035
host = (parsed.hostname or "").lower()
31-
if not host.endswith("pximg.net"):
36+
if not (host.endswith("pximg.net") or host in ALLOWED_PXIMG_MIRROR_HOSTS):
3237
raise ValueError("unsupported host")
3338

34-
m = _PIXIV_P_RE.search(parsed.path) or _PIXIV_UGOIRA_RE.search(parsed.path)
39+
m = _PIXIV_P_RE.search(parsed.path) or _PIXIV_UGOIRA_RE.search(parsed.path) or _PIXIV_UGOIRA_ZIP_RE.search(parsed.path)
3540
if not m:
3641
raise ValueError("unsupported pixiv original url")
3742

3843
illust_id = int(m.group("illust_id"))
39-
page_index = int(m.group("page_index"))
44+
page_index_raw = m.groupdict().get("page_index")
45+
page_index = int(page_index_raw) if page_index_raw is not None else 0
4046
ext = m.group("ext").lower()
4147
if ext not in ALLOWED_IMAGE_EXTS:
4248
raise ValueError("unsupported ext")

backend/app/db/session.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import os
5+
import random
46
import sqlite3
57
from collections.abc import AsyncIterator, Awaitable, Callable
68
from typing import TypeVar
79

8-
from sqlalchemy.exc import OperationalError
10+
from sqlalchemy.exc import OperationalError, TimeoutError
911
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
1012

1113
T = TypeVar("T")
1214

1315

1416
def is_sqlite_busy_error(exc: BaseException) -> bool:
17+
if isinstance(exc, TimeoutError):
18+
return True
1519
if isinstance(exc, sqlite3.OperationalError):
1620
msg = str(exc).lower()
1721
return (
@@ -29,17 +33,41 @@ def is_sqlite_busy_error(exc: BaseException) -> bool:
2933
async def with_sqlite_busy_retry(
3034
op: Callable[[], Awaitable[T]],
3135
*,
32-
retries: int = 3,
36+
retries: int = 8,
3337
base_delay_s: float = 0.05,
3438
) -> T:
39+
try:
40+
env_retries = int((os.environ.get("SQLITE_BUSY_RETRIES") or "").strip() or retries)
41+
except Exception:
42+
env_retries = int(retries)
43+
retries_i = max(0, min(int(env_retries), 50))
44+
45+
try:
46+
env_base = float((os.environ.get("SQLITE_BUSY_BASE_DELAY_S") or "").strip() or base_delay_s)
47+
except Exception:
48+
env_base = float(base_delay_s)
49+
base_delay = float(max(0.0, min(float(env_base), 5.0)))
50+
51+
try:
52+
env_max_delay = float((os.environ.get("SQLITE_BUSY_MAX_DELAY_S") or "").strip() or 2.0)
53+
except Exception:
54+
env_max_delay = 2.0
55+
max_delay = float(max(0.0, min(float(env_max_delay), 30.0)))
56+
3557
attempt = 0
3658
while True:
3759
try:
3860
return await op()
3961
except Exception as exc:
40-
if attempt >= retries or not is_sqlite_busy_error(exc):
62+
if attempt >= retries_i or not is_sqlite_busy_error(exc):
4163
raise
42-
await asyncio.sleep(base_delay_s * (2**attempt))
64+
delay = base_delay * (2**attempt)
65+
if max_delay > 0:
66+
delay = min(float(delay), float(max_delay))
67+
if delay > 0:
68+
# Add a tiny jitter to avoid synchronized retries under load.
69+
delay *= 0.9 + (random.random() * 0.2)
70+
await asyncio.sleep(float(delay))
4371
attempt += 1
4472

4573

backend/app/jobs/handlers/hydrate_metadata.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1312,11 +1312,15 @@ async def _hydrate_single_illust(*, illust_id: int, source_import_id: int | None
13121312

13131313
pages: list[_IllustPage] = []
13141314
for idx, url in enumerate(urls):
1315+
url_s = str(url)
13151316
try:
1316-
ext = _parse_pximg_ext(url)
1317+
ext = _parse_pximg_ext(url_s)
13171318
except Exception as exc:
1318-
raise JobPermanentError("Pixiv illust detail invalid original url") from exc
1319-
pages.append(_IllustPage(page_index=int(idx), original_url=str(url), ext=ext))
1319+
bad = redact_text(url_s)
1320+
if len(bad) > 300:
1321+
bad = bad[:297] + "..."
1322+
raise JobPermanentError(f"Pixiv illust detail invalid original url: {bad}") from exc
1323+
pages.append(_IllustPage(page_index=int(idx), original_url=url_s, ext=ext))
13201324

13211325
width = _as_int(illust.get("width"))
13221326
height = _as_int(illust.get("height"))

backend/app/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import time
55

66
from fastapi import FastAPI, Request
7+
from fastapi.responses import Response
78

89
from app.api.admin.router import router as admin_router
910
from app.api.metrics import router as metrics_router
@@ -233,6 +234,10 @@ async def _shutdown() -> None: # type: ignore[no-redef]
233234
if engine is not None:
234235
await engine.dispose()
235236

237+
@app.get("/favicon.ico", include_in_schema=False)
238+
async def _favicon() -> Response: # type: ignore[no-redef]
239+
return Response(status_code=204, headers={"Cache-Control": "public, max-age=86400"})
240+
236241
app.include_router(healthz_router)
237242
app.include_router(docs_page_router)
238243
app.include_router(authors_router)

0 commit comments

Comments
 (0)