use std::time::Duration;
use ruststream_fred::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "cache.warmups")]
struct Warmup {
page: String,
}
#[subscriber(RedisList::new("cache.warmups"))]
async fn warm_cache(warmup: &Warmup) -> HandlerOutcome {
tracing::info!(warmup.page, "page cached");
HandlerOutcome::ack()
}
const MIN_IDLE: Duration = Duration::from_secs(60);
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
struct ThumbnailJob {
image: String,
}
#[subscriber(
RedisList::new("{thumbnails}")
.reliable()
.min_idle(MIN_IDLE)
.recovery_zset("{thumbnails}.claims")
)]
async fn render(job: &ThumbnailJob) -> HandlerOutcome {
tracing::info!(job.image, "thumbnail rendered");
HandlerOutcome::ack()
}
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "uploads")]
struct Upload {
image: String,
}
#[subscriber(RedisStream::new("uploads").group("thumbnailer"), reply("{thumbnails}"))]
async fn queue_thumbnail(upload: &Upload) -> ThumbnailJob {
ThumbnailJob {
image: upload.image.clone(),
}
}
#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("images", "0.1.0")).with_broker(
RedisBroker::standalone("redis://localhost:6379"),
|b| {
b.include(warm_cache);
b.include(render);
b.include(queue_thumbnail)
.out_reply(list::Publish::new().ttl(Duration::from_hours(24)));
},
)
}
#[cfg(test)]
mod tests {
use std::error::Error;
use ruststream::testing::TestApp;
use ruststream_fred::prelude::*;
use super::{MIN_IDLE, ThumbnailJob, Upload, Warmup, app};
#[tokio::test]
async fn a_warmup_is_handled_once() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
let warmup = Warmup {
page: "/pricing".to_owned(),
};
tb.broker::<RedisBroker>()
.message(&warmup)
.publish()
.await?;
tb.broker::<RedisBroker>()
.subscriber("cache.warmups")
.assert_called_once()
.with(&warmup)
.settled(HandlerOutcome::ack());
tb.shutdown().await?;
Ok(())
}
#[tokio::test]
async fn an_upload_queues_a_job_that_is_rendered() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.broker::<RedisBroker>()
.message(&Upload {
image: "cat.png".to_owned(),
})
.publish()
.await?;
let job = ThumbnailJob {
image: "cat.png".to_owned(),
};
tb.broker::<RedisBroker>()
.published::<ThumbnailJob>("{thumbnails}")
.assert_called_once()
.with(&job);
tb.broker::<RedisBroker>()
.subscriber("{thumbnails}")
.assert_called_once()
.with(&job)
.settled(HandlerOutcome::ack());
tb.shutdown().await?;
Ok(())
}
#[tokio::test(start_paused = true)]
async fn a_rendered_job_is_not_recovered() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(app()).await?;
tb.broker::<RedisBroker>()
.message(&ThumbnailJob {
image: "dog.png".to_owned(),
})
.to("{thumbnails}")
.publish()
.await?;
tb.advance(MIN_IDLE * 2).await?;
tb.broker::<RedisBroker>()
.subscriber("{thumbnails}")
.assert_called_once()
.settled(HandlerOutcome::ack());
tb.shutdown().await?;
Ok(())
}
}