rate_limit.rs registers four limiters (strict/upload/image/comment) plus
an "unknown" fallback bucket for when the real client IP can't be
determined, but only the strict/upload/image tiers were listed in
.env.example and AGENTS.md. Add the missing comment and unknown tier
tunables so the documented surface matches the code.
DB migration failure at startup panicked via .expect() (main.rs:223),
dumping a raw backtrace for what is usually just "PostgreSQL isn't running
yet". Worse, the 1.6s runtime retry budget was reused at startup, so it
almost always failed against a cold/slow DB (docker-compose without
healthchecks, local Postgres not started, etc.).
Changes:
- pool: extract build_pg_config() (returns Result, no panic); add
validate_database_url() so URL-format / DB_POOL_SIZE errors surface as
friendly exit(1) instead of hitting the LazyLock's unreachable .expect()
- pool: add get_conn_for_startup() — a startup-only retry loop with a
configurable total-duration window (MIGRATE_STARTUP_TIMEOUT_SECS,
default 30s, 500ms interval), separate from the runtime get_conn()
anti-avalanche fast retry (untouched)
- migrate: split run() into run_on_conn(&mut conn) so callers control the
connection-acquisition strategy; main.rs pairs this with the startup
retry. Advisory-lock release comment updated: exit(1) terminates the
process just like panic, so the session-level lock is still freed
- main: startup fatal errors (bad URL, DB unreachable, migration failed)
now each emit tracing::error! + eprintln! ERROR + targeted HINT, then
exit(1) — no panic, no backtrace nudge
- .env.example / AGENTS.md: document MIGRATE_STARTUP_TIMEOUT_SECS
No runtime behavior change: get_conn() and its ~40 call sites are
unchanged. Advisory-lock safety preserved (documented in migrate.rs).
upload_image declared ConnectInfo<SocketAddr> as a required Axum extractor,
but dioxus::server::serve() owns the listener and cannot call
into_make_service_with_connect_info::<SocketAddr>(), so the request extension
is absent on manually-merged routes. The extractor failed and Axum returned
500. dev (dx serve) passed by luck; release builds failed consistently.
Match serve_image's graceful-degradation pattern: take
Option<Extension<ConnectInfo<SocketAddr>>>, fall back to the 'unknown'
rate-limit bucket when missing. Production should deploy behind a reverse
proxy with TRUSTED_PROXY_COUNT so rate limiting keys on the real client IP.
Also correct the misleading comment in main.rs: axum 0.8 does have
ConnectInfoLayer/into_make_service_with_connect_info; the real blocker is
that Dioxus owns the listener.
Embed database migrations into the server binary: migrations now run
automatically on startup before the server listens, via a hand-written
runner (~150 LOC) using the existing tokio-postgres/deadpool stack.
- New src/db/migrate.rs: MIGRATIONS constant (include_str!), MigrateError,
run() with advisory lock + per-migration transactions
- schema_migrations table tracks applied versions
- main.rs drives the async runner via a dedicated transient tokio runtime
- migrate.sh retained as a manual/CI fallback with updated header
- 4 unit tests (sorted, unique, non-empty, files-match-rows)
- Verified end-to-end: cold start, warm restart, rewind, failure path
main() is sync, so the async migrate::run() is driven by a dedicated
multi-thread tokio runtime that is built, blocked-on, and dropped before
dioxus::server::serve() starts its own runtime. This keeps the two
runtimes from overlapping.
- 006: ADD COLUMN 补 IF NOT EXISTS
- 002: 5 个索引(status_published/slug_unique/post_tags_post/post_tags_tag/cover)补 IF NOT EXISTS
- migrate.sh: 区分「已应用」与「真出错」,真错误时打印输出并中止,不再静默吞掉(M6)
已验证:第二次运行全部 OK(幂等),不再出现重复对象错误。
- idx_posts_deleted_at: 回收站查询 WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC
- idx_posts_created_at_admin: 管理后台全量列表 WHERE deleted_at IS NULL ORDER BY created_at DESC