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
160 lines
4.3 KiB
Go
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
|
|
}
|