Skip to content

Commit 13b8d17

Browse files
FeilixXclaude
andcommitted
perf: lazy checkpoint generation for concurrent write throughput
Move Merkle checkpoint from per-event write transaction to on-demand read path. This eliminates write lock contention under concurrent agent submissions — checkpoint is now generated lazily via ensure_checkpoint() when read operations need a current tree head. Also removes unused tree_size variables left over from the refactor. Bump to v0.2.9. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3adc4a1 commit 13b8d17

4 files changed

Lines changed: 41 additions & 30 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
resolver = "2"
88

99
[workspace.package]
10-
version = "0.2.8"
10+
version = "0.2.9"
1111
edition = "2024"
1212
authors = ["Felix <feijiu@punkgo.ai>"]
1313
license = "MIT"

crates/punkgo-kernel/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ default = []
1717
testkit = []
1818

1919
[dependencies]
20-
punkgo-core = { path = "../punkgo-core", version = "0.2.8" }
20+
punkgo-core = { path = "../punkgo-core", version = "0.2.9" }
2121
anyhow.workspace = true
2222
async-trait.workspace = true
2323
serde.workspace = true

crates/punkgo-kernel/src/audit.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ impl AuditLog {
220220
/// Returns the current tree size (number of leaves stored).
221221
pub async fn tree_size(&self) -> Result<u64, AuditError> {
222222
// Use the latest checkpoint's tree_size as authoritative.
223+
// When checkpoints are generated lazily, this may lag behind the actual
224+
// event count. Call `ensure_checkpoint()` first if you need an up-to-date value.
223225
let row =
224226
sqlx::query("SELECT tree_size FROM audit_checkpoints ORDER BY tree_size DESC LIMIT 1")
225227
.fetch_optional(&self.pool)
@@ -229,6 +231,21 @@ impl AuditLog {
229231
.unwrap_or(0))
230232
}
231233

234+
/// Ensure the checkpoint is up-to-date with the given event count.
235+
/// If the latest checkpoint is stale, generates a new one.
236+
/// Called lazily on read operations that need a current checkpoint.
237+
pub async fn ensure_checkpoint(&self, event_count: u64) -> Result<(), AuditError> {
238+
if event_count == 0 {
239+
return Ok(());
240+
}
241+
let current = self.tree_size().await?;
242+
if current >= event_count {
243+
return Ok(()); // already up to date
244+
}
245+
self.make_checkpoint(event_count).await?;
246+
Ok(())
247+
}
248+
232249
async fn read_hash_in_tx(
233250
&self,
234251
tx: &mut Transaction<'_, Sqlite>,

crates/punkgo-kernel/src/runtime/kernel.rs

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -397,16 +397,11 @@ impl Kernel {
397397

398398
// Audit trail — atomic with event (whitepaper §3 invariant 5).
399399
let log_index = hold_event.log_index as u64;
400-
let tree_size = log_index + 1;
401400
self.audit_log
402401
.append_leaf_in_tx(&mut tx, log_index, &hold_event.event_hash)
403402
.await
404403
.map_err(|e| KernelError::Audit(e.to_string()))?;
405-
self.audit_log
406-
.make_checkpoint_in_tx(&mut tx, tree_size)
407-
.await
408-
.map_err(|e| KernelError::Audit(e.to_string()))?;
409-
404+
// Checkpoint generated lazily on read, not here.
410405
tx.commit().await?;
411406

412407
return Err(KernelError::HoldTriggered {
@@ -710,16 +705,15 @@ impl Kernel {
710705

711706
// Audit trail update — atomic with event (whitepaper §3 invariant 5).
712707
let log_index = event.log_index as u64;
713-
let tree_size = log_index + 1;
714708
self.audit_log
715709
.append_leaf_in_tx(&mut tx, log_index, &event.event_hash)
716710
.await
717711
.map_err(|e| KernelError::Audit(e.to_string()))?;
718-
self.audit_log
719-
.make_checkpoint_in_tx(&mut tx, tree_size)
720-
.await
721-
.map_err(|e| KernelError::Audit(e.to_string()))?;
722712

713+
// Checkpoint is NOT generated on every event. It is a derived artifact
714+
// (tree root computed from leaf hashes) and can be generated on-demand
715+
// when queried (receipt, show, verify) or via explicit checkpoint command.
716+
// This keeps the write path fast and lock-free for concurrent access.
723717
tx.commit().await?;
724718
info!(event_id = %event.id, log_index = event.log_index, "event committed");
725719
Ok(())
@@ -816,6 +810,8 @@ impl Kernel {
816810
// Legacy: snapshot is superseded by audit checkpoint.
817811
// Return audit checkpoint data for backward compatibility.
818812
let event_count = self.event_log.count().await?;
813+
self.audit_log.ensure_checkpoint(event_count as u64).await
814+
.map_err(|e| KernelError::Audit(e.to_string()))?;
819815
let cp = self
820816
.audit_log
821817
.latest_checkpoint()
@@ -837,6 +833,9 @@ impl Kernel {
837833
}))
838834
}
839835
"audit_checkpoint" => {
836+
let event_count = self.event_log.count().await?;
837+
self.audit_log.ensure_checkpoint(event_count as u64).await
838+
.map_err(|e| KernelError::Audit(e.to_string()))?;
840839
let cp = self
841840
.audit_log
842841
.latest_checkpoint()
@@ -852,11 +851,16 @@ impl Kernel {
852851
})? as u64;
853852
let tree_size = match query.tree_size {
854853
Some(s) => s as u64,
855-
None => self
856-
.audit_log
857-
.tree_size()
858-
.await
859-
.map_err(|e| KernelError::Audit(e.to_string()))?,
854+
None => {
855+
// Ensure checkpoint is current before deriving tree_size.
856+
let event_count = self.event_log.count().await? as u64;
857+
self.audit_log.ensure_checkpoint(event_count).await
858+
.map_err(|e| KernelError::Audit(e.to_string()))?;
859+
self.audit_log
860+
.tree_size()
861+
.await
862+
.map_err(|e| KernelError::Audit(e.to_string()))?
863+
}
860864
};
861865
let proof = self
862866
.audit_log
@@ -1402,16 +1406,11 @@ impl Kernel {
14021406

14031407
// Audit trail — atomic with event (whitepaper §3 invariant 5).
14041408
let log_index = response_event.log_index as u64;
1405-
let tree_size = log_index + 1;
14061409
self.audit_log
14071410
.append_leaf_in_tx(&mut tx, log_index, &response_event.event_hash)
14081411
.await
14091412
.map_err(|e| KernelError::Audit(e.to_string()))?;
1410-
self.audit_log
1411-
.make_checkpoint_in_tx(&mut tx, tree_size)
1412-
.await
1413-
.map_err(|e| KernelError::Audit(e.to_string()))?;
1414-
1413+
// Checkpoint generated lazily on read, not here.
14151414
tx.commit().await?;
14161415

14171416
info!(
@@ -1604,16 +1603,11 @@ impl Kernel {
16041603

16051604
// Audit trail — atomic with event (whitepaper §3 invariant 5).
16061605
let t_log_index = timeout_event.log_index as u64;
1607-
let t_tree_size = t_log_index + 1;
16081606
self.audit_log
16091607
.append_leaf_in_tx(&mut tx, t_log_index, &timeout_event.event_hash)
16101608
.await
16111609
.map_err(|e| KernelError::Audit(e.to_string()))?;
1612-
self.audit_log
1613-
.make_checkpoint_in_tx(&mut tx, t_tree_size)
1614-
.await
1615-
.map_err(|e| KernelError::Audit(e.to_string()))?;
1616-
1610+
// Checkpoint generated lazily on read, not here.
16171611
tx.commit().await?;
16181612

16191613
info!(

0 commit comments

Comments
 (0)