feat(mutation): Mutation interface, Submit(), Validate/Apply split
This commit is contained in:
parent
1728780aed
commit
bfe56bbb65
1 changed files with 99 additions and 0 deletions
99
mutation.go
Normal file
99
mutation.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
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:
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue