blob: e96cbbab51bc25c083640c83653c6513f7b290d3 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Pending queue control state. Revisions fence edits against canonical delivery.
use crate::{EventLog, StoreError, message_admissions::PendingMessageAdmission};
use maka_agent_facts::event::Invocation;
use maka_agent_facts::input::MessageInput;
use serde::{Deserialize, Serialize};
use sqlx::{Connection, SqliteConnection};
pub(crate) mod edit;
mod receipts;
pub use receipts::{QueueCommand, QueueCommandKind, QueueReceipt};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MessageQueue {
pub revision: u64,
pub entries: Vec<PendingMessageAdmission>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum QueueEdit {
RetractAll {
cancellation_id: String,
},
Retract {
message_id: String,
cancellation_id: String,
},
Promote {
message_id: String,
invocation: Invocation,
},
Update {
message_id: String,
content: Box<MessageInput>,
unprepared_content: Box<MessageInput>,
required_tools: std::collections::BTreeSet<String>,
},
Reorder {
message_ids: Vec<String>,
},
}
#[derive(Debug)]
pub(crate) struct QueueChange {
pub queue: MessageQueue,
pub retracted: Vec<PendingMessageAdmission>,
}
impl EventLog {
/// Read the control revision without loading the potentially large pending bodies.
pub async fn message_queue_revision(&self, session: &str) -> Result<u64, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
let session = session.to_owned();
self.connection
.run(move |connection| Box::pin(async move { revision(connection, &session).await }))
.await
}
pub async fn message_queue(&self, session: &str) -> Result<MessageQueue, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
let session = session.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin().await?;
read(&mut tx, &session).await
})
})
.await
}
/// The SQL owner completes accepted edits even if the caller stops awaiting.
/// Host admission supplies the preflighted revision; engine consumption uses
/// the same revision, so an edit never silently races delivered content.
pub async fn edit_message_queue(
&self,
session: &str,
expected_revision: u64,
edit: QueueEdit,
command: QueueCommand,
) -> Result<QueueReceipt, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
command.validate()?;
if command.kind != edit.command_kind() {
return Err(invalid("queue command kind does not match edit"));
}
let session = session.to_owned();
let commits = self.commits.clone();
self.connection
.run(move |connection| {
Box::pin(async move {
let mut tx = connection.begin_with("BEGIN IMMEDIATE").await?;
if let Some(receipt) = receipts::read(&mut tx, &session, &command).await? {
return Ok(receipt);
}
let previous = read(&mut tx, &session).await?;
if previous.revision != expected_revision {
return Err(StoreError::RevisionConflict {
expected: expected_revision.to_string(),
actual: previous.revision.to_string(),
});
}
let archived: Option<bool> =
sqlx::query_scalar("SELECT archived FROM session_control WHERE id = ?")
.bind(&session)
.fetch_optional(&mut *tx)
.await?;
match archived {
None => return Err(StoreError::SessionNotFound),
Some(true) => {
return Err(StoreError::InvalidTransition(
"Session is archived".into(),
));
}
Some(false) => {}
}
let mut change = edit::apply(&mut tx, &session, previous, edit).await?;
if change.changed {
change.result.queue.revision = bump(&mut tx, &session).await?;
}
let receipt = QueueReceipt {
revision: change.result.queue.revision,
retracted: if command.kind == QueueCommandKind::RetractAll {
change.result.retracted
} else {
Vec::new()
},
};
receipts::insert(&mut tx, &session, &command, &receipt).await?;
tx.commit().await.map_err(StoreError::CommitUnknown)?;
if change.changed {
// A wakeup is not a new execution fact. The same high-water is
// enough to refresh a projection whose queue control changed.
commits.send_modify(|_| {});
}
Ok(receipt)
})
})
.await
}
pub async fn message_cancelled(
&self,
session: &str,
message: &str,
) -> Result<bool, StoreError> {
self.validate_root()?;
crate::sessions::validate_id(session)?;
crate::sessions::validate_id(message)?;
let session = session.to_owned();
let message = message.to_owned();
self.connection
.run(move |connection| {
Box::pin(async move { cancelled(connection, &session, &message).await })
})
.await
}
}
pub(crate) async fn read(
connection: &mut SqliteConnection,
session: &str,
) -> Result<MessageQueue, StoreError> {
Ok(MessageQueue {
revision: revision(connection, session).await?,
entries: crate::message_admissions::pending(connection, session).await?,
})
}
pub(super) async fn revision(
connection: &mut SqliteConnection,
session: &str,
) -> Result<u64, StoreError> {
let revision: Option<i64> =
sqlx::query_scalar("SELECT revision FROM message_queue_state WHERE session_id = ?")
.bind(session)
.fetch_optional(connection)
.await?;
u64::try_from(revision.unwrap_or(0)).map_err(|_| invalid("invalid queue revision"))
}
pub(crate) async fn bump(
connection: &mut SqliteConnection,
session: &str,
) -> Result<u64, StoreError> {
let next = revision(connection, session)
.await?
.checked_add(1)
.filter(|n| *n <= maka_agent_facts::configuration::validation::MAX_SAFE_INTEGER)
.ok_or_else(|| invalid("message queue revision exhausted"))?;
sqlx::query(
"INSERT INTO message_queue_state(session_id, revision) VALUES (?, ?)
ON CONFLICT(session_id) DO UPDATE SET revision = excluded.revision",
)
.bind(session)
.bind(next as i64)
.execute(connection)
.await?;
Ok(next)
}
pub(crate) async fn cancelled(
connection: &mut SqliteConnection,
session: &str,
message: &str,
) -> Result<bool, StoreError> {
Ok(sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM message_cancellations WHERE session_id = ? AND message_id = ?)")
.bind(session).bind(message).fetch_one(connection).await?)
}
fn invalid(message: &str) -> StoreError {
StoreError::InvalidTransition(message.into())
}