@@ -547,10 +547,17 @@ pub async fn get_blob(
547547
548548 let caller = auth. as_ref ( ) . map ( |e| e. 0 . 0 . as_str ( ) ) ;
549549 let gate_path = format ! ( "/{file_path}" ) ;
550- let ( record, _rules) =
551- crate :: api:: authorize_repo_read ( & state, & owner, & name, caller, & gate_path) . await ?;
552-
553550 let acquire_deadline = std:: time:: Duration :: from_secs ( state. config . git_acquire_timeout_secs ) ;
551+ let ( record, _rules) = tokio:: time:: timeout (
552+ acquire_deadline,
553+ crate :: api:: authorize_repo_read ( & state, & owner, & name, caller, & gate_path) ,
554+ )
555+ . await
556+ . map_err ( |_elapsed| {
557+ tracing:: warn!( repo = %name, "repo authorization timed out; shedding blob request with 503" ) ;
558+ AppError :: Overloaded ( "git service acquisition timed out, retry shortly" . into ( ) )
559+ } ) ??;
560+
554561 let disk_path = tokio:: time:: timeout (
555562 acquire_deadline,
556563 state. repo_store . acquire ( & record. owner_did , & record. name ) ,
@@ -4125,6 +4132,46 @@ mod tests {
41254132 assert_eq ! ( state. git_read_per_caller. tracked_keys( ) , 0 ) ;
41264133 }
41274134
4135+ #[ sqlx:: test]
4136+ async fn blob_route_authorization_timeout_sheds_503_and_releases_permits ( pool : sqlx:: PgPool ) {
4137+ use axum:: http:: StatusCode ;
4138+ use http_body_util:: BodyExt ;
4139+
4140+ let mut state = crate :: test_support:: test_state ( pool. clone ( ) ) . await ;
4141+ state. push_limiter_trust = crate :: rate_limit:: TrustedProxy :: None ;
4142+ let mut cfg = ( * state. config ) . clone ( ) ;
4143+ cfg. git_acquire_timeout_secs = 1 ;
4144+ state. config = Arc :: new ( cfg) ;
4145+
4146+ // Hold an exclusive lock on `repos` on a separate connection so `get_repo` blocks.
4147+ let mut tx = pool. begin ( ) . await . unwrap ( ) ;
4148+ sqlx:: query ( "LOCK TABLE repos IN ACCESS EXCLUSIVE MODE" )
4149+ . execute ( & mut * tx)
4150+ . await
4151+ . unwrap ( ) ;
4152+
4153+ let response = blob_route_request_path (
4154+ state. clone ( ) ,
4155+ "z6blobauthztimeout" ,
4156+ "file.txt" ,
4157+ "203.0.113.31:5000" ,
4158+ )
4159+ . await ;
4160+
4161+ tx. rollback ( ) . await . unwrap ( ) ;
4162+
4163+ assert_eq ! ( response. status( ) , StatusCode :: SERVICE_UNAVAILABLE ) ;
4164+ let bytes = response. into_body ( ) . collect ( ) . await . unwrap ( ) . to_bytes ( ) ;
4165+ let body: serde_json:: Value = serde_json:: from_slice ( & bytes) . unwrap ( ) ;
4166+ assert_eq ! ( body[ "error" ] , "overloaded" ) ;
4167+ assert_eq ! ( state. git_read_semaphore. available_permits( ) , 64 ) ;
4168+ assert_eq ! (
4169+ state. git_blob_semaphore. available_permits( ) ,
4170+ MAX_CONCURRENT_BLOB_READS
4171+ ) ;
4172+ assert_eq ! ( state. git_read_per_caller. tracked_keys( ) , 0 ) ;
4173+ }
4174+
41284175 #[ cfg( unix) ]
41294176 #[ sqlx:: test]
41304177 async fn blob_route_returns_200_with_expected_headers_and_content ( pool : sqlx:: PgPool ) {
@@ -4347,6 +4394,8 @@ mod tests {
43474394 fn write_fake_git ( dir : & std:: path:: Path , body : & str ) -> String {
43484395 use std:: io:: Write ;
43494396 let p = dir. join ( "fakegit" ) ;
4397+ // Write in a child so parallel tests cannot inherit an open writable
4398+ // script descriptor when they fork (which would cause ETXTBSY).
43504399 let mut writer = std:: process:: Command :: new ( "sh" )
43514400 . args ( [ "-c" , "cat > \" $1\" && chmod 755 \" $1\" " , "fixture-writer" ] )
43524401 . arg ( & p)
0 commit comments