feat(shared-queues): group-keyed queue store + enqueue service + SDK (D3.1)

The producer side of shared durable queues:
- migration 0065 group_queue_messages (mirrors 0034, keyed by (group_id,
  collection); CASCADE on group delete).
- GroupQueueRepo/PostgresGroupQueueRepo: enqueue + the competing-consumer
  claim (FOR UPDATE SKIP LOCKED) + ack/nack/drop_exhausted/reclaim/depth.
- GroupQueueService trait (shared) + GroupQueueServiceImpl: resolve owning
  group (kind='queue') from cx.app_id's chain, require editor+
  (GroupQueueEnqueue, fails closed on anon), size-cap, enqueue.
- SDK: `queue::shared_collection("name")` -> GroupQueueHandle with
  `.enqueue(msg[, opts])` / `.depth()` / `.depth_pending()`; wired through
  Services + the picloud binary.
- authz: Capability::GroupQueueEnqueue(GroupId), editor+ / script:write.

Deterministic test proves competing consumers claim each message exactly
once. Consumption wiring (materialized consumers + dispatcher branch) lands
in D3.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-02 22:08:21 +02:00
parent 19ba6dbfb4
commit ccd3644aa4
12 changed files with 929 additions and 28 deletions

View File

@@ -21,12 +21,13 @@ use std::sync::Arc;
use crate::{
DeadLetterService, DocsService, EmailService, FilesService, GroupDocsService,
GroupFilesService, GroupKvService, GroupPubsubService, HttpService, InvokeService, KvService,
ModuleSource, NoopDeadLetterService, NoopDocsService, NoopEmailService, NoopEventEmitter,
NoopFilesService, NoopGroupDocsService, NoopGroupFilesService, NoopGroupKvService,
NoopGroupPubsubService, NoopHttpService, NoopInvokeService, NoopKvService, NoopModuleSource,
NoopPubsubService, NoopQueueService, NoopSecretsService, NoopUsersService, NoopVarsService,
PubsubService, QueueService, SecretsService, ServiceEventEmitter, UsersService, VarsService,
GroupFilesService, GroupKvService, GroupPubsubService, GroupQueueService, HttpService,
InvokeService, KvService, ModuleSource, NoopDeadLetterService, NoopDocsService,
NoopEmailService, NoopEventEmitter, NoopFilesService, NoopGroupDocsService,
NoopGroupFilesService, NoopGroupKvService, NoopGroupPubsubService, NoopGroupQueueService,
NoopHttpService, NoopInvokeService, NoopKvService, NoopModuleSource, NoopPubsubService,
NoopQueueService, NoopSecretsService, NoopUsersService, NoopVarsService, PubsubService,
QueueService, SecretsService, ServiceEventEmitter, UsersService, VarsService,
};
/// SDK service bundle. See module docs for the lifecycle and the v1.1.x
@@ -139,6 +140,12 @@ pub struct Services {
/// `shared = true` group pubsub triggers. Wired via
/// [`Services::with_group_pubsub`]; defaults to `NoopGroupPubsubService`.
pub group_pubsub: Arc<dyn GroupPubsubService>,
/// Shared cross-app group QUEUES (§11.6 D3). Scripts get
/// `queue::shared_collection(name)` — enqueue into a group-keyed store
/// drained by competing per-descendant consumers. Wired via
/// [`Services::with_group_queue`]; defaults to `NoopGroupQueueService`.
pub group_queue: Arc<dyn GroupQueueService>,
}
impl Services {
@@ -185,6 +192,7 @@ impl Services {
group_docs: Arc::new(NoopGroupDocsService),
group_files: Arc::new(NoopGroupFilesService),
group_pubsub: Arc::new(NoopGroupPubsubService),
group_queue: Arc::new(NoopGroupQueueService),
}
}
@@ -217,6 +225,13 @@ impl Services {
self
}
/// Set the §11.6 D3 shared group-QUEUE service (picloud binary; tests leave noop).
#[must_use]
pub fn with_group_queue(mut self, group_queue: Arc<dyn GroupQueueService>) -> Self {
self.group_queue = group_queue;
self
}
/// All-noop bundle for tests that build an `Engine` but don't
/// exercise the stateful services. Returns the same shape as
/// `Services::new` so callers can't accidentally rely on a stub