99 lines
2.7 KiB
Go
99 lines
2.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
// Mutation — typed intent to mutate state. Every write to the store goes through
|
|
// this. Validate() runs WITHOUT holding any application lock; it inspects the
|
|
// current state for preconditions. Apply() is called with an acquired LockRegion
|
|
// and is the only path that mutates state.
|
|
//
|
|
// AffectedAuthors() is used for read_state bookkeeping and mention targeting.
|
|
type Mutation interface {
|
|
Validate(s *Store) error
|
|
Apply(s *Store, lock LockRegion) error
|
|
AffectedAuthors() []string
|
|
Kind() string // for logging and VOPR transition recording
|
|
}
|
|
|
|
// LockScope — the granularity the controller chose for this mutation.
|
|
type LockScope int
|
|
|
|
const (
|
|
ScopeCoarse LockScope = iota // per-author queue
|
|
ScopeMedium // per-author queue + per-post sub-queue
|
|
ScopeFine // per-mutation lock, no queueing
|
|
)
|
|
|
|
func (s LockScope) String() string {
|
|
switch s {
|
|
case ScopeCoarse:
|
|
return "coarse"
|
|
case ScopeMedium:
|
|
return "medium"
|
|
case ScopeFine:
|
|
return "fine"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// LockRegion — opaque handle to an acquired lock. The mutation's Apply method
|
|
// receives this so it can hold the lock for the duration of the apply and
|
|
// release it explicitly if needed.
|
|
type LockRegion interface {
|
|
Scope() LockScope
|
|
Release()
|
|
}
|
|
|
|
// ErrValidation — sentinel for precondition failures. Distinct from runtime
|
|
// errors so callers can map to HTTP 400 vs 500.
|
|
var ErrValidation = errors.New("validation failed")
|
|
|
|
// ValidationError — typed validation failure with a message for the caller.
|
|
type ValidationError struct {
|
|
Field string
|
|
Message string
|
|
}
|
|
|
|
func (e *ValidationError) Error() string {
|
|
return fmt.Sprintf("validation: %s: %s", e.Field, e.Message)
|
|
}
|
|
|
|
func (e *ValidationError) Unwrap() error { return ErrValidation }
|
|
|
|
// Submit — the canonical entry point. The lock controller decides the scope
|
|
// based on observed contention; we acquire the lock, then call Validate, then
|
|
// Apply. (Validate can also be called standalone for dry-runs.)
|
|
//
|
|
// On Validate failure, the lock is released and no state changes.
|
|
// On Apply failure, the lock is released and state is left consistent (Apply
|
|
// is expected to be transactional; if it can't be, that's a bug).
|
|
func Submit(ctx context.Context, s *Store, m Mutation) error {
|
|
lock := s.lockCtrl.Acquire(s, m)
|
|
defer lock.Release()
|
|
if err := m.Validate(s); err != nil {
|
|
return err
|
|
}
|
|
if err := m.Apply(s, lock); err != nil {
|
|
return err
|
|
}
|
|
s.bumpFlush()
|
|
return nil
|
|
}
|
|
|
|
// ValidateOnly — dry-run. No lock, no state change. Useful for "can I post?"
|
|
// preflight checks from agents.
|
|
func ValidateOnly(s *Store, m Mutation) error {
|
|
return m.Validate(s)
|
|
}
|
|
|
|
func (s *Store) bumpFlush() {
|
|
select {
|
|
case s.flushCh <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|