1use std::fmt;
8use std::sync::Arc;
9
10use crate::{
11 ActorRef, Clock, JobInstanceId, JobRepository, OperationId, PurgeCounts, PurgePlan,
12 PurgePlanRequest, ReasonCode, RetentionAction, RetentionError, RetentionHold, RetentionOutcome,
13 RetentionRecord, RetentionRecordDraft, TelemetryEventKind, TelemetryEventSink, TelemetryRecord,
14};
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct RetentionReport {
19 outcome: RetentionOutcome,
20 record: RetentionRecord,
21 hold: Option<RetentionHold>,
22}
23
24impl RetentionReport {
25 const fn new(
26 outcome: RetentionOutcome,
27 record: RetentionRecord,
28 hold: Option<RetentionHold>,
29 ) -> Self {
30 Self {
31 outcome,
32 record,
33 hold,
34 }
35 }
36
37 #[must_use]
39 pub const fn outcome(&self) -> RetentionOutcome {
40 self.outcome
41 }
42
43 #[must_use]
45 pub const fn record(&self) -> &RetentionRecord {
46 &self.record
47 }
48
49 #[must_use]
51 pub const fn hold(&self) -> Option<&RetentionHold> {
52 self.hold.as_ref()
53 }
54
55 #[must_use]
57 pub const fn counts(&self) -> PurgeCounts {
58 self.record.counts()
59 }
60}
61
62#[derive(Clone)]
69pub struct RetentionService<R> {
70 repository: R,
71 clock: Arc<dyn Clock>,
72 event_sinks: Vec<Arc<dyn TelemetryEventSink>>,
73}
74
75impl<R> fmt::Debug for RetentionService<R> {
76 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77 formatter
78 .debug_struct("RetentionService")
79 .finish_non_exhaustive()
80 }
81}
82
83impl<R: JobRepository> RetentionService<R> {
84 pub const fn new(repository: R, clock: Arc<dyn Clock>) -> Self {
86 Self {
87 repository,
88 clock,
89 event_sinks: Vec::new(),
90 }
91 }
92
93 #[must_use]
95 pub fn with_event_sink(mut self, sink: Arc<dyn TelemetryEventSink>) -> Self {
96 self.event_sinks.push(sink);
97 self
98 }
99
100 pub const fn repository(&self) -> &R {
102 &self.repository
103 }
104
105 pub async fn hold(
111 &self,
112 job_instance_id: JobInstanceId,
113 ) -> Result<Option<RetentionHold>, RetentionError> {
114 let mut unit = self.repository.begin().await?;
115 let hold = unit.job_instance_hold(job_instance_id).await?;
116 unit.rollback().await?;
117 Ok(hold)
118 }
119
120 pub async fn place_hold(
127 &self,
128 operation_id: OperationId,
129 actor: ActorRef,
130 reason: ReasonCode,
131 job_instance_id: JobInstanceId,
132 ) -> Result<RetentionReport, RetentionError> {
133 let result = self
134 .hold_action(
135 RetentionAction::Hold,
136 operation_id,
137 actor,
138 reason,
139 job_instance_id,
140 )
141 .await;
142 if let Ok(report) = &result {
143 self.emit_report(report);
144 }
145 result
146 }
147
148 pub async fn release_hold(
155 &self,
156 operation_id: OperationId,
157 actor: ActorRef,
158 reason: ReasonCode,
159 job_instance_id: JobInstanceId,
160 ) -> Result<RetentionReport, RetentionError> {
161 let result = self
162 .hold_action(
163 RetentionAction::ReleaseHold,
164 operation_id,
165 actor,
166 reason,
167 job_instance_id,
168 )
169 .await;
170 if let Ok(report) = &result {
171 self.emit_report(report);
172 }
173 result
174 }
175
176 async fn hold_action(
177 &self,
178 action: RetentionAction,
179 operation_id: OperationId,
180 actor: ActorRef,
181 reason: ReasonCode,
182 job_instance_id: JobInstanceId,
183 ) -> Result<RetentionReport, RetentionError> {
184 if let Some(record) = self.replay(action, &operation_id).await? {
185 return Ok(RetentionReport::new(
186 RetentionOutcome::Replayed,
187 record,
188 None,
189 ));
190 }
191 let applied_at = self.clock.now();
192 let mut unit = self.repository.begin().await?;
193 let hold = match action {
194 RetentionAction::Hold => Some(
195 unit.place_instance_hold(job_instance_id, &actor, &reason, applied_at)
196 .await?,
197 ),
198 RetentionAction::ReleaseHold => {
199 unit.release_instance_hold(job_instance_id).await?;
200 None
201 }
202 _ => None,
206 };
207 let draft = RetentionRecordDraft::instance_action(
208 action,
209 operation_id,
210 actor,
211 reason,
212 job_instance_id,
213 applied_at,
214 );
215 let record = unit.append_retention_action(&draft).await?;
216 unit.commit().await?;
217 Ok(RetentionReport::new(
218 RetentionOutcome::Applied,
219 record,
220 hold,
221 ))
222 }
223
224 pub async fn plan_purge(
235 &self,
236 request: &PurgePlanRequest,
237 ) -> Result<PurgePlan, RetentionError> {
238 let mut unit = self.repository.begin().await?;
239 let survey = unit.purge_survey(request).await?;
240 unit.rollback().await?;
241 let plan = PurgePlan::new(request.clone(), survey);
242 self.emit_record(&TelemetryRecord::retention(
243 TelemetryEventKind::RetentionPlanned,
244 None,
245 None,
246 PurgeCounts::default(),
247 ));
248 Ok(plan)
249 }
250
251 pub async fn apply_purge(
264 &self,
265 operation_id: OperationId,
266 actor: ActorRef,
267 reason: ReasonCode,
268 plan: &PurgePlan,
269 ) -> Result<RetentionReport, RetentionError> {
270 if let Some(record) = self
271 .replay(RetentionAction::ApplyPurge, &operation_id)
272 .await?
273 {
274 let report = RetentionReport::new(RetentionOutcome::Replayed, record, None);
275 self.emit_report(&report);
276 return Ok(report);
277 }
278 let applied_at = self.clock.now();
279 let mut unit = self.repository.begin().await?;
280 let counts = unit.apply_purge(plan).await?;
281 let draft = RetentionRecordDraft::purge(
282 operation_id,
283 actor,
284 reason,
285 *plan.digest(),
286 counts,
287 plan.request().batch(),
288 applied_at,
289 );
290 let record = unit.append_retention_action(&draft).await?;
291 unit.commit().await?;
292 let report = RetentionReport::new(RetentionOutcome::Applied, record, None);
293 self.emit_report(&report);
294 Ok(report)
295 }
296
297 async fn replay(
298 &self,
299 action: RetentionAction,
300 operation_id: &OperationId,
301 ) -> Result<Option<RetentionRecord>, RetentionError> {
302 let mut unit = self.repository.begin().await?;
303 let recorded = unit.find_retention_action(action, operation_id).await?;
304 unit.rollback().await?;
305 Ok(recorded)
306 }
307
308 fn emit_report(&self, report: &RetentionReport) {
309 self.emit_record(&TelemetryRecord::retention(
310 TelemetryEventKind::RetentionApplied,
311 Some(report.record().action()),
312 Some(report.outcome()),
313 report.counts(),
314 ));
315 }
316
317 fn emit_record(&self, record: &TelemetryRecord) {
318 for sink in &self.event_sinks {
319 crate::telemetry::emit_safely(Some(sink), record);
320 }
321 }
322}