carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3// restore_recover.go extends the control writer's restore transitions
4// (control_writer.go: PrepareRestore, CompleteRestore, RecoverMutation) with
5// the one case RecoverMutation leaves pending: a restore operation that never
6// saved its receipt.
7//
8// A restore's only external effect is the exact-ETag delete of one trash
9// marker, and the issuer saves the receipt (PrepareRestore) right after
10// BeginExclusive, before it issues any effect and before any SQL. So an
11// operation with no receipt has issued nothing, and reopening it is a fence,
12// not a guess: after the reopen CAS (which advances the epoch) the stale
13// issuer can neither save a receipt (OperationHandle.current refuses), nor
14// issue an effect (EffectRunner.authorized refuses), nor pass the data
15// boundary (its permit needs the closed operation, its epoch and an issued
16// effect). An operation that does have an issued or uncertain effect is
17// never abandoned, whatever its age or receipt. The age bound only avoids
18// interrupting a live issuer between BeginExclusive and PrepareRestore; it
19// is not the safety argument.
20
21import (
22 "context"
23 "math"
24 "strconv"
25 "time"
26)
27
28// RestoreActionStarted is the intent key the restore issuer sets at
29// BeginExclusive (unix seconds); AbandonRestore measures age from it.
30const RestoreActionStarted = "started"
31
32// RestoreOutcome is what RecoverRestore did.
33type RestoreOutcome string
34
35const (
36 RestoreNone RestoreOutcome = "" // no restore operation owns the app
37 RestoreRestored RestoreOutcome = "restored" // a saved receipt was finished and admission reopened
38 RestoreAbandoned RestoreOutcome = "abandoned" // no receipt was saved; admission reopened
39)
40
41// IsRestoreOp reports whether op is a restore app-mutation.
42func IsRestoreOp(op *AppOperation) bool {
43 return op != nil && op.Kind == OpAppMutation && op.Intent["action"] == "restore"
44}
45
46// AbandonRestore reopens admission held by a receipt-less restore operation
47// at least minAge old. It reopens only when nothing of the operation was
48// issued: each of its effects is absent, already complete, or merely
49// prepared (settled "superseded", as EffectRunner.Issue supersedes one). An
50// issued or uncertain effect, a saved receipt, a registration or any other
51// operation's unsettled effect refuses. The reopen advances the epoch, as
52// every reopen does.
53func (w *ControlWriter) AbandonRestore(ctx context.Context, account, app, appID, opID string, now time.Time, minAge time.Duration) error {
54 a, err := w.read(ctx, account, app, appID)
55 if err != nil {
56 return err
57 }
58 _, err = w.updateAdmission(ctx, a, func(v *AppAdmission) error {
59 if v.State != "closed" || v.Op == nil || v.Op.ID != opID || v.Op.AppID != appID || !IsRestoreOp(v.Op) || len(v.Registrations) != 0 {
60 return ErrPending
61 }
62 if r := v.LastRestore; r != nil && r.ID == opID {
63 return ErrPending // a saved receipt is finished by RecoverMutation, never abandoned
64 }
65 sec, perr := strconv.ParseInt(v.Op.Intent[RestoreActionStarted], 10, 64)
66 if perr != nil || sec <= 0 || now.Before(time.Unix(sec, 0).Add(minAge)) {
67 return ErrPending
68 }
69 for id, e := range v.Effects {
70 switch {
71 case e.State == "complete":
72 case e.OperationID == opID && e.AppID == appID && e.State == "prepared":
73 e.State, e.ResultReference = "complete", "superseded"
74 v.Effects[id] = e
75 default:
76 return ErrPending // issued, uncertain, or another operation's
77 }
78 }
79 if v.Epoch == math.MaxUint64 {
80 return ErrPending
81 }
82 v.Op, v.State = nil, "open"
83 v.Epoch++
84 return nil
85 })
86 return err
87}
88
89// RecoverRestore finishes whatever restore operation owns the app: a saved
90// receipt through RecoverMutation (the exact-ETag delete replay; a prepared
91// receipt may have no effect yet, because the receipt precedes Issue), a
92// receipt-less operation through AbandonRestore. Any other owner, or no
93// owner, is RestoreNone. It never starts an operation and never reopens an
94// admission a non-restore operation holds.
95func (w *ControlWriter) RecoverRestore(ctx context.Context, account, app, appID string, now time.Time, minAge time.Duration) (RestoreOutcome, error) {
96 a, err := w.read(ctx, account, app, appID)
97 if err != nil {
98 return RestoreNone, err
99 }
100 op := a.Value.Op
101 if a.Value.State != "closed" || !IsRestoreOp(op) {
102 return RestoreNone, nil
103 }
104 if r := a.Value.LastRestore; r != nil && r.ID == op.ID {
105 if err := w.RecoverMutation(ctx, account, app, appID, op.ID); err != nil {
106 return RestoreNone, err
107 }
108 return RestoreRestored, nil
109 }
110 if err := w.AbandonRestore(ctx, account, app, appID, op.ID, now, minAge); err != nil {
111 return RestoreNone, err
112 }
113 return RestoreAbandoned, nil
114}
115