You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
109 lines
2.9 KiB
109 lines
2.9 KiB
package replication
|
|
|
|
import "sync/atomic"
|
|
|
|
// sessionIDCounter generates globally unique session IDs.
|
|
var sessionIDCounter atomic.Uint64
|
|
|
|
// Session represents one recovery attempt for a specific replica at a
|
|
// specific epoch. All mutable state is unexported — external code interacts
|
|
// through Sender execution APIs only.
|
|
type Session struct {
|
|
id uint64
|
|
replicaID string
|
|
epoch uint64
|
|
kind SessionKind
|
|
phase SessionPhase
|
|
invalidateReason string
|
|
|
|
startLSN uint64
|
|
targetLSN uint64
|
|
frozenTargetLSN uint64
|
|
recoveredTo uint64
|
|
|
|
truncateRequired bool
|
|
truncateToLSN uint64
|
|
truncateRecorded bool
|
|
|
|
budget *CatchUpBudget
|
|
tracker BudgetCheck
|
|
|
|
rebuild *RebuildState
|
|
}
|
|
|
|
func newSession(replicaID string, epoch uint64, kind SessionKind) *Session {
|
|
s := &Session{
|
|
id: sessionIDCounter.Add(1),
|
|
replicaID: replicaID,
|
|
epoch: epoch,
|
|
kind: kind,
|
|
phase: PhaseInit,
|
|
}
|
|
if kind == SessionRebuild {
|
|
s.rebuild = NewRebuildState()
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Read-only accessors.
|
|
|
|
func (s *Session) ID() uint64 { return s.id }
|
|
func (s *Session) ReplicaID() string { return s.replicaID }
|
|
func (s *Session) Epoch() uint64 { return s.epoch }
|
|
func (s *Session) Kind() SessionKind { return s.kind }
|
|
func (s *Session) Phase() SessionPhase { return s.phase }
|
|
func (s *Session) InvalidateReason() string { return s.invalidateReason }
|
|
func (s *Session) StartLSN() uint64 { return s.startLSN }
|
|
func (s *Session) TargetLSN() uint64 { return s.targetLSN }
|
|
func (s *Session) FrozenTargetLSN() uint64 { return s.frozenTargetLSN }
|
|
func (s *Session) RecoveredTo() uint64 { return s.recoveredTo }
|
|
|
|
// Active returns true if the session is not completed or invalidated.
|
|
func (s *Session) Active() bool {
|
|
return s.phase != PhaseCompleted && s.phase != PhaseInvalidated
|
|
}
|
|
|
|
// Converged returns true if recovery reached the target.
|
|
func (s *Session) Converged() bool {
|
|
return s.targetLSN > 0 && s.recoveredTo >= s.targetLSN
|
|
}
|
|
|
|
// Internal mutation methods — called by Sender under its lock.
|
|
|
|
func (s *Session) advance(phase SessionPhase) bool {
|
|
if !s.Active() {
|
|
return false
|
|
}
|
|
if !validTransitions[s.phase][phase] {
|
|
return false
|
|
}
|
|
s.phase = phase
|
|
return true
|
|
}
|
|
|
|
func (s *Session) setRange(start, target uint64) {
|
|
s.startLSN = start
|
|
s.targetLSN = target
|
|
// Initialize recoveredTo to startLSN so that delta-based entry counting
|
|
// in RecordCatchUpProgress measures only the actual catch-up work,
|
|
// not the replica's pre-existing prefix.
|
|
s.recoveredTo = start
|
|
}
|
|
|
|
func (s *Session) updateProgress(recoveredTo uint64) {
|
|
if recoveredTo > s.recoveredTo {
|
|
s.recoveredTo = recoveredTo
|
|
}
|
|
}
|
|
|
|
func (s *Session) complete() {
|
|
s.phase = PhaseCompleted
|
|
}
|
|
|
|
func (s *Session) invalidate(reason string) {
|
|
if !s.Active() {
|
|
return
|
|
}
|
|
s.phase = PhaseInvalidated
|
|
s.invalidateReason = reason
|
|
}
|