├── .gitignore ├── examples ├── Procfile ├── namespaced_demo.rs ├── unique.rs ├── consumer-demo.rs ├── demo.rs └── producer-demo.rs ├── .github └── workflows │ └── rust.yml ├── LICENSE.md ├── Cargo.toml ├── src ├── scheduled.rs ├── stats.rs ├── periodic.rs ├── redis.rs ├── middleware.rs ├── processor.rs └── lib.rs ├── tests ├── process_unique_job_test.rs ├── process_async_test.rs ├── process_scheduled_job_test.rs ├── process_cron_job_test.rs └── server_middleware_test.rs ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /examples/Procfile: -------------------------------------------------------------------------------- 1 | worker1: cargo run --offline --release --example demo 2 | worker2: cargo run --offline --release --example demo 3 | worker3: cargo run --offline --release --example demo 4 | worker4: cargo run --offline --release --example demo 5 | worker5: cargo run --offline --release --example demo 6 | worker6: cargo run --offline --release --example demo 7 | worker7: cargo run --offline --release --example demo 8 | worker8: cargo run --offline --release --example demo 9 | worker9: cargo run --offline --release --example demo 10 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Start Redis 20 | uses: supercharge/redis-github-action@1.4.0 21 | with: 22 | redis-version: 6 23 | - name: Build 24 | run: cargo build --verbose 25 | - name: Run tests 26 | run: cargo test --verbose 27 | - name: Clippy 28 | run: cargo clippy -- -D warnings 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Garrett Thornburg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/namespaced_demo.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use bb8::Pool; 3 | use sidekiq::{Processor, RedisConnectionManager, Result, Worker}; 4 | 5 | #[derive(Clone)] 6 | struct HelloWorker; 7 | 8 | #[async_trait] 9 | impl Worker<()> for HelloWorker { 10 | async fn perform(&self, _args: ()) -> Result<()> { 11 | println!("Hello, world!"); 12 | 13 | Ok(()) 14 | } 15 | } 16 | 17 | #[tokio::main] 18 | async fn main() -> Result<()> { 19 | tracing_subscriber::fmt::init(); 20 | 21 | // Redis 22 | let manager = RedisConnectionManager::new("redis://127.0.0.1/")?; 23 | let redis = Pool::builder() 24 | .max_size(100) 25 | .connection_customizer(sidekiq::with_custom_namespace("yolo_app".to_string())) 26 | .build(manager) 27 | .await?; 28 | 29 | tokio::spawn({ 30 | let redis = redis.clone(); 31 | 32 | async move { 33 | loop { 34 | HelloWorker::perform_async(&redis, ()).await.unwrap(); 35 | 36 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 37 | } 38 | } 39 | }); 40 | 41 | // Sidekiq server 42 | let mut p = Processor::new(redis.clone(), vec!["default".to_string()]); 43 | 44 | // Add known workers 45 | p.register(HelloWorker); 46 | 47 | // Start! 48 | p.run().await; 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusty-sidekiq" 3 | version = "0.13.2" 4 | edition = "2021" 5 | description = "A rust sidekiq server and client using tokio" 6 | authors = ["Garrett Thornburg "] 7 | homepage = "https://github.com/film42/sidekiq-rs" 8 | repository = "https://github.com/film42/sidekiq-rs.git" 9 | keywords = ["sidekiq", "worker", "tokio", "ruby"] 10 | license = "MIT" 11 | readme = "README.md" 12 | 13 | [features] 14 | default = ["rss-stats"] 15 | 16 | # There is currently a dependency bug for mac in the `darwin-libproc` crate making it so 17 | # cargo cannot resolve deps. Once resolved we may choose to remove this feature flag. 18 | rss-stats = ["dep:simple-process-stats"] 19 | 20 | [lib] 21 | name = "sidekiq" 22 | 23 | [dependencies] 24 | gethostname = "0.5.0" 25 | tokio = { version = "1", features = ["full"] } 26 | tokio-util = "0.7.10" 27 | serde_json = { version = "1" } 28 | serde = { version = "1.0", features = ["derive"] } 29 | redis = { version = "0.28", features = ["aio", "default", "tokio-comp"] } 30 | async-trait = "0.1.74" 31 | slog-term = "2.9" 32 | thiserror = "2.0.0" 33 | bb8 = "0.9.0" 34 | num_cpus = "1.13" 35 | chrono = "0.4" 36 | rand = "0.8" 37 | hex = "0.4" 38 | cron_clock = "0.8.0" 39 | simple-process-stats = { version = "1.0.0", optional = true } 40 | sha2 = "0.10.6" 41 | convert_case = "0.7.1" 42 | 43 | tracing = "0.1.40" 44 | tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] } 45 | serial_test = "3.1.1" 46 | -------------------------------------------------------------------------------- /examples/unique.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use bb8::Pool; 3 | use serde::{Deserialize, Serialize}; 4 | use sidekiq::{Processor, RedisConnectionManager, Result, Worker}; 5 | 6 | #[derive(Clone)] 7 | struct CustomerNotificationWorker; 8 | 9 | #[async_trait] 10 | impl Worker for CustomerNotificationWorker { 11 | fn opts() -> sidekiq::WorkerOpts { 12 | // Use default options to set the unique_for option by default. 13 | sidekiq::WorkerOpts::new() 14 | .queue("customers") 15 | .unique_for(std::time::Duration::from_secs(30)) 16 | } 17 | 18 | async fn perform(&self, _args: CustomerNotification) -> Result<()> { 19 | Ok(()) 20 | } 21 | } 22 | 23 | #[derive(Deserialize, Debug, Serialize)] 24 | struct CustomerNotification { 25 | customer_guid: String, 26 | } 27 | 28 | #[tokio::main] 29 | async fn main() -> Result<()> { 30 | tracing_subscriber::fmt::init(); 31 | 32 | // Redis 33 | let manager = RedisConnectionManager::new("redis://127.0.0.1/")?; 34 | let redis = Pool::builder().build(manager).await?; 35 | 36 | // Sidekiq server 37 | let mut p = Processor::new(redis.clone(), vec!["customers".to_string()]); 38 | 39 | // Add known workers 40 | p.register(CustomerNotificationWorker); 41 | 42 | // Create a bunch of jobs with the default uniqueness options. Only 43 | // one of these should be created within a 30 second period. 44 | for _ in 1..10 { 45 | CustomerNotificationWorker::perform_async( 46 | &redis, 47 | CustomerNotification { 48 | customer_guid: "CST-123".to_string(), 49 | }, 50 | ) 51 | .await?; 52 | } 53 | 54 | // Override the unique_for option. Note: Because the code above 55 | // uses the default unique_for value of 30, this code is essentially 56 | // a no-op. 57 | CustomerNotificationWorker::opts() 58 | .unique_for(std::time::Duration::from_secs(90)) 59 | .perform_async( 60 | &redis, 61 | CustomerNotification { 62 | customer_guid: "CST-123".to_string(), 63 | }, 64 | ) 65 | .await?; 66 | 67 | p.run().await; 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /src/scheduled.rs: -------------------------------------------------------------------------------- 1 | use crate::{periodic::PeriodicJob, RedisPool, UnitOfWork}; 2 | use tracing::debug; 3 | 4 | pub struct Scheduled { 5 | redis: RedisPool, 6 | } 7 | 8 | impl Scheduled { 9 | #[must_use] 10 | pub fn new(redis: RedisPool) -> Self { 11 | Self { redis } 12 | } 13 | 14 | pub async fn enqueue_jobs( 15 | &self, 16 | now: chrono::DateTime, 17 | sorted_sets: &Vec, 18 | ) -> Result> { 19 | let mut n = 0; 20 | for sorted_set in sorted_sets { 21 | let mut redis = self.redis.get().await?; 22 | 23 | let jobs: Vec = redis 24 | .zrangebyscore_limit(sorted_set.clone(), "-inf", now.timestamp(), 0, 10) 25 | .await?; 26 | 27 | for job in jobs { 28 | if redis.zrem(sorted_set.clone(), job.clone()).await? > 0 { 29 | let work = UnitOfWork::from_job_string(job)?; 30 | 31 | debug!({ 32 | "class" = &work.job.class, 33 | "queue" = &work.queue 34 | }, "Enqueueing job"); 35 | 36 | work.enqueue_direct(&mut redis).await?; 37 | 38 | n += 1; 39 | } 40 | } 41 | } 42 | 43 | Ok(n) 44 | } 45 | 46 | pub async fn enqueue_periodic_jobs( 47 | &self, 48 | now: chrono::DateTime, 49 | ) -> Result> { 50 | let mut conn = self.redis.get().await?; 51 | 52 | let periodic_jobs: Vec = conn 53 | .zrangebyscore_limit("periodic".to_string(), "-inf", now.timestamp(), 0, 100) 54 | .await?; 55 | 56 | for periodic_job in &periodic_jobs { 57 | let pj = PeriodicJob::from_periodic_job_string(periodic_job.clone())?; 58 | 59 | if pj.update(&mut conn, periodic_job).await? { 60 | let job = pj.into_job(); 61 | let work = UnitOfWork::from_job(job); 62 | 63 | debug!({ 64 | "args" = &pj.args, 65 | "class" = &work.job.class, 66 | "queue" = &work.queue, 67 | "name" = &pj.name, 68 | "cron" = &pj.cron, 69 | }, "Enqueueing periodic job"); 70 | 71 | work.enqueue_direct(&mut conn).await?; 72 | } 73 | } 74 | 75 | Ok(periodic_jobs.len()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/process_unique_job_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test { 3 | use async_trait::async_trait; 4 | use bb8::Pool; 5 | use sidekiq::{Processor, RedisConnectionManager, RedisPool, Result, WorkFetcher, Worker}; 6 | use std::sync::{Arc, Mutex}; 7 | 8 | #[async_trait] 9 | trait FlushAll { 10 | async fn flushall(&self); 11 | } 12 | 13 | #[async_trait] 14 | impl FlushAll for RedisPool { 15 | async fn flushall(&self) { 16 | let mut conn = self.get().await.unwrap(); 17 | let _: String = redis::cmd("FLUSHALL") 18 | .query_async(conn.unnamespaced_borrow_mut()) 19 | .await 20 | .unwrap(); 21 | } 22 | } 23 | 24 | async fn new_base_processor(queue: String) -> (Processor, RedisPool) { 25 | // Redis 26 | let manager = RedisConnectionManager::new("redis://127.0.0.1/").unwrap(); 27 | let redis = Pool::builder().build(manager).await.unwrap(); 28 | redis.flushall().await; 29 | 30 | // Sidekiq server 31 | let p = Processor::new(redis.clone(), vec![queue]); 32 | 33 | (p, redis) 34 | } 35 | 36 | #[tokio::test] 37 | async fn can_create_and_process_an_unique_job_only_once_within_the_ttl() { 38 | #[derive(Clone)] 39 | struct TestWorker { 40 | did_process: Arc>, 41 | } 42 | 43 | #[async_trait] 44 | impl Worker<()> for TestWorker { 45 | async fn perform(&self, _args: ()) -> Result<()> { 46 | let mut this = self.did_process.lock().unwrap(); 47 | *this = true; 48 | 49 | Ok(()) 50 | } 51 | } 52 | 53 | let worker = TestWorker { 54 | did_process: Arc::new(Mutex::new(false)), 55 | }; 56 | let queue = "random123".to_string(); 57 | let (mut p, redis) = new_base_processor(queue.clone()).await; 58 | 59 | p.register(worker.clone()); 60 | 61 | TestWorker::opts() 62 | .queue(queue.clone()) 63 | .unique_for(std::time::Duration::from_secs(5)) 64 | .perform_async(&redis, ()) 65 | .await 66 | .unwrap(); 67 | 68 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 69 | assert!(*worker.did_process.lock().unwrap()); 70 | 71 | TestWorker::opts() 72 | .queue(queue) 73 | .unique_for(std::time::Duration::from_secs(5)) 74 | .perform_async(&redis, ()) 75 | .await 76 | .unwrap(); 77 | 78 | assert_eq!( 79 | p.process_one_tick_once().await.unwrap(), 80 | WorkFetcher::NoWorkFound 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/process_async_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test { 3 | use async_trait::async_trait; 4 | use bb8::Pool; 5 | use sidekiq::{ 6 | BalanceStrategy, Processor, ProcessorConfig, QueueConfig, RedisConnectionManager, 7 | RedisPool, Result, WorkFetcher, Worker, 8 | }; 9 | use std::sync::{Arc, Mutex}; 10 | 11 | #[async_trait] 12 | trait FlushAll { 13 | async fn flushall(&self); 14 | } 15 | 16 | #[async_trait] 17 | impl FlushAll for RedisPool { 18 | async fn flushall(&self) { 19 | let mut conn = self.get().await.unwrap(); 20 | let _: String = redis::cmd("FLUSHALL") 21 | .query_async(conn.unnamespaced_borrow_mut()) 22 | .await 23 | .unwrap(); 24 | } 25 | } 26 | 27 | async fn new_base_processor(queue: String) -> (Processor, RedisPool) { 28 | // Redis 29 | let manager = RedisConnectionManager::new("redis://127.0.0.1/").unwrap(); 30 | let redis = Pool::builder().build(manager).await.unwrap(); 31 | redis.flushall().await; 32 | 33 | // Sidekiq server 34 | let p = Processor::new(redis.clone(), vec![queue]).with_config( 35 | ProcessorConfig::default() 36 | .num_workers(1) 37 | .balance_strategy(BalanceStrategy::RoundRobin) 38 | .queue_config( 39 | "dedicated queue 1".to_string(), 40 | QueueConfig::default().num_workers(10), 41 | ) 42 | .queue_config( 43 | "dedicated queue 2".to_string(), 44 | QueueConfig::default().num_workers(100), 45 | ), 46 | ); 47 | 48 | (p, redis) 49 | } 50 | 51 | #[tokio::test] 52 | async fn can_process_an_async_job() { 53 | #[derive(Clone)] 54 | struct TestWorker { 55 | did_process: Arc>, 56 | } 57 | 58 | #[async_trait] 59 | impl Worker<()> for TestWorker { 60 | async fn perform(&self, _args: ()) -> Result<()> { 61 | let mut this = self.did_process.lock().unwrap(); 62 | *this = true; 63 | 64 | Ok(()) 65 | } 66 | } 67 | 68 | let worker = TestWorker { 69 | did_process: Arc::new(Mutex::new(false)), 70 | }; 71 | let queue = "random123".to_string(); 72 | let (mut p, redis) = new_base_processor(queue.clone()).await; 73 | 74 | p.register(worker.clone()); 75 | 76 | TestWorker::opts() 77 | .queue(queue) 78 | .perform_async(&redis, ()) 79 | .await 80 | .unwrap(); 81 | 82 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 83 | assert!(*worker.did_process.lock().unwrap()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/process_scheduled_job_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test { 3 | use async_trait::async_trait; 4 | use bb8::Pool; 5 | use sidekiq::{ 6 | Processor, RedisConnectionManager, RedisPool, Result, Scheduled, WorkFetcher, Worker, 7 | }; 8 | use std::sync::{Arc, Mutex}; 9 | 10 | #[async_trait] 11 | trait FlushAll { 12 | async fn flushall(&self); 13 | } 14 | 15 | #[async_trait] 16 | impl FlushAll for RedisPool { 17 | async fn flushall(&self) { 18 | let mut conn = self.get().await.unwrap(); 19 | let _: String = redis::cmd("FLUSHALL") 20 | .query_async(conn.unnamespaced_borrow_mut()) 21 | .await 22 | .unwrap(); 23 | } 24 | } 25 | 26 | async fn new_base_processor(queue: String) -> (Processor, RedisPool) { 27 | // Redis 28 | let manager = RedisConnectionManager::new("redis://127.0.0.1/").unwrap(); 29 | let redis = Pool::builder().build(manager).await.unwrap(); 30 | redis.flushall().await; 31 | 32 | // Sidekiq server 33 | let p = Processor::new(redis.clone(), vec![queue]); 34 | 35 | (p, redis) 36 | } 37 | 38 | #[tokio::test] 39 | async fn can_process_a_scheduled_job() { 40 | #[derive(Clone)] 41 | struct TestWorker { 42 | did_process: Arc>, 43 | } 44 | 45 | #[async_trait] 46 | impl Worker<()> for TestWorker { 47 | async fn perform(&self, _args: ()) -> Result<()> { 48 | let mut this = self.did_process.lock().unwrap(); 49 | *this = true; 50 | 51 | Ok(()) 52 | } 53 | } 54 | 55 | let worker = TestWorker { 56 | did_process: Arc::new(Mutex::new(false)), 57 | }; 58 | let queue = "random123".to_string(); 59 | let (mut p, redis) = new_base_processor(queue.clone()).await; 60 | 61 | p.register(worker.clone()); 62 | 63 | TestWorker::opts() 64 | .queue(queue) 65 | .perform_in(&redis, std::time::Duration::from_secs(10), ()) 66 | .await 67 | .unwrap(); 68 | 69 | assert_eq!( 70 | p.process_one_tick_once().await.unwrap(), 71 | WorkFetcher::NoWorkFound 72 | ); 73 | 74 | let sched = Scheduled::new(redis.clone()); 75 | let sorted_sets = vec!["retry".to_string(), "schedule".to_string()]; 76 | let n = sched 77 | .enqueue_jobs( 78 | chrono::Utc::now() + chrono::Duration::seconds(11), 79 | &sorted_sets, 80 | ) 81 | .await 82 | .unwrap(); 83 | 84 | assert_eq!(n, 1); 85 | 86 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 87 | 88 | assert!(*worker.did_process.lock().unwrap()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/process_cron_job_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test { 3 | use async_trait::async_trait; 4 | use bb8::Pool; 5 | use sidekiq::{ 6 | periodic, Processor, RedisConnectionManager, RedisPool, Result, Scheduled, WorkFetcher, 7 | Worker, 8 | }; 9 | use std::sync::{Arc, Mutex}; 10 | 11 | #[async_trait] 12 | trait FlushAll { 13 | async fn flushall(&self); 14 | } 15 | 16 | #[async_trait] 17 | impl FlushAll for RedisPool { 18 | async fn flushall(&self) { 19 | let mut conn = self.get().await.unwrap(); 20 | let _: String = redis::cmd("FLUSHALL") 21 | .query_async(conn.unnamespaced_borrow_mut()) 22 | .await 23 | .unwrap(); 24 | } 25 | } 26 | 27 | async fn new_base_processor(queue: String) -> (Processor, RedisPool) { 28 | // Redis 29 | let manager = RedisConnectionManager::new("redis://127.0.0.1/").unwrap(); 30 | let redis = Pool::builder().build(manager).await.unwrap(); 31 | redis.flushall().await; 32 | 33 | // Sidekiq server 34 | let p = Processor::new(redis.clone(), vec![queue]); 35 | 36 | (p, redis) 37 | } 38 | 39 | async fn set_cron_scores_to_zero(redis: RedisPool) { 40 | let mut conn = redis.get().await.unwrap(); 41 | 42 | let jobs = conn 43 | .zrange("periodic".to_string(), isize::MIN, isize::MAX) 44 | .await 45 | .unwrap(); 46 | 47 | for job in jobs { 48 | let _: usize = conn 49 | .zadd("periodic".to_string(), job.clone(), 0) 50 | .await 51 | .unwrap(); 52 | } 53 | } 54 | 55 | #[tokio::test] 56 | async fn can_process_a_cron_job() { 57 | #[derive(Clone)] 58 | struct TestWorker { 59 | did_process: Arc>, 60 | } 61 | 62 | #[async_trait] 63 | impl Worker<()> for TestWorker { 64 | async fn perform(&self, _args: ()) -> Result<()> { 65 | let mut this = self.did_process.lock().unwrap(); 66 | *this = true; 67 | 68 | Ok(()) 69 | } 70 | } 71 | 72 | let worker = TestWorker { 73 | did_process: Arc::new(Mutex::new(false)), 74 | }; 75 | let queue = "random123".to_string(); 76 | let (mut p, redis) = new_base_processor(queue.clone()).await; 77 | 78 | p.register(worker.clone()); 79 | 80 | // Cron jobs 81 | periodic::builder("0 * * * * *") 82 | .unwrap() 83 | .name("Payment report processing for a user using json args") 84 | .queue(queue.clone()) 85 | .register(&mut p, worker.clone()) 86 | .await 87 | .unwrap(); 88 | 89 | assert_eq!( 90 | p.process_one_tick_once().await.unwrap(), 91 | WorkFetcher::NoWorkFound 92 | ); 93 | 94 | set_cron_scores_to_zero(redis.clone()).await; 95 | 96 | let sched = Scheduled::new(redis.clone()); 97 | let n = sched 98 | .enqueue_periodic_jobs(chrono::Utc::now()) 99 | .await 100 | .unwrap(); 101 | 102 | assert_eq!(n, 1); 103 | 104 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 105 | 106 | assert!(*worker.did_process.lock().unwrap()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/stats.rs: -------------------------------------------------------------------------------- 1 | use crate::RedisPool; 2 | use rand::RngCore; 3 | use serde::Serialize; 4 | use std::sync::atomic::{AtomicUsize, Ordering}; 5 | use std::sync::Arc; 6 | 7 | #[derive(Clone)] 8 | pub struct Counter { 9 | count: Arc, 10 | } 11 | 12 | impl Counter { 13 | #[must_use] 14 | pub fn new(n: usize) -> Self { 15 | Self { 16 | count: Arc::new(AtomicUsize::new(n)), 17 | } 18 | } 19 | 20 | #[must_use] 21 | pub fn value(&self) -> usize { 22 | self.count.load(Ordering::SeqCst) 23 | } 24 | 25 | pub fn decrby(&self, n: usize) { 26 | self.count.fetch_sub(n, Ordering::SeqCst); 27 | } 28 | 29 | pub fn incrby(&self, n: usize) { 30 | self.count.fetch_add(n, Ordering::SeqCst); 31 | } 32 | } 33 | 34 | struct ProcessStats { 35 | rtt_us: String, 36 | quiet: bool, 37 | busy: usize, 38 | beat: chrono::DateTime, 39 | info: ProcessInfo, 40 | rss: String, 41 | } 42 | 43 | #[derive(Serialize)] 44 | struct ProcessInfo { 45 | hostname: String, 46 | identity: String, 47 | started_at: f64, 48 | pid: u32, 49 | tag: Option, 50 | concurrency: usize, 51 | queues: Vec, 52 | labels: Vec, 53 | } 54 | 55 | pub struct StatsPublisher { 56 | hostname: String, 57 | identity: String, 58 | queues: Vec, 59 | started_at: chrono::DateTime, 60 | busy_jobs: Counter, 61 | concurrency: usize, 62 | } 63 | 64 | fn generate_identity(hostname: &String) -> String { 65 | let pid = std::process::id(); 66 | let mut bytes = [0u8; 12]; 67 | rand::thread_rng().fill_bytes(&mut bytes); 68 | let nonce = hex::encode(bytes); 69 | 70 | format!("{hostname}:{pid}:{nonce}") 71 | } 72 | 73 | impl StatsPublisher { 74 | #[must_use] 75 | pub fn new( 76 | hostname: String, 77 | queues: Vec, 78 | busy_jobs: Counter, 79 | concurrency: usize, 80 | ) -> Self { 81 | let identity = generate_identity(&hostname); 82 | let started_at = chrono::Utc::now(); 83 | 84 | Self { 85 | hostname, 86 | identity, 87 | queues, 88 | started_at, 89 | busy_jobs, 90 | concurrency, 91 | } 92 | } 93 | 94 | // 127.0.0.1:6379> hkeys "yolo_app:DESKTOP-UMSV21A:107068:5075431aeb06" 95 | // 1) "rtt_us" 96 | // 2) "quiet" 97 | // 3) "busy" 98 | // 4) "beat" 99 | // 5) "info" 100 | // 6) "rss" 101 | // 127.0.0.1:6379> hget "yolo_app:DESKTOP-UMSV21A:107068:5075431aeb06" info 102 | // "{\"hostname\":\"DESKTOP-UMSV21A\",\"started_at\":1658082501.5606177,\"pid\":107068,\"tag\":\"\",\"concurrency\":10,\"queues\":[\"ruby:v1_statistics\",\"ruby:v2_statistics\"],\"labels\":[],\"identity\":\"DESKTOP-UMSV21A:107068:5075431aeb06\"}" 103 | // 127.0.0.1:6379> hget "yolo_app:DESKTOP-UMSV21A:107068:5075431aeb06" irss 104 | // (nil) 105 | pub async fn publish_stats(&self, redis: RedisPool) -> Result<(), Box> { 106 | let stats = self.create_process_stats().await?; 107 | let mut conn = redis.get().await?; 108 | let _: () = conn 109 | .cmd_with_key("HSET", self.identity.clone()) 110 | .arg("rss") 111 | .arg(stats.rss) 112 | .arg("rtt_us") 113 | .arg(stats.rtt_us) 114 | .arg("busy") 115 | .arg(stats.busy) 116 | .arg("quiet") 117 | .arg(stats.quiet) 118 | .arg("beat") 119 | .arg(stats.beat.timestamp()) 120 | .arg("info") 121 | .arg(serde_json::to_string(&stats.info)?) 122 | .query_async::<()>(conn.unnamespaced_borrow_mut()) 123 | .await?; 124 | 125 | conn.expire(self.identity.clone(), 30).await?; 126 | 127 | conn.sadd("processes".to_string(), self.identity.clone()) 128 | .await?; 129 | 130 | Ok(()) 131 | } 132 | 133 | async fn create_process_stats(&self) -> Result> { 134 | #[cfg(feature = "rss-stats")] 135 | let rss_in_kb = format!( 136 | "{}", 137 | simple_process_stats::ProcessStats::get() 138 | .await? 139 | .memory_usage_bytes 140 | / 1024 141 | ); 142 | 143 | #[cfg(not(feature = "rss-stats"))] 144 | let rss_in_kb = "0".to_string(); 145 | 146 | Ok(ProcessStats { 147 | rtt_us: "0".into(), 148 | busy: self.busy_jobs.value(), 149 | quiet: false, 150 | rss: rss_in_kb, 151 | 152 | beat: chrono::Utc::now(), 153 | info: ProcessInfo { 154 | concurrency: self.concurrency, 155 | hostname: self.hostname.clone(), 156 | identity: self.identity.clone(), 157 | queues: self.queues.clone(), 158 | started_at: self.started_at.clone().timestamp() as f64, 159 | pid: std::process::id(), 160 | 161 | // TODO: Fill out labels and tags. 162 | labels: vec![], 163 | tag: None, 164 | }, 165 | }) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /examples/consumer-demo.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use bb8::Pool; 3 | use serde::{Deserialize, Serialize}; 4 | use sidekiq::{ 5 | ChainIter, Job, Processor, RedisConnectionManager, Result, ServerMiddleware, Worker, WorkerRef, 6 | }; 7 | use std::sync::Arc; 8 | use tracing::{error, info}; 9 | 10 | #[derive(Clone)] 11 | struct HelloWorker; 12 | 13 | #[async_trait] 14 | impl Worker<()> for HelloWorker { 15 | async fn perform(&self, _args: ()) -> Result<()> { 16 | // I don't use any args. I do my own work. 17 | Ok(()) 18 | } 19 | } 20 | 21 | #[derive(Clone)] 22 | struct PaymentReportWorker; 23 | 24 | impl PaymentReportWorker { 25 | async fn send_report(&self, user_guid: String) -> Result<()> { 26 | // TODO: Some actual work goes here... 27 | info!({"user_guid" = user_guid, "class_name" = Self::class_name()}, "Sending payment report to user"); 28 | 29 | Ok(()) 30 | } 31 | } 32 | 33 | #[derive(Deserialize, Debug, Serialize)] 34 | struct PaymentReportArgs { 35 | user_guid: String, 36 | } 37 | 38 | #[async_trait] 39 | impl Worker for PaymentReportWorker { 40 | fn opts() -> sidekiq::WorkerOpts { 41 | sidekiq::WorkerOpts::new().queue("yolo") 42 | } 43 | 44 | async fn perform(&self, args: PaymentReportArgs) -> Result<()> { 45 | self.send_report(args.user_guid).await 46 | } 47 | } 48 | 49 | struct FilterExpiredUsersMiddleware; 50 | 51 | #[derive(Deserialize)] 52 | struct FiltereExpiredUsersArgs { 53 | user_guid: String, 54 | } 55 | 56 | impl FiltereExpiredUsersArgs { 57 | fn is_expired(&self) -> bool { 58 | self.user_guid == "USR-123-EXPIRED" 59 | } 60 | } 61 | 62 | #[async_trait] 63 | impl ServerMiddleware for FilterExpiredUsersMiddleware { 64 | async fn call( 65 | &self, 66 | chain: ChainIter, 67 | job: &Job, 68 | worker: Arc, 69 | redis: Pool, 70 | ) -> Result<()> { 71 | let args: std::result::Result<(FiltereExpiredUsersArgs,), serde_json::Error> = 72 | serde_json::from_value(job.args.clone()); 73 | 74 | // If we can safely deserialize then attempt to filter based on user guid. 75 | if let Ok((filter,)) = args { 76 | if filter.is_expired() { 77 | error!({ 78 | "class" = &job.class, 79 | "jid" = &job.jid, 80 | "user_guid" = filter.user_guid, 81 | }, "Detected an expired user, skipping this job"); 82 | return Ok(()); 83 | } 84 | } 85 | 86 | chain.next(job, worker, redis).await 87 | } 88 | } 89 | 90 | #[tokio::main] 91 | async fn main() -> Result<()> { 92 | tracing_subscriber::fmt::init(); 93 | 94 | // Redis 95 | let manager = RedisConnectionManager::new("redis://127.0.0.1/")?; 96 | let redis = Pool::builder().build(manager).await?; 97 | // 98 | // tokio::spawn({ 99 | // let mut redis = redis.clone(); 100 | // 101 | // async move { 102 | // loop { 103 | // PaymentReportWorker::perform_async( 104 | // &mut redis, 105 | // PaymentReportArgs { 106 | // user_guid: "USR-123".into(), 107 | // }, 108 | // ) 109 | // .await 110 | // .unwrap(); 111 | // 112 | // tokio::time::sleep(std::time::Duration::from_secs(1)).await; 113 | // } 114 | // } 115 | // }); 116 | // 117 | // // Enqueue a job with the worker! There are many ways to do this. 118 | // PaymentReportWorker::perform_async( 119 | // &mut redis, 120 | // PaymentReportArgs { 121 | // user_guid: "USR-123".into(), 122 | // }, 123 | // ) 124 | // .await?; 125 | // 126 | // PaymentReportWorker::perform_in( 127 | // &mut redis, 128 | // std::time::Duration::from_secs(10), 129 | // PaymentReportArgs { 130 | // user_guid: "USR-123".into(), 131 | // }, 132 | // ) 133 | // .await?; 134 | // 135 | // PaymentReportWorker::opts() 136 | // .queue("brolo") 137 | // .perform_async( 138 | // &mut redis, 139 | // PaymentReportArgs { 140 | // user_guid: "USR-123-EXPIRED".into(), 141 | // }, 142 | // ) 143 | // .await?; 144 | // 145 | // sidekiq::perform_async( 146 | // &mut redis, 147 | // "PaymentReportWorker".into(), 148 | // "yolo".into(), 149 | // PaymentReportArgs { 150 | // user_guid: "USR-123".to_string(), 151 | // }, 152 | // ) 153 | // .await?; 154 | // 155 | // // Enqueue a job 156 | // sidekiq::perform_async( 157 | // &mut redis, 158 | // "PaymentReportWorker".into(), 159 | // "yolo".into(), 160 | // PaymentReportArgs { 161 | // user_guid: "USR-123".to_string(), 162 | // }, 163 | // ) 164 | // .await?; 165 | // 166 | // // Enqueue a job with options 167 | // sidekiq::opts() 168 | // .queue("yolo".to_string()) 169 | // .perform_async( 170 | // &mut redis, 171 | // "PaymentReportWorker".into(), 172 | // PaymentReportArgs { 173 | // user_guid: "USR-123".to_string(), 174 | // }, 175 | // ) 176 | // .await?; 177 | 178 | // Sidekiq server 179 | let mut p = Processor::new(redis.clone(), vec!["yolo".to_string(), "brolo".to_string()]); 180 | 181 | // Add known workers 182 | p.register(HelloWorker); 183 | p.register(PaymentReportWorker); 184 | 185 | // Custom Middlewares 186 | p.using(FilterExpiredUsersMiddleware).await; 187 | 188 | // // Reset cron jobs 189 | // periodic::destroy_all(redis.clone()).await?; 190 | // 191 | // // Cron jobs 192 | // periodic::builder("0 * * * * *")? 193 | // .name("Payment report processing for a random user") 194 | // .queue("yolo") 195 | // //.args(PaymentReportArgs { 196 | // // user_guid: "USR-123-PERIODIC".to_string(), 197 | // //})? 198 | // .args(json!({ "user_guid": "USR-123-PERIODIC" }))? 199 | // .register(&mut p, PaymentReportWorker::new(logger.clone())) 200 | // .await?; 201 | 202 | p.run().await; 203 | Ok(()) 204 | } 205 | -------------------------------------------------------------------------------- /src/periodic.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use crate::{new_jid, Error, Job, Processor, RedisConnection, RedisPool, RetryOpts, Worker}; 3 | pub use cron_clock::{Schedule as Cron, Utc}; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_json::Value as JsonValue; 6 | use std::str::FromStr; 7 | 8 | pub fn parse(cron: &str) -> Result { 9 | Ok(Cron::from_str(cron)?) 10 | } 11 | 12 | pub async fn destroy_all(redis: RedisPool) -> Result<()> { 13 | let mut conn = redis.get().await?; 14 | conn.del("periodic".to_string()).await?; 15 | Ok(()) 16 | } 17 | 18 | pub struct Builder { 19 | pub(crate) name: Option, 20 | pub(crate) queue: Option, 21 | pub(crate) args: Option, 22 | pub(crate) retry: Option, 23 | pub(crate) cron: Cron, 24 | } 25 | 26 | pub fn builder(cron_str: &str) -> Result { 27 | Ok(Builder { 28 | name: None, 29 | queue: None, 30 | args: None, 31 | retry: None, 32 | cron: Cron::from_str(cron_str)?, 33 | }) 34 | } 35 | 36 | impl Builder { 37 | pub fn name>(self, name: S) -> Builder { 38 | Builder { 39 | name: Some(name.into()), 40 | ..self 41 | } 42 | } 43 | 44 | #[must_use] 45 | pub fn retry(self, retry: RO) -> Builder 46 | where 47 | RO: Into, 48 | { 49 | Self { 50 | retry: Some(retry.into()), 51 | ..self 52 | } 53 | } 54 | 55 | pub fn queue>(self, queue: S) -> Builder { 56 | Builder { 57 | queue: Some(queue.into()), 58 | ..self 59 | } 60 | } 61 | pub fn args(self, args: Args) -> Result 62 | where 63 | Args: Sync + Send + for<'de> serde::Deserialize<'de> + serde::Serialize + 'static, 64 | { 65 | let args = serde_json::to_value(args)?; 66 | 67 | // Ensure args are always wrapped in an array. 68 | let args = if args.is_array() { 69 | args 70 | } else { 71 | JsonValue::Array(vec![args]) 72 | }; 73 | 74 | Ok(Self { 75 | args: Some(args), 76 | ..self 77 | }) 78 | } 79 | 80 | pub async fn register(self, processor: &mut Processor, worker: W) -> Result<()> 81 | where 82 | Args: Sync + Send + for<'de> serde::Deserialize<'de> + 'static, 83 | W: Worker + 'static, 84 | { 85 | processor.register(worker); 86 | processor 87 | .register_periodic(self.into_periodic_job(W::class_name())?) 88 | .await?; 89 | 90 | Ok(()) 91 | } 92 | 93 | pub fn into_periodic_job(&self, class_name: String) -> Result { 94 | let name = self 95 | .name 96 | .clone() 97 | .unwrap_or_else(|| "Scheduled PeriodicJob".into()); 98 | 99 | let mut pj = PeriodicJob { 100 | name, 101 | class: class_name, 102 | cron: self.cron.to_string(), 103 | 104 | ..Default::default() 105 | }; 106 | 107 | pj.retry.clone_from(&self.retry); 108 | pj.queue.clone_from(&self.queue); 109 | pj.args = self.args.clone().map(|a| a.to_string()); 110 | 111 | pj.hydrate_attributes()?; 112 | 113 | Ok(pj) 114 | } 115 | } 116 | 117 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 118 | pub struct PeriodicJob { 119 | pub(crate) name: String, 120 | pub(crate) class: String, 121 | pub(crate) cron: String, 122 | pub(crate) queue: Option, 123 | pub(crate) args: Option, 124 | retry: Option, 125 | 126 | #[serde(skip)] 127 | cron_schedule: Option, 128 | 129 | #[serde(skip)] 130 | json_args: Option, 131 | } 132 | 133 | impl PeriodicJob { 134 | pub fn from_periodic_job_string(periodic_job_str: String) -> Result { 135 | let mut pj: Self = serde_json::from_str(&periodic_job_str)?; 136 | pj.hydrate_attributes()?; 137 | Ok(pj) 138 | } 139 | 140 | fn hydrate_attributes(&mut self) -> Result<()> { 141 | self.cron_schedule = Some(Cron::from_str(&self.cron)?); 142 | self.json_args = if let Some(ref args) = self.args { 143 | Some(serde_json::from_str(args)?) 144 | } else { 145 | Some(JsonValue::Null) 146 | }; 147 | Ok(()) 148 | } 149 | 150 | pub async fn insert(&self, conn: &mut RedisConnection) -> Result { 151 | let payload = serde_json::to_string(self)?; 152 | self.update(conn, &payload).await 153 | } 154 | 155 | pub async fn update(&self, conn: &mut RedisConnection, periodic_job_str: &str) -> Result { 156 | if let Some(next_scheduled_time) = self.next_scheduled_time() { 157 | // [ZADD key CH score value] will return true/ false if the value added changed 158 | // when we submit it to redis. We can use this to determine if we were the lucky 159 | // process that changed the periodic job to its next scheduled time and enqueue 160 | // the job. 161 | return Ok(conn 162 | .zadd_ch( 163 | "periodic".to_string(), 164 | periodic_job_str, 165 | next_scheduled_time, 166 | ) 167 | .await?); 168 | } 169 | 170 | Err(Error::Message(format!( 171 | "Unable to fetch next schedled time for periodic job: class: {}, name: {}", 172 | &self.class, &self.name 173 | ))) 174 | } 175 | 176 | #[must_use] 177 | pub fn next_scheduled_time(&self) -> Option { 178 | if let Some(ref cron_sched) = self.cron_schedule { 179 | cron_sched 180 | .upcoming(Utc) 181 | .next() 182 | .map(|dt| dt.timestamp() as f64) 183 | } else { 184 | None 185 | } 186 | } 187 | 188 | #[must_use] 189 | pub fn into_job(&self) -> Job { 190 | let args = self.json_args.clone().expect("always set in contructor"); 191 | 192 | Job { 193 | queue: self.queue.clone().unwrap_or_else(|| "default".to_string()), 194 | class: self.class.clone(), 195 | jid: new_jid(), 196 | created_at: chrono::Utc::now().timestamp() as f64, 197 | enqueued_at: None, 198 | retry: self.retry.clone().unwrap_or(RetryOpts::Never), 199 | args, 200 | 201 | // Make default eventually... 202 | error_message: None, 203 | error_class: None, 204 | failed_at: None, 205 | retry_count: None, 206 | retried_at: None, 207 | retry_queue: None, 208 | 209 | // Meta data not used in periodic jobs right now... 210 | unique_for: None, 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use bb8::Pool; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::json; 5 | use sidekiq::{ 6 | periodic, ChainIter, Job, Processor, RedisConnectionManager, RedisPool, Result, 7 | ServerMiddleware, Worker, WorkerRef, 8 | }; 9 | use std::sync::Arc; 10 | use tracing::{debug, error, info, Level}; 11 | 12 | #[derive(Clone)] 13 | struct HelloWorker; 14 | 15 | #[async_trait] 16 | impl Worker<()> for HelloWorker { 17 | async fn perform(&self, _args: ()) -> Result<()> { 18 | // I don't use any args. I do my own work. 19 | Ok(()) 20 | } 21 | } 22 | 23 | #[derive(Clone)] 24 | struct PaymentReportWorker { 25 | redis: RedisPool, 26 | } 27 | 28 | impl PaymentReportWorker { 29 | fn new(redis: RedisPool) -> Self { 30 | Self { redis } 31 | } 32 | 33 | async fn send_report(&self, user_guid: String) -> Result<()> { 34 | // TODO: Some actual work goes here... 35 | info!({ 36 | "user_guid" = user_guid, 37 | "class_name" = Self::class_name() 38 | }, "Sending payment report to user"); 39 | 40 | Ok(()) 41 | } 42 | } 43 | 44 | #[derive(Deserialize, Debug, Serialize)] 45 | struct PaymentReportArgs { 46 | user_guid: String, 47 | } 48 | 49 | #[async_trait] 50 | impl Worker for PaymentReportWorker { 51 | fn opts() -> sidekiq::WorkerOpts { 52 | sidekiq::WorkerOpts::new().queue("yolo") 53 | } 54 | 55 | async fn perform(&self, args: PaymentReportArgs) -> Result<()> { 56 | use redis::AsyncCommands; 57 | 58 | let times_called: usize = self 59 | .redis 60 | .get() 61 | .await? 62 | .unnamespaced_borrow_mut() 63 | .incr("example_of_accessing_the_raw_redis_connection", 1) 64 | .await?; 65 | 66 | debug!({ "times_called" = times_called }, "Called this worker"); 67 | 68 | self.send_report(args.user_guid).await 69 | } 70 | } 71 | 72 | struct FilterExpiredUsersMiddleware; 73 | 74 | #[derive(Deserialize)] 75 | struct FiltereExpiredUsersArgs { 76 | user_guid: String, 77 | } 78 | 79 | impl FiltereExpiredUsersArgs { 80 | fn is_expired(&self) -> bool { 81 | self.user_guid == "USR-123-EXPIRED" 82 | } 83 | } 84 | 85 | #[async_trait] 86 | impl ServerMiddleware for FilterExpiredUsersMiddleware { 87 | async fn call( 88 | &self, 89 | chain: ChainIter, 90 | job: &Job, 91 | worker: Arc, 92 | redis: RedisPool, 93 | ) -> Result<()> { 94 | let args: std::result::Result<(FiltereExpiredUsersArgs,), serde_json::Error> = 95 | serde_json::from_value(job.args.clone()); 96 | 97 | // If we can safely deserialize then attempt to filter based on user guid. 98 | if let Ok((filter,)) = args { 99 | if filter.is_expired() { 100 | error!({ 101 | "class" = &job.class, 102 | "jid" = &job.jid, 103 | "user_guid" = filter.user_guid 104 | }, "Detected an expired user, skipping this job"); 105 | return Ok(()); 106 | } 107 | } 108 | 109 | chain.next(job, worker, redis).await 110 | } 111 | } 112 | 113 | #[tokio::main] 114 | async fn main() -> Result<()> { 115 | tracing_subscriber::fmt().with_max_level(Level::INFO).init(); 116 | 117 | // Redis 118 | let manager = RedisConnectionManager::new("redis://127.0.0.1/")?; 119 | let redis = Pool::builder().build(manager).await?; 120 | 121 | tokio::spawn({ 122 | let redis = redis.clone(); 123 | 124 | async move { 125 | loop { 126 | PaymentReportWorker::perform_async( 127 | &redis, 128 | PaymentReportArgs { 129 | user_guid: "USR-123".into(), 130 | }, 131 | ) 132 | .await 133 | .unwrap(); 134 | 135 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 136 | } 137 | } 138 | }); 139 | 140 | // Enqueue a job with the worker! There are many ways to do this. 141 | PaymentReportWorker::perform_async( 142 | &redis, 143 | PaymentReportArgs { 144 | user_guid: "USR-123".into(), 145 | }, 146 | ) 147 | .await?; 148 | 149 | PaymentReportWorker::perform_in( 150 | &redis, 151 | std::time::Duration::from_secs(10), 152 | PaymentReportArgs { 153 | user_guid: "USR-123".into(), 154 | }, 155 | ) 156 | .await?; 157 | 158 | PaymentReportWorker::opts() 159 | .queue("brolo") 160 | .perform_async( 161 | &redis, 162 | PaymentReportArgs { 163 | user_guid: "USR-123-EXPIRED".into(), 164 | }, 165 | ) 166 | .await?; 167 | 168 | sidekiq::perform_async( 169 | &redis, 170 | "PaymentReportWorker".into(), 171 | "yolo".into(), 172 | PaymentReportArgs { 173 | user_guid: "USR-123".to_string(), 174 | }, 175 | ) 176 | .await?; 177 | 178 | // Enqueue a job 179 | sidekiq::perform_async( 180 | &redis, 181 | "PaymentReportWorker".into(), 182 | "yolo".into(), 183 | PaymentReportArgs { 184 | user_guid: "USR-123".to_string(), 185 | }, 186 | ) 187 | .await?; 188 | 189 | // Enqueue a job with options 190 | sidekiq::opts() 191 | .queue("yolo".to_string()) 192 | .perform_async( 193 | &redis, 194 | "PaymentReportWorker".into(), 195 | PaymentReportArgs { 196 | user_guid: "USR-123".to_string(), 197 | }, 198 | ) 199 | .await?; 200 | 201 | // Sidekiq server 202 | let mut p = Processor::new(redis.clone(), vec!["yolo".to_string(), "brolo".to_string()]); 203 | 204 | // Add known workers 205 | p.register(HelloWorker); 206 | p.register(PaymentReportWorker::new(redis.clone())); 207 | 208 | // Custom Middlewares 209 | p.using(FilterExpiredUsersMiddleware).await; 210 | 211 | // Reset cron jobs 212 | periodic::destroy_all(redis.clone()).await?; 213 | 214 | // Cron jobs 215 | periodic::builder("0 * * * * *")? 216 | .name("Payment report processing for a user using json args") 217 | .queue("yolo") 218 | .args(json!({ "user_guid": "USR-123-PERIODIC-FROM-JSON-ARGS" }))? 219 | .register(&mut p, PaymentReportWorker::new(redis.clone())) 220 | .await?; 221 | 222 | periodic::builder("0 * * * * *")? 223 | .name("Payment report processing for a user using typed args") 224 | .queue("yolo") 225 | .args(PaymentReportArgs { 226 | user_guid: "USR-123-PERIODIC-FROM-TYPED-ARGS".to_string(), 227 | })? 228 | .register(&mut p, PaymentReportWorker::new(redis.clone())) 229 | .await?; 230 | 231 | p.run().await; 232 | Ok(()) 233 | } 234 | -------------------------------------------------------------------------------- /examples/producer-demo.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use bb8::Pool; 3 | use serde::{Deserialize, Serialize}; 4 | use sidekiq::{ 5 | ChainIter, Job, RedisConnectionManager, Result, ServerMiddleware, Worker, WorkerRef, 6 | }; 7 | use std::sync::Arc; 8 | use tracing::{error, info}; 9 | 10 | #[derive(Clone)] 11 | struct HelloWorker; 12 | 13 | #[async_trait] 14 | impl Worker<()> for HelloWorker { 15 | async fn perform(&self, _args: ()) -> Result<()> { 16 | // I don't use any args. I do my own work. 17 | Ok(()) 18 | } 19 | } 20 | 21 | #[derive(Clone)] 22 | struct PaymentReportWorker {} 23 | 24 | impl PaymentReportWorker { 25 | async fn send_report(&self, user_guid: String) -> Result<()> { 26 | // TODO: Some actual work goes here... 27 | info!({"user_guid" = user_guid, "class_name" = Self::class_name()}, "Sending payment report to user"); 28 | 29 | Ok(()) 30 | } 31 | } 32 | 33 | #[derive(Deserialize, Debug, Serialize)] 34 | struct PaymentReportArgs { 35 | user_guid: String, 36 | } 37 | 38 | #[async_trait] 39 | impl Worker for PaymentReportWorker { 40 | fn opts() -> sidekiq::WorkerOpts { 41 | sidekiq::WorkerOpts::new().queue("yolo") 42 | } 43 | 44 | async fn perform(&self, args: PaymentReportArgs) -> Result<()> { 45 | self.send_report(args.user_guid).await 46 | } 47 | } 48 | 49 | struct FilterExpiredUsersMiddleware {} 50 | 51 | #[derive(Deserialize)] 52 | struct FiltereExpiredUsersArgs { 53 | user_guid: String, 54 | } 55 | 56 | impl FiltereExpiredUsersArgs { 57 | fn is_expired(&self) -> bool { 58 | self.user_guid == "USR-123-EXPIRED" 59 | } 60 | } 61 | 62 | #[async_trait] 63 | impl ServerMiddleware for FilterExpiredUsersMiddleware { 64 | async fn call( 65 | &self, 66 | chain: ChainIter, 67 | job: &Job, 68 | worker: Arc, 69 | redis: Pool, 70 | ) -> Result<()> { 71 | let args: std::result::Result<(FiltereExpiredUsersArgs,), serde_json::Error> = 72 | serde_json::from_value(job.args.clone()); 73 | 74 | // If we can safely deserialize then attempt to filter based on user guid. 75 | if let Ok((filter,)) = args { 76 | if filter.is_expired() { 77 | error!({ 78 | "class" = &job.class, 79 | "jid" = &job.jid, 80 | "user_guid" = filter.user_guid 81 | }, 82 | "Detected an expired user, skipping this job" 83 | ); 84 | return Ok(()); 85 | } 86 | } 87 | 88 | chain.next(job, worker, redis).await 89 | } 90 | } 91 | 92 | #[tokio::main] 93 | async fn main() -> Result<()> { 94 | tracing_subscriber::fmt::init(); 95 | 96 | // Redis 97 | let manager = RedisConnectionManager::new("redis://127.0.0.1/")?; 98 | let redis = Pool::builder().build(manager).await?; 99 | 100 | let mut n = 0; 101 | let mut last = 0; 102 | let mut then = std::time::Instant::now(); 103 | 104 | loop { 105 | PaymentReportWorker::perform_async( 106 | &redis, 107 | PaymentReportArgs { 108 | user_guid: "USR-123".into(), 109 | }, 110 | ) 111 | .await 112 | .unwrap(); 113 | 114 | //tokio::time::sleep(std::time::Duration::from_millis(1)).await; 115 | 116 | n += 1; 117 | 118 | if n % 100000 == 0 { 119 | let now = std::time::Instant::now(); 120 | let delta = n - last; 121 | last = n; 122 | let delta_time = now - then; 123 | if delta_time.as_secs() == 0 { 124 | continue; 125 | } 126 | then = now; 127 | let rate = delta / delta_time.as_secs(); 128 | println!("Iterations since last: {delta} at a rate of: {rate} iter/sec"); 129 | } 130 | } 131 | 132 | // // Enqueue a job with the worker! There are many ways to do this. 133 | // PaymentReportWorker::perform_async( 134 | // &mut redis, 135 | // PaymentReportArgs { 136 | // user_guid: "USR-123".into(), 137 | // }, 138 | // ) 139 | // .await?; 140 | // 141 | // PaymentReportWorker::perform_in( 142 | // &mut redis, 143 | // std::time::Duration::from_secs(10), 144 | // PaymentReportArgs { 145 | // user_guid: "USR-123".into(), 146 | // }, 147 | // ) 148 | // .await?; 149 | // 150 | // PaymentReportWorker::opts() 151 | // .queue("brolo") 152 | // .perform_async( 153 | // &mut redis, 154 | // PaymentReportArgs { 155 | // user_guid: "USR-123-EXPIRED".into(), 156 | // }, 157 | // ) 158 | // .await?; 159 | // 160 | // sidekiq::perform_async( 161 | // &mut redis, 162 | // "PaymentReportWorker".into(), 163 | // "yolo".into(), 164 | // PaymentReportArgs { 165 | // user_guid: "USR-123".to_string(), 166 | // }, 167 | // ) 168 | // .await?; 169 | // 170 | // // Enqueue a job 171 | // sidekiq::perform_async( 172 | // &mut redis, 173 | // "PaymentReportWorker".into(), 174 | // "yolo".into(), 175 | // PaymentReportArgs { 176 | // user_guid: "USR-123".to_string(), 177 | // }, 178 | // ) 179 | // .await?; 180 | // 181 | // // Enqueue a job with options 182 | // sidekiq::opts() 183 | // .queue("yolo".to_string()) 184 | // .perform_async( 185 | // &mut redis, 186 | // "PaymentReportWorker".into(), 187 | // PaymentReportArgs { 188 | // user_guid: "USR-123".to_string(), 189 | // }, 190 | // ) 191 | // .await?; 192 | 193 | // // Sidekiq server 194 | // let mut p = Processor::new( 195 | // redis.clone(), 196 | // logger.clone(), 197 | // //vec!["yolo".to_string(), "brolo".to_string()], 198 | // vec![], 199 | // ); 200 | // 201 | // // // Add known workers 202 | // // p.register(HelloWorker); 203 | // // p.register(PaymentReportWorker::new(logger.clone())); 204 | // // 205 | // // Custom Middlewares 206 | // p.using(FilterExpiredUsersMiddleware::new(logger.clone())) 207 | // .await; 208 | // 209 | // // Reset cron jobs 210 | // periodic::destroy_all(redis.clone()).await?; 211 | // 212 | // // Cron jobs 213 | // periodic::builder("0 * * * * *")? 214 | // .name("Payment report processing for a random user") 215 | // .queue("yolo") 216 | // //.args(PaymentReportArgs { 217 | // // user_guid: "USR-123-PERIODIC".to_string(), 218 | // //})? 219 | // .args(json!({ "user_guid": "USR-123-PERIODIC" }))? 220 | // .register(&mut p, PaymentReportWorker::new(logger.clone())) 221 | // .await?; 222 | // 223 | // p.run().await; 224 | } 225 | -------------------------------------------------------------------------------- /src/redis.rs: -------------------------------------------------------------------------------- 1 | use bb8::{CustomizeConnection, ManageConnection, Pool}; 2 | use redis::AsyncCommands; 3 | pub use redis::RedisError; 4 | use redis::ToRedisArgs; 5 | pub use redis::Value as RedisValue; 6 | use redis::{aio::MultiplexedConnection as Connection, ErrorKind}; 7 | use redis::{Client, IntoConnectionInfo}; 8 | use std::future::Future; 9 | use std::ops::DerefMut; 10 | use std::pin::Pin; 11 | 12 | pub type RedisPool = Pool; 13 | 14 | #[derive(Debug)] 15 | pub struct NamespaceCustomizer { 16 | namespace: String, 17 | } 18 | 19 | impl CustomizeConnection for NamespaceCustomizer { 20 | fn on_acquire<'a>( 21 | &'a self, 22 | connection: &'a mut RedisConnection, 23 | ) -> Pin> + Send + 'a>> { 24 | Box::pin(async { 25 | // All redis operations used by the sidekiq lib will use this as a prefix. 26 | connection.set_namespace(self.namespace.clone()); 27 | 28 | Ok(()) 29 | }) 30 | } 31 | } 32 | 33 | #[must_use] 34 | pub fn with_custom_namespace(namespace: String) -> Box { 35 | Box::new(NamespaceCustomizer { namespace }) 36 | } 37 | 38 | /// A `bb8::ManageConnection` for `redis::Client::get_async_connection` wrapped in a helper type 39 | /// for namespacing. 40 | #[derive(Clone, Debug)] 41 | pub struct RedisConnectionManager { 42 | client: Client, 43 | } 44 | 45 | impl RedisConnectionManager { 46 | /// Create a new `RedisConnectionManager`. 47 | /// See `redis::Client::open` for a description of the parameter types. 48 | pub fn new(info: T) -> Result { 49 | Ok(Self { 50 | client: Client::open(info.into_connection_info()?)?, 51 | }) 52 | } 53 | } 54 | 55 | impl ManageConnection for RedisConnectionManager { 56 | type Connection = RedisConnection; 57 | type Error = RedisError; 58 | 59 | async fn connect(&self) -> Result { 60 | Ok(RedisConnection::new( 61 | self.client.get_multiplexed_async_connection().await?, 62 | )) 63 | } 64 | 65 | async fn is_valid(&self, mut conn: &mut Self::Connection) -> Result<(), Self::Error> { 66 | let pong: String = redis::cmd("PING") 67 | .query_async(&mut conn.deref_mut().connection) 68 | .await?; 69 | match pong.as_str() { 70 | "PONG" => Ok(()), 71 | _ => Err((ErrorKind::ResponseError, "ping request").into()), 72 | } 73 | } 74 | 75 | fn has_broken(&self, _conn: &mut Self::Connection) -> bool { 76 | false 77 | } 78 | } 79 | 80 | /// A wrapper type for making the redis crate compatible with namespacing. 81 | pub struct RedisConnection { 82 | connection: Connection, 83 | namespace: Option, 84 | } 85 | 86 | impl RedisConnection { 87 | #[must_use] 88 | pub fn new(connection: Connection) -> Self { 89 | Self { 90 | connection, 91 | namespace: None, 92 | } 93 | } 94 | 95 | pub fn set_namespace(&mut self, namespace: String) { 96 | self.namespace = Some(namespace); 97 | } 98 | 99 | #[must_use] 100 | pub fn with_namespace(self, namespace: String) -> Self { 101 | Self { 102 | connection: self.connection, 103 | namespace: Some(namespace), 104 | } 105 | } 106 | 107 | fn namespaced_key(&self, key: String) -> String { 108 | if let Some(ref namespace) = self.namespace { 109 | return format!("{namespace}:{key}"); 110 | } 111 | 112 | key 113 | } 114 | 115 | fn namespaced_keys(&self, keys: Vec) -> Vec { 116 | if let Some(ref namespace) = self.namespace { 117 | let keys: Vec = keys 118 | .iter() 119 | .map(|key| format!("{namespace}:{key}")) 120 | .collect(); 121 | 122 | return keys; 123 | } 124 | 125 | keys 126 | } 127 | 128 | /// This allows you to borrow the raw redis connection without any namespacing support. 129 | pub fn unnamespaced_borrow_mut(&mut self) -> &mut Connection { 130 | &mut self.connection 131 | } 132 | 133 | pub async fn brpop( 134 | &mut self, 135 | keys: Vec, 136 | timeout: usize, 137 | ) -> Result, RedisError> { 138 | self.connection 139 | .brpop(self.namespaced_keys(keys), timeout as f64) 140 | .await 141 | } 142 | 143 | pub fn cmd_with_key(&mut self, cmd: &str, key: String) -> redis::Cmd { 144 | let mut c = redis::cmd(cmd); 145 | c.arg(self.namespaced_key(key)); 146 | c 147 | } 148 | 149 | pub async fn del(&mut self, key: String) -> Result { 150 | self.connection.del(self.namespaced_key(key)).await 151 | } 152 | 153 | pub async fn expire(&mut self, key: String, value: usize) -> Result { 154 | self.connection 155 | .expire(self.namespaced_key(key), value as i64) 156 | .await 157 | } 158 | 159 | pub async fn lpush(&mut self, key: String, value: V) -> Result<(), RedisError> 160 | where 161 | V: ToRedisArgs + Send + Sync, 162 | { 163 | self.connection.lpush(self.namespaced_key(key), value).await 164 | } 165 | 166 | pub async fn sadd(&mut self, key: String, value: V) -> Result<(), RedisError> 167 | where 168 | V: ToRedisArgs + Send + Sync, 169 | { 170 | self.connection.sadd(self.namespaced_key(key), value).await 171 | } 172 | 173 | pub async fn set_nx_ex( 174 | &mut self, 175 | key: String, 176 | value: V, 177 | ttl_in_seconds: usize, 178 | ) -> Result 179 | where 180 | V: ToRedisArgs + Send + Sync, 181 | { 182 | redis::cmd("SET") 183 | .arg(self.namespaced_key(key)) 184 | .arg(value) 185 | .arg("NX") 186 | .arg("EX") 187 | .arg(ttl_in_seconds) 188 | .query_async(self.unnamespaced_borrow_mut()) 189 | .await 190 | } 191 | 192 | pub async fn zrange( 193 | &mut self, 194 | key: String, 195 | lower: isize, 196 | upper: isize, 197 | ) -> Result, RedisError> { 198 | self.connection 199 | .zrange(self.namespaced_key(key), lower, upper) 200 | .await 201 | } 202 | 203 | pub async fn zrangebyscore_limit( 204 | &mut self, 205 | key: String, 206 | lower: L, 207 | upper: U, 208 | offset: isize, 209 | limit: isize, 210 | ) -> Result, RedisError> { 211 | self.connection 212 | .zrangebyscore_limit(self.namespaced_key(key), lower, upper, offset, limit) 213 | .await 214 | } 215 | 216 | pub async fn zadd( 217 | &mut self, 218 | key: String, 219 | value: V, 220 | score: S, 221 | ) -> Result { 222 | self.connection 223 | .zadd(self.namespaced_key(key), value, score) 224 | .await 225 | } 226 | 227 | pub async fn zadd_ch( 228 | &mut self, 229 | key: String, 230 | value: V, 231 | score: S, 232 | ) -> Result { 233 | redis::cmd("ZADD") 234 | .arg(self.namespaced_key(key)) 235 | .arg("CH") 236 | .arg(score) 237 | .arg(value) 238 | .query_async(self.unnamespaced_borrow_mut()) 239 | .await 240 | } 241 | 242 | pub async fn zrem(&mut self, key: String, value: V) -> Result 243 | where 244 | V: ToRedisArgs + Send + Sync, 245 | { 246 | self.connection.zrem(self.namespaced_key(key), value).await 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/middleware.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use crate::{Counter, Job, RedisPool, RetryOpts, UnitOfWork, WorkerRef}; 3 | use async_trait::async_trait; 4 | use std::sync::Arc; 5 | use tokio::sync::RwLock; 6 | use tracing::error; 7 | 8 | #[async_trait] 9 | pub trait ServerMiddleware { 10 | async fn call( 11 | &self, 12 | iter: ChainIter, 13 | job: &Job, 14 | worker: Arc, 15 | redis: RedisPool, 16 | ) -> Result<()>; 17 | } 18 | 19 | /// A pseudo iterator used to know which middleware should be called next. 20 | /// This is created by the Chain type. 21 | #[derive(Clone)] 22 | pub struct ChainIter { 23 | stack: Arc>>>, 24 | index: usize, 25 | } 26 | 27 | impl ChainIter { 28 | #[inline] 29 | pub async fn next(&self, job: &Job, worker: Arc, redis: RedisPool) -> Result<()> { 30 | let stack = self.stack.read().await; 31 | 32 | if let Some(middleware) = stack.get(self.index) { 33 | middleware 34 | .call( 35 | ChainIter { 36 | stack: self.stack.clone(), 37 | index: self.index + 1, 38 | }, 39 | job, 40 | worker, 41 | redis, 42 | ) 43 | .await?; 44 | } 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | /// A chain of middlewares that will be called in order by different server middlewares. 51 | #[derive(Clone)] 52 | pub(crate) struct Chain { 53 | stack: Arc>>>, 54 | } 55 | 56 | impl Chain { 57 | // Testing helper to get an empty chain. 58 | #[allow(dead_code)] 59 | pub(crate) fn empty() -> Self { 60 | Self { 61 | stack: Arc::new(RwLock::new(vec![])), 62 | } 63 | } 64 | 65 | pub(crate) fn new_with_stats(counter: Counter) -> Self { 66 | Self { 67 | stack: Arc::new(RwLock::new(vec![ 68 | Box::new(RetryMiddleware), 69 | Box::new(StatsMiddleware::new(counter)), 70 | Box::new(HandlerMiddleware), 71 | ])), 72 | } 73 | } 74 | 75 | pub(crate) async fn using(&mut self, middleware: Box) { 76 | let mut stack = self.stack.write().await; 77 | // HACK: Insert after retry middleware but before the handler middleware. 78 | let index = if stack.is_empty() { 0 } else { stack.len() - 1 }; 79 | 80 | stack.insert(index, middleware); 81 | } 82 | 83 | #[inline] 84 | pub(crate) fn iter(&self) -> ChainIter { 85 | ChainIter { 86 | stack: self.stack.clone(), 87 | index: 0, 88 | } 89 | } 90 | 91 | #[inline] 92 | pub(crate) async fn call( 93 | &mut self, 94 | job: &Job, 95 | worker: Arc, 96 | redis: RedisPool, 97 | ) -> Result<()> { 98 | // The middleware must call bottom of the stack to the top. 99 | // Each middleware should receive a lambda to the next middleware 100 | // up the stack. Each middleware can short-circuit the stack by 101 | // not calling the "next" middleware. 102 | self.iter().next(job, worker, redis).await 103 | } 104 | } 105 | 106 | pub struct StatsMiddleware { 107 | busy_count: Counter, 108 | } 109 | 110 | impl StatsMiddleware { 111 | fn new(busy_count: Counter) -> Self { 112 | Self { busy_count } 113 | } 114 | } 115 | 116 | #[async_trait] 117 | impl ServerMiddleware for StatsMiddleware { 118 | #[inline] 119 | async fn call( 120 | &self, 121 | chain: ChainIter, 122 | job: &Job, 123 | worker: Arc, 124 | redis: RedisPool, 125 | ) -> Result<()> { 126 | self.busy_count.incrby(1); 127 | let res = chain.next(job, worker, redis).await; 128 | self.busy_count.decrby(1); 129 | res 130 | } 131 | } 132 | 133 | struct HandlerMiddleware; 134 | 135 | #[async_trait] 136 | impl ServerMiddleware for HandlerMiddleware { 137 | #[inline] 138 | async fn call( 139 | &self, 140 | _chain: ChainIter, 141 | job: &Job, 142 | worker: Arc, 143 | _redis: RedisPool, 144 | ) -> Result<()> { 145 | worker.call(job.args.clone()).await 146 | } 147 | } 148 | 149 | struct RetryMiddleware; 150 | 151 | #[async_trait] 152 | impl ServerMiddleware for RetryMiddleware { 153 | #[inline] 154 | async fn call( 155 | &self, 156 | chain: ChainIter, 157 | job: &Job, 158 | worker: Arc, 159 | redis: RedisPool, 160 | ) -> Result<()> { 161 | // Check the job for a max retries N in the retry field and then fall 162 | // back to the worker default max retries. 163 | let max_retries = if let RetryOpts::Max(max_retries) = job.retry { 164 | max_retries 165 | } else { 166 | worker.max_retries() 167 | }; 168 | 169 | let err = { 170 | match chain.next(job, worker, redis.clone()).await { 171 | Ok(()) => return Ok(()), 172 | Err(err) => format!("{err:?}"), 173 | } 174 | }; 175 | 176 | let mut job = job.clone(); 177 | 178 | // Update error fields on the job. 179 | job.error_message = Some(err); 180 | if job.retry_count.is_some() { 181 | job.retried_at = Some(chrono::Utc::now().timestamp() as f64); 182 | } else { 183 | job.failed_at = Some(chrono::Utc::now().timestamp() as f64); 184 | } 185 | let retry_count = job.retry_count.unwrap_or(0) + 1; 186 | job.retry_count = Some(retry_count); 187 | 188 | // Attempt the retry. 189 | if retry_count > max_retries || job.retry == RetryOpts::Never { 190 | error!({ 191 | "status" = "fail", 192 | "class" = &job.class, 193 | "jid" = &job.jid, 194 | "queue" = &job.queue, 195 | "err" = &job.error_message 196 | }, "Max retries exceeded, will not reschedule job"); 197 | } else { 198 | error!({ 199 | "status" = "fail", 200 | "class" = &job.class, 201 | "jid" = &job.jid, 202 | "queue" = &job.queue, 203 | "retry_queue" = &job.retry_queue, 204 | "err" = &job.error_message 205 | }, "Scheduling job for retry in the future"); 206 | 207 | // We will now make sure we use the new retry_queue option if set. 208 | if let Some(ref retry_queue) = job.retry_queue { 209 | job.queue = retry_queue.into(); 210 | } 211 | 212 | UnitOfWork::from_job(job).reenqueue(&redis).await?; 213 | } 214 | 215 | Ok(()) 216 | } 217 | } 218 | 219 | #[cfg(test)] 220 | mod test { 221 | use super::*; 222 | use crate::{RedisConnectionManager, RedisPool, RetryOpts, Worker}; 223 | use bb8::Pool; 224 | use tokio::sync::Mutex; 225 | 226 | async fn redis() -> RedisPool { 227 | let manager = RedisConnectionManager::new("redis://127.0.0.1/").unwrap(); 228 | Pool::builder().build(manager).await.unwrap() 229 | } 230 | 231 | fn job() -> Job { 232 | Job { 233 | class: "TestWorker".into(), 234 | queue: "default".into(), 235 | args: vec![1337].into(), 236 | retry: RetryOpts::Yes, 237 | jid: crate::new_jid(), 238 | created_at: 1337.0, 239 | enqueued_at: None, 240 | failed_at: None, 241 | error_message: None, 242 | error_class: None, 243 | retry_count: None, 244 | retried_at: None, 245 | retry_queue: None, 246 | unique_for: None, 247 | } 248 | } 249 | 250 | #[derive(Clone)] 251 | struct TestWorker { 252 | touched: Arc>, 253 | } 254 | 255 | #[async_trait] 256 | impl Worker<()> for TestWorker { 257 | async fn perform(&self, _args: ()) -> Result<()> { 258 | *self.touched.lock().await = true; 259 | Ok(()) 260 | } 261 | } 262 | 263 | #[tokio::test] 264 | async fn calls_through_a_middleware_stack() { 265 | let inner = Arc::new(TestWorker { 266 | touched: Arc::new(Mutex::new(false)), 267 | }); 268 | let worker = Arc::new(WorkerRef::wrap(Arc::clone(&inner))); 269 | 270 | let job = job(); 271 | let mut chain = Chain::empty(); 272 | chain.using(Box::new(HandlerMiddleware)).await; 273 | chain 274 | .call(&job, worker.clone(), redis().await) 275 | .await 276 | .unwrap(); 277 | 278 | assert!( 279 | *inner.touched.lock().await, 280 | "The job was processed by the middleware", 281 | ); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /tests/server_middleware_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod test { 3 | use async_trait::async_trait; 4 | use bb8::Pool; 5 | use serial_test::serial; 6 | use sidekiq::{ 7 | ChainIter, Error, Job, Processor, RedisConnectionManager, RedisPool, Result, RetryOpts, 8 | Scheduled, ServerMiddleware, UnitOfWork, WorkFetcher, Worker, WorkerRef, 9 | }; 10 | use std::sync::{Arc, Mutex}; 11 | 12 | #[async_trait] 13 | trait FlushAll { 14 | async fn flushall(&self); 15 | } 16 | 17 | #[async_trait] 18 | impl FlushAll for RedisPool { 19 | async fn flushall(&self) { 20 | let mut conn = self.get().await.unwrap(); 21 | let _: String = redis::cmd("FLUSHALL") 22 | .arg("SYNC") 23 | .query_async(conn.unnamespaced_borrow_mut()) 24 | .await 25 | .unwrap(); 26 | } 27 | } 28 | 29 | async fn new_base_processor(queue: String) -> (Processor, RedisPool) { 30 | // Redis 31 | let manager = RedisConnectionManager::new("redis://127.0.0.1/").unwrap(); 32 | let redis = Pool::builder().build(manager).await.unwrap(); 33 | redis.flushall().await; 34 | 35 | // Sidekiq server 36 | let p = Processor::new(redis.clone(), vec![queue]); 37 | 38 | (p, redis) 39 | } 40 | 41 | #[derive(Clone)] 42 | struct AlwaysFailWorker; 43 | 44 | #[async_trait] 45 | impl Worker<()> for AlwaysFailWorker { 46 | async fn perform(&self, _args: ()) -> Result<()> { 47 | Err(Error::Message("big ouchie".to_string())) 48 | } 49 | } 50 | 51 | #[derive(Clone)] 52 | struct TestWorker { 53 | did_process: Arc>, 54 | } 55 | 56 | #[async_trait] 57 | impl Worker<()> for TestWorker { 58 | async fn perform(&self, _args: ()) -> Result<()> { 59 | let mut this = self.did_process.lock().unwrap(); 60 | *this = true; 61 | 62 | Ok(()) 63 | } 64 | } 65 | 66 | #[derive(Clone)] 67 | struct TestMiddleware { 68 | should_halt: bool, 69 | did_process: Arc>, 70 | } 71 | 72 | #[async_trait] 73 | impl ServerMiddleware for TestMiddleware { 74 | async fn call( 75 | &self, 76 | chain: ChainIter, 77 | job: &Job, 78 | worker: Arc, 79 | redis: RedisPool, 80 | ) -> Result<()> { 81 | { 82 | let mut this = self.did_process.lock().unwrap(); 83 | *this = true; 84 | } 85 | 86 | if self.should_halt { 87 | return Ok(()); 88 | } else { 89 | return chain.next(job, worker, redis).await; 90 | } 91 | } 92 | } 93 | 94 | #[tokio::test] 95 | #[serial] 96 | async fn can_process_job_with_middleware() { 97 | let worker = TestWorker { 98 | did_process: Arc::new(Mutex::new(false)), 99 | }; 100 | let queue = "random123".to_string(); 101 | let (mut p, redis) = new_base_processor(queue.clone()).await; 102 | 103 | let middleware = TestMiddleware { 104 | should_halt: false, 105 | did_process: Arc::new(Mutex::new(false)), 106 | }; 107 | 108 | p.register(worker.clone()); 109 | p.using(middleware.clone()).await; 110 | 111 | TestWorker::opts() 112 | .queue(queue) 113 | .perform_async(&redis, ()) 114 | .await 115 | .unwrap(); 116 | 117 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 118 | assert!(*worker.did_process.lock().unwrap()); 119 | assert!(*middleware.did_process.lock().unwrap()); 120 | } 121 | 122 | #[tokio::test] 123 | #[serial] 124 | async fn can_prevent_job_from_being_processed_with_halting_middleware() { 125 | let worker = TestWorker { 126 | did_process: Arc::new(Mutex::new(false)), 127 | }; 128 | let queue = "random123".to_string(); 129 | let (mut p, redis) = new_base_processor(queue.clone()).await; 130 | 131 | let middleware = TestMiddleware { 132 | should_halt: true, 133 | did_process: Arc::new(Mutex::new(false)), 134 | }; 135 | 136 | p.register(worker.clone()); 137 | p.using(middleware.clone()).await; 138 | 139 | TestWorker::opts() 140 | .queue(queue) 141 | .perform_async(&redis, ()) 142 | .await 143 | .unwrap(); 144 | 145 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 146 | assert!(!*worker.did_process.lock().unwrap()); 147 | assert!(*middleware.did_process.lock().unwrap()); 148 | } 149 | 150 | #[tokio::test] 151 | #[serial] 152 | async fn can_retry_a_job() { 153 | let worker = AlwaysFailWorker; 154 | let queue = "failure_zone".to_string(); 155 | let (mut p, redis) = new_base_processor(queue.clone()).await; 156 | p.register(worker.clone()); 157 | 158 | AlwaysFailWorker::opts() 159 | .queue(queue) 160 | .retry(true) 161 | .perform_async(&redis, ()) 162 | .await 163 | .unwrap(); 164 | 165 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 166 | let sets = vec!["retry".to_string()]; 167 | let sched = Scheduled::new(redis.clone()); 168 | let future_date = chrono::Utc::now() + chrono::Duration::days(30); 169 | 170 | // We should be able to reenqueue the job. 171 | let n_jobs_retried = sched.enqueue_jobs(future_date, &sets).await; 172 | assert!(n_jobs_retried.is_ok()); 173 | let n_jobs_retried = n_jobs_retried.unwrap(); 174 | assert_eq!(n_jobs_retried, 1, "one job in the retry queue"); 175 | 176 | // Let's grab that job. 177 | let job = p.fetch().await; 178 | assert!(job.is_ok()); 179 | let job = job.unwrap(); 180 | assert!(job.is_some()); 181 | let job = job.unwrap(); 182 | 183 | assert_eq!(job.job.retry, RetryOpts::Yes); 184 | assert_eq!(job.job.retry_count, Some(1)); 185 | assert_eq!(job.job.class, "AlwaysFailWorker"); 186 | } 187 | 188 | #[tokio::test] 189 | #[serial] 190 | async fn can_retry_only_until_the_max_global_retries() { 191 | let worker = AlwaysFailWorker; 192 | let queue = "failure_zone_global".to_string(); 193 | let (mut p, redis) = new_base_processor(queue.clone()).await; 194 | p.register(worker.clone()); 195 | 196 | let mut job = AlwaysFailWorker::opts() 197 | .queue(queue) 198 | .retry(true) 199 | .into_opts() 200 | .create_job(AlwaysFailWorker::class_name(), ()) 201 | .expect("never fails"); 202 | 203 | // One last retry remaining. 204 | assert_eq!(worker.max_retries(), 25, "default is 25 retries"); 205 | job.retry_count = Some(worker.max_retries()); 206 | 207 | UnitOfWork::from_job(job) 208 | .enqueue(&redis) 209 | .await 210 | .expect("enqueues"); 211 | 212 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 213 | let sets = vec!["retry".to_string()]; 214 | let sched = Scheduled::new(redis.clone()); 215 | let future_date = chrono::Utc::now() + chrono::Duration::days(30); 216 | 217 | // We should have no jobs that need retrying. 218 | let n_jobs_retried = sched.enqueue_jobs(future_date, &sets).await; 219 | assert!(n_jobs_retried.is_ok()); 220 | let n_jobs_retried = n_jobs_retried.unwrap(); 221 | 222 | assert_eq!(n_jobs_retried, 0, "no jobs in the retry queue"); 223 | } 224 | 225 | #[tokio::test] 226 | #[serial] 227 | async fn can_retry_based_on_job_opts_retries() { 228 | let worker = AlwaysFailWorker; 229 | let queue = "failure_zone_max_on_job".to_string(); 230 | let (mut p, redis) = new_base_processor(queue.clone()).await; 231 | p.register(worker.clone()); 232 | 233 | let mut job = AlwaysFailWorker::opts() 234 | .queue(queue) 235 | .retry(5) 236 | .into_opts() 237 | .create_job(AlwaysFailWorker::class_name(), ()) 238 | .expect("never fails"); 239 | 240 | // One last retry remaining from the retry(5) on the job params. 241 | assert_eq!(worker.max_retries(), 25, "default is 25 retries"); 242 | job.retry_count = Some(5); 243 | 244 | UnitOfWork::from_job(job) 245 | .enqueue(&redis) 246 | .await 247 | .expect("enqueues"); 248 | 249 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 250 | let sets = vec!["retry".to_string()]; 251 | let sched = Scheduled::new(redis.clone()); 252 | let future_date = chrono::Utc::now() + chrono::Duration::days(30); 253 | 254 | // We should have no jobs that need retrying. 255 | let n_jobs_retried = sched.enqueue_jobs(future_date, &sets).await; 256 | assert!(n_jobs_retried.is_ok()); 257 | let n_jobs_retried = n_jobs_retried.unwrap(); 258 | 259 | assert_eq!(n_jobs_retried, 0, "no jobs in the retry queue"); 260 | } 261 | 262 | #[tokio::test] 263 | #[serial] 264 | async fn can_set_retry_to_false_per_job() { 265 | let worker = AlwaysFailWorker; 266 | let queue = "failure_zone_never_retry_the_job".to_string(); 267 | let (mut p, redis) = new_base_processor(queue.clone()).await; 268 | p.register(worker.clone()); 269 | 270 | AlwaysFailWorker::opts() 271 | .queue(queue) 272 | .retry(false) 273 | .perform_async(&redis, ()) 274 | .await 275 | .expect("never fails"); 276 | 277 | // One last retry remaining from the retry(5) on the job params. 278 | assert_eq!(worker.max_retries(), 25, "default is 25 retries"); 279 | 280 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 281 | let sets = vec!["retry".to_string()]; 282 | let sched = Scheduled::new(redis.clone()); 283 | let future_date = chrono::Utc::now() + chrono::Duration::days(30); 284 | 285 | // We should have no jobs that need retrying. 286 | let n_jobs_retried = sched.enqueue_jobs(future_date, &sets).await; 287 | assert!(n_jobs_retried.is_ok()); 288 | let n_jobs_retried = n_jobs_retried.unwrap(); 289 | 290 | assert_eq!(n_jobs_retried, 0, "no jobs in the retry queue"); 291 | } 292 | 293 | #[tokio::test] 294 | #[serial] 295 | async fn can_retry_job_into_different_retry_queue() { 296 | let worker = AlwaysFailWorker; 297 | let queue = "failure_zone_max_on_job".to_string(); 298 | let retry_queue = "the_retry_queue".to_string(); 299 | let (mut p, redis) = new_base_processor(queue.clone()).await; 300 | let (mut retry_p, _retry_redis) = new_base_processor(retry_queue.clone()).await; 301 | p.register(worker.clone()); 302 | 303 | let mut job = AlwaysFailWorker::opts() 304 | .queue(queue) 305 | .retry(5) 306 | .retry_queue(&retry_queue) 307 | .perform_async(&redis, ()) 308 | .await 309 | .expect("enqueues"); 310 | 311 | assert_eq!(p.process_one_tick_once().await.unwrap(), WorkFetcher::Done); 312 | let sets = vec!["retry".to_string()]; 313 | let sched = Scheduled::new(redis.clone()); 314 | let future_date = chrono::Utc::now() + chrono::Duration::days(30); 315 | 316 | // We should have one job that needs retrying. 317 | let n_jobs_retried = sched.enqueue_jobs(future_date, &sets).await; 318 | assert!(n_jobs_retried.is_ok()); 319 | let n_jobs_retried = n_jobs_retried.unwrap(); 320 | assert_eq!(n_jobs_retried, 1, "we have one job to retry in the queue"); 321 | 322 | // Let's grab that job. 323 | let job = retry_p.fetch().await; 324 | assert!(job.is_ok()); 325 | let job = job.unwrap(); 326 | assert!(job.is_some()); 327 | let job = job.unwrap(); 328 | 329 | assert_eq!(job.job.class, "AlwaysFailWorker"); 330 | assert_eq!(job.job.retry_queue, Some(retry_queue)); 331 | assert_eq!(job.job.retry_count, Some(1)); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sidekiq.rs (aka `rusty-sidekiq`) 2 | ================================ 3 | 4 | [![crates.io](https://img.shields.io/crates/v/rusty-sidekiq.svg)](https://crates.io/crates/rusty-sidekiq/) 5 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.md) 6 | [![Documentation](https://docs.rs/rusty-sidekiq/badge.svg)](https://docs.rs/rusty-sidekiq/) 7 | 8 | This is a reimplementation of sidekiq in rust. It is compatible with sidekiq.rb for both submitting and processing jobs. 9 | Sidekiq.rb is obviously much more mature than this repo, but I hope you enjoy using it. This library is built using tokio 10 | so it is async by default. 11 | 12 | 13 | ## The Worker 14 | 15 | This library uses serde to make worker arguments strongly typed as needed. Below is an example of a worker with strongly 16 | typed arguments. It also has custom options that will be used whenever a job is submitted. These can be overridden at 17 | enqueue time making it easy to change the queue name, for example, should you need to. 18 | 19 | ```rust 20 | use tracing::info; 21 | use sidekiq::Result; 22 | 23 | #[derive(Clone)] 24 | struct PaymentReportWorker {} 25 | 26 | impl PaymentReportWorker { 27 | fn new() -> Self { 28 | Self { } 29 | } 30 | 31 | async fn send_report(&self, user_guid: String) -> Result<()> { 32 | // TODO: Some actual work goes here... 33 | info!({"user_guid" = user_guid}, "Sending payment report to user"); 34 | 35 | Ok(()) 36 | } 37 | } 38 | 39 | #[derive(Deserialize, Debug, Serialize)] 40 | struct PaymentReportArgs { 41 | user_guid: String, 42 | } 43 | 44 | #[async_trait] 45 | impl Worker for PaymentReportWorker { 46 | // Default worker options 47 | fn opts() -> sidekiq::WorkerOpts { 48 | sidekiq::WorkerOpts::new().queue("yolo") 49 | } 50 | 51 | // Worker implementation 52 | async fn perform(&self, args: PaymentReportArgs) -> Result<()> { 53 | self.send_report(args.user_guid).await 54 | } 55 | } 56 | ``` 57 | 58 | 59 | ## Creating a Job 60 | 61 | There are several ways to insert a job, but for this example, we'll keep it simple. Given some worker, insert using strongly 62 | typed arguments. 63 | 64 | ```rust 65 | PaymentReportWorker::perform_async( 66 | &mut redis, 67 | PaymentReportArgs { 68 | user_guid: "USR-123".into(), 69 | }, 70 | ) 71 | .await?; 72 | ``` 73 | 74 | You can make custom overrides at enqueue time. 75 | 76 | ```rust 77 | PaymentReportWorker::opts() 78 | .queue("brolo") 79 | .perform_async( 80 | &mut redis, 81 | PaymentReportArgs { 82 | user_guid: "USR-123".into(), 83 | }, 84 | ) 85 | .await?; 86 | ``` 87 | 88 | Or you can have more control by using the crate level method. 89 | 90 | ```rust 91 | sidekiq::perform_async( 92 | &mut redis, 93 | "PaymentReportWorker".into(), 94 | "yolo".into(), 95 | PaymentReportArgs { 96 | user_guid: "USR-123".to_string(), 97 | }, 98 | ) 99 | .await?; 100 | ``` 101 | 102 | See more examples in `examples/demo.rs`. 103 | 104 | #### Unique jobs 105 | 106 | Unique jobs are supported via the `unique_for` option which can be defined by default on the 107 | worker or via `SomeWorker::opts().unique_for(duration)`. See the `examples/unique.rs` example 108 | to only enqueue a job that is unique via (worker_name, queue_name, sha256_hash_of_job_args) for 109 | some defined `ttl`. Note: This is using `SET key value NX EX duration` under the hood as a "good 110 | enough" lock on the job. 111 | 112 | 113 | ## Starting the Server 114 | 115 | Below is an example of how you should create a `Processor`, register workers, include any 116 | custom middlewares, and start the server. 117 | 118 | ```rust 119 | // Redis 120 | let manager = sidekiq::RedisConnectionManager::new("redis://127.0.0.1/").unwrap(); 121 | let mut redis = bb8::Pool::builder().build(manager).await.unwrap(); 122 | 123 | // Sidekiq server 124 | let mut p = Processor::new( 125 | redis, 126 | vec!["yolo".to_string(), "brolo".to_string()], 127 | ); 128 | 129 | // Add known workers 130 | p.register(PaymentReportWorker::new()); 131 | 132 | // Custom Middlewares 133 | p.using(FilterExpiredUsersMiddleware::new()) 134 | .await; 135 | 136 | // Start the server 137 | p.run().await; 138 | ``` 139 | 140 | 141 | ## Periodic Jobs 142 | 143 | Periodic cron jobs are supported out of the box. All you need to specify is a valid 144 | cron string and a worker instance. You can optionally supply arguments, a queue, a 145 | retry flag, and a name that will be logged when a worker is submitted. 146 | 147 | Example: 148 | 149 | ```rust 150 | // Clear out all periodic jobs and their schedules 151 | periodic::destroy_all(redis).await?; 152 | 153 | // Add a new periodic job 154 | periodic::builder("0 0 8 * * *")? 155 | .name("Email clients with an oustanding balance daily at 8am UTC") 156 | .queue("reminders") 157 | .args(EmailReminderArgs { 158 | report_type: "outstanding_balance", 159 | })? 160 | .register(&mut p, EmailReminderWorker) 161 | .await?; 162 | ``` 163 | 164 | Periodic jobs are not removed automatically. If your project adds a periodic job and 165 | then later removes the `periodic::builder` call, the periodic job will still exist in 166 | redis. You can call `periodic::destroy_all(redis).await?` at the start of your program 167 | to ensure only the periodic jobs added by the latest version of your program will be 168 | executed. 169 | 170 | The implementation relies on a sorted set in redis. It stores a json payload of the 171 | periodic job with a score equal to the next scheduled UTC time of the cron string. All 172 | processes will periodically poll for changes and atomically update the score to the new 173 | next scheduled UTC time for the cron string. The worker that successfully changes the 174 | score atomically will enqueue a new job. Processes that don't successfully update the 175 | score will move on. This implementation detail means periodic jobs never leave redis. 176 | Another detail is that json when decoded and then encoded might not produce the same 177 | value as the original string. Ex: `{"a":"b","c":"d"}` might become `{"c":"d","a":b"}`. 178 | To keep the json representation consistent, when updating a periodic job with its new 179 | score in redis, the original json string will be used again to keep things consistent. 180 | 181 | 182 | ## Server Middleware 183 | 184 | One great feature of sidekiq is its middleware pattern. This library reimplements the 185 | sidekiq server middleware pattern in rust. In the example below supposes you have an 186 | app that performs work only for paying customers. The middleware below will hault jobs 187 | from being executed if the customers have expired. One thing kind of interesting about 188 | the implementation is that we can rely on serde to conditionally type-check workers. 189 | For example, suppose I only care about user-centric workers, and I identify those by their 190 | `user_guid` as a parameter. With serde it's easy to validate your paramters. 191 | 192 | ```rust 193 | use tracing::info; 194 | 195 | struct FilterExpiredUsersMiddleware {} 196 | 197 | impl FilterExpiredUsersMiddleware { 198 | fn new() -> Self { 199 | Self { } 200 | } 201 | } 202 | 203 | #[derive(Deserialize)] 204 | struct FiltereExpiredUsersArgs { 205 | user_guid: String, 206 | } 207 | 208 | impl FiltereExpiredUsersArgs { 209 | fn is_expired(&self) -> bool { 210 | self.user_guid == "USR-123-EXPIRED" 211 | } 212 | } 213 | 214 | #[async_trait] 215 | impl ServerMiddleware for FilterExpiredUsersMiddleware { 216 | async fn call( 217 | &self, 218 | chain: ChainIter, 219 | job: &Job, 220 | worker: Arc, 221 | redis: RedisPool, 222 | ) -> ServerResult { 223 | // Use serde to check if a user_guid is part of the job args. 224 | let args: Result<(FiltereExpiredUsersArgs,), serde_json::Error> = 225 | serde_json::from_value(job.args.clone()); 226 | 227 | // If we can safely deserialize then attempt to filter based on user guid. 228 | if let Ok((filter,)) = args { 229 | if filter.is_expired() { 230 | error!({ 231 | "class" = job.class, 232 | "jid" = job.jid, 233 | "user_guid" = filter.user_guid }, 234 | "Detected an expired user, skipping this job" 235 | ); 236 | return Ok(()); 237 | } 238 | } 239 | 240 | // This customer is not expired, so we may continue. 241 | chain.next(job, worker, redis).await 242 | } 243 | } 244 | ``` 245 | 246 | ## Best practices 247 | 248 | ### Separate enqueue vs fetch connection pools 249 | 250 | Though not required, it's recommended to use separate Redis connection pools for pushing jobs to Redis vs fetching 251 | jobs. This has the following benefits: 252 | 253 | - The pools can have different sizes, each optimized depending on the resource usage/constraints of your application. 254 | - If the `sidekiq::Processor` is configured to have more worker tasks than the max size of the connection pool, then 255 | there may be a delay in acquiring a connection from the queue. This is a problem for enqueuing jobs, as it's normally 256 | desired that enqueuing be as fast as possible to avoid delaying the critical path of another operation (e.g., an API 257 | request). With a separate pool for enqueuing, enqueuing jobs is not impacted by the `sidekiq::Processor`'s usage of 258 | the pool. 259 | 260 | ```rust 261 | #[tokio::main] 262 | async fn main() -> Result<()> { 263 | let manager = sidekiq::RedisConnectionManager::new("redis://127.0.0.1/").unwrap(); 264 | let redis_enqueue = bb8::Pool::builder().build(manager).await.unwrap(); 265 | let redis_fetch = bb8::Pool::builder().build(manager).await.unwrap(); 266 | 267 | let p = Processor::new( 268 | redis_fetch, 269 | vec!["default".to_string()], 270 | ); 271 | p.run().await; 272 | 273 | // ... 274 | 275 | ExampleWorker::perform_async(&redis_enqueue, ExampleArgs { foo: "bar".to_string() }).await?; 276 | 277 | Ok(()) 278 | } 279 | ``` 280 | 281 | ## Customization Details 282 | 283 | ### Namespacing the workers 284 | 285 | It's still very common to use the `redis-namespace` gem with ruby sidekiq workers. This library 286 | supports namespacing redis commands by using a connection customizer when you build the connection 287 | pool. 288 | 289 | ```rust 290 | let manager = sidekiq::RedisConnectionManager::new("redis://127.0.0.1/")?; 291 | let redis = bb8::Pool::builder() 292 | .connection_customizer(sidekiq::with_custom_namespace("my_cool_app".to_string())) 293 | .build(manager) 294 | .await?; 295 | ``` 296 | 297 | Now all commands used by this library will be prefixed with `my_cool_app:`, example: `ZDEL my_cool_app:scheduled {...}`. 298 | 299 | ### Passing database connections into the workers 300 | 301 | Workers will often need access to other software components like database connections, http clients, 302 | etc. You can define these on your worker struct so long as they implement `Clone`. Example: 303 | 304 | ```rust 305 | use tracing::debug; 306 | use sidekiq::Result; 307 | 308 | #[derive(Clone)] 309 | struct ExampleWorker { 310 | redis: RedisPool, 311 | } 312 | 313 | 314 | #[async_trait] 315 | impl Worker<()> for ExampleWorker { 316 | async fn perform(&self, args: PaymentReportArgs) -> Result<()> { 317 | use redis::AsyncCommands; 318 | 319 | // And then they are available here... 320 | let times_called: usize = self 321 | .redis 322 | .get() 323 | .await? 324 | .unnamespaced_borrow_mut() 325 | .incr("example_of_accessing_the_raw_redis_connection", 1) 326 | .await?; 327 | 328 | debug!({"times_called" = times_called}, "Called this worker"); 329 | } 330 | } 331 | 332 | #[tokio::main] 333 | async fn main() -> Result<()> { 334 | // ... 335 | let mut p = Processor::new( 336 | redis.clone(), 337 | vec!["low_priority".to_string()], 338 | ); 339 | 340 | p.register(ExampleWorker{ redis: redis.clone() }); 341 | } 342 | ``` 343 | 344 | ### Customizing the worker name for workers under a nested ruby module 345 | 346 | You mind find that your worker under a module does not match with a ruby worker under a module. 347 | A nested rusty-sidekiq worker `workers::MyWorker` will only keep the final type name `MyWorker` when 348 | registering the worker for some "class name". Meaning, if a ruby worker is enqueued with the class 349 | `Workers::MyWorker`, the `workers::MyWorker` type will not process that work. This is because by default 350 | the class name is generated at compile time based on the worker struct name. To override this, redefine one 351 | of the default trait methods: 352 | 353 | ```rust 354 | pub struct MyWorker; 355 | use sidekiq::Result; 356 | 357 | #[async_trait] 358 | impl Worker<()> for MyWorker { 359 | async fn perform(&self, _args: ()) -> Result<()> { 360 | Ok(()) 361 | } 362 | 363 | fn class_name() -> String 364 | where 365 | Self: Sized, 366 | { 367 | "Workers::MyWorker".to_string() 368 | } 369 | } 370 | ``` 371 | 372 | And now when ruby enqueues a `Workers::MyWorker` job, it will be picked up by rust-sidekiq. 373 | 374 | ### Customizing the number of worker tasks spawned by the `sidekiq::Processor` 375 | 376 | If an app's workload is largely IO bound (querying a DB, making web requests and waiting for responses, etc), its 377 | workers will spend a large percentage of time idle `await`ing for futures to complete. This in turn means the will CPU 378 | sit idle a large percentage of the time (if nothing else is running on the host), resulting in under-utilizing available 379 | CPU resources. 380 | 381 | By default, the number of worker tasks spawned by the `sidekiq::Processor` is the host's CPU count, but this can 382 | be configured depending on the needs of the app, allowing to use CPU resources more efficiently. 383 | 384 | ```rust 385 | #[tokio::main] 386 | async fn main() -> Result<()> { 387 | // ... 388 | let num_workers = usize::from_str(&env::var("NUM_WORKERS").unwrap()).unwrap(); 389 | let config: ProcessorConfig = Default::default(); 390 | let config = config.num_workers(num_workers); 391 | let processor = Processor::new(redis_fetch, queues.clone()) 392 | .with_config(config); 393 | // ... 394 | } 395 | ``` 396 | 397 | ## License 398 | 399 | MIT 400 | -------------------------------------------------------------------------------- /src/processor.rs: -------------------------------------------------------------------------------- 1 | use super::Result; 2 | use crate::{ 3 | periodic::PeriodicJob, Chain, Counter, Job, RedisPool, Scheduled, ServerMiddleware, 4 | StatsPublisher, UnitOfWork, Worker, WorkerRef, 5 | }; 6 | use std::collections::{BTreeMap, VecDeque}; 7 | use std::sync::Arc; 8 | use tokio::select; 9 | use tokio::task::JoinSet; 10 | use tokio_util::sync::CancellationToken; 11 | use tracing::{debug, error, info}; 12 | 13 | #[derive(Clone, Eq, PartialEq, Debug)] 14 | pub enum WorkFetcher { 15 | NoWorkFound, 16 | Done, 17 | } 18 | 19 | #[derive(Clone)] 20 | pub struct Processor { 21 | redis: RedisPool, 22 | queues: VecDeque, 23 | human_readable_queues: Vec, 24 | periodic_jobs: Vec, 25 | workers: BTreeMap>, 26 | chain: Chain, 27 | busy_jobs: Counter, 28 | cancellation_token: CancellationToken, 29 | config: ProcessorConfig, 30 | } 31 | 32 | #[derive(Clone)] 33 | #[non_exhaustive] 34 | pub struct ProcessorConfig { 35 | /// The number of Sidekiq workers that can run at the same time. Adjust as needed based on 36 | /// your workload and resource (cpu/memory/etc) usage. 37 | /// 38 | /// This config value controls how many workers are spawned to handle the queues provided 39 | /// to [`Processor::new`]. These workers will be shared across all of these queues. 40 | /// 41 | /// If your workload is largely CPU-bound (computationally expensive), this should probably 42 | /// match your CPU count. This is the default. 43 | /// 44 | /// If your workload is largely IO-bound (e.g. reading from a DB, making web requests and 45 | /// waiting for responses, etc), this can probably be quite a bit higher than your CPU count. 46 | pub num_workers: usize, 47 | 48 | /// The strategy for balancing the priority of fetching queues' jobs from Redis. Defaults 49 | /// to [`BalanceStrategy::RoundRobin`]. 50 | /// 51 | /// The Redis API used to fetch jobs ([brpop](https://redis.io/docs/latest/commands/brpop/)) 52 | /// checks queues for jobs in the order the queues are provided. This means that if the first 53 | /// queue in the list provided to [`Processor::new`] always has an item, the other queues 54 | /// will never have their jobs run. To mitigate this, a [`BalanceStrategy`] can be provided 55 | /// to allow ensuring that no queue is starved indefinitely. 56 | pub balance_strategy: BalanceStrategy, 57 | 58 | /// Queue-specific configurations. The queues specified in this field do not need to match 59 | /// the list of queues provided to [`Processor::new`]. 60 | pub queue_configs: BTreeMap, 61 | } 62 | 63 | #[derive(Default, Clone)] 64 | #[non_exhaustive] 65 | pub enum BalanceStrategy { 66 | /// Rotate the list of queues by 1 every time jobs are fetched from Redis. This allows each 67 | /// queue in the list to have an equal opportunity to have its jobs run. 68 | #[default] 69 | RoundRobin, 70 | /// Do not modify the list of queues. Warning: This can lead to queue starvation! For example, 71 | /// if the first queue in the list provided to [`Processor::new`] is heavily used and always 72 | /// has a job available to run, then the jobs in the other queues will never run. 73 | None, 74 | } 75 | 76 | #[derive(Default, Clone)] 77 | #[non_exhaustive] 78 | pub struct QueueConfig { 79 | /// Similar to `ProcessorConfig#num_workers`, except allows configuring the number of 80 | /// additional workers to dedicate to a specific queue. If provided, `num_workers` additional 81 | /// workers will be created for this specific queue. 82 | pub num_workers: usize, 83 | } 84 | 85 | impl ProcessorConfig { 86 | #[must_use] 87 | pub fn num_workers(mut self, num_workers: usize) -> Self { 88 | self.num_workers = num_workers; 89 | self 90 | } 91 | 92 | #[must_use] 93 | pub fn balance_strategy(mut self, balance_strategy: BalanceStrategy) -> Self { 94 | self.balance_strategy = balance_strategy; 95 | self 96 | } 97 | 98 | #[must_use] 99 | pub fn queue_config(mut self, queue: String, config: QueueConfig) -> Self { 100 | self.queue_configs.insert(queue, config); 101 | self 102 | } 103 | } 104 | 105 | impl Default for ProcessorConfig { 106 | fn default() -> Self { 107 | Self { 108 | num_workers: num_cpus::get(), 109 | balance_strategy: Default::default(), 110 | queue_configs: Default::default(), 111 | } 112 | } 113 | } 114 | 115 | impl QueueConfig { 116 | #[must_use] 117 | pub fn num_workers(mut self, num_workers: usize) -> Self { 118 | self.num_workers = num_workers; 119 | self 120 | } 121 | } 122 | 123 | impl Processor { 124 | #[must_use] 125 | pub fn new(redis: RedisPool, queues: Vec) -> Self { 126 | let busy_jobs = Counter::new(0); 127 | 128 | Self { 129 | chain: Chain::new_with_stats(busy_jobs.clone()), 130 | workers: BTreeMap::new(), 131 | periodic_jobs: vec![], 132 | busy_jobs, 133 | 134 | redis, 135 | queues: queues 136 | .iter() 137 | .map(|queue| format!("queue:{queue}")) 138 | .collect(), 139 | human_readable_queues: queues, 140 | cancellation_token: CancellationToken::new(), 141 | config: Default::default(), 142 | } 143 | } 144 | 145 | pub fn with_config(mut self, config: ProcessorConfig) -> Self { 146 | self.config = config; 147 | self 148 | } 149 | 150 | pub async fn fetch(&mut self) -> Result> { 151 | self.run_balance_strategy(); 152 | 153 | let response: Option<(String, String)> = self 154 | .redis 155 | .get() 156 | .await? 157 | .brpop(self.queues.clone().into(), 2) 158 | .await?; 159 | 160 | if let Some((queue, job_raw)) = response { 161 | let job: Job = serde_json::from_str(&job_raw)?; 162 | return Ok(Some(UnitOfWork { queue, job })); 163 | } 164 | 165 | Ok(None) 166 | } 167 | 168 | /// Re-order the `Processor#queues` based on the `ProcessorConfig#balance_strategy`. 169 | fn run_balance_strategy(&mut self) { 170 | if self.queues.is_empty() { 171 | return; 172 | } 173 | 174 | match self.config.balance_strategy { 175 | BalanceStrategy::RoundRobin => self.queues.rotate_right(1), 176 | BalanceStrategy::None => {} 177 | } 178 | } 179 | 180 | pub async fn process_one(&mut self) -> Result<()> { 181 | loop { 182 | if self.cancellation_token.is_cancelled() { 183 | return Ok(()); 184 | } 185 | 186 | if let WorkFetcher::NoWorkFound = self.process_one_tick_once().await? { 187 | continue; 188 | } 189 | 190 | return Ok(()); 191 | } 192 | } 193 | 194 | pub async fn process_one_tick_once(&mut self) -> Result { 195 | let work = self.fetch().await?; 196 | 197 | if work.is_none() { 198 | // If there is no job to handle, we need to add a `yield_now` in order to allow tokio's 199 | // scheduler to wake up another task that may be waiting to acquire a connection from 200 | // the Redis connection pool. See the following issue for more details: 201 | // https://github.com/film42/sidekiq-rs/issues/43 202 | tokio::task::yield_now().await; 203 | return Ok(WorkFetcher::NoWorkFound); 204 | } 205 | let mut work = work.expect("polled and found some work"); 206 | 207 | let started = std::time::Instant::now(); 208 | 209 | info!({ 210 | "status" = "start", 211 | "class" = &work.job.class, 212 | "queue" = &work.job.queue, 213 | "jid" = &work.job.jid 214 | }, "sidekiq"); 215 | 216 | if let Some(worker) = self.workers.get_mut(&work.job.class) { 217 | self.chain 218 | .call(&work.job, worker.clone(), self.redis.clone()) 219 | .await?; 220 | } else { 221 | error!({ 222 | "staus" = "fail", 223 | "class" = &work.job.class, 224 | "queue" = &work.job.queue, 225 | "jid" = &work.job.jid 226 | },"!!! Worker not found !!!"); 227 | work.reenqueue(&self.redis).await?; 228 | } 229 | 230 | // TODO: Make this only say "done" when the job is successful. 231 | // We might need to change the ChainIter to return the final job and 232 | // detect any retries? 233 | info!({ 234 | "elapsed" = format!("{:?}", started.elapsed()), 235 | "status" = "done", 236 | "class" = &work.job.class, 237 | "queue" = &work.job.queue, 238 | "jid" = &work.job.jid}, "sidekiq"); 239 | 240 | Ok(WorkFetcher::Done) 241 | } 242 | 243 | pub fn register< 244 | Args: Sync + Send + for<'de> serde::Deserialize<'de> + 'static, 245 | W: Worker + 'static, 246 | >( 247 | &mut self, 248 | worker: W, 249 | ) { 250 | self.workers 251 | .insert(W::class_name(), Arc::new(WorkerRef::wrap(Arc::new(worker)))); 252 | } 253 | 254 | pub fn get_cancellation_token(&self) -> CancellationToken { 255 | self.cancellation_token.clone() 256 | } 257 | 258 | pub(crate) async fn register_periodic(&mut self, periodic_job: PeriodicJob) -> Result<()> { 259 | self.periodic_jobs.push(periodic_job.clone()); 260 | 261 | let mut conn = self.redis.get().await?; 262 | periodic_job.insert(&mut conn).await?; 263 | 264 | info!({ 265 | "args" = &periodic_job.args, 266 | "class" = &periodic_job.class, 267 | "queue" = &periodic_job.queue, 268 | "name" = &periodic_job.name, 269 | "cron" = &periodic_job.cron, 270 | },"Inserting periodic job"); 271 | 272 | Ok(()) 273 | } 274 | 275 | /// Takes self to consume the processor. This is for life-cycle management, not 276 | /// memory safety because you can clone processor pretty easily. 277 | pub async fn run(self) { 278 | let mut join_set: JoinSet<()> = JoinSet::new(); 279 | 280 | // Logic for spawning shared workers (workers that handles multiple queues) and dedicated 281 | // workers (workers that handle a single queue). 282 | let spawn_worker = |mut processor: Processor, 283 | cancellation_token: CancellationToken, 284 | num: usize, 285 | dedicated_queue_name: Option| { 286 | async move { 287 | loop { 288 | if let Err(err) = processor.process_one().await { 289 | error!("Error leaked out the bottom: {:?}", err); 290 | } 291 | 292 | if cancellation_token.is_cancelled() { 293 | break; 294 | } 295 | } 296 | 297 | let dedicated_queue_str = dedicated_queue_name 298 | .map(|name| format!(" dedicated to queue '{name}'")) 299 | .unwrap_or_default(); 300 | debug!("Broke out of loop for worker {num}{dedicated_queue_str}"); 301 | } 302 | }; 303 | 304 | // Start worker routines. 305 | for i in 0..self.config.num_workers { 306 | join_set.spawn(spawn_worker( 307 | self.clone(), 308 | self.cancellation_token.clone(), 309 | i, 310 | None, 311 | )); 312 | } 313 | 314 | // Start dedicated worker routines. 315 | for (queue, config) in &self.config.queue_configs { 316 | for i in 0..config.num_workers { 317 | join_set.spawn({ 318 | let mut processor = self.clone(); 319 | processor.queues = [queue.clone()].into(); 320 | spawn_worker( 321 | processor, 322 | self.cancellation_token.clone(), 323 | i, 324 | Some(queue.clone()), 325 | ) 326 | }); 327 | } 328 | } 329 | 330 | // Start sidekiq-web metrics publisher. 331 | join_set.spawn({ 332 | let redis = self.redis.clone(); 333 | let queues = self.human_readable_queues.clone(); 334 | let busy_jobs = self.busy_jobs.clone(); 335 | let cancellation_token = self.cancellation_token.clone(); 336 | async move { 337 | let hostname = if let Some(host) = gethostname::gethostname().to_str() { 338 | host.to_string() 339 | } else { 340 | "UNKNOWN_HOSTNAME".to_string() 341 | }; 342 | 343 | let stats_publisher = 344 | StatsPublisher::new(hostname, queues, busy_jobs, self.config.num_workers); 345 | 346 | loop { 347 | // TODO: Use process count to meet a 5 second avg. 348 | select! { 349 | _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {} 350 | _ = cancellation_token.cancelled() => { 351 | break; 352 | } 353 | } 354 | 355 | if let Err(err) = stats_publisher.publish_stats(redis.clone()).await { 356 | error!("Error publishing processor stats: {:?}", err); 357 | } 358 | } 359 | 360 | debug!("Broke out of loop web metrics"); 361 | } 362 | }); 363 | 364 | // Start retry and scheduled routines. 365 | join_set.spawn({ 366 | let redis = self.redis.clone(); 367 | let cancellation_token = self.cancellation_token.clone(); 368 | async move { 369 | let sched = Scheduled::new(redis); 370 | let sorted_sets = vec!["retry".to_string(), "schedule".to_string()]; 371 | 372 | loop { 373 | // TODO: Use process count to meet a 5 second avg. 374 | select! { 375 | _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {} 376 | _ = cancellation_token.cancelled() => { 377 | break; 378 | } 379 | } 380 | 381 | if let Err(err) = sched.enqueue_jobs(chrono::Utc::now(), &sorted_sets).await { 382 | error!("Error in scheduled poller routine: {:?}", err); 383 | } 384 | } 385 | 386 | debug!("Broke out of loop for retry and scheduled"); 387 | } 388 | }); 389 | 390 | // Watch for periodic jobs and enqueue jobs. 391 | join_set.spawn({ 392 | let redis = self.redis.clone(); 393 | let cancellation_token = self.cancellation_token.clone(); 394 | async move { 395 | let sched = Scheduled::new(redis); 396 | 397 | loop { 398 | // TODO: Use process count to meet a 30 second avg. 399 | select! { 400 | _ = tokio::time::sleep(std::time::Duration::from_secs(30)) => {} 401 | _ = cancellation_token.cancelled() => { 402 | break; 403 | } 404 | } 405 | 406 | if let Err(err) = sched.enqueue_periodic_jobs(chrono::Utc::now()).await { 407 | error!("Error in periodic job poller routine: {}", err); 408 | } 409 | } 410 | 411 | debug!("Broke out of loop for periodic"); 412 | } 413 | }); 414 | 415 | while let Some(result) = join_set.join_next().await { 416 | if let Err(err) = result { 417 | error!("Processor had a spawned task return an error: {}", err); 418 | } 419 | } 420 | } 421 | 422 | pub async fn using(&mut self, middleware: M) 423 | where 424 | M: ServerMiddleware + Send + Sync + 'static, 425 | { 426 | self.chain.using(Box::new(middleware)).await; 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use middleware::Chain; 3 | use rand::{Rng, RngCore}; 4 | use serde::{ 5 | de::{self, Deserializer, Visitor}, 6 | Deserialize, Serialize, Serializer, 7 | }; 8 | use serde_json::Value as JsonValue; 9 | use sha2::{Digest, Sha256}; 10 | use std::future::Future; 11 | use std::marker::PhantomData; 12 | use std::pin::Pin; 13 | use std::sync::Arc; 14 | 15 | pub mod periodic; 16 | 17 | mod middleware; 18 | mod processor; 19 | mod redis; 20 | mod scheduled; 21 | mod stats; 22 | 23 | // Re-export 24 | pub use crate::redis::{ 25 | with_custom_namespace, RedisConnection, RedisConnectionManager, RedisError, RedisPool, 26 | }; 27 | pub use ::redis as redis_rs; 28 | pub use middleware::{ChainIter, ServerMiddleware}; 29 | pub use processor::{BalanceStrategy, Processor, ProcessorConfig, QueueConfig, WorkFetcher}; 30 | pub use scheduled::Scheduled; 31 | pub use stats::{Counter, StatsPublisher}; 32 | 33 | #[derive(thiserror::Error, Debug)] 34 | pub enum Error { 35 | #[error("{0}")] 36 | Message(String), 37 | 38 | #[error(transparent)] 39 | Json(#[from] serde_json::Error), 40 | 41 | #[error(transparent)] 42 | CronClock(#[from] cron_clock::error::Error), 43 | 44 | #[error(transparent)] 45 | BB8(#[from] bb8::RunError), 46 | 47 | #[error(transparent)] 48 | ChronoRange(#[from] chrono::OutOfRangeError), 49 | 50 | #[error(transparent)] 51 | Redis(#[from] redis::RedisError), 52 | 53 | #[error(transparent)] 54 | Any(#[from] Box), 55 | } 56 | 57 | pub type Result = std::result::Result; 58 | 59 | #[must_use] 60 | pub fn opts() -> EnqueueOpts { 61 | EnqueueOpts { 62 | queue: "default".into(), 63 | retry: RetryOpts::Yes, 64 | unique_for: None, 65 | retry_queue: None, 66 | } 67 | } 68 | 69 | pub struct EnqueueOpts { 70 | queue: String, 71 | retry: RetryOpts, 72 | unique_for: Option, 73 | retry_queue: Option, 74 | } 75 | 76 | impl EnqueueOpts { 77 | #[must_use] 78 | pub fn queue>(self, queue: S) -> Self { 79 | Self { 80 | queue: queue.into(), 81 | ..self 82 | } 83 | } 84 | 85 | #[must_use] 86 | pub fn retry(self, retry: RO) -> Self 87 | where 88 | RO: Into, 89 | { 90 | Self { 91 | retry: retry.into(), 92 | ..self 93 | } 94 | } 95 | 96 | #[must_use] 97 | pub fn unique_for(self, unique_for: std::time::Duration) -> Self { 98 | Self { 99 | unique_for: Some(unique_for), 100 | ..self 101 | } 102 | } 103 | 104 | #[must_use] 105 | pub fn retry_queue(self, retry_queue: String) -> Self { 106 | Self { 107 | retry_queue: Some(retry_queue), 108 | ..self 109 | } 110 | } 111 | 112 | pub fn create_job(&self, class: String, args: impl serde::Serialize) -> Result { 113 | let args = serde_json::to_value(args)?; 114 | 115 | // Ensure args are always wrapped in an array. 116 | let args = if args.is_array() { 117 | args 118 | } else { 119 | JsonValue::Array(vec![args]) 120 | }; 121 | 122 | Ok(Job { 123 | queue: self.queue.clone(), 124 | class, 125 | jid: new_jid(), 126 | created_at: chrono::Utc::now().timestamp() as f64, 127 | enqueued_at: None, 128 | retry: self.retry.clone(), 129 | args, 130 | 131 | // Make default eventually... 132 | error_message: None, 133 | error_class: None, 134 | failed_at: None, 135 | retry_count: None, 136 | retried_at: None, 137 | 138 | // Meta for enqueueing 139 | retry_queue: self.retry_queue.clone(), 140 | unique_for: self.unique_for, 141 | }) 142 | } 143 | 144 | pub async fn perform_async( 145 | self, 146 | redis: &RedisPool, 147 | class: String, 148 | args: impl serde::Serialize, 149 | ) -> Result<()> { 150 | let job = self.create_job(class, args)?; 151 | UnitOfWork::from_job(job).enqueue(redis).await?; 152 | Ok(()) 153 | } 154 | 155 | pub async fn perform_in( 156 | &self, 157 | redis: &RedisPool, 158 | class: String, 159 | duration: std::time::Duration, 160 | args: impl serde::Serialize, 161 | ) -> Result<()> { 162 | let job = self.create_job(class, args)?; 163 | UnitOfWork::from_job(job).schedule(redis, duration).await?; 164 | Ok(()) 165 | } 166 | } 167 | 168 | /// Helper function for enqueueing a worker into sidekiq. 169 | /// This can be used to enqueue a job for a ruby sidekiq worker to process. 170 | pub async fn perform_async( 171 | redis: &RedisPool, 172 | class: String, 173 | queue: String, 174 | args: impl serde::Serialize, 175 | ) -> Result<()> { 176 | opts().queue(queue).perform_async(redis, class, args).await 177 | } 178 | 179 | /// Helper function for enqueueing a worker into sidekiq. 180 | /// This can be used to enqueue a job for a ruby sidekiq worker to process. 181 | pub async fn perform_in( 182 | redis: &RedisPool, 183 | duration: std::time::Duration, 184 | class: String, 185 | queue: String, 186 | args: impl serde::Serialize, 187 | ) -> Result<()> { 188 | opts() 189 | .queue(queue) 190 | .perform_in(redis, class, duration, args) 191 | .await 192 | } 193 | 194 | fn new_jid() -> String { 195 | let mut bytes = [0u8; 12]; 196 | rand::thread_rng().fill_bytes(&mut bytes); 197 | hex::encode(bytes) 198 | } 199 | 200 | pub struct WorkerOpts + ?Sized> { 201 | queue: String, 202 | retry: RetryOpts, 203 | args: PhantomData, 204 | worker: PhantomData, 205 | unique_for: Option, 206 | retry_queue: Option, 207 | } 208 | 209 | impl WorkerOpts 210 | where 211 | W: Worker, 212 | { 213 | #[must_use] 214 | pub fn new() -> Self { 215 | Self { 216 | queue: "default".into(), 217 | retry: RetryOpts::Yes, 218 | args: PhantomData, 219 | worker: PhantomData, 220 | unique_for: None, 221 | retry_queue: None, 222 | } 223 | } 224 | 225 | #[must_use] 226 | pub fn retry(self, retry: RO) -> Self 227 | where 228 | RO: Into, 229 | { 230 | Self { 231 | retry: retry.into(), 232 | ..self 233 | } 234 | } 235 | 236 | #[must_use] 237 | pub fn retry_queue>(self, retry_queue: S) -> Self { 238 | Self { 239 | retry_queue: Some(retry_queue.into()), 240 | ..self 241 | } 242 | } 243 | 244 | #[must_use] 245 | pub fn queue>(self, queue: S) -> Self { 246 | Self { 247 | queue: queue.into(), 248 | ..self 249 | } 250 | } 251 | 252 | #[must_use] 253 | pub fn unique_for(self, unique_for: std::time::Duration) -> Self { 254 | Self { 255 | unique_for: Some(unique_for), 256 | ..self 257 | } 258 | } 259 | 260 | #[allow(clippy::wrong_self_convention)] 261 | pub fn into_opts(&self) -> EnqueueOpts { 262 | self.into() 263 | } 264 | 265 | pub async fn perform_async( 266 | &self, 267 | redis: &RedisPool, 268 | args: impl serde::Serialize + Send + 'static, 269 | ) -> Result<()> { 270 | self.into_opts() 271 | .perform_async(redis, W::class_name(), args) 272 | .await 273 | } 274 | 275 | pub async fn perform_in( 276 | &self, 277 | redis: &RedisPool, 278 | duration: std::time::Duration, 279 | args: impl serde::Serialize + Send + 'static, 280 | ) -> Result<()> { 281 | self.into_opts() 282 | .perform_in(redis, W::class_name(), duration, args) 283 | .await 284 | } 285 | } 286 | 287 | impl> From<&WorkerOpts> for EnqueueOpts { 288 | fn from(opts: &WorkerOpts) -> Self { 289 | Self { 290 | retry: opts.retry.clone(), 291 | queue: opts.queue.clone(), 292 | unique_for: opts.unique_for, 293 | retry_queue: opts.retry_queue.clone(), 294 | } 295 | } 296 | } 297 | 298 | impl> Default for WorkerOpts { 299 | fn default() -> Self { 300 | Self::new() 301 | } 302 | } 303 | 304 | #[async_trait] 305 | pub trait Worker: Send + Sync { 306 | /// Signal to WorkerRef to not attempt to modify the JsonValue args 307 | /// before calling the perform function. This is useful if the args 308 | /// are expected to be a `Vec` that might be `len() == 1` or a 309 | /// single sized tuple `(T,)`. 310 | fn disable_argument_coercion(&self) -> bool { 311 | false 312 | } 313 | 314 | #[must_use] 315 | fn opts() -> WorkerOpts 316 | where 317 | Self: Sized, 318 | { 319 | WorkerOpts::new() 320 | } 321 | 322 | // TODO: Make configurable through opts and make opts accessible to the 323 | // retry middleware through a Box. 324 | fn max_retries(&self) -> usize { 325 | 25 326 | } 327 | 328 | /// Derive a class_name from the Worker type to be used with sidekiq. By default 329 | /// this method will 330 | #[must_use] 331 | fn class_name() -> String 332 | where 333 | Self: Sized, 334 | { 335 | use convert_case::{Case, Casing}; 336 | 337 | let type_name = std::any::type_name::(); 338 | let name = type_name.split("::").last().unwrap_or(type_name); 339 | name.to_case(Case::UpperCamel) 340 | } 341 | 342 | async fn perform_async(redis: &RedisPool, args: Args) -> Result<()> 343 | where 344 | Self: Sized, 345 | Args: Send + Sync + serde::Serialize + 'static, 346 | { 347 | Self::opts().perform_async(redis, args).await 348 | } 349 | 350 | async fn perform_in(redis: &RedisPool, duration: std::time::Duration, args: Args) -> Result<()> 351 | where 352 | Self: Sized, 353 | Args: Send + Sync + serde::Serialize + 'static, 354 | { 355 | Self::opts().perform_in(redis, duration, args).await 356 | } 357 | 358 | async fn perform(&self, args: Args) -> Result<()>; 359 | } 360 | 361 | // We can't store a Vec>>, because that will only work 362 | // for a single arg type, but since any worker is JsonValue in and Result out, 363 | // we can wrap that generic work in a callback that shares the same type. 364 | // I'm sure this has a fancy name, but I don't know what it is. 365 | #[derive(Clone)] 366 | pub struct WorkerRef { 367 | #[allow(clippy::type_complexity)] 368 | work_fn: Arc< 369 | Box Pin> + Send>> + Send + Sync>, 370 | >, 371 | max_retries: usize, 372 | } 373 | 374 | async fn invoke_worker(args: JsonValue, worker: Arc) -> Result<()> 375 | where 376 | Args: Send + Sync + 'static, 377 | W: Worker + 'static, 378 | for<'de> Args: Deserialize<'de>, 379 | { 380 | let args = if worker.disable_argument_coercion() { 381 | args 382 | } else { 383 | // Ensure any caller expecting to receive `()` will always work. 384 | if std::any::TypeId::of::() == std::any::TypeId::of::<()>() { 385 | JsonValue::Null 386 | } else { 387 | // If the value contains a single item Vec then 388 | // you can probably be sure that this is a single value item. 389 | // Otherwise, the caller can impl a tuple type. 390 | match args { 391 | JsonValue::Array(mut arr) if arr.len() == 1 => { 392 | arr.pop().expect("value change after size check") 393 | } 394 | _ => args, 395 | } 396 | } 397 | }; 398 | 399 | let args: Args = serde_json::from_value(args)?; 400 | worker.perform(args).await 401 | } 402 | 403 | impl WorkerRef { 404 | pub(crate) fn wrap(worker: Arc) -> Self 405 | where 406 | Args: Send + Sync + 'static, 407 | W: Worker + 'static, 408 | for<'de> Args: Deserialize<'de>, 409 | { 410 | Self { 411 | work_fn: Arc::new(Box::new({ 412 | let worker = worker.clone(); 413 | move |args: JsonValue| { 414 | let worker = worker.clone(); 415 | Box::pin(async move { invoke_worker(args, worker).await }) 416 | } 417 | })), 418 | max_retries: worker.max_retries(), 419 | } 420 | } 421 | 422 | #[must_use] 423 | pub fn max_retries(&self) -> usize { 424 | self.max_retries 425 | } 426 | 427 | pub async fn call(&self, args: JsonValue) -> Result<()> { 428 | (Arc::clone(&self.work_fn))(args).await 429 | } 430 | } 431 | 432 | #[derive(Clone, Debug, PartialEq)] 433 | pub enum RetryOpts { 434 | Yes, 435 | Never, 436 | Max(usize), 437 | } 438 | 439 | impl Serialize for RetryOpts { 440 | fn serialize(&self, serializer: S) -> std::result::Result 441 | where 442 | S: Serializer, 443 | { 444 | match *self { 445 | RetryOpts::Yes => serializer.serialize_bool(true), 446 | RetryOpts::Never => serializer.serialize_bool(false), 447 | RetryOpts::Max(value) => serializer.serialize_u64(value as u64), 448 | } 449 | } 450 | } 451 | 452 | impl<'de> Deserialize<'de> for RetryOpts { 453 | fn deserialize(deserializer: D) -> std::result::Result 454 | where 455 | D: Deserializer<'de>, 456 | { 457 | struct RetryOptsVisitor; 458 | 459 | impl Visitor<'_> for RetryOptsVisitor { 460 | type Value = RetryOpts; 461 | 462 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 463 | formatter.write_str("a boolean, null, or a positive integer") 464 | } 465 | 466 | fn visit_bool(self, value: bool) -> std::result::Result 467 | where 468 | E: de::Error, 469 | { 470 | if value { 471 | Ok(RetryOpts::Yes) 472 | } else { 473 | Ok(RetryOpts::Never) 474 | } 475 | } 476 | 477 | fn visit_none(self) -> std::result::Result 478 | where 479 | E: de::Error, 480 | { 481 | Ok(RetryOpts::Never) 482 | } 483 | 484 | fn visit_u64(self, value: u64) -> std::result::Result 485 | where 486 | E: de::Error, 487 | { 488 | Ok(RetryOpts::Max(value as usize)) 489 | } 490 | } 491 | 492 | deserializer.deserialize_any(RetryOptsVisitor) 493 | } 494 | } 495 | 496 | impl From for RetryOpts { 497 | fn from(value: bool) -> Self { 498 | match value { 499 | true => RetryOpts::Yes, 500 | false => RetryOpts::Never, 501 | } 502 | } 503 | } 504 | 505 | impl From for RetryOpts { 506 | fn from(value: usize) -> Self { 507 | RetryOpts::Max(value) 508 | } 509 | } 510 | 511 | // 512 | // { 513 | // "retry": true, 514 | // "queue": "yolo", 515 | // "class": "YoloWorker", 516 | // "args": [ 517 | // { 518 | // "yolo": "hiiii" 519 | // } 520 | // ], 521 | // "jid": "f33f7063c6d7a4db0869289a", 522 | // "created_at": 1647119929.3788748, 523 | // "enqueued_at": 1647119929.378998 524 | // } 525 | // 526 | #[derive(Serialize, Deserialize, Debug, Clone)] 527 | pub struct Job { 528 | pub queue: String, 529 | pub args: JsonValue, 530 | pub retry: RetryOpts, 531 | pub class: String, 532 | pub jid: String, 533 | pub created_at: f64, 534 | pub enqueued_at: Option, 535 | pub failed_at: Option, 536 | pub error_message: Option, 537 | pub error_class: Option, 538 | pub retry_count: Option, 539 | pub retried_at: Option, 540 | pub retry_queue: Option, 541 | 542 | #[serde(skip)] 543 | pub unique_for: Option, 544 | } 545 | 546 | #[derive(Debug)] 547 | pub struct UnitOfWork { 548 | pub queue: String, 549 | pub job: Job, 550 | } 551 | 552 | impl UnitOfWork { 553 | #[must_use] 554 | pub fn from_job(job: Job) -> Self { 555 | Self { 556 | queue: format!("queue:{}", &job.queue), 557 | job, 558 | } 559 | } 560 | 561 | pub fn from_job_string(job_str: String) -> Result { 562 | let job: Job = serde_json::from_str(&job_str)?; 563 | Ok(Self::from_job(job)) 564 | } 565 | 566 | pub async fn enqueue(&self, redis: &RedisPool) -> Result<()> { 567 | let mut redis = redis.get().await?; 568 | self.enqueue_direct(&mut redis).await 569 | } 570 | 571 | async fn enqueue_direct(&self, redis: &mut RedisConnection) -> Result<()> { 572 | let mut job = self.job.clone(); 573 | job.enqueued_at = Some(chrono::Utc::now().timestamp() as f64); 574 | 575 | if let Some(ref duration) = job.unique_for { 576 | // Check to see if this is unique for the given duration. 577 | // Even though SET k v NX EQ ttl isn't the best locking 578 | // mechanism, I think it's "good enough" to prove this out. 579 | let args_as_json_string: String = serde_json::to_string(&job.args)?; 580 | let args_hash = format!("{:x}", Sha256::digest(&args_as_json_string)); 581 | let redis_key = format!( 582 | "sidekiq:unique:{}:{}:{}", 583 | &job.queue, &job.class, &args_hash 584 | ); 585 | if let redis::RedisValue::Nil = redis 586 | .set_nx_ex(redis_key, "", duration.as_secs() as usize) 587 | .await? 588 | { 589 | // This job has already been enqueued. Do not submit it to redis. 590 | return Ok(()); 591 | } 592 | } 593 | 594 | redis.sadd("queues".to_string(), job.queue.clone()).await?; 595 | 596 | redis 597 | .lpush(self.queue.clone(), serde_json::to_string(&job)?) 598 | .await?; 599 | Ok(()) 600 | } 601 | 602 | pub async fn reenqueue(&mut self, redis: &RedisPool) -> Result<()> { 603 | if let Some(retry_count) = self.job.retry_count { 604 | redis 605 | .get() 606 | .await? 607 | .zadd( 608 | "retry".to_string(), 609 | serde_json::to_string(&self.job)?, 610 | Self::retry_job_at(retry_count).timestamp(), 611 | ) 612 | .await?; 613 | } 614 | 615 | Ok(()) 616 | } 617 | 618 | fn retry_job_at(count: usize) -> chrono::DateTime { 619 | let seconds_to_delay = 620 | count.pow(4) + 15 + (rand::thread_rng().gen_range(0..30) * (count + 1)); 621 | 622 | chrono::Utc::now() + chrono::Duration::seconds(seconds_to_delay as i64) 623 | } 624 | 625 | pub async fn schedule( 626 | &mut self, 627 | redis: &RedisPool, 628 | duration: std::time::Duration, 629 | ) -> Result<()> { 630 | let enqueue_at = chrono::Utc::now() + chrono::Duration::from_std(duration)?; 631 | 632 | redis 633 | .get() 634 | .await? 635 | .zadd( 636 | "schedule".to_string(), 637 | serde_json::to_string(&self.job)?, 638 | enqueue_at.timestamp(), 639 | ) 640 | .await?; 641 | 642 | Ok(()) 643 | } 644 | } 645 | 646 | #[cfg(test)] 647 | mod test { 648 | use super::*; 649 | 650 | mod my { 651 | pub mod cool { 652 | pub mod workers { 653 | use super::super::super::super::*; 654 | 655 | pub struct TestOpts; 656 | 657 | #[async_trait] 658 | impl Worker<()> for TestOpts { 659 | fn opts() -> WorkerOpts<(), Self> 660 | where 661 | Self: Sized, 662 | { 663 | WorkerOpts::new() 664 | // Test bool 665 | .retry(false) 666 | // Test usize 667 | .retry(42) 668 | // Test the new type 669 | .retry(RetryOpts::Never) 670 | .unique_for(std::time::Duration::from_secs(30)) 671 | .queue("yolo_quue") 672 | } 673 | 674 | async fn perform(&self, _args: ()) -> Result<()> { 675 | Ok(()) 676 | } 677 | } 678 | 679 | pub struct X1Y2MyJob; 680 | 681 | #[async_trait] 682 | impl Worker<()> for X1Y2MyJob { 683 | async fn perform(&self, _args: ()) -> Result<()> { 684 | Ok(()) 685 | } 686 | } 687 | 688 | pub struct TestModuleWorker; 689 | 690 | #[async_trait] 691 | impl Worker<()> for TestModuleWorker { 692 | async fn perform(&self, _args: ()) -> Result<()> { 693 | Ok(()) 694 | } 695 | } 696 | 697 | pub struct TestCustomClassNameWorker; 698 | 699 | #[async_trait] 700 | impl Worker<()> for TestCustomClassNameWorker { 701 | async fn perform(&self, _args: ()) -> Result<()> { 702 | Ok(()) 703 | } 704 | 705 | fn class_name() -> String 706 | where 707 | Self: Sized, 708 | { 709 | "My::Cool::Workers::TestCustomClassNameWorker".to_string() 710 | } 711 | } 712 | } 713 | } 714 | } 715 | 716 | #[tokio::test] 717 | async fn ignores_modules_in_ruby_worker_name() { 718 | assert_eq!( 719 | my::cool::workers::TestModuleWorker::class_name(), 720 | "TestModuleWorker".to_string() 721 | ); 722 | } 723 | 724 | #[tokio::test] 725 | async fn does_not_reformat_valid_ruby_class_names() { 726 | assert_eq!( 727 | my::cool::workers::X1Y2MyJob::class_name(), 728 | "X1Y2MyJob".to_string() 729 | ); 730 | } 731 | 732 | #[tokio::test] 733 | async fn supports_custom_class_name_for_workers() { 734 | assert_eq!( 735 | my::cool::workers::TestCustomClassNameWorker::class_name(), 736 | "My::Cool::Workers::TestCustomClassNameWorker".to_string() 737 | ); 738 | } 739 | 740 | #[derive(Clone, Deserialize, Serialize, Debug)] 741 | struct TestArg { 742 | name: String, 743 | age: i32, 744 | } 745 | 746 | struct TestGenericWorker; 747 | #[async_trait] 748 | impl Worker for TestGenericWorker { 749 | async fn perform(&self, _args: TestArg) -> Result<()> { 750 | Ok(()) 751 | } 752 | } 753 | 754 | struct TestMultiArgWorker; 755 | #[async_trait] 756 | impl Worker<(TestArg, TestArg)> for TestMultiArgWorker { 757 | async fn perform(&self, _args: (TestArg, TestArg)) -> Result<()> { 758 | Ok(()) 759 | } 760 | } 761 | 762 | struct TestTupleArgWorker; 763 | #[async_trait] 764 | impl Worker<(TestArg,)> for TestTupleArgWorker { 765 | fn disable_argument_coercion(&self) -> bool { 766 | true 767 | } 768 | async fn perform(&self, _args: (TestArg,)) -> Result<()> { 769 | Ok(()) 770 | } 771 | } 772 | 773 | struct TestVecArgWorker; 774 | #[async_trait] 775 | impl Worker> for TestVecArgWorker { 776 | fn disable_argument_coercion(&self) -> bool { 777 | true 778 | } 779 | async fn perform(&self, _args: Vec) -> Result<()> { 780 | Ok(()) 781 | } 782 | } 783 | 784 | #[tokio::test] 785 | async fn can_have_a_vec_with_one_or_more_items() { 786 | // One item 787 | let worker = Arc::new(TestVecArgWorker); 788 | let wrap = Arc::new(WorkerRef::wrap(worker)); 789 | let wrap = wrap.clone(); 790 | let arg = serde_json::to_value(vec![TestArg { 791 | name: "test A".into(), 792 | age: 1337, 793 | }]) 794 | .unwrap(); 795 | wrap.call(arg).await.unwrap(); 796 | 797 | // Multiple items 798 | let worker = Arc::new(TestVecArgWorker); 799 | let wrap = Arc::new(WorkerRef::wrap(worker)); 800 | let wrap = wrap.clone(); 801 | let arg = serde_json::to_value(vec![ 802 | TestArg { 803 | name: "test A".into(), 804 | age: 1337, 805 | }, 806 | TestArg { 807 | name: "test A".into(), 808 | age: 1337, 809 | }, 810 | ]) 811 | .unwrap(); 812 | wrap.call(arg).await.unwrap(); 813 | } 814 | 815 | #[tokio::test] 816 | async fn can_have_multiple_arguments() { 817 | let worker = Arc::new(TestMultiArgWorker); 818 | let wrap = Arc::new(WorkerRef::wrap(worker)); 819 | let wrap = wrap.clone(); 820 | let arg = serde_json::to_value(( 821 | TestArg { 822 | name: "test A".into(), 823 | age: 1337, 824 | }, 825 | TestArg { 826 | name: "test B".into(), 827 | age: 1336, 828 | }, 829 | )) 830 | .unwrap(); 831 | wrap.call(arg).await.unwrap(); 832 | } 833 | 834 | #[tokio::test] 835 | async fn can_have_a_single_tuple_argument() { 836 | let worker = Arc::new(TestTupleArgWorker); 837 | let wrap = Arc::new(WorkerRef::wrap(worker)); 838 | let wrap = wrap.clone(); 839 | let arg = serde_json::to_value((TestArg { 840 | name: "test".into(), 841 | age: 1337, 842 | },)) 843 | .unwrap(); 844 | wrap.call(arg).await.unwrap(); 845 | } 846 | 847 | #[tokio::test] 848 | async fn can_have_a_single_argument() { 849 | let worker = Arc::new(TestGenericWorker); 850 | let wrap = Arc::new(WorkerRef::wrap(worker)); 851 | let wrap = wrap.clone(); 852 | let arg = serde_json::to_value(TestArg { 853 | name: "test".into(), 854 | age: 1337, 855 | }) 856 | .unwrap(); 857 | wrap.call(arg).await.unwrap(); 858 | } 859 | 860 | #[tokio::test] 861 | async fn processor_config_has_workers_by_default() { 862 | let cfg = ProcessorConfig::default(); 863 | 864 | assert!( 865 | cfg.num_workers > 0, 866 | "num_workers should be greater than 0 (using num cpu)" 867 | ); 868 | 869 | let cfg = cfg.num_workers(1000); 870 | 871 | assert_eq!(cfg.num_workers, 1000); 872 | } 873 | } 874 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.15.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "adler2" 22 | version = "2.0.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 25 | 26 | [[package]] 27 | name = "android-tzdata" 28 | version = "0.1.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 31 | 32 | [[package]] 33 | name = "android_system_properties" 34 | version = "0.1.5" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 37 | dependencies = [ 38 | "libc", 39 | ] 40 | 41 | [[package]] 42 | name = "arc-swap" 43 | version = "1.7.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" 46 | 47 | [[package]] 48 | name = "async-trait" 49 | version = "0.1.85" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" 52 | dependencies = [ 53 | "proc-macro2", 54 | "quote", 55 | "syn", 56 | ] 57 | 58 | [[package]] 59 | name = "autocfg" 60 | version = "1.4.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 63 | 64 | [[package]] 65 | name = "backtrace" 66 | version = "0.3.59" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" 69 | dependencies = [ 70 | "addr2line", 71 | "cc", 72 | "cfg-if", 73 | "libc", 74 | "miniz_oxide 0.4.4", 75 | "object", 76 | "rustc-demangle", 77 | ] 78 | 79 | [[package]] 80 | name = "bb8" 81 | version = "0.9.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "212d8b8e1a22743d9241575c6ba822cf9c8fef34771c86ab7e477a4fbfd254e5" 84 | dependencies = [ 85 | "futures-util", 86 | "parking_lot", 87 | "tokio", 88 | ] 89 | 90 | [[package]] 91 | name = "bitflags" 92 | version = "1.3.2" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 95 | 96 | [[package]] 97 | name = "bitflags" 98 | version = "2.7.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" 101 | 102 | [[package]] 103 | name = "block-buffer" 104 | version = "0.10.4" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 107 | dependencies = [ 108 | "generic-array", 109 | ] 110 | 111 | [[package]] 112 | name = "bumpalo" 113 | version = "3.16.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 116 | 117 | [[package]] 118 | name = "byteorder" 119 | version = "1.5.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 122 | 123 | [[package]] 124 | name = "bytes" 125 | version = "1.9.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 128 | 129 | [[package]] 130 | name = "cc" 131 | version = "1.2.9" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" 134 | dependencies = [ 135 | "shlex", 136 | ] 137 | 138 | [[package]] 139 | name = "cfg-if" 140 | version = "1.0.0" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 143 | 144 | [[package]] 145 | name = "chrono" 146 | version = "0.4.39" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 149 | dependencies = [ 150 | "android-tzdata", 151 | "iana-time-zone", 152 | "js-sys", 153 | "num-traits", 154 | "wasm-bindgen", 155 | "windows-targets", 156 | ] 157 | 158 | [[package]] 159 | name = "combine" 160 | version = "4.6.7" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" 163 | dependencies = [ 164 | "bytes", 165 | "futures-core", 166 | "memchr", 167 | "pin-project-lite", 168 | "tokio", 169 | "tokio-util", 170 | ] 171 | 172 | [[package]] 173 | name = "convert_case" 174 | version = "0.7.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 177 | dependencies = [ 178 | "unicode-segmentation", 179 | ] 180 | 181 | [[package]] 182 | name = "core-foundation-sys" 183 | version = "0.8.7" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 186 | 187 | [[package]] 188 | name = "cpufeatures" 189 | version = "0.2.16" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" 192 | dependencies = [ 193 | "libc", 194 | ] 195 | 196 | [[package]] 197 | name = "crc32fast" 198 | version = "1.4.2" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 201 | dependencies = [ 202 | "cfg-if", 203 | ] 204 | 205 | [[package]] 206 | name = "cron_clock" 207 | version = "0.8.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "5a8699d8ed16e3db689f8ae04d8dc3c6666a4ba7e724e5a157884b7cc385d16b" 210 | dependencies = [ 211 | "chrono", 212 | "nom", 213 | "once_cell", 214 | ] 215 | 216 | [[package]] 217 | name = "crypto-common" 218 | version = "0.1.6" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 221 | dependencies = [ 222 | "generic-array", 223 | "typenum", 224 | ] 225 | 226 | [[package]] 227 | name = "darwin-libproc" 228 | version = "0.2.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "cc629b7cf42586fee31dae31f9ab73fa5ff5f0170016aa61be5fcbc12a90c516" 231 | dependencies = [ 232 | "darwin-libproc-sys", 233 | "libc", 234 | "memchr", 235 | ] 236 | 237 | [[package]] 238 | name = "darwin-libproc-sys" 239 | version = "0.2.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "ef0aa083b94c54aa4cfd9bbfd37856714c139d1dc511af80270558c7ba3b4816" 242 | dependencies = [ 243 | "libc", 244 | ] 245 | 246 | [[package]] 247 | name = "deranged" 248 | version = "0.3.11" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 251 | dependencies = [ 252 | "powerfmt", 253 | ] 254 | 255 | [[package]] 256 | name = "digest" 257 | version = "0.10.7" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 260 | dependencies = [ 261 | "block-buffer", 262 | "crypto-common", 263 | ] 264 | 265 | [[package]] 266 | name = "dirs-next" 267 | version = "2.0.0" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" 270 | dependencies = [ 271 | "cfg-if", 272 | "dirs-sys-next", 273 | ] 274 | 275 | [[package]] 276 | name = "dirs-sys-next" 277 | version = "0.1.2" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" 280 | dependencies = [ 281 | "libc", 282 | "redox_users", 283 | "winapi", 284 | ] 285 | 286 | [[package]] 287 | name = "displaydoc" 288 | version = "0.2.5" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 291 | dependencies = [ 292 | "proc-macro2", 293 | "quote", 294 | "syn", 295 | ] 296 | 297 | [[package]] 298 | name = "errno" 299 | version = "0.3.10" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 302 | dependencies = [ 303 | "libc", 304 | "windows-sys 0.59.0", 305 | ] 306 | 307 | [[package]] 308 | name = "flate2" 309 | version = "1.0.35" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 312 | dependencies = [ 313 | "crc32fast", 314 | "miniz_oxide 0.8.3", 315 | ] 316 | 317 | [[package]] 318 | name = "form_urlencoded" 319 | version = "1.2.1" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 322 | dependencies = [ 323 | "percent-encoding", 324 | ] 325 | 326 | [[package]] 327 | name = "futures" 328 | version = "0.3.31" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 331 | dependencies = [ 332 | "futures-channel", 333 | "futures-core", 334 | "futures-executor", 335 | "futures-io", 336 | "futures-sink", 337 | "futures-task", 338 | "futures-util", 339 | ] 340 | 341 | [[package]] 342 | name = "futures-channel" 343 | version = "0.3.31" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 346 | dependencies = [ 347 | "futures-core", 348 | "futures-sink", 349 | ] 350 | 351 | [[package]] 352 | name = "futures-core" 353 | version = "0.3.31" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 356 | 357 | [[package]] 358 | name = "futures-executor" 359 | version = "0.3.31" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 362 | dependencies = [ 363 | "futures-core", 364 | "futures-task", 365 | "futures-util", 366 | ] 367 | 368 | [[package]] 369 | name = "futures-io" 370 | version = "0.3.31" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 373 | 374 | [[package]] 375 | name = "futures-sink" 376 | version = "0.3.31" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 379 | 380 | [[package]] 381 | name = "futures-task" 382 | version = "0.3.31" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 385 | 386 | [[package]] 387 | name = "futures-util" 388 | version = "0.3.31" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 391 | dependencies = [ 392 | "futures-channel", 393 | "futures-core", 394 | "futures-io", 395 | "futures-sink", 396 | "futures-task", 397 | "memchr", 398 | "pin-project-lite", 399 | "pin-utils", 400 | "slab", 401 | ] 402 | 403 | [[package]] 404 | name = "generic-array" 405 | version = "0.14.7" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 408 | dependencies = [ 409 | "typenum", 410 | "version_check", 411 | ] 412 | 413 | [[package]] 414 | name = "gethostname" 415 | version = "0.5.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" 418 | dependencies = [ 419 | "rustix", 420 | "windows-targets", 421 | ] 422 | 423 | [[package]] 424 | name = "getrandom" 425 | version = "0.2.15" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 428 | dependencies = [ 429 | "cfg-if", 430 | "libc", 431 | "wasi", 432 | ] 433 | 434 | [[package]] 435 | name = "gimli" 436 | version = "0.24.0" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" 439 | 440 | [[package]] 441 | name = "hermit-abi" 442 | version = "0.3.9" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 445 | 446 | [[package]] 447 | name = "hermit-abi" 448 | version = "0.4.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 451 | 452 | [[package]] 453 | name = "hex" 454 | version = "0.4.3" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 457 | 458 | [[package]] 459 | name = "iana-time-zone" 460 | version = "0.1.61" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 463 | dependencies = [ 464 | "android_system_properties", 465 | "core-foundation-sys", 466 | "iana-time-zone-haiku", 467 | "js-sys", 468 | "wasm-bindgen", 469 | "windows-core", 470 | ] 471 | 472 | [[package]] 473 | name = "iana-time-zone-haiku" 474 | version = "0.1.2" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 477 | dependencies = [ 478 | "cc", 479 | ] 480 | 481 | [[package]] 482 | name = "icu_collections" 483 | version = "1.5.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 486 | dependencies = [ 487 | "displaydoc", 488 | "yoke", 489 | "zerofrom", 490 | "zerovec", 491 | ] 492 | 493 | [[package]] 494 | name = "icu_locid" 495 | version = "1.5.0" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 498 | dependencies = [ 499 | "displaydoc", 500 | "litemap", 501 | "tinystr", 502 | "writeable", 503 | "zerovec", 504 | ] 505 | 506 | [[package]] 507 | name = "icu_locid_transform" 508 | version = "1.5.0" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 511 | dependencies = [ 512 | "displaydoc", 513 | "icu_locid", 514 | "icu_locid_transform_data", 515 | "icu_provider", 516 | "tinystr", 517 | "zerovec", 518 | ] 519 | 520 | [[package]] 521 | name = "icu_locid_transform_data" 522 | version = "1.5.0" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 525 | 526 | [[package]] 527 | name = "icu_normalizer" 528 | version = "1.5.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 531 | dependencies = [ 532 | "displaydoc", 533 | "icu_collections", 534 | "icu_normalizer_data", 535 | "icu_properties", 536 | "icu_provider", 537 | "smallvec", 538 | "utf16_iter", 539 | "utf8_iter", 540 | "write16", 541 | "zerovec", 542 | ] 543 | 544 | [[package]] 545 | name = "icu_normalizer_data" 546 | version = "1.5.0" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 549 | 550 | [[package]] 551 | name = "icu_properties" 552 | version = "1.5.1" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 555 | dependencies = [ 556 | "displaydoc", 557 | "icu_collections", 558 | "icu_locid_transform", 559 | "icu_properties_data", 560 | "icu_provider", 561 | "tinystr", 562 | "zerovec", 563 | ] 564 | 565 | [[package]] 566 | name = "icu_properties_data" 567 | version = "1.5.0" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 570 | 571 | [[package]] 572 | name = "icu_provider" 573 | version = "1.5.0" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 576 | dependencies = [ 577 | "displaydoc", 578 | "icu_locid", 579 | "icu_provider_macros", 580 | "stable_deref_trait", 581 | "tinystr", 582 | "writeable", 583 | "yoke", 584 | "zerofrom", 585 | "zerovec", 586 | ] 587 | 588 | [[package]] 589 | name = "icu_provider_macros" 590 | version = "1.5.0" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 593 | dependencies = [ 594 | "proc-macro2", 595 | "quote", 596 | "syn", 597 | ] 598 | 599 | [[package]] 600 | name = "idna" 601 | version = "1.0.3" 602 | source = "registry+https://github.com/rust-lang/crates.io-index" 603 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 604 | dependencies = [ 605 | "idna_adapter", 606 | "smallvec", 607 | "utf8_iter", 608 | ] 609 | 610 | [[package]] 611 | name = "idna_adapter" 612 | version = "1.2.0" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 615 | dependencies = [ 616 | "icu_normalizer", 617 | "icu_properties", 618 | ] 619 | 620 | [[package]] 621 | name = "is-terminal" 622 | version = "0.4.13" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" 625 | dependencies = [ 626 | "hermit-abi 0.4.0", 627 | "libc", 628 | "windows-sys 0.52.0", 629 | ] 630 | 631 | [[package]] 632 | name = "itoa" 633 | version = "1.0.14" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 636 | 637 | [[package]] 638 | name = "js-sys" 639 | version = "0.3.77" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 642 | dependencies = [ 643 | "once_cell", 644 | "wasm-bindgen", 645 | ] 646 | 647 | [[package]] 648 | name = "lazy_static" 649 | version = "1.5.0" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 652 | 653 | [[package]] 654 | name = "libc" 655 | version = "0.2.169" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 658 | 659 | [[package]] 660 | name = "libredox" 661 | version = "0.1.3" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 664 | dependencies = [ 665 | "bitflags 2.7.0", 666 | "libc", 667 | ] 668 | 669 | [[package]] 670 | name = "linux-raw-sys" 671 | version = "0.4.15" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 674 | 675 | [[package]] 676 | name = "litemap" 677 | version = "0.7.4" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 680 | 681 | [[package]] 682 | name = "lock_api" 683 | version = "0.4.12" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 686 | dependencies = [ 687 | "autocfg", 688 | "scopeguard", 689 | ] 690 | 691 | [[package]] 692 | name = "log" 693 | version = "0.4.22" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 696 | 697 | [[package]] 698 | name = "matchers" 699 | version = "0.1.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 702 | dependencies = [ 703 | "regex-automata", 704 | ] 705 | 706 | [[package]] 707 | name = "memchr" 708 | version = "2.3.4" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" 711 | 712 | [[package]] 713 | name = "minimal-lexical" 714 | version = "0.2.1" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 717 | 718 | [[package]] 719 | name = "miniz_oxide" 720 | version = "0.4.4" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" 723 | dependencies = [ 724 | "adler", 725 | "autocfg", 726 | ] 727 | 728 | [[package]] 729 | name = "miniz_oxide" 730 | version = "0.8.3" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" 733 | dependencies = [ 734 | "adler2", 735 | ] 736 | 737 | [[package]] 738 | name = "mio" 739 | version = "1.0.3" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 742 | dependencies = [ 743 | "libc", 744 | "wasi", 745 | "windows-sys 0.52.0", 746 | ] 747 | 748 | [[package]] 749 | name = "nom" 750 | version = "7.1.3" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 753 | dependencies = [ 754 | "memchr", 755 | "minimal-lexical", 756 | ] 757 | 758 | [[package]] 759 | name = "nu-ansi-term" 760 | version = "0.46.0" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 763 | dependencies = [ 764 | "overload", 765 | "winapi", 766 | ] 767 | 768 | [[package]] 769 | name = "num-bigint" 770 | version = "0.4.6" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 773 | dependencies = [ 774 | "num-integer", 775 | "num-traits", 776 | ] 777 | 778 | [[package]] 779 | name = "num-conv" 780 | version = "0.1.0" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 783 | 784 | [[package]] 785 | name = "num-integer" 786 | version = "0.1.46" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 789 | dependencies = [ 790 | "num-traits", 791 | ] 792 | 793 | [[package]] 794 | name = "num-traits" 795 | version = "0.2.19" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 798 | dependencies = [ 799 | "autocfg", 800 | ] 801 | 802 | [[package]] 803 | name = "num_cpus" 804 | version = "1.16.0" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 807 | dependencies = [ 808 | "hermit-abi 0.3.9", 809 | "libc", 810 | ] 811 | 812 | [[package]] 813 | name = "object" 814 | version = "0.24.0" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" 817 | 818 | [[package]] 819 | name = "once_cell" 820 | version = "1.20.2" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 823 | 824 | [[package]] 825 | name = "overload" 826 | version = "0.1.1" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 829 | 830 | [[package]] 831 | name = "parking_lot" 832 | version = "0.12.3" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 835 | dependencies = [ 836 | "lock_api", 837 | "parking_lot_core", 838 | ] 839 | 840 | [[package]] 841 | name = "parking_lot_core" 842 | version = "0.9.10" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 845 | dependencies = [ 846 | "cfg-if", 847 | "libc", 848 | "redox_syscall", 849 | "smallvec", 850 | "windows-targets", 851 | ] 852 | 853 | [[package]] 854 | name = "percent-encoding" 855 | version = "2.3.1" 856 | source = "registry+https://github.com/rust-lang/crates.io-index" 857 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 858 | 859 | [[package]] 860 | name = "pin-project-lite" 861 | version = "0.2.16" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 864 | 865 | [[package]] 866 | name = "pin-utils" 867 | version = "0.1.0" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 870 | 871 | [[package]] 872 | name = "powerfmt" 873 | version = "0.2.0" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 876 | 877 | [[package]] 878 | name = "ppv-lite86" 879 | version = "0.2.20" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 882 | dependencies = [ 883 | "zerocopy", 884 | ] 885 | 886 | [[package]] 887 | name = "proc-macro2" 888 | version = "1.0.93" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 891 | dependencies = [ 892 | "unicode-ident", 893 | ] 894 | 895 | [[package]] 896 | name = "procfs" 897 | version = "0.9.1" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "ab8809e0c18450a2db0f236d2a44ec0b4c1412d0eb936233579f0990faa5d5cd" 900 | dependencies = [ 901 | "bitflags 1.3.2", 902 | "byteorder", 903 | "chrono", 904 | "flate2", 905 | "hex", 906 | "lazy_static", 907 | "libc", 908 | ] 909 | 910 | [[package]] 911 | name = "quote" 912 | version = "1.0.38" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 915 | dependencies = [ 916 | "proc-macro2", 917 | ] 918 | 919 | [[package]] 920 | name = "rand" 921 | version = "0.8.5" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 924 | dependencies = [ 925 | "libc", 926 | "rand_chacha", 927 | "rand_core", 928 | ] 929 | 930 | [[package]] 931 | name = "rand_chacha" 932 | version = "0.3.1" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 935 | dependencies = [ 936 | "ppv-lite86", 937 | "rand_core", 938 | ] 939 | 940 | [[package]] 941 | name = "rand_core" 942 | version = "0.6.4" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 945 | dependencies = [ 946 | "getrandom", 947 | ] 948 | 949 | [[package]] 950 | name = "redis" 951 | version = "0.28.1" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "9f89727cba9cec05cc579942321ff6dd09fe57a8b3217f52f952301efa010da5" 954 | dependencies = [ 955 | "arc-swap", 956 | "bytes", 957 | "combine", 958 | "futures-util", 959 | "itoa", 960 | "num-bigint", 961 | "percent-encoding", 962 | "pin-project-lite", 963 | "ryu", 964 | "sha1_smol", 965 | "socket2", 966 | "tokio", 967 | "tokio-util", 968 | "url", 969 | ] 970 | 971 | [[package]] 972 | name = "redox_syscall" 973 | version = "0.5.8" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 976 | dependencies = [ 977 | "bitflags 2.7.0", 978 | ] 979 | 980 | [[package]] 981 | name = "redox_users" 982 | version = "0.4.6" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 985 | dependencies = [ 986 | "getrandom", 987 | "libredox", 988 | "thiserror 1.0.69", 989 | ] 990 | 991 | [[package]] 992 | name = "regex" 993 | version = "1.8.4" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" 996 | dependencies = [ 997 | "regex-syntax 0.7.5", 998 | ] 999 | 1000 | [[package]] 1001 | name = "regex-automata" 1002 | version = "0.1.10" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1005 | dependencies = [ 1006 | "regex-syntax 0.6.29", 1007 | ] 1008 | 1009 | [[package]] 1010 | name = "regex-syntax" 1011 | version = "0.6.29" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1014 | 1015 | [[package]] 1016 | name = "regex-syntax" 1017 | version = "0.7.5" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" 1020 | 1021 | [[package]] 1022 | name = "rustc-demangle" 1023 | version = "0.1.24" 1024 | source = "registry+https://github.com/rust-lang/crates.io-index" 1025 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1026 | 1027 | [[package]] 1028 | name = "rustix" 1029 | version = "0.38.43" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" 1032 | dependencies = [ 1033 | "bitflags 2.7.0", 1034 | "errno", 1035 | "libc", 1036 | "linux-raw-sys", 1037 | "windows-sys 0.59.0", 1038 | ] 1039 | 1040 | [[package]] 1041 | name = "rustversion" 1042 | version = "1.0.19" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 1045 | 1046 | [[package]] 1047 | name = "rusty-sidekiq" 1048 | version = "0.13.2" 1049 | dependencies = [ 1050 | "async-trait", 1051 | "bb8", 1052 | "chrono", 1053 | "convert_case", 1054 | "cron_clock", 1055 | "gethostname", 1056 | "hex", 1057 | "num_cpus", 1058 | "rand", 1059 | "redis", 1060 | "serde", 1061 | "serde_json", 1062 | "serial_test", 1063 | "sha2", 1064 | "simple-process-stats", 1065 | "slog-term", 1066 | "thiserror 2.0.11", 1067 | "tokio", 1068 | "tokio-util", 1069 | "tracing", 1070 | "tracing-subscriber", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "ryu" 1075 | version = "1.0.18" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1078 | 1079 | [[package]] 1080 | name = "scc" 1081 | version = "2.3.0" 1082 | source = "registry+https://github.com/rust-lang/crates.io-index" 1083 | checksum = "28e1c91382686d21b5ac7959341fcb9780fa7c03773646995a87c950fa7be640" 1084 | dependencies = [ 1085 | "sdd", 1086 | ] 1087 | 1088 | [[package]] 1089 | name = "scopeguard" 1090 | version = "1.2.0" 1091 | source = "registry+https://github.com/rust-lang/crates.io-index" 1092 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1093 | 1094 | [[package]] 1095 | name = "sdd" 1096 | version = "3.0.5" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" 1099 | 1100 | [[package]] 1101 | name = "serde" 1102 | version = "1.0.217" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 1105 | dependencies = [ 1106 | "serde_derive", 1107 | ] 1108 | 1109 | [[package]] 1110 | name = "serde_derive" 1111 | version = "1.0.217" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 1114 | dependencies = [ 1115 | "proc-macro2", 1116 | "quote", 1117 | "syn", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "serde_json" 1122 | version = "1.0.135" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" 1125 | dependencies = [ 1126 | "itoa", 1127 | "memchr", 1128 | "ryu", 1129 | "serde", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "serial_test" 1134 | version = "3.2.0" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" 1137 | dependencies = [ 1138 | "futures", 1139 | "log", 1140 | "once_cell", 1141 | "parking_lot", 1142 | "scc", 1143 | "serial_test_derive", 1144 | ] 1145 | 1146 | [[package]] 1147 | name = "serial_test_derive" 1148 | version = "3.2.0" 1149 | source = "registry+https://github.com/rust-lang/crates.io-index" 1150 | checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" 1151 | dependencies = [ 1152 | "proc-macro2", 1153 | "quote", 1154 | "syn", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "sha1_smol" 1159 | version = "1.0.1" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" 1162 | 1163 | [[package]] 1164 | name = "sha2" 1165 | version = "0.10.8" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1168 | dependencies = [ 1169 | "cfg-if", 1170 | "cpufeatures", 1171 | "digest", 1172 | ] 1173 | 1174 | [[package]] 1175 | name = "sharded-slab" 1176 | version = "0.1.7" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1179 | dependencies = [ 1180 | "lazy_static", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "shlex" 1185 | version = "1.3.0" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1188 | 1189 | [[package]] 1190 | name = "signal-hook-registry" 1191 | version = "1.4.2" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1194 | dependencies = [ 1195 | "libc", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "simple-process-stats" 1200 | version = "1.0.0" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "fbac11c9c71488f54be642954a9d41eefeecbe009fe2c98a6d0ad8c785565199" 1203 | dependencies = [ 1204 | "darwin-libproc", 1205 | "libc", 1206 | "procfs", 1207 | "thiserror 1.0.69", 1208 | "tokio", 1209 | "winapi", 1210 | ] 1211 | 1212 | [[package]] 1213 | name = "slab" 1214 | version = "0.4.9" 1215 | source = "registry+https://github.com/rust-lang/crates.io-index" 1216 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1217 | dependencies = [ 1218 | "autocfg", 1219 | ] 1220 | 1221 | [[package]] 1222 | name = "slog" 1223 | version = "2.7.0" 1224 | source = "registry+https://github.com/rust-lang/crates.io-index" 1225 | checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" 1226 | 1227 | [[package]] 1228 | name = "slog-term" 1229 | version = "2.9.1" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" 1232 | dependencies = [ 1233 | "is-terminal", 1234 | "slog", 1235 | "term", 1236 | "thread_local", 1237 | "time", 1238 | ] 1239 | 1240 | [[package]] 1241 | name = "smallvec" 1242 | version = "1.13.2" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1245 | 1246 | [[package]] 1247 | name = "socket2" 1248 | version = "0.5.8" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1251 | dependencies = [ 1252 | "libc", 1253 | "windows-sys 0.52.0", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "stable_deref_trait" 1258 | version = "1.2.0" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1261 | 1262 | [[package]] 1263 | name = "syn" 1264 | version = "2.0.96" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 1267 | dependencies = [ 1268 | "proc-macro2", 1269 | "quote", 1270 | "unicode-ident", 1271 | ] 1272 | 1273 | [[package]] 1274 | name = "synstructure" 1275 | version = "0.13.1" 1276 | source = "registry+https://github.com/rust-lang/crates.io-index" 1277 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1278 | dependencies = [ 1279 | "proc-macro2", 1280 | "quote", 1281 | "syn", 1282 | ] 1283 | 1284 | [[package]] 1285 | name = "term" 1286 | version = "0.7.0" 1287 | source = "registry+https://github.com/rust-lang/crates.io-index" 1288 | checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" 1289 | dependencies = [ 1290 | "dirs-next", 1291 | "rustversion", 1292 | "winapi", 1293 | ] 1294 | 1295 | [[package]] 1296 | name = "thiserror" 1297 | version = "1.0.69" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1300 | dependencies = [ 1301 | "thiserror-impl 1.0.69", 1302 | ] 1303 | 1304 | [[package]] 1305 | name = "thiserror" 1306 | version = "2.0.11" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 1309 | dependencies = [ 1310 | "thiserror-impl 2.0.11", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "thiserror-impl" 1315 | version = "1.0.69" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1318 | dependencies = [ 1319 | "proc-macro2", 1320 | "quote", 1321 | "syn", 1322 | ] 1323 | 1324 | [[package]] 1325 | name = "thiserror-impl" 1326 | version = "2.0.11" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 1329 | dependencies = [ 1330 | "proc-macro2", 1331 | "quote", 1332 | "syn", 1333 | ] 1334 | 1335 | [[package]] 1336 | name = "thread_local" 1337 | version = "1.1.8" 1338 | source = "registry+https://github.com/rust-lang/crates.io-index" 1339 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1340 | dependencies = [ 1341 | "cfg-if", 1342 | "once_cell", 1343 | ] 1344 | 1345 | [[package]] 1346 | name = "time" 1347 | version = "0.3.37" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 1350 | dependencies = [ 1351 | "deranged", 1352 | "itoa", 1353 | "num-conv", 1354 | "powerfmt", 1355 | "serde", 1356 | "time-core", 1357 | "time-macros", 1358 | ] 1359 | 1360 | [[package]] 1361 | name = "time-core" 1362 | version = "0.1.2" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1365 | 1366 | [[package]] 1367 | name = "time-macros" 1368 | version = "0.2.19" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" 1371 | dependencies = [ 1372 | "num-conv", 1373 | "time-core", 1374 | ] 1375 | 1376 | [[package]] 1377 | name = "tinystr" 1378 | version = "0.7.6" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1381 | dependencies = [ 1382 | "displaydoc", 1383 | "zerovec", 1384 | ] 1385 | 1386 | [[package]] 1387 | name = "tokio" 1388 | version = "1.43.0" 1389 | source = "registry+https://github.com/rust-lang/crates.io-index" 1390 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 1391 | dependencies = [ 1392 | "backtrace", 1393 | "bytes", 1394 | "libc", 1395 | "mio", 1396 | "parking_lot", 1397 | "pin-project-lite", 1398 | "signal-hook-registry", 1399 | "socket2", 1400 | "tokio-macros", 1401 | "windows-sys 0.52.0", 1402 | ] 1403 | 1404 | [[package]] 1405 | name = "tokio-macros" 1406 | version = "2.5.0" 1407 | source = "registry+https://github.com/rust-lang/crates.io-index" 1408 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1409 | dependencies = [ 1410 | "proc-macro2", 1411 | "quote", 1412 | "syn", 1413 | ] 1414 | 1415 | [[package]] 1416 | name = "tokio-util" 1417 | version = "0.7.13" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 1420 | dependencies = [ 1421 | "bytes", 1422 | "futures-core", 1423 | "futures-sink", 1424 | "pin-project-lite", 1425 | "tokio", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "tracing" 1430 | version = "0.1.41" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1433 | dependencies = [ 1434 | "pin-project-lite", 1435 | "tracing-attributes", 1436 | "tracing-core", 1437 | ] 1438 | 1439 | [[package]] 1440 | name = "tracing-attributes" 1441 | version = "0.1.28" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1444 | dependencies = [ 1445 | "proc-macro2", 1446 | "quote", 1447 | "syn", 1448 | ] 1449 | 1450 | [[package]] 1451 | name = "tracing-core" 1452 | version = "0.1.33" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1455 | dependencies = [ 1456 | "once_cell", 1457 | "valuable", 1458 | ] 1459 | 1460 | [[package]] 1461 | name = "tracing-log" 1462 | version = "0.2.0" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1465 | dependencies = [ 1466 | "log", 1467 | "once_cell", 1468 | "tracing-core", 1469 | ] 1470 | 1471 | [[package]] 1472 | name = "tracing-serde" 1473 | version = "0.2.0" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" 1476 | dependencies = [ 1477 | "serde", 1478 | "tracing-core", 1479 | ] 1480 | 1481 | [[package]] 1482 | name = "tracing-subscriber" 1483 | version = "0.3.19" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1486 | dependencies = [ 1487 | "matchers", 1488 | "nu-ansi-term", 1489 | "once_cell", 1490 | "regex", 1491 | "serde", 1492 | "serde_json", 1493 | "sharded-slab", 1494 | "smallvec", 1495 | "thread_local", 1496 | "tracing", 1497 | "tracing-core", 1498 | "tracing-log", 1499 | "tracing-serde", 1500 | ] 1501 | 1502 | [[package]] 1503 | name = "typenum" 1504 | version = "1.17.0" 1505 | source = "registry+https://github.com/rust-lang/crates.io-index" 1506 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1507 | 1508 | [[package]] 1509 | name = "unicode-ident" 1510 | version = "1.0.14" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1513 | 1514 | [[package]] 1515 | name = "unicode-segmentation" 1516 | version = "1.12.0" 1517 | source = "registry+https://github.com/rust-lang/crates.io-index" 1518 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1519 | 1520 | [[package]] 1521 | name = "url" 1522 | version = "2.5.4" 1523 | source = "registry+https://github.com/rust-lang/crates.io-index" 1524 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1525 | dependencies = [ 1526 | "form_urlencoded", 1527 | "idna", 1528 | "percent-encoding", 1529 | ] 1530 | 1531 | [[package]] 1532 | name = "utf16_iter" 1533 | version = "1.0.5" 1534 | source = "registry+https://github.com/rust-lang/crates.io-index" 1535 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1536 | 1537 | [[package]] 1538 | name = "utf8_iter" 1539 | version = "1.0.4" 1540 | source = "registry+https://github.com/rust-lang/crates.io-index" 1541 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1542 | 1543 | [[package]] 1544 | name = "valuable" 1545 | version = "0.1.0" 1546 | source = "registry+https://github.com/rust-lang/crates.io-index" 1547 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1548 | 1549 | [[package]] 1550 | name = "version_check" 1551 | version = "0.9.5" 1552 | source = "registry+https://github.com/rust-lang/crates.io-index" 1553 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1554 | 1555 | [[package]] 1556 | name = "wasi" 1557 | version = "0.11.0+wasi-snapshot-preview1" 1558 | source = "registry+https://github.com/rust-lang/crates.io-index" 1559 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1560 | 1561 | [[package]] 1562 | name = "wasm-bindgen" 1563 | version = "0.2.100" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1566 | dependencies = [ 1567 | "cfg-if", 1568 | "once_cell", 1569 | "rustversion", 1570 | "wasm-bindgen-macro", 1571 | ] 1572 | 1573 | [[package]] 1574 | name = "wasm-bindgen-backend" 1575 | version = "0.2.100" 1576 | source = "registry+https://github.com/rust-lang/crates.io-index" 1577 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1578 | dependencies = [ 1579 | "bumpalo", 1580 | "log", 1581 | "proc-macro2", 1582 | "quote", 1583 | "syn", 1584 | "wasm-bindgen-shared", 1585 | ] 1586 | 1587 | [[package]] 1588 | name = "wasm-bindgen-macro" 1589 | version = "0.2.100" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1592 | dependencies = [ 1593 | "quote", 1594 | "wasm-bindgen-macro-support", 1595 | ] 1596 | 1597 | [[package]] 1598 | name = "wasm-bindgen-macro-support" 1599 | version = "0.2.100" 1600 | source = "registry+https://github.com/rust-lang/crates.io-index" 1601 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1602 | dependencies = [ 1603 | "proc-macro2", 1604 | "quote", 1605 | "syn", 1606 | "wasm-bindgen-backend", 1607 | "wasm-bindgen-shared", 1608 | ] 1609 | 1610 | [[package]] 1611 | name = "wasm-bindgen-shared" 1612 | version = "0.2.100" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1615 | dependencies = [ 1616 | "unicode-ident", 1617 | ] 1618 | 1619 | [[package]] 1620 | name = "winapi" 1621 | version = "0.3.9" 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" 1623 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1624 | dependencies = [ 1625 | "winapi-i686-pc-windows-gnu", 1626 | "winapi-x86_64-pc-windows-gnu", 1627 | ] 1628 | 1629 | [[package]] 1630 | name = "winapi-i686-pc-windows-gnu" 1631 | version = "0.4.0" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1634 | 1635 | [[package]] 1636 | name = "winapi-x86_64-pc-windows-gnu" 1637 | version = "0.4.0" 1638 | source = "registry+https://github.com/rust-lang/crates.io-index" 1639 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1640 | 1641 | [[package]] 1642 | name = "windows-core" 1643 | version = "0.52.0" 1644 | source = "registry+https://github.com/rust-lang/crates.io-index" 1645 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1646 | dependencies = [ 1647 | "windows-targets", 1648 | ] 1649 | 1650 | [[package]] 1651 | name = "windows-sys" 1652 | version = "0.52.0" 1653 | source = "registry+https://github.com/rust-lang/crates.io-index" 1654 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1655 | dependencies = [ 1656 | "windows-targets", 1657 | ] 1658 | 1659 | [[package]] 1660 | name = "windows-sys" 1661 | version = "0.59.0" 1662 | source = "registry+https://github.com/rust-lang/crates.io-index" 1663 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1664 | dependencies = [ 1665 | "windows-targets", 1666 | ] 1667 | 1668 | [[package]] 1669 | name = "windows-targets" 1670 | version = "0.52.6" 1671 | source = "registry+https://github.com/rust-lang/crates.io-index" 1672 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1673 | dependencies = [ 1674 | "windows_aarch64_gnullvm", 1675 | "windows_aarch64_msvc", 1676 | "windows_i686_gnu", 1677 | "windows_i686_gnullvm", 1678 | "windows_i686_msvc", 1679 | "windows_x86_64_gnu", 1680 | "windows_x86_64_gnullvm", 1681 | "windows_x86_64_msvc", 1682 | ] 1683 | 1684 | [[package]] 1685 | name = "windows_aarch64_gnullvm" 1686 | version = "0.52.6" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1689 | 1690 | [[package]] 1691 | name = "windows_aarch64_msvc" 1692 | version = "0.52.6" 1693 | source = "registry+https://github.com/rust-lang/crates.io-index" 1694 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1695 | 1696 | [[package]] 1697 | name = "windows_i686_gnu" 1698 | version = "0.52.6" 1699 | source = "registry+https://github.com/rust-lang/crates.io-index" 1700 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1701 | 1702 | [[package]] 1703 | name = "windows_i686_gnullvm" 1704 | version = "0.52.6" 1705 | source = "registry+https://github.com/rust-lang/crates.io-index" 1706 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1707 | 1708 | [[package]] 1709 | name = "windows_i686_msvc" 1710 | version = "0.52.6" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1713 | 1714 | [[package]] 1715 | name = "windows_x86_64_gnu" 1716 | version = "0.52.6" 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" 1718 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1719 | 1720 | [[package]] 1721 | name = "windows_x86_64_gnullvm" 1722 | version = "0.52.6" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1725 | 1726 | [[package]] 1727 | name = "windows_x86_64_msvc" 1728 | version = "0.52.6" 1729 | source = "registry+https://github.com/rust-lang/crates.io-index" 1730 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1731 | 1732 | [[package]] 1733 | name = "write16" 1734 | version = "1.0.0" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1737 | 1738 | [[package]] 1739 | name = "writeable" 1740 | version = "0.5.5" 1741 | source = "registry+https://github.com/rust-lang/crates.io-index" 1742 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1743 | 1744 | [[package]] 1745 | name = "yoke" 1746 | version = "0.7.5" 1747 | source = "registry+https://github.com/rust-lang/crates.io-index" 1748 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1749 | dependencies = [ 1750 | "serde", 1751 | "stable_deref_trait", 1752 | "yoke-derive", 1753 | "zerofrom", 1754 | ] 1755 | 1756 | [[package]] 1757 | name = "yoke-derive" 1758 | version = "0.7.5" 1759 | source = "registry+https://github.com/rust-lang/crates.io-index" 1760 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1761 | dependencies = [ 1762 | "proc-macro2", 1763 | "quote", 1764 | "syn", 1765 | "synstructure", 1766 | ] 1767 | 1768 | [[package]] 1769 | name = "zerocopy" 1770 | version = "0.7.35" 1771 | source = "registry+https://github.com/rust-lang/crates.io-index" 1772 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1773 | dependencies = [ 1774 | "byteorder", 1775 | "zerocopy-derive", 1776 | ] 1777 | 1778 | [[package]] 1779 | name = "zerocopy-derive" 1780 | version = "0.7.35" 1781 | source = "registry+https://github.com/rust-lang/crates.io-index" 1782 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1783 | dependencies = [ 1784 | "proc-macro2", 1785 | "quote", 1786 | "syn", 1787 | ] 1788 | 1789 | [[package]] 1790 | name = "zerofrom" 1791 | version = "0.1.5" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1794 | dependencies = [ 1795 | "zerofrom-derive", 1796 | ] 1797 | 1798 | [[package]] 1799 | name = "zerofrom-derive" 1800 | version = "0.1.5" 1801 | source = "registry+https://github.com/rust-lang/crates.io-index" 1802 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1803 | dependencies = [ 1804 | "proc-macro2", 1805 | "quote", 1806 | "syn", 1807 | "synstructure", 1808 | ] 1809 | 1810 | [[package]] 1811 | name = "zerovec" 1812 | version = "0.10.4" 1813 | source = "registry+https://github.com/rust-lang/crates.io-index" 1814 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1815 | dependencies = [ 1816 | "yoke", 1817 | "zerofrom", 1818 | "zerovec-derive", 1819 | ] 1820 | 1821 | [[package]] 1822 | name = "zerovec-derive" 1823 | version = "0.10.3" 1824 | source = "registry+https://github.com/rust-lang/crates.io-index" 1825 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1826 | dependencies = [ 1827 | "proc-macro2", 1828 | "quote", 1829 | "syn", 1830 | ] 1831 | --------------------------------------------------------------------------------