88//!
99//! ```no_run
1010//! use pluto_core::{
11- //! deadline::{DeadlinerTask, DutyDeadlineCalculator},
11+ //! deadline::{Deadliner, DeadlinerTask, DutyDeadlineCalculator},
1212//! types::{Duty, SlotNumber},
1313//! };
1414//! use pluto_eth2api::EthBeaconNodeApiClient;
15- //! use std::sync::Arc;
1615//! use tokio_util::sync::CancellationToken;
1716//!
1817//! # async fn example(client: &EthBeaconNodeApiClient) -> anyhow::Result<()> {
1918//! let cancel_token = CancellationToken::new();
2019//! let calculator = DutyDeadlineCalculator::from_client(client).await?;
21- //! let deadliner = DeadlinerTask::start(cancel_token, "example", calculator);
20+ //! let ( deadliner, mut rx) = DeadlinerTask::start(cancel_token, "example", calculator);
2221//!
2322//! let duty = Duty::new_attester_duty(SlotNumber::new(1));
2423//! let added = deadliner.add(duty).await;
2524//!
26- //! if let Some(mut rx) = deadliner.c() {
27- //! while let Some(expired_duty) = rx.recv().await {
28- //! println!("Duty expired: {}", expired_duty);
29- //! }
25+ //! while let Some(expired_duty) = rx.recv().await {
26+ //! println!("Duty expired: {}", expired_duty);
3027//! }
3128//! # Ok(())
3229//! # }
@@ -41,11 +38,7 @@ use crate::types::{Duty, DutyType, SlotNumber};
4138use async_trait:: async_trait;
4239use chrono:: { DateTime , Utc } ;
4340use pluto_eth2api:: EthBeaconNodeApiClientError ;
44- use std:: {
45- collections:: HashSet ,
46- sync:: { Arc , Mutex } ,
47- time:: Duration ,
48- } ;
41+ use std:: { collections:: HashSet , time:: Duration } ;
4942use tokio:: {
5043 sync:: { mpsc, oneshot} ,
5144 time:: sleep,
@@ -87,10 +80,9 @@ fn to_chrono_duration(duration: Duration) -> Result<chrono::Duration> {
8780
8881/// Deadliner provides duty deadline functionality.
8982///
90- /// The `c()` method returns a channel for receiving expired duties.
91- /// It may only be called once and the returned channel should be used
92- /// by a single task. Multiple instances are required for different
93- /// components and use cases.
83+ /// Producers submit duties via [`add`](Self::add). Expired duties are
84+ /// delivered on the receiver paired with the handle at
85+ /// [`DeadlinerTask::start`].
9486#[ async_trait]
9587pub trait Deadliner : Send + Sync {
9688 /// Adds a duty for deadline scheduling.
@@ -104,12 +96,6 @@ pub trait Deadliner: Send + Sync {
10496 /// - The calculator reports the duty has no deadline (`Ok(None)`)
10597 /// - The calculator failed to compute the deadline (`Err(_)`)
10698 async fn add ( & self , duty : Duty ) -> bool ;
107-
108- /// Returns the channel for receiving deadlined duties.
109- ///
110- /// This method may only be called once and returns `None` on subsequent
111- /// calls. The returned channel should only be used by a single task.
112- fn c ( & self ) -> Option < mpsc:: Receiver < Duty > > ;
11399}
114100
115101/// Internal message type for adding duties to the deadliner.
@@ -118,13 +104,13 @@ struct DeadlineInput {
118104 response_tx : oneshot:: Sender < bool > ,
119105}
120106
121- /// Public-facing handle: the `Arc<dyn Deadliner>` returned by
122- /// [`DeadlinerTask::start`] wraps this. Holds the input channel, the
123- /// take-once output receiver, and the cancellation token.
124- struct DeadlinerHandle {
107+ /// Public-facing handle returned (paired with the expired-duty receiver) by
108+ /// [`DeadlinerTask::start`]. Cloning is cheap and shares the same background
109+ /// task — share it freely across producers inside one service.
110+ #[ derive( Clone ) ]
111+ pub struct DeadlinerHandle {
125112 cancel_token : CancellationToken ,
126113 input_tx : mpsc:: Sender < DeadlineInput > ,
127- output_rx : Mutex < Option < mpsc:: Receiver < Duty > > > ,
128114}
129115
130116#[ async_trait]
@@ -146,18 +132,11 @@ impl Deadliner for DeadlinerHandle {
146132 // Wait for response
147133 response_rx. await . unwrap_or ( false )
148134 }
149-
150- fn c ( & self ) -> Option < mpsc:: Receiver < Duty > > {
151- self . output_rx
152- . lock ( )
153- . ok ( )
154- . and_then ( |mut guard| guard. take ( ) )
155- }
156135}
157136
158137/// Owned state of the background task that drives a [`DeadlinerHandle`]'s
159138/// duty timers. Held exclusively by the spawned task — that's why it lives
160- /// outside the `Arc<dyn Deadliner>` and `run_task` can take `mut self`.
139+ /// outside the public handle and `run_task` can take `mut self`.
161140/// Constructed and spawned via [`DeadlinerTask::start`].
162141pub struct DeadlinerTask < C > {
163142 cancel_token : CancellationToken ,
@@ -172,14 +151,15 @@ pub struct DeadlinerTask<C> {
172151}
173152
174153impl < C : DeadlineCalculator > DeadlinerTask < C > {
175- /// Builds the public-facing [`Deadliner`] handle and spawns the background
176- /// task that drives it. The background loop exits when `cancel_token` is
177- /// cancelled.
154+ /// Spawns the background task and returns a `(handle, expired_rx)` pair.
155+ /// The cloneable `handle` is for adding duties from any number of
156+ /// producers; `expired_rx` is the single consumer's receiver of expired
157+ /// duties. The background loop exits when `cancel_token` is cancelled.
178158 pub fn start (
179159 cancel_token : CancellationToken ,
180160 label : impl Into < String > ,
181161 calculator : C ,
182- ) -> Arc < dyn Deadliner > {
162+ ) -> ( DeadlinerHandle , mpsc :: Receiver < Duty > ) {
183163 // Matches Charon's `outputBuffer = 10` — big enough for all duty
184164 // types expiring simultaneously while the consumer drains synchronously.
185165 const OUTPUT_BUFFER : usize = 10 ;
@@ -204,13 +184,12 @@ impl<C: DeadlineCalculator> DeadlinerTask<C> {
204184 } ;
205185 tokio:: spawn ( task. run_task ( ) ) ;
206186
207- let link = DeadlinerHandle {
187+ let handle = DeadlinerHandle {
208188 cancel_token,
209189 input_tx,
210- output_rx : Mutex :: new ( Some ( output_rx) ) ,
211190 } ;
212191
213- Arc :: new ( link )
192+ ( handle , output_rx )
214193 }
215194
216195 /// Background task that manages duty deadlines.
@@ -407,7 +386,7 @@ mod tests {
407386 /// channel.
408387 async fn add_duties (
409388 duties : Vec < Duty > ,
410- deadliner : Arc < dyn Deadliner > ,
389+ deadliner : DeadlinerHandle ,
411390 result_tx : mpsc:: Sender < bool > ,
412391 ) {
413392 for duty in duties {
@@ -461,9 +440,8 @@ mod tests {
461440 } ;
462441
463442 let cancel_token = CancellationToken :: new ( ) ;
464- let deadliner = DeadlinerTask :: start ( cancel_token. clone ( ) , "test" , calculator) ;
465-
466- let mut output_rx = deadliner. c ( ) . context ( "output receiver already taken" ) ?;
443+ let ( deadliner, mut output_rx) =
444+ DeadlinerTask :: start ( cancel_token. clone ( ) , "test" , calculator) ;
467445
468446 let ( expired_tx, mut expired_rx) = mpsc:: channel ( 100 ) ;
469447 let ( non_expired_tx, mut non_expired_rx) = mpsc:: channel ( 100 ) ;
@@ -472,21 +450,15 @@ mod tests {
472450 let non_expired_len = non_expired_duties. len ( ) ;
473451 let future_duties_len = future_duties. len ( ) ;
474452
475- let handler_expired = tokio:: spawn ( add_duties (
476- expired_duties,
477- Arc :: clone ( & deadliner) ,
478- expired_tx,
479- ) ) ;
453+ let handler_expired =
454+ tokio:: spawn ( add_duties ( expired_duties, deadliner. clone ( ) , expired_tx) ) ;
480455 let handler_non_expired = tokio:: spawn ( add_duties (
481456 non_expired_duties. clone ( ) ,
482- Arc :: clone ( & deadliner ) ,
457+ deadliner . clone ( ) ,
483458 non_expired_tx. clone ( ) ,
484459 ) ) ;
485- let handler_future_duties = tokio:: spawn ( add_duties (
486- future_duties,
487- Arc :: clone ( & deadliner) ,
488- non_expired_tx,
489- ) ) ;
460+ let handler_future_duties =
461+ tokio:: spawn ( add_duties ( future_duties, deadliner. clone ( ) , non_expired_tx) ) ;
490462
491463 let ( result_expired, result_non_expired, result_future_duties) =
492464 tokio:: join!( handler_expired, handler_non_expired, handler_future_duties) ;
@@ -545,9 +517,8 @@ mod tests {
545517 } ;
546518
547519 let cancel_token = CancellationToken :: new ( ) ;
548- let deadliner = DeadlinerTask :: start ( cancel_token. clone ( ) , "order-test" , calculator) ;
549-
550- let mut output_rx = deadliner. c ( ) . context ( "output receiver already taken" ) ?;
520+ let ( deadliner, mut output_rx) =
521+ DeadlinerTask :: start ( cancel_token. clone ( ) , "order-test" , calculator) ;
551522
552523 // TestCalculator: deadline = start_time + slot * 500ms.
553524 // Insert the later one first to make sure ordering is by deadline,
0 commit comments