Skip to content

Commit ad003b3

Browse files
committed
fix(api): clamp authorize_repo_read with acquire timeout in get_blob
1 parent bbab52e commit ad003b3

1 file changed

Lines changed: 52 additions & 3 deletions

File tree

‎crates/gitlawb-node/src/api/repos.rs‎

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)