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:
MechaCat02
2026-07-02 22:33:11 +02:00
parent ccd3644aa4
commit 50205e7b7e
8 changed files with 299 additions and 76 deletions

View File

@@ -220,6 +220,8 @@ pub async fn run(app_ident: &str, dir: &Path, force: bool, mode: OutputMode) ->
visibility_timeout_secs: Some(d.visibility_timeout_secs),
dispatch_mode,
retry_max_attempts,
// app-owned triggers are never shared (group-only).
shared: false,
});
}
// `email` is skipped: the server stores the sealed secret value,

View File

@@ -516,6 +516,11 @@ pub struct QueueTriggerSpec {
pub dispatch_mode: Option<DispatchMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retry_max_attempts: Option<u32>,
/// §11.6 D3: `true` for a shared-QUEUE group consumer over a declared
/// `kind = "queue"` collection — competing per-descendant consumers drain
/// one group-owned store. Group-only.
#[serde(default, skip_serializing_if = "is_false")]
pub shared: bool,
}
/// `[secrets] names = [...]` — declares which secrets the app expects.