use std::fmt;
use std::sync::atomic::AtomicU64;
use std::time::{Duration, Instant};
use crossbeam_utils::atomic::AtomicCell;
use parking_lot::{Mutex, RwLock};
use super::error::TransactionError;
use super::mutation_journal::MutationJournal;
use super::participant::TransactionMutationRecorder;
use super::types::*;
use super::undo_log::{UndoLogEntry, UndoLogManager};
use super::wal::buffer::LocalWalBuffer;
use super::wal::Timestamp;
use linkrs_core::types::VertexId;
pub struct TransactionContext {
pub id: TransactionId,
pub txn_type: TransactionType,
state: AtomicCell<TransactionState>,
pub start_timestamp: Timestamp,
commit_timestamp: AtomicU64,
refreshed_read_ts: RwLock<Option<Timestamp>>,
statement_snapshot_pin: RwLock<Option<Timestamp>>,
pub start_time: Instant,
timeout: Duration,
pub read_only: bool,
pub auto_commit: bool,
pub isolation_level: IsolationLevel,
pub query_timeout: Option<Duration>,
pub statement_timeout: Option<Duration>,
pub idle_timeout: Option<Duration>,
last_activity: AtomicCell<Instant>,
statement_start: AtomicCell<Instant>,
query_count: AtomicU64,
pub durability: DurabilityLevel,
modified_tables: Mutex<Vec<String>>,
savepoint_manager: RwLock<SavepointManager>,
undo_logs: RwLock<UndoLogManager>,
write_set: Mutex<WriteSet>,
read_set: Mutex<WriteSet>,
local_wal: Mutex<LocalWalBuffer>,
write_validated: AtomicCell<bool>,
rollback_only: AtomicCell<bool>,
resources_released: AtomicCell<bool>,
staged_bytes: AtomicU64,
commit_published: AtomicCell<bool>,
commit_lsn: AtomicU64,
owner: RwLock<Option<String>>,
max_mutation_count: u64,
max_undo_bytes: u64,
mutation_count: AtomicU64,
undo_bytes: AtomicU64,
budget_warning_threshold: f64,
mutation_warning_emitted: AtomicCell<bool>,
undo_warning_emitted: AtomicCell<bool>,
budget_warnings: Mutex<Vec<TransactionEvent>>,
pessimistic_lock_held: AtomicCell<bool>,
concurrency_mode: ConcurrencyMode,
schema_catalog_version: AtomicU64,
serializable_full_scan_threshold: Option<usize>,
ssi_state: RwLock<super::types::SsiState>,
mutation_journal: RwLock<MutationJournal>,
}
impl fmt::Debug for TransactionContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TransactionContext")
.field("id", &self.id)
.field("txn_type", &self.txn_type)
.field("state", &self.state.load())
.field("start_timestamp", &self.start_timestamp)
.field("refreshed_read_ts", &self.effective_read_timestamp())
.field("read_only", &self.read_only)
.field("auto_commit", &self.auto_commit)
.field("isolation_level", &self.isolation_level)
.field("durability", &self.durability)
.finish()
}
}
mod budget;
mod certification;
mod journal;
mod lifecycle;
mod savepoint;
mod snapshot;
use savepoint::SavepointManager;
impl TransactionContext {
pub fn new(id: TransactionId, start_timestamp: Timestamp, config: TransactionConfig) -> Self {
let now = Instant::now();
Self {
id,
txn_type: TransactionType::Write,
state: AtomicCell::new(TransactionState::Active),
start_timestamp,
commit_timestamp: AtomicU64::new(0),
refreshed_read_ts: RwLock::new(None),
statement_snapshot_pin: RwLock::new(None),
start_time: now,
timeout: config.timeout,
read_only: false,
auto_commit: config.auto_commit,
isolation_level: config.isolation_level,
query_timeout: config.query_timeout,
statement_timeout: config.statement_timeout,
idle_timeout: config.idle_timeout,
last_activity: AtomicCell::new(now),
statement_start: AtomicCell::new(now),
query_count: AtomicU64::new(0),
durability: config.durability,
modified_tables: Mutex::new(Vec::new()),
savepoint_manager: RwLock::new(SavepointManager::new()),
undo_logs: RwLock::new(UndoLogManager::new()),
write_set: Mutex::new(WriteSet::new()),
read_set: Mutex::new(WriteSet::new()),
local_wal: Mutex::new(LocalWalBuffer::new()),
write_validated: AtomicCell::new(false),
rollback_only: AtomicCell::new(false),
resources_released: AtomicCell::new(false),
staged_bytes: AtomicU64::new(0),
commit_published: AtomicCell::new(false),
commit_lsn: AtomicU64::new(0),
owner: RwLock::new(None),
max_mutation_count: config.max_mutation_count,
max_undo_bytes: config.max_undo_bytes,
mutation_count: AtomicU64::new(0),
undo_bytes: AtomicU64::new(0),
budget_warning_threshold: config.budget_warning_threshold,
mutation_warning_emitted: AtomicCell::new(false),
undo_warning_emitted: AtomicCell::new(false),
budget_warnings: Mutex::new(Vec::new()),
pessimistic_lock_held: AtomicCell::new(false),
concurrency_mode: config.concurrency_mode,
schema_catalog_version: AtomicU64::new(0),
serializable_full_scan_threshold: config.serializable_full_scan_threshold,
ssi_state: RwLock::new(super::types::SsiState::new()),
mutation_journal: RwLock::new(MutationJournal::new()),
}
}
pub fn new_readonly(
id: TransactionId,
start_timestamp: Timestamp,
config: TransactionConfig,
) -> Self {
let now = Instant::now();
Self {
id,
txn_type: TransactionType::ReadOnly,
state: AtomicCell::new(TransactionState::Active),
start_timestamp,
commit_timestamp: AtomicU64::new(0),
refreshed_read_ts: RwLock::new(None),
statement_snapshot_pin: RwLock::new(None),
start_time: now,
timeout: config.timeout,
read_only: true,
auto_commit: config.auto_commit,
isolation_level: config.isolation_level,
query_timeout: config.query_timeout,
statement_timeout: config.statement_timeout,
idle_timeout: config.idle_timeout,
last_activity: AtomicCell::new(now),
statement_start: AtomicCell::new(now),
query_count: AtomicU64::new(0),
durability: config.durability,
modified_tables: Mutex::new(Vec::new()),
savepoint_manager: RwLock::new(SavepointManager::new()),
undo_logs: RwLock::new(UndoLogManager::new()),
write_set: Mutex::new(WriteSet::new()),
read_set: Mutex::new(WriteSet::new()),
local_wal: Mutex::new(LocalWalBuffer::new()),
write_validated: AtomicCell::new(false),
rollback_only: AtomicCell::new(false),
resources_released: AtomicCell::new(false),
staged_bytes: AtomicU64::new(0),
commit_published: AtomicCell::new(false),
commit_lsn: AtomicU64::new(0),
owner: RwLock::new(None),
max_mutation_count: config.max_mutation_count,
max_undo_bytes: config.max_undo_bytes,
mutation_count: AtomicU64::new(0),
undo_bytes: AtomicU64::new(0),
budget_warning_threshold: config.budget_warning_threshold,
mutation_warning_emitted: AtomicCell::new(false),
undo_warning_emitted: AtomicCell::new(false),
budget_warnings: Mutex::new(Vec::new()),
pessimistic_lock_held: AtomicCell::new(false),
concurrency_mode: config.concurrency_mode,
schema_catalog_version: AtomicU64::new(0),
serializable_full_scan_threshold: config.serializable_full_scan_threshold,
ssi_state: RwLock::new(super::types::SsiState::new()),
mutation_journal: RwLock::new(MutationJournal::new()),
}
}
pub fn new_checkpoint(
id: TransactionId,
write_timestamp: Timestamp,
config: TransactionConfig,
) -> Self {
let mut context = Self::new(id, write_timestamp, config);
context.txn_type = TransactionType::Checkpoint;
context
}
pub fn new_recovery(
id: TransactionId,
write_timestamp: Timestamp,
config: TransactionConfig,
) -> Self {
let mut ctx = Self::new(id, write_timestamp, config);
ctx.txn_type = TransactionType::Recovery;
ctx
}
pub fn new_dummy(
id: TransactionId,
write_timestamp: Timestamp,
config: TransactionConfig,
) -> Self {
let mut ctx = Self::new(id, write_timestamp, config);
ctx.txn_type = TransactionType::Dummy;
ctx
}
}
impl TransactionMutationRecorder for TransactionContext {
fn record_mutation(&self, mutation: MutationResult) -> Result<(), TransactionError> {
self.record_mutation(mutation)
}
fn record_vertex_write(&self, vertex_id: VertexId) {
self.record_vertex_write(vertex_id);
}
fn record_vertex_delete(&self, vertex_id: VertexId) {
self.record_vertex_delete(vertex_id);
}
fn record_edge_write(&self, edge: linkrs_core::types::EdgeIdentifier) {
self.record_edge_write(edge);
}
fn add_undo_log(&self, entry: UndoLogEntry) -> Result<(), TransactionError> {
self.add_undo_log(entry)
}
fn record_table_modification(&self, table_name: &str) {
self.record_table_modification(table_name);
}
fn record_schema_write(&self, resource: &str) -> Result<(), TransactionError> {
self.record_schema_write(resource)
}
fn record_index_write(&self, resource: &str) {
self.record_index_write(resource);
}
fn record_vertex_read(&self, vertex_id: VertexId) {
self.record_vertex_read(vertex_id);
}
fn record_edge_read(&self, edge: linkrs_core::types::EdgeIdentifier) {
self.record_edge_read(edge);
}
fn record_schema_read(&self, resource: &str) {
self.record_schema_read(resource);
}
fn record_index_read(&self, resource: &str) {
self.record_index_read(resource);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn budget_warnings_drain_exactly_once() {
let config = TransactionConfig::default().with_max_mutation_count(10);
let ctx = TransactionContext::new(TransactionId(1), 1, config);
for _ in 0..8 {
ctx.record_mutation(MutationResult::default()).unwrap();
}
assert_eq!(ctx.pending_budget_warnings(), 1);
let drained = ctx.drain_budget_warnings();
assert_eq!(drained.len(), 1);
assert!(matches!(
&drained[0],
TransactionEvent::BudgetWarning { resource, current: 8, limit: 10, .. }
if resource == "mutation count"
));
assert!(ctx.drain_budget_warnings().is_empty());
}
#[test]
fn test_transaction_context_basic() {
let config = TransactionConfig::default();
let ctx = TransactionContext::new(TransactionId(1), 1, config);
assert_eq!(ctx.id, TransactionId(1));
assert_eq!(ctx.timestamp(), 1);
assert_eq!(ctx.state(), TransactionState::Active);
assert!(!ctx.read_only);
}
#[test]
fn test_transaction_context_readonly() {
let config = TransactionConfig::default();
let ctx = TransactionContext::new_readonly(TransactionId(1), 1, config);
assert!(ctx.read_only);
}
#[test]
fn test_transaction_context_state_transition() {
let config = TransactionConfig::default();
let ctx = TransactionContext::new(TransactionId(1), 1, config);
assert!(ctx.transition_to(TransactionState::Committing).is_ok());
assert_eq!(ctx.state(), TransactionState::Committing);
assert!(ctx.transition_to(TransactionState::Aborting).is_ok());
assert_eq!(ctx.state(), TransactionState::Aborting);
assert!(ctx.transition_to(TransactionState::Aborted).is_ok());
assert_eq!(ctx.state(), TransactionState::Aborted);
}
#[test]
fn test_transaction_context_savepoint() {
let config = TransactionConfig::default();
let ctx = TransactionContext::new(TransactionId(1), 1, config);
let sp_id = ctx.create_savepoint(Some("test".to_string()), 0);
assert!(ctx.get_savepoint(sp_id).is_some());
let sp = ctx.get_savepoint(sp_id).unwrap();
assert_eq!(sp.name, Some("test".to_string()));
}
}