carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "log"
8 "sort"
9 "strings"
10 "time"
11
12 "github.com/carlosframework/platform/internal/hostbox"
13)
14
15const servingParkRecoveryLimit = 128
16
17var errServingParkRetagPending = errors.New("the earlier park is incomplete; retain its evidence and inspect the root service journal before retrying")
18
19type servingParkRecoveryResult struct {
20 Host, ID string
21 Err error
22}
23
24type servingParkRecoveryPage struct {
25 Results []servingParkRecoveryResult
26 Next string
27 More bool
28}
29
30// The cursor bounds record processing and lets a later pass reach records
31// beyond a blocked host. Store.Names still enumerates the private directory.
32func (l *servingParkLedger) recoveryIDs(ctx context.Context, after string, limit int) ([]string, bool, error) {
33 if ctx == nil || l == nil || l.state == nil || limit < 1 || limit > servingParkRecoveryLimit {
34 return nil, false, hostbox.ErrUnsupported
35 }
36 if err := ctx.Err(); err != nil {
37 return nil, false, err
38 }
39 names, err := l.state.Names()
40 if err != nil {
41 return nil, false, err
42 }
43 var ids []string
44 for _, name := range names {
45 if strings.HasPrefix(name, "retirement-") && strings.HasSuffix(name, ".json") {
46 id := strings.TrimSuffix(strings.TrimPrefix(name, "retirement-"), ".json")
47 if id > after {
48 ids = append(ids, id)
49 }
50 }
51 }
52 sort.Strings(ids)
53 more := len(ids) > limit
54 if more {
55 ids = ids[:limit]
56 }
57 return ids, more, ctx.Err()
58}
59
60// Completed deletion needs no live claim or agent. Its original durable cause
61// and recorded identities remain the authority after a host is removed.
62func (l *servingParkLedger) recoverCompleted(ctx context.Context, id string) error {
63 return l.withNamespace(ctx, func() error {
64 r, err := l.read(id)
65 if err != nil {
66 return err
67 }
68 unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(r.Authorization.Successor.Box, r.Authorization.Successor.Host)+".lock")
69 if err != nil {
70 return err
71 }
72 defer unlock()
73 unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock")
74 if err != nil {
75 return err
76 }
77 defer unlockRecord()
78 if !r.Complete {
79 return hostbox.ErrChanged
80 }
81 if r.Deleting {
82 if !r.Closed {
83 err = l.deleteLocked(ctx, &r, r.Cause)
84 } else {
85 // Older records can be closed with their empty event retained.
86 err = l.fs.Delete(&r, func() error {
87 if err := ctx.Err(); err != nil {
88 return err
89 }
90 return l.save(r)
91 })
92 }
93 if err != nil {
94 return err
95 }
96 }
97 // Completion may be durable while clearing Pending lost its reply.
98 // Last and ID are replay references and survive this cleanup.
99 slot, err := l.slot(r.Authorization)
100 if err != nil {
101 return err
102 }
103 if slot.Pending == id {
104 if err = ctx.Err(); err != nil {
105 return err
106 }
107 slot.Pending = ""
108 if err = l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot); err != nil {
109 return err
110 }
111 }
112 return l.collectClosedLocked(ctx, &r, slot)
113 })
114}
115
116func (p *servingParkRuntime) recoverPending(ctx context.Context) error {
117 if p == nil {
118 return nil
119 }
120 call, done, err := p.begin(ctx)
121 if err != nil {
122 return err
123 }
124 defer done()
125 recovery, ok := p.authority.(interface {
126 recoverServingParks(context.Context, string, int) (servingParkRecoveryPage, error)
127 })
128 if !ok {
129 return hostbox.ErrUnsupported
130 }
131 p.recoveryMu.Lock()
132 defer p.recoveryMu.Unlock()
133 page, err := recovery.recoverServingParks(call, p.recoveryAfter, servingParkRecoveryLimit)
134 if err == nil {
135 p.recoveryAfter = ""
136 if page.More {
137 p.recoveryAfter = page.Next
138 }
139 }
140 var failures []error
141 if err != nil {
142 failures = append(failures, hostbox.ErrChanged)
143 }
144 for _, result := range page.Results {
145 if result.Err != nil {
146 // Draft retirement.reap_failed and reason.park_pending from the index.
147 reason := "a local park operation failed; inspect the root service journal"
148 if errors.Is(result.Err, errServingParkRetagPending) {
149 reason = errServingParkRetagPending.Error()
150 } else if errors.Is(result.Err, hostbox.ErrChanged) || errors.Is(result.Err, ErrClaimStopReceipt) {
151 reason = "the parked artifact or its parent no longer matches the root-recorded identity"
152 }
153 if result.Host == "unknown" {
154 reason = "the private retirement record could not be validated; retain the data for investigation"
155 }
156 failures = append(failures, fmt.Errorf("Host %q: serving database retirement %q retained: %s.", result.Host, result.ID, reason))
157 }
158 }
159 if len(failures) > 0 && time.Since(p.lastRecoveryLog) >= time.Hour {
160 log.Print(errors.Join(failures...))
161 p.lastRecoveryLog = time.Now()
162 }
163 return errors.Join(err, call.Err())
164}
165