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:
@@ -337,6 +337,11 @@ pub struct ActiveQueueConsumer {
|
||||
pub retry_backoff: BackoffShape,
|
||||
pub retry_base_ms: u32,
|
||||
pub registered_by_principal: AdminUserId,
|
||||
/// §11.6 D3: `Some(group)` when this consumer is a materialized copy of a
|
||||
/// SHARED group queue template — it drains `group_queue_messages(group,
|
||||
/// queue_name)` (competing consumers) instead of the per-app store. `None`
|
||||
/// for an ordinary per-app or non-shared-template consumer.
|
||||
pub shared_group: Option<GroupId>,
|
||||
}
|
||||
|
||||
/// What the inbound-email webhook receiver needs to verify + dispatch a
|
||||
@@ -1792,13 +1797,19 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
&self,
|
||||
) -> Result<Vec<ActiveQueueConsumer>, TriggerRepoError> {
|
||||
let rows: Vec<QueueConsumerRow> = sqlx::query_as(
|
||||
// §11.6 D3: LEFT JOIN the SOURCE template of a materialized copy; if
|
||||
// that template is a SHARED group queue, `shared_group` is its
|
||||
// owning group and this consumer drains the group store instead of
|
||||
// the per-app one.
|
||||
"SELECT t.id AS trigger_id, t.app_id, t.script_id, d.queue_name, \
|
||||
d.visibility_timeout_secs, \
|
||||
t.retry_max_attempts, t.retry_backoff, t.retry_base_ms, \
|
||||
t.registered_by_principal \
|
||||
t.registered_by_principal, tmpl.group_id AS shared_group \
|
||||
FROM triggers t \
|
||||
JOIN queue_trigger_details d ON d.trigger_id = t.id \
|
||||
JOIN scripts s ON s.id = t.script_id \
|
||||
LEFT JOIN triggers tmpl \
|
||||
ON tmpl.id = t.materialized_from AND tmpl.shared = TRUE \
|
||||
WHERE t.kind = 'queue' AND t.enabled = TRUE AND s.enabled = TRUE \
|
||||
AND t.app_id IS NOT NULL",
|
||||
)
|
||||
@@ -1817,6 +1828,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
.unwrap_or(BackoffShape::Exponential),
|
||||
retry_base_ms: u32::try_from(r.retry_base_ms).unwrap_or(1000),
|
||||
registered_by_principal: r.registered_by_principal.into(),
|
||||
shared_group: r.shared_group.map(Into::into),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -2096,6 +2108,9 @@ struct QueueConsumerRow {
|
||||
retry_backoff: String,
|
||||
retry_base_ms: i32,
|
||||
registered_by_principal: Uuid,
|
||||
// §11.6 D3: the owning group of a SHARED queue template this consumer was
|
||||
// materialized from; NULL for a per-app / non-shared consumer.
|
||||
shared_group: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
|
||||
Reference in New Issue
Block a user