From 590c4a163bc8a41b74eb86be3e9f9845b960013b Mon Sep 17 00:00:00 2001 From: Zyy Date: Thu, 17 Sep 2026 05:36:00 +0800 Subject: [PATCH] fix(store): release rejected proxy import locks before returning Explicitly unlock rejected import reservations before returning their error so immediate proxy operations do not race connection teardown. Cover repeated rejection followed by writes with PostgreSQL integration tests. Co-authored-by: Codex --- .../gateway-store/src/postgres/proxies.rs | 16 ++++++-- .../gateway-store/tests/postgres/proxies.rs | 41 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/backend/crates/gateway-store/src/postgres/proxies.rs b/backend/crates/gateway-store/src/postgres/proxies.rs index f92ef58e9..76c5280d7 100644 --- a/backend/crates/gateway-store/src/postgres/proxies.rs +++ b/backend/crates/gateway-store/src/postgres/proxies.rs @@ -441,10 +441,18 @@ impl ProxyStore for PgProxyRepository { if !acquired { return Err(store_error(conflict(id))); } - let record = self.get(id).await?; - if !record.last_test.is_some_and(|test| test.success) { - return Err(store_error(conflict(id))); - } + let record = match self.get(id).await { + Ok(record) if record.last_test.as_ref().is_some_and(|test| test.success) => record, + result => { + // 拒绝预留时先等待数据库释放锁,避免连接关闭尚未生效就误挡后续代理操作。 + sqlx::query("select pg_advisory_unlock_shared(hashtextextended($1, 739219))") + .bind(id) + .execute(&mut connection) + .await + .map_err(|_| store_error(unavailable()))?; + return Err(result.err().unwrap_or_else(|| store_error(conflict(id)))); + } + }; Ok(ProxyImportReservation { binding: ImportProxyBinding { id: record.id, diff --git a/backend/crates/gateway-store/tests/postgres/proxies.rs b/backend/crates/gateway-store/tests/postgres/proxies.rs index c81b87d64..901e0a30f 100644 --- a/backend/crates/gateway-store/tests/postgres/proxies.rs +++ b/backend/crates/gateway-store/tests/postgres/proxies.rs @@ -473,6 +473,47 @@ async fn proxy_accounts_paginate_thousands_of_accounts_and_search_without_loadin database.close().await; } +#[tokio::test] +async fn rejected_import_reservations_release_proxy_lock_before_returning() { + let Some(database) = TestDatabase::create("rejected_proxy_import").await else { + return; + }; + let store = PgProxyRepository::new(database.pool.clone()); + let other_process = PgProxyRepository::new(database.pool.clone()); + let context = context(); + let saved = store + .create( + NewProxy { + location: None, + name: "未通过检测的出口".to_owned(), + proxy: OutboundProxy::parse("http://127.0.0.1:8080").unwrap(), + }, + &context, + ) + .await + .unwrap() + .record; + + // 每次失败后立即写入检测结果,不等待连接关闭或重试锁冲突。 + for _ in 0..64 { + let error = store.reserve_import(&saved.id).await.err().unwrap(); + assert_eq!(error.kind(), AdminStoreErrorKind::Conflict); + other_process + .record_test( + &saved.id, + saved.revision, + ProxyTestResult { + success: false, + ..success() + }, + &context, + ) + .await + .unwrap(); + } + database.close().await; +} + #[tokio::test] async fn import_reservation_blocks_proxy_mutations_until_rotated_credentials_are_committed() { use gateway_store::postgres::{