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:
@@ -260,6 +260,10 @@ pub enum BundleTrigger {
|
||||
dispatch_mode: Option<TriggerDispatchMode>,
|
||||
#[serde(default)]
|
||||
retry_max_attempts: Option<u32>,
|
||||
/// §11.6 D3: `true` for a shared-QUEUE group consumer — materializes a
|
||||
/// competing consumer per descendant app, all claiming the group store.
|
||||
#[serde(default)]
|
||||
shared: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -313,8 +317,9 @@ impl BundleTrigger {
|
||||
Self::Kv { shared, .. }
|
||||
| Self::Docs { shared, .. }
|
||||
| Self::Files { shared, .. }
|
||||
| Self::Pubsub { shared, .. } => *shared,
|
||||
Self::Cron { .. } | Self::Email { .. } | Self::Queue { .. } => false,
|
||||
| Self::Pubsub { shared, .. }
|
||||
| Self::Queue { shared, .. } => *shared,
|
||||
Self::Cron { .. } | Self::Email { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +395,9 @@ impl BundleTrigger {
|
||||
..
|
||||
} => format!("pubsub|{script}|{topic_pattern}|{sealed}|{shared}"),
|
||||
Self::Email { script, .. } => format!("email|{script}"),
|
||||
Self::Queue { queue_name, .. } => format!("queue|{queue_name}"),
|
||||
Self::Queue {
|
||||
queue_name, shared, ..
|
||||
} => format!("queue|{queue_name}|{shared}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,9 +794,10 @@ impl ApplyService {
|
||||
BundleTrigger::Pubsub { topic_pattern, .. } => {
|
||||
(topic_pattern.split('.').next().unwrap_or(""), "topic")
|
||||
}
|
||||
BundleTrigger::Queue { queue_name, .. } => (queue_name.as_str(), "queue"),
|
||||
_ => {
|
||||
return Err(ApplyError::Invalid(format!(
|
||||
"a `shared` trigger must be a kv/docs/files/pubsub kind; \
|
||||
"a `shared` trigger must be a kv/docs/files/pubsub/queue kind; \
|
||||
`{}` has no shared collection store",
|
||||
t.kind_str()
|
||||
)));
|
||||
@@ -3416,7 +3424,7 @@ fn current_trigger_identity(t: &Trigger, name_by_id: &HashMap<ScriptId, String>)
|
||||
Some(format!("pubsub|{script}|{topic_pattern}|{sealed}|{shared}"))
|
||||
}
|
||||
TriggerDetails::Email { .. } => Some(format!("email|{script}")),
|
||||
TriggerDetails::Queue { queue_name, .. } => Some(format!("queue|{queue_name}")),
|
||||
TriggerDetails::Queue { queue_name, .. } => Some(format!("queue|{queue_name}|{shared}")),
|
||||
TriggerDetails::DeadLetter { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -4254,6 +4262,7 @@ mod tests {
|
||||
visibility_timeout_secs: vis,
|
||||
dispatch_mode: None,
|
||||
retry_max_attempts: None,
|
||||
shared: false,
|
||||
};
|
||||
assert!(validate_trigger_shape(&queue(Some(10))).is_err());
|
||||
assert!(validate_trigger_shape(&queue(Some(
|
||||
@@ -4715,6 +4724,7 @@ mod tests {
|
||||
visibility_timeout_secs: None,
|
||||
dispatch_mode: None,
|
||||
retry_max_attempts: None,
|
||||
shared: false,
|
||||
}];
|
||||
let p = compute_diff(¤t, &b);
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user