| index fa6e582e9..ec5eadfc3 100644 |
| --- a/internal/host/movesrecoveryowners.go |
| +++ b/internal/host/movesrecoveryowners.go |
| @@ -9,6 +9,7 @@ import ( |
| "path/filepath" |
| "reflect" |
| "strconv" |
| + "strings" |
| |
| "github.com/carlosframework/platform/internal/activator" |
| "github.com/carlosframework/platform/internal/appconfig" |
| @@ -105,6 +106,8 @@ type recoveryProbe struct { |
| dirty, uncovered bool |
| srcGen string |
| predecessor string |
| + // cut is the cut's journalled session (op.journal.cut_session). |
| + cut string |
| // A dirty return's: the rollback id (its generation), the return seal |
| // and the install manifest its install journalled. |
| rb string |
| @@ -169,7 +172,7 @@ func (r movesRecoveryOwners) recovery(ctx context.Context, o hostclaim.Observed) |
| if err != nil || stamp == 0 || !journalled { |
| return v, fmt.Errorf("%w: the source's stamp and generation are not journalled", hostbox.ErrChanged) |
| } |
| - v.first, v.srcGen = v.holder.Stamp == stamp, srcGen |
| + v.first, v.srcGen, v.cut = v.holder.Stamp == stamp, srcGen, c.Op.Journal[hostclaim.JournalCutSession] |
| rec, err := m.Records(ctx, v.holder.Account, v.holder.App, o.Host) |
| if err != nil { |
| return v, err |
| @@ -318,9 +321,20 @@ func (r movesRecoveryOwners) admission(ctx context.Context, o hostclaim.Observed |
| // baseline is a fresh generation's baseline: the certified remote; for a |
| // dirty return's restart while the location's marker is not yet the return |
| // generation's (nor a generation descending from it on this box), the |
| -// return generation's pin (H14). |
| +// return generation's pin (H14); for a clean return while the recovery has |
| +// no root, the recorded source baseline (sourceBaseline, fix round 1 ruling |
| +// 4). |
| func (r movesRecoveryOwners) baseline(ctx context.Context, host string, v recoveryProbe) (*genselect.Plan, error) { |
| m := r.m |
| + if v.kind == hostclaim.KindRestoring && !v.dirty { |
| + rooted, err := r.recoveryRooted(ctx, host, v) |
| + if err != nil { |
| + return nil, err |
| + } |
| + if !rooted { |
| + return r.sourceBaseline(ctx, host, v) |
| + } |
| + } |
| if v.dirty { |
| digest, err := manifestDigest(v.manifest) |
| if err != nil { |
| @@ -492,6 +506,9 @@ func (r movesRecoveryOwners) Materialize(ctx context.Context, o hostclaim.Observ |
| if err := r.recordPlacement(ctx, o.Host, v, work, in.Identity); err != nil { |
| return err |
| } |
| + if err := r.recordSourceBaseline(o.Host, v, in.Baseline); err != nil { |
| + return err |
| + } |
| if v.dirty { |
| receipt, err := m.preparedMarker(ctx, o.Host, v.prepared(), fence) |
| if err != nil { |
| @@ -776,3 +793,107 @@ func (r movesRecoveryOwners) verifyContent(host string, p recoveryPlacement, wan |
| } |
| return nil |
| } |
| + |
| +// rcStepSourceBaseline is the rollback journal's record of a clean return's |
| +// source baseline (JSON genselect.Plan): src_gen's files by exact version, |
| +// as the recovery's first start restored them. |
| +const rcStepSourceBaseline = "rc_source_baseline" |
| + |
| +// recoveryRooted reports whether the clean return's recovery has a root: an |
| +// object under a generation one of its admissions made (this box, this |
| +// region, a session after the cut's). Before it has, ordinary selection |
| +// would follow the claim's predecessor chain through the target's admission |
| +// (AdmitWriter records it as the first recovery admission's predecessor, |
| +// though the restore was from src_gen) and can end in a covered baseline. |
| +func (r movesRecoveryOwners) recoveryRooted(ctx context.Context, host string, v recoveryProbe) (bool, error) { |
| + m := r.m |
| + h := v.holder |
| + cut, err := strconv.ParseUint(v.cut, 10, 64) |
| + if err != nil { |
| + return false, fmt.Errorf("%w: the cut's session is not journalled", hostbox.ErrChanged) |
| + } |
| + location := strings.TrimSuffix(h.Location, "/") |
| + for _, a := range h.Admissions { |
| + if a.Box != m.Box || a.Session <= cut || a.Generation == "" || !park.SameRegion(m.HomeRegion, a.Region, m.Region) { |
| + continue |
| + } |
| + prefix := location + "/g-" + a.Generation + "/" |
| + versions, err := m.Data.ListVersions(ctx, m.Bucket, prefix) |
| + if err != nil { |
| + return false, err |
| + } |
| + for _, ver := range versions { |
| + if strings.HasPrefix(ver.Key, prefix) && ver.IsLatest && !ver.DeleteMarker { |
| + return true, nil |
| + } |
| + } |
| + } |
| + return false, nil |
| +} |
| + |
| +// sourceBaseline is a clean return's restore point before the recovery has |
| +// a root: src_gen in this region, selected as the first admission selected |
| +// it (the holder viewed at src_gen, whose marker names it), and, once |
| +// recorded, exactly the recorded files by version. |
| +func (r movesRecoveryOwners) sourceBaseline(ctx context.Context, host string, v recoveryProbe) (*genselect.Plan, error) { |
| + m := r.m |
| + view := *v.holder |
| + view.Generation = v.srcGen |
| + plan, err := genselect.Select(ctx, m.Data, m.request(host, &view)) |
| + if err != nil { |
| + return nil, err |
| + } |
| + if plan.Generation != v.srcGen { |
| + return nil, fmt.Errorf("%w: the source baseline is not src_gen", hostbox.ErrChanged) |
| + } |
| + recorded, ok, err := r.recordedSourceBaseline(host, v) |
| + if err != nil { |
| + return nil, err |
| + } |
| + if ok && (recorded.Generation != plan.Generation || recorded.MaxTXID != plan.MaxTXID || !reflect.DeepEqual(recorded.Files, plan.Files) || !reflect.DeepEqual(recorded.Pin, plan.Pin)) { |
| + return nil, fmt.Errorf("%w: src_gen is not the recorded source baseline", hostbox.ErrChanged) |
| + } |
| + return plan, nil |
| +} |
| + |
| +func (r movesRecoveryOwners) recordedSourceBaseline(host string, v recoveryProbe) (genselect.Plan, bool, error) { |
| + j, err := moves.ReadJournal(r.m.DataDir, host, v.journalOp()) |
| + if err != nil { |
| + return genselect.Plan{}, false, err |
| + } |
| + raw, ok := j.Steps[rcStepSourceBaseline] |
| + if !ok { |
| + return genselect.Plan{}, false, nil |
| + } |
| + var p genselect.Plan |
| + if err := json.Unmarshal([]byte(raw), &p); err != nil || p.Generation == "" || len(p.Files) == 0 { |
| + return genselect.Plan{}, false, fmt.Errorf("%w: the recorded source baseline does not read (%v)", hostbox.ErrChanged, err) |
| + } |
| + return p, true, nil |
| +} |
| + |
| +// recordSourceBaseline journals a clean return's source baseline before the |
| +// first start that restores it; once recorded it is never rewritten. |
| +func (r movesRecoveryOwners) recordSourceBaseline(host string, v recoveryProbe, plan *genselect.Plan) error { |
| + if v.kind != hostclaim.KindRestoring || v.dirty || plan == nil || plan.Generation != v.srcGen { |
| + return nil |
| + } |
| + m := r.m |
| + body, err := json.Marshal(genselect.Plan{Generation: plan.Generation, Files: plan.Files, Root: plan.Root, Pin: plan.Pin, MaxTXID: plan.MaxTXID, MaxLevel0TXID: plan.MaxLevel0TXID}) |
| + if err != nil { |
| + return err |
| + } |
| + m.mu.Lock() |
| + defer m.mu.Unlock() |
| + err = moves.UpdateJournal(m.DataDir, host, v.journalOp(), func(j *moves.Journal) error { |
| + if _, ok := j.Steps[rcStepSourceBaseline]; ok { |
| + return errJournalUnchanged |
| + } |
| + j.Mark(rcStepSourceBaseline, string(body)) |
| + return nil |
| + }) |
| + if errors.Is(err, errJournalUnchanged) { |
| + return nil |
| + } |
| + return err |
| +} |