use crate::errors::PagedbError;
use crate::pager::freelist;
use crate::pager::header::commit_header;
use crate::pager::structural_header::MainDbHeaderFields;
use crate::vfs::Vfs;
use crate::{CommitId, Result};
use std::collections::HashSet;
use super::super::db::{CommitHistoryMeta, encode_free_list_root};
use super::txn::WriteTxn;
impl<V: Vfs + Clone> WriteTxn<'_, V> {
#[allow(clippy::too_many_lines)]
pub async fn commit(mut self) -> Result<CommitId> {
let _span = tracing::debug_span!("txn.commit");
let new_commit_id = self.guard.latest_commit_id + 1;
self.btree.materialize_dirty().await?;
self.sync_allocator_to_catalog();
self.catalog_tree.materialize_dirty().await?;
self.sync_allocator_from_catalog();
let new_root = self.btree.root_page_id();
let new_catalog_root = self.catalog_tree.root_page_id();
self.guard.next_page_id = self
.btree
.next_page_id()
.max(self.catalog_tree.next_page_id());
let unix_seconds = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let history_meta = CommitHistoryMeta {
active_root_page_id: new_root,
catalog_root_page_id: new_catalog_root,
free_list_root_page_id: self.guard.free_list_root_page_id,
next_page_id: self.guard.next_page_id,
unix_seconds,
};
let mut hist_freed: Vec<u64> = Vec::new();
if !matches!(
self.db.options.commit_history_retain,
crate::options::RetainPolicy::Disabled
) {
hist_freed = self
.db
.write_commit_history_entry(&mut self.guard, new_commit_id, history_meta)
.await?;
}
let all_freed: Vec<u64> = self
.btree
.drain_freed()
.into_iter()
.chain(self.catalog_tree.drain_freed())
.chain(hist_freed)
.filter(|&pid| pid >= 4)
.collect();
let consumed: HashSet<u64> = std::mem::take(&mut *self.db.free_page_consumed.lock())
.into_iter()
.collect();
let prior_free: Vec<(u64, u64)> = std::mem::take(&mut self.free_set_loaded)
.into_iter()
.filter(|(_, pid)| !consumed.contains(pid))
.collect();
let old_chain: Vec<u64> = std::mem::take(&mut self.old_chain_pages);
let host_candidates: Vec<u64> = std::mem::take(&mut *self.db.free_page_cache.lock())
.into_iter()
.filter(|pid| !consumed.contains(pid))
.collect();
let mut entries: Vec<(u64, u64)> = prior_free;
entries.extend(all_freed.iter().map(|&pid| (new_commit_id, pid)));
entries.extend(old_chain.iter().map(|&pid| (new_commit_id, pid)));
let stuck = entries
.iter()
.filter(|(cid, _)| *cid >= self.reclaim_floor)
.count() as u64;
self.db.evaluate_stall_policy(stuck)?;
let (new_free_list_root, new_next_page) = freelist::rewrite_chain(
&self.db.pager,
self.db.realm_id,
self.db.page_size,
entries,
host_candidates,
self.guard.next_page_id,
)
.await?;
self.guard.next_page_id = new_next_page;
self.db.pager.flush_main(self.db.realm_id).await?;
let new_next = self.guard.next_page_id;
let new_seq = self.guard.seq + 1;
let counter_anchor = self.db.pager.pending_anchor();
let mut catalog_root_bytes = [0u8; 16];
catalog_root_bytes[..8].copy_from_slice(&new_catalog_root.to_le_bytes());
catalog_root_bytes[8..].copy_from_slice(&new_commit_id.to_le_bytes());
let (policy_tag, policy_value) =
encode_retain_policy(&self.db.options.commit_history_retain);
let fields = MainDbHeaderFields {
format_version: 1,
cipher_id: self.db.cipher_id.as_byte(),
page_size_log2: page_size_log2(self.db.page_size)?,
flags: 0,
file_id: self.db.file_id,
kek_salt: self.db.kek_salt,
mk_epoch: self.db.mk_epoch.load(std::sync::atomic::Ordering::SeqCst),
seq: new_seq,
active_root_page_id: new_root,
active_root_txn_id: new_commit_id,
counter_anchor,
commit_id: CommitId(new_commit_id),
free_list_root: encode_free_list_root(new_free_list_root),
catalog_root: catalog_root_bytes,
apply_journal_root_page_id: 0,
apply_journal_root_version: 0,
commit_history_root_page_id: self.guard.commit_history_root_page_id,
commit_history_root_version: self.guard.commit_history_root_version,
restore_mode: 0,
next_page_id: new_next,
commit_retain_policy_tag: policy_tag,
commit_retain_policy_value: policy_value,
};
let hk_clone = { self.db.hk.read().clone() };
let new_slot = commit_header(
&*self.db.vfs,
&self.db.main_db_path,
&hk_clone,
&fields,
self.guard.active_slot,
self.db.page_size,
)
.await?;
self.guard.root_page_id = new_root;
self.guard.next_page_id = new_next;
self.guard.active_slot = new_slot;
self.guard.seq = new_seq;
self.guard.latest_commit_id = new_commit_id;
self.guard.catalog_root_page_id = new_catalog_root;
self.guard.catalog_root_txn_id = new_commit_id;
self.guard.free_list_root_page_id = new_free_list_root;
self.committed_or_aborted = true;
let _visibility = &self.visibility_guard;
if self
.db
.finish_durable_commit_visible(
&self.visibility_guard,
&self.guard,
CommitId(new_commit_id),
counter_anchor,
&self.pending_segments,
)
.await
.is_err()
{
self.cleanup_spill_async().await;
self.db
.spill_bytes_in_use
.store(0, std::sync::atomic::Ordering::Relaxed);
return Err(PagedbError::durably_committed_but_unpublished(CommitId(
new_commit_id,
)));
}
self.cleanup_spill_async().await;
self.db
.spill_bytes_in_use
.store(0, std::sync::atomic::Ordering::Relaxed);
tracing::debug!(
name = "txn.commit",
commit_id = new_commit_id,
"write transaction committed"
);
Ok(CommitId(new_commit_id))
}
}
fn page_size_log2(page_size: usize) -> Result<u8> {
match page_size {
4096 => Ok(12),
8192 => Ok(13),
16384 => Ok(14),
32768 => Ok(15),
65536 => Ok(16),
_ => Err(PagedbError::Unsupported),
}
}
fn encode_retain_policy(policy: &crate::options::RetainPolicy) -> (u8, u64) {
match policy {
crate::options::RetainPolicy::Count(n) => (0, u64::from(*n)),
crate::options::RetainPolicy::Age(d) => (1, d.as_secs()),
crate::options::RetainPolicy::Unbounded => (2, 0),
crate::options::RetainPolicy::Disabled => (3, 0),
}
}