Files
silo-server/internal/requests/store.go
T
Silo Server MigrationandClaude Opus 4.7 fca23d77e9 fix(requests): serialize quota check and clamp list limits
CreateRequest previously read the user's request count outside the
insert transaction, so two concurrent submissions at MaxRequests-1
could both pass the quota gate and end up at MaxRequests+1. Move the
count inside the same transaction as the insert and acquire a per-user
advisory lock so concurrent inserts serialize. The store reports
ErrQuotaExceeded when the racing path catches the user at the limit
and the service maps it back to QuotaError.

normalizeListFilter previously reset limit to 50 when callers asked
for more than 100, which is surprising. Clamp to the cap instead so a
request for 150 returns 100 and a request for 1_000_000 still cannot
hit the database with an unbounded scan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:12:45 -04:00

47 lines
1.9 KiB
Go

package requests
import (
"context"
"time"
)
type Store interface {
GetSettings(ctx context.Context) (Settings, error)
UpdateSettings(ctx context.Context, settings Settings) (Settings, error)
GetUserLimit(ctx context.Context, userID int) (*UserLimit, error)
UpsertUserLimit(ctx context.Context, limit UserLimit) (*UserLimit, error)
CountUserRequestsSince(ctx context.Context, userID int, since time.Time) (int, error)
ListActiveByTMDB(ctx context.Context, mediaType MediaType, tmdbIDs []int) (map[int]*Request, error)
CreateRequest(ctx context.Context, input CreateRequestRecord) (*Request, error)
GetRequest(ctx context.Context, id string) (*Request, error)
ListReconciliationCandidates(ctx context.Context, limit int) ([]*Request, error)
ListMine(ctx context.Context, userID int, filter ListFilter) ([]*Request, error)
ListAdmin(ctx context.Context, filter ListFilter) ([]*Request, error)
SetStatus(ctx context.Context, id string, status Status, actor Viewer) (*Request, error)
MarkQueued(ctx context.Context, id string, update QueueUpdate, actor Viewer) (*Request, error)
SetOutcome(ctx context.Context, id string, outcome Outcome, actor Viewer, message string) (*Request, error)
ListIntegrations(ctx context.Context) ([]Integration, error)
UpsertIntegration(ctx context.Context, integration Integration) (*Integration, error)
UpsertIntegrations(ctx context.Context, integrations []Integration) ([]Integration, error)
}
type CreateRequestRecord struct {
ID string
Input CreateRequestInput
Status Status
Outcome Outcome
Requester Viewer
Now time.Time
// Quota, when non-nil, instructs the store to atomically verify the
// requester is below their per-user limit before inserting. The check
// runs inside the same transaction as the insert with a per-user
// advisory lock so concurrent submissions cannot both exceed the limit.
Quota *QuotaCheck
}
type QuotaCheck struct {
UserID int
WindowStart time.Time
MaxRequests int
}