Skip to main content

radixdb_api/
application.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8
9//! Public audit and transactional-outbox lifecycle.
10//!
11//! These handles are thin connection/transaction facades over ordinary
12//! RadixDB MVCC relations. They do not own a second log or transaction model.
13
14use chrono::{DateTime, Duration, Utc};
15use radixdb_core::Result;
16
17pub use radixdb_executor::application::{
18    ApplicationRelationIdentity, ApplicationRetentionOutcome, ApplicationRetentionPolicy,
19    AuditEvent, ObjectId, OutboxClaim, OutboxCompletion, OutboxMessage, OutboxRetryDisposition,
20    AUDIT_RELATION_NAME, OUTBOX_RELATION_NAME,
21};
22use radixdb_executor::context::ExecutionContext;
23
24use crate::{Database, Transaction};
25
26impl Database {
27    /// Explicitly install the system-owned audit and outbox relation contract.
28    /// Database open never performs this catalog mutation implicitly.
29    pub fn install_application_relations(&self) -> Result<ApplicationRelationIdentity> {
30        self.with_connection_executor(|executor| executor.install_application_relations())
31    }
32
33    /// Resolve the already-installed relation identities without mutation.
34    pub fn application_relation_identity(&self) -> Result<ApplicationRelationIdentity> {
35        self.with_connection_executor(|executor| executor.application_relation_identity())
36    }
37
38    /// Atomically claim a bounded outbox batch and publish leases only after
39    /// that claim transaction commits.
40    pub fn claim_outbox(
41        &self,
42        worker_id: &str,
43        now: DateTime<Utc>,
44        lease_duration: Duration,
45        limit: usize,
46        max_attempts: u32,
47    ) -> Result<Vec<OutboxClaim>> {
48        self.with_connection_executor(|executor| {
49            executor.claim_outbox(worker_id, now, lease_duration, limit, max_attempts)
50        })
51    }
52
53    /// Mark an externally delivered message complete with its durable lease.
54    pub fn complete_outbox(
55        &self,
56        message_id: [u8; 16],
57        lease_token: [u8; 16],
58        completed_at: DateTime<Utc>,
59    ) -> Result<OutboxCompletion> {
60        self.with_connection_executor(|executor| {
61            executor.complete_outbox(message_id, lease_token, completed_at)
62        })
63    }
64
65    /// Release a failed delivery for retry or move its exhausted attempt to
66    /// the dead-letter state.
67    pub fn retry_outbox(
68        &self,
69        message_id: [u8; 16],
70        lease_token: [u8; 16],
71        failed_at: DateTime<Utc>,
72        retry_at: DateTime<Utc>,
73        error: &str,
74        max_attempts: u32,
75    ) -> Result<OutboxRetryDisposition> {
76        self.with_connection_executor(|executor| {
77            executor.retry_outbox(
78                message_id,
79                lease_token,
80                failed_at,
81                retry_at,
82                error,
83                max_attempts,
84            )
85        })
86    }
87
88    /// Perform one bounded retention pass over immutable audit history and
89    /// terminal outbox rows.
90    pub fn prune_application_history(
91        &self,
92        now: DateTime<Utc>,
93        policy: ApplicationRetentionPolicy,
94    ) -> Result<ApplicationRetentionOutcome> {
95        self.with_connection_executor(|executor| executor.prune_application_history(now, policy))
96    }
97}
98
99impl Transaction {
100    /// Append one audit event to this transaction. Embedded callers act as the
101    /// bootstrap principal; server/procedural execution uses its authenticated
102    /// execution context inside the executor host bridge.
103    pub fn append_audit_event(&mut self, event: AuditEvent) -> Result<[u8; 16]> {
104        self.check_active()?;
105        self.executor()
106            .append_audit_event(&ExecutionContext::new(), event)
107    }
108
109    /// Append one external side-effect intent to this business transaction.
110    pub fn append_outbox_message(&mut self, message: OutboxMessage) -> Result<[u8; 16]> {
111        self.check_active()?;
112        self.executor().append_outbox_message(message)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use radixdb_core::Value;
119
120    use super::*;
121
122    #[test]
123    fn public_facade_keeps_business_audit_and_outbox_in_one_transaction() {
124        let database = Database::open_in_memory().unwrap();
125        database.install_application_relations().unwrap();
126        database
127            .execute("CREATE TABLE business_record (id INTEGER PRIMARY KEY)", ())
128            .unwrap();
129
130        let mut transaction = database.begin().unwrap();
131        transaction
132            .execute("INSERT INTO business_record VALUES (1)", ())
133            .unwrap();
134        transaction
135            .append_audit_event(AuditEvent {
136                object_id: ObjectId::BOOTSTRAP_NAMESPACE,
137                command_fingerprint: [3; 32],
138                metadata: Value::json(r#"{"operation":"create"}"#),
139            })
140            .unwrap();
141        transaction
142            .append_outbox_message(OutboxMessage {
143                idempotency_key: "business-record-1".to_owned(),
144                schema_version: 1,
145                payload: Value::json(r#"{"id":1}"#),
146            })
147            .unwrap();
148        transaction.commit().unwrap();
149
150        assert_eq!(
151            database
152                .query_one::<i64, _>("SELECT COUNT(*) FROM business_record", ())
153                .unwrap(),
154            1
155        );
156        assert_eq!(
157            database
158                .query_one::<i64, _>("SELECT COUNT(*) FROM audit.event", ())
159                .unwrap(),
160            1
161        );
162        assert_eq!(
163            database
164                .query_one::<i64, _>("SELECT COUNT(*) FROM outbox.message", ())
165                .unwrap(),
166            1
167        );
168    }
169}