use crate::app::context::AppContext;
use crate::service::worker::sidekiq::app_worker::AppWorkerConfig;
use crate::service::worker::sidekiq::app_worker::DEFAULT_MAX_DURATION;
use async_trait::async_trait;
use serde::Serialize;
use sidekiq::{RedisPool, Worker, WorkerOpts};
use std::marker::PhantomData;
use std::time::Duration;
use tracing::{error, instrument};
pub(crate) struct RoadsterWorker<Args, W>
where
Args: Send + Sync + Serialize + 'static,
W: Worker<Args>,
{
inner: W,
inner_config: Option<AppWorkerConfig>,
context: AppContext,
_args: PhantomData<Args>,
}
impl<Args, W> RoadsterWorker<Args, W>
where
Args: Send + Sync + Serialize,
W: Worker<Args>,
{
pub(crate) fn new(context: &AppContext, inner: W, config: Option<AppWorkerConfig>) -> Self {
Self {
inner,
inner_config: config,
context: context.clone(),
_args: PhantomData,
}
}
}
#[async_trait]
impl<Args, W> Worker<Args> for RoadsterWorker<Args, W>
where
Args: Send + Sync + Serialize,
W: Worker<Args>,
{
fn disable_argument_coercion(&self) -> bool {
self.inner_config
.as_ref()
.and_then(|config| config.disable_argument_coercion)
.unwrap_or_else(|| {
self.context
.config()
.service
.sidekiq
.custom
.app_worker
.disable_argument_coercion
.unwrap_or_else(|| W::disable_argument_coercion(&self.inner))
})
}
fn opts() -> WorkerOpts<Args, Self>
where
Self: Sized,
{
unimplemented!()
}
fn max_retries(&self) -> usize {
self.inner_config
.as_ref()
.and_then(|config| config.max_retries)
.unwrap_or_else(|| {
self.context
.config()
.service
.sidekiq
.custom
.app_worker
.max_retries
.unwrap_or_else(|| W::max_retries(&self.inner))
})
}
fn class_name() -> String
where
Self: Sized,
{
W::class_name()
}
async fn perform_async(_redis: &RedisPool, _args: Args) -> sidekiq::Result<()>
where
Self: Sized,
Args: Send + Sync + Serialize + 'static,
{
unimplemented!()
}
async fn perform_in(_redis: &RedisPool, _duration: Duration, _args: Args) -> sidekiq::Result<()>
where
Self: Sized,
Args: Send + Sync + Serialize + 'static,
{
unimplemented!()
}
#[instrument(skip_all)]
async fn perform(&self, args: Args) -> sidekiq::Result<()> {
let inner = self.inner.perform(args);
let timeout = self
.inner_config
.as_ref()
.and_then(|config| config.timeout)
.unwrap_or_else(|| {
self.context
.config()
.service
.sidekiq
.custom
.app_worker
.timeout
.unwrap_or_default()
});
if timeout {
let max_duration = self
.inner_config
.as_ref()
.and_then(|config| config.max_duration)
.unwrap_or_else(|| {
self.context
.config()
.service
.sidekiq
.custom
.app_worker
.max_duration
.unwrap_or(DEFAULT_MAX_DURATION)
});
tokio::time::timeout(max_duration, inner)
.await
.map_err(|err| {
error!(
worker = %W::class_name(),
max_duration = %max_duration.as_secs(),
%err,
"Worker timed out"
);
sidekiq::Error::Any(Box::new(err))
})?
} else {
inner.await
}
}
}
#[cfg(test)]
mod tests {
use crate::app::context::AppContext;
use crate::config::AppConfig;
use crate::service::worker::sidekiq::app_worker::AppWorkerConfig;
use crate::service::worker::sidekiq::roadster_worker::RoadsterWorker;
use crate::testing::snapshot::TestCase;
use async_trait::async_trait;
use rstest::{fixture, rstest};
use sidekiq::Worker;
use std::time::Duration;
use tokio::time::sleep;
use typed_builder::TypedBuilder;
#[derive(Debug, TypedBuilder)]
struct ExampleWorker {
#[builder(default)]
disable_argument_coercion: bool,
#[builder(default)]
max_retries: usize,
#[builder(default)]
duration: Option<Duration>,
}
#[async_trait]
impl Worker<()> for ExampleWorker {
fn disable_argument_coercion(&self) -> bool {
self.disable_argument_coercion
}
fn max_retries(&self) -> usize {
self.max_retries
}
async fn perform(&self, _args: ()) -> sidekiq::Result<()> {
sleep(self.duration.unwrap_or_default()).await;
Ok(())
}
}
#[fixture]
#[cfg_attr(coverage_nightly, coverage(off))]
fn case() -> TestCase {
Default::default()
}
#[rstest]
#[case(None, None, false, false)]
#[case(None, None, true, true)]
#[case(None, Some(false), false, false)]
#[case(None, Some(true), false, true)]
#[case(None, Some(false), true, false)]
#[case(Some(false), None, false, false)]
#[case(Some(true), None, false, true)]
#[case(Some(false), None, true, false)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn disable_argument_coercion(
_case: TestCase,
#[case] config_disable: Option<bool>,
#[case] worker_config_disable: Option<bool>,
#[case] worker_field_disable: bool,
#[case] expected: bool,
) {
let mut config = AppConfig::test(None).unwrap();
config
.service
.sidekiq
.custom
.app_worker
.disable_argument_coercion = config_disable;
let context = AppContext::test(Some(config), None, None).unwrap();
let app_worker_config = worker_config_disable.map(|disable| {
AppWorkerConfig::builder()
.disable_argument_coercion(disable)
.build()
});
let example_worker = ExampleWorker::builder()
.disable_argument_coercion(worker_field_disable)
.build();
let roadster_worker = RoadsterWorker::new(&context, example_worker, app_worker_config);
let disable_argument_coercion = roadster_worker.disable_argument_coercion();
assert_eq!(disable_argument_coercion, expected);
}
#[rstest]
#[case(None, None, 0, 0)]
#[case(None, None, 1, 1)]
#[case(None, Some(1), 0, 1)]
#[case(Some(1), None, 0, 1)]
#[case(Some(1), None, 0, 1)]
#[cfg_attr(coverage_nightly, coverage(off))]
fn max_retries(
_case: TestCase,
#[case] config_max_retries: Option<usize>,
#[case] worker_config_max_retries: Option<usize>,
#[case] worker_field_max_retries: usize,
#[case] expected: usize,
) {
let mut config = AppConfig::test(None).unwrap();
config.service.sidekiq.custom.app_worker.max_retries = config_max_retries;
let context = AppContext::test(Some(config), None, None).unwrap();
let app_worker_config = worker_config_max_retries
.map(|max_retries| AppWorkerConfig::builder().max_retries(max_retries).build());
let example_worker = ExampleWorker::builder()
.max_retries(worker_field_max_retries)
.build();
let roadster_worker = RoadsterWorker::new(&context, example_worker, app_worker_config);
let max_retries = roadster_worker.max_retries();
assert_eq!(max_retries, expected);
}
#[test]
#[should_panic]
#[cfg_attr(coverage_nightly, coverage(off))]
fn opts() {
let _ = RoadsterWorker::<(), ExampleWorker>::opts();
}
#[test]
#[cfg_attr(coverage_nightly, coverage(off))]
fn class_name() {
assert_eq!(
RoadsterWorker::<(), ExampleWorker>::class_name(),
ExampleWorker::class_name()
);
}
#[tokio::test]
#[should_panic]
#[cfg_attr(coverage_nightly, coverage(off))]
async fn perform_async() {
let context = AppContext::test(None, None, None).unwrap();
let _ =
RoadsterWorker::<(), ExampleWorker>::perform_async(context.redis_enqueue(), ()).await;
}
#[tokio::test]
#[should_panic]
#[cfg_attr(coverage_nightly, coverage(off))]
async fn perform_in() {
let context = AppContext::test(None, None, None).unwrap();
let _ = RoadsterWorker::<(), ExampleWorker>::perform_in(
context.redis_enqueue(),
Duration::from_secs(1),
(),
)
.await;
}
#[rstest]
#[case(None, None, None, true)]
#[case(None, None, Some(Duration::from_secs(2)), true)]
#[case(None, Some(true), None, true)]
#[case(Some(true), None, None, true)]
#[case(None, Some(true), Some(Duration::from_secs(2)), false)]
#[case(Some(true), None, Some(Duration::from_secs(2)), false)]
#[case(Some(false), Some(true), Some(Duration::from_secs(2)), false)]
#[case(Some(true), Some(false), Some(Duration::from_secs(2)), true)]
#[tokio::test]
#[cfg_attr(coverage_nightly, coverage(off))]
async fn perform(
_case: TestCase,
#[case] config_timeout: Option<bool>,
#[case] worker_config_timeout: Option<bool>,
#[case] worker_duration: Option<Duration>,
#[case] success: bool,
) {
let mut config = AppConfig::test(None).unwrap();
config.service.sidekiq.custom.app_worker.timeout = config_timeout;
config.service.sidekiq.custom.app_worker.max_duration = Some(Duration::from_secs(1));
let context = AppContext::test(Some(config), None, None).unwrap();
let app_worker_config = worker_config_timeout
.map(|timeout| AppWorkerConfig::builder().timeout(timeout).build());
let example_worker = ExampleWorker::builder().duration(worker_duration).build();
let roadster_worker = RoadsterWorker::new(&context, example_worker, app_worker_config);
let result = roadster_worker.perform(()).await;
assert_eq!(result.is_ok(), success);
}
}