hammerwork/job.rs
1//! Job types and utilities for representing work units in the job queue.
2//!
3//! This module provides the core [`Job`] struct and [`JobStatus`] enum that represent
4//! individual units of work to be processed by workers. Jobs contain all the metadata
5//! needed for scheduling, prioritization, retry logic, and lifecycle management.
6
7use crate::cron::CronSchedule;
8use crate::priority::JobPriority;
9use crate::retry::RetryStrategy;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[cfg(any(feature = "postgres", feature = "mysql"))]
15use sqlx::{Decode, Encode, Type};
16
17#[cfg(feature = "postgres")]
18use sqlx::Postgres;
19
20#[cfg(feature = "mysql")]
21use sqlx::MySql;
22
23/// Unique identifier for a job.
24///
25/// Each job gets a unique UUID when created to enable tracking and management
26/// throughout its lifecycle.
27pub type JobId = Uuid;
28
29/// The current status of a job in its lifecycle.
30///
31/// Jobs progress through various states from creation to completion or failure.
32/// This enum tracks the current state to enable proper job management and statistics.
33///
34/// # Examples
35///
36/// ```rust
37/// use hammerwork::JobStatus;
38///
39/// // Check if a job is in a final state
40/// let status = JobStatus::Completed;
41/// let is_final = matches!(status, JobStatus::Completed | JobStatus::Dead | JobStatus::TimedOut | JobStatus::Archived);
42/// assert!(is_final);
43/// ```
44#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
45pub enum JobStatus {
46 /// Job is waiting to be processed by a worker.
47 Pending,
48 /// Job is currently being processed by a worker.
49 Running,
50 /// Job completed successfully.
51 Completed,
52 /// Job failed but may be retried if it hasn't exhausted retry attempts.
53 Failed,
54 /// Job failed permanently after exhausting all retry attempts.
55 Dead,
56 /// Job was terminated due to exceeding its timeout duration.
57 TimedOut,
58 /// Job failed but is scheduled for retry.
59 Retrying,
60 /// Job has been archived for long-term storage.
61 Archived,
62}
63
64impl JobStatus {
65 /// Returns the string representation of the job status.
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 JobStatus::Pending => "Pending",
69 JobStatus::Running => "Running",
70 JobStatus::Completed => "Completed",
71 JobStatus::Failed => "Failed",
72 JobStatus::Dead => "Dead",
73 JobStatus::TimedOut => "TimedOut",
74 JobStatus::Retrying => "Retrying",
75 JobStatus::Archived => "Archived",
76 }
77 }
78}
79
80// SQLx implementations for JobStatus to handle database encoding/decoding
81
82#[cfg(feature = "postgres")]
83impl Type<Postgres> for JobStatus {
84 fn type_info() -> sqlx::postgres::PgTypeInfo {
85 <String as Type<Postgres>>::type_info()
86 }
87
88 // Accept every column type `String` decodes from (TEXT, VARCHAR, ...); the status
89 // columns are VARCHAR, which the default `compatible` (== TEXT) rejects.
90 fn compatible(ty: &sqlx::postgres::PgTypeInfo) -> bool {
91 <String as Type<Postgres>>::compatible(ty)
92 }
93}
94
95#[cfg(feature = "postgres")]
96impl Encode<'_, Postgres> for JobStatus {
97 fn encode_by_ref(
98 &self,
99 buf: &mut sqlx::postgres::PgArgumentBuffer,
100 ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync + 'static>> {
101 let status_str = match self {
102 JobStatus::Pending => "Pending",
103 JobStatus::Running => "Running",
104 JobStatus::Completed => "Completed",
105 JobStatus::Failed => "Failed",
106 JobStatus::Dead => "Dead",
107 JobStatus::TimedOut => "TimedOut",
108 JobStatus::Retrying => "Retrying",
109 JobStatus::Archived => "Archived",
110 };
111 <&str as Encode<'_, Postgres>>::encode_by_ref(&status_str, buf)
112 }
113}
114
115#[cfg(feature = "postgres")]
116impl Decode<'_, Postgres> for JobStatus {
117 fn decode(value: sqlx::postgres::PgValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
118 let status_str = <String as Decode<Postgres>>::decode(value)?;
119 // Handle both quoted (old format) and unquoted (new format) status values
120 let cleaned_str = status_str.trim_matches('"');
121 match cleaned_str {
122 "Pending" => Ok(JobStatus::Pending),
123 "Running" => Ok(JobStatus::Running),
124 "Completed" => Ok(JobStatus::Completed),
125 "Failed" => Ok(JobStatus::Failed),
126 "Dead" => Ok(JobStatus::Dead),
127 "TimedOut" => Ok(JobStatus::TimedOut),
128 "Retrying" => Ok(JobStatus::Retrying),
129 "Archived" => Ok(JobStatus::Archived),
130 _ => Err(format!("Unknown job status: {}", status_str).into()),
131 }
132 }
133}
134
135#[cfg(feature = "mysql")]
136impl Type<MySql> for JobStatus {
137 fn type_info() -> sqlx::mysql::MySqlTypeInfo {
138 <String as Type<MySql>>::type_info()
139 }
140
141 // Accept every column type `String` decodes from (TEXT, VARCHAR, ...); the status
142 // columns are VARCHAR, which the default `compatible` (== TEXT) rejects.
143 fn compatible(ty: &sqlx::mysql::MySqlTypeInfo) -> bool {
144 <String as Type<MySql>>::compatible(ty)
145 }
146}
147
148#[cfg(feature = "mysql")]
149impl Encode<'_, MySql> for JobStatus {
150 fn encode_by_ref(
151 &self,
152 buf: &mut Vec<u8>,
153 ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync + 'static>> {
154 let status_str = match self {
155 JobStatus::Pending => "Pending",
156 JobStatus::Running => "Running",
157 JobStatus::Completed => "Completed",
158 JobStatus::Failed => "Failed",
159 JobStatus::Dead => "Dead",
160 JobStatus::TimedOut => "TimedOut",
161 JobStatus::Retrying => "Retrying",
162 JobStatus::Archived => "Archived",
163 };
164 <&str as Encode<'_, MySql>>::encode_by_ref(&status_str, buf)
165 }
166}
167
168#[cfg(feature = "mysql")]
169impl Decode<'_, MySql> for JobStatus {
170 fn decode(value: sqlx::mysql::MySqlValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
171 let status_str = <String as Decode<MySql>>::decode(value)?;
172 // Handle both quoted (old format) and unquoted (new format) status values
173 let cleaned_str = status_str.trim_matches('"');
174 match cleaned_str {
175 "Pending" => Ok(JobStatus::Pending),
176 "Running" => Ok(JobStatus::Running),
177 "Completed" => Ok(JobStatus::Completed),
178 "Failed" => Ok(JobStatus::Failed),
179 "Dead" => Ok(JobStatus::Dead),
180 "TimedOut" => Ok(JobStatus::TimedOut),
181 "Retrying" => Ok(JobStatus::Retrying),
182 "Archived" => Ok(JobStatus::Archived),
183 _ => Err(format!("Unknown job status: {}", status_str).into()),
184 }
185 }
186}
187
188/// Configuration for job result storage.
189///
190/// This enum determines where and how job results are stored when jobs complete successfully.
191/// Different storage backends offer different trade-offs between performance, persistence,
192/// and resource usage.
193///
194/// # Examples
195///
196/// ```rust
197/// use hammerwork::job::ResultStorage;
198///
199/// // Store results in the database
200/// let db_storage = ResultStorage::Database;
201///
202/// // Store results in memory (faster but not persistent)
203/// let memory_storage = ResultStorage::Memory;
204///
205/// // Don't store results (default behavior)
206/// let no_storage = ResultStorage::None;
207/// ```
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
209pub enum ResultStorage {
210 /// Store results in the database (persistent across restarts).
211 Database,
212 /// Store results in memory (faster access but lost on restart).
213 Memory,
214 /// Don't store job results (default behavior).
215 #[default]
216 None,
217}
218
219/// Configuration for job result storage and management.
220///
221/// This struct contains settings that control how job results are stored,
222/// how long they're retained, and when they should be cleaned up.
223///
224/// # Examples
225///
226/// ```rust
227/// use hammerwork::job::{ResultConfig, ResultStorage};
228/// use std::time::Duration;
229///
230/// // Store results in database for 7 days
231/// let config = ResultConfig::new(ResultStorage::Database)
232/// .with_ttl(Duration::from_secs(7 * 24 * 60 * 60));
233///
234/// // Store results in memory for 1 hour
235/// let config = ResultConfig::new(ResultStorage::Memory)
236/// .with_ttl(Duration::from_secs(3600));
237/// ```
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct ResultConfig {
240 /// Where to store the job results.
241 pub storage: ResultStorage,
242 /// How long to keep results before expiring them.
243 pub ttl: Option<std::time::Duration>,
244 /// Maximum size of result data in bytes (for validation).
245 pub max_size_bytes: Option<usize>,
246}
247
248impl ResultConfig {
249 /// Creates a new result configuration with the specified storage backend.
250 ///
251 /// # Arguments
252 ///
253 /// * `storage` - The storage backend to use for results
254 ///
255 /// # Examples
256 ///
257 /// ```rust
258 /// use hammerwork::job::{ResultConfig, ResultStorage};
259 ///
260 /// let config = ResultConfig::new(ResultStorage::Database);
261 /// assert_eq!(config.storage, ResultStorage::Database);
262 /// assert!(config.ttl.is_none());
263 /// ```
264 pub fn new(storage: ResultStorage) -> Self {
265 Self {
266 storage,
267 ttl: None,
268 max_size_bytes: None,
269 }
270 }
271
272 /// Sets the time-to-live (TTL) for stored results.
273 ///
274 /// After this duration elapses, the result will be eligible for cleanup.
275 ///
276 /// # Arguments
277 ///
278 /// * `ttl` - How long to keep results before they expire
279 ///
280 /// # Examples
281 ///
282 /// ```rust
283 /// use hammerwork::job::{ResultConfig, ResultStorage};
284 /// use std::time::Duration;
285 ///
286 /// let config = ResultConfig::new(ResultStorage::Database)
287 /// .with_ttl(Duration::from_secs(3600)); // 1 hour
288 /// ```
289 pub fn with_ttl(mut self, ttl: std::time::Duration) -> Self {
290 self.ttl = Some(ttl);
291 self
292 }
293
294 /// Sets the maximum size for result data.
295 ///
296 /// This is used to validate result data before storage to prevent
297 /// extremely large results from impacting system performance.
298 ///
299 /// # Arguments
300 ///
301 /// * `max_bytes` - Maximum allowed size for result data
302 ///
303 /// # Examples
304 ///
305 /// ```rust
306 /// use hammerwork::job::{ResultConfig, ResultStorage};
307 ///
308 /// let config = ResultConfig::new(ResultStorage::Database)
309 /// .with_max_size(1024 * 1024); // 1MB limit
310 /// ```
311 pub fn with_max_size(mut self, max_bytes: usize) -> Self {
312 self.max_size_bytes = Some(max_bytes);
313 self
314 }
315}
316
317impl Default for ResultConfig {
318 fn default() -> Self {
319 Self::new(ResultStorage::None)
320 }
321}
322
323/// A unit of work to be processed by the job queue.
324///
325/// Jobs are the fundamental building blocks of the Hammerwork system. Each job contains:
326/// - A unique identifier for tracking
327/// - Queue name for routing to appropriate workers
328/// - JSON payload containing the work data
329/// - Scheduling and retry configuration
330/// - Priority level for queue ordering
331/// - Optional cron schedule for recurring jobs
332/// - Timeout configuration for automatic termination
333///
334/// # Examples
335///
336/// ## Basic Job Creation
337///
338/// ```rust
339/// use hammerwork::Job;
340/// use serde_json::json;
341///
342/// let job = Job::new("email_queue".to_string(), json!({
343/// "to": "user@example.com",
344/// "subject": "Welcome!",
345/// "body": "Thanks for signing up"
346/// }));
347///
348/// assert_eq!(job.queue_name, "email_queue");
349/// assert_eq!(job.max_attempts, 3); // Default retry attempts
350/// ```
351///
352/// ## Job with Priority and Timeout
353///
354/// ```rust
355/// use hammerwork::{Job, JobPriority};
356/// use serde_json::json;
357/// use std::time::Duration;
358///
359/// let job = Job::new("processing".to_string(), json!({"data": "important"}))
360/// .as_high_priority()
361/// .with_timeout(Duration::from_secs(300))
362/// .with_max_attempts(5);
363///
364/// assert_eq!(job.priority, JobPriority::High);
365/// assert_eq!(job.timeout, Some(Duration::from_secs(300)));
366/// assert_eq!(job.max_attempts, 5);
367/// ```
368///
369/// ## Delayed Job
370///
371/// ```rust
372/// use hammerwork::Job;
373/// use serde_json::json;
374/// use chrono::Duration;
375///
376/// let job = Job::with_delay(
377/// "notifications".to_string(),
378/// json!({"message": "Reminder"}),
379/// Duration::hours(1)
380/// );
381///
382/// // Job will be scheduled to run 1 hour from now
383/// assert!(job.scheduled_at > job.created_at);
384/// ```
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct Job {
387 /// Unique identifier for this job.
388 pub id: JobId,
389 /// Name of the queue this job belongs to.
390 pub queue_name: String,
391 /// JSON payload containing the work data.
392 pub payload: serde_json::Value,
393 /// Current status of the job.
394 pub status: JobStatus,
395 /// Number of times this job has been attempted.
396 pub attempts: i32,
397 /// Maximum number of attempts before marking the job as dead.
398 pub max_attempts: i32,
399 /// When the job was created.
400 pub created_at: DateTime<Utc>,
401 /// When the job should be processed (may be in the future for delayed jobs).
402 pub scheduled_at: DateTime<Utc>,
403 /// When the job started processing (if it has started).
404 pub started_at: Option<DateTime<Utc>>,
405 /// When the job completed successfully (if it completed).
406 pub completed_at: Option<DateTime<Utc>>,
407 /// When the job failed permanently (if it failed).
408 pub failed_at: Option<DateTime<Utc>>,
409 /// When the job timed out (if it timed out).
410 pub timed_out_at: Option<DateTime<Utc>>,
411 /// Maximum duration the job is allowed to run before timing out.
412 pub timeout: Option<std::time::Duration>,
413 /// Error message if the job failed.
414 pub error_message: Option<String>,
415 /// Priority level for queue ordering.
416 pub priority: JobPriority,
417 /// Cron expression for recurring jobs.
418 pub cron_schedule: Option<String>,
419 /// Next scheduled execution time for recurring jobs.
420 pub next_run_at: Option<DateTime<Utc>>,
421 /// Whether this is a recurring job.
422 pub recurring: bool,
423 /// Timezone for cron calculations.
424 pub timezone: Option<String>,
425 /// Batch ID if this job is part of a batch operation.
426 pub batch_id: Option<crate::batch::BatchId>,
427 /// Configuration for how job results should be stored.
428 pub result_config: ResultConfig,
429 /// The actual result data from job execution (if stored).
430 pub result_data: Option<serde_json::Value>,
431 /// When the result was stored (if applicable).
432 pub result_stored_at: Option<DateTime<Utc>>,
433 /// When the stored result will expire (if applicable).
434 pub result_expires_at: Option<DateTime<Utc>>,
435 /// Retry strategy for this job (overrides worker default if specified).
436 pub retry_strategy: Option<RetryStrategy>,
437 /// Job IDs this job depends on (must complete before this job can run).
438 pub depends_on: Vec<JobId>,
439 /// Job IDs that depend on this job (cached for performance).
440 pub dependents: Vec<JobId>,
441 /// Status of dependency resolution for this job.
442 pub dependency_status: crate::workflow::DependencyStatus,
443 /// ID of the workflow this job belongs to (if any).
444 pub workflow_id: Option<crate::workflow::WorkflowId>,
445 /// Name of the workflow this job belongs to (if any).
446 pub workflow_name: Option<String>,
447 /// Distributed trace identifier for cross-service tracing.
448 pub trace_id: Option<String>,
449 /// Business correlation identifier for grouping related operations.
450 pub correlation_id: Option<String>,
451 /// Parent span identifier for hierarchical tracing.
452 pub parent_span_id: Option<String>,
453 /// Serialized span context for trace propagation.
454 pub span_context: Option<String>,
455 /// Encryption configuration for this job's payload (if encrypted).
456 #[cfg(feature = "encryption")]
457 pub encryption_config: Option<crate::encryption::EncryptionConfig>,
458 /// List of field names that contain PII and should be encrypted.
459 pub pii_fields: Vec<String>,
460 /// Retention policy for encrypted data (overrides default if specified).
461 #[cfg(feature = "encryption")]
462 pub retention_policy: Option<crate::encryption::RetentionPolicy>,
463 /// Whether the payload is currently encrypted.
464 pub is_encrypted: bool,
465 /// Encrypted payload data (if job is encrypted).
466 #[cfg(feature = "encryption")]
467 pub encrypted_payload: Option<crate::encryption::EncryptedPayload>,
468}
469
470impl Job {
471 /// Creates a new job with default settings.
472 ///
473 /// The job will be created with:
474 /// - A unique UUID identifier
475 /// - Normal priority level
476 /// - 3 maximum retry attempts
477 /// - Scheduled to run immediately
478 /// - Pending status
479 ///
480 /// # Arguments
481 ///
482 /// * `queue_name` - The name of the queue this job should be processed by
483 /// * `payload` - JSON data containing the work to be performed
484 ///
485 /// # Examples
486 ///
487 /// ```rust
488 /// use hammerwork::{Job, JobStatus, JobPriority};
489 /// use serde_json::json;
490 ///
491 /// let job = Job::new("email_queue".to_string(), json!({
492 /// "to": "user@example.com",
493 /// "subject": "Welcome!",
494 /// "template": "welcome"
495 /// }));
496 ///
497 /// assert_eq!(job.queue_name, "email_queue");
498 /// assert_eq!(job.status, JobStatus::Pending);
499 /// assert_eq!(job.priority, JobPriority::Normal);
500 /// assert_eq!(job.max_attempts, 3);
501 /// assert_eq!(job.attempts, 0);
502 /// assert!(!job.is_recurring());
503 /// ```
504 pub fn new(queue_name: String, payload: serde_json::Value) -> Self {
505 let now = Utc::now();
506 Self {
507 id: Uuid::new_v4(),
508 queue_name,
509 payload,
510 status: JobStatus::Pending,
511 attempts: 0,
512 max_attempts: 3,
513 created_at: now,
514 scheduled_at: now,
515 started_at: None,
516 completed_at: None,
517 failed_at: None,
518 timed_out_at: None,
519 timeout: None,
520 error_message: None,
521 priority: JobPriority::default(),
522 cron_schedule: None,
523 next_run_at: None,
524 recurring: false,
525 timezone: None,
526 batch_id: None,
527 result_config: ResultConfig::default(),
528 result_data: None,
529 result_stored_at: None,
530 result_expires_at: None,
531 retry_strategy: None,
532 depends_on: Vec::new(),
533 dependents: Vec::new(),
534 dependency_status: crate::workflow::DependencyStatus::None,
535 workflow_id: None,
536 workflow_name: None,
537 trace_id: None,
538 correlation_id: None,
539 parent_span_id: None,
540 span_context: None,
541 #[cfg(feature = "encryption")]
542 encryption_config: None,
543 pii_fields: Vec::new(),
544 #[cfg(feature = "encryption")]
545 retention_policy: None,
546 is_encrypted: false,
547 #[cfg(feature = "encryption")]
548 encrypted_payload: None,
549 }
550 }
551
552 /// Creates a new job scheduled to run after a delay.
553 ///
554 /// This is useful for implementing delayed notifications, retries with backoff,
555 /// or any work that should be performed at a specific time in the future.
556 ///
557 /// # Arguments
558 ///
559 /// * `queue_name` - The name of the queue this job should be processed by
560 /// * `payload` - JSON data containing the work to be performed
561 /// * `delay` - How long to wait before the job becomes eligible for processing
562 ///
563 /// # Examples
564 ///
565 /// ```rust
566 /// use hammerwork::Job;
567 /// use serde_json::json;
568 /// use chrono::Duration;
569 ///
570 /// // Send a reminder email in 24 hours
571 /// let job = Job::with_delay(
572 /// "email_queue".to_string(),
573 /// json!({
574 /// "to": "user@example.com",
575 /// "subject": "Don't forget to complete your profile",
576 /// "template": "reminder"
577 /// }),
578 /// Duration::hours(24)
579 /// );
580 ///
581 /// // Job will be scheduled 24 hours from now
582 /// assert!(job.scheduled_at > job.created_at);
583 /// let delay_diff = job.scheduled_at - job.created_at;
584 /// assert_eq!(delay_diff, Duration::hours(24));
585 /// ```
586 pub fn with_delay(
587 queue_name: String,
588 payload: serde_json::Value,
589 delay: chrono::Duration,
590 ) -> Self {
591 let now = Utc::now();
592 Self {
593 id: Uuid::new_v4(),
594 queue_name,
595 payload,
596 status: JobStatus::Pending,
597 attempts: 0,
598 max_attempts: 3,
599 created_at: now,
600 scheduled_at: now + delay,
601 started_at: None,
602 completed_at: None,
603 failed_at: None,
604 timed_out_at: None,
605 timeout: None,
606 error_message: None,
607 priority: JobPriority::default(),
608 cron_schedule: None,
609 next_run_at: None,
610 recurring: false,
611 timezone: None,
612 batch_id: None,
613 result_config: ResultConfig::default(),
614 result_data: None,
615 result_stored_at: None,
616 result_expires_at: None,
617 retry_strategy: None,
618 depends_on: Vec::new(),
619 dependents: Vec::new(),
620 dependency_status: crate::workflow::DependencyStatus::None,
621 workflow_id: None,
622 workflow_name: None,
623 trace_id: None,
624 correlation_id: None,
625 parent_span_id: None,
626 span_context: None,
627 #[cfg(feature = "encryption")]
628 encryption_config: None,
629 pii_fields: Vec::new(),
630 #[cfg(feature = "encryption")]
631 retention_policy: None,
632 is_encrypted: false,
633 #[cfg(feature = "encryption")]
634 encrypted_payload: None,
635 }
636 }
637
638 /// Sets the maximum number of retry attempts for this job.
639 ///
640 /// When a job fails, it will be retried up to this many times before being
641 /// marked as dead. The default is 3 attempts.
642 ///
643 /// # Arguments
644 ///
645 /// * `max_attempts` - Maximum number of attempts (including the initial attempt)
646 ///
647 /// # Examples
648 ///
649 /// ```rust
650 /// use hammerwork::Job;
651 /// use serde_json::json;
652 ///
653 /// // Critical job that should be retried many times
654 /// let job = Job::new("critical_task".to_string(), json!({"task": "important"}))
655 /// .with_max_attempts(10);
656 ///
657 /// assert_eq!(job.max_attempts, 10);
658 /// ```
659 pub fn with_max_attempts(mut self, max_attempts: i32) -> Self {
660 self.max_attempts = max_attempts;
661 self
662 }
663
664 /// Sets a timeout duration for this job.
665 ///
666 /// If the job takes longer than this duration to complete, it will be
667 /// automatically terminated and marked as timed out. Job-level timeouts
668 /// take precedence over worker-level default timeouts.
669 ///
670 /// The PostgreSQL and MySQL backends store the timeout in whole seconds, rounding a
671 /// fractional timeout up (`200ms` is stored as one second). `Duration::ZERO` means no
672 /// job-level timeout, so the worker's default applies.
673 ///
674 /// # Arguments
675 ///
676 /// * `timeout` - Maximum duration the job is allowed to run
677 ///
678 /// # Examples
679 ///
680 /// ```rust
681 /// use hammerwork::Job;
682 /// use serde_json::json;
683 /// use std::time::Duration;
684 ///
685 /// // API call that should timeout after 30 seconds
686 /// let job = Job::new("api_call".to_string(), json!({"url": "https://api.example.com"}))
687 /// .with_timeout(Duration::from_secs(30));
688 ///
689 /// assert_eq!(job.timeout, Some(Duration::from_secs(30)));
690 /// ```
691 pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
692 self.timeout = Some(timeout);
693 self
694 }
695
696 /// Sets the priority level for this job.
697 ///
698 /// Priority affects the order in which jobs are processed by workers.
699 /// Higher priority jobs are generally processed before lower priority ones.
700 ///
701 /// # Arguments
702 ///
703 /// * `priority` - The priority level to assign to this job
704 ///
705 /// # Examples
706 ///
707 /// ```rust
708 /// use hammerwork::{Job, JobPriority};
709 /// use serde_json::json;
710 ///
711 /// let job = Job::new("task".to_string(), json!({"data": "test"}))
712 /// .with_priority(JobPriority::High);
713 ///
714 /// assert_eq!(job.priority, JobPriority::High);
715 /// assert!(job.is_high_priority());
716 /// ```
717 pub fn with_priority(mut self, priority: JobPriority) -> Self {
718 self.priority = priority;
719 self
720 }
721
722 /// Sets the job as critical priority (highest priority).
723 ///
724 /// Critical jobs are processed with the highest priority and should be used
725 /// sparingly for truly urgent work like system alerts or emergency responses.
726 ///
727 /// # Examples
728 ///
729 /// ```rust
730 /// use hammerwork::{Job, JobPriority};
731 /// use serde_json::json;
732 ///
733 /// let job = Job::new("system_alert".to_string(), json!({"alert": "system_down"}))
734 /// .as_critical();
735 ///
736 /// assert_eq!(job.priority, JobPriority::Critical);
737 /// assert!(job.is_critical());
738 /// ```
739 pub fn as_critical(mut self) -> Self {
740 self.priority = JobPriority::Critical;
741 self
742 }
743
744 /// Sets the job as high priority.
745 ///
746 /// High priority jobs are processed before normal priority jobs but after
747 /// critical jobs. Suitable for user-facing operations or important business logic.
748 ///
749 /// # Examples
750 ///
751 /// ```rust
752 /// use hammerwork::{Job, JobPriority};
753 /// use serde_json::json;
754 ///
755 /// let job = Job::new("user_notification".to_string(), json!({"user_id": 123}))
756 /// .as_high_priority();
757 ///
758 /// assert_eq!(job.priority, JobPriority::High);
759 /// assert!(job.is_high_priority());
760 /// ```
761 pub fn as_high_priority(mut self) -> Self {
762 self.priority = JobPriority::High;
763 self
764 }
765
766 /// Sets the job as low priority.
767 ///
768 /// Low priority jobs are processed after normal priority jobs but before
769 /// background jobs. Suitable for analytics, reporting, or non-urgent tasks.
770 ///
771 /// # Examples
772 ///
773 /// ```rust
774 /// use hammerwork::{Job, JobPriority};
775 /// use serde_json::json;
776 ///
777 /// let job = Job::new("analytics".to_string(), json!({"event": "page_view"}))
778 /// .as_low_priority();
779 ///
780 /// assert_eq!(job.priority, JobPriority::Low);
781 /// assert!(job.is_low_priority());
782 /// ```
783 pub fn as_low_priority(mut self) -> Self {
784 self.priority = JobPriority::Low;
785 self
786 }
787
788 /// Sets the job as background priority (lowest priority).
789 ///
790 /// Background jobs are processed only when no higher priority jobs are available.
791 /// Suitable for cleanup tasks, maintenance, or work that can wait indefinitely.
792 ///
793 /// # Examples
794 ///
795 /// ```rust
796 /// use hammerwork::{Job, JobPriority};
797 /// use serde_json::json;
798 ///
799 /// let job = Job::new("cleanup".to_string(), json!({"type": "temp_files"}))
800 /// .as_background();
801 ///
802 /// assert_eq!(job.priority, JobPriority::Background);
803 /// assert!(job.is_background());
804 /// ```
805 pub fn as_background(mut self) -> Self {
806 self.priority = JobPriority::Background;
807 self
808 }
809
810 /// Sets a custom retry strategy for this job.
811 ///
812 /// The retry strategy determines how long to wait between retry attempts
813 /// when the job fails. This overrides any default retry strategy configured
814 /// on the worker.
815 ///
816 /// # Arguments
817 ///
818 /// * `strategy` - The retry strategy to use for this job
819 ///
820 /// # Examples
821 ///
822 /// ```rust
823 /// use hammerwork::{Job, retry::RetryStrategy};
824 /// use serde_json::json;
825 /// use std::time::Duration;
826 ///
827 /// // Use exponential backoff for API calls
828 /// let job = Job::new("api_call".to_string(), json!({"url": "https://api.example.com"}))
829 /// .with_retry_strategy(RetryStrategy::exponential(
830 /// Duration::from_secs(1),
831 /// 2.0,
832 /// Some(Duration::from_secs(10 * 60))
833 /// ));
834 /// ```
835 pub fn with_retry_strategy(mut self, strategy: RetryStrategy) -> Self {
836 self.retry_strategy = Some(strategy);
837 self
838 }
839
840 /// Sets exponential backoff retry strategy for this job.
841 ///
842 /// This is a convenience method for the most common retry pattern.
843 /// Each retry attempt waits exponentially longer than the previous one.
844 ///
845 /// # Arguments
846 ///
847 /// * `base` - Base delay for the first retry attempt
848 /// * `multiplier` - Exponential growth multiplier (typically 2.0)
849 /// * `max_delay` - Maximum delay to cap exponential growth
850 ///
851 /// # Examples
852 ///
853 /// ```rust
854 /// use hammerwork::Job;
855 /// use serde_json::json;
856 /// use std::time::Duration;
857 ///
858 /// // Exponential backoff: 1s, 2s, 4s, 8s, 16s... (capped at 10 minutes)
859 /// let job = Job::new("network_request".to_string(), json!({"url": "https://example.com"}))
860 /// .with_exponential_backoff(
861 /// Duration::from_secs(1),
862 /// 2.0,
863 /// Duration::from_secs(10 * 60)
864 /// );
865 /// ```
866 pub fn with_exponential_backoff(
867 mut self,
868 base: std::time::Duration,
869 multiplier: f64,
870 max_delay: std::time::Duration,
871 ) -> Self {
872 self.retry_strategy = Some(RetryStrategy::exponential(
873 base,
874 multiplier,
875 Some(max_delay),
876 ));
877 self
878 }
879
880 /// Sets linear backoff retry strategy for this job.
881 ///
882 /// Each retry attempt waits longer than the previous by a fixed increment.
883 ///
884 /// # Arguments
885 ///
886 /// * `base` - Base delay for the first retry attempt
887 /// * `increment` - Amount to add for each subsequent attempt
888 /// * `max_delay` - Optional maximum delay to cap growth
889 ///
890 /// # Examples
891 ///
892 /// ```rust
893 /// use hammerwork::Job;
894 /// use serde_json::json;
895 /// use std::time::Duration;
896 ///
897 /// // Linear backoff: 10s, 20s, 30s, 40s... (capped at 2 minutes)
898 /// let job = Job::new("database_operation".to_string(), json!({"query": "SELECT ..."}))
899 /// .with_linear_backoff(
900 /// Duration::from_secs(10),
901 /// Duration::from_secs(10),
902 /// Some(Duration::from_secs(2 * 60))
903 /// );
904 /// ```
905 pub fn with_linear_backoff(
906 mut self,
907 base: std::time::Duration,
908 increment: std::time::Duration,
909 max_delay: Option<std::time::Duration>,
910 ) -> Self {
911 self.retry_strategy = Some(RetryStrategy::linear(base, increment, max_delay));
912 self
913 }
914
915 /// Sets Fibonacci sequence backoff retry strategy for this job.
916 ///
917 /// Each retry waits according to the Fibonacci sequence multiplied by the base delay.
918 ///
919 /// # Arguments
920 ///
921 /// * `base` - Base delay multiplied by Fibonacci numbers
922 /// * `max_delay` - Optional maximum delay to cap growth
923 ///
924 /// # Examples
925 ///
926 /// ```rust
927 /// use hammerwork::Job;
928 /// use serde_json::json;
929 /// use std::time::Duration;
930 ///
931 /// // Fibonacci backoff: 2s, 2s, 4s, 6s, 10s, 16s, 26s...
932 /// let job = Job::new("file_processing".to_string(), json!({"file": "data.csv"}))
933 /// .with_fibonacci_backoff(
934 /// Duration::from_secs(2),
935 /// Some(Duration::from_secs(5 * 60))
936 /// );
937 /// ```
938 pub fn with_fibonacci_backoff(
939 mut self,
940 base: std::time::Duration,
941 max_delay: Option<std::time::Duration>,
942 ) -> Self {
943 self.retry_strategy = Some(RetryStrategy::fibonacci(base, max_delay));
944 self
945 }
946
947 /// Create a recurring job with a cron schedule
948 pub fn with_cron_schedule(
949 queue_name: String,
950 payload: serde_json::Value,
951 cron_schedule: CronSchedule,
952 ) -> Result<Self, crate::cron::CronError> {
953 let now = Utc::now();
954 let next_run = cron_schedule.next_execution_from_now();
955
956 Ok(Self {
957 id: Uuid::new_v4(),
958 queue_name,
959 payload,
960 status: JobStatus::Pending,
961 attempts: 0,
962 max_attempts: 3,
963 created_at: now,
964 scheduled_at: next_run.unwrap_or(now),
965 started_at: None,
966 completed_at: None,
967 failed_at: None,
968 timed_out_at: None,
969 timeout: None,
970 error_message: None,
971 priority: JobPriority::default(),
972 cron_schedule: Some(cron_schedule.expression.clone()),
973 next_run_at: next_run,
974 recurring: true,
975 timezone: Some(cron_schedule.timezone.clone()),
976 batch_id: None,
977 result_config: ResultConfig::default(),
978 result_data: None,
979 result_stored_at: None,
980 result_expires_at: None,
981 retry_strategy: None,
982 depends_on: Vec::new(),
983 dependents: Vec::new(),
984 dependency_status: crate::workflow::DependencyStatus::None,
985 workflow_id: None,
986 workflow_name: None,
987 trace_id: None,
988 correlation_id: None,
989 parent_span_id: None,
990 span_context: None,
991 #[cfg(feature = "encryption")]
992 encryption_config: None,
993 pii_fields: Vec::new(),
994 #[cfg(feature = "encryption")]
995 retention_policy: None,
996 is_encrypted: false,
997 #[cfg(feature = "encryption")]
998 encrypted_payload: None,
999 })
1000 }
1001
1002 /// Add a cron schedule to an existing job
1003 pub fn with_cron(
1004 mut self,
1005 cron_schedule: CronSchedule,
1006 ) -> Result<Self, crate::cron::CronError> {
1007 let next_run = cron_schedule.next_execution_from_now();
1008 self.cron_schedule = Some(cron_schedule.expression.clone());
1009 self.next_run_at = next_run;
1010 self.recurring = true;
1011 self.timezone = Some(cron_schedule.timezone.clone());
1012 self.scheduled_at = next_run.unwrap_or(self.scheduled_at);
1013 Ok(self)
1014 }
1015
1016 /// Schedule the job to run at `scheduled_at` instead of as soon as possible.
1017 ///
1018 /// # Examples
1019 ///
1020 /// ```rust
1021 /// use chrono::{Duration, Utc};
1022 /// use hammerwork::Job;
1023 /// use serde_json::json;
1024 ///
1025 /// let run_at = Utc::now() + Duration::days(7);
1026 /// let job = Job::new("reports".to_string(), json!({})).with_scheduled_at(run_at);
1027 /// assert_eq!(job.scheduled_at, run_at);
1028 /// ```
1029 pub fn with_scheduled_at(mut self, scheduled_at: DateTime<Utc>) -> Self {
1030 self.scheduled_at = scheduled_at;
1031 self
1032 }
1033
1034 /// Set the job as recurring without a cron schedule (for manual rescheduling)
1035 pub fn as_recurring(mut self) -> Self {
1036 self.recurring = true;
1037 self
1038 }
1039
1040 /// Set the timezone for the job
1041 pub fn with_timezone(mut self, timezone: String) -> Self {
1042 self.timezone = Some(timezone);
1043 self
1044 }
1045
1046 /// Configure how job results should be stored.
1047 ///
1048 /// This allows jobs to store their results for later retrieval by other systems.
1049 /// Results can be stored in the database, memory, or not stored at all.
1050 ///
1051 /// # Arguments
1052 ///
1053 /// * `storage` - The storage backend to use for results
1054 ///
1055 /// # Examples
1056 ///
1057 /// ```rust
1058 /// use hammerwork::{Job, job::ResultStorage};
1059 /// use serde_json::json;
1060 ///
1061 /// let job = Job::new("data_processing".to_string(), json!({"input": "data"}))
1062 /// .with_result_storage(ResultStorage::Database);
1063 /// ```
1064 pub fn with_result_storage(mut self, storage: ResultStorage) -> Self {
1065 self.result_config.storage = storage;
1066 self
1067 }
1068
1069 /// Set the time-to-live (TTL) for stored job results.
1070 ///
1071 /// After this duration elapses, the result will be eligible for cleanup.
1072 /// This is useful for managing storage costs and compliance requirements.
1073 ///
1074 /// # Arguments
1075 ///
1076 /// * `ttl` - How long to keep results before they expire
1077 ///
1078 /// # Examples
1079 ///
1080 /// ```rust
1081 /// use hammerwork::Job;
1082 /// use serde_json::json;
1083 /// use std::time::Duration;
1084 ///
1085 /// let job = Job::new("report_generation".to_string(), json!({"type": "monthly"}))
1086 /// .with_result_ttl(Duration::from_secs(7 * 24 * 60 * 60)); // 7 days
1087 /// ```
1088 pub fn with_result_ttl(mut self, ttl: std::time::Duration) -> Self {
1089 self.result_config.ttl = Some(ttl);
1090 self
1091 }
1092
1093 /// Configure complete result storage settings.
1094 ///
1095 /// This provides full control over how results are stored and managed.
1096 ///
1097 /// # Arguments
1098 ///
1099 /// * `config` - Complete result configuration
1100 ///
1101 /// # Examples
1102 ///
1103 /// ```rust
1104 /// use hammerwork::{Job, job::{ResultConfig, ResultStorage}};
1105 /// use serde_json::json;
1106 /// use std::time::Duration;
1107 ///
1108 /// let config = ResultConfig::new(ResultStorage::Database)
1109 /// .with_ttl(Duration::from_secs(3600))
1110 /// .with_max_size(1024 * 1024); // 1MB
1111 ///
1112 /// let job = Job::new("large_processing".to_string(), json!({"data": "..."}))
1113 /// .with_result_config(config);
1114 /// ```
1115 pub fn with_result_config(mut self, config: ResultConfig) -> Self {
1116 self.result_config = config;
1117 self
1118 }
1119
1120 /// Check if the job has result storage configured.
1121 ///
1122 /// Returns `true` if the job is configured to store results in any backend
1123 /// other than `ResultStorage::None`.
1124 ///
1125 /// # Examples
1126 ///
1127 /// ```rust
1128 /// use hammerwork::{Job, job::ResultStorage};
1129 /// use serde_json::json;
1130 ///
1131 /// let job1 = Job::new("test".to_string(), json!({}));
1132 /// assert!(!job1.has_result_storage());
1133 ///
1134 /// let job2 = Job::new("test".to_string(), json!({}))
1135 /// .with_result_storage(ResultStorage::Database);
1136 /// assert!(job2.has_result_storage());
1137 /// ```
1138 pub fn has_result_storage(&self) -> bool {
1139 self.result_config.storage != ResultStorage::None
1140 }
1141
1142 /// Check if the job has stored result data.
1143 ///
1144 /// Returns `true` if the job has result data available for retrieval.
1145 ///
1146 /// # Examples
1147 ///
1148 /// ```rust
1149 /// use hammerwork::Job;
1150 /// use serde_json::json;
1151 ///
1152 /// let job = Job::new("test".to_string(), json!({}));
1153 /// assert!(!job.has_result_data());
1154 /// ```
1155 pub fn has_result_data(&self) -> bool {
1156 self.result_data.is_some()
1157 }
1158
1159 /// Check if the job is dead (failed all retry attempts)
1160 pub fn is_dead(&self) -> bool {
1161 self.status == JobStatus::Dead
1162 }
1163
1164 /// Check if the job has timed out
1165 pub fn is_timed_out(&self) -> bool {
1166 self.status == JobStatus::TimedOut
1167 }
1168
1169 /// Check if the job is critical priority
1170 pub fn is_critical(&self) -> bool {
1171 self.priority == JobPriority::Critical
1172 }
1173
1174 /// Check if the job is high priority
1175 pub fn is_high_priority(&self) -> bool {
1176 self.priority == JobPriority::High
1177 }
1178
1179 /// Check if the job is normal priority
1180 pub fn is_normal_priority(&self) -> bool {
1181 self.priority == JobPriority::Normal
1182 }
1183
1184 /// Check if the job is low priority
1185 pub fn is_low_priority(&self) -> bool {
1186 self.priority == JobPriority::Low
1187 }
1188
1189 /// Check if the job is background priority
1190 pub fn is_background(&self) -> bool {
1191 self.priority == JobPriority::Background
1192 }
1193
1194 /// Get the priority level as a numeric value for comparison
1195 pub fn priority_value(&self) -> i32 {
1196 self.priority.as_i32()
1197 }
1198
1199 /// Check if the job has exhausted all retry attempts
1200 pub fn has_exhausted_retries(&self) -> bool {
1201 self.attempts >= self.max_attempts
1202 }
1203
1204 /// Checks if the job should timeout based on its start time and timeout setting.
1205 ///
1206 /// Returns `true` if the job has been running longer than its configured timeout
1207 /// duration. Returns `false` if the job hasn't started yet, has no timeout set,
1208 /// or is still within the timeout window.
1209 ///
1210 /// # Examples
1211 ///
1212 /// ```rust
1213 /// use hammerwork::Job;
1214 /// use serde_json::json;
1215 /// use std::time::Duration;
1216 /// use chrono::Utc;
1217 ///
1218 /// let mut job = Job::new("test".to_string(), json!({"data": "test"}))
1219 /// .with_timeout(Duration::from_secs(30));
1220 ///
1221 /// // Job hasn't started yet, so it shouldn't timeout
1222 /// assert!(!job.should_timeout());
1223 ///
1224 /// // Simulate job starting 45 seconds ago
1225 /// job.started_at = Some(Utc::now() - chrono::Duration::seconds(45));
1226 ///
1227 /// // Job should timeout since 45s > 30s timeout
1228 /// assert!(job.should_timeout());
1229 /// ```
1230 pub fn should_timeout(&self) -> bool {
1231 if let (Some(started_at), Some(timeout)) = (self.started_at, self.timeout) {
1232 let elapsed = Utc::now() - started_at;
1233 let timeout_duration = chrono::Duration::from_std(timeout).unwrap_or_default();
1234 elapsed >= timeout_duration
1235 } else {
1236 false
1237 }
1238 }
1239
1240 /// Gets the duration since the job was created.
1241 ///
1242 /// This is useful for monitoring how long jobs have been in the system
1243 /// and identifying jobs that may be stuck or delayed.
1244 ///
1245 /// # Examples
1246 ///
1247 /// ```rust
1248 /// use hammerwork::Job;
1249 /// use serde_json::json;
1250 ///
1251 /// let job = Job::new("test".to_string(), json!({"data": "test"}));
1252 /// let age = job.age();
1253 ///
1254 /// // Job was just created, so age should be very small
1255 /// assert!(age.num_milliseconds() >= 0);
1256 /// assert!(age.num_seconds() < 1);
1257 /// ```
1258 pub fn age(&self) -> chrono::Duration {
1259 Utc::now() - self.created_at
1260 }
1261
1262 /// Gets the processing duration if the job has started.
1263 ///
1264 /// Returns the time between when the job started and when it finished
1265 /// (completed, failed, or timed out). If the job is still running,
1266 /// returns the time since it started. Returns `None` if the job hasn't
1267 /// started yet.
1268 ///
1269 /// # Examples
1270 ///
1271 /// ```rust
1272 /// use hammerwork::Job;
1273 /// use serde_json::json;
1274 /// use chrono::Utc;
1275 ///
1276 /// let mut job = Job::new("test".to_string(), json!({"data": "test"}));
1277 ///
1278 /// // Job hasn't started, so no processing duration
1279 /// assert!(job.processing_duration().is_none());
1280 ///
1281 /// // Simulate job that started and completed
1282 /// let start_time = Utc::now() - chrono::Duration::seconds(10);
1283 /// let end_time = start_time + chrono::Duration::seconds(5);
1284 /// job.started_at = Some(start_time);
1285 /// job.completed_at = Some(end_time);
1286 ///
1287 /// let duration = job.processing_duration().unwrap();
1288 /// assert_eq!(duration.num_seconds(), 5);
1289 /// ```
1290 pub fn processing_duration(&self) -> Option<chrono::Duration> {
1291 self.started_at.map(|started| {
1292 self.completed_at
1293 .or(self.failed_at)
1294 .or(self.timed_out_at)
1295 .unwrap_or_else(Utc::now)
1296 - started
1297 })
1298 }
1299
1300 /// Check if this is a recurring job
1301 pub fn is_recurring(&self) -> bool {
1302 self.recurring
1303 }
1304
1305 /// Check if this job has a cron schedule
1306 pub fn has_cron_schedule(&self) -> bool {
1307 self.cron_schedule.is_some()
1308 }
1309
1310 /// Get the cron schedule if it exists
1311 pub fn get_cron_schedule(&self) -> Option<Result<CronSchedule, crate::cron::CronError>> {
1312 self.cron_schedule
1313 .as_ref()
1314 .map(|expr| match &self.timezone {
1315 Some(tz) => CronSchedule::with_timezone(expr, tz),
1316 None => CronSchedule::new(expr),
1317 })
1318 }
1319
1320 /// Calculate the next run time for a recurring job
1321 pub fn calculate_next_run(&self) -> Option<DateTime<Utc>> {
1322 if !self.recurring {
1323 return None;
1324 }
1325
1326 if let Some(cron_schedule) = self.get_cron_schedule() {
1327 match cron_schedule {
1328 Ok(schedule) => schedule.next_execution_from_now(),
1329 Err(_) => None,
1330 }
1331 } else {
1332 None
1333 }
1334 }
1335
1336 /// Update the job for the next run (for recurring jobs)
1337 pub fn prepare_for_next_run(&mut self) -> Option<DateTime<Utc>> {
1338 if !self.recurring {
1339 return None;
1340 }
1341
1342 let next_run = self.calculate_next_run();
1343 if let Some(next_time) = next_run {
1344 self.status = JobStatus::Pending;
1345 self.attempts = 0;
1346 self.scheduled_at = next_time;
1347 self.next_run_at = Some(next_time);
1348 self.started_at = None;
1349 self.completed_at = None;
1350 self.failed_at = None;
1351 self.timed_out_at = None;
1352 self.error_message = None;
1353 }
1354 next_run
1355 }
1356
1357 /// Adds a dependency on another job.
1358 ///
1359 /// This job will not be executed until the specified job completes successfully.
1360 /// If the dependency job fails, this job's dependency status will be set to Failed.
1361 ///
1362 /// # Arguments
1363 ///
1364 /// * `job_id` - The ID of the job this job should depend on
1365 ///
1366 /// # Examples
1367 ///
1368 /// ```rust
1369 /// use hammerwork::Job;
1370 /// use serde_json::json;
1371 ///
1372 /// let job1 = Job::new("step1".to_string(), json!({"data": "step1"}));
1373 /// let job2 = Job::new("step2".to_string(), json!({"data": "step2"}))
1374 /// .depends_on(&job1.id);
1375 /// assert!(job2.has_dependencies());
1376 /// ```
1377 pub fn depends_on(mut self, job_id: &JobId) -> Self {
1378 self.depends_on.push(*job_id);
1379 self.dependency_status = crate::workflow::DependencyStatus::Waiting;
1380 self
1381 }
1382
1383 /// Adds multiple dependencies on other jobs.
1384 ///
1385 /// This job will not be executed until all specified jobs complete successfully.
1386 ///
1387 /// # Arguments
1388 ///
1389 /// * `job_ids` - The IDs of the jobs this job should depend on
1390 ///
1391 /// # Examples
1392 ///
1393 /// ```rust
1394 /// use hammerwork::Job;
1395 /// use serde_json::json;
1396 ///
1397 /// let job1 = Job::new("step1".to_string(), json!({}));
1398 /// let job2 = Job::new("step2".to_string(), json!({}));
1399 /// let final_job = Job::new("final".to_string(), json!({}))
1400 /// .depends_on_jobs(&[job1.id, job2.id]);
1401 /// assert_eq!(final_job.depends_on.len(), 2);
1402 /// ```
1403 pub fn depends_on_jobs(mut self, job_ids: &[JobId]) -> Self {
1404 self.depends_on.extend_from_slice(job_ids);
1405 if !job_ids.is_empty() {
1406 self.dependency_status = crate::workflow::DependencyStatus::Waiting;
1407 }
1408 self
1409 }
1410
1411 /// Sets the workflow this job belongs to.
1412 ///
1413 /// # Arguments
1414 ///
1415 /// * `workflow_id` - The ID of the workflow
1416 /// * `workflow_name` - The name of the workflow
1417 ///
1418 /// # Examples
1419 ///
1420 /// ```rust
1421 /// use hammerwork::Job;
1422 /// use serde_json::json;
1423 /// use uuid::Uuid;
1424 ///
1425 /// let workflow_id = Uuid::new_v4();
1426 /// let job = Job::new("test".to_string(), json!({}))
1427 /// .with_workflow(workflow_id, "data_pipeline");
1428 ///
1429 /// assert_eq!(job.workflow_id, Some(workflow_id));
1430 /// assert_eq!(job.workflow_name, Some("data_pipeline".to_string()));
1431 /// ```
1432 pub fn with_workflow(
1433 mut self,
1434 workflow_id: crate::workflow::WorkflowId,
1435 workflow_name: impl Into<String>,
1436 ) -> Self {
1437 self.workflow_id = Some(workflow_id);
1438 self.workflow_name = Some(workflow_name.into());
1439 self
1440 }
1441
1442 /// Checks if this job has any dependencies.
1443 ///
1444 /// # Examples
1445 ///
1446 /// ```rust
1447 /// use hammerwork::Job;
1448 /// use serde_json::json;
1449 ///
1450 /// let job1 = Job::new("independent".to_string(), json!({}));
1451 /// assert!(!job1.has_dependencies());
1452 ///
1453 /// let job2 = Job::new("dependent".to_string(), json!({}))
1454 /// .depends_on(&job1.id);
1455 /// assert!(job2.has_dependencies());
1456 /// ```
1457 pub fn has_dependencies(&self) -> bool {
1458 !self.depends_on.is_empty()
1459 }
1460
1461 /// Checks if this job is part of a workflow.
1462 ///
1463 /// # Examples
1464 ///
1465 /// ```rust
1466 /// use hammerwork::Job;
1467 /// use serde_json::json;
1468 /// use uuid::Uuid;
1469 ///
1470 /// let job1 = Job::new("standalone".to_string(), json!({}));
1471 /// assert!(!job1.is_part_of_workflow());
1472 ///
1473 /// let job2 = Job::new("workflow_job".to_string(), json!({}))
1474 /// .with_workflow(Uuid::new_v4(), "test_workflow");
1475 /// assert!(job2.is_part_of_workflow());
1476 /// ```
1477 pub fn is_part_of_workflow(&self) -> bool {
1478 self.workflow_id.is_some()
1479 }
1480
1481 /// Checks if all dependencies for this job are satisfied.
1482 ///
1483 /// # Examples
1484 ///
1485 /// ```rust
1486 /// use hammerwork::Job;
1487 /// use serde_json::json;
1488 ///
1489 /// let job1 = Job::new("independent".to_string(), json!({}));
1490 /// assert!(job1.dependencies_satisfied());
1491 ///
1492 /// let job2 = Job::new("dependent".to_string(), json!({}))
1493 /// .depends_on(&job1.id);
1494 /// assert!(!job2.dependencies_satisfied()); // Dependencies not satisfied yet
1495 /// ```
1496 pub fn dependencies_satisfied(&self) -> bool {
1497 matches!(
1498 self.dependency_status,
1499 crate::workflow::DependencyStatus::None | crate::workflow::DependencyStatus::Satisfied
1500 )
1501 }
1502
1503 /// Checks if any dependencies for this job have failed.
1504 ///
1505 /// # Examples
1506 ///
1507 /// ```rust
1508 /// use hammerwork::Job;
1509 /// use serde_json::json;
1510 ///
1511 /// let mut job = Job::new("test".to_string(), json!({}));
1512 /// job.dependency_status = hammerwork::workflow::DependencyStatus::Failed;
1513 /// assert!(job.dependencies_failed());
1514 /// ```
1515 pub fn dependencies_failed(&self) -> bool {
1516 matches!(
1517 self.dependency_status,
1518 crate::workflow::DependencyStatus::Failed
1519 )
1520 }
1521
1522 /// Gets the number of dependencies for this job.
1523 ///
1524 /// # Examples
1525 ///
1526 /// ```rust
1527 /// use hammerwork::Job;
1528 /// use serde_json::json;
1529 /// use uuid::Uuid;
1530 ///
1531 /// let job = Job::new("test".to_string(), json!({}))
1532 /// .depends_on_jobs(&[Uuid::new_v4(), Uuid::new_v4()]);
1533 /// assert_eq!(job.dependency_count(), 2);
1534 /// ```
1535 pub fn dependency_count(&self) -> usize {
1536 self.depends_on.len()
1537 }
1538
1539 /// Gets the number of jobs that depend on this job.
1540 ///
1541 /// # Examples
1542 ///
1543 /// ```rust
1544 /// use hammerwork::Job;
1545 /// use serde_json::json;
1546 ///
1547 /// let job = Job::new("test".to_string(), json!({}));
1548 /// assert_eq!(job.dependent_count(), 0);
1549 /// ```
1550 pub fn dependent_count(&self) -> usize {
1551 self.dependents.len()
1552 }
1553
1554 /// Sets the distributed trace identifier for this job.
1555 ///
1556 /// The trace ID is used to track jobs across service boundaries in distributed
1557 /// systems. All related operations should share the same trace ID.
1558 ///
1559 /// # Arguments
1560 ///
1561 /// * `trace_id` - The distributed trace identifier
1562 ///
1563 /// # Examples
1564 ///
1565 /// ```rust
1566 /// use hammerwork::Job;
1567 /// use serde_json::json;
1568 ///
1569 /// let job = Job::new("service_call".to_string(), json!({"data": "test"}))
1570 /// .with_trace_id("trace-123-456");
1571 ///
1572 /// assert_eq!(job.trace_id, Some("trace-123-456".to_string()));
1573 /// ```
1574 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
1575 self.trace_id = Some(trace_id.into());
1576 self
1577 }
1578
1579 /// Sets the business correlation identifier for this job.
1580 ///
1581 /// The correlation ID groups related business operations together, even if they
1582 /// span multiple traces or services. Use this to correlate jobs that process
1583 /// the same business entity or workflow.
1584 ///
1585 /// # Arguments
1586 ///
1587 /// * `correlation_id` - The business correlation identifier
1588 ///
1589 /// # Examples
1590 ///
1591 /// ```rust
1592 /// use hammerwork::Job;
1593 /// use serde_json::json;
1594 ///
1595 /// let job = Job::new("order_processing".to_string(), json!({"order_id": 12345}))
1596 /// .with_correlation_id("order-12345");
1597 ///
1598 /// assert_eq!(job.correlation_id, Some("order-12345".to_string()));
1599 /// ```
1600 pub fn with_correlation_id(mut self, correlation_id: impl Into<String>) -> Self {
1601 self.correlation_id = Some(correlation_id.into());
1602 self
1603 }
1604
1605 /// Sets the parent span identifier for hierarchical tracing.
1606 ///
1607 /// Use this to create a parent-child relationship between spans, enabling
1608 /// hierarchical trace visualization in tracing systems.
1609 ///
1610 /// # Arguments
1611 ///
1612 /// * `parent_span_id` - The parent span identifier
1613 ///
1614 /// # Examples
1615 ///
1616 /// ```rust
1617 /// use hammerwork::Job;
1618 /// use serde_json::json;
1619 ///
1620 /// let job = Job::new("child_task".to_string(), json!({"data": "test"}))
1621 /// .with_parent_span_id("span-parent-123");
1622 ///
1623 /// assert_eq!(job.parent_span_id, Some("span-parent-123".to_string()));
1624 /// ```
1625 pub fn with_parent_span_id(mut self, parent_span_id: impl Into<String>) -> Self {
1626 self.parent_span_id = Some(parent_span_id.into());
1627 self
1628 }
1629
1630 /// Sets the serialized span context for trace propagation.
1631 ///
1632 /// The span context contains all the information needed to propagate tracing
1633 /// across service boundaries. This is typically a serialized representation
1634 /// of the current span context.
1635 ///
1636 /// # Arguments
1637 ///
1638 /// * `span_context` - The serialized span context
1639 ///
1640 /// # Examples
1641 ///
1642 /// ```rust
1643 /// use hammerwork::Job;
1644 /// use serde_json::json;
1645 ///
1646 /// let job = Job::new("distributed_task".to_string(), json!({"data": "test"}))
1647 /// .with_span_context("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
1648 ///
1649 /// assert_eq!(job.span_context, Some("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string()));
1650 /// ```
1651 pub fn with_span_context(mut self, span_context: impl Into<String>) -> Self {
1652 self.span_context = Some(span_context.into());
1653 self
1654 }
1655
1656 /// Convenience method to set both trace ID and correlation ID.
1657 ///
1658 /// This is useful when the trace ID and correlation ID are the same,
1659 /// which is common in simple tracing scenarios.
1660 ///
1661 /// # Arguments
1662 ///
1663 /// * `id` - The identifier to use for both trace and correlation
1664 ///
1665 /// # Examples
1666 ///
1667 /// ```rust
1668 /// use hammerwork::Job;
1669 /// use serde_json::json;
1670 ///
1671 /// let job = Job::new("task".to_string(), json!({"data": "test"}))
1672 /// .with_tracing_id("unified-id-123");
1673 ///
1674 /// assert_eq!(job.trace_id, Some("unified-id-123".to_string()));
1675 /// assert_eq!(job.correlation_id, Some("unified-id-123".to_string()));
1676 /// ```
1677 pub fn with_tracing_id(mut self, id: impl Into<String>) -> Self {
1678 let id_string = id.into();
1679 self.trace_id = Some(id_string.clone());
1680 self.correlation_id = Some(id_string);
1681 self
1682 }
1683
1684 /// Checks if this job has any tracing information.
1685 ///
1686 /// Returns `true` if any of the tracing fields (trace_id, correlation_id,
1687 /// parent_span_id, or span_context) are set.
1688 ///
1689 /// # Examples
1690 ///
1691 /// ```rust
1692 /// use hammerwork::Job;
1693 /// use serde_json::json;
1694 ///
1695 /// let job1 = Job::new("untraced".to_string(), json!({}));
1696 /// assert!(!job1.has_tracing_info());
1697 ///
1698 /// let job2 = Job::new("traced".to_string(), json!({}))
1699 /// .with_trace_id("trace-123");
1700 /// assert!(job2.has_tracing_info());
1701 /// ```
1702 pub fn has_tracing_info(&self) -> bool {
1703 self.trace_id.is_some()
1704 || self.correlation_id.is_some()
1705 || self.parent_span_id.is_some()
1706 || self.span_context.is_some()
1707 }
1708
1709 /// Gets the trace ID if available.
1710 ///
1711 /// # Examples
1712 ///
1713 /// ```rust
1714 /// use hammerwork::Job;
1715 /// use serde_json::json;
1716 ///
1717 /// let job = Job::new("test".to_string(), json!({}))
1718 /// .with_trace_id("trace-123");
1719 ///
1720 /// assert_eq!(job.get_trace_id(), Some("trace-123"));
1721 /// ```
1722 pub fn get_trace_id(&self) -> Option<&str> {
1723 self.trace_id.as_deref()
1724 }
1725
1726 /// Gets the correlation ID if available.
1727 ///
1728 /// # Examples
1729 ///
1730 /// ```rust
1731 /// use hammerwork::Job;
1732 /// use serde_json::json;
1733 ///
1734 /// let job = Job::new("test".to_string(), json!({}))
1735 /// .with_correlation_id("corr-456");
1736 ///
1737 /// assert_eq!(job.get_correlation_id(), Some("corr-456"));
1738 /// ```
1739 pub fn get_correlation_id(&self) -> Option<&str> {
1740 self.correlation_id.as_deref()
1741 }
1742
1743 /// Sets the encryption configuration for this job.
1744 ///
1745 /// When encryption is configured, the job payload will be encrypted before
1746 /// being stored in the database and decrypted when retrieved.
1747 ///
1748 /// # Arguments
1749 ///
1750 /// * `config` - The encryption configuration to use
1751 ///
1752 /// # Examples
1753 ///
1754 /// ```rust,no_run
1755 /// # #[cfg(feature = "encryption")]
1756 /// # {
1757 /// use hammerwork::{Job, encryption::{EncryptionConfig, EncryptionAlgorithm}};
1758 /// use serde_json::json;
1759 ///
1760 /// let job = Job::new("secure_task".to_string(), json!({"data": "sensitive"}))
1761 /// .with_encryption(EncryptionConfig::new(EncryptionAlgorithm::AES256GCM));
1762 ///
1763 /// assert!(job.has_encryption());
1764 /// # }
1765 /// ```
1766 #[cfg(feature = "encryption")]
1767 pub fn with_encryption(mut self, config: crate::encryption::EncryptionConfig) -> Self {
1768 self.encryption_config = Some(config);
1769 self
1770 }
1771
1772 /// Sets the PII fields for this job.
1773 ///
1774 /// PII (Personally Identifiable Information) fields are tracked separately
1775 /// for compliance and auditing purposes. When encryption is enabled, these
1776 /// fields receive special handling.
1777 ///
1778 /// # Arguments
1779 ///
1780 /// * `pii_fields` - List of field names that contain PII
1781 ///
1782 /// # Examples
1783 ///
1784 /// ```rust
1785 /// use hammerwork::Job;
1786 /// use serde_json::json;
1787 ///
1788 /// let job = Job::new("user_processing".to_string(), json!({
1789 /// "user_id": "123",
1790 /// "email": "user@example.com",
1791 /// "ssn": "123-45-6789"
1792 /// }))
1793 /// .with_pii_fields(vec!["email", "ssn"]);
1794 ///
1795 /// assert_eq!(job.pii_fields.len(), 2);
1796 /// assert!(job.has_pii_fields());
1797 /// ```
1798 pub fn with_pii_fields(mut self, pii_fields: Vec<impl Into<String>>) -> Self {
1799 self.pii_fields = pii_fields.into_iter().map(|f| f.into()).collect();
1800 self
1801 }
1802
1803 /// Sets the retention policy for this job's encrypted data.
1804 ///
1805 /// The retention policy determines how long encrypted job data should be
1806 /// kept before automatic cleanup.
1807 ///
1808 /// # Arguments
1809 ///
1810 /// * `policy` - The retention policy to apply
1811 ///
1812 /// # Examples
1813 ///
1814 /// ```rust,no_run
1815 /// # #[cfg(feature = "encryption")]
1816 /// # {
1817 /// use hammerwork::{Job, encryption::RetentionPolicy};
1818 /// use serde_json::json;
1819 /// use std::time::Duration;
1820 ///
1821 /// let job = Job::new("temp_processing".to_string(), json!({"data": "temp"}))
1822 /// .with_retention_policy(RetentionPolicy::DeleteAfter(Duration::from_secs(3600)));
1823 /// # }
1824 /// ```
1825 #[cfg(feature = "encryption")]
1826 pub fn with_retention_policy(mut self, policy: crate::encryption::RetentionPolicy) -> Self {
1827 self.retention_policy = Some(policy);
1828 self
1829 }
1830
1831 /// Checks if this job has encryption configured.
1832 ///
1833 /// # Examples
1834 ///
1835 /// ```rust
1836 /// use hammerwork::Job;
1837 /// use serde_json::json;
1838 ///
1839 /// let job1 = Job::new("plain".to_string(), json!({}));
1840 /// assert!(!job1.has_encryption());
1841 ///
1842 /// #[cfg(feature = "encryption")]
1843 /// {
1844 /// use hammerwork::encryption::{EncryptionConfig, EncryptionAlgorithm};
1845 /// let job2 = Job::new("encrypted".to_string(), json!({}))
1846 /// .with_encryption(EncryptionConfig::new(EncryptionAlgorithm::AES256GCM));
1847 /// assert!(job2.has_encryption());
1848 /// }
1849 /// ```
1850 pub fn has_encryption(&self) -> bool {
1851 #[cfg(feature = "encryption")]
1852 {
1853 self.encryption_config.is_some()
1854 }
1855 #[cfg(not(feature = "encryption"))]
1856 {
1857 false
1858 }
1859 }
1860
1861 /// Checks if this job has PII fields configured.
1862 ///
1863 /// # Examples
1864 ///
1865 /// ```rust
1866 /// use hammerwork::Job;
1867 /// use serde_json::json;
1868 ///
1869 /// let job1 = Job::new("normal".to_string(), json!({}));
1870 /// assert!(!job1.has_pii_fields());
1871 ///
1872 /// let job2 = Job::new("with_pii".to_string(), json!({}))
1873 /// .with_pii_fields(vec!["email"]);
1874 /// assert!(job2.has_pii_fields());
1875 /// ```
1876 pub fn has_pii_fields(&self) -> bool {
1877 !self.pii_fields.is_empty()
1878 }
1879
1880 /// Checks if this job's payload is currently encrypted.
1881 ///
1882 /// # Examples
1883 ///
1884 /// ```rust
1885 /// use hammerwork::Job;
1886 /// use serde_json::json;
1887 ///
1888 /// let job = Job::new("test".to_string(), json!({}));
1889 /// assert!(!job.is_payload_encrypted());
1890 /// ```
1891 pub fn is_payload_encrypted(&self) -> bool {
1892 self.is_encrypted
1893 }
1894
1895 /// Gets the list of PII fields for this job.
1896 ///
1897 /// # Examples
1898 ///
1899 /// ```rust
1900 /// use hammerwork::Job;
1901 /// use serde_json::json;
1902 ///
1903 /// let job = Job::new("test".to_string(), json!({}))
1904 /// .with_pii_fields(vec!["email", "ssn"]);
1905 ///
1906 /// let pii_fields = job.get_pii_fields();
1907 /// assert_eq!(pii_fields.len(), 2);
1908 /// assert!(pii_fields.contains(&"email".to_string()));
1909 /// ```
1910 pub fn get_pii_fields(&self) -> &[String] {
1911 &self.pii_fields
1912 }
1913
1914 /// Gets the encryption configuration if available.
1915 ///
1916 /// # Examples
1917 ///
1918 /// ```rust,no_run
1919 /// # #[cfg(feature = "encryption")]
1920 /// # {
1921 /// use hammerwork::{Job, encryption::{EncryptionConfig, EncryptionAlgorithm}};
1922 /// use serde_json::json;
1923 ///
1924 /// let config = EncryptionConfig::new(EncryptionAlgorithm::AES256GCM);
1925 /// let job = Job::new("test".to_string(), json!({}))
1926 /// .with_encryption(config.clone());
1927 ///
1928 /// let retrieved_config = job.get_encryption_config().unwrap();
1929 /// assert_eq!(retrieved_config.algorithm, EncryptionAlgorithm::AES256GCM);
1930 /// # }
1931 /// ```
1932 #[cfg(feature = "encryption")]
1933 pub fn get_encryption_config(&self) -> Option<&crate::encryption::EncryptionConfig> {
1934 self.encryption_config.as_ref()
1935 }
1936
1937 /// Gets the retention policy if available.
1938 ///
1939 /// # Examples
1940 ///
1941 /// ```rust
1942 /// # #[cfg(feature = "encryption")]
1943 /// # {
1944 /// use hammerwork::{Job, encryption::RetentionPolicy};
1945 /// use serde_json::json;
1946 /// use std::time::Duration;
1947 ///
1948 /// let policy = RetentionPolicy::DeleteAfter(Duration::from_secs(3600));
1949 /// let job = Job::new("test".to_string(), json!({}))
1950 /// .with_retention_policy(policy.clone());
1951 ///
1952 /// assert_eq!(job.get_retention_policy(), Some(&policy));
1953 /// # }
1954 /// ```
1955 #[cfg(feature = "encryption")]
1956 pub fn get_retention_policy(&self) -> Option<&crate::encryption::RetentionPolicy> {
1957 self.retention_policy.as_ref()
1958 }
1959
1960 /// Checks if this job should have its encrypted data cleaned up now.
1961 ///
1962 /// This method checks the retention policy and encrypted payload metadata
1963 /// to determine if the data has exceeded its retention period.
1964 ///
1965 /// # Examples
1966 ///
1967 /// ```rust
1968 /// use hammerwork::Job;
1969 /// use serde_json::json;
1970 ///
1971 /// let job = Job::new("test".to_string(), json!({}));
1972 /// assert!(!job.should_cleanup_encrypted_data());
1973 /// ```
1974 pub fn should_cleanup_encrypted_data(&self) -> bool {
1975 #[cfg(feature = "encryption")]
1976 {
1977 if let Some(encrypted_payload) = &self.encrypted_payload {
1978 encrypted_payload.should_delete_now()
1979 } else if let Some(retention_policy) = &self.retention_policy {
1980 retention_policy.should_delete_now(
1981 self.created_at,
1982 self.completed_at,
1983 self.encryption_config
1984 .as_ref()
1985 .and_then(|c| c.default_retention),
1986 )
1987 } else {
1988 false
1989 }
1990 }
1991 #[cfg(not(feature = "encryption"))]
1992 {
1993 false
1994 }
1995 }
1996}
1997
1998#[cfg(test)]
1999mod tests {
2000 use super::*;
2001 use serde_json::json;
2002
2003 #[test]
2004 fn test_job_new() {
2005 let queue_name = "test_queue".to_string();
2006 let payload = json!({"key": "value"});
2007
2008 let job = Job::new(queue_name.clone(), payload.clone());
2009
2010 assert_eq!(job.queue_name, queue_name);
2011 assert_eq!(job.payload, payload);
2012 assert_eq!(job.status, JobStatus::Pending);
2013 assert_eq!(job.attempts, 0);
2014 assert_eq!(job.max_attempts, 3);
2015 assert!(job.started_at.is_none());
2016 assert!(job.completed_at.is_none());
2017 assert!(job.failed_at.is_none());
2018 assert!(job.error_message.is_none());
2019 assert_eq!(job.created_at, job.scheduled_at);
2020 }
2021
2022 #[test]
2023 fn test_job_with_delay() {
2024 let queue_name = "test_queue".to_string();
2025 let payload = json!({"key": "value"});
2026 let delay = chrono::Duration::minutes(5);
2027
2028 let job = Job::with_delay(queue_name.clone(), payload.clone(), delay);
2029
2030 assert_eq!(job.queue_name, queue_name);
2031 assert_eq!(job.payload, payload);
2032 assert_eq!(job.status, JobStatus::Pending);
2033 assert_eq!(job.attempts, 0);
2034 assert_eq!(job.max_attempts, 3);
2035 assert!(job.scheduled_at > job.created_at);
2036 assert_eq!(job.scheduled_at - job.created_at, delay);
2037 }
2038
2039 #[test]
2040 fn test_job_with_max_attempts() {
2041 let queue_name = "test_queue".to_string();
2042 let payload = json!({"key": "value"});
2043
2044 let job = Job::new(queue_name, payload).with_max_attempts(5);
2045
2046 assert_eq!(job.max_attempts, 5);
2047 }
2048
2049 #[test]
2050 fn test_job_with_delay_and_max_attempts() {
2051 let queue_name = "test_queue".to_string();
2052 let payload = json!({"key": "value"});
2053 let delay = chrono::Duration::hours(1);
2054
2055 let job = Job::with_delay(queue_name, payload, delay).with_max_attempts(10);
2056
2057 assert_eq!(job.max_attempts, 10);
2058 assert!(job.scheduled_at > job.created_at);
2059 }
2060
2061 #[test]
2062 fn test_job_status_equality() {
2063 assert_eq!(JobStatus::Pending, JobStatus::Pending);
2064 assert_eq!(JobStatus::Running, JobStatus::Running);
2065 assert_eq!(JobStatus::Completed, JobStatus::Completed);
2066 assert_eq!(JobStatus::Failed, JobStatus::Failed);
2067 assert_eq!(JobStatus::Dead, JobStatus::Dead);
2068 assert_eq!(JobStatus::TimedOut, JobStatus::TimedOut);
2069 assert_eq!(JobStatus::Retrying, JobStatus::Retrying);
2070
2071 assert_ne!(JobStatus::Pending, JobStatus::Running);
2072 assert_ne!(JobStatus::Completed, JobStatus::Failed);
2073 assert_ne!(JobStatus::Failed, JobStatus::Dead);
2074 assert_ne!(JobStatus::Dead, JobStatus::TimedOut);
2075 assert_ne!(JobStatus::TimedOut, JobStatus::Failed);
2076 }
2077
2078 #[test]
2079 fn test_job_serialization() {
2080 let job = Job::new("test".to_string(), json!({"data": "test"}));
2081
2082 let serialized = serde_json::to_string(&job).unwrap();
2083 let deserialized: Job = serde_json::from_str(&serialized).unwrap();
2084
2085 assert_eq!(job.id, deserialized.id);
2086 assert_eq!(job.queue_name, deserialized.queue_name);
2087 assert_eq!(job.payload, deserialized.payload);
2088 assert_eq!(job.status, deserialized.status);
2089 assert_eq!(job.attempts, deserialized.attempts);
2090 assert_eq!(job.max_attempts, deserialized.max_attempts);
2091 }
2092
2093 #[test]
2094 fn test_job_status_serialization() {
2095 let statuses = vec![
2096 JobStatus::Pending,
2097 JobStatus::Running,
2098 JobStatus::Completed,
2099 JobStatus::Failed,
2100 JobStatus::Dead,
2101 JobStatus::TimedOut,
2102 JobStatus::Retrying,
2103 ];
2104
2105 for status in statuses {
2106 let serialized = serde_json::to_string(&status).unwrap();
2107 let deserialized: JobStatus = serde_json::from_str(&serialized).unwrap();
2108 assert_eq!(status, deserialized);
2109 }
2110 }
2111
2112 #[test]
2113 fn test_job_dead_status_methods() {
2114 let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2115
2116 // Initially not dead
2117 assert!(!job.is_dead());
2118 assert!(!job.has_exhausted_retries());
2119
2120 // Simulate exhausting retries
2121 job.attempts = 3;
2122 job.max_attempts = 3;
2123 assert!(job.has_exhausted_retries());
2124 assert!(!job.is_dead()); // Still not dead until status is set
2125
2126 // Mark as dead
2127 job.status = JobStatus::Dead;
2128 job.failed_at = Some(Utc::now());
2129 assert!(job.is_dead());
2130 assert!(job.has_exhausted_retries());
2131 }
2132
2133 #[test]
2134 fn test_job_processing_duration() {
2135 let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2136
2137 // No processing duration when not started
2138 assert!(job.processing_duration().is_none());
2139
2140 // Set start time
2141 let start_time = Utc::now();
2142 job.started_at = Some(start_time);
2143
2144 // Should have some duration now (very small)
2145 let duration = job.processing_duration().unwrap();
2146 assert!(duration.num_milliseconds() >= 0);
2147
2148 // Set completion time
2149 let completion_time = start_time + chrono::Duration::seconds(5);
2150 job.completed_at = Some(completion_time);
2151
2152 let final_duration = job.processing_duration().unwrap();
2153 assert_eq!(final_duration.num_seconds(), 5);
2154 }
2155
2156 #[test]
2157 fn test_job_age() {
2158 let job = Job::new("test".to_string(), json!({"data": "test"}));
2159 let age = job.age();
2160
2161 // Age should be very small (just created)
2162 assert!(age.num_milliseconds() >= 0);
2163 assert!(age.num_seconds() < 1);
2164 }
2165
2166 #[test]
2167 fn test_job_with_timeout() {
2168 let timeout = std::time::Duration::from_secs(30);
2169 let job = Job::new("test".to_string(), json!({"data": "test"})).with_timeout(timeout);
2170
2171 assert_eq!(job.timeout, Some(timeout));
2172 assert!(!job.is_timed_out()); // Not timed out until status is set
2173 }
2174
2175 #[test]
2176 fn test_job_timeout_status_methods() {
2177 let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2178
2179 // Initially not timed out
2180 assert!(!job.is_timed_out());
2181
2182 // Set timed out status
2183 job.status = JobStatus::TimedOut;
2184 job.timed_out_at = Some(Utc::now());
2185 assert!(job.is_timed_out());
2186 }
2187
2188 #[test]
2189 fn test_job_should_timeout() {
2190 let mut job = Job::new("test".to_string(), json!({"data": "test"}))
2191 .with_timeout(std::time::Duration::from_millis(100));
2192
2193 // Should not timeout before it starts
2194 assert!(!job.should_timeout());
2195
2196 // Set start time to simulate job starting
2197 job.started_at = Some(Utc::now() - chrono::Duration::milliseconds(200));
2198
2199 // Should timeout since 200ms > 100ms timeout
2200 assert!(job.should_timeout());
2201
2202 // Job without timeout should never timeout
2203 let mut job_no_timeout = Job::new("test".to_string(), json!({"data": "test"}));
2204 job_no_timeout.started_at = Some(Utc::now() - chrono::Duration::hours(1));
2205 assert!(!job_no_timeout.should_timeout());
2206 }
2207
2208 #[test]
2209 fn test_job_with_delay_and_timeout() {
2210 let delay = chrono::Duration::minutes(5);
2211 let timeout = std::time::Duration::from_secs(120);
2212
2213 let job = Job::with_delay("test".to_string(), json!({"data": "test"}), delay)
2214 .with_timeout(timeout)
2215 .with_max_attempts(5);
2216
2217 assert_eq!(job.timeout, Some(timeout));
2218 assert_eq!(job.max_attempts, 5);
2219 assert!(job.scheduled_at > job.created_at);
2220 assert_eq!(job.scheduled_at - job.created_at, delay);
2221 }
2222
2223 #[test]
2224 fn test_processing_duration_with_timeout() {
2225 let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2226
2227 // Set start time and timed out time
2228 let start_time = Utc::now() - chrono::Duration::seconds(5);
2229 let timeout_time = start_time + chrono::Duration::seconds(3);
2230
2231 job.started_at = Some(start_time);
2232 job.timed_out_at = Some(timeout_time);
2233
2234 let duration = job.processing_duration().unwrap();
2235 assert_eq!(duration.num_seconds(), 3);
2236 }
2237
2238 #[test]
2239 fn test_timeout_builder_methods() {
2240 let job = Job::new("test".to_string(), json!({"key": "value"}))
2241 .with_timeout(std::time::Duration::from_secs(120))
2242 .with_max_attempts(5);
2243
2244 assert_eq!(job.timeout, Some(std::time::Duration::from_secs(120)));
2245 assert_eq!(job.max_attempts, 5);
2246 assert_eq!(job.queue_name, "test");
2247 }
2248
2249 #[test]
2250 fn test_job_timeout_edge_cases() {
2251 let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2252
2253 // Job without timeout should never timeout
2254 assert!(!job.should_timeout());
2255
2256 // Job with timeout but not started should not timeout
2257 job.timeout = Some(std::time::Duration::from_millis(100));
2258 assert!(!job.should_timeout());
2259
2260 // Job with timeout and started but within timeout window should not timeout
2261 job.started_at = Some(Utc::now() - chrono::Duration::milliseconds(50));
2262 assert!(!job.should_timeout());
2263
2264 // Job with timeout and started beyond timeout window should timeout
2265 job.started_at = Some(Utc::now() - chrono::Duration::milliseconds(150));
2266 assert!(job.should_timeout());
2267 }
2268
2269 #[test]
2270 fn test_job_status_transitions_with_timeout() {
2271 let mut job = Job::new("test".to_string(), json!({"data": "test"}));
2272
2273 // Initial state
2274 assert_eq!(job.status, JobStatus::Pending);
2275 assert!(!job.is_timed_out());
2276
2277 // Simulate timeout
2278 job.status = JobStatus::TimedOut;
2279 job.timed_out_at = Some(Utc::now());
2280
2281 assert!(job.is_timed_out());
2282 assert!(!job.is_dead()); // TimedOut is different from Dead
2283 }
2284
2285 #[test]
2286 fn test_timeout_serialization_compatibility() {
2287 let original_job = Job::new("test_queue".to_string(), json!({"data": "test"}))
2288 .with_timeout(std::time::Duration::from_secs(300))
2289 .with_max_attempts(5);
2290
2291 // Serialize and deserialize
2292 let serialized = serde_json::to_string(&original_job).unwrap();
2293 let deserialized: Job = serde_json::from_str(&serialized).unwrap();
2294
2295 // Verify timeout field is preserved
2296 assert_eq!(original_job.timeout, deserialized.timeout);
2297 assert_eq!(original_job.timed_out_at, deserialized.timed_out_at);
2298 assert_eq!(original_job.status, deserialized.status);
2299 }
2300
2301 #[test]
2302 fn test_job_with_all_timeout_fields() {
2303 let timeout_duration = std::time::Duration::from_secs(60);
2304 let mut job = Job::new("comprehensive_test".to_string(), json!({"test": true}))
2305 .with_timeout(timeout_duration)
2306 .with_max_attempts(3);
2307
2308 // Simulate job lifecycle with timeout
2309 job.started_at = Some(Utc::now() - chrono::Duration::seconds(30));
2310 job.status = JobStatus::Running;
2311
2312 // Should not timeout yet (30s < 60s)
2313 assert!(!job.should_timeout());
2314
2315 // Simulate timeout occurring
2316 job.status = JobStatus::TimedOut;
2317 job.timed_out_at = Some(Utc::now());
2318 job.error_message = Some("Job timed out after 60s".to_string());
2319
2320 assert!(job.is_timed_out());
2321 assert_eq!(job.timeout, Some(timeout_duration));
2322 assert!(job.timed_out_at.is_some());
2323 assert!(job.error_message.is_some());
2324 }
2325
2326 #[test]
2327 fn test_job_status_backward_compatibility_string_matching() {
2328 // Test that our string matching logic handles both quoted and unquoted formats
2329 // This simulates what happens in the Decode implementation
2330 let test_cases = [
2331 // (input_string, expected_status)
2332 ("Pending", JobStatus::Pending),
2333 ("\"Pending\"", JobStatus::Pending),
2334 ("Running", JobStatus::Running),
2335 ("\"Running\"", JobStatus::Running),
2336 ("Completed", JobStatus::Completed),
2337 ("\"Completed\"", JobStatus::Completed),
2338 ("Failed", JobStatus::Failed),
2339 ("\"Failed\"", JobStatus::Failed),
2340 ("Dead", JobStatus::Dead),
2341 ("\"Dead\"", JobStatus::Dead),
2342 ("TimedOut", JobStatus::TimedOut),
2343 ("\"TimedOut\"", JobStatus::TimedOut),
2344 ("Retrying", JobStatus::Retrying),
2345 ("\"Retrying\"", JobStatus::Retrying),
2346 ("Archived", JobStatus::Archived),
2347 ("\"Archived\"", JobStatus::Archived),
2348 ];
2349
2350 for (input, expected) in &test_cases {
2351 // This is the same logic used in our Decode implementations
2352 let cleaned_str = input.trim_matches('"');
2353 let parsed_status = match cleaned_str {
2354 "Pending" => JobStatus::Pending,
2355 "Running" => JobStatus::Running,
2356 "Completed" => JobStatus::Completed,
2357 "Failed" => JobStatus::Failed,
2358 "Dead" => JobStatus::Dead,
2359 "TimedOut" => JobStatus::TimedOut,
2360 "Retrying" => JobStatus::Retrying,
2361 "Archived" => JobStatus::Archived,
2362 _ => panic!("Unknown job status: {}", input),
2363 };
2364
2365 assert_eq!(
2366 *expected, parsed_status,
2367 "Failed to parse '{}' correctly",
2368 input
2369 );
2370 }
2371 }
2372
2373 #[test]
2374 fn test_job_status_encoding_logic() {
2375 // Verify that our encoding logic produces the expected unquoted strings
2376 let statuses = [
2377 (JobStatus::Pending, "Pending"),
2378 (JobStatus::Running, "Running"),
2379 (JobStatus::Completed, "Completed"),
2380 (JobStatus::Failed, "Failed"),
2381 (JobStatus::Dead, "Dead"),
2382 (JobStatus::TimedOut, "TimedOut"),
2383 (JobStatus::Retrying, "Retrying"),
2384 (JobStatus::Archived, "Archived"),
2385 ];
2386
2387 for (status, expected_str) in &statuses {
2388 // This matches the logic in our Encode implementations
2389 let encoded_str = match status {
2390 JobStatus::Pending => "Pending",
2391 JobStatus::Running => "Running",
2392 JobStatus::Completed => "Completed",
2393 JobStatus::Failed => "Failed",
2394 JobStatus::Dead => "Dead",
2395 JobStatus::TimedOut => "TimedOut",
2396 JobStatus::Retrying => "Retrying",
2397 JobStatus::Archived => "Archived",
2398 };
2399
2400 assert_eq!(
2401 *expected_str, encoded_str,
2402 "Encoding mismatch for {:?}",
2403 status
2404 );
2405
2406 // Verify the encoded string does not have quotes
2407 assert!(
2408 !encoded_str.starts_with('"'),
2409 "Encoded string should not start with quotes: {}",
2410 encoded_str
2411 );
2412 assert!(
2413 !encoded_str.ends_with('"'),
2414 "Encoded string should not end with quotes: {}",
2415 encoded_str
2416 );
2417 }
2418 }
2419
2420 #[test]
2421 fn test_status_as_str_for_every_status() {
2422 for (status, name) in [
2423 (JobStatus::Pending, "Pending"),
2424 (JobStatus::Running, "Running"),
2425 (JobStatus::Completed, "Completed"),
2426 (JobStatus::Failed, "Failed"),
2427 (JobStatus::Dead, "Dead"),
2428 (JobStatus::TimedOut, "TimedOut"),
2429 (JobStatus::Retrying, "Retrying"),
2430 (JobStatus::Archived, "Archived"),
2431 ] {
2432 assert_eq!(status.as_str(), name);
2433 }
2434 }
2435
2436 #[test]
2437 fn test_result_config_builders() {
2438 let config = ResultConfig::new(ResultStorage::Database)
2439 .with_ttl(std::time::Duration::from_secs(60))
2440 .with_max_size(1024);
2441 assert_eq!(config.ttl, Some(std::time::Duration::from_secs(60)));
2442 assert_eq!(config.max_size_bytes, Some(1024));
2443 assert_eq!(ResultConfig::default().storage, ResultStorage::None);
2444
2445 let job = Job::new("q".to_string(), json!({}));
2446 assert!(!job.has_result_storage());
2447 assert!(!job.has_result_data());
2448 let mut job = job.with_result_config(config);
2449 assert!(job.has_result_storage());
2450 job.result_data = Some(json!({"ok": true}));
2451 assert!(job.has_result_data());
2452 }
2453
2454 #[test]
2455 fn test_backoff_builders_set_the_retry_strategy() {
2456 use std::time::Duration;
2457 let job = Job::new("q".to_string(), json!({})).with_exponential_backoff(
2458 Duration::from_secs(1),
2459 2.0,
2460 Duration::from_secs(60),
2461 );
2462 assert_eq!(
2463 job.retry_strategy,
2464 Some(RetryStrategy::exponential(
2465 Duration::from_secs(1),
2466 2.0,
2467 Some(Duration::from_secs(60))
2468 ))
2469 );
2470 let job = Job::new("q".to_string(), json!({})).with_linear_backoff(
2471 Duration::from_secs(1),
2472 Duration::from_secs(2),
2473 None,
2474 );
2475 assert_eq!(
2476 job.retry_strategy.as_ref().unwrap().calculate_delay(3),
2477 Duration::from_secs(7),
2478 "base + 3 * increment"
2479 );
2480 let job = Job::new("q".to_string(), json!({}))
2481 .with_fibonacci_backoff(Duration::from_secs(1), Some(Duration::from_secs(4)));
2482 assert_eq!(
2483 job.retry_strategy.as_ref().unwrap().calculate_delay(10),
2484 Duration::from_secs(4),
2485 "capped at max_delay"
2486 );
2487 }
2488
2489 #[test]
2490 fn test_dependency_and_workflow_predicates() {
2491 let a = Job::new("q".to_string(), json!({}));
2492 let b = Job::new("q".to_string(), json!({}));
2493 assert!(!a.has_dependencies());
2494 assert!(a.dependencies_satisfied());
2495 assert!(!a.dependencies_failed());
2496 assert_eq!(a.dependency_count(), 0);
2497 assert_eq!(a.dependent_count(), 0);
2498
2499 let mut c = Job::new("q".to_string(), json!({})).depends_on_jobs(&[a.id, b.id]);
2500 assert!(c.has_dependencies());
2501 assert_eq!(c.dependency_count(), 2);
2502 assert!(!c.dependencies_satisfied());
2503 c.dependency_status = crate::workflow::DependencyStatus::Satisfied;
2504 assert!(c.dependencies_satisfied());
2505 c.dependency_status = crate::workflow::DependencyStatus::Failed;
2506 assert!(c.dependencies_failed());
2507 assert!(!c.dependencies_satisfied());
2508
2509 // An empty list does not make a job wait.
2510 let d = Job::new("q".to_string(), json!({})).depends_on_jobs(&[]);
2511 assert!(d.dependencies_satisfied());
2512
2513 let mut e = Job::new("q".to_string(), json!({}));
2514 e.dependents.push(a.id);
2515 assert_eq!(e.dependent_count(), 1);
2516 assert!(!e.is_part_of_workflow());
2517 let e = e.with_workflow(uuid::Uuid::new_v4(), "flow");
2518 assert!(e.is_part_of_workflow());
2519 assert_eq!(e.workflow_name.as_deref(), Some("flow"));
2520 }
2521
2522 #[test]
2523 fn test_tracing_helpers() {
2524 let job = Job::new("q".to_string(), json!({}));
2525 assert!(!job.has_tracing_info());
2526 assert_eq!(job.get_trace_id(), None);
2527 assert_eq!(job.get_correlation_id(), None);
2528
2529 let job = job.with_tracing_id("op-1");
2530 assert!(job.has_tracing_info());
2531 assert_eq!(job.get_trace_id(), Some("op-1"));
2532 assert_eq!(job.get_correlation_id(), Some("op-1"));
2533
2534 for job in [
2535 Job::new("q".to_string(), json!({})).with_span_context("ctx"),
2536 Job::new("q".to_string(), json!({})).with_parent_span_id("p"),
2537 Job::new("q".to_string(), json!({})).with_correlation_id("c"),
2538 ] {
2539 assert!(job.has_tracing_info());
2540 }
2541 assert_eq!(
2542 Job::new("q".to_string(), json!({}))
2543 .with_span_context("ctx")
2544 .span_context
2545 .as_deref(),
2546 Some("ctx")
2547 );
2548 }
2549
2550 #[test]
2551 fn test_next_run_needs_a_valid_schedule_on_a_recurring_job() {
2552 let mut job = Job::new("q".to_string(), json!({}));
2553 assert_eq!(job.calculate_next_run(), None, "not recurring");
2554 assert_eq!(job.prepare_for_next_run(), None);
2555
2556 job.recurring = true;
2557 assert_eq!(job.calculate_next_run(), None, "no schedule");
2558 job.cron_schedule = Some("not a cron expression".to_string());
2559 assert!(matches!(job.get_cron_schedule(), Some(Err(_))));
2560 assert_eq!(job.calculate_next_run(), None, "invalid schedule");
2561
2562 job.cron_schedule = Some("0 0 * * * *".to_string());
2563 job.timezone = Some("America/New_York".to_string());
2564 let next = job.calculate_next_run().expect("next run");
2565 assert!(next > chrono::Utc::now());
2566 }
2567
2568 #[cfg(feature = "encryption")]
2569 #[test]
2570 fn test_encryption_accessors_and_cleanup() {
2571 use crate::encryption::{EncryptionAlgorithm, EncryptionConfig, RetentionPolicy};
2572
2573 let job = Job::new("q".to_string(), json!({}));
2574 assert!(!job.has_pii_fields());
2575 assert!(!job.is_payload_encrypted());
2576 assert!(job.get_pii_fields().is_empty());
2577 assert!(job.get_encryption_config().is_none());
2578 assert!(job.get_retention_policy().is_none());
2579 assert!(!job.should_cleanup_encrypted_data());
2580
2581 let config = EncryptionConfig::new(EncryptionAlgorithm::ChaCha20Poly1305);
2582 let job = Job::new("q".to_string(), json!({}))
2583 .with_encryption(config.clone())
2584 .with_pii_fields(vec!["ssn"])
2585 .with_retention_policy(RetentionPolicy::DeleteAt(
2586 chrono::Utc::now() - chrono::Duration::minutes(1),
2587 ));
2588 assert!(job.has_pii_fields());
2589 assert_eq!(job.get_pii_fields(), ["ssn".to_string()]);
2590 assert_eq!(
2591 job.get_encryption_config().unwrap().algorithm,
2592 EncryptionAlgorithm::ChaCha20Poly1305
2593 );
2594 assert!(job.should_cleanup_encrypted_data(), "retention ended");
2595
2596 let kept = Job::new("q".to_string(), json!({}))
2597 .with_retention_policy(RetentionPolicy::KeepIndefinitely);
2598 assert!(!kept.should_cleanup_encrypted_data());
2599 assert_eq!(
2600 kept.get_retention_policy(),
2601 Some(&RetentionPolicy::KeepIndefinitely)
2602 );
2603 }
2604}