наблюдаемость: завершить базовый контур Community
Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user