use crate::db::{
BatchedCommand, BatchedCommandResult, OwnedSnapshotContext, Pool, SnapshotContext,
SubmitCommandOutcome,
};
use crate::encryption::EncryptionMasterKey;
use crate::error::{Error, Result};
use crate::event_cache::EventCache;
use crate::event_store::{CommandType, Event, EventBroadcaster};
use crate::plugin::{CommandDispatcher, ProjectionDispatcher};
use crate::shared::{CommandDecision, Tag};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use tokio::sync::{oneshot, RwLock, Semaphore};
pub const DEFAULT_MAX_BATCH_SIZE: usize = 256;
struct PendingCommand {
command: BatchedCommand,
reply: oneshot::Sender<BatchedCommandResult>,
}
type Queue = Arc<Mutex<Vec<PendingCommand>>>;
const DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
#[derive(Clone)]
pub struct CommandBatcher {
queues: Arc<RwLock<HashMap<String, Queue>>>,
idle_in_transaction_timeout: std::time::Duration,
leader_permits: Arc<std::sync::OnceLock<Arc<Semaphore>>>,
max_batch_size: usize,
max_concurrent_leaders: Option<usize>,
}
impl Default for CommandBatcher {
fn default() -> Self {
Self::new()
}
}
impl CommandBatcher {
pub fn new() -> Self {
Self {
queues: Arc::new(RwLock::new(HashMap::new())),
idle_in_transaction_timeout: DEFAULT_IDLE_IN_TRANSACTION_TIMEOUT,
leader_permits: Arc::new(std::sync::OnceLock::new()),
max_batch_size: DEFAULT_MAX_BATCH_SIZE,
max_concurrent_leaders: None,
}
}
pub fn with_max_batch_size(mut self, max_batch_size: usize) -> Self {
self.max_batch_size = max_batch_size.max(1);
self
}
pub fn with_max_concurrent_leaders(mut self, max_concurrent_leaders: usize) -> Self {
self.max_concurrent_leaders = Some(max_concurrent_leaders.max(1));
self
}
fn leader_permits(&self, pool: &Pool) -> Arc<Semaphore> {
self.leader_permits
.get_or_init(|| {
let permits = self
.max_concurrent_leaders
.unwrap_or((pool.options().get_max_connections() as usize / 2).max(1));
Arc::new(Semaphore::new(permits))
})
.clone()
}
pub fn with_idle_in_transaction_timeout(mut self, timeout: std::time::Duration) -> Self {
self.idle_in_transaction_timeout = timeout;
self
}
async fn queue_for(&self, bounded_context: &str) -> Queue {
if let Some(queue) = self.queues.read().await.get(bounded_context) {
return queue.clone();
}
self.queues
.write()
.await
.entry(bounded_context.to_string())
.or_insert_with(|| Arc::new(Mutex::new(Vec::new())))
.clone()
}
#[allow(clippy::too_many_arguments)]
pub async fn submit(
&self,
pool: &Pool,
dispatcher: &dyn CommandDispatcher,
projection_dispatcher: &dyn ProjectionDispatcher,
broadcaster: &EventBroadcaster,
event_cache: &EventCache,
command_type: &CommandType,
payload: &str,
client_id: &str,
correlation_id: Option<&str>,
causation_id: Option<&str>,
bounded_context_events: &[Event],
consistency_tags: &[Tag],
matching_events: &[Event],
initial_decision: CommandDecision,
encryption_master_key: Option<&EncryptionMasterKey>,
now: DateTime<Utc>,
snapshot: Option<SnapshotContext<'_>>,
idempotency_key: Option<&str>,
) -> Result<SubmitCommandOutcome> {
let bounded_context_name = command_type.bounded_context.name.clone();
let (event_types_by_name, resolved) = crate::db::warm_up_event_types_and_encryption_keys(
pool,
&bounded_context_name,
command_type,
payload,
&initial_decision,
encryption_master_key,
)
.await?;
let command = BatchedCommand {
command_type: command_type.clone(),
payload: payload.to_string(),
client_id: client_id.to_string(),
correlation_id: correlation_id.map(str::to_string),
causation_id: causation_id.map(str::to_string),
bounded_context_events: bounded_context_events.to_vec(),
consistency_tags: consistency_tags.to_vec(),
matching_events: matching_events.to_vec(),
initial_decision,
now,
snapshot: snapshot.map(|s| OwnedSnapshotContext {
state_json: s.state_json.to_string(),
as_of_sequence: s.as_of_sequence,
}),
idempotency_key: idempotency_key.map(str::to_string),
event_types_by_name,
resolved,
};
let queue = self.queue_for(&bounded_context_name).await;
let (reply_tx, reply_rx) = oneshot::channel();
let is_leader = {
let mut queue = lock_queue(&queue);
queue.push(PendingCommand {
command,
reply: reply_tx,
});
queue.len() == 1
};
if !is_leader {
return reply_rx.await.unwrap_or_else(|_| {
Err(Error::batch_failed_msg(
"command batch leader task ended without producing a result",
))
});
}
self.run_as_leader(
pool,
dispatcher,
projection_dispatcher,
broadcaster,
event_cache,
encryption_master_key,
&bounded_context_name,
&queue,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn decide_and_submit(
&self,
pool: &Pool,
dispatcher: &dyn CommandDispatcher,
projection_dispatcher: &dyn ProjectionDispatcher,
snapshot_dispatcher: &dyn crate::plugin::SnapshotDispatcher,
broadcaster: &EventBroadcaster,
event_cache: &EventCache,
command_type: &CommandType,
payload: &str,
client_id: &str,
correlation_id: Option<&str>,
causation_id: Option<&str>,
encryption_master_key: Option<&EncryptionMasterKey>,
now: DateTime<Utc>,
idempotency_key: Option<&str>,
) -> Result<SubmitCommandOutcome> {
let resolved = crate::db::resolve_command_submission(
pool,
dispatcher,
snapshot_dispatcher,
event_cache,
command_type,
payload,
)
.await?;
self.submit(
pool,
dispatcher,
projection_dispatcher,
broadcaster,
event_cache,
command_type,
payload,
client_id,
correlation_id,
causation_id,
&resolved.bounded_context_events,
&resolved.consistency_tags,
&resolved.matching_events,
resolved.decision,
encryption_master_key,
now,
resolved
.snapshot_context
.as_ref()
.map(|ctx| SnapshotContext {
state_json: &ctx.state_json,
as_of_sequence: ctx.as_of_sequence,
}),
idempotency_key,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn run_as_leader(
&self,
pool: &Pool,
dispatcher: &dyn CommandDispatcher,
projection_dispatcher: &dyn ProjectionDispatcher,
broadcaster: &EventBroadcaster,
event_cache: &EventCache,
encryption_master_key: Option<&EncryptionMasterKey>,
bounded_context_name: &str,
queue: &Queue,
) -> Result<SubmitCommandOutcome> {
let mut guard = LeaderGuard {
queue: queue.clone(),
armed: true,
};
let permits = self.leader_permits(pool);
let mut own_result: Option<Result<SubmitCommandOutcome>> = None;
loop {
let _permit = permits
.acquire()
.await
.expect("leader semaphore is never closed");
let lock_wait_started = std::time::Instant::now();
let leader_tx = crate::db::begin_command_batch_leader_tx(
pool,
bounded_context_name,
Some(self.idle_in_transaction_timeout),
)
.await;
tracing::debug!(
bounded_context = %bounded_context_name,
lock_wait_us = lock_wait_started.elapsed().as_micros(),
"command batch leader lock wait"
);
let (pending, more_remaining) = drain_up_to(queue, self.max_batch_size);
if !more_remaining {
guard.armed = false;
}
let first_pass = own_result.is_none();
let (batch, mut replies): (
Vec<BatchedCommand>,
Vec<oneshot::Sender<BatchedCommandResult>>,
) = pending.into_iter().map(|p| (p.command, p.reply)).unzip();
let skip = usize::from(first_pass);
let outcome = match leader_tx {
Err(e) => Err(e),
Ok(leader_tx) => {
crate::db::commit_command_batch(
leader_tx,
pool,
dispatcher,
projection_dispatcher,
encryption_master_key,
batch,
)
.await
}
};
match outcome {
Err(e) => {
for reply in replies.drain(skip..) {
let _ = reply.send(Err(Error::batch_failed(&e)));
}
if first_pass {
own_result = Some(Err(e));
}
}
Ok(results) => {
for outcome in results.iter().flatten() {
crate::db::broadcast_appended_events(
pool,
broadcaster,
event_cache,
outcome,
)
.await;
}
let mut results = results.into_iter();
if first_pass {
own_result = Some(results.next().expect(
"run_as_leader's own batch always has at least its own leader in it",
));
}
for (reply, result) in replies.into_iter().skip(skip).zip(results) {
let _ = reply.send(result);
}
}
}
if !more_remaining {
break;
}
}
own_result.expect("the first pass always records the leader's own result")
}
}
fn lock_queue(queue: &Queue) -> std::sync::MutexGuard<'_, Vec<PendingCommand>> {
queue.lock().unwrap_or_else(|e| e.into_inner())
}
fn drain_up_to(queue: &Queue, max: usize) -> (Vec<PendingCommand>, bool) {
let mut queue = lock_queue(queue);
if queue.len() <= max {
(std::mem::take(&mut *queue), false)
} else {
(queue.drain(..max).collect(), true)
}
}
struct LeaderGuard {
queue: Queue,
armed: bool,
}
impl Drop for LeaderGuard {
fn drop(&mut self) {
if !self.armed {
return;
}
let stranded = std::mem::take(&mut *lock_queue(&self.queue));
for pending in stranded {
let _ = pending.reply.send(Err(Error::batch_failed_msg(
"the command batch leader was cancelled before processing this command; retry",
)));
}
}
}