package domain import "fmt" var ( ErrRevisionGap = fmt.Errorf("revision gap requires resync") ErrSyncConflict = fmt.Errorf("conflicting revisioned event") ) type SyncEvent struct { Kind ResourceKind ResourceID string Revision uint64 State State } type ReplicaResource struct { Kind ResourceKind ResourceID string State State Revision uint64 NeedsResync bool } func NewReplicaResource(kind ResourceKind, resourceID string, state State) (*ReplicaResource, error) { if resourceID == "" || !validStateForKind(kind, state) { return nil, fmt.Errorf("invalid replica resource") } return &ReplicaResource{Kind: kind, ResourceID: resourceID, State: state}, nil } // ApplyEvent makes duplicate/out-of-order delivery converge. A gap is not // guessed through; callers must fetch the authoritative REST snapshot and use // ReplaceSnapshot before resuming the event stream. func (r *ReplicaResource) ApplyEvent(event SyncEvent) error { if event.Kind != r.Kind || event.ResourceID != r.ResourceID { return ErrSyncConflict } if r.NeedsResync { return ErrRevisionGap } if event.Revision <= r.Revision { if event.Revision == r.Revision && event.State != r.State { return ErrSyncConflict } return nil } if event.Revision != r.Revision+1 { r.NeedsResync = true return ErrRevisionGap } if !legalTransition(r.Kind, r.State, event.State) { return ErrSyncConflict } r.State, r.Revision = event.State, event.Revision return nil } func (r *ReplicaResource) ReplaceSnapshot(revision uint64, state State) error { if revision < r.Revision || !validStateForKind(r.Kind, state) { return ErrSyncConflict } r.State, r.Revision, r.NeedsResync = state, revision, false return nil } func validStateForKind(kind ResourceKind, state State) bool { switch kind { case ResourceQueueTicket: _, ok := queueTransitions[state] return ok || state == Queued case ResourceProposal: _, ok := proposalTransitions[state] return ok || state == Open case ResourceMatch: _, ok := matchTransitions[state] return ok default: return false } }