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:
@@ -170,6 +170,10 @@ pub enum Capability {
|
||||
/// group; fails closed for an anonymous principal, like `GroupKvWrite` — a
|
||||
/// publish is a write to shared state.
|
||||
GroupPubsubPublish(GroupId),
|
||||
/// Enqueue into a group-owned shared QUEUE (§11.6 D3). editor+ on the owning
|
||||
/// group; fails closed for an anonymous principal, like `GroupKvWrite` — an
|
||||
/// enqueue is a write to shared state.
|
||||
GroupQueueEnqueue(GroupId),
|
||||
/// Send an outbound email from a script in this app (v1.1.7). Maps
|
||||
/// to `script:write` on API keys (sending mail is an outbound
|
||||
/// side-effect like an HTTP request). Granted to `editor`+.
|
||||
@@ -243,7 +247,8 @@ impl Capability {
|
||||
| Self::GroupDocsWrite(_)
|
||||
| Self::GroupFilesRead(_)
|
||||
| Self::GroupFilesWrite(_)
|
||||
| Self::GroupPubsubPublish(_) => None,
|
||||
| Self::GroupPubsubPublish(_)
|
||||
| Self::GroupQueueEnqueue(_) => None,
|
||||
Self::AppRead(id)
|
||||
| Self::AppWriteScript(id)
|
||||
| Self::AppWriteRoute(id)
|
||||
@@ -321,6 +326,7 @@ impl Capability {
|
||||
| Self::GroupDocsWrite(_)
|
||||
| Self::GroupFilesWrite(_)
|
||||
| Self::GroupPubsubPublish(_)
|
||||
| Self::GroupQueueEnqueue(_)
|
||||
| Self::AppInvoke(_) => Scope::ScriptWrite,
|
||||
Self::AppWriteRoute(_) => Scope::RouteWrite,
|
||||
Self::AppManageDomains(_) => Scope::DomainManage,
|
||||
@@ -543,7 +549,8 @@ async fn role_grants(
|
||||
| Capability::GroupDocsWrite(g)
|
||||
| Capability::GroupFilesRead(g)
|
||||
| Capability::GroupFilesWrite(g)
|
||||
| Capability::GroupPubsubPublish(g) => {
|
||||
| Capability::GroupPubsubPublish(g)
|
||||
| Capability::GroupQueueEnqueue(g) => {
|
||||
group_member_grants(repo, principal.user_id, cap, g).await
|
||||
}
|
||||
// Creating a root-level group is an instance act — members
|
||||
@@ -618,7 +625,8 @@ const fn group_role_satisfies(role: AppRole, cap: Capability) -> bool {
|
||||
| Capability::GroupKvWrite(_)
|
||||
| Capability::GroupDocsWrite(_)
|
||||
| Capability::GroupFilesWrite(_)
|
||||
| Capability::GroupPubsubPublish(_) => {
|
||||
| Capability::GroupPubsubPublish(_)
|
||||
| Capability::GroupQueueEnqueue(_) => {
|
||||
matches!(role, AppRole::Editor | AppRole::AppAdmin)
|
||||
}
|
||||
// group_admin manages the group + reads secret VALUES (the
|
||||
|
||||
Reference in New Issue
Block a user