feat: add protocol and reconciliation decisions
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
|
||||
)
|
||||
|
||||
var ErrReconcileContradiction = errors.New("reconciliation evidence contradicts durable state")
|
||||
|
||||
type ClientEvidenceKind uint8
|
||||
|
||||
const (
|
||||
ClientEvidenceAbsent ClientEvidenceKind = iota
|
||||
ClientEvidenceRetained
|
||||
ClientEvidenceTombstone
|
||||
)
|
||||
|
||||
type ReconcileAction uint8
|
||||
|
||||
const (
|
||||
ReconcileNoop ReconcileAction = iota
|
||||
ReconcileRequeue
|
||||
ReconcileResumeDelivery
|
||||
ReconcileInterruptServerSuppressReplay
|
||||
ReconcileInterruptServerClientStateLoss
|
||||
ReconcileTerminateLocal
|
||||
ReconcileDiscardLocalTerminal
|
||||
ReconcileRecordContradiction
|
||||
)
|
||||
|
||||
type ReconcileInput struct {
|
||||
ServerPresent bool
|
||||
ServerLifecycle rvboxv1.CommandLifecycle
|
||||
ServerRevision CommandRevision
|
||||
ServerLastEventSeq uint64
|
||||
ServerHasTombstone bool
|
||||
ClientEvidence ClientEvidenceKind
|
||||
ClientLifecycle rvboxv1.CommandLifecycle
|
||||
ClientRevision CommandRevision
|
||||
ClientLastEventSeq uint64
|
||||
ImmutableHashMatches bool
|
||||
}
|
||||
|
||||
type ReconcileDecision struct {
|
||||
Action ReconcileAction
|
||||
EffectiveRevision CommandRevision
|
||||
RecordIncident bool
|
||||
SuppressReplay bool
|
||||
}
|
||||
|
||||
// DecideReconciliation implements the explicit server/client evidence matrix.
|
||||
// Persistence and message emission remain the caller's atomic responsibility.
|
||||
func DecideReconciliation(input ReconcileInput) (ReconcileDecision, error) {
|
||||
if input.ClientEvidence > ClientEvidenceTombstone {
|
||||
return ReconcileDecision{}, fmt.Errorf("%w: unknown client evidence", ErrReconcileContradiction)
|
||||
}
|
||||
if input.ServerPresent {
|
||||
if input.ServerLifecycle == rvboxv1.CommandLifecycle_COMMAND_LIFECYCLE_UNSPECIFIED || input.ServerRevision == 0 {
|
||||
return ReconcileDecision{}, fmt.Errorf("%w: invalid server row", ErrReconcileContradiction)
|
||||
}
|
||||
}
|
||||
if input.ClientEvidence != ClientEvidenceAbsent {
|
||||
if input.ClientRevision == 0 || !input.ImmutableHashMatches {
|
||||
return ReconcileDecision{}, fmt.Errorf("%w: invalid revision or immutable hash", ErrReconcileContradiction)
|
||||
}
|
||||
}
|
||||
|
||||
if !input.ServerPresent {
|
||||
if input.ClientEvidence == ClientEvidenceAbsent {
|
||||
return ReconcileDecision{Action: ReconcileNoop}, nil
|
||||
}
|
||||
if input.ClientEvidence == ClientEvidenceTombstone || IsTerminal(input.ClientLifecycle) || input.ServerHasTombstone {
|
||||
return ReconcileDecision{Action: ReconcileDiscardLocalTerminal, EffectiveRevision: input.ClientRevision, SuppressReplay: true}, nil
|
||||
}
|
||||
return ReconcileDecision{Action: ReconcileTerminateLocal, EffectiveRevision: input.ClientRevision, RecordIncident: true, SuppressReplay: true}, nil
|
||||
}
|
||||
|
||||
if IsTerminal(input.ServerLifecycle) {
|
||||
if input.ClientEvidence == ClientEvidenceAbsent {
|
||||
return ReconcileDecision{Action: ReconcileNoop, EffectiveRevision: input.ServerRevision}, nil
|
||||
}
|
||||
if input.ClientEvidence == ClientEvidenceTombstone || IsTerminal(input.ClientLifecycle) {
|
||||
if input.ClientEvidence == ClientEvidenceRetained && input.ClientLifecycle != input.ServerLifecycle {
|
||||
return ReconcileDecision{Action: ReconcileRecordContradiction, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, fmt.Errorf("%w: terminal lifecycle mismatch", ErrReconcileContradiction)
|
||||
}
|
||||
return ReconcileDecision{Action: ReconcileDiscardLocalTerminal, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), SuppressReplay: true}, nil
|
||||
}
|
||||
return ReconcileDecision{Action: ReconcileTerminateLocal, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, nil
|
||||
}
|
||||
|
||||
if input.ClientEvidence == ClientEvidenceTombstone {
|
||||
return ReconcileDecision{Action: ReconcileInterruptServerSuppressReplay, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, nil
|
||||
}
|
||||
if input.ClientEvidence == ClientEvidenceAbsent {
|
||||
switch input.ServerLifecycle {
|
||||
case rvboxv1.CommandLifecycle_COMMAND_QUEUED, rvboxv1.CommandLifecycle_COMMAND_DISPATCHED:
|
||||
return ReconcileDecision{Action: ReconcileRequeue, EffectiveRevision: input.ServerRevision}, nil
|
||||
case rvboxv1.CommandLifecycle_COMMAND_ACCEPTED, rvboxv1.CommandLifecycle_COMMAND_RUNNING:
|
||||
return ReconcileDecision{Action: ReconcileInterruptServerClientStateLoss, EffectiveRevision: input.ServerRevision, RecordIncident: true, SuppressReplay: true}, nil
|
||||
default:
|
||||
return ReconcileDecision{}, fmt.Errorf("%w: unsupported non-terminal server state", ErrReconcileContradiction)
|
||||
}
|
||||
}
|
||||
|
||||
if input.ClientLifecycle == rvboxv1.CommandLifecycle_COMMAND_LIFECYCLE_UNSPECIFIED {
|
||||
return ReconcileDecision{}, fmt.Errorf("%w: invalid client lifecycle", ErrReconcileContradiction)
|
||||
}
|
||||
if input.ClientLastEventSeq < input.ServerLastEventSeq {
|
||||
return ReconcileDecision{Action: ReconcileRecordContradiction, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, fmt.Errorf("%w: client lost server-confirmed events", ErrReconcileContradiction)
|
||||
}
|
||||
if input.ServerLifecycle == rvboxv1.CommandLifecycle_COMMAND_QUEUED {
|
||||
return ReconcileDecision{Action: ReconcileRecordContradiction, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision), RecordIncident: true, SuppressReplay: true}, fmt.Errorf("%w: queued server command exists at client", ErrReconcileContradiction)
|
||||
}
|
||||
return ReconcileDecision{Action: ReconcileResumeDelivery, EffectiveRevision: maxRevision(input.ServerRevision, input.ClientRevision)}, nil
|
||||
}
|
||||
|
||||
func maxRevision(left, right CommandRevision) CommandRevision {
|
||||
if left > right {
|
||||
return left
|
||||
}
|
||||
return right
|
||||
}
|
||||
Reference in New Issue
Block a user