feat: sync multilayer td integration
This commit is contained in:
parent
20a310f6ca
commit
766c5db992
491 changed files with 26235 additions and 35340 deletions
|
|
@ -8,7 +8,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"telesrv/internal/store"
|
||||
|
|
@ -56,24 +55,26 @@ WHERE auth_keys.body = EXCLUDED.body
|
|||
// 注册进 SessionManager”的窗口被后台清理。
|
||||
func (s *AuthKeyStore) Get(ctx context.Context, id [8]byte) (store.AuthKeyData, bool, error) {
|
||||
var (
|
||||
body []byte
|
||||
serverSalt int64
|
||||
expiresAt int
|
||||
createdAt pgtype.Timestamptz
|
||||
layer int
|
||||
deviceModel string
|
||||
platform string
|
||||
systemVersion string
|
||||
apiID int
|
||||
appVersion string
|
||||
body []byte
|
||||
serverSalt int64
|
||||
expiresAt int
|
||||
createdAt pgtype.Timestamptz
|
||||
layer int
|
||||
layerObservationID int64
|
||||
deviceModel string
|
||||
platform string
|
||||
systemVersion string
|
||||
apiID int
|
||||
appVersion string
|
||||
)
|
||||
err := s.db.QueryRow(ctx, `
|
||||
UPDATE auth_keys
|
||||
SET last_used_at = now()
|
||||
WHERE auth_key_id = $1
|
||||
RETURNING auth_key_id, body, server_salt, created_at,
|
||||
expires_at, layer, device_model, platform, system_version, api_id, app_version
|
||||
`, authKeyIDToInt64(id)).Scan(new(int64), &body, &serverSalt, &createdAt, &expiresAt, &layer, &deviceModel, &platform, &systemVersion, &apiID, &appVersion)
|
||||
expires_at, layer, layer_observation_id,
|
||||
device_model, platform, system_version, api_id, app_version
|
||||
`, authKeyIDToInt64(id)).Scan(new(int64), &body, &serverSalt, &createdAt, &expiresAt, &layer, &layerObservationID, &deviceModel, &platform, &systemVersion, &apiID, &appVersion)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return store.AuthKeyData{}, false, nil
|
||||
|
|
@ -84,15 +85,16 @@ RETURNING auth_key_id, body, server_salt, created_at,
|
|||
return store.AuthKeyData{}, false, fmt.Errorf("auth key body length = %d, want 256", len(body))
|
||||
}
|
||||
data := store.AuthKeyData{
|
||||
ID: id,
|
||||
ServerSalt: serverSalt,
|
||||
ExpiresAt: expiresAt,
|
||||
Layer: layer,
|
||||
DeviceModel: deviceModel,
|
||||
Platform: platform,
|
||||
SystemVersion: systemVersion,
|
||||
APIID: apiID,
|
||||
AppVersion: appVersion,
|
||||
ID: id,
|
||||
ServerSalt: serverSalt,
|
||||
ExpiresAt: expiresAt,
|
||||
Layer: layer,
|
||||
LayerObservationID: layerObservationID,
|
||||
DeviceModel: deviceModel,
|
||||
Platform: platform,
|
||||
SystemVersion: systemVersion,
|
||||
APIID: apiID,
|
||||
AppVersion: appVersion,
|
||||
}
|
||||
copy(data.Value[:], body)
|
||||
if createdAt.Valid {
|
||||
|
|
@ -148,7 +150,8 @@ WHERE auth_key_id = ANY($1::bigint[])`, batch)
|
|||
}
|
||||
|
||||
func (s *AuthKeyStore) UpdateClientInfo(ctx context.Context, id [8]byte, info store.AuthKeyClientInfo) error {
|
||||
if _, err := s.db.Exec(ctx, `
|
||||
var updated int
|
||||
err := s.db.QueryRow(ctx, `
|
||||
UPDATE auth_keys
|
||||
SET layer = CASE WHEN $2::integer > 0 THEN $2 ELSE layer END,
|
||||
device_model = CASE WHEN $3::text <> '' THEN $3 ELSE device_model END,
|
||||
|
|
@ -157,7 +160,31 @@ SET layer = CASE WHEN $2::integer > 0 THEN $2 ELSE layer END,
|
|||
api_id = CASE WHEN $6::integer <> 0 THEN $6 ELSE api_id END,
|
||||
app_version = CASE WHEN $7::text <> '' THEN $7 ELSE app_version END
|
||||
WHERE auth_key_id = $1
|
||||
`, authKeyIDToInt64(id), info.Layer, info.DeviceModel, info.Platform, info.SystemVersion, info.APIID, info.AppVersion); err != nil {
|
||||
AND ($2::integer <= 0 OR layer_observation_id = 0 OR layer = $2::integer)
|
||||
RETURNING 1
|
||||
`, authKeyIDToInt64(id), info.Layer, info.DeviceModel, info.Platform, info.SystemVersion, info.APIID, info.AppVersion).Scan(&updated)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
var (
|
||||
currentLayer int
|
||||
observation int64
|
||||
)
|
||||
lookupErr := s.db.QueryRow(ctx, `
|
||||
SELECT layer, layer_observation_id
|
||||
FROM auth_keys
|
||||
WHERE auth_key_id = $1
|
||||
`, authKeyIDToInt64(id)).Scan(¤tLayer, &observation)
|
||||
switch {
|
||||
case errors.Is(lookupErr, pgx.ErrNoRows):
|
||||
return store.ErrAuthKeyNotFound
|
||||
case lookupErr != nil:
|
||||
return fmt.Errorf("classify auth key client info update: %w", lookupErr)
|
||||
case info.Layer > 0 && observation > 0 && currentLayer != info.Layer:
|
||||
return store.ErrAuthKeySessionLayerConflict
|
||||
default:
|
||||
return fmt.Errorf("update auth key client info: guarded update affected no row")
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("update auth key client info: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -171,25 +198,20 @@ WHERE auth_key_id = $1
|
|||
// RESTRICT FK 防止悬空,因此被踢/销毁 perm key 时必须先把关联 temp key 一并删掉。否则 Web/上传连接用
|
||||
// raw temp key 重连时仍能进入 RPC 层,只得到 AUTH_KEY_UNREGISTERED,而不是连接层 404。
|
||||
func (s *AuthKeyStore) Delete(ctx context.Context, id [8]byte) error {
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
err := s.deleteAuthKeyOnce(ctx, id)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isPermAuthKeyDeleteRace(err) {
|
||||
return err
|
||||
}
|
||||
if _, inTx := s.db.(pgx.Tx); inTx {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("delete auth key: permanent-key binding changed during all retries")
|
||||
return withAuthIdentityTx(ctx, s.db, "delete auth key", func(tx pgx.Tx) error {
|
||||
return deleteAuthKeyTx(ctx, tx, authKeyIDToInt64(id))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthKeyStore) deleteAuthKeyOnce(ctx context.Context, id [8]byte) error {
|
||||
keyID := authKeyIDToInt64(id)
|
||||
func deleteAuthKeyTx(ctx context.Context, tx pgx.Tx, keyID int64) error {
|
||||
if _, _, _, err := lockRawAuthKeyInIdentityOrder(ctx, tx, keyID); err != nil {
|
||||
if errors.Is(err, store.ErrAuthKeyNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var touched int
|
||||
if err := s.db.QueryRow(ctx, `
|
||||
if err := tx.QueryRow(ctx, `
|
||||
WITH doomed_temp AS MATERIALIZED (
|
||||
SELECT temp_auth_key_id
|
||||
FROM temp_auth_key_bindings
|
||||
|
|
@ -225,13 +247,6 @@ SELECT
|
|||
|
||||
const tempAuthKeyPermFKConstraint = "temp_auth_key_bindings_perm_auth_key_id_fkey"
|
||||
|
||||
func isPermAuthKeyDeleteRace(err error) bool {
|
||||
var pgErr *pgconn.PgError
|
||||
return errors.As(err, &pgErr) &&
|
||||
pgErr.Code == "23503" &&
|
||||
pgErr.ConstraintName == tempAuthKeyPermFKConstraint
|
||||
}
|
||||
|
||||
// DeleteOrphaned 回收握手已落库、但从未形成 authorization/temp binding 且当前没有
|
||||
// 活跃物理连接的旧 auth key。last_used_at 与 Get 的 UPDATE ... RETURNING 行锁配对,封住
|
||||
// active-key 快照之后新连接开始使用旧 key 的竞态;所有引用条件仍在最终 DELETE 中复核。
|
||||
|
|
@ -248,51 +263,140 @@ func (s *AuthKeyStore) DeleteOrphaned(ctx context.Context, olderThan time.Durati
|
|||
protectedIDs = append(protectedIDs, authKeyIDToInt64(id))
|
||||
}
|
||||
var deleted int
|
||||
err := s.db.QueryRow(ctx, `
|
||||
WITH candidates AS MATERIALIZED (
|
||||
err := withAuthIdentityTx(ctx, s.db, "delete orphaned auth keys", func(tx pgx.Tx) error {
|
||||
var err error
|
||||
deleted, err = deleteOrphanedAuthKeysTx(ctx, tx, olderThan, limit, protectedIDs)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("delete orphaned auth keys: %w", err)
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func deleteOrphanedAuthKeysTx(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
olderThan time.Duration,
|
||||
limit int,
|
||||
protectedIDs []int64,
|
||||
) (int, error) {
|
||||
// Phase 1 is only a bounded hint. It must not lock rows before the complete
|
||||
// permanent-identity advisory set has been derived and acquired.
|
||||
rows, err := tx.Query(ctx, `
|
||||
/* orphan_identity_candidates */
|
||||
SELECT k.auth_key_id, k.expires_at
|
||||
FROM auth_keys AS k
|
||||
WHERE k.last_used_at < now() - make_interval(secs => $1::double precision)
|
||||
AND NOT (k.auth_key_id = ANY($2::bigint[]))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM authorizations AS a WHERE a.auth_key_id = k.auth_key_id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM temp_auth_key_bindings AS b
|
||||
WHERE b.temp_auth_key_id = k.auth_key_id OR b.perm_auth_key_id = k.auth_key_id
|
||||
)
|
||||
ORDER BY k.last_used_at, k.auth_key_id
|
||||
LIMIT $3`, olderThan.Seconds(), protectedIDs, limit)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("select orphan auth key candidates: %w", err)
|
||||
}
|
||||
candidates := make([]int64, 0, limit)
|
||||
permanentCandidates := make([]int64, 0, limit)
|
||||
for rows.Next() {
|
||||
var (
|
||||
id int64
|
||||
expiresAt int
|
||||
)
|
||||
if err := rows.Scan(&id, &expiresAt); err != nil {
|
||||
rows.Close()
|
||||
return 0, fmt.Errorf("scan orphan auth key candidate: %w", err)
|
||||
}
|
||||
candidates = append(candidates, id)
|
||||
if expiresAt == 0 {
|
||||
permanentCandidates = append(permanentCandidates, id)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return 0, fmt.Errorf("iterate orphan auth key candidates: %w", err)
|
||||
}
|
||||
rows.Close()
|
||||
if len(candidates) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if err := lockPermanentAuthIdentities(ctx, tx, permanentCandidates); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Phase 2 locks only the hinted raw rows, in real-ID order, after every P
|
||||
// gate is held. A temp bind already holding its raw row is skipped; if it
|
||||
// committed immediately before this lock, phase 3's new READ COMMITTED
|
||||
// statement sees the binding and excludes it.
|
||||
lockRows, err := tx.Query(ctx, `
|
||||
SELECT auth_key_id
|
||||
FROM auth_keys
|
||||
WHERE auth_key_id = ANY($1::bigint[])
|
||||
ORDER BY auth_key_id
|
||||
FOR UPDATE SKIP LOCKED`, candidates)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("lock orphan auth key candidates: %w", err)
|
||||
}
|
||||
lockedIDs := make([]int64, 0, len(candidates))
|
||||
for lockRows.Next() {
|
||||
var id int64
|
||||
if err := lockRows.Scan(&id); err != nil {
|
||||
lockRows.Close()
|
||||
return 0, fmt.Errorf("scan locked orphan auth key: %w", err)
|
||||
}
|
||||
lockedIDs = append(lockedIDs, id)
|
||||
}
|
||||
if err := lockRows.Err(); err != nil {
|
||||
lockRows.Close()
|
||||
return 0, fmt.Errorf("iterate locked orphan auth keys: %w", err)
|
||||
}
|
||||
lockRows.Close()
|
||||
if len(lockedIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Phase 3 is a separate statement snapshot and repeats every ownership,
|
||||
// activity and protection predicate. Never delete from the phase-1 hint.
|
||||
var deleted int
|
||||
err = tx.QueryRow(ctx, `
|
||||
WITH still_orphaned AS MATERIALIZED (
|
||||
SELECT k.auth_key_id
|
||||
FROM auth_keys k
|
||||
WHERE k.last_used_at < now() - make_interval(secs => $1::double precision)
|
||||
AND NOT (k.auth_key_id = ANY($2::bigint[]))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM authorizations a WHERE a.auth_key_id = k.auth_key_id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM temp_auth_key_bindings b
|
||||
WHERE b.temp_auth_key_id = k.auth_key_id OR b.perm_auth_key_id = k.auth_key_id
|
||||
)
|
||||
ORDER BY k.last_used_at ASC, k.auth_key_id ASC
|
||||
LIMIT $3
|
||||
FOR UPDATE OF k SKIP LOCKED
|
||||
), deleted_update_states AS (
|
||||
-- Historical authorization-only deletion could leave a cursor without an
|
||||
-- auth_keys FK. GC owns that stale row once the raw key is proven orphaned.
|
||||
DELETE FROM update_states s
|
||||
USING candidates c
|
||||
WHERE s.auth_key_id = c.auth_key_id
|
||||
RETURNING s.auth_key_id
|
||||
), deleted_keys AS (
|
||||
DELETE FROM auth_keys k
|
||||
USING candidates c
|
||||
WHERE k.auth_key_id = c.auth_key_id
|
||||
FROM auth_keys AS k
|
||||
WHERE k.auth_key_id = ANY($3::bigint[])
|
||||
AND k.last_used_at < now() - make_interval(secs => $1::double precision)
|
||||
AND NOT (k.auth_key_id = ANY($2::bigint[]))
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM authorizations a WHERE a.auth_key_id = k.auth_key_id
|
||||
SELECT 1 FROM authorizations AS a WHERE a.auth_key_id = k.auth_key_id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM temp_auth_key_bindings b
|
||||
FROM temp_auth_key_bindings AS b
|
||||
WHERE b.temp_auth_key_id = k.auth_key_id OR b.perm_auth_key_id = k.auth_key_id
|
||||
)
|
||||
RETURNING k.auth_key_id
|
||||
), deleted_update_states AS (
|
||||
DELETE FROM update_states AS state
|
||||
USING still_orphaned AS orphan
|
||||
WHERE state.auth_key_id = orphan.auth_key_id
|
||||
RETURNING state.auth_key_id
|
||||
), deleted_keys AS (
|
||||
DELETE FROM auth_keys AS key
|
||||
USING still_orphaned AS orphan
|
||||
WHERE key.auth_key_id = orphan.auth_key_id
|
||||
RETURNING key.auth_key_id
|
||||
)
|
||||
SELECT count(*)::int
|
||||
SELECT count(*)::integer
|
||||
FROM deleted_keys
|
||||
CROSS JOIN LATERAL (SELECT count(*) FROM deleted_update_states) AS touched`, olderThan.Seconds(), protectedIDs, limit).Scan(&deleted)
|
||||
CROSS JOIN LATERAL (SELECT count(*) FROM deleted_update_states) AS touched`,
|
||||
olderThan.Seconds(), protectedIDs, lockedIDs,
|
||||
).Scan(&deleted)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("delete orphaned auth keys: %w", err)
|
||||
return 0, fmt.Errorf("delete revalidated orphan auth keys: %w", err)
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue