blob: 2fdc4755f555ed6e25f3bc2b0bafd3d54be1412b [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.
*/
use super::*;
use maka_agent_facts::event::CommitError;
use maka_agent_facts::{
event::{Fact, Invocation, InvocationInput, LogScope, RuntimeEvent},
model::{ModelEvent, TextKind},
};
use std::sync::{Arc, atomic::Ordering};
use std::{sync::atomic::AtomicUsize, time::Duration};
use tokio::sync::oneshot;
fn event(id: &str, fact: Fact) -> EventWrite {
let mut event = RuntimeEvent::new(
Invocation {
session_id: "session".into(),
turn_id: "turn".into(),
run_id: "run".into(),
invocation_id: "invocation".into(),
},
fact,
);
event.id = id.into();
EventWrite::plain(event).unwrap()
}
fn delta(id: &str, text: &str) -> EventWrite {
event(
id,
Fact::ModelObserved {
step_id: "step".into(),
event: ModelEvent::PartDelta {
id: "part".into(),
text: text.into(),
provider_options: None,
},
},
)
}
async fn open(path: &std::path::Path) -> Arc<EventLog> {
let log = Arc::new(EventLog::open(path).await.unwrap());
log.append_batch(&[
event(
"opened",
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
source_messages: vec![],
content: "user".into(),
request_fingerprint: None,
},
},
),
event(
"requested",
Fact::ModelRequested {
context_breakdown: None,
item_acceptance: false,
purpose: maka_agent_facts::context::ModelPurpose::Main,
context: None,
checkpoint_event_id: None,
step_id: "step".into(),
model_id: "model".into(),
source_scope: LogScope::Root,
source_high_water: 0,
source_digest: "source".into(),
effective_source_digest: None,
input_digest: "input".into(),
route_identity: "route".into(),
},
),
event(
"part",
Fact::ModelObserved {
step_id: "step".into(),
event: ModelEvent::PartStarted {
id: "part".into(),
text_kind: TextKind::Text,
provider_options: None,
},
},
),
])
.await
.unwrap();
log
}
async fn gate(log: &Arc<EventLog>) -> (oneshot::Sender<()>, tokio::task::JoinHandle<()>) {
let (entered, reached) = oneshot::channel();
let (release, gate) = oneshot::channel();
let log = log.clone();
let job = tokio::spawn(async move {
log.connection
.run(move |_| {
Box::pin(async move {
entered.send(()).unwrap();
gate.await.unwrap();
Ok(())
})
})
.await
.unwrap();
});
tokio::time::timeout(Duration::from_secs(5), reached)
.await
.unwrap()
.unwrap();
(release, job)
}
async fn hook(log: &EventLog, callback: impl FnMut() -> bool + Send + 'static) {
log.connection
.run(move |db| {
Box::pin(async move {
db.lock_handle().await?.set_commit_hook(callback);
Ok(())
})
})
.await
.unwrap();
}
fn ids(path: &std::path::Path) -> Vec<String> {
let db = rusqlite::Connection::open(path).unwrap();
db.prepare("SELECT event_id FROM runtime_events ORDER BY sequence")
.unwrap()
.query_map([], |r| r.get(0))
.unwrap()
.collect::<Result<_, _>>()
.unwrap()
}
#[tokio::test]
async fn neighbors_share_commit_and_isolate_bad_batch_and_conflicting_replay() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("events.sqlite");
let log = open(&path).await;
let count = Arc::new(AtomicUsize::new(0));
let observed = count.clone();
hook(&log, move || {
observed.fetch_add(1, Ordering::SeqCst);
true
})
.await;
let before_failures = log.connection.writer().failures();
let (release, gate) = gate(&log).await;
let first = [delta("first", "one")];
let bad = [
delta("rolled-back", "bad prefix"),
event(
"bad-tail",
Fact::ModelObserved {
step_id: "missing".into(),
event: ModelEvent::PartDelta {
id: "part".into(),
text: "bad tail".into(),
provider_options: None,
},
},
),
];
let conflict = [delta("first", "different")];
let last = [delta("last", "two")];
let mut a = Box::pin(log.append_batch(&first));
let mut b = Box::pin(log.append_batch(&bad));
let mut replay = Box::pin(log.append_batch(&first));
let mut c = Box::pin(log.append_batch(&conflict));
let mut d = Box::pin(log.append_batch(&last));
assert!(futures_util::poll!(&mut a).is_pending());
assert!(futures_util::poll!(&mut b).is_pending());
assert!(futures_util::poll!(&mut replay).is_pending());
assert!(futures_util::poll!(&mut c).is_pending());
assert!(futures_util::poll!(&mut d).is_pending());
release.send(()).unwrap();
gate.await.unwrap();
assert_eq!(a.await.unwrap(), [4]);
assert!(matches!(b.await, Err(CommitError::Rejected(_))));
assert_eq!(replay.await.unwrap(), [4]);
assert!(matches!(c.await, Err(CommitError::Rejected(_))));
assert_eq!(d.await.unwrap(), [5]);
assert_eq!(count.load(Ordering::SeqCst), 1);
assert_eq!(log.connection.writer().failures() - before_failures, 2);
assert_eq!(ids(&path), ["opened", "requested", "part", "first", "last"]);
let mut commits = log.subscribe_commits();
commits.borrow_and_update();
assert_eq!(log.append_batch(&first).await.unwrap(), [4]);
assert!(!commits.has_changed().unwrap());
assert_eq!(*commits.borrow(), 5);
assert_eq!(
count.load(Ordering::SeqCst),
1,
"replay has no durable write"
);
log.connection.shutdown().await.unwrap();
}
#[tokio::test]
async fn commit_failure_preserves_domain_rejection_and_stops_pending_control() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("events.sqlite");
let log = open(&path).await;
hook(&log, || false).await;
let (release, gate) = gate(&log).await;
let first = [delta("first", "one")];
let bad = [delta("part", "conflicting original")];
let last = [delta("last", "two")];
let mut a = Box::pin(log.append_batch(&first));
let mut b = Box::pin(log.append_batch(&bad));
let mut c = Box::pin(log.append_batch(&last));
assert!(futures_util::poll!(&mut a).is_pending());
assert!(futures_util::poll!(&mut b).is_pending());
assert!(futures_util::poll!(&mut c).is_pending());
let ran = Arc::new(AtomicUsize::new(0));
let called = ran.clone();
let mut control = Box::pin(log.connection.run(move |_| {
Box::pin(async move {
called.fetch_add(1, Ordering::SeqCst);
Ok(())
})
}));
assert!(futures_util::poll!(&mut control).is_pending());
release.send(()).unwrap();
gate.await.unwrap();
assert!(matches!(a.await, Err(CommitError::OutcomeUnknown(_))));
assert!(matches!(b.await, Err(CommitError::Rejected(_))));
assert!(matches!(c.await, Err(CommitError::OutcomeUnknown(_))));
assert!(matches!(control.await, Err(StoreError::OperationUnknown)));
assert_eq!(ran.load(Ordering::SeqCst), 0);
log.connection.shutdown().await.unwrap();
assert_eq!(ids(&path), ["opened", "requested", "part"]);
let reopened = EventLog::open(&path).await.unwrap();
assert_eq!(reopened.append_batch(&first).await.unwrap(), [4]);
reopened.close().await.unwrap();
}
#[tokio::test]
async fn receipts_wait_for_commit_and_dropped_waiter_is_drained_on_shutdown() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("events.sqlite");
let log = open(&path).await;
let (entered, reached) = oneshot::channel();
let mut entered = Some(entered);
let (release_commit, commit_gate) = std::sync::mpsc::channel();
hook(&log, move || {
entered.take().unwrap().send(()).unwrap();
commit_gate.recv_timeout(Duration::from_secs(10)).unwrap();
true
})
.await;
let (release, gate) = gate(&log).await;
let first = [delta("first", "one")];
let last = [delta("last", "two")];
let mut a = Box::pin(log.append_batch(&first));
let mut b = Box::pin(log.append_batch(&last));
assert!(futures_util::poll!(&mut a).is_pending());
assert!(futures_util::poll!(&mut b).is_pending());
let mut commits = log.subscribe_commits();
commits.borrow_and_update();
release.send(()).unwrap();
gate.await.unwrap();
tokio::time::timeout(Duration::from_secs(5), reached)
.await
.unwrap()
.unwrap();
assert!(futures_util::poll!(&mut a).is_pending());
assert!(futures_util::poll!(&mut b).is_pending());
assert!(!commits.has_changed().unwrap());
assert_eq!(ids(&path), ["opened", "requested", "part"]);
drop(a);
let mut close = Box::pin(log.connection.shutdown());
assert!(futures_util::poll!(&mut close).is_pending());
release_commit.send(()).unwrap();
assert_eq!(b.await.unwrap(), [5]);
close.await.unwrap();
assert_eq!(ids(&path), ["opened", "requested", "part", "first", "last"]);
}
#[tokio::test]
async fn generic_control_keeps_its_original_position_between_observation_groups() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("events.sqlite");
let log = open(&path).await;
let count = Arc::new(AtomicUsize::new(0));
let observed = count.clone();
hook(&log, move || {
observed.fetch_add(1, Ordering::SeqCst);
true
})
.await;
let (release, gate) = gate(&log).await;
let first = [delta("first", "one")];
let last = [delta("last", "two")];
let mut a = Box::pin(log.append_batch(&first));
assert!(futures_util::poll!(&mut a).is_pending());
let mut control = Box::pin(log.connection.run(|db| {
Box::pin(async move {
Ok(
sqlx::query_scalar::<_, i64>("SELECT max(sequence) FROM runtime_events")
.fetch_one(db)
.await?,
)
})
}));
assert!(futures_util::poll!(&mut control).is_pending());
let mut b = Box::pin(log.append_batch(&last));
assert!(futures_util::poll!(&mut b).is_pending());
let mut close = Box::pin(log.connection.shutdown());
assert!(futures_util::poll!(&mut close).is_pending());
release.send(()).unwrap();
gate.await.unwrap();
assert_eq!(a.await.unwrap(), [4]);
assert_eq!(control.await.unwrap(), 4);
assert_eq!(b.await.unwrap(), [5]);
close.await.unwrap();
assert_eq!(count.load(Ordering::SeqCst), 2);
assert_eq!(ids(&path), ["opened", "requested", "part", "first", "last"]);
}
#[tokio::test]
async fn savepoint_failures_abort_the_group_and_close_without_running_control() {
unsafe extern "C" fn deny_savepoint(
mode: *mut std::ffi::c_void,
action: std::ffi::c_int,
operation: *const std::ffi::c_char,
_: *const std::ffi::c_char,
_: *const std::ffi::c_char,
_: *const std::ffi::c_char,
) -> std::ffi::c_int {
if action == rusqlite::ffi::SQLITE_SAVEPOINT && !operation.is_null() {
// SAFETY: SQLite supplies a callback-local NUL-terminated operation.
let operation = unsafe { std::ffi::CStr::from_ptr(operation) }.to_bytes();
let denied = if mode as usize == 1 {
b"RELEASE".as_slice()
} else {
b"ROLLBACK".as_slice()
};
if operation == denied {
return rusqlite::ffi::SQLITE_DENY;
}
}
rusqlite::ffi::SQLITE_OK
}
for mode in [1_usize, 2] {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("events.sqlite");
let log = open(&path).await;
log.connection
.run(move |db| {
Box::pin(async move {
let mut handle = db.lock_handle().await?;
// SAFETY: static callback; mode is an integer tag, never dereferenced.
// The exclusive handle is borrowed only for installing SQLite's authorizer.
assert_eq!(
unsafe {
rusqlite::ffi::sqlite3_set_authorizer(
handle.as_raw_handle().as_mut(),
Some(deny_savepoint),
mode as *mut std::ffi::c_void,
)
},
rusqlite::ffi::SQLITE_OK
);
Ok(())
})
})
.await
.unwrap();
let (release, gate) = gate(&log).await;
let first = [delta("first", "one")];
let bad = [delta("rolled-back", "prefix"), delta("part", "conflict")];
let last = [delta("last", "two")];
let mut a = Box::pin(log.append_batch(&first));
let mut b = Box::pin(log.append_batch(&bad));
let mut c = Box::pin(log.append_batch(&last));
assert!(futures_util::poll!(&mut a).is_pending());
assert!(futures_util::poll!(&mut b).is_pending());
assert!(futures_util::poll!(&mut c).is_pending());
let mut control = Box::pin(log.connection.run::<()>(|_| {
Box::pin(async {
panic!("failed savepoint must retire the owner before pending control")
})
}));
assert!(futures_util::poll!(&mut control).is_pending());
release.send(()).unwrap();
gate.await.unwrap();
for result in [a.await, b.await, c.await] {
assert!(
matches!(result, Err(CommitError::Rejected(_))),
"outer rollback confirmed: {result:?}"
);
}
assert!(matches!(control.await, Err(StoreError::OperationUnknown)));
log.connection.shutdown().await.unwrap();
assert_eq!(ids(&path), ["opened", "requested", "part"]);
let reopened = EventLog::open(&path).await.unwrap();
assert_eq!(reopened.append_batch(&first).await.unwrap(), [4]);
reopened.close().await.unwrap();
}
}
#[tokio::test]
async fn sqlite_automatic_rollback_is_not_falsely_reported_as_confirmed_rollback() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("events.sqlite");
let log = open(&path).await;
log.connection.run(|db| Box::pin(async move {
sqlx::raw_sql("CREATE TEMP TRIGGER rollback_observation BEFORE INSERT ON event_log
WHEN NEW.event_id='poison' BEGIN SELECT RAISE(ROLLBACK, 'injected storage failure'); END;")
.execute(db).await?;
Ok(())
})).await.unwrap();
let (release, gate) = gate(&log).await;
let first = [delta("first", "one")];
let poison = [delta("poison", "two")];
let last = [delta("last", "three")];
let mut a = Box::pin(log.append_batch(&first));
let mut b = Box::pin(log.append_batch(&poison));
let mut c = Box::pin(log.append_batch(&last));
assert!(futures_util::poll!(&mut a).is_pending());
assert!(futures_util::poll!(&mut b).is_pending());
assert!(futures_util::poll!(&mut c).is_pending());
release.send(()).unwrap();
gate.await.unwrap();
assert!(matches!(a.await, Err(CommitError::OutcomeUnknown(_))));
assert!(matches!(b.await, Err(CommitError::OutcomeUnknown(_))));
assert!(
matches!(c.await, Err(CommitError::Rejected(_))),
"unstarted request has no uncertain write"
);
log.connection.shutdown().await.unwrap();
assert_eq!(ids(&path), ["opened", "requested", "part"]);
}
#[tokio::test]
async fn authority_lost_during_last_request_rolls_back_before_commit() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("events.sqlite");
let log = open(&path).await;
let lock = dir.path().join("events.sqlite.writer.lock");
let displaced = dir.path().join("displaced.lock");
log.connection
.run(move |db| {
Box::pin(async move {
db.lock_handle().await?.set_update_hook(move |update| {
if update.table == "event_log" && update.rowid == 5 {
std::fs::rename(&lock, &displaced).unwrap();
}
});
Ok(())
})
})
.await
.unwrap();
let (release, gate) = gate(&log).await;
let first = [delta("first", "one")];
let last = [delta("last", "two")];
let mut a = Box::pin(log.append_batch(&first));
let mut b = Box::pin(log.append_batch(&last));
assert!(futures_util::poll!(&mut a).is_pending());
assert!(futures_util::poll!(&mut b).is_pending());
release.send(()).unwrap();
gate.await.unwrap();
assert!(matches!(a.await, Err(CommitError::Rejected(_))));
assert!(matches!(b.await, Err(CommitError::Rejected(_))));
log.connection.shutdown().await.unwrap();
assert_eq!(ids(&path), ["opened", "requested", "part"]);
}