feat(stateful-templates): journey + docs + concurrency fix; email deferred (M5.6)
- 0063: partial unique index on (app_id, materialized_from) + ON CONFLICT DO NOTHING in the reconciler, so a materialized copy is idempotent under concurrent reconciles (two parallel app-creates no longer duplicate a copy). - stateful_templates journey: a group cron template + a descendant app → a materialized cron copy in `pic triggers ls --app`; re-apply is a NoOp. - docs (§4.5 + CLAUDE.md): cron+queue materialization implemented; group EMAIL templates deferred (per-descendant inbound-secret reseal needs the master key threaded through the hooks + a secret-ref schema) and cleanly rejected at apply for now, alongside a `materialized` ls column. Completes the v1.2 near-term batch (M1-M5, cron+queue); group email is the one scoped follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -141,7 +141,10 @@ async fn materialize_one(
|
||||
}
|
||||
|
||||
// Parent row: copy the template's settings, owned by the app, linked back.
|
||||
let new_id: (Uuid,) = sqlx::query_as(
|
||||
// `ON CONFLICT DO NOTHING` (against the (app_id, materialized_from) partial
|
||||
// unique index) makes this idempotent under concurrent reconciles — the
|
||||
// loser gets no RETURNING row and skips the detail insert.
|
||||
let new_id: Option<(Uuid,)> = sqlx::query_as(
|
||||
"INSERT INTO triggers ( \
|
||||
app_id, script_id, kind, enabled, dispatch_mode, \
|
||||
retry_max_attempts, retry_backoff, retry_base_ms, \
|
||||
@@ -149,12 +152,17 @@ async fn materialize_one(
|
||||
SELECT $1, script_id, kind, enabled, dispatch_mode, \
|
||||
retry_max_attempts, retry_backoff, retry_base_ms, \
|
||||
registered_by_principal, id \
|
||||
FROM triggers WHERE id = $2 RETURNING id",
|
||||
FROM triggers WHERE id = $2 \
|
||||
ON CONFLICT DO NOTHING RETURNING id",
|
||||
)
|
||||
.bind(app_id)
|
||||
.bind(template_id)
|
||||
.fetch_one(&mut **tx)
|
||||
.fetch_optional(&mut **tx)
|
||||
.await?;
|
||||
let Some(new_id) = new_id else {
|
||||
// A concurrent reconcile already created this copy.
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
match kind {
|
||||
"cron" => {
|
||||
|
||||
Reference in New Issue
Block a user