package moves import ( "math" "time" ) // The T11 cut budget (moves spec §7.3 T11, §7.4): pure arithmetic that // prices a move's stop-to-open time from measured rates and decides how // many probe attempts fit the operator's window. Ported from the model in // internal/sim/hostlife/budget_test.go at 9c417aaa; the constants below // that duplicate the model's (cutWindowDefault, probeAttemptsMax, // probeSeconds, settleSeconds, admitSeconds, mirrorBarMiB) are twins of it // and change together. All times are seconds, all sizes and rates MiB. const ( CutWindowDefault = 120.0 // the operator's default cut window OpenBudget = 5.0 // the target's open after the commit (§7.4) ProbeAttemptsMax = 3 // §7.7: never more than three attempts ProbeSeconds = 30.0 SettleSeconds = 20.0 AdmitSeconds = 2.0 // the admitting CAS, the purge and generation.json MirrorBarMiB = 16.0 // §7.3: the final mirror round is at most this much ArchiveSeconds = 2.0 // the archive phase's floor (ruling T7-budget) ) // CutEstimate is a cut priced phase by phase. type CutEstimate struct { Stop, Checkpoint, Mirror, Archive, Render, Open float64 RestoreStaged, SnapshotBuild, Anchor, Verify float64 // target-side build, whole database RestoreAttempt, Admit, FreshUpload float64 // per probe attempt Probe, Settle float64 // one attempt } // Rates are the measured rates T11 reads, in MiB/s: the cross-region copy // path, regional S3 (reads and writes from a box in the bucket's region) // and the box's disk. MirrorBar is the final mirror round's size bar in // MiB; zero means MirrorBarMiB. It is the one knob Task 11 turns // (CARLOS_MOVE_READY_DELTA_BYTES, ruling F28(e)). FsyncRate is the target // disk's per-file sync rate in files per second (ruling T7-budget): the // landing's extract syncs every file, so the archive's entry count prices // the Archive phase. type Rates struct { Cross, S3, Disk float64 MirrorBar float64 FsyncRate float64 } // ExampleRates are the rates the spec's worked example uses (§7.3 T11): // 10 MiB/s across regions, 250 MiB/s to regional S3, 500 MiB/s on disk, // and (not in the spec's example, which has no files) 1600 files a second // synced, about what Task 7 measured for its 16-worker pool. Tests and // documentation only: T11 reads measured rates. var ExampleRates = Rates{Cross: 10, S3: 250, Disk: 500, FsyncRate: 1600} // Estimate prices a cut for a database of dbMiB at the given rates. The // final mirror is within the bar across regions. On the target every // build pass moves the whole database once across S3 and once across the // disk, bar the verification, which reads the disk alone. Each probe // attempt restores the prepared generation again and pays the admission; // a retried probe also uploads the whole database as a fresh generation. // The archive phase is at least ArchiveSeconds, and longer when the // archive's entries take longer than that to sync on the target at // r.FsyncRate (ruling T7-budget): max(2, entries/FsyncRate). // // An input that is not a usable measurement (a rate that is not a positive // finite number, a negative, NaN or infinite database size or bar, a // negative entry count, or entries with no usable fsync rate) prices the cut // at infinity, so it is refused: a negative rate must never price a phase // below zero and let a move in. With no entries the fsync rate is not // needed, but a negative, NaN or infinite one is still refused. func Estimate(dbMiB float64, entries int, r Rates) CutEstimate { if !usable(r.Cross) || !usable(r.S3) || !usable(r.Disk) || !finite(dbMiB) || dbMiB < 0 || !finite(r.MirrorBar) || r.MirrorBar < 0 || entries < 0 || !finite(r.FsyncRate) || r.FsyncRate < 0 || (entries > 0 && !usable(r.FsyncRate)) { inf := math.Inf(1) return CutEstimate{Stop: inf, Checkpoint: inf, Mirror: inf, Archive: inf, Render: inf, Open: inf} } bar := r.MirrorBar if bar == 0 { bar = MirrorBarMiB } archive := ArchiveSeconds if entries > 0 { archive = math.Max(ArchiveSeconds, float64(entries)/r.FsyncRate) } return CutEstimate{ Stop: 2, Checkpoint: 1, Archive: archive, Render: 1, Open: OpenBudget, Mirror: bar / r.Cross, RestoreStaged: dbMiB/r.S3 + dbMiB/r.Disk, SnapshotBuild: dbMiB/r.Disk + dbMiB/r.S3, Anchor: dbMiB/r.Disk + dbMiB/r.S3, Verify: dbMiB / r.Disk, RestoreAttempt: dbMiB/r.S3 + dbMiB/r.Disk, Admit: AdmitSeconds, FreshUpload: dbMiB/r.Disk + dbMiB/r.S3, Probe: ProbeSeconds, Settle: SettleSeconds, } } // Fixed is the cost of every phase outside the probe attempts. func (e CutEstimate) Fixed() float64 { return e.Stop + e.Checkpoint + e.Mirror + e.Archive + e.RestoreStaged + e.SnapshotBuild + e.Anchor + e.Verify + e.Render + e.Open } // Attempt prices the i-th probe attempt (from 1): the restore of the // prepared generation and the admission overhead, the probe and its // settle; from the second attempt on, the fresh generation's first upload // as well. func (e CutEstimate) Attempt(i int) float64 { c := e.RestoreAttempt + e.Admit + e.Probe + e.Settle if i > 1 { c += e.FreshUpload } return c } // AttemptsAdmitted is T11: the attempts permitted are those whose summed // costs fit the window after the fixed costs, at most ProbeAttemptsMax. // The instance is eligible when that is at least one, and the probe loop // runs at most that many. func (e CutEstimate) AttemptsAdmitted(window float64) int { left := window - e.Fixed() n := 0 for n < ProbeAttemptsMax && left >= e.Attempt(n+1) { left -= e.Attempt(n + 1) n++ } return n } // Total prices the cut with n attempts. func (e CutEstimate) Total(n int) float64 { t := e.Fixed() for i := 1; i <= n; i++ { t += e.Attempt(i) } return t } // CutDeadline is §7.4's one absolute deadline: cut_at + cut_budget - // open_budget. Every phase before the commit checks it; the commit CAS is // conditional on it. func CutDeadline(cutAt time.Time, windowSeconds float64) time.Time { return cutAt.Add(time.Duration((windowSeconds - OpenBudget) * float64(time.Second))) } func finite(x float64) bool { return !math.IsNaN(x) && !math.IsInf(x, 0) } // usable is a rate that can be divided by: positive and finite. func usable(rate float64) bool { return finite(rate) && rate > 0 }