carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package provision
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "strconv"
10 "strings"
11
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// IsRecordKey recognizes both instance and sidecar control records.
16func IsRecordKey(key string) bool {
17 p := strings.Split(key, "/")
18 return len(p) == 5 && p[0] == "apps" && (p[3] == "instances" || p[3] == "sidecars") && strings.HasSuffix(p[4], ".json")
19}
20
21var ErrRecordSequence = errors.New("provision: invalid record sequence")
22var ErrRecordSequenceExhausted = errors.New("provision: record sequence exhausted")
23
24type recordSequenceKey struct{}
25type preparedRecordKey struct{}
26
27// WithRecordSequence introduces the counter on a first claim-mode write.
28// A populated counter advances even without this context. This is encoding,
29// not mutation authority; callers retain their existing claim and app gates.
30func WithRecordSequence(ctx context.Context) context.Context {
31 return context.WithValue(ctx, recordSequenceKey{}, true)
32}
33
34// RecordSequence preserves unknown fields and parses the counter without a
35// float64 round trip. Missing means legacy zero; null, aliases, duplicates,
36// fractions, negatives and overflow refuse at the claim-on write boundary.
37func RecordSequence(body []byte) (map[string]json.RawMessage, uint64, error) {
38 d := json.NewDecoder(bytes.NewReader(body))
39 if first, err := d.Token(); err != nil || first != json.Delim('{') {
40 return nil, 0, ErrRecordSequence
41 }
42 fields := map[string]json.RawMessage{}
43 var seq uint64
44 for d.More() {
45 token, err := d.Token()
46 if err != nil {
47 return nil, 0, ErrRecordSequence
48 }
49 name, ok := token.(string)
50 if !ok {
51 return nil, 0, ErrRecordSequence
52 }
53 if _, exists := fields[name]; exists {
54 return nil, 0, ErrRecordSequence
55 }
56 var value json.RawMessage
57 if d.Decode(&value) != nil {
58 return nil, 0, ErrRecordSequence
59 }
60 if strings.EqualFold(name, "record_seq") {
61 if name != "record_seq" {
62 return nil, 0, ErrRecordSequence
63 }
64 seq, err = strconv.ParseUint(string(value), 10, 64)
65 if err != nil {
66 return nil, 0, ErrRecordSequence
67 }
68 }
69 fields[name] = value
70 }
71 if end, err := d.Token(); err != nil || end != json.Delim('}') {
72 return nil, 0, ErrRecordSequence
73 }
74 if _, err := d.Token(); err != io.EOF {
75 return nil, 0, ErrRecordSequence
76 }
77 return fields, seq, nil
78}
79
80// AdvanceRecordSequence alone owns the counter. Callers must preserve the
81// read value, including on retries; they cannot reset it or choose a future
82// revision. Exhaustion refuses instead of wrapping to a reusable body.
83func AdvanceRecordSequence(before, body []byte) ([]byte, error) {
84 _, base, err := RecordSequence(before)
85 if err != nil {
86 return nil, err
87 }
88 fields, candidate, err := RecordSequence(body)
89 if err != nil {
90 return nil, err
91 }
92 if candidate != base {
93 return nil, ErrRecordSequence
94 }
95 if base == ^uint64(0) {
96 return nil, ErrRecordSequenceExhausted
97 }
98 fields["record_seq"] = json.RawMessage(strconv.FormatUint(base+1, 10))
99 return json.Marshal(fields)
100}
101
102// preparedRecord binds one synchronous Put to its bytes and CAS base. Store
103// wrappers may call WriteRecord again without advancing twice. An inner
104// claim boundary can introduce the counter and report the written value back
105// to the typed outer writer. This private context grants no write authority.
106type preparedRecord struct {
107 key, etag string
108 body []byte
109 base, seq uint64
110 sequenced bool
111}
112
113// WriteRecord is the only record-key Put primitive. before is the exact
114// If-Match read ({} for create). body preserves that read's counter. Never
115// call it with a synthesized before to reset a live record's sequence.
116// Legacy absent counters and no claim context retain the exact body bytes.
117func WriteRecord(ctx context.Context, s release.Store, key string, before, body []byte, etag string) (string, uint64, error) {
118 if !IsRecordKey(key) {
119 return "", 0, ErrRecordSequence
120 }
121 fields, base, err := RecordSequence(before)
122 if err != nil {
123 return "", 0, err
124 }
125 _, candidate, err := RecordSequence(body)
126 if err != nil {
127 return "", 0, err
128 }
129 enabled, _ := ctx.Value(recordSequenceKey{}).(bool)
130 _, present := fields["record_seq"]
131 p, nested := ctx.Value(preparedRecordKey{}).(*preparedRecord)
132 if nested {
133 if p.key != key || p.etag != etag || p.base != base || p.seq != candidate || !bytes.Equal(p.body, body) {
134 return "", 0, ErrRecordSequence
135 }
136 } else {
137 if candidate != base {
138 return "", 0, ErrRecordSequence
139 }
140 p = &preparedRecord{key: key, etag: etag, body: append([]byte(nil), body...), base: base, seq: base}
141 ctx = context.WithValue(ctx, preparedRecordKey{}, p)
142 }
143 if !p.sequenced && (enabled || present) {
144 body, err = AdvanceRecordSequence(before, body)
145 if err != nil {
146 return "", 0, err
147 }
148 p.body, p.seq, p.sequenced = append([]byte(nil), body...), base+1, true
149 }
150 tag, err := s.Put(ctx, key, body, etag)
151 return tag, p.seq, err
152}
153