mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 16:06:08 +00:00
Report per-run daemon progress to reset health failures and restart backoff after successful work, with stale-run isolation and recovery regression tests. Co-authored-by: Zyy <zyycn@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com>
1135 lines
39 KiB
Rust
1135 lines
39 KiB
Rust
//! 验证执行写入队列的容量、请求内顺序与排空行为
|
|
|
|
use std::num::NonZeroUsize;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::{Duration, Instant, SystemTime};
|
|
|
|
use async_trait::async_trait;
|
|
use gateway_core::account::ProviderAccountId;
|
|
use gateway_core::engine::{
|
|
AttemptRecord, ExecutionStore, IntermediateFailure, ModelRequestFinalization, ModelRequestId,
|
|
NewModelRequest, ProbeFailure, RecoveryReport,
|
|
};
|
|
use gateway_core::error::{
|
|
OpaqueUpstreamValue, ProviderError, ProviderErrorKind, StoreError, StoreErrorKind,
|
|
};
|
|
use gateway_core::lifecycle::CancellationToken;
|
|
use gateway_core::routing::{ProviderKind, UpstreamModelId};
|
|
use gateway_core::task::{DaemonTask as _, WorkerDaemonContext};
|
|
use gateway_core::upstream::UpstreamSendState;
|
|
use gateway_store::postgres::{BufferedExecutionStore, PgExecutionStore};
|
|
use tokio::sync::{Notify, Semaphore};
|
|
|
|
use super::{
|
|
TestDatabase,
|
|
execution::{accepted_request, early_failure},
|
|
observability_repository,
|
|
};
|
|
|
|
#[derive(Default)]
|
|
struct RecordingStore {
|
|
operations: Mutex<Vec<&'static str>>,
|
|
fail_once: Mutex<Option<&'static str>>,
|
|
}
|
|
|
|
impl RecordingStore {
|
|
fn failing_once(operation: &'static str) -> Self {
|
|
Self {
|
|
operations: Mutex::new(Vec::new()),
|
|
fail_once: Mutex::new(Some(operation)),
|
|
}
|
|
}
|
|
|
|
fn record(&self, operation: &'static str) -> Result<(), StoreError> {
|
|
self.operations
|
|
.lock()
|
|
.expect("operations lock")
|
|
.push(operation);
|
|
let mut fail_once = self.fail_once.lock().expect("failure lock");
|
|
if fail_once.as_ref().is_some_and(|value| *value == operation) {
|
|
*fail_once = None;
|
|
return Err(StoreError::new(StoreErrorKind::Unavailable));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ExecutionStore for RecordingStore {
|
|
async fn create_model_request(&self, _: NewModelRequest) -> Result<(), StoreError> {
|
|
self.record("create")
|
|
}
|
|
|
|
async fn record_attempt(&self, _: AttemptRecord) -> Result<(), StoreError> {
|
|
self.record("attempt")
|
|
}
|
|
|
|
async fn mark_send_state(
|
|
&self,
|
|
_: &ModelRequestId,
|
|
_: UpstreamSendState,
|
|
) -> Result<(), StoreError> {
|
|
self.record("send")
|
|
}
|
|
|
|
async fn mark_downstream_committed(
|
|
&self,
|
|
_: &ModelRequestId,
|
|
_: SystemTime,
|
|
_: Option<u16>,
|
|
) -> Result<(), StoreError> {
|
|
self.record("commit")
|
|
}
|
|
|
|
async fn record_client_status(&self, _: &ModelRequestId, _: u16) -> Result<(), StoreError> {
|
|
self.record("status")
|
|
}
|
|
|
|
async fn record_intermediate_failure(&self, _: IntermediateFailure) -> Result<(), StoreError> {
|
|
self.record("intermediate_failure")
|
|
}
|
|
|
|
async fn record_probe_failure(&self, _: ProbeFailure) -> Result<(), StoreError> {
|
|
self.record("probe_failure")
|
|
}
|
|
|
|
async fn finalize_model_request(&self, _: ModelRequestFinalization) -> Result<(), StoreError> {
|
|
self.record("finalize")
|
|
}
|
|
|
|
async fn recover_expired(&self, _: SystemTime) -> Result<RecoveryReport, StoreError> {
|
|
self.record("recover")?;
|
|
Ok(RecoveryReport::default())
|
|
}
|
|
}
|
|
|
|
struct BlockingStore {
|
|
active: AtomicUsize,
|
|
maximum_active: AtomicUsize,
|
|
started: AtomicUsize,
|
|
started_changed: Notify,
|
|
releases: Semaphore,
|
|
operations: Mutex<Vec<(String, &'static str)>>,
|
|
}
|
|
|
|
impl Default for BlockingStore {
|
|
fn default() -> Self {
|
|
Self {
|
|
active: AtomicUsize::new(0),
|
|
maximum_active: AtomicUsize::new(0),
|
|
started: AtomicUsize::new(0),
|
|
started_changed: Notify::new(),
|
|
releases: Semaphore::new(0),
|
|
operations: Mutex::new(Vec::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ActiveWrite<'a>(&'a AtomicUsize);
|
|
|
|
impl Drop for ActiveWrite<'_> {
|
|
fn drop(&mut self) {
|
|
self.0.fetch_sub(1, Ordering::AcqRel);
|
|
}
|
|
}
|
|
|
|
impl BlockingStore {
|
|
async fn record(&self, request_id: &str, operation: &'static str) -> Result<(), StoreError> {
|
|
self.operations
|
|
.lock()
|
|
.expect("operations lock")
|
|
.push((request_id.to_owned(), operation));
|
|
let active = self.active.fetch_add(1, Ordering::AcqRel) + 1;
|
|
self.maximum_active.fetch_max(active, Ordering::AcqRel);
|
|
let _active = ActiveWrite(&self.active);
|
|
self.started.fetch_add(1, Ordering::AcqRel);
|
|
self.started_changed.notify_waiters();
|
|
self.releases
|
|
.acquire()
|
|
.await
|
|
.expect("release semaphore")
|
|
.forget();
|
|
Ok(())
|
|
}
|
|
|
|
async fn wait_for_started(&self, expected: usize) {
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
loop {
|
|
let changed = self.started_changed.notified();
|
|
if self.started.load(Ordering::Acquire) >= expected {
|
|
break;
|
|
}
|
|
changed.await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("expected writes should start");
|
|
}
|
|
|
|
fn operations_for(&self, request_id: &str) -> Vec<&'static str> {
|
|
self.operations
|
|
.lock()
|
|
.expect("operations lock")
|
|
.iter()
|
|
.filter_map(|(candidate, operation)| (candidate == request_id).then_some(*operation))
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ExecutionStore for BlockingStore {
|
|
async fn create_model_request(&self, request: NewModelRequest) -> Result<(), StoreError> {
|
|
self.record(request.id.as_str(), "create").await
|
|
}
|
|
|
|
async fn record_attempt(&self, attempt: AttemptRecord) -> Result<(), StoreError> {
|
|
self.record(attempt.request_id.as_str(), "attempt").await
|
|
}
|
|
|
|
async fn mark_send_state(
|
|
&self,
|
|
request_id: &ModelRequestId,
|
|
_: UpstreamSendState,
|
|
) -> Result<(), StoreError> {
|
|
self.record(request_id.as_str(), "send").await
|
|
}
|
|
|
|
async fn mark_downstream_committed(
|
|
&self,
|
|
request_id: &ModelRequestId,
|
|
_: SystemTime,
|
|
_: Option<u16>,
|
|
) -> Result<(), StoreError> {
|
|
self.record(request_id.as_str(), "commit").await
|
|
}
|
|
|
|
async fn record_client_status(
|
|
&self,
|
|
request_id: &ModelRequestId,
|
|
_: u16,
|
|
) -> Result<(), StoreError> {
|
|
self.record(request_id.as_str(), "status").await
|
|
}
|
|
|
|
async fn record_intermediate_failure(
|
|
&self,
|
|
failure: IntermediateFailure,
|
|
) -> Result<(), StoreError> {
|
|
self.record(failure.request_id.as_str(), "intermediate_failure")
|
|
.await
|
|
}
|
|
|
|
async fn record_probe_failure(&self, _: ProbeFailure) -> Result<(), StoreError> {
|
|
self.record("probe", "probe_failure").await
|
|
}
|
|
|
|
async fn finalize_model_request(
|
|
&self,
|
|
finalization: ModelRequestFinalization,
|
|
) -> Result<(), StoreError> {
|
|
self.record(finalization.request_id.as_str(), "finalize")
|
|
.await
|
|
}
|
|
|
|
async fn recover_expired(&self, _: SystemTime) -> Result<RecoveryReport, StoreError> {
|
|
Ok(RecoveryReport::default())
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn full_observation_queue_never_waits_for_the_database() {
|
|
let inner = Arc::new(RecordingStore::default());
|
|
let (store, _writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(1).expect("capacity"),
|
|
);
|
|
let request_id = ModelRequestId::new("req_queue_full").expect("request id");
|
|
|
|
tokio::time::timeout(
|
|
Duration::from_millis(50),
|
|
store.mark_send_state(&request_id, UpstreamSendState::Sent),
|
|
)
|
|
.await
|
|
.expect("first enqueue must not wait")
|
|
.expect("first enqueue is fail-open");
|
|
tokio::time::timeout(
|
|
Duration::from_millis(50),
|
|
store.record_client_status(&request_id, 200),
|
|
)
|
|
.await
|
|
.expect("full queue must not wait")
|
|
.expect("full queue is fail-open");
|
|
|
|
assert!(inner.operations.lock().expect("operations lock").is_empty());
|
|
assert_eq!(store.stats().queued_items, 1);
|
|
assert_eq!(store.stats().dropped_total, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn observation_writer_persists_commands_in_enqueue_order() {
|
|
let inner = Arc::new(RecordingStore::default());
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(8).expect("capacity"),
|
|
);
|
|
let request_id = ModelRequestId::new("req_queue_order").expect("request id");
|
|
store
|
|
.mark_send_state(&request_id, UpstreamSendState::Sent)
|
|
.await
|
|
.expect("enqueue send state");
|
|
store
|
|
.record_client_status(&request_id, 200)
|
|
.await
|
|
.expect("enqueue client status");
|
|
|
|
let cancellation = CancellationToken::new();
|
|
let writer = Arc::new(writer);
|
|
let task = tokio::spawn({
|
|
let writer = Arc::clone(&writer);
|
|
let cancellation = cancellation.clone();
|
|
async move { writer.run(WorkerDaemonContext::new(cancellation)).await }
|
|
});
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
loop {
|
|
if inner.operations.lock().expect("operations lock").len() == 2 {
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("writer should drain queued observations");
|
|
cancellation.cancel();
|
|
task.await
|
|
.expect("writer task")
|
|
.expect("writer cancellation");
|
|
|
|
assert_eq!(
|
|
*inner.operations.lock().expect("operations lock"),
|
|
["send", "status"]
|
|
);
|
|
assert_eq!(store.stats().persisted_total, 2);
|
|
assert_eq!(store.stats().queued_items, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn request_affine_lanes_run_independently_and_preserve_each_request_order() {
|
|
let inner = Arc::new(BlockingStore::default());
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(4).expect("capacity"),
|
|
);
|
|
let first = ModelRequestId::new("req_lane_0").expect("first request id");
|
|
let second = ModelRequestId::new("req_lane_1").expect("second request id");
|
|
for request_id in [&first, &second] {
|
|
store
|
|
.mark_send_state(request_id, UpstreamSendState::Sent)
|
|
.await
|
|
.expect("enqueue send state");
|
|
store
|
|
.record_client_status(request_id, 200)
|
|
.await
|
|
.expect("enqueue status");
|
|
}
|
|
|
|
let cancellation = CancellationToken::new();
|
|
let writer = Arc::new(writer);
|
|
let task = tokio::spawn({
|
|
let writer = Arc::clone(&writer);
|
|
let cancellation = cancellation.clone();
|
|
async move { writer.run(WorkerDaemonContext::new(cancellation)).await }
|
|
});
|
|
inner.wait_for_started(2).await;
|
|
assert_eq!(inner.maximum_active.load(Ordering::Acquire), 2);
|
|
assert_eq!(inner.operations_for(first.as_str()), ["send"]);
|
|
assert_eq!(inner.operations_for(second.as_str()), ["send"]);
|
|
|
|
inner.releases.add_permits(2);
|
|
inner.wait_for_started(4).await;
|
|
inner.releases.add_permits(2);
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
while store.stats().persisted_total != 4 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("all writes should persist");
|
|
cancellation.cancel();
|
|
task.await
|
|
.expect("writer task")
|
|
.expect("writer cancellation");
|
|
|
|
assert_eq!(inner.operations_for(first.as_str()), ["send", "status"]);
|
|
assert_eq!(inner.operations_for(second.as_str()), ["send", "status"]);
|
|
assert_eq!(store.stats().dropped_total, 0);
|
|
assert_eq!(store.stats().queued_items, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn idle_wait_includes_a_dequeued_write_until_persistence_finishes() {
|
|
let inner = Arc::new(BlockingStore::default());
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(1).expect("capacity"),
|
|
);
|
|
let request_id = ModelRequestId::new("req_idle_inflight").expect("request id");
|
|
store
|
|
.mark_send_state(&request_id, UpstreamSendState::Sent)
|
|
.await
|
|
.expect("enqueue write");
|
|
let cancellation = CancellationToken::new();
|
|
let writer = Arc::new(writer);
|
|
let task = tokio::spawn({
|
|
let writer = Arc::clone(&writer);
|
|
let cancellation = cancellation.clone();
|
|
async move { writer.run(WorkerDaemonContext::new(cancellation)).await }
|
|
});
|
|
inner.wait_for_started(1).await;
|
|
|
|
let idle = writer.wait_until_idle(Instant::now() + Duration::from_secs(1));
|
|
tokio::pin!(idle);
|
|
assert!(
|
|
tokio::time::timeout(Duration::from_millis(20), &mut idle)
|
|
.await
|
|
.is_err(),
|
|
"从队列取出的写入仍应计入 drain"
|
|
);
|
|
inner.releases.add_permits(1);
|
|
assert!(idle.await);
|
|
|
|
cancellation.cancel();
|
|
task.await
|
|
.expect("writer task")
|
|
.expect("writer cancellation");
|
|
assert_eq!(store.stats().persisted_total, 1);
|
|
assert_eq!(store.stats().queued_items, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn global_item_budget_includes_writes_running_in_every_lane() {
|
|
let inner = Arc::new(BlockingStore::default());
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(2).expect("capacity"),
|
|
);
|
|
let first = ModelRequestId::new("req_lane_0").expect("first request id");
|
|
let second = ModelRequestId::new("req_lane_1").expect("second request id");
|
|
let rejected = ModelRequestId::new("req_lane_2").expect("rejected request id");
|
|
store
|
|
.mark_send_state(&first, UpstreamSendState::Sent)
|
|
.await
|
|
.expect("enqueue first lane");
|
|
store
|
|
.mark_send_state(&second, UpstreamSendState::Sent)
|
|
.await
|
|
.expect("enqueue second lane");
|
|
|
|
let cancellation = CancellationToken::new();
|
|
let writer = Arc::new(writer);
|
|
let task = tokio::spawn({
|
|
let writer = Arc::clone(&writer);
|
|
let cancellation = cancellation.clone();
|
|
async move { writer.run(WorkerDaemonContext::new(cancellation)).await }
|
|
});
|
|
inner.wait_for_started(2).await;
|
|
assert_eq!(store.stats().queued_items, 2);
|
|
store
|
|
.mark_send_state(&rejected, UpstreamSendState::Sent)
|
|
.await
|
|
.expect("full global budget remains fail open");
|
|
assert_eq!(store.stats().dropped_total, 1);
|
|
assert_eq!(store.stats().queued_items, 2);
|
|
|
|
inner.releases.add_permits(2);
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
while store.stats().persisted_total != 2 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("accepted writes should persist");
|
|
cancellation.cancel();
|
|
task.await
|
|
.expect("writer task")
|
|
.expect("writer cancellation");
|
|
assert_eq!(store.stats().queued_items, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn all_lanes_share_one_shutdown_deadline_for_inflight_writes() {
|
|
let inner = Arc::new(BlockingStore::default());
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(4).expect("capacity"),
|
|
);
|
|
for index in 0..4 {
|
|
let request_id = ModelRequestId::new(format!("req_lane_{index}"))
|
|
.expect("request id assigned to a distinct lane");
|
|
store
|
|
.mark_send_state(&request_id, UpstreamSendState::Sent)
|
|
.await
|
|
.expect("enqueue lane write");
|
|
}
|
|
|
|
let cancellation = CancellationToken::new();
|
|
let writer = Arc::new(writer);
|
|
let task = tokio::spawn({
|
|
let writer = Arc::clone(&writer);
|
|
let cancellation = cancellation.clone();
|
|
async move { writer.run(WorkerDaemonContext::new(cancellation)).await }
|
|
});
|
|
inner.wait_for_started(4).await;
|
|
cancellation.cancel();
|
|
tokio::time::timeout(Duration::from_secs(3), task)
|
|
.await
|
|
.expect("four blocked lanes must share the two-second shutdown deadline")
|
|
.expect("writer task")
|
|
.expect("writer cancellation");
|
|
|
|
assert_eq!(store.stats().persisted_total, 0);
|
|
assert_eq!(store.stats().dropped_total, 4);
|
|
assert_eq!(store.stats().queued_items, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn observation_writer_continues_after_a_database_write_failure() {
|
|
let inner = Arc::new(RecordingStore::failing_once("send"));
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(8).expect("capacity"),
|
|
);
|
|
let request_id = ModelRequestId::new("req_queue_failure").expect("request id");
|
|
store
|
|
.mark_send_state(&request_id, UpstreamSendState::Sent)
|
|
.await
|
|
.expect("enqueue failed write");
|
|
store
|
|
.record_client_status(&request_id, 200)
|
|
.await
|
|
.expect("enqueue following write");
|
|
|
|
let cancellation = CancellationToken::new();
|
|
cancellation.cancel();
|
|
writer
|
|
.run(WorkerDaemonContext::new(cancellation))
|
|
.await
|
|
.expect("shutdown drains observations");
|
|
|
|
assert_eq!(
|
|
*inner.operations.lock().expect("operations lock"),
|
|
["send", "status"]
|
|
);
|
|
assert_eq!(store.stats().write_failure_total, 1);
|
|
assert_eq!(store.stats().persisted_total, 1);
|
|
assert_eq!(store.stats().queued_items, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn shutdown_drains_observations_already_accepted_by_the_queue() {
|
|
let inner = Arc::new(RecordingStore::default());
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(8).expect("capacity"),
|
|
);
|
|
let request_id = ModelRequestId::new("req_queue_shutdown").expect("request id");
|
|
store
|
|
.mark_send_state(&request_id, UpstreamSendState::Sent)
|
|
.await
|
|
.expect("enqueue send state");
|
|
store
|
|
.record_client_status(&request_id, 200)
|
|
.await
|
|
.expect("enqueue client status");
|
|
|
|
let cancellation = CancellationToken::new();
|
|
cancellation.cancel();
|
|
writer
|
|
.run(WorkerDaemonContext::new(cancellation))
|
|
.await
|
|
.expect("shutdown drains observations");
|
|
|
|
assert_eq!(
|
|
*inner.operations.lock().expect("operations lock"),
|
|
["send", "status"]
|
|
);
|
|
assert_eq!(store.stats().persisted_total, 2);
|
|
assert_eq!(store.stats().dropped_total, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn observation_byte_budget_drops_payload_without_waiting_for_the_database() {
|
|
for provider_error in [
|
|
ProviderError::new(ProviderErrorKind::Transport, UpstreamSendState::NotSent)
|
|
.with_source(std::io::Error::other("x".repeat(2_048))),
|
|
ProviderError::new(ProviderErrorKind::Transport, UpstreamSendState::NotSent)
|
|
.with_upstream_code(OpaqueUpstreamValue::new("x".repeat(2_048))),
|
|
ProviderError::new(ProviderErrorKind::Transport, UpstreamSendState::NotSent)
|
|
.with_raw_upstream_error(gateway_core::error::RawUpstreamError::new(
|
|
"x".repeat(2_048),
|
|
)),
|
|
ProviderError::new(ProviderErrorKind::Transport, UpstreamSendState::NotSent)
|
|
.with_diagnostic(gateway_core::error::ProviderDiagnostic::new(
|
|
"x".repeat(2_048),
|
|
)),
|
|
] {
|
|
let inner = Arc::new(RecordingStore::default());
|
|
let (store, _writer) = BufferedExecutionStore::with_limits(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(8).expect("capacity"),
|
|
NonZeroUsize::new(1_024).expect("byte capacity"),
|
|
);
|
|
let failure = ProbeFailure {
|
|
provider_kind: ProviderKind::new("openai").expect("provider"),
|
|
account_id: ProviderAccountId::new("acct_byte_budget").expect("account"),
|
|
upstream_model_id: UpstreamModelId::new("gpt-byte-budget").expect("model"),
|
|
error: provider_error,
|
|
latency: Duration::from_millis(1),
|
|
};
|
|
|
|
tokio::time::timeout(
|
|
Duration::from_millis(50),
|
|
store.record_probe_failure(failure),
|
|
)
|
|
.await
|
|
.expect("byte budget must not wait")
|
|
.expect("byte budget is fail-open");
|
|
|
|
assert!(inner.operations.lock().expect("operations lock").is_empty());
|
|
assert_eq!(store.stats().queued_items, 0);
|
|
assert_eq!(store.stats().dropped_total, 1);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn final_error_body_counts_towards_the_observation_byte_budget() {
|
|
let inner = Arc::new(RecordingStore::default());
|
|
let (store, _writer) = BufferedExecutionStore::with_limits(
|
|
Arc::clone(&inner),
|
|
NonZeroUsize::new(8).unwrap(),
|
|
NonZeroUsize::new(1_024).unwrap(),
|
|
);
|
|
let request = accepted_request("req_error_body_budget");
|
|
let mut finalization = early_failure(&request);
|
|
finalization.error_details = Some("x".repeat(2_048));
|
|
store.finalize_model_request(finalization).await.unwrap();
|
|
assert_eq!(store.stats().dropped_total, 1);
|
|
assert_eq!(store.stats().queued_bytes, 0);
|
|
assert!(inner.operations.lock().unwrap().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn zero_attempt_create_and_finalize_drain_in_order_with_trace() {
|
|
let Some(database) = TestDatabase::create("zero_attempt_queue_order").await else {
|
|
return;
|
|
};
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::new(PgExecutionStore::new(database.pool.clone())),
|
|
NonZeroUsize::new(2).expect("capacity"),
|
|
);
|
|
let request = accepted_request("req_zero_attempt_queue_order");
|
|
let finalization = early_failure(&request);
|
|
let expected_trace: serde_json::Value = serde_json::from_str(
|
|
finalization
|
|
.diagnostic_trace_json
|
|
.as_deref()
|
|
.expect("trace"),
|
|
)
|
|
.expect("trace JSON");
|
|
store
|
|
.create_model_request(request.clone())
|
|
.await
|
|
.expect("enqueue create");
|
|
store
|
|
.finalize_model_request(finalization)
|
|
.await
|
|
.expect("enqueue finalization");
|
|
let before: i64 = sqlx::query_scalar("select count(*) from model_requests")
|
|
.fetch_one(&database.pool)
|
|
.await
|
|
.expect("not yet persisted");
|
|
assert_eq!(before, 0);
|
|
assert_eq!(store.stats().queued_items, 2);
|
|
let cancellation = CancellationToken::new();
|
|
cancellation.cancel();
|
|
writer
|
|
.run(WorkerDaemonContext::new(cancellation))
|
|
.await
|
|
.expect("shutdown drains accepted commands");
|
|
let stats = store.stats();
|
|
assert_eq!(stats.persisted_total, 2);
|
|
assert_eq!(stats.write_failure_total, 0);
|
|
assert_eq!(stats.dropped_total, 0);
|
|
assert_eq!(stats.queued_items, 0);
|
|
assert_eq!(stats.queued_bytes, 0);
|
|
let detail = observability_repository(&database.pool)
|
|
.usage_record_detail(request.id.as_str())
|
|
.await
|
|
.expect("queued failure detail");
|
|
assert_eq!(detail.request.outcome.as_str(), "failed");
|
|
assert_eq!(detail.request.attempt_count, 0);
|
|
assert_eq!(detail.request.upstream_send_state, "not_sent");
|
|
assert_eq!(detail.trace, Some(expected_trace));
|
|
assert!(detail.attempts.is_empty());
|
|
database.close().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn request_affine_lanes_persist_a_postgres_lifecycle_burst_without_orphans() {
|
|
const REQUESTS: usize = 256;
|
|
const WRITES: u64 = (REQUESTS * 2) as u64;
|
|
|
|
let Some(database) = TestDatabase::create("execution_lane_burst").await else {
|
|
return;
|
|
};
|
|
let (store, writer) =
|
|
BufferedExecutionStore::new(Arc::new(PgExecutionStore::new(database.pool.clone())));
|
|
for index in 0..REQUESTS {
|
|
let request = accepted_request(&format!("req_execution_lane_burst_{index}"));
|
|
store
|
|
.create_model_request(request.clone())
|
|
.await
|
|
.expect("enqueue create");
|
|
store
|
|
.finalize_model_request(early_failure(&request))
|
|
.await
|
|
.expect("enqueue finalization");
|
|
}
|
|
assert_eq!(store.stats().enqueued_total, WRITES);
|
|
assert_eq!(store.stats().dropped_total, 0);
|
|
|
|
let cancellation = CancellationToken::new();
|
|
let writer = Arc::new(writer);
|
|
let task = tokio::spawn({
|
|
let writer = Arc::clone(&writer);
|
|
let cancellation = cancellation.clone();
|
|
async move { writer.run(WorkerDaemonContext::new(cancellation)).await }
|
|
});
|
|
tokio::time::timeout(Duration::from_secs(15), async {
|
|
while store.stats().persisted_total != WRITES {
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("PostgreSQL burst should drain");
|
|
cancellation.cancel();
|
|
task.await
|
|
.expect("writer task")
|
|
.expect("writer cancellation");
|
|
|
|
let (total, failed): (i64, i64) = sqlx::query_as(
|
|
"select count(*), count(*) filter (where outcome = 'failed')
|
|
from model_requests where id like 'req_execution_lane_burst_%'",
|
|
)
|
|
.fetch_one(&database.pool)
|
|
.await
|
|
.expect("load burst rows");
|
|
assert_eq!((total, failed), (REQUESTS as i64, REQUESTS as i64));
|
|
let stats = store.stats();
|
|
assert_eq!(stats.persisted_total, WRITES);
|
|
assert_eq!(stats.write_failure_total, 0);
|
|
assert_eq!(stats.dropped_total, 0);
|
|
assert_eq!(stats.queued_items, 0);
|
|
assert_eq!(stats.queued_bytes, 0);
|
|
database.close().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn zero_attempt_dropped_finalize_is_fail_open_and_recoverable_at_deadline() {
|
|
let Some(database) = TestDatabase::create("zero_attempt_queue_full").await else {
|
|
return;
|
|
};
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::new(PgExecutionStore::new(database.pool.clone())),
|
|
NonZeroUsize::new(1).expect("capacity"),
|
|
);
|
|
let request = accepted_request("req_zero_attempt_queue_full");
|
|
tokio::time::timeout(Duration::from_secs(1), async {
|
|
store
|
|
.create_model_request(request.clone())
|
|
.await
|
|
.expect("enqueue create");
|
|
store
|
|
.finalize_model_request(early_failure(&request))
|
|
.await
|
|
.expect("full queue is fail open");
|
|
})
|
|
.await
|
|
.expect("no database wait");
|
|
assert_eq!(store.stats().enqueued_total, 1);
|
|
assert_eq!(store.stats().dropped_total, 1);
|
|
let cancellation = CancellationToken::new();
|
|
cancellation.cancel();
|
|
writer
|
|
.run(WorkerDaemonContext::new(cancellation))
|
|
.await
|
|
.expect("drain create only");
|
|
let repository = observability_repository(&database.pool);
|
|
let pending = repository
|
|
.usage_record_detail(request.id.as_str())
|
|
.await
|
|
.expect("pending record");
|
|
assert_eq!(pending.request.outcome.as_str(), "running");
|
|
assert_eq!(pending.trace, None);
|
|
assert!(pending.attempts.is_empty());
|
|
assert_eq!(
|
|
store
|
|
.recover_expired(request.deadline_at.at().unwrap())
|
|
.await
|
|
.expect("recover without queue")
|
|
.requests,
|
|
1
|
|
);
|
|
let recovered = repository
|
|
.usage_record_detail(request.id.as_str())
|
|
.await
|
|
.expect("recovered record");
|
|
assert_eq!(recovered.request.outcome.as_str(), "incomplete");
|
|
assert_eq!(
|
|
recovered.request.error_kind.as_deref(),
|
|
Some("process_interrupted")
|
|
);
|
|
assert_eq!(recovered.request.attempt_count, 0);
|
|
assert_eq!(recovered.trace, None);
|
|
assert!(recovered.attempts.is_empty());
|
|
assert_eq!(
|
|
store
|
|
.recover_expired(request.deadline_at.at().unwrap())
|
|
.await
|
|
.expect("repeat recovery")
|
|
.requests,
|
|
0
|
|
);
|
|
|
|
// 已关闭队列同样不把观测失败返回客户端,也不能覆盖已经恢复的终态
|
|
store
|
|
.finalize_model_request(early_failure(&request))
|
|
.await
|
|
.expect("closed queue is fail open");
|
|
assert_eq!(store.stats().dropped_total, 2);
|
|
assert_eq!(store.stats().persisted_total, 1);
|
|
assert_eq!(store.stats().write_failure_total, 0);
|
|
assert_eq!(
|
|
repository
|
|
.usage_record_detail(request.id.as_str())
|
|
.await
|
|
.expect("unchanged recovery"),
|
|
recovered
|
|
);
|
|
database.close().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn zero_attempt_dropped_create_does_not_make_finalize_an_upsert() {
|
|
let Some(database) = TestDatabase::create("zero_attempt_create_dropped").await else {
|
|
return;
|
|
};
|
|
let (store, writer) = BufferedExecutionStore::with_limits(
|
|
Arc::new(PgExecutionStore::new(database.pool.clone())),
|
|
NonZeroUsize::new(2).expect("capacity"),
|
|
NonZeroUsize::new(8_192).expect("byte budget"),
|
|
);
|
|
let mut request = accepted_request("req_zero_attempt_create_dropped");
|
|
// 合成大 header 只触发 Create 字节预算;Finalize 不携带 user_agent,仍可入队
|
|
request.user_agent = Some("x".repeat(16_384));
|
|
store
|
|
.create_model_request(request.clone())
|
|
.await
|
|
.expect("oversized create is fail open");
|
|
assert_eq!(store.stats().dropped_total, 1);
|
|
store
|
|
.finalize_model_request(early_failure(&request))
|
|
.await
|
|
.expect("enqueue finalization without create");
|
|
assert_eq!(store.stats().enqueued_total, 1);
|
|
let cancellation = CancellationToken::new();
|
|
cancellation.cancel();
|
|
writer
|
|
.run(WorkerDaemonContext::new(cancellation))
|
|
.await
|
|
.expect("drain orphan finalization");
|
|
assert_eq!(store.stats().persisted_total, 0);
|
|
assert_eq!(store.stats().write_failure_total, 1);
|
|
assert_eq!(store.stats().queued_bytes, 0);
|
|
let count: i64 = sqlx::query_scalar("select count(*) from model_requests")
|
|
.fetch_one(&database.pool)
|
|
.await
|
|
.expect("no fabricated row");
|
|
assert_eq!(count, 0);
|
|
assert_eq!(
|
|
store
|
|
.recover_expired(request.deadline_at.at().unwrap())
|
|
.await
|
|
.expect("nothing to recover")
|
|
.requests,
|
|
0
|
|
);
|
|
database.close().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn zero_attempt_postgres_write_failures_are_not_retried_and_do_not_stop_the_queue() {
|
|
let Some(database) = TestDatabase::create("zero_attempt_write_failure").await else {
|
|
return;
|
|
};
|
|
// 只在随机测试 schema 注入语句失败;序列不随语句回滚,用于核对实际写入次数
|
|
sqlx::raw_sql(
|
|
"create sequence zero_attempt_rejected_writes;
|
|
create function reject_zero_attempt_observation() returns trigger language plpgsql as $$
|
|
begin
|
|
if (TG_OP = 'INSERT' and NEW.id = 'req_zero_attempt_create_rejected')
|
|
or (TG_OP = 'UPDATE' and NEW.id = 'req_zero_attempt_finalize_rejected'
|
|
and NEW.outcome = 'failed') then
|
|
perform nextval('zero_attempt_rejected_writes');
|
|
raise exception 'synthetic observation write failure';
|
|
end if;
|
|
return NEW;
|
|
end $$;
|
|
create trigger reject_zero_attempt_observation
|
|
before insert or update on model_requests
|
|
for each row execute function reject_zero_attempt_observation();",
|
|
)
|
|
.execute(&database.pool)
|
|
.await
|
|
.expect("install isolated write failure");
|
|
let (store, writer) = BufferedExecutionStore::with_capacity(
|
|
Arc::new(PgExecutionStore::new(database.pool.clone())),
|
|
NonZeroUsize::new(6).expect("capacity"),
|
|
);
|
|
let request = accepted_request("req_zero_attempt_create_rejected");
|
|
for id in [
|
|
"req_zero_attempt_create_rejected",
|
|
"req_zero_attempt_finalize_rejected",
|
|
"req_zero_attempt_after_rejection",
|
|
] {
|
|
let mut request = request.clone();
|
|
request.id = ModelRequestId::new(id).expect("request id");
|
|
store
|
|
.create_model_request(request.clone())
|
|
.await
|
|
.expect("enqueue create");
|
|
store
|
|
.finalize_model_request(early_failure(&request))
|
|
.await
|
|
.expect("enqueue finalization");
|
|
}
|
|
let cancellation = CancellationToken::new();
|
|
cancellation.cancel();
|
|
writer
|
|
.run(WorkerDaemonContext::new(cancellation))
|
|
.await
|
|
.expect("continue draining after PG errors");
|
|
let stats = store.stats();
|
|
assert_eq!(stats.enqueued_total, 6);
|
|
assert_eq!(stats.persisted_total, 3);
|
|
assert_eq!(stats.write_failure_total, 3);
|
|
assert_eq!(stats.dropped_total, 0);
|
|
assert_eq!(stats.queued_items, 0);
|
|
assert_eq!(stats.queued_bytes, 0);
|
|
let rejected_writes: i64 =
|
|
sqlx::query_scalar("select last_value from zero_attempt_rejected_writes")
|
|
.fetch_one(&database.pool)
|
|
.await
|
|
.expect("count failed PostgreSQL writes without rollback");
|
|
assert_eq!(rejected_writes, 2);
|
|
let rows: Vec<(String, String, Option<serde_json::Value>)> =
|
|
sqlx::query_as("select id, outcome, diagnostic_trace_json from model_requests order by id")
|
|
.fetch_all(&database.pool)
|
|
.await
|
|
.expect("load successfully persisted rows");
|
|
assert_eq!(rows.len(), 2);
|
|
assert_eq!(rows[0].0, "req_zero_attempt_after_rejection");
|
|
assert_eq!(rows[0].1, "failed");
|
|
assert!(rows[0].2.is_some());
|
|
assert_eq!(
|
|
rows[1],
|
|
(
|
|
"req_zero_attempt_finalize_rejected".to_owned(),
|
|
"running".to_owned(),
|
|
None
|
|
)
|
|
);
|
|
assert_eq!(
|
|
store
|
|
.recover_expired(request.deadline_at.at().unwrap())
|
|
.await
|
|
.expect("recover only inserted unfinished row")
|
|
.requests,
|
|
1
|
|
);
|
|
let recovered = observability_repository(&database.pool)
|
|
.usage_record_detail("req_zero_attempt_finalize_rejected")
|
|
.await
|
|
.expect("recovered detail");
|
|
assert_eq!(
|
|
recovered.request.error_kind.as_deref(),
|
|
Some("process_interrupted")
|
|
);
|
|
assert_eq!(recovered.request.attempt_count, 0);
|
|
assert!(recovered.attempts.is_empty());
|
|
assert_eq!(recovered.trace, None);
|
|
database.close().await;
|
|
}
|
|
|
|
#[async_trait]
|
|
impl gateway_core::diagnostics::OperationalDiagnostics for RecordingStore {
|
|
async fn record_failure(
|
|
&self,
|
|
_: gateway_core::diagnostics::OperationalFailure,
|
|
) -> Result<(), StoreError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl gateway_core::diagnostics::OperationalDiagnostics for BlockingStore {
|
|
async fn record_failure(
|
|
&self,
|
|
_: gateway_core::diagnostics::OperationalFailure,
|
|
) -> Result<(), StoreError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn operational_failure_uses_the_bounded_queue_without_creating_a_model_request() {
|
|
use gateway_core::diagnostics::{OperationalDiagnostics as _, OperationalFailure};
|
|
use gateway_core::error::{ErrorDetails, ErrorSource};
|
|
let Some(database) = TestDatabase::create("operational_details").await else {
|
|
return;
|
|
};
|
|
let inner = Arc::new(PgExecutionStore::new(database.pool.clone()));
|
|
let (store, writer) =
|
|
BufferedExecutionStore::with_capacity(inner, NonZeroUsize::new(1).unwrap());
|
|
let mut failure =
|
|
OperationalFailure::new("admin", "http_request", "unavailable", "safe summary");
|
|
failure.correlation_id = Some("admin-request-123".to_owned());
|
|
failure.details = ErrorDetails::capture(
|
|
Some(&ErrorSource::new(std::io::Error::other(
|
|
"PRIVATE_STORE_CAUSE",
|
|
))),
|
|
None,
|
|
false,
|
|
);
|
|
let occurred_at = failure.occurred_at;
|
|
store.record_failure(failure).await.unwrap();
|
|
store
|
|
.record_failure(OperationalFailure::new(
|
|
"worker",
|
|
"run",
|
|
"worker_failed",
|
|
"safe",
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(store.stats().queued_items, 1);
|
|
assert_eq!(store.stats().dropped_total, 1);
|
|
let cancellation = CancellationToken::new();
|
|
cancellation.cancel();
|
|
writer
|
|
.run(WorkerDaemonContext::new(cancellation))
|
|
.await
|
|
.unwrap();
|
|
let row: (
|
|
Option<String>,
|
|
String,
|
|
String,
|
|
chrono::DateTime<chrono::Utc>,
|
|
) = sqlx::query_as(
|
|
"select model_request_id, message, error_details, created_at
|
|
from ops_events where component = 'admin'",
|
|
)
|
|
.fetch_one(&database.pool)
|
|
.await
|
|
.unwrap();
|
|
assert!(row.0.is_none());
|
|
let message: serde_json::Value = serde_json::from_str(&row.1).unwrap();
|
|
assert_eq!(message["correlationId"], "admin-request-123");
|
|
assert!(!row.1.contains("PRIVATE_STORE_CAUSE"));
|
|
assert!(row.2.contains("PRIVATE_STORE_CAUSE"));
|
|
assert_eq!(
|
|
row.3.timestamp_micros(),
|
|
chrono::DateTime::<chrono::Utc>::from(occurred_at).timestamp_micros()
|
|
);
|
|
let requests: i64 = sqlx::query_scalar("select count(*) from model_requests")
|
|
.fetch_one(&database.pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(requests, 0);
|
|
database.close().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn progress_congestion_preserves_failure_reserve_without_waiting() {
|
|
use futures::FutureExt as _;
|
|
use gateway_core::diagnostics::{OperationalDiagnostics as _, OperationalFailure};
|
|
use gateway_core::error::{ErrorDetails, ErrorSource};
|
|
let Some(database) = TestDatabase::create("failure_reserve").await else {
|
|
return;
|
|
};
|
|
let inner = Arc::new(PgExecutionStore::new(database.pool.clone()));
|
|
let mut request_ids = Vec::new();
|
|
for index in 0..4 {
|
|
let request = accepted_request(&format!("req_reserved_failure_{index}"));
|
|
request_ids.push(request.id.clone());
|
|
inner.create_model_request(request).await.unwrap();
|
|
}
|
|
let request_id = &request_ids[0];
|
|
let (store, writer) =
|
|
BufferedExecutionStore::with_capacity(inner, NonZeroUsize::new(4).unwrap());
|
|
// 不启动消费者,确定性地填满普通额度;每次调用必须首次 poll 就返回
|
|
for (request_id, status) in request_ids.iter().zip([200, 201, 202, 203]) {
|
|
store
|
|
.record_client_status(request_id, status)
|
|
.now_or_never()
|
|
.expect("enqueue waited")
|
|
.unwrap();
|
|
}
|
|
let mut failure =
|
|
OperationalFailure::new("admin", "http_request", "unavailable", "safe failure");
|
|
failure.correlation_id = Some(request_id.as_str().to_owned());
|
|
failure.details = ErrorDetails::capture(
|
|
Some(&ErrorSource::new(std::io::Error::other(
|
|
"ORIGINAL_RESERVED_CAUSE",
|
|
))),
|
|
None,
|
|
false,
|
|
);
|
|
store
|
|
.record_failure(failure)
|
|
.now_or_never()
|
|
.expect("failure waited for queue capacity")
|
|
.unwrap();
|
|
assert_eq!(store.stats().queued_items, 5);
|
|
assert_eq!(store.stats().dropped_total, 0);
|
|
store
|
|
.record_client_status(request_id, 204)
|
|
.now_or_never()
|
|
.expect("overflow waited")
|
|
.unwrap();
|
|
assert_eq!(store.stats().queued_items, 5);
|
|
assert_eq!(store.stats().dropped_total, 1);
|
|
let cancelled = CancellationToken::new();
|
|
cancelled.cancel();
|
|
writer
|
|
.run(WorkerDaemonContext::new(cancelled))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(store.stats().persisted_total, 5);
|
|
let detail: String =
|
|
sqlx::query_scalar("select error_details from ops_events where component = 'admin'")
|
|
.fetch_one(&database.pool)
|
|
.await
|
|
.unwrap();
|
|
assert!(detail.contains("ORIGINAL_RESERVED_CAUSE"));
|
|
let status: i32 =
|
|
sqlx::query_scalar("select client_status_code from model_requests where id = $1")
|
|
.bind(request_id.as_str())
|
|
.fetch_one(&database.pool)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(status, 200);
|
|
assert_eq!(store.stats().queued_bytes, 0);
|
|
database.close().await;
|
|
}
|