Files
mytheclipse/crates/mytheclipse-queue/src/in_memory.rs
T
asepharyana 8106f8943e feat: add 4 new crates (queue, tracing, http, cli) + enhancements to existing crates
New crates:
- mytheclipse-queue: unified job queue with WorkerPool, retry/backoff, DLQ
  Backends: in-memory (default), Redis, NATS, PostgreSQL
- mytheclipse-tracing: tracing subscriber layers with env filter + OTLP/Jaeger export
- mytheclipse-http: HTTP client/server with timeout + tracing, axum server
- mytheclipse-cli: CLI framework with clap derive, built-in subcommands

Enhancements to existing crates:
- mytheclipse-core: SemaphorePool, LeaderElection, HealthRegistry/HealthCheck
- mytheclipse-cache: AutoRefreshCache (bg refresh on miss), CacheMetrics
- mytheclipse-config: ConfigSchema for JSON Schema generation
- mytheclipse-storage: MultipartUploadDriver trait
- mytheclipse-crypto: PASETO v4.local token support

All features compile with --all-features; tests + clippy pass clean.
2026-08-29 16:05:02 +07:00

163 lines
4.5 KiB
Rust

//! In-process job queue using `tokio::sync::Mutex` + `Notify`.
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use tokio::sync::{Mutex, Notify};
use crate::error::{JobError, QueueError};
use crate::job::{Job, JobId};
use crate::traits::Queue;
struct TopicQueue {
jobs: Mutex<Vec<Job>>,
notify: Notify,
}
impl std::fmt::Debug for TopicQueue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TopicQueue")
.field("jobs_len", &self.jobs.try_lock().map(|j| j.len()).unwrap_or(0))
.finish()
}
}
struct Inner {
topics: std::collections::HashMap<String, Arc<TopicQueue>>,
}
impl std::fmt::Debug for Inner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let keys: Vec<&String> = self.topics.keys().collect();
f.debug_struct("Inner").field("topics", &keys).finish()
}
}
/// An in-memory queue. Each topic is a shared mutex-protected vector + Notify.
#[derive(Clone)]
pub struct InMemoryQueue {
inner: Arc<Mutex<Inner>>,
}
impl std::fmt::Debug for InMemoryQueue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InMemoryQueue").finish()
}
}
impl Default for InMemoryQueue {
fn default() -> Self {
Self::new()
}
}
impl InMemoryQueue {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(Inner {
topics: std::collections::HashMap::new(),
})),
}
}
async fn get_topic(&self, topic: &str) -> Arc<TopicQueue> {
let mut inner = self.inner.lock().await;
inner
.topics
.entry(topic.to_string())
.or_insert_with(|| {
Arc::new(TopicQueue {
jobs: Mutex::new(Vec::new()),
notify: Notify::new(),
})
})
.clone()
}
}
#[async_trait]
impl Queue for InMemoryQueue {
async fn enqueue(&self, topic: &str, payload: Vec<u8>) -> Result<(), QueueError> {
let tq = self.get_topic(topic).await;
tq.jobs.lock().await.push(Job::new(JobId::generate(), topic, payload));
tq.notify.notify_one();
Ok(())
}
async fn dequeue(&self, topic: &str, timeout: Duration) -> Result<Option<Job>, QueueError> {
let tq = self.get_topic(topic).await;
let tq2 = Arc::clone(&tq);
loop {
if let Some(job) = tq.jobs.lock().await.pop() {
return Ok(Some(job));
}
tokio::select! {
_ = tq2.notify.notified() => {}
_ = tokio::time::sleep(timeout) => {
if let Some(job) = tq.jobs.lock().await.pop() {
return Ok(Some(job));
}
return Ok(None);
}
}
}
}
async fn ack(&self, _job: &Job) -> Result<(), JobError> {
Ok(())
}
async fn nack(&self, job: &Job, requeue: bool) -> Result<(), JobError> {
if requeue {
self.enqueue(&job.topic, job.payload.clone())
.await
.map_err(|_| JobError::AckFailed("requeue failed".into()))?;
}
Ok(())
}
async fn dlq_move(&self, topic: &str, job: Job) -> Result<(), QueueError> {
let dlq_topic = format!("dlq:{topic}");
let tq = self.get_topic(&dlq_topic).await;
tq.jobs.lock().await.push(job);
tq.notify.notify_one();
Ok(())
}
async fn len(&self, topic: &str) -> Result<u64, QueueError> {
let tq = self.get_topic(topic).await;
let guard = tq.jobs.lock().await;
Ok(guard.len() as u64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn enqueue_dequeue_roundtrip() {
let q = InMemoryQueue::new();
q.enqueue("test", b"hello".to_vec()).await.unwrap();
let job = q.dequeue("test", Duration::from_millis(500)).await.unwrap().unwrap();
assert_eq!(job.payload, b"hello");
assert_eq!(job.topic, "test");
}
#[tokio::test]
async fn empty_returns_none() {
let q = InMemoryQueue::new();
let result = q.dequeue("none", Duration::from_millis(50)).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn len_tracks_jobs() {
let q = InMemoryQueue::new();
q.enqueue("t", vec![1]).await.unwrap();
q.enqueue("t", vec![2]).await.unwrap();
assert_eq!(q.len("t").await.unwrap(), 2);
}
}