Files
mailks/status.rs
T
zhenyi c4824ef261 feat(k8s): add Kubernetes Helm chart for emailks service
- Create Helm chart structure with Chart.yaml and values.yaml
- Add deployment template with container configuration and environment variables
- Implement service template for gRPC port exposure
- Add service account template with security configuration
- Include horizontal pod autoscaler template for scaling capabilities
- Add helper templates for naming and label management
- Configure SMTP settings as configurable parameters in values.yaml
- Set up resource limits and requests for container performance
- Implement liveness and readiness probes for health checks
- Add support for existing secrets and custom configurations
2026-06-07 22:59:06 +08:00

109 lines
2.8 KiB
Rust

use std::{
collections::HashMap,
sync::{Arc, RwLock},
time::{Duration, Instant},
};
use tracing;
use crate::pb::email::v1::SendStatus;
const STATUS_TTL: Duration = Duration::from_secs(24 * 60 * 60);
const MAX_STATUS_ENTRIES: usize = 10_000;
#[derive(Debug, Clone, Default)]
pub struct JobStatusStore {
inner: Arc<RwLock<HashMap<u64, JobStatusEntry>>>,
}
#[derive(Debug, Clone)]
pub struct JobStatusEntry {
pub status: SendStatus,
pub error: Option<String>,
updated_at: Instant,
}
impl JobStatusStore {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn set_queued(&self, id: u64) {
self.write(id, SendStatus::Queued, None);
}
pub fn set_sending(&self, id: u64) {
self.write(id, SendStatus::Sending, None);
}
pub fn set_sent(&self, id: u64) {
self.write(id, SendStatus::Sent, None);
}
pub fn set_failed(&self, id: u64, error: String) {
self.write(id, SendStatus::Failed, Some(error));
}
pub fn get(&self, id: u64) -> Option<JobStatusEntry> {
let guard = match self.inner.read() {
Ok(g) => g,
Err(poisoned) => {
tracing::error!("JobStatusStore read lock poisoned, recovering");
poisoned.into_inner()
}
};
guard.get(&id).cloned()
}
pub fn remove(&self, id: u64) {
let mut guard = match self.inner.write() {
Ok(g) => g,
Err(poisoned) => {
tracing::error!("JobStatusStore write lock poisoned, recovering");
poisoned.into_inner()
}
};
guard.remove(&id);
}
fn write(&self, id: u64, status: SendStatus, error: Option<String>) {
let mut guard = match self.inner.write() {
Ok(g) => g,
Err(poisoned) => {
tracing::error!("JobStatusStore write lock poisoned, recovering");
poisoned.into_inner()
}
};
prune_statuses(&mut guard);
guard.insert(
id,
JobStatusEntry {
status,
error,
updated_at: Instant::now(),
},
);
}
}
fn prune_statuses(entries: &mut HashMap<u64, JobStatusEntry>) {
let now = Instant::now();
entries.retain(|_, entry| now.duration_since(entry.updated_at) <= STATUS_TTL);
// Evict at most one entry per write to avoid O(n²) behaviour under load.
// Repeated writes will gradually shrink the map if it stays above capacity.
if entries.len() >= MAX_STATUS_ENTRIES {
if let Some(oldest_id) = entries
.iter()
.min_by_key(|(_, entry)| entry.updated_at)
.map(|(id, _)| *id)
{
entries.remove(&oldest_id);
}
}
}