Watch
0
0
Fork
You've already forked hyperhive
0

drop the host side of the agent broker send

Remove the agent-side Send and AckUntil requests, their hive-c0re handlers
(handle_send with its 4 KiB size cap, handle_ack_until) and Broker::ack_until.
Everything that existed only for them goes with them: the OkWarn response
(its sole producer was Send's reserved-name warning), Recv's max parameter
and RECV_BATCH_MAX (the harness never asked for more than one message),
OPERATOR_SEND_REFUSAL, and the MESSAGE_MAX_BYTES / check_size limit (the
reminder timer keeps its own copy of the cap, so its comment is reworded).

Messages.remaining stays on the wire (default on read) because a harness
built before this change still requires it. Update the conventions docs.
This commit is contained in:
damocles 2026-11-08 02:35:34 +01:00 • committed by mara
commit 1c3b5e52cf

View file

@ -229,8 +229,9 @@ impl Broker {
/// lines without popping the queue.
pub fn count_pending(&self, recipient: &str) -> Result<u64> {
let conn = self.conn.lock().unwrap();
// Skip rows closed by `ack_until` while still pending — they
// will never pop, so counting them would show phantom unread.
// Skip rows acked while still pending (the removed `ack_until` could
// leave such rows in an older db) — they never pop, so counting them
// would show phantom unread.
let n: i64 = conn.query_row(
"SELECT COUNT(*) FROM messages
WHERE recipient = ?1 AND delivered_at IS NULL AND acked_at IS NULL",
@ -345,9 +346,10 @@ impl Broker {
let mut inflight = self.inflight.lock().unwrap();
let conn = self.conn.lock().unwrap();
let max_i = i64::try_from(max).unwrap_or(i64::MAX);
// `acked_at IS NULL` matters for rows closed by `ack_until`
// while still pending (never delivered): they carry an ack but
// no `delivered_at`, and must not pop.
// `acked_at IS NULL` matters for rows acked while still pending
// (never delivered; the removed `ack_until` could leave such rows in
// an older db): they carry an ack but no `delivered_at`, and must
// not pop.
let mut stmt = conn.prepare(
"SELECT id, sender, recipient, body, in_reply_to
FROM messages
@ -460,38 +462,6 @@ impl Broker {
Ok(u64::try_from(n).unwrap_or(0))
}
/// Bulk-ack every message addressed to `recipient` with row id
/// `<= up_to` that isn't acked yet — pending AND delivered rows
/// alike. The agent-facing triage tool behind `AckUntil`: after a
/// redelivered flood the agent acks everything up to the highest id
/// it has seen instead of re-popping each row. Recipient-scoped by
/// the WHERE clause, so an agent can never touch another agent's
/// rows. Also drains the in-memory `unacked_ids` / `requeued_ids`
/// bookkeeping below the cutoff so a later `ack_turn` doesn't
/// re-update rows this call already closed and a stale redelivery
/// tag doesn't outlive its row. Returns the number of rows newly
/// acked.
///
/// # Errors
///
/// Propagates sqlite errors from the `UPDATE`.
pub fn ack_until(&self, recipient: &str, up_to: i64) -> Result<u64> {
// Same lock order as `recv` / `ack_turn` / `requeue_inflight`:
// `inflight` FIRST, then `conn`.
let mut inflight = self.inflight.lock().unwrap();
if let Some(state) = inflight.get_mut(recipient) {
state.unacked_ids.retain(|&id| id > up_to);
state.requeued_ids.retain(|&id| id > up_to);
}
let conn = self.conn.lock().unwrap();
let n = conn.execute(
"UPDATE messages SET acked_at = ?1
WHERE recipient = ?2 AND id <= ?3 AND acked_at IS NULL",
params![Utc::now().timestamp(), recipient, up_to],
)?;
Ok(u64::try_from(n).unwrap_or(0))
}
/// Resurface every message the broker previously handed to this
/// recipient that never got `acked_at` set. Used by the harness at
/// boot to recover from the crashed-mid-turn / OOM-killed /
@ -711,61 +681,6 @@ mod tests {
assert_eq!(broker.ack_turn("b").unwrap(), 0);
}
/// Bulk triage: three queued messages, agent acks up to the second
/// id — the first two never deliver again, the third still pops.
#[test]
fn ack_until_acks_only_rows_at_or_below_cutoff() {
let h = open_broker();
let broker = &h.broker;
broker.send(&msg("a", "b", "one")).unwrap();
broker.send(&msg("a", "b", "two")).unwrap();
broker.send(&msg("a", "b", "three")).unwrap();
// Pop the first two so we know their ids (FIFO).
let d1 = pop_one(broker, "b").expect("popped one");
let d2 = pop_one(broker, "b").expect("popped two");
assert_eq!(broker.ack_until("b", d2.id).unwrap(), 2);
// The cutoff also drained the in-memory unacked list, so a
// turn-level ack right after finds nothing left to do.
assert_eq!(broker.ack_turn("b").unwrap(), 0);
// A restart-style requeue finds nothing below the cutoff …
assert_eq!(broker.requeue_inflight("b").unwrap(), 0);
// … and the third message (id above the cutoff) still pops.
let d3 = pop_one(broker, "b").expect("third still pending");
assert_eq!(d3.message.body, "three");
assert!(d1.id < d2.id && d2.id < d3.id);
}
/// Pending (never-delivered) rows below the cutoff are acked too —
/// that's the whole point for a stale backlog the agent never
/// popped individually.
#[test]
fn ack_until_covers_pending_rows() {
let h = open_broker();
let broker = &h.broker;
broker.send(&msg("a", "b", "stale-1")).unwrap();
broker.send(&msg("a", "b", "stale-2")).unwrap();
// Learn the highest id by peeking via recent_for (non-mutating).
let rows = broker.recent_for("b", 10).unwrap();
let max_id = rows.iter().map(|r| r.id).max().expect("rows");
assert_eq!(broker.ack_until("b", max_id).unwrap(), 2);
assert!(pop_one(broker, "b").is_none(), "backlog cleared");
}
/// Recipient scoping: acking b's inbox never touches c's rows,
/// even when c's ids fall below the cutoff.
#[test]
fn ack_until_is_recipient_scoped() {
let h = open_broker();
let broker = &h.broker;
broker.send(&msg("a", "c", "for-c")).unwrap();
broker.send(&msg("a", "b", "for-b")).unwrap();
let rows = broker.recent_for("b", 10).unwrap();
let max_id = rows.iter().map(|r| r.id).max().expect("rows");
assert_eq!(broker.ack_until("b", max_id).unwrap(), 1);
let d = pop_one(broker, "c").expect("c's message untouched");
assert_eq!(d.message.body, "for-c");
}
/// Crash-recovery: send → recv → (no ack) → `requeue_inflight`
/// resets `delivered_at` + tags the next pop as redelivered. After
/// that `ack_turn` closes it out cleanly.
@ -884,9 +799,9 @@ mod tests {
assert_eq!(broker.ack_turn("b").unwrap(), 5);
}
/// Wake messages go through sqlite and get real row ids that the
/// agent can pass to `ack_until`. A wake sent when no recv is parked
/// is not lost — it sits in sqlite until the next recv pops it.
/// Wake messages go through sqlite and get real row ids. A wake sent
/// when no recv is parked is not lost — it sits in sqlite until the
/// next recv pops it.
#[test]
fn wake_persisted_with_real_id_and_ackable() {
let h = open_broker();
@ -903,13 +818,9 @@ mod tests {
// Every delivery has a real (non-zero) id.
assert!(batch[0].id > 0);
assert!(batch[1].id > 0);
// Agent can ack them explicitly via ack_until.
let max_id = batch.iter().map(|d| d.id).max().unwrap();
assert_eq!(broker.ack_turn("b").unwrap(), 2);
// Confirming the rows are now closed.
assert!(pop_one(broker, "b").is_none());
// ack_until on already-acked ids is a no-op.
assert_eq!(broker.ack_until("b", max_id).unwrap(), 0);
}
/// Wakes and regular mail both go through sqlite: FIFO by insertion order.