наблюдаемость: завершить базовый контур Community
CI / Rust Checks (push) Failing after 4m28s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Community Image Smoke (push) Has been skipped
CI / Deploy (push) Has been skipped

Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
This commit is contained in:
2026-07-31 01:01:14 +03:00
parent 99bd05c145
commit 0e8f1ca03a
160 changed files with 13506 additions and 1499 deletions
+145 -15
View File
@@ -1,9 +1,14 @@
use std::time::Duration;
use std::{
ffi::OsString,
sync::{Mutex, MutexGuard},
time::Duration,
};
use crank_core::{
CacheBackend, CacheScope, CacheStoreError, CachedHeader, CachedResponse,
CoordinationStateStore, CoordinationStateValue, RateLimitBucketState, RateLimitStateStore,
ReplayGuardStatus, ReplayGuardStore, ResponseCacheStore,
CoordinationStateReservation, CoordinationStateStore, CoordinationStateValue,
RateLimitBucketState, RateLimitStateStore, ReplayGuardStatus, ReplayGuardStore,
ResponseCacheStore,
};
use crank_runtime::{
InMemoryCoordinationStateStore, InMemoryRateLimitStateStore, InMemoryReplayGuardStore,
@@ -12,6 +17,13 @@ use crank_runtime::{
};
use serde_json::json;
const CACHE_ENV_NAMES: [&str; 3] = [
"CRANK_CACHE_BACKEND",
"CRANK_CACHE_URL",
"CRANK_CACHE_DEFAULT_TTL_MS",
];
static CACHE_ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn defaults_to_in_memory_cache_without_url() {
let config = RuntimeCacheConfig::default();
@@ -21,8 +33,24 @@ fn defaults_to_in_memory_cache_without_url() {
assert_eq!(config.default_ttl_ms, None);
}
#[test]
fn treats_blank_optional_cache_values_as_unset() {
let _env = IsolatedCacheEnv::new();
unsafe {
std::env::set_var("CRANK_CACHE_URL", " ");
std::env::set_var("CRANK_CACHE_DEFAULT_TTL_MS", " ");
}
let config = RuntimeCacheConfig::from_env().unwrap();
assert_eq!(config.backend, CacheBackend::Memory);
assert_eq!(config.url, None);
assert_eq!(config.default_ttl_ms, None);
}
#[test]
fn loads_valkey_config_from_env() {
let _env = IsolatedCacheEnv::new();
unsafe {
std::env::set_var("CRANK_CACHE_BACKEND", "valkey");
std::env::set_var("CRANK_CACHE_URL", "redis://cache:6379/0");
@@ -34,16 +62,11 @@ fn loads_valkey_config_from_env() {
assert_eq!(config.backend, CacheBackend::Valkey);
assert_eq!(config.url.as_deref(), Some("redis://cache:6379/0"));
assert_eq!(config.default_ttl_ms, Some(15_000));
unsafe {
std::env::remove_var("CRANK_CACHE_BACKEND");
std::env::remove_var("CRANK_CACHE_URL");
std::env::remove_var("CRANK_CACHE_DEFAULT_TTL_MS");
}
}
#[test]
fn rejects_external_backend_without_url() {
let _env = IsolatedCacheEnv::new();
unsafe {
std::env::set_var("CRANK_CACHE_BACKEND", "redis");
std::env::remove_var("CRANK_CACHE_URL");
@@ -57,14 +80,11 @@ fn rejects_external_backend_without_url() {
backend: CacheBackend::Redis
}
));
unsafe {
std::env::remove_var("CRANK_CACHE_BACKEND");
}
}
#[test]
fn rejects_zero_ttl() {
let _env = IsolatedCacheEnv::new();
unsafe {
std::env::set_var("CRANK_CACHE_DEFAULT_TTL_MS", "0");
}
@@ -77,9 +97,44 @@ fn rejects_zero_ttl() {
name: "CRANK_CACHE_DEFAULT_TTL_MS"
}
));
}
unsafe {
std::env::remove_var("CRANK_CACHE_DEFAULT_TTL_MS");
struct IsolatedCacheEnv {
_lock: MutexGuard<'static, ()>,
previous: Vec<(&'static str, Option<OsString>)>,
}
impl IsolatedCacheEnv {
fn new() -> Self {
let lock = CACHE_ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let previous = CACHE_ENV_NAMES
.iter()
.map(|name| (*name, std::env::var_os(name)))
.collect();
for name in CACHE_ENV_NAMES {
unsafe {
std::env::remove_var(name);
}
}
Self {
_lock: lock,
previous,
}
}
}
impl Drop for IsolatedCacheEnv {
fn drop(&mut self) {
for (name, value) in &self.previous {
unsafe {
match value {
Some(value) => std::env::set_var(name, value),
None => std::env::remove_var(name),
}
}
}
}
}
@@ -209,6 +264,81 @@ async fn in_memory_coordination_store_scopes_keys() {
);
}
#[tokio::test]
async fn in_memory_coordination_store_atomically_takes_and_reserves_values() {
let store = InMemoryCoordinationStateStore::default();
let value = CoordinationStateValue {
payload: json!({ "state": "pending" }),
};
store
.put_value(
CacheScope::Coordination,
"atomic-job",
value.clone(),
Duration::from_secs(20),
)
.await
.unwrap();
let (first, second) = tokio::join!(
store.take_value(CacheScope::Coordination, "atomic-job"),
store.take_value(CacheScope::Coordination, "atomic-job")
);
assert_eq!(
usize::from(first.unwrap().is_some()) + usize::from(second.unwrap().is_some()),
1
);
assert_eq!(
store
.reserve_value(
CacheScope::Coordination,
"reservation",
value.clone(),
Duration::from_secs(20),
)
.await
.unwrap(),
CoordinationStateReservation::Reserved
);
assert_eq!(
store
.reserve_value(
CacheScope::Coordination,
"reservation",
CoordinationStateValue {
payload: json!({ "state": "other" }),
},
Duration::from_secs(20),
)
.await
.unwrap(),
CoordinationStateReservation::Existing(value.clone())
);
let completed = CoordinationStateValue {
payload: json!({ "state": "completed" }),
};
assert!(
store
.compare_and_set_value(
CacheScope::Coordination,
"reservation",
&value,
completed.clone(),
Duration::from_secs(20),
)
.await
.unwrap()
);
assert_eq!(
store
.get_value(CacheScope::Coordination, "reservation")
.await
.unwrap(),
Some(completed)
);
}
#[tokio::test]
async fn in_memory_stores_reject_empty_keys() {
let response_store = InMemoryResponseCacheStore::default();