Files
sub2api/backend/internal/repository/auth_cache_invalidation_outbox_repo.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

160 lines
4.3 KiB
Go

package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
)
type authCacheInvalidationOutboxRepository struct {
db *sql.DB
}
func NewAuthCacheInvalidationOutboxRepository(db *sql.DB) service.AuthCacheInvalidationOutboxRepository {
return &authCacheInvalidationOutboxRepository{db: db}
}
func (r *authCacheInvalidationOutboxRepository) Claim(ctx context.Context, workerID string, limit int, lease time.Duration) ([]service.AuthCacheInvalidationEvent, error) {
if r == nil || r.db == nil {
return nil, errors.New("nil auth cache invalidation outbox database")
}
if limit <= 0 {
limit = 100
}
leaseSeconds := int64(lease / time.Second)
if leaseSeconds < 1 {
leaseSeconds = 30
}
rows, err := r.db.QueryContext(ctx, `
WITH candidates AS (
SELECT id
FROM auth_cache_invalidation_outbox
WHERE available_at <= NOW()
AND (claimed_at IS NULL OR claimed_at < NOW() - ($3 * INTERVAL '1 second'))
ORDER BY id ASC
LIMIT $2
FOR UPDATE SKIP LOCKED
)
UPDATE auth_cache_invalidation_outbox AS o
SET claimed_at = NOW(), claimed_by = $1
FROM candidates AS c
WHERE o.id = c.id
RETURNING o.id, o.cache_key, o.attempts, o.delivery_stage, o.created_at
`, workerID, limit, leaseSeconds)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
events := make([]service.AuthCacheInvalidationEvent, 0, limit)
for rows.Next() {
var event service.AuthCacheInvalidationEvent
if err := rows.Scan(&event.ID, &event.CacheKey, &event.Attempts, &event.Stage, &event.CreatedAt); err != nil {
return nil, err
}
event.CacheKey = strings.TrimSpace(event.CacheKey)
events = append(events, event)
}
if err := rows.Err(); err != nil {
return nil, err
}
return events, nil
}
func (r *authCacheInvalidationOutboxRepository) ScheduleSecondPass(ctx context.Context, id int64, workerID string, availableAt time.Time) error {
result, err := r.db.ExecContext(ctx, `
UPDATE auth_cache_invalidation_outbox
SET delivery_stage = 1,
available_at = $3,
last_error = NULL,
claimed_at = NULL,
claimed_by = NULL
WHERE id = $1 AND claimed_by = $2 AND delivery_stage = 0
`, id, workerID, availableAt)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return fmt.Errorf("auth cache invalidation claim %d cannot schedule second pass", id)
}
return nil
}
func (r *authCacheInvalidationOutboxRepository) DeleteClaimed(ctx context.Context, id int64, workerID string) error {
result, err := r.db.ExecContext(ctx, `
DELETE FROM auth_cache_invalidation_outbox
WHERE id = $1 AND claimed_by = $2
`, id, workerID)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return fmt.Errorf("auth cache invalidation claim %d is no longer owned by %s", id, workerID)
}
return nil
}
func (r *authCacheInvalidationOutboxRepository) RetryClaimed(ctx context.Context, id int64, workerID string, availableAt time.Time, lastError string) error {
result, err := r.db.ExecContext(ctx, `
UPDATE auth_cache_invalidation_outbox
SET attempts = attempts + 1,
available_at = $3,
last_error = $4,
claimed_at = NULL,
claimed_by = NULL
WHERE id = $1 AND claimed_by = $2
`, id, workerID, availableAt, lastError)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected != 1 {
return fmt.Errorf("auth cache invalidation claim %d is no longer owned by %s", id, workerID)
}
return nil
}
func (r *authCacheInvalidationOutboxRepository) Stats(ctx context.Context) (service.AuthCacheInvalidationOutboxStats, error) {
var (
stats service.AuthCacheInvalidationOutboxStats
oldest sql.NullTime
lastError sql.NullString
)
err := r.db.QueryRowContext(ctx, `
SELECT COUNT(*), MIN(created_at), COALESCE(MAX(attempts), 0),
(SELECT last_error
FROM auth_cache_invalidation_outbox
WHERE last_error IS NOT NULL
ORDER BY available_at DESC, id DESC
LIMIT 1)
FROM auth_cache_invalidation_outbox
`).Scan(&stats.Pending, &oldest, &stats.MaxAttempts, &lastError)
if err != nil {
return stats, err
}
if oldest.Valid {
value := oldest.Time
stats.OldestCreatedAt = &value
}
if lastError.Valid {
stats.LastError = lastError.String
}
return stats, nil
}