1use std::{
46 fmt::{self},
47 sync::{
48 Arc, Mutex,
49 atomic::{AtomicBool, AtomicUsize, Ordering},
50 },
51 task::{Context, Waker},
52 time::{Duration, Instant},
53};
54
55#[cfg(feature = "task-context")]
56use crate::task::context::{TaskContext, TaskStateError};
57
58use crate::{
59 error::{WorkerError, WorkerStateError},
60 monitor::shutdown::Shutdown,
61 task::from_request::FromRequest,
62 task::{ExecutionContext, Task, data::MissingDataError},
63 worker::{
64 event::{Event, EventListener, RawEventListener},
65 lifecycle::TaskLifecycleError,
66 state::{InnerWorkerState, WorkerState},
67 },
68};
69
70#[derive(Clone)]
77pub struct WorkerContext {
78 pub(crate) name: Arc<String>,
79 waker: Arc<Mutex<Option<Waker>>>,
81 state: Arc<WorkerState>,
82 pub(crate) shutdown: Option<Shutdown>,
83 event_handler: EventListener,
84 pub(super) is_ready: Arc<AtomicBool>,
85 service: Arc<String>,
86 #[cfg(feature = "task-context")]
87 tasks: Arc<dashmap::DashSet<TaskContext>>,
88
89 #[cfg(not(feature = "task-context"))]
90 tasks: Arc<AtomicUsize>,
91
92 instant: Instant,
93 restarts: Arc<AtomicUsize>,
94}
95
96impl fmt::Debug for WorkerContext {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 f.debug_struct("WorkerContext")
99 .field("shutdown", &["Shutdown handle"])
100 .field("task_count", &self.task_count())
101 .field("state", &self.state.load(Ordering::SeqCst))
102 .field("service", &self.service)
103 .field("is_ready", &self.is_ready)
104 .field("tasks", &"[..]")
105 .field("elapsed", &self.instant.elapsed())
106 .field("restarts", &self.restarts)
107 .finish()
108 }
109}
110
111impl WorkerContext {
112 #[must_use]
114 pub fn new(name: &str) -> Self {
115 Self {
116 name: Arc::new(name.to_owned()),
117 service: "Unspecified".to_owned().into(),
118 waker: Default::default(),
119 state: Default::default(),
120 shutdown: Default::default(),
121 event_handler: Arc::new(Box::new(|_, _| {
122 })),
124 is_ready: Default::default(),
125 tasks: Default::default(),
126 instant: Instant::now(),
127 restarts: Default::default(),
128 }
129 }
130
131 #[must_use]
133 pub fn name(&self) -> &str {
134 &self.name
135 }
136
137 pub fn start(&mut self) -> Result<(), WorkerError> {
139 let current_state = self.state.load(Ordering::SeqCst);
140 if current_state != InnerWorkerState::Pending {
141 return Err(WorkerError::StateError(WorkerStateError::AlreadyStarted));
142 }
143 self.state
144 .store(InnerWorkerState::Running, Ordering::SeqCst);
145 self.is_ready.store(false, Ordering::SeqCst);
146 info!("Worker {} started", self.name());
147 self.wake();
148 Ok(())
149 }
150
151 pub(crate) fn restart(&self) -> Result<(), WorkerError> {
153 self.state
154 .store(InnerWorkerState::Pending, Ordering::SeqCst);
155 self.is_ready.store(false, Ordering::SeqCst);
156 #[cfg(feature = "task-context")]
157 self.cleanup()
158 .map_err(|e| WorkerError::StateError(WorkerStateError::InvalidState(e.to_string())))?;
159 self.restarts.fetch_add(1, Ordering::SeqCst);
160 info!("Worker {} restarted", self.name());
161 self.wake();
162 Ok(())
163 }
164
165 pub fn pause(&self) -> Result<(), WorkerError> {
167 if !self.is_running() {
168 return Err(WorkerError::StateError(WorkerStateError::NotRunning));
169 }
170 self.state.store(InnerWorkerState::Paused, Ordering::SeqCst);
171 info!("Worker {} paused", self.name());
172 Ok(())
173 }
174
175 pub fn resume(&self) -> Result<(), WorkerError> {
177 if !self.is_paused() {
178 return Err(WorkerError::StateError(WorkerStateError::NotPaused));
179 }
180 if self.is_shutting_down() {
181 return Err(WorkerError::StateError(WorkerStateError::ShuttingDown));
182 }
183 self.state
184 .store(InnerWorkerState::Running, Ordering::SeqCst);
185 self.wake();
186 info!("Worker {} resumed", self.name());
187 Ok(())
188 }
189
190 pub fn stop(&self) -> Result<(), WorkerError> {
192 let current_state = self.state.load(Ordering::SeqCst);
193 if current_state == InnerWorkerState::Pending {
194 return Err(WorkerError::StateError(WorkerStateError::NotStarted));
195 }
196 self.state
197 .store(InnerWorkerState::Stopped, Ordering::SeqCst);
198 self.wake();
199 self.emit_ref(&Event::Stop);
200 info!("Worker {} stopped", self.name());
201 Ok(())
202 }
203
204 #[must_use]
206 pub fn is_ready(&self) -> bool {
207 self.is_running() && !self.is_shutting_down() && self.is_ready.load(Ordering::SeqCst)
208 }
209
210 #[must_use]
220 pub fn get_service(&self) -> &str {
221 self.service.as_ref()
222 }
223
224 pub(crate) fn bind_service<T>(&mut self) {
225 let service = std::any::type_name::<T>();
226
227 const RULES: &[(&str, &str, &str)] = &[
228 (
229 "Retry<",
230 "Trace<",
231 "`retries()` must be before `enable_tracing()`; traces produced will provide invalid attempt information",
232 ),
233 (
234 "Retry<",
235 "PrometheusService<",
236 "`retries()` must be before `prometheus()`; metrics inside retries will be invalid",
237 ),
238 (
239 "Retry<",
240 "Timeout<",
241 "`retries()` must be before `timeout()`; timeouts will be applied to the total retry process",
242 ),
243 (
244 "Timeout<",
245 "Trace<",
246 "`timeout()` should be before `enable_tracing()`; otherwise timeout failures may not be reflected correctly in traces",
247 ),
248 (
249 "Timeout<",
250 "PrometheusService<",
251 "`timeout()` should be before `prometheus()`; otherwise timeout failures may not be reflected correctly in metrics",
252 ),
253 (
254 "ConcurrencyLimit<",
255 "Retry<",
256 "`concurrency()` should generally be before `retries()`; otherwise each retry may consume a separate concurrency slot",
257 ),
258 (
259 "ConcurrencyLimit<",
260 "Timeout<",
261 "`concurrency()` should generally be before `timeout()`; otherwise queued requests may consume timeout duration",
262 ),
263 (
264 "RateLimit<",
265 "Retry<",
266 "`rate_limit()` should generally be before `retries()`; otherwise retries may consume rate-limit capacity",
267 ),
268 (
269 "LoadShed<",
270 "Retry<",
271 "`load_shed()` should generally be before `retries()`; otherwise retries may repeatedly encounter load-shed failures",
272 ),
273 (
274 "Retry<",
275 "CatchPanicService<",
276 "`catch_panic()` should generally be after `retries()` if panics are intended to participate in retry handling",
277 ),
278 ];
279
280 for &(outer, inner, message) in RULES {
281 if let (Some(a), Some(b)) = (service.find(outer), service.find(inner)) {
282 if a > b {
283 warn!("{message}");
284 }
285 }
286 }
287
288 *Arc::make_mut(&mut self.service) = service.to_owned();
289 }
290
291 #[must_use]
293 pub fn is_running(&self) -> bool {
294 self.state.load(Ordering::SeqCst) == InnerWorkerState::Running
295 }
296
297 #[must_use]
299 pub fn is_pending(&self) -> bool {
300 self.state.load(Ordering::SeqCst) == InnerWorkerState::Pending
301 }
302
303 #[must_use]
305 pub fn is_paused(&self) -> bool {
306 self.state.load(Ordering::SeqCst) == InnerWorkerState::Paused
307 }
308
309 #[must_use]
311 pub fn is_stopped(&self) -> bool {
312 self.state.load(Ordering::SeqCst) == InnerWorkerState::Stopped || self.is_terminated()
313 }
314
315 #[must_use]
318 pub fn is_terminated(&self) -> bool {
319 self.state.load(Ordering::SeqCst) == InnerWorkerState::Terminated
320 }
321
322 #[must_use]
325 pub fn task_count(&self) -> usize {
326 #[cfg(feature = "task-context")]
327 {
328 self.tasks.len()
329 }
330
331 #[cfg(not(feature = "task-context"))]
332 self.tasks.load(Ordering::SeqCst)
333 }
334
335 #[must_use]
337 pub fn has_pending_tasks(&self) -> bool {
338 self.task_count() > 0
339 }
340
341 #[must_use]
343 pub fn is_shutting_down(&self) -> bool {
344 self.is_stopped() || self.shutdown.as_ref().is_some_and(|s| s.is_shutting_down())
345 }
346
347 #[must_use]
349 pub fn state(&self) -> &str {
350 self.state.as_str()
351 }
352
353 pub(crate) fn emit_event(&self, event: &Event) {
355 self.emit_ref(event);
356 }
357
358 pub fn emit<T: Send + Sync + 'static>(&self, data: T) {
360 self.emit_ref(&Event::custom(data));
361 }
362
363 fn emit_ref(&self, event: &Event) {
364 let handler = self.event_handler.as_ref();
365 handler(self, event);
366 }
367
368 pub fn heartbeat(&self, cx: &mut Context<'_>) {
370 self.register_waker(cx);
371 self.emit_ref(&Event::HeartBeat);
372 self.is_ready.store(true, Ordering::SeqCst);
374 }
375
376 pub(crate) fn add_listener<F: Fn(&Self, &Event) + Send + Sync + 'static>(&mut self, f: F) {
378 let cur = self.event_handler.clone();
379 let new: RawEventListener = Box::new(move |ctx, ev| {
380 f(ctx, ev);
381 cur(ctx, ev);
382 });
383 self.event_handler = Arc::new(new);
384 }
385
386 pub(crate) fn register_waker(&self, cx: &Context<'_>) {
390 if let Ok(mut guard) = self.waker.lock() {
391 if guard
392 .as_ref()
393 .is_none_or(|stored| !stored.will_wake(cx.waker()))
394 {
395 *guard = Some(cx.waker().clone());
396 }
397 }
398 }
399
400 pub(crate) fn wake(&self) {
401 if let Ok(waker) = self.waker.lock() {
402 if let Some(waker) = &*waker {
403 waker.wake_by_ref();
404 }
405 }
406 }
407
408 #[cfg(feature = "task-context")]
410 pub(super) fn register_task(
411 &self,
412 ctx: &Arc<ExecutionContext>,
413 ) -> Result<TaskContext, TaskLifecycleError> {
414 let tasks = &self.tasks;
415 let task_id = ctx
416 .task_id()
417 .ok_or(TaskLifecycleError::MissingTaskId)?
418 .to_string();
419
420 if tasks.contains(task_id.as_str()) {
421 return Err(TaskLifecycleError::Duplicate);
422 }
423
424 let token = TaskContext::new(ctx);
425 tasks.insert(token.clone());
426 Ok(token)
427 }
428
429 #[cfg(not(feature = "task-context"))]
430 pub(super) fn register_task(
431 &self,
432 _: &Arc<ExecutionContext>,
433 ) -> Result<(), TaskLifecycleError> {
434 self.tasks.fetch_add(1, Ordering::SeqCst);
435 Ok(())
436 }
437
438 #[cfg(feature = "task-context")]
440 pub fn cancel_task(&self, context: &TaskContext) -> Result<(), TaskStateError> {
441 if let Some(token) = &self.tasks.get(context) {
442 token.cancel()
443 } else {
444 Err(TaskStateError::TaskNotFound)
445 }
446 }
447
448 #[cfg(feature = "task-context")]
450 pub(super) fn remove_task(&self, ctx: &TaskContext) -> Result<(), TaskStateError> {
451 let task_id = ctx.task_id();
452 self.tasks
453 .remove(task_id)
454 .ok_or(TaskStateError::TaskNotFound)?;
455 Ok(())
456 }
457
458 #[cfg(not(feature = "task-context"))]
460 pub(super) fn remove_task(&self, _: &()) -> Result<(), WorkerError> {
461 self.tasks.fetch_sub(1, Ordering::SeqCst);
462 Ok(())
463 }
464
465 #[cfg(feature = "task-context")]
467 pub(crate) fn get_task_context(
468 &self,
469 ctx: &Arc<ExecutionContext>,
470 ) -> Result<TaskContext, TaskStateError> {
471 self.get_task(ctx.task_id().unwrap().to_string().as_str())
472 }
473
474 #[cfg(feature = "task-context")]
476 pub fn get_task(&self, task_id: &str) -> Result<TaskContext, TaskStateError> {
477 Ok(self
478 .tasks
479 .get(task_id)
480 .ok_or(TaskStateError::TaskNotFound)?
481 .clone())
482 }
483
484 #[cfg(feature = "task-context")]
486 pub fn cleanup(&self) -> Result<(), TaskStateError> {
487 let tasks = &self.tasks;
488 tasks.retain(|token| !(token.is_completed() && token.is_empty()));
489 Ok(())
490 }
491
492 #[cfg(feature = "task-context")]
494 pub fn tasks(&self) -> Result<Vec<TaskContext>, TaskStateError> {
495 let tasks = &self.tasks;
496 Ok(tasks.iter().map(|s| s.clone()).collect())
497 }
498
499 #[must_use]
501 pub fn elapsed(&self) -> Duration {
502 self.instant.elapsed()
503 }
504
505 #[must_use]
511 pub fn restarts(&self) -> usize {
512 self.restarts.load(Ordering::SeqCst)
513 }
514
515 pub fn kill(&mut self) -> Result<(), WorkerError> {
517 if !self.is_shutting_down() {
518 return Err(WorkerError::StateError(WorkerStateError::InvalidState(
519 "Worker is not shutting down".to_owned(),
520 )));
521 }
522 #[cfg(feature = "task-context")]
523 if self.task_count() != 0 {
524 self.tasks()
525 .map_err(|e| {
526 WorkerError::StateError(WorkerStateError::InvalidState(e.to_string()))
527 })?
528 .into_iter()
529 .map(|a| a.cancel())
530 .collect::<Result<Vec<_>, _>>()
531 .map_err(|e| {
532 WorkerError::StateError(WorkerStateError::InvalidState(e.to_string()))
533 })?;
534 }
535 self.state
536 .store(InnerWorkerState::Terminated, Ordering::SeqCst);
537 self.wake();
538 Ok(())
539 }
540}
541
542impl From<&str> for WorkerContext {
543 fn from(name: &str) -> Self {
544 Self::new(name)
545 }
546}
547
548impl From<String> for WorkerContext {
549 fn from(name: String) -> Self {
550 Self::new(&name)
551 }
552}
553
554impl From<&Self> for WorkerContext {
555 fn from(context: &Self) -> Self {
556 context.clone()
557 }
558}
559
560impl<Args: Sync> FromRequest<Task<Args>> for WorkerContext {
561 type Error = MissingDataError;
562 async fn from_request(task: &Task<Args>) -> Result<Self, Self::Error> {
563 task.data().get_checked().cloned()
564 }
565}
566
567impl Drop for WorkerContext {
568 fn drop(&mut self) {
569 if Arc::strong_count(&self.state) > 1 {
570 return;
572 }
573 if self.is_running() && self.has_pending_tasks() {
574 error!(
575 "Worker '{}' is being dropped while running with `{}` tasks. Consider calling stop() before dropping.",
576 self.name(),
577 self.task_count()
578 );
579 }
580 }
581}
582
583#[cfg(test)]
584mod tests {
585
586 use futures_util::FutureExt;
587
588 use crate::{
589 backend::memory::MemoryStorage, error::BoxDynError, worker::builder::WorkerBuilder,
590 };
591 use std::time::Duration;
592
593 use super::*;
594
595 #[tokio::test]
596 async fn test_worker_state_transitions() {
597 let backend = MemoryStorage::<u32>::new();
598
599 let ctx = WorkerContext::new("test-worker");
600
601 let worker = WorkerBuilder::new(&ctx)
602 .backend(backend)
603 .build(|_task: u32| async { Ok::<_, BoxDynError>(()) });
604
605 let worker_handle = tokio::spawn(async move { worker.run().boxed().await });
606 tokio::time::sleep(Duration::from_millis(50)).await;
607
608 assert!(ctx.is_running());
610 assert!(!ctx.is_shutting_down());
611 assert!(!ctx.is_stopped());
612
613 ctx.pause().unwrap();
615 assert!(ctx.is_paused());
616 assert!(
617 !ctx.is_shutting_down(),
618 "Paused worker should NOT be considered shutting down"
619 );
620
621 ctx.resume().unwrap();
623 assert!(ctx.is_running());
624 assert!(!ctx.is_paused());
625
626 ctx.stop().unwrap();
628 assert!(ctx.is_stopped());
629 assert!(ctx.is_shutting_down());
630
631 assert!(
633 matches!(
634 ctx.resume(),
635 Err(WorkerError::StateError(WorkerStateError::NotPaused))
636 ),
637 "Resuming a stopped worker should fail with NotPaused error"
638 );
639
640 worker_handle.await.unwrap().unwrap();
641 }
642}