Expand description
Commit hooks for executing custom logic before and after transaction commits.
This module provides the CommitHook trait and supporting types that allow you to
register hooks that execute during the commit lifecycle of a transaction. This is useful for:
- Publishing events to message queues after successful commits
- Updating caches
- Triggering side effects that should only occur if the transaction succeeds
- Accumulating operations across multiple entity updates in a transaction
§Hook Lifecycle
- Registration: Hooks are registered using
AtomicOperation::add_commit_hook() - Merging: Multiple hooks of the same type may be merged via
CommitHook::merge() - Pre-commit:
CommitHook::pre_commit()executes before the transaction commits - Commit: The underlying database transaction is committed
- Post-commit:
CommitHook::post_commit()executes after successful commit
If the commit fails instead — either a later hook’s pre_commit errors (the
transaction is rolled back first) or the COMMIT itself errors — then
CommitHook::on_rollback() is fired on every hook whose pre_commit had already
completed, in registration order, in place of post_commit. It is a synchronous,
infallible, signal-only callback (see its docs).
§Hook Ordering
Hooks execute in registration order — per hook, not per type:
- Hooks run in the order they were added to the operation, regardless of their
type. A hook that refuses to merge (
CommitHook::merge()returnsfalse) executes at its own (later) registration position, even when an earlier hook of the same type exists. - A hook that merges executes at the position of the hook it merged into (the earlier one). For always-merging hook types this means the type’s position is anchored by its first registration in the operation; all later registrations fold into that position.
CommitHook::post_commit()hooks run in the same order as theirCommitHook::pre_commit()counterparts (registration order).- A hook’s
pre_commitmay itself register further hooks — seeAtomicOperation::add_commit_hook()on theHookOperationit is handed. Those join the tail of the same commit pass rather than freezing the set before the firstpre_commitruns. See “Re-entrant Registration” below.
Registration order is a determinism guarantee, not a priority mechanism — there is no way to reorder hooks independently of the order in which they were added.
§Re-entrant Registration
AtomicOperation::add_commit_hook() succeeds on the HookOperation passed to
a pre_commit that is running as part of a real commit pass — a hook can register
more hooks, and they join the pass instead of being silently dropped or forced to
run outside the commit lifecycle:
- A newly-registered hook merges into a still-pending hook of the same type,
keeping that hook’s queue position — indistinguishable from having registered it
there directly. An already-executed hook of that type is never a merge
target (its
pre_commitalready ran), so registering that type again after its own execution always starts a fresh instance, which runs its ownpre_commitlater in the same pass. - A bound (
MAX_HOOK_GENERATIONSre-entrant generations) guards against a registration cycle (A registers B, B registers A, …), which would otherwise grow the queue forever inside an open transaction. Exceeding it fails the commit loudly instead of hanging. - This only applies to a real commit pass.
CommitHook::force_execute_pre_commit()— the escape hatch for ops that don’t support hooks at all — still returnsErrfromadd_commit_hook, because there is no pass for a registered hook to join; itspost_commit/on_rollbackwould simply never run.
§Savepoints
Hooks registered on a SavepointOp are staged and only enter the parent
operation’s set — through the same registration/merge path — when the savepoint
is released; a rolled-back savepoint discards them. No callback runs at a
savepoint boundary, so the lifecycle above is unchanged: one pre_commit pass at
the parent’s commit, post_commit only after a durable COMMIT. See
SavepointOp for details.
§Examples
§Hook with Database Operations and Channel-Based Publishing
This example shows a complete event publishing hook that:
- Stores events in the database during pre-commit (within the transaction)
- Sends events to a channel during post-commit for async processing
- Merges multiple hook instances to batch operations
Note: post_commit() is synchronous and cannot fail, so it’s best used for
fire-and-forget operations like sending to channels. A background task can then
handle the async work of publishing to external systems.
use es_entity::{AtomicOperation, operation::hooks::{CommitHook, HookOperation, PreCommitRet}};
#[derive(Debug, Clone)]
struct Event {
entity_id: uuid::Uuid,
event_type: String,
}
#[derive(Debug)]
struct EventPublisher {
events: Vec<Event>,
// Channel sender for publishing events to a background processor
// In production, this might be tokio::sync::mpsc::Sender or similar
tx: std::sync::mpsc::Sender<Event>,
}
impl CommitHook for EventPublisher {
async fn pre_commit(self, mut op: HookOperation<'_>)
-> Result<PreCommitRet<'_, Self>, sqlx::Error>
{
// Store events in the database within the transaction
// If the transaction fails, these inserts will be rolled back
for event in &self.events {
sqlx::query!(
"INSERT INTO hook_events (entity_id, event_type, created_at) VALUES ($1, $2, NOW())",
event.entity_id,
event.event_type
)
.execute(op.as_executor())
.await?;
}
PreCommitRet::ok(self, op)
}
fn post_commit(self) {
// Send events to a channel for async processing
// This only runs if the transaction succeeded
// Channel sends are fast and don't block; a background task handles publishing
for event in self.events {
// In production, handle send failures appropriately (logging, metrics, etc.)
// The channel might be bounded to apply backpressure
let _ = self.tx.send(event);
}
}
fn merge(&mut self, other: &mut Self) -> bool {
// Merge multiple EventPublisher hooks into one to batch operations
self.events.append(&mut other.events);
true
}
}
// Separate background task for async event publishing
// async fn event_publisher_task(mut rx: tokio::sync::mpsc::Receiver<Event>) {
// while let Some(event) = rx.recv().await {
// // Publish to Kafka, RabbitMQ, webhooks, etc.
// // Handle failures with retries, dead-letter queues, etc.
// match publish_to_external_system(&event).await {
// Ok(_) => log::info!("Published event: {:?}", event),
// Err(e) => log::error!("Failed to publish event: {:?}", e),
// }
// }
// }§Usage
let user_id = uuid::Uuid::nil();
let (tx, _rx) = std::sync::mpsc::channel();
let mut op = DbOp::init(&pool).await?;
// Add first hook
op.add_commit_hook(EventPublisher {
events: vec![Event { entity_id: user_id, event_type: "user.created".to_string() }],
tx: tx.clone(),
}).expect("could not add hook");
// Add second hook - will merge with the first
op.add_commit_hook(EventPublisher {
events: vec![Event { entity_id: user_id, event_type: "email.sent".to_string() }],
tx: tx.clone(),
}).expect("could not add hook");
// Both hooks merge into one, events are stored in DB, then sent to channel
op.commit().await?;Structs§
- Hook
Operation - Wrapper around a database connection passed to
CommitHook::pre_commit(). - Post
Commit Hooks - PreCommit
Ret - Return type for
CommitHook::pre_commit().
Constants§
- MAX_
HOOK_ GENERATIONS - Maximum number of re-entrant “generations” a single commit pass will execute.
Generation 0 is the hook set registered on the operation before
commit()starts; a hook staged by a generation-N hook’spre_commitis generation N+1. Bounds a registration cycle (A registers B, B registers A, …), which would otherwise grow the queue forever inside an open transaction — see the module docs.
Traits§
- Commit
Hook - Trait for implementing custom commit hooks that execute before and after transaction commits.
Type Aliases§
- BoxFuture
- Type alias for boxed async futures.