refactor(workspace): enforce layered crate boundaries

This commit is contained in:
elky
2026-07-15 23:47:19 +08:00
parent a728c090a9
commit 8616fe6ee2
969 changed files with 40187 additions and 27240 deletions
@@ -55,11 +55,16 @@ async fn resolve_wallet_auth_gate_with_cache(
None => WalletAccessDecision::wallet_unavailable(None),
};
if !auth_snapshot.api_key_is_standalone {
if let Some(quota) = state
.find_user_daily_quota_availability(&auth_snapshot.user_id)
.await?
.filter(|quota| quota.has_active_daily_quota)
{
let quota = if use_cache {
state
.find_user_daily_quota_availability_for_auth(&auth_snapshot.user_id)
.await?
} else {
state
.find_user_daily_quota_availability_for_auth_uncached(&auth_snapshot.user_id)
.await?
};
if let Some(quota) = quota.filter(|quota| quota.has_active_daily_quota) {
let has_remaining_quota = quota.remaining_usd > DAILY_QUOTA_EPSILON_USD;
if decision.failure == Some(WalletAccessFailure::BalanceDenied) && has_remaining_quota {
return Ok(Some(WalletAccessDecision::allowed(Some(
@@ -106,6 +111,7 @@ fn map_wallet_snapshot(snapshot: &StoredWalletSnapshot) -> WalletSnapshot {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::time::Duration;
use aether_data::repository::usage::InMemoryUsageReadRepository;
use aether_data::repository::wallet::{InMemoryWalletRepository, StoredWalletSnapshot};
@@ -113,6 +119,7 @@ mod tests {
BillingReadRepository, StoredBillingModelContext, UserDailyQuotaAvailabilityRecord,
};
use aether_data_contracts::DataLayerError;
use aether_runtime::ConcurrencyGate;
use aether_wallet::{WalletAccessFailure, WalletLimitMode, WalletSnapshot, WalletStatus};
use async_trait::async_trait;
@@ -257,6 +264,41 @@ mod tests {
assert_eq!(decision.remaining, Some(4.0));
}
#[tokio::test]
async fn disabled_auth_capacity_cache_still_gates_wallet_reads() {
let mut state = state_with_wallet_and_quota(empty_user_wallet(), None);
let mut guard_config = (*state.frontdoor_runtime_guards).clone();
guard_config.auth_capacity_cache_ttl = Duration::ZERO;
state = state.with_frontdoor_runtime_guard_config_for_tests(guard_config);
state.auth_snapshot_load_gate =
Some(Arc::new(ConcurrencyGate::new("test_auth_wallet_load", 1)));
let held = state
.acquire_auth_snapshot_load_gate()
.await
.expect("auth gate acquisition should succeed")
.expect("auth gate should be configured");
let blocked = tokio::time::timeout(
Duration::from_millis(25),
state.read_wallet_snapshot_for_auth("user-1", "api-key-1", false),
)
.await;
assert!(
blocked.is_err(),
"zero-TTL wallet reads must wait for the auth DB gate"
);
drop(held);
let wallet = tokio::time::timeout(
Duration::from_secs(1),
state.read_wallet_snapshot_for_auth("user-1", "api-key-1", false),
)
.await
.expect("wallet read should resume after releasing the auth gate")
.expect("wallet read should succeed");
assert!(wallet.is_some());
}
#[tokio::test]
async fn admin_wallet_recharge_invalidates_cached_auth_capacity_state() {
let wallet = empty_user_wallet();
+27 -19
View File
@@ -1,9 +1,9 @@
use std::sync::Arc;
use std::time::Duration;
use tracing::warn;
use crate::data::GatewayDataState;
use crate::AppState;
const QUOTA_RESET_INTERVAL: Duration = Duration::from_secs(60 * 60);
@@ -18,26 +18,31 @@ pub(crate) async fn reset_due_provider_quotas_once(
}
pub(crate) fn spawn_provider_quota_reset_worker(
data: Arc<GatewayDataState>,
app: AppState,
) -> Option<tokio::task::JoinHandle<()>> {
if !data.has_provider_quota_writer() {
if !app.data.has_provider_quota_writer() {
return None;
}
Some(tokio::spawn(async move {
if let Err(err) = reset_due_provider_quotas_once(&data).await {
warn!(error = %err, "gateway provider quota reset startup failed");
}
let mut interval = tokio::time::interval(QUOTA_RESET_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval.tick().await;
loop {
interval.tick().await;
Some(crate::task_runtime::spawn_singleton_worker(
app,
crate::task_runtime::TASK_KEY_PROVIDER_QUOTA_RESET,
|app| async move {
let data = app.data;
if let Err(err) = reset_due_provider_quotas_once(&data).await {
warn!(error = %err, "gateway provider quota reset tick failed");
warn!(error = %err, "gateway provider quota reset startup failed");
}
}
}))
let mut interval = tokio::time::interval(QUOTA_RESET_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval.tick().await;
loop {
interval.tick().await;
if let Err(err) = reset_due_provider_quotas_once(&data).await {
warn!(error = %err, "gateway provider quota reset tick failed");
}
}
},
))
}
#[cfg(test)]
@@ -52,6 +57,7 @@ mod tests {
use super::{reset_due_provider_quotas_once, spawn_provider_quota_reset_worker};
use crate::data::GatewayDataState;
use crate::AppState;
#[tokio::test]
async fn resets_due_provider_quotas_from_runtime() {
@@ -98,10 +104,12 @@ mod tests {
)
.expect("quota should build"),
]));
let data = Arc::new(GatewayDataState::with_provider_quota_repository_for_tests(
repository.clone(),
));
let handle = spawn_provider_quota_reset_worker(data).expect("worker should spawn");
let state = AppState::new()
.expect("gateway state should build")
.with_data_state_for_tests(GatewayDataState::with_provider_quota_repository_for_tests(
repository.clone(),
));
let handle = spawn_provider_quota_reset_worker(state).expect("worker should spawn");
let stored = tokio::time::timeout(Duration::from_secs(1), async {
loop {