@@ -10,6 +10,7 @@ mod types;
1010pub mod timing;
1111
1212use std:: sync:: Arc ;
13+ use std:: time:: Duration ;
1314
1415use anyhow:: Context ;
1516use axum:: extract:: { FromRef , FromRequest , FromRequestParts , Path as AxumPath , State as AxumState } ;
@@ -36,17 +37,19 @@ use tower_http::compression::{DefaultPredicate, Predicate};
3637use tower_http:: { compression:: CompressionLayer , cors} ;
3738
3839use crate :: auth:: { Auth , AuthError , Authenticated , Jwt , Permission , UserAuthContext } ;
40+ use crate :: connection:: program:: { Program , Step } ;
3941use crate :: connection:: { Connection , RequestContext } ;
4042use crate :: error:: Error ;
4143use crate :: http:: user:: db_factory:: MakeConnectionExtractorPath ;
4244use crate :: http:: user:: timing:: timings_middleware;
4345use crate :: http:: user:: types:: HttpQuery ;
4446use crate :: metrics:: LEGACY_HTTP_CALL ;
45- use crate :: namespace:: NamespaceStore ;
47+ use crate :: namespace:: { NamespaceName , NamespaceStore } ;
4648use crate :: net:: Accept ;
49+ use crate :: query:: Params ;
4750use crate :: query:: { self , Query } ;
4851use crate :: query_analysis:: { predict_final_state, Statement , TxnStatus } ;
49- use crate :: query_result_builder:: QueryResultBuilder ;
52+ use crate :: query_result_builder:: { QueryResultBuilder , StepResult , StepResultsBuilder } ;
5053use crate :: rpc:: proxy:: rpc:: proxy_server:: { Proxy , ProxyServer } ;
5154use crate :: schema:: { MigrationDetails , MigrationSummary } ;
5255use crate :: utils:: services:: idle_shutdown:: IdleShutdownKicker ;
@@ -163,9 +166,75 @@ async fn show_console(
163166 }
164167}
165168
166- async fn handle_health ( ) -> Response < Body > {
167- // return empty OK
168- Response :: new ( Body :: empty ( ) )
169+ /// How long we are willing to wait for the database to prove it is servable
170+ /// before declaring the instance unhealthy.
171+ ///
172+ /// The default kubelet liveness probe timeout is 1s and the probe is usually
173+ /// run more frequently than the failure threshold, so the budget here must be
174+ /// comfortably inside the probe timeout: a healthy-but-busy database gets the
175+ /// full budget, a wedged one (e.g. a checkpoint or S3 path holding the write
176+ /// lock on the single async worker) times out and the probe returns 503.
177+ const HEALTH_QUERY_TIMEOUT : Duration = Duration :: from_millis ( 500 ) ;
178+
179+ async fn handle_health ( AxumState ( state) : AxumState < AppState > ) -> impl IntoResponse {
180+ // The default namespace reports "is this whole process able to serve its
181+ // *data* path" — not merely "is HTTP up".
182+ match serve_default_namespace_health ( & state) . await {
183+ true => ( StatusCode :: OK , "" ) . into_response ( ) ,
184+ false => ( StatusCode :: SERVICE_UNAVAILABLE , "database not servable" ) . into_response ( ) ,
185+ }
186+ }
187+
188+ /// Runs a real, bounded read against the default namespace to prove the
189+ /// datapath is servable. Returns `true` when a query completed within the
190+ /// budget. The entire round-trip — resolving the namespace, opening the
191+ /// connection, and running the query — is bounded by [`HEALTH_QUERY_TIMEOUT`]
192+ /// so a wedge anywhere in the datapath (not just query execution) becomes a
193+ /// 503.
194+ async fn serve_default_namespace_health ( state : & AppState ) -> bool {
195+ let namespace = NamespaceName :: default ( ) ;
196+
197+ tokio:: time:: timeout ( HEALTH_QUERY_TIMEOUT , async {
198+ // Mirror how a real request resolves the namespace so the check
199+ // exercises the same connection path (including the read-txn upgrade
200+ // path that contends on the connection-manager write lock during
201+ // checkpoints).
202+ let Ok ( connection_maker) = state
203+ . namespaces
204+ . with ( namespace. clone ( ) , |ns| ns. db . connection_maker ( ) )
205+ . await
206+ else {
207+ return false ;
208+ } ;
209+ let Ok ( connection) = connection_maker. create ( ) . await else {
210+ return false ;
211+ } ;
212+ let meta_store = state. namespaces . meta_store ( ) . clone ( ) ;
213+ let ctx = RequestContext :: new ( Authenticated :: FullAccess , namespace, meta_store) ;
214+
215+ // A single, pure read that exercises the same datapath as a real
216+ // request. `SELECT 1` is constant and must always parse.
217+ let query = Query {
218+ stmt : Statement :: parse ( "SELECT 1" )
219+ . next ( )
220+ . expect ( "constant health query must parse" )
221+ . expect ( "constant health query must parse" ) ,
222+ params : Params :: empty ( ) ,
223+ want_rows : false ,
224+ } ;
225+ let program = Program :: new ( vec ! [ Step { cond: None , query } ] ) ;
226+
227+ // `SELECT 1` is a pure read: servable iff the single step succeeded.
228+ match connection
229+ . execute_program ( program, ctx, StepResultsBuilder :: default ( ) , None )
230+ . await
231+ {
232+ Ok ( builder) => matches ! ( builder. into_ret( ) . as_slice( ) , [ StepResult :: Ok ] ) ,
233+ Err ( _) => false ,
234+ }
235+ } )
236+ . await
237+ . unwrap_or ( false )
169238}
170239
171240async fn handle_upgrade (
0 commit comments