feat(shared-queues): materialized competing consumers + dispatcher branch (D3.2)
The consumption side of shared durable queues: - Thread `shared` through BundleTrigger::Queue + QueueTriggerSpec + the trigger identity; validate_bundle_for requires a shared queue on a group to name a declared kind='queue' collection (a shared queue on an app is rejected by the existing app-owner shared guard). - materialize: a shared queue template materializes a consumer per descendant (the M5 one-consumer-slot skip is bypassed for shared — competing consumers are intended; each descendant gets one copy). - dispatcher: ActiveQueueConsumer gains shared_group (from the materialized copy's source template via LEFT JOIN); dispatch_one_queue + handle_queue_failure route claim/ack/nack/terminal to the group store when shared_group is Some, via q_claim/q_ack/q_nack/q_terminal helpers. A group claim is normalized to a ClaimedMessage under the consuming app so the handler path is unchanged; the reclaim task also drains the group store. Exhausted shared messages are dropped (no group dead-letter store yet — documented deferral). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
//! Deterministic: drives `GroupQueueRepo` directly (no dispatcher). Skips when
|
||||
//! `DATABASE_URL` is unset.
|
||||
|
||||
#![allow(clippy::too_many_lines)]
|
||||
#![allow(clippy::too_many_lines, clippy::many_single_char_names)]
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -52,8 +52,8 @@ async fn competing_consumers_claim_each_message_exactly_once() {
|
||||
let repo = PostgresGroupQueueRepo::new(pool.clone());
|
||||
|
||||
// Enqueue 50 distinct messages into the shared `tasks` queue.
|
||||
const N: usize = 50;
|
||||
for i in 0..N {
|
||||
let n_msgs: usize = 50;
|
||||
for i in 0..n_msgs {
|
||||
repo.enqueue(NewGroupQueueMessage {
|
||||
group_id: group,
|
||||
collection: "tasks".into(),
|
||||
@@ -70,14 +70,9 @@ async fn competing_consumers_claim_each_message_exactly_once() {
|
||||
// Every claimed payload id must be unique — no message delivered twice.
|
||||
let claim_loop = |repo: PostgresGroupQueueRepo, group: GroupId| async move {
|
||||
let mut got: Vec<i64> = Vec::new();
|
||||
loop {
|
||||
match repo.claim(group, "tasks").await.unwrap() {
|
||||
Some(msg) => {
|
||||
got.push(msg.payload["i"].as_i64().unwrap());
|
||||
assert!(repo.ack(msg.id, msg.claim_token).await.unwrap(), "ack ok");
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
while let Some(msg) = repo.claim(group, "tasks").await.unwrap() {
|
||||
got.push(msg.payload["i"].as_i64().unwrap());
|
||||
assert!(repo.ack(msg.id, msg.claim_token).await.unwrap(), "ack ok");
|
||||
}
|
||||
got
|
||||
};
|
||||
@@ -96,10 +91,10 @@ async fn competing_consumers_claim_each_message_exactly_once() {
|
||||
let unique: HashSet<i64> = all.iter().copied().collect();
|
||||
assert_eq!(
|
||||
all.len(),
|
||||
N,
|
||||
n_msgs,
|
||||
"every message delivered exactly once (no dupes)"
|
||||
);
|
||||
assert_eq!(unique.len(), N, "all N distinct ids covered");
|
||||
assert_eq!(unique.len(), n_msgs, "all distinct ids covered");
|
||||
assert_eq!(
|
||||
repo.depth(group, "tasks").await.unwrap(),
|
||||
0,
|
||||
|
||||
Reference in New Issue
Block a user