1use crate::{ProviderError, ProviderResult};
12use appcore_contracts::{CapabilityId, CoreId, JobId};
13
14#[derive(Clone, PartialEq, Eq)]
16pub struct JobSpec {
17 job_id: JobId,
18 capability: CapabilityId,
19 payload_reference: String,
20 available_at_ms: u64,
21 max_attempts: u32,
22}
23
24impl std::fmt::Debug for JobSpec {
25 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 formatter
27 .debug_struct("JobSpec")
28 .field("job_id", &self.job_id)
29 .field("capability", &self.capability)
30 .field("payload_reference", &"REDACTED")
31 .field("available_at_ms", &self.available_at_ms)
32 .field("max_attempts", &self.max_attempts)
33 .finish()
34 }
35}
36
37impl JobSpec {
38 pub fn new(
40 job_id: JobId,
41 capability: CapabilityId,
42 payload_reference: impl Into<String>,
43 available_at_ms: u64,
44 max_attempts: u32,
45 ) -> ProviderResult<Self> {
46 let payload_reference = payload_reference.into();
47 validate_payload_reference(&payload_reference)?;
48 if max_attempts == 0 {
49 return Err(ProviderError::InvalidConfiguration(
50 "job max_attempts must be greater than zero".to_string(),
51 ));
52 }
53 Ok(Self {
54 job_id,
55 capability,
56 payload_reference,
57 available_at_ms,
58 max_attempts,
59 })
60 }
61
62 pub fn job_id(&self) -> &JobId {
64 &self.job_id
65 }
66
67 pub fn capability(&self) -> &CapabilityId {
69 &self.capability
70 }
71
72 pub fn payload_reference(&self) -> &str {
74 &self.payload_reference
75 }
76
77 pub fn available_at_ms(&self) -> u64 {
79 self.available_at_ms
80 }
81
82 pub fn max_attempts(&self) -> u32 {
84 self.max_attempts
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct JobLease {
91 job_id: JobId,
92 holder_core_id: CoreId,
93 epoch: u64,
94 expires_at_ms: u64,
95}
96
97impl JobLease {
98 pub fn new(
100 job_id: JobId,
101 holder_core_id: CoreId,
102 epoch: u64,
103 expires_at_ms: u64,
104 ) -> ProviderResult<Self> {
105 if epoch == 0 || expires_at_ms == 0 {
106 return Err(ProviderError::InvalidConfiguration(
107 "job lease epoch and expiration must be greater than zero".to_string(),
108 ));
109 }
110 Ok(Self {
111 job_id,
112 holder_core_id,
113 epoch,
114 expires_at_ms,
115 })
116 }
117
118 pub fn job_id(&self) -> &JobId {
120 &self.job_id
121 }
122
123 pub fn holder_core_id(&self) -> &CoreId {
125 &self.holder_core_id
126 }
127
128 pub fn epoch(&self) -> u64 {
130 self.epoch
131 }
132
133 pub fn expires_at_ms(&self) -> u64 {
135 self.expires_at_ms
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum JobCompletion {
142 Completed,
144 Failed,
146 RetryAt(u64),
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum JobAtomicity {
153 FencedCompareAndSwap,
155}
156
157pub trait JobProvider: Send + Sync {
165 fn atomicity(&self) -> JobAtomicity {
167 JobAtomicity::FencedCompareAndSwap
168 }
169
170 fn submit(&self, job: JobSpec) -> ProviderResult<()>;
172
173 fn claim(
175 &self,
176 capability: &CapabilityId,
177 holder_core_id: &CoreId,
178 now_ms: u64,
179 lease_duration_ms: u64,
180 ) -> ProviderResult<Option<JobLease>>;
181
182 fn complete(&self, lease: &JobLease, completion: JobCompletion) -> ProviderResult<()>;
184}
185
186fn validate_payload_reference(reference: &str) -> ProviderResult<()> {
187 if reference.trim().is_empty()
188 || reference.len() > 2_048
189 || reference.chars().any(char::is_control)
190 {
191 return Err(ProviderError::InvalidConfiguration(
192 "job payload reference is invalid".to_string(),
193 ));
194 }
195 Ok(())
196}