1use std::sync::{Arc, Mutex};
4
5use khive_db::stores::{
6 entity::{entity_insert_if_absent_statement, entity_replace_if_unchanged_statement},
7 event::event_insert_statements,
8};
9use khive_storage::{Entity, Event, SqlAccess, SqlStatement, SqlValue, SqlWriter, StorageError};
10use khive_types::{EventKind, EventOutcome, SubstrateKind};
11
12use super::entity_admission::{observe, PreparedEntityExemption};
13use super::log_sink::{FinalizerAuditGap, LogSink, TracingLogSink};
14use super::outcome::{
15 ExemptionCommit, FailureClass, FailureDiagnostic, FinalizerOutcome, Substrate,
16};
17use crate::atomic_plan::{AffectedRowGuard, PlanStatement, PostCommitEffect};
18use crate::atomic_runner::AtomicOpFailure;
19use crate::entity_write::EntityWriteGuard;
20use crate::{RuntimeError, RuntimeResult};
21
22#[derive(Debug, Clone)]
24#[allow(clippy::large_enum_variant)] pub enum EntityCandidateMutation {
26 CreateIfAbsent,
27 ReplaceIfUnchanged {
28 expected: Entity,
29 expected_version: Option<i64>,
30 },
31}
32
33#[derive(Debug)]
34enum RecordedFailure {
35 Contention,
36 Finalization {
37 diagnostic: FailureDiagnostic,
38 source: Option<Box<StorageError>>,
39 },
40}
41
42#[derive(Debug)]
45pub struct EntityFinalizationPlan {
46 prepared: PreparedEntityExemption,
47 row: SqlStatement,
48 guard: Option<EntityWriteGuard>,
49 required: Vec<PlanStatement>,
50 success_audit: Vec<SqlStatement>,
51 post_commit: PostCommitEffect,
52 replacing: bool,
53 committed_version: i64,
54 failure: Arc<Mutex<Option<RecordedFailure>>>,
55}
56
57impl Clone for EntityFinalizationPlan {
58 fn clone(&self) -> Self {
59 let mut plan = self.for_settlement();
60 plan.failure = Arc::new(Mutex::new(None));
62 plan
63 }
64}
65
66impl PreparedEntityExemption {
67 pub(crate) fn into_plan(
68 self,
69 mutation: EntityCandidateMutation,
70 required: Vec<PlanStatement>,
71 post_commit: PostCommitEffect,
72 ) -> RuntimeResult<EntityFinalizationPlan> {
73 let (row, guard, replacing, committed_version) = match mutation {
74 EntityCandidateMutation::CreateIfAbsent => (
75 entity_insert_if_absent_statement(&self.entity),
76 None,
77 false,
78 1,
79 ),
80 EntityCandidateMutation::ReplaceIfUnchanged {
81 expected,
82 expected_version,
83 } => {
84 crate::entity_write::validate_expected_version(expected_version)?;
85 if expected.id != self.entity.id
86 || expected.version != self.entity.version
87 || expected.namespace != self.entity.namespace
88 || expected.created_at != self.entity.created_at
89 {
90 return Err(RuntimeError::InvalidInput(
91 "entity finalization CAS snapshot mismatch".into(),
92 ));
93 }
94 let committed_version = expected
95 .version
96 .checked_add(1)
97 .ok_or_else(|| RuntimeError::InvalidInput("entity version exhausted".into()))?;
98 let mut row = entity_replace_if_unchanged_statement(
99 &self.entity,
100 expected.updated_at,
101 expected.deleted_at,
102 );
103 row.sql
106 .push_str(&format!(" AND namespace = ?{}", row.params.len() + 1));
107 row.params.push(SqlValue::Text(expected.namespace));
108 row.sql
109 .push_str(&format!(" AND created_at = ?{}", row.params.len() + 1));
110 row.params.push(SqlValue::Integer(expected.created_at));
111 let guard = expected_version.map(|expected_version| EntityWriteGuard {
112 id: expected.id,
113 expected_version,
114 });
115 (row, guard, true, committed_version)
116 }
117 };
118 let event = self.attribution.stamp(
119 Event::new(
120 &self.entity.namespace,
121 self.entry.verb(),
122 EventKind::Audit,
123 SubstrateKind::Entity,
124 "",
125 )
126 .with_target(self.entity.id)
127 .with_payload(serde_json::json!({
128 "mechanism": "content-sha256-manifest-v1",
129 "digest_sha256": self.digest_sha256,
130 "field_scope": self.field_scope,
131 "manifest_id": self.snapshot.manifest_id(),
132 "canonical_verb": self.entry.verb(),
133 "namespace": self.entity.namespace,
134 "overridden_detector": self.overridden_detector,
135 "outcome": "exempted",
136 "record_id": self.entity.id,
137 "entry_point": self.entry.family(replacing),
138 })),
139 );
140 let mut event = event;
141 event.payload["actor"] = event.actor.clone().into();
142 let success_audit = event_insert_statements(&event).map_err(|error| {
143 RuntimeError::Internal(format!("finalizer event preparation: {error}"))
144 })?;
145 Ok(EntityFinalizationPlan {
146 prepared: self,
147 row,
148 guard,
149 required,
150 success_audit,
151 post_commit,
152 replacing,
153 committed_version,
154 failure: Arc::new(Mutex::new(None)),
155 })
156 }
157}
158
159impl EntityFinalizationPlan {
160 pub(crate) fn for_settlement(&self) -> Self {
161 Self {
162 prepared: self.prepared.clone(),
163 row: self.row.clone(),
164 guard: self.guard.clone(),
165 required: self.required.clone(),
166 success_audit: self.success_audit.clone(),
167 post_commit: self.post_commit.clone(),
168 replacing: self.replacing,
169 committed_version: self.committed_version,
170 failure: Arc::clone(&self.failure),
171 }
172 }
173
174 pub(crate) fn committed_entity(&self) -> Entity {
175 let mut entity = self.prepared.entity.clone();
176 entity.version = self.committed_version;
177 entity
178 }
179
180 pub(crate) async fn apply(
181 &self,
182 writer: &mut dyn SqlWriter,
183 ) -> Result<Option<PostCommitEffect>, AtomicOpFailure> {
184 if let Some(guard) = &self.guard {
185 if let Some(conflict) = guard
186 .check(writer)
187 .await
188 .map_err(|source| self.fail(FailureClass::RecordWrite, Some(source)))?
189 {
190 self.contention();
191 return Err(AtomicOpFailure::EntityConflict(conflict));
192 }
193 }
194 if super::faults::consume_record_write_fail(&self.prepared.entity.namespace) {
195 return Err(self.fail(FailureClass::RecordWrite, None));
196 }
197 let affected = writer
198 .execute(self.row.clone())
199 .await
200 .map_err(|source| self.fail(FailureClass::RecordWrite, Some(source)))?;
201 if affected != 1 {
202 self.contention();
203 return Err(AtomicOpFailure::GuardFailed {
204 statement_label: self.row.label.clone(),
205 expected: AffectedRowGuard::exactly(1),
206 observed: affected,
207 });
208 }
209 if super::faults::consume_stamp_fail(&self.prepared.entity.namespace) {
212 return Err(self.fail(FailureClass::Stamp, None));
213 }
214 for statement in &self.required {
215 let affected = writer
216 .execute(statement.statement.clone())
217 .await
218 .map_err(|source| self.fail(FailureClass::RecordWrite, Some(source)))?;
219 if statement
220 .guard
221 .is_some_and(|guard| !guard.holds_for(affected))
222 {
223 return Err(self.fail(FailureClass::RecordWrite, None));
224 }
225 }
226 if super::faults::consume_success_audit_fail(&self.prepared.entity.namespace) {
227 return Err(self.fail(FailureClass::SuccessAudit, None));
228 }
229 for statement in &self.success_audit {
230 let affected = writer
231 .execute(statement.clone())
232 .await
233 .map_err(|source| self.fail(FailureClass::SuccessAudit, Some(source)))?;
234 if affected != 1 {
235 return Err(self.fail(FailureClass::SuccessAudit, None));
236 }
237 }
238 Ok((self.post_commit != PostCommitEffect::None).then(|| self.post_commit.clone()))
239 }
240
241 fn contention(&self) {
242 *self
243 .failure
244 .lock()
245 .expect("entity finalizer failure lock poisoned") = Some(RecordedFailure::Contention);
246 }
247
248 fn fail(&self, class: FailureClass, source: Option<StorageError>) -> AtomicOpFailure {
249 let diagnostic = self.diagnostic(class);
250 *self
251 .failure
252 .lock()
253 .expect("entity finalizer failure lock poisoned") =
254 Some(RecordedFailure::Finalization {
255 diagnostic,
256 source: source.map(Box::new),
257 });
258 AtomicOpFailure::SqlError {
259 statement_label: Some("entity-manifest-finalization".into()),
260 message: format!("entity manifest finalization failed at {class:?}"),
261 }
262 }
263
264 fn diagnostic(&self, class: FailureClass) -> FailureDiagnostic {
265 FailureDiagnostic::new(
266 self.prepared.entity.id,
267 Substrate::Entity,
268 &self.prepared.entity.namespace,
269 self.prepared.entry.family(self.replacing),
270 class,
271 )
272 }
273
274 pub(crate) fn finish_commit(&self) {
275 observe(
276 &self.prepared.entity.namespace,
277 FinalizerOutcome::Exempted(ExemptionCommit {
278 record_id: self.prepared.entity.id,
279 substrate: Substrate::Entity,
280 entry_point: self.prepared.entry.family(self.replacing),
281 digest_sha256: self.prepared.digest_sha256.clone(),
282 field_scope: self.prepared.field_scope,
283 manifest_id: self.prepared.snapshot.manifest_id().into(),
284 }),
285 );
286 }
287
288 pub(crate) async fn finish_rollback(&self, access: &dyn SqlAccess) -> Option<StorageError> {
291 let recorded = self
292 .failure
293 .lock()
294 .expect("entity finalizer failure lock poisoned")
295 .take();
296 let (diagnostic, source) = match recorded {
297 Some(RecordedFailure::Contention) => return None,
298 Some(RecordedFailure::Finalization { diagnostic, source }) => (diagnostic, source),
299 None => (self.diagnostic(FailureClass::RecordWrite), None),
300 };
301 let outcome = match diagnostic.failure_class {
302 FailureClass::RecordWrite => FinalizerOutcome::RecordWriteFailed(diagnostic.clone()),
303 FailureClass::Stamp => FinalizerOutcome::StampFailed(diagnostic.clone()),
304 FailureClass::SuccessAudit => FinalizerOutcome::AuditFailed(diagnostic.clone()),
305 };
306 observe(&diagnostic.namespace, outcome);
307 let event = self.prepared.attribution.stamp(Event::new(
308 &diagnostic.namespace, self.prepared.entry.verb(), EventKind::Audit, SubstrateKind::Entity, "",
309 ).with_target(diagnostic.record_id).with_outcome(EventOutcome::Error).with_payload(serde_json::json!({
310 "mechanism": "content-sha256-manifest-v1", "outcome": match diagnostic.failure_class {
311 FailureClass::RecordWrite => "record-write-failed",
312 FailureClass::Stamp => "stamp-failed",
313 FailureClass::SuccessAudit => "audit-failed",
314 },
315 "diagnostic_id": diagnostic.diagnostic_id, "record_id": diagnostic.record_id,
316 "entry_point": diagnostic.entry_point, "failure_class": format!("{:?}", diagnostic.failure_class),
317 })));
318 let failed = if super::faults::consume_failure_audit_fail(&diagnostic.namespace) {
319 true
320 } else {
321 match event_insert_statements(&event) {
322 Err(_) => true,
323 Ok(statements) => access
324 .atomic_unit(Box::new(move |writer| {
325 Box::pin(async move {
326 for statement in statements {
327 if writer.execute(statement).await? != 1 {
328 return Err(StorageError::Internal(
329 "entity finalizer failure audit was not persisted".into(),
330 ));
331 }
332 }
333 Ok(Box::new(()) as Box<dyn std::any::Any + Send>)
334 })
335 }))
336 .await
337 .is_err(),
338 }
339 };
340 if failed {
341 let gap = FinalizerAuditGap {
342 failure_class: diagnostic.failure_class,
343 diagnostic_id: diagnostic.diagnostic_id,
344 record_id: diagnostic.record_id,
345 substrate: diagnostic.substrate,
346 namespace: diagnostic.namespace,
347 entry_point: diagnostic.entry_point,
348 };
349 TracingLogSink.record_audit_gap(&gap);
350 #[cfg(test)]
351 super::entity_admission::fixture::record_gap(gap);
352 }
353 source.map(|source| *source)
354 }
355}