feat: sync multilayer td integration
This commit is contained in:
parent
20a310f6ca
commit
766c5db992
491 changed files with 26235 additions and 35340 deletions
222
internal/store/postgres/auth_identity_lock.go
Normal file
222
internal/store/postgres/auth_identity_lock.go
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
|
||||
"telesrv/internal/store"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
const (
|
||||
// authIdentityAdvisoryNamespace is deliberately a two-int advisory-lock
|
||||
// namespace. PostgreSQL keeps it disjoint from the one-bigint advisory locks
|
||||
// used elsewhere in the store. AUTH in ASCII is stable and recognizable in
|
||||
// pg_locks diagnostics.
|
||||
authIdentityAdvisoryNamespace int32 = 0x41555448
|
||||
authIdentityTxMaxAttempts = 3
|
||||
)
|
||||
|
||||
var errAuthIdentityChanged = errors.New("auth key permanent identity changed while acquiring locks")
|
||||
|
||||
type authKeyIdentityHint struct {
|
||||
found bool
|
||||
expiresAt int
|
||||
bound bool
|
||||
permID int64
|
||||
identityID int64
|
||||
hasIdentity bool
|
||||
}
|
||||
|
||||
// withAuthIdentityTx gives identity-sensitive stores an explicit transaction
|
||||
// boundary. Application-level identity visibility changes are retried after a
|
||||
// savepoint/transaction rollback. PostgreSQL deadlock/serialization retries are
|
||||
// only safe when this store owns the top-level transaction; an injected pgx.Tx
|
||||
// is never silently replayed after 40P01/40001.
|
||||
func withAuthIdentityTx(
|
||||
ctx context.Context,
|
||||
db sqlcgen.DBTX,
|
||||
op string,
|
||||
fn func(pgx.Tx) error,
|
||||
) error {
|
||||
_, embedded := db.(pgx.Tx)
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < authIdentityTxMaxAttempts; attempt++ {
|
||||
err := withTx(ctx, db, op, fn)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
lastErr = err
|
||||
switch {
|
||||
case errors.Is(err, errAuthIdentityChanged):
|
||||
// The attempt owns a nested savepoint even for an injected pgx.Tx,
|
||||
// so all row/advisory locks from the stale hint have been released
|
||||
// before the next READ COMMITTED statement snapshot is taken.
|
||||
continue
|
||||
case !embedded && isAuthIdentityRetryableDatabaseError(err):
|
||||
// Defensive retry only. The identity gate is the deadlock fix; this
|
||||
// does not substitute for the global lock order.
|
||||
continue
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s did not stabilize after %d attempts: %w", op, authIdentityTxMaxAttempts, lastErr)
|
||||
}
|
||||
|
||||
func isAuthIdentityRetryableDatabaseError(err error) bool {
|
||||
var pgErr *pgconn.PgError
|
||||
return errors.As(err, &pgErr) && (pgErr.Code == "40P01" || pgErr.Code == "40001")
|
||||
}
|
||||
|
||||
// lockPermanentAuthIdentities acquires the complete batch before any auth-key,
|
||||
// binding, authorization, or update-state row lock. Ordering is by the final
|
||||
// int32 hashint8 key, not by the source bigint identity: hash collisions are
|
||||
// intentionally one lock and cannot create an opposite acquisition order.
|
||||
func lockPermanentAuthIdentities(ctx context.Context, tx pgx.Tx, permIDs []int64) error {
|
||||
if len(permIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
rows, err := tx.Query(ctx, `
|
||||
SELECT DISTINCT hashint8(identity_id)::integer AS lock_key
|
||||
FROM unnest($1::bigint[]) AS identities(identity_id)
|
||||
ORDER BY lock_key`, permIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("derive permanent auth identity lock keys: %w", err)
|
||||
}
|
||||
lockKeys := make([]int32, 0, len(permIDs))
|
||||
for rows.Next() {
|
||||
var key int32
|
||||
if err := rows.Scan(&key); err != nil {
|
||||
rows.Close()
|
||||
return fmt.Errorf("scan permanent auth identity lock key: %w", err)
|
||||
}
|
||||
lockKeys = append(lockKeys, key)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return fmt.Errorf("iterate permanent auth identity lock keys: %w", err)
|
||||
}
|
||||
rows.Close()
|
||||
for _, key := range lockKeys {
|
||||
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock($1::integer, $2::integer)`, authIdentityAdvisoryNamespace, key); err != nil {
|
||||
return fmt.Errorf("lock permanent auth identity %d: %w", key, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lookupAuthKeyIdentityHint is intentionally lock-free. A positive-expiry raw
|
||||
// key has no permanent identity until a binding is committed; a permanent raw
|
||||
// key is its own identity. Callers must re-read after the raw row is locked.
|
||||
func lookupAuthKeyIdentityHint(ctx context.Context, tx pgx.Tx, rawID int64) (authKeyIdentityHint, error) {
|
||||
var hint authKeyIdentityHint
|
||||
err := tx.QueryRow(ctx, `
|
||||
/* auth_identity_hint */
|
||||
SELECT key.expires_at,
|
||||
binding.temp_auth_key_id IS NOT NULL,
|
||||
COALESCE(binding.perm_auth_key_id, 0)
|
||||
FROM auth_keys AS key
|
||||
LEFT JOIN temp_auth_key_bindings AS binding
|
||||
ON binding.temp_auth_key_id = key.auth_key_id
|
||||
WHERE key.auth_key_id = $1`, rawID).Scan(&hint.expiresAt, &hint.bound, &hint.permID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return authKeyIdentityHint{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return authKeyIdentityHint{}, fmt.Errorf("resolve auth key identity hint: %w", err)
|
||||
}
|
||||
hint.found = true
|
||||
switch {
|
||||
case hint.bound:
|
||||
hint.identityID = hint.permID
|
||||
hint.hasIdentity = true
|
||||
case hint.expiresAt == 0:
|
||||
hint.identityID = rawID
|
||||
hint.hasIdentity = true
|
||||
}
|
||||
return hint, nil
|
||||
}
|
||||
|
||||
// lockRawAuthKeyInIdentityOrder establishes the only cross-identity row-lock
|
||||
// order used by bind, selector advance and direct key deletion:
|
||||
//
|
||||
// permanent identity advisory gate -> raw auth-key row -> permanent row
|
||||
//
|
||||
// If an initially-unbound temp key becomes bound before the raw lock is
|
||||
// acquired, taking its newly discovered identity advisory lock at that point
|
||||
// would recreate raw->identity inversion. The caller must roll back and retry.
|
||||
func lockRawAuthKeyInIdentityOrder(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
rawID int64,
|
||||
) (rawExpiry int, permID int64, bound bool, err error) {
|
||||
hint, err := lookupAuthKeyIdentityHint(ctx, tx, rawID)
|
||||
if err != nil || !hint.found {
|
||||
if err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
return 0, 0, false, store.ErrAuthKeyNotFound
|
||||
}
|
||||
if hint.hasIdentity {
|
||||
if err := lockPermanentAuthIdentities(ctx, tx, []int64{hint.identityID}); err != nil {
|
||||
return 0, 0, false, err
|
||||
}
|
||||
}
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT expires_at
|
||||
FROM auth_keys
|
||||
WHERE auth_key_id = $1
|
||||
FOR UPDATE`, rawID).Scan(&rawExpiry); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, 0, false, store.ErrAuthKeyNotFound
|
||||
}
|
||||
return 0, 0, false, fmt.Errorf("lock raw auth key: %w", err)
|
||||
}
|
||||
|
||||
var actualPermID int64
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT perm_auth_key_id
|
||||
FROM temp_auth_key_bindings
|
||||
WHERE temp_auth_key_id = $1`, rawID).Scan(&actualPermID)
|
||||
switch {
|
||||
case err == nil:
|
||||
bound = true
|
||||
permID = actualPermID
|
||||
case errors.Is(err, pgx.ErrNoRows):
|
||||
permID = rawID
|
||||
default:
|
||||
return 0, 0, false, fmt.Errorf("revalidate auth key permanent identity: %w", err)
|
||||
}
|
||||
|
||||
actualHasIdentity := bound || rawExpiry == 0
|
||||
actualIdentityID := permID
|
||||
if actualHasIdentity != hint.hasIdentity ||
|
||||
(actualHasIdentity && actualIdentityID != hint.identityID) ||
|
||||
bound != hint.bound || rawExpiry != hint.expiresAt {
|
||||
return 0, 0, false, errAuthIdentityChanged
|
||||
}
|
||||
if !bound {
|
||||
return rawExpiry, permID, false, nil
|
||||
}
|
||||
|
||||
var permExpiry int
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT expires_at
|
||||
FROM auth_keys
|
||||
WHERE auth_key_id = $1
|
||||
FOR UPDATE`, permID).Scan(&permExpiry); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, 0, false, store.ErrAuthKeyBindingInvalid
|
||||
}
|
||||
return 0, 0, false, fmt.Errorf("lock permanent auth key: %w", err)
|
||||
}
|
||||
if rawExpiry <= 0 || permExpiry != 0 {
|
||||
return 0, 0, false, store.ErrAuthKeyBindingInvalid
|
||||
}
|
||||
return rawExpiry, permID, true, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue