1use futures_sink::Sink;
97use futures_util::SinkExt;
98use futures_util::{Stream, StreamExt};
99use std::pin::Pin;
100use std::sync::Arc;
101use std::task::{Context, Poll};
102use std::{fmt, marker::PhantomData};
103use thiserror::Error;
104use tower_layer::Identity;
105
106use crate::backend::BackendConfig;
107use crate::backend::finalize::Ephemeral;
108use crate::error::BoxDynError;
109use crate::task::task_id::RandomId;
110use crate::{backend::Backend, task::Task, worker::context::WorkerContext};
111
112type Fetcher<DB, Config, Fetch> =
113 Arc<Box<dyn Fn(&mut DB, &Config, &WorkerContext) -> Fetch + Send + Sync>>;
114
115type Sinker<DB, Config, Sink> = Arc<Box<dyn Fn(&mut DB, &Config) -> Sink + Send + Sync>>;
116
117#[doc = features_table! {
132 setup = "{ unreachable!() }",
133 TaskSink => supported("Ability to push new tasks", false),
134 Serialization => supported("Serialization support for arguments", false),
135 FetchById => not_supported("Allow fetching a task by its ID"),
136 RegisterWorker => not_implemented("Allow registering a worker with the backend"),
137 PipeExt => limited("Allow other backends to pipe to this backend", false), BackendFactory => not_implemented("Share the same [`CustomBackend`] across multiple workers", false),
139 Workflow => not_implemented("Flexible enough to support workflows"),
140 WaitForCompletion => not_implemented("Wait for tasks to complete without blocking"), ResumeById => not_supported("Resume a task by its ID"),
142 ResumeAbandoned => not_supported("Resume abandoned tasks"),
143 ListWorkers => not_implemented("List all workers registered with the backend"),
144 ListTasks => not_implemented("List all tasks in the backend"),
145}]
146#[pin_project::pin_project]
147#[must_use = "Custom backends must be polled or used as a sink"]
148pub struct CustomBackend<Args, DB, Fetch, Sink, Config = ()> {
149 _marker: PhantomData<Args>,
150 db: DB,
151 fetcher: Fetcher<DB, Config, Fetch>,
152 sinker: Sinker<DB, Config, Sink>,
153 #[pin]
154 current_sink: Sink,
155 config: Config,
156
157 stream: Option<Fetch>,
158}
159
160impl<Args, DB, Fetch, Sink, Config> Clone for CustomBackend<Args, DB, Fetch, Sink, Config>
161where
162 DB: Clone,
163 Config: Clone,
164{
165 fn clone(&self) -> Self {
166 let mut db = self.db.clone();
167 let current_sink = (self.sinker)(&mut db, &self.config);
168 Self {
169 _marker: PhantomData,
170 db,
171 fetcher: Arc::clone(&self.fetcher),
172 sinker: Arc::clone(&self.sinker),
173 current_sink,
174 config: self.config.clone(),
175
176 stream: None,
177 }
178 }
179}
180
181impl<Args, DB, Fetch, Sink, Config> fmt::Debug for CustomBackend<Args, DB, Fetch, Sink, Config>
182where
183 DB: fmt::Debug,
184 Config: fmt::Debug,
185{
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 f.debug_struct("CustomBackend")
188 .field(
189 "_marker",
190 &format_args!("PhantomData<({})>", std::any::type_name::<Args>()),
191 )
192 .field("db", &self.db)
193 .field("fetcher", &"Fn(&mut DB, &Config, &WorkerContext) -> Fetch")
194 .field("sink", &"Fn(&mut DB, &Config) -> Sink")
195 .field("config", &self.config)
196 .finish()
197 }
198}
199
200type FetcherBuilder<DB, Config, Fetch> =
201 Box<dyn Fn(&mut DB, &Config, &WorkerContext) -> Fetch + Send + Sync + 'static>;
202
203type SinkerBuilder<DB, Config, Sink> =
204 Box<dyn Fn(&mut DB, &Config) -> Sink + Send + Sync + 'static>;
205
206pub struct BackendBuilder<Args, DB, Fetch, Sink, Config = ()> {
210 _marker: PhantomData<Args>,
211 database: Option<DB>,
212 fetcher: Option<FetcherBuilder<DB, Config, Fetch>>,
213 sink: Option<SinkerBuilder<DB, Config, Sink>>,
214 config: Option<Config>,
215}
216
217impl<Args, DB, Fetch, Sink, Config> fmt::Debug for BackendBuilder<Args, DB, Fetch, Sink, Config>
218where
219 DB: fmt::Debug,
220 Config: fmt::Debug,
221{
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 f.debug_struct("BackendBuilder")
224 .field(
225 "_marker",
226 &format_args!("PhantomData<({})>", std::any::type_name::<Args>(),),
227 )
228 .field("database", &self.database)
229 .field("fetcher", &self.fetcher.as_ref().map(|_| "Some(fn)"))
230 .field("sink", &self.sink.as_ref().map(|_| "Some(fn)"))
231 .field("config", &self.config)
232 .finish()
233 }
234}
235
236impl<Args, DB, Fetch, Sink, Config> Default for BackendBuilder<Args, DB, Fetch, Sink, Config> {
237 fn default() -> Self {
238 Self {
239 _marker: PhantomData,
240 database: None,
241 fetcher: None,
242 sink: None,
243 config: None,
244 }
245 }
246}
247
248impl<Args, DB, Fetch, Sink> BackendBuilder<Args, DB, Fetch, Sink, ()> {
249 #[must_use]
251 pub fn new() -> Self {
252 Self::new_with_cfg(())
253 }
254
255 pub fn new_with_cfg<Config>(config: Config) -> BackendBuilder<Args, DB, Fetch, Sink, Config> {
257 BackendBuilder {
258 config: Some(config),
259 ..Default::default()
260 }
261 }
262}
263
264impl<Args, DB, Fetch, Sink, Config> BackendBuilder<Args, DB, Fetch, Sink, Config> {
265 #[must_use]
267 pub fn database(mut self, db: DB) -> Self {
268 self.database = Some(db);
269 self
270 }
271
272 #[must_use]
274 pub fn fetcher<F: Fn(&mut DB, &Config, &WorkerContext) -> Fetch + Send + Sync + 'static>(
275 mut self,
276 fetcher: F,
277 ) -> Self {
278 self.fetcher = Some(Box::new(fetcher));
279 self
280 }
281
282 #[must_use]
284 pub fn sink<F: Fn(&mut DB, &Config) -> Sink + Send + Sync + 'static>(
285 mut self,
286 sink: F,
287 ) -> Self {
288 self.sink = Some(Box::new(sink));
289 self
290 }
291
292 #[allow(clippy::type_complexity)]
293 pub fn build(self) -> Result<CustomBackend<Args, DB, Fetch, Sink, Config>, BuildError> {
295 let mut db = self.database.ok_or(BuildError::MissingDb)?;
296 let config = self.config.ok_or(BuildError::MissingConfig)?;
297 let sink_fn = self.sink.ok_or(BuildError::MissingSink)?;
298 let sink = sink_fn(&mut db, &config);
299
300 Ok(CustomBackend {
301 _marker: PhantomData,
302 db,
303 fetcher: self
304 .fetcher
305 .map(Arc::new)
306 .ok_or(BuildError::MissingFetcher)?,
307 current_sink: sink,
308 sinker: Arc::new(sink_fn),
309 config,
310 stream: None,
311 })
312 }
313}
314
315#[derive(Debug, Error)]
317#[non_exhaustive]
318pub enum BuildError {
319 #[error("Database db is required")]
321 MissingDb,
322 #[error("Fetcher is required")]
324 MissingFetcher,
325 #[error("Sink is required")]
327 MissingSink,
328 #[error("Config is required")]
330 MissingConfig,
331}
332
333#[derive(Debug, Error)]
335#[non_exhaustive]
336pub enum CustomBackendError {
337 #[error("Inner error: {0}")]
339 Inner(#[from] BoxDynError),
340}
341
342impl<Args: 'static, DB, Fetch, S, E, Config> Backend for CustomBackend<Args, DB, Fetch, S, Config>
343where
344 Fetch: Stream<Item = Result<Option<Task<Args>>, E>> + Unpin + Send + 'static,
345 S: Sink<Task<Args>, Error = E> + Unpin + Send + 'static,
346 E: Into<BoxDynError>,
347{
348 type Task = Task<Args>;
349
350 type Error = CustomBackendError;
351
352 fn poll_ready(
353 &mut self,
354 _: &mut Context<'_>,
355 _: &WorkerContext,
356 ) -> Poll<Result<(), Self::Error>> {
357 Poll::Ready(Ok(()))
358 }
359
360 fn poll_next(
361 &mut self,
362 cx: &mut Context<'_>,
363 worker: &WorkerContext,
364 ) -> Poll<Option<Result<Self::Task, Self::Error>>> {
365 if self.stream.is_none() {
366 self.stream = Some((self.fetcher)(&mut self.db, &self.config, worker));
367 }
368 let stream = self.stream.as_mut().unwrap();
369 stream.poll_next_unpin(cx).map(|item| {
370 item.transpose()
371 .map(|res| res.flatten())
372 .map_err(|e| CustomBackendError::Inner(e.into()))
373 .transpose()
374 })
375 }
376
377 fn poll_close(
378 &mut self,
379 cx: &mut Context<'_>,
380 _: &WorkerContext,
381 ) -> Poll<Result<(), Self::Error>> {
382 self.current_sink
383 .poll_close_unpin(cx)
384 .map_err(|e| CustomBackendError::Inner(e.into()))
385 }
386}
387
388impl<Args, DB, Fetch, S, Config, E> BackendConfig for CustomBackend<Args, DB, Fetch, S, Config>
389where
390 Fetch: Stream<Item = Result<Option<Task<Args>>, E>> + Unpin + Send + 'static,
391 S: Sink<Task<Args>, Error = E> + Unpin + Send + 'static,
392 E: Into<BoxDynError>,
393 Args: 'static,
394{
395 type Id = RandomId;
396 type Args = Args;
397 type Kind = Ephemeral;
398
399 type Layer = Identity;
400
401 type Config = Config;
402
403 fn config(&self) -> &Self::Config {
404 &self.config
405 }
406
407 fn middleware(&mut self, _: &mut WorkerContext) -> Self::Layer {
408 Identity::new()
409 }
410}
411
412impl<Args, DB, Fetch, S, Config> Sink<Task<Args>> for CustomBackend<Args, DB, Fetch, S, Config>
413where
414 S: Sink<Task<Args>>,
415 S::Error: Into<BoxDynError>,
416{
417 type Error = CustomBackendError;
418
419 fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
420 self.project()
421 .current_sink
422 .poll_ready_unpin(cx)
423 .map_err(|e| CustomBackendError::Inner(e.into()))
424 }
425
426 fn start_send(self: Pin<&mut Self>, item: Task<Args>) -> Result<(), Self::Error> {
427 self.project()
428 .current_sink
429 .start_send_unpin(item)
430 .map_err(|e| CustomBackendError::Inner(e.into()))
431 }
432
433 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
434 self.project()
435 .current_sink
436 .poll_flush_unpin(cx)
437 .map_err(|e| CustomBackendError::Inner(e.into()))
438 }
439
440 fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
441 self.project()
442 .current_sink
443 .poll_close_unpin(cx)
444 .map_err(|e| CustomBackendError::Inner(e.into()))
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use std::{collections::VecDeque, time::Duration};
451
452 use futures_util::{FutureExt, lock::Mutex, sink, stream};
453
454 use crate::{
455 backend::TaskSink,
456 error::BoxDynError,
457 task::task_id::{RandomId, TaskId},
458 worker::{builder::WorkerBuilder, ext::event_listener::EventListenerExt},
459 };
460
461 use super::*;
462
463 const ITEMS: u32 = 10;
464
465 #[tokio::test]
466 async fn basic_custom_backend() {
467 let memory: Arc<Mutex<VecDeque<Task<u32>>>> = Arc::new(Mutex::new(VecDeque::new()));
468
469 let mut backend = BackendBuilder::new()
470 .database(memory)
471 .fetcher(|db, _, _| {
472 stream::unfold(db.clone(), |p| async move {
473 tokio::time::sleep(Duration::from_millis(100)).await; let mut db = p.try_lock().unwrap();
475 let item = db.pop_front();
476 drop(db);
477 match item {
478 Some(item) => Some((Ok::<_, CustomBackendError>(Some(item)), p)),
479 None => Some((Ok::<_, CustomBackendError>(None), p)),
480 }
481 })
482 .boxed()
483 })
484 .sink(|db, _| {
485 sink::unfold(db.clone(), move |p, item: Task<u32>| {
486 async move {
487 let mut db = p.try_lock().unwrap();
488 let item = item
489 .into_builder()
490 .task_id(TaskId::String(RandomId::default().to_string()))
491 .build();
492 db.push_back(item);
493 drop(db);
494 Ok::<_, CustomBackendError>(p)
495 }
496 .boxed()
497 })
498 })
499 .build()
500 .unwrap();
501
502 for i in 0..ITEMS {
503 backend.push(i).await.unwrap();
504 }
505
506 async fn task(task: u32, worker: WorkerContext) -> Result<(), BoxDynError> {
507 tokio::time::sleep(Duration::from_secs(1)).await;
508 if task == ITEMS - 1 {
509 worker.stop().unwrap();
510 return Err("Worker stopped!")?;
511 }
512 Ok(())
513 }
514
515 let worker = WorkerBuilder::new("rango-tango")
516 .backend(backend)
517 .on_event(|worker, ev| {
518 println!("On Event = {ev:?} from {}", worker.name());
519 })
520 .build(task);
521 worker.run().await.unwrap();
522 }
523}