carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package runs
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "sort"
9
10 "github.com/carlosframework/platform/internal/release"
11)
12
13// checkRun is what every helper below does first. It refuses an account or
14// app that release does not accept (ErrBadApp) and an id that is not a run
15// id (ErrBadID) before it builds a key, so no caller can reach another run's objects, another app's runs, or
16// a key with a ".." in it, through a crafted value. Nothing is read or written
17// for a refused call.
18func checkRun(acct, app, id string) error {
19 if !release.ValidAccount(acct) || !release.ValidApp(app) {
20 return ErrBadApp
21 }
22 if !ValidID(id) {
23 return ErrBadID
24 }
25 return nil
26}
27
28// ReadRequest reads request.json; absent is os.ErrNotExist.
29func ReadRequest(ctx context.Context, s release.Store, acct, app, id string) (*Request, error) {
30 if err := checkRun(acct, app, id); err != nil {
31 return nil, err
32 }
33 raw, _, err := s.Get(ctx, RequestKey(acct, app, id))
34 if err != nil {
35 return nil, err
36 }
37 var r Request
38 if err := json.Unmarshal(raw, &r); err != nil {
39 return nil, err
40 }
41 return &r, nil
42}
43
44// ReadResult reads result.json; absent is (nil, nil).
45func ReadResult(ctx context.Context, s release.Store, acct, app, id string) (*Result, error) {
46 if err := checkRun(acct, app, id); err != nil {
47 return nil, err
48 }
49 raw, _, err := s.Get(ctx, ResultKey(acct, app, id))
50 if errors.Is(err, os.ErrNotExist) {
51 return nil, nil
52 }
53 if err != nil {
54 return nil, err
55 }
56 var r Result
57 if err := json.Unmarshal(raw, &r); err != nil {
58 return nil, err
59 }
60 return &r, nil
61}
62
63// WriteResult writes result.json create-only: the first result wins, and a
64// later writer (the sweep's lost result racing the box's) changes nothing.
65func WriteResult(ctx context.Context, s release.Store, acct, app, id string, r Result) error {
66 if err := checkRun(acct, app, id); err != nil {
67 return err
68 }
69 body, err := json.Marshal(r)
70 if err != nil {
71 return err
72 }
73 if _, err := s.Put(ctx, ResultKey(acct, app, id), body, ""); err != nil && !errors.Is(err, release.ErrCASConflict) {
74 return err
75 }
76 return nil
77}
78
79// ListChunks lists chunk keys after seq, sorted, at most limit (0: all).
80func ListChunks(ctx context.Context, s release.Store, acct, app, id string, after int64, limit int) ([]string, error) {
81 if err := checkRun(acct, app, id); err != nil {
82 return nil, err
83 }
84 prefix := OutPrefix(acct, app, id)
85 startAfter := prefix
86 if after > 0 {
87 startAfter = ChunkKey(acct, app, id, after, "~") // "~" sorts after both streams of seq
88 }
89 if pl, ok := s.(release.PageLister); ok {
90 return pl.ListPage(ctx, prefix, startAfter, limit)
91 }
92 keys, err := s.List(ctx, prefix)
93 if err != nil {
94 return nil, err
95 }
96 sort.Strings(keys)
97 var out []string
98 for _, k := range keys {
99 if k <= startAfter {
100 continue
101 }
102 out = append(out, k)
103 if limit > 0 && len(out) == limit {
104 break
105 }
106 }
107 return out, nil
108}
109
110// LastChunkSeq is the highest seq written, 0 when none.
111func LastChunkSeq(ctx context.Context, s release.Store, acct, app, id string) (int64, error) {
112 if err := checkRun(acct, app, id); err != nil {
113 return 0, err
114 }
115 keys, err := ListChunks(ctx, s, acct, app, id, 0, 0)
116 if err != nil {
117 return 0, err
118 }
119 var last int64
120 for _, k := range keys {
121 if seq, _, ok := ParseChunkKey(k, OutPrefix(acct, app, id)); ok && seq > last {
122 last = seq
123 }
124 }
125 return last, nil
126}
127
128// DeleteRun deletes every object of the run, in an order that keeps a
129// half-deleted run findable and audited once. The input and output objects
130// go first; result.json goes only after they are all gone, so a run whose
131// delete failed part way still answers a follower with how it ended;
132// request.json, which the orphan sweep ages a run by and which says whose
133// run it was, goes after the result; the audited marker goes last, so a run
134// that still has any object still has its marker, and nothing can write the
135// run-end row a second time. A tier that fails stops the later ones, and the
136// call can be repeated.
137func DeleteRun(ctx context.Context, s release.Store, acct, app, id string) error {
138 if err := checkRun(acct, app, id); err != nil {
139 return err
140 }
141 keys, err := s.List(ctx, Prefix(acct, app, id))
142 if err != nil {
143 return err
144 }
145 result, request, audited := ResultKey(acct, app, id), RequestKey(acct, app, id), AuditedKey(acct, app, id)
146 tiers := [4][]string{}
147 for _, k := range keys {
148 switch k {
149 case audited:
150 tiers[3] = append(tiers[3], k)
151 case request:
152 tiers[2] = append(tiers[2], k)
153 case result:
154 tiers[1] = append(tiers[1], k)
155 default:
156 tiers[0] = append(tiers[0], k)
157 }
158 }
159 for _, tier := range tiers {
160 var errs []error
161 for _, k := range tier {
162 if err := s.Delete(ctx, k); err != nil {
163 errs = append(errs, err)
164 }
165 }
166 if err := errors.Join(errs...); err != nil {
167 return err
168 }
169 }
170 return nil
171}
172