Skip to content

Commit 4f141ee

Browse files
committed
perf(startup): drop duplicate Tortoise init, keep ORM exception handlers
1 parent 496a713 commit 4f141ee

2 files changed

Lines changed: 78 additions & 11 deletions

File tree

‎main.py‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from fastapi.middleware.cors import CORSMiddleware
1111
from fastapi.responses import JSONResponse
1212
from tortoise import Tortoise
13-
from tortoise.contrib.fastapi import register_tortoise
13+
from tortoise.contrib.fastapi import tortoise_exception_handlers
1414

1515
from apps.admin.views import admin_api
1616
from apps.base.config import (
@@ -29,7 +29,7 @@
2929
)
3030
from apps.base.views import share_api, chunk_api, presign_api
3131
from apps.base.mcp import router as mcp_router
32-
from core.database import db_startup_lock, get_db_config, init_db
32+
from core.database import db_startup_lock, init_db
3333
from core.errors import StorageError
3434
from core.logger import get_log_level_name, is_access_log_enabled, logger
3535
from core.settings import settings
@@ -89,7 +89,14 @@ async def load_config():
8989
)
9090

9191

92-
app = FastAPI(lifespan=lifespan, version=APP_VERSION)
92+
# Tortoise 的 ORM 异常映射(DoesNotExist → 404、IntegrityError → 422)。
93+
# 只注册处理器;连接初始化统一由 lifespan 里的 init_db() 负责,
94+
# 避免 register_tortoise 合并 lifespan 造成的双重初始化。
95+
app = FastAPI(
96+
lifespan=lifespan,
97+
version=APP_VERSION,
98+
exception_handlers=tortoise_exception_handlers(),
99+
)
93100

94101

95102
@app.exception_handler(StorageError)
@@ -126,14 +133,6 @@ async def refresh_settings_middleware(request, call_next):
126133
allow_headers=["*"],
127134
)
128135

129-
# 使用 register_tortoise 来添加异常处理器
130-
register_tortoise(
131-
app,
132-
config=get_db_config(),
133-
generate_schemas=False,
134-
add_exception_handlers=True,
135-
)
136-
137136
app.include_router(share_api)
138137
app.include_router(chunk_api)
139138
app.include_router(presign_api)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""One Tortoise init path; ORM exception mappings still registered."""
2+
import asyncio
3+
import unittest
4+
from pathlib import Path
5+
from unittest import mock
6+
7+
from tortoise.exceptions import DoesNotExist, IntegrityError
8+
9+
import main
10+
11+
12+
async def _idle_task():
13+
# Stand-in for the real background loops; the lifespan cancels it on exit.
14+
await asyncio.sleep(3600)
15+
16+
17+
class StartupSingleInitTests(unittest.TestCase):
18+
def test_orm_exception_handlers_are_registered(self):
19+
# Behavior pinned by test_admin_write_paths: DoesNotExist -> 404 JSON.
20+
# IntegrityError -> 422 must stay available too.
21+
self.assertIn(DoesNotExist, main.app.exception_handlers)
22+
self.assertIn(IntegrityError, main.app.exception_handlers)
23+
24+
def test_register_tortoise_is_not_used(self):
25+
# register_tortoise merges another lifespan that calls Tortoise.init
26+
# before init_db(); batch 4 (SQLite PRAGMAs) needs a single init path.
27+
source = Path("main.py").read_text(encoding="utf-8")
28+
self.assertNotIn("import register_tortoise", source)
29+
self.assertNotIn("register_tortoise(", source)
30+
31+
def test_lifespan_does_not_initialize_tortoise_outside_init_db(self):
32+
async def scenario():
33+
init_db_calls = []
34+
35+
async def fake_init_db():
36+
init_db_calls.append(True)
37+
38+
async def fake_load_config():
39+
return None
40+
41+
with (
42+
mock.patch.object(main, "init_db", fake_init_db),
43+
mock.patch.object(main, "load_config", fake_load_config),
44+
mock.patch.object(main, "delete_expire_files", _idle_task),
45+
mock.patch.object(main, "clean_incomplete_uploads", _idle_task),
46+
mock.patch.object(
47+
main, "clean_expired_presign_sessions", _idle_task
48+
),
49+
mock.patch.object(main.Tortoise, "init") as tortoise_init,
50+
mock.patch.object(
51+
main.Tortoise, "close_connections"
52+
),
53+
):
54+
# FastAPI merges the lifespan of every included router, so use
55+
# the installed context rather than main.lifespan directly.
56+
async with main.app.router.lifespan_context(main.app):
57+
pass
58+
59+
return init_db_calls, tortoise_init
60+
61+
init_db_calls, tortoise_init = asyncio.run(scenario())
62+
63+
self.assertEqual(init_db_calls, [True])
64+
tortoise_init.assert_not_awaited()
65+
66+
67+
if __name__ == "__main__":
68+
unittest.main()

0 commit comments

Comments
 (0)