Background services
Start workers managed by the HTTP host and open a separate DI scope for each unit of work.
Work registered with add_background_service belongs to the application lifecycle. When a unit of work uses Scoped services, create a new scope for each execution.
Define a worker#
The http-api feature of lilyrs provides both the worker contract and DI types. Also add tokio (macros, rt-multi-thread, time):
use std::{sync::Arc, time::Duration};
use lilyrs::http_api::{
ApplicationScopeFactory, BackgroundServiceTrait, ExecutionCancellation,
InjectionError, ProcessContext, ServiceTrait,
};
use lilyrs::http_api::Injectable;
#[derive(Injectable)]
#[service(lifetime = "Scoped")]
struct JobService;
impl ServiceTrait for JobService {}
impl JobService {
async fn tick(&self, stopping: ExecutionCancellation) -> Result<(), InjectionError> {
if stopping.is_cancelled() { return Ok(()); }
Ok(()) // One unit of application work runs here.
}
}
pub struct PollingWorker { scopes: Arc<ApplicationScopeFactory> }
#[lilyrs::http_api::async_trait::async_trait]
impl BackgroundServiceTrait for PollingWorker {
type Error = InjectionError;
async fn new(scopes: Arc<ApplicationScopeFactory>) -> Result<Self, Self::Error> {
Ok(Self { scopes })
}
async fn execute_async(&mut self, stopping: ExecutionCancellation) -> Result<(), Self::Error> {
while !stopping.is_cancelled() {
let cancellation = stopping.clone();
self.scopes.create_scope(ProcessContext::new())?
.run(move |extensions| Box::pin(async move {
let job = extensions.get_service::<JobService>(None).await?;
job.tick(cancellation).await
})).await?;
tokio::select! {
_ = stopping.cancelled() => break,
_ = tokio::time::sleep(Duration::from_secs(1)) => {}
}
}
Ok(())
}
}tick marks the unit of work here; your application supplies actual polling or processing code. The scope's run result is awaited. A Scoped service is not carried into the next tick or into a detached task.
Register with the host#
Add .add_background_service::<PollingWorker>() to the builder chain. The worker itself does not have to be Injectable. Repeated registrations of the same worker type are deduplicated.
Runtime behavior#
new runs during build, and execute_async runs once after a successful listener bind. A normal Ok(()) does not shut down the host. An uncaught error or panic initiates host shutdown.
Automatic retry, restart, cron scheduling, or distributed leader election are not part of this API's contract. Explicitly implement the required work policy inside the worker.
Shutdown#
The worker's stopping view belongs to host shutdown; it differs from normal request cancellation. The loop must observe the signal and finish its work. Your own tokio::spawn calls do not automatically join this registered worker lifecycle.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.