feat(queue): dead-letter store for group shared queues (§11.6 D3 / Track A M2)

An exhausted SHARED durable-queue message was `drop_exhausted()`-ed with a
warning — silent data loss. Per-app queues persist to `dead_letters` (0010);
add the symmetric group store so an exhausted shared-queue message is preserved
and operator-visible.

- Migration 0068: `group_dead_letters`, keyed by (group_id, collection),
  CASCADE on the group (an app delete leaves the data — it belongs to the
  group, not the consuming app).
- `GroupQueueRepo::dead_letter` (replaces `drop_exhausted`): one tx that INSERTs
  the dead-letter + DELETEs the live message, filtered by claim_token so a lost
  lease can't dead-letter a re-claimed message (mirrors queue_repo::dead_letter).
- Dispatcher `q_terminal` shared arm now dead-letters instead of dropping. It
  returns None (not the dl id) so the per-app `fan_out_dead_letter` is SKIPPED —
  firing the consuming app's per-app handlers on a shared message (competing
  consumers → nondeterministic app) would be wrong. Fan-out to a *shared*
  dead_letter trigger is deferred (needs a new trigger kind).
- Read side: `GroupDeadLetterRepo::list_for_group` backs a new read-only
  operator endpoint GET /api/v1/admin/groups/{id}/dead-letters (GroupKvRead,
  mirrors the M4 group-blobs surface). `pic dead-letters ls --group` deferred
  (optional; the HTTP endpoint is the operator surface).

Pinned by group_queue::dead_letter_moves_an_exhausted_message_to_the_group_store
(store move + claim-token-mismatch guard + operator read). Schema golden
reblessed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-11 14:41:04 +02:00
parent c06a9e801e
commit fd4336e883
9 changed files with 536 additions and 28 deletions

View File

@@ -273,6 +273,11 @@ pub async fn build_app(
));
// §11.6 D3: the group shared-queue store + enqueue service.
let group_queue_repo = Arc::new(PostgresGroupQueueRepo::new(pool.clone()));
let group_dead_letter_repo = Arc::new(
picloud_manager_core::group_dead_letter_repo::PostgresGroupDeadLetterRepo::new(
pool.clone(),
),
);
let group_queue: Arc<dyn picloud_shared::GroupQueueService> =
Arc::new(GroupQueueServiceImpl::new(
group_queue_repo.clone(),
@@ -722,6 +727,7 @@ pub async fn build_app(
kv: group_kv_repo.clone(),
docs: group_docs_repo.clone(),
files: group_files_repo.clone(),
dead_letters: group_dead_letter_repo.clone(),
groups: groups_repo.clone(),
authz: authz.clone(),
},