From 937c8e999c24a3e84663b9b84629a6fab6ff2114 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 25 May 2026 15:00:26 +0300 Subject: [PATCH] recover from stale db connection Index and /register handlers used to silently render the "welcome / register" page whenever get_users_count() returned 0, including when the underlying SQL call errored. With a long-lived Postgres handle dropped by NAT or idle_session_timeout, the entire site flipped to "fresh install" until systemd restart. - get_users_count() now propagates errors instead of swallowing to 0 - add reconnect_db() that drops and reopens the handle - add get_users_count_with_reconnect() that retries once after reconnect - render a 503 db_error page when the db is genuinely unreachable --- database.v | 34 ++++++++++++++++++++++++++++++++++ gitly.v | 7 +++---- user/user.v | 2 +- user/user_routes.v | 4 ++-- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/database.v b/database.v index bda038d..b16052b 100644 --- a/database.v +++ b/database.v @@ -1,5 +1,39 @@ module main +import veb + +// reconnect_db drops the current DB handle and opens a fresh connection. +// Used to recover from stale connections (e.g. an idle Postgres connection +// dropped by NAT or by PG's idle_session_timeout). v's db.pg has no built-in +// reconnect, so without this the process keeps using a dead handle until restart. +pub fn (mut app App) reconnect_db() ! { + app.db.close() or {} + app.db = connect_db(app.config)! +} + +// get_users_count_with_reconnect retries the user count query once after +// reconnecting on failure. This is the recovery path for the specific bug +// where a dead DB handle made get_users_count() silently return 0 and the +// site rendered the "welcome / register" page until systemd restart. +pub fn (mut app App) get_users_count_with_reconnect() !int { + if count := app.get_users_count() { + return count + } else { + app.warn('db query failed, attempting reconnect: ${err}') + app.reconnect_db() or { return error('db unavailable; reconnect failed: ${err}') } + return app.get_users_count()! + } +} + +// db_error renders a 503 response describing a database failure. +// We render an explicit page rather than letting callers fall back to a +// misleading default (e.g. redirecting to /register on a swallowed error). +pub fn (mut ctx Context) db_error(err IError) veb.Result { + ctx.res.set_status(.service_unavailable) + body := 'Gitly — database unavailable

Database unavailable

Gitly could not reach its database. This is usually transient — please try again in a moment.

${err}
' + return ctx.html(body) +} + fn sql_table(name string) string { return '"' + name.to_lower().replace('"', '""') + '"' } diff --git a/gitly.v b/gitly.v index aef7e70..fedd446 100644 --- a/gitly.v +++ b/gitly.v @@ -181,10 +181,9 @@ pub fn (mut app App) open_source() veb.Result { } @['/'] -pub fn (mut app App) index() veb.Result { - user_count := app.get_users_count() or { 0 } - no_users := user_count == 0 - if no_users { +pub fn (mut app App) index(mut ctx Context) veb.Result { + user_count := app.get_users_count_with_reconnect() or { return ctx.db_error(err) } + if user_count == 0 { return ctx.redirect('/register') } diff --git a/user/user.v b/user/user.v index 91179c6..396698e 100644 --- a/user/user.v +++ b/user/user.v @@ -343,7 +343,7 @@ fn (app App) search_users(query string) []User { pub fn (mut app App) get_users_count() !int { return sql app.db { select count from User - } or { 0 } + }! } pub fn (mut app App) get_count_repo_contributors(id int) !int { diff --git a/user/user_routes.v b/user/user_routes.v index 0519ff9..87a5508 100644 --- a/user/user_routes.v +++ b/user/user_routes.v @@ -193,7 +193,7 @@ pub fn (mut app App) register(mut ctx Context) veb.Result { csrf := rand.string(30) ctx.set_cookie(name: 'csrf', value: csrf) - user_count := app.get_users_count() or { 0 } + user_count := app.get_users_count_with_reconnect() or { return ctx.db_error(err) } no_users := user_count == 0 ctx.current_path = '' @@ -212,7 +212,7 @@ fn (mut app App) register_failed(mut ctx Context, no_redirect string, msg string @['/register'; post] pub fn (mut app App) handle_register(mut ctx Context, username string, email string, password string, no_redirect string) veb.Result { - user_count := app.get_users_count() or { + user_count := app.get_users_count_with_reconnect() or { eprintln('[register] get_users_count failed: ${err}') return app.register_failed(mut ctx, no_redirect, 'Failed to register: ${err}') } -- 2.39.5