tenseleyflow/shithub / 5690fdf

Browse files

billing: TryAcquireWebhookEventLock sqlc query (transaction-scoped advisory lock)

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
5690fdf4b34ff55e6691071fa3d45d2f7f0875f6
Parents
9374798
Tree
d8badf9

3 changed files

StatusFile+-
M internal/billing/queries/billing.sql 13 0
M internal/billing/sqlc/billing.sql.go 21 0
M internal/billing/sqlc/querier.go 11 0
internal/billing/queries/billing.sqlmodified
@@ -457,6 +457,19 @@ UPDATE billing_webhook_events
457457
  WHERE provider = 'stripe'
458458
    AND provider_event_id = sqlc.arg(provider_event_id)::text;
459459
 
460
+-- name: TryAcquireWebhookEventLock :one
461
+-- PRO08 A3: transaction-scoped advisory lock keyed on the hash of
462
+-- the provider_event_id. Two concurrent webhook deliveries for the
463
+-- same event_id race past CreateWebhookEventReceipt before either has
464
+-- marked it processed; without serialization, both proceed to apply
465
+-- and double-mutate state. This lock makes the apply path mutually
466
+-- exclusive per event. Returns true when acquired; false means
467
+-- another worker holds it — caller should let Stripe retry.
468
+--
469
+-- pg_try_advisory_xact_lock takes a bigint; hashtext returns int4
470
+-- which sign-extends safely. The lock auto-releases at txn end.
471
+SELECT pg_try_advisory_xact_lock(hashtext($1)::bigint) AS acquired;
472
+
460473
 -- name: ListFailedWebhookEvents :many
461474
 -- Operator query for "events we received but failed to process."
462475
 -- A row is "failed" when it has a non-empty process_error OR when
internal/billing/sqlc/billing.sql.gomodified
@@ -1691,6 +1691,27 @@ func (q *Queries) SetWebhookEventSubject(ctx context.Context, db DBTX, arg SetWe
16911691
 	return err
16921692
 }
16931693
 
1694
+const tryAcquireWebhookEventLock = `-- name: TryAcquireWebhookEventLock :one
1695
+SELECT pg_try_advisory_xact_lock(hashtext($1)::bigint) AS acquired
1696
+`
1697
+
1698
+// PRO08 A3: transaction-scoped advisory lock keyed on the hash of
1699
+// the provider_event_id. Two concurrent webhook deliveries for the
1700
+// same event_id race past CreateWebhookEventReceipt before either has
1701
+// marked it processed; without serialization, both proceed to apply
1702
+// and double-mutate state. This lock makes the apply path mutually
1703
+// exclusive per event. Returns true when acquired; false means
1704
+// another worker holds it — caller should let Stripe retry.
1705
+//
1706
+// pg_try_advisory_xact_lock takes a bigint; hashtext returns int4
1707
+// which sign-extends safely. The lock auto-releases at txn end.
1708
+func (q *Queries) TryAcquireWebhookEventLock(ctx context.Context, db DBTX, hashtext string) (bool, error) {
1709
+	row := db.QueryRow(ctx, tryAcquireWebhookEventLock, hashtext)
1710
+	var acquired bool
1711
+	err := row.Scan(&acquired)
1712
+	return acquired, err
1713
+}
1714
+
16941715
 const upsertInvoice = `-- name: UpsertInvoice :one
16951716
 
16961717
 INSERT INTO billing_invoices (
internal/billing/sqlc/querier.gomodified
@@ -67,6 +67,17 @@ type Querier interface {
6767
 	// subsequent apply fails. Migration 0075's CHECK constraint enforces
6868
 	// both-or-neither; callers must pass a non-zero subject.
6969
 	SetWebhookEventSubject(ctx context.Context, db DBTX, arg SetWebhookEventSubjectParams) error
70
+	// PRO08 A3: transaction-scoped advisory lock keyed on the hash of
71
+	// the provider_event_id. Two concurrent webhook deliveries for the
72
+	// same event_id race past CreateWebhookEventReceipt before either has
73
+	// marked it processed; without serialization, both proceed to apply
74
+	// and double-mutate state. This lock makes the apply path mutually
75
+	// exclusive per event. Returns true when acquired; false means
76
+	// another worker holds it — caller should let Stripe retry.
77
+	//
78
+	// pg_try_advisory_xact_lock takes a bigint; hashtext returns int4
79
+	// which sign-extends safely. The lock auto-releases at txn end.
80
+	TryAcquireWebhookEventLock(ctx context.Context, db DBTX, hashtext string) (bool, error)
7081
 	// ─── billing_invoices ──────────────────────────────────────────────
7182
 	// PRO03: writes both legacy `org_id` and polymorphic
7283
 	// `(subject_kind, subject_id)`. Callers continue to bind org_id only;