carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package dbreads
2
3import (
4 "bytes"
5 "context"
6 "crypto/ecdh"
7 "crypto/sha256"
8 "encoding/hex"
9 "errors"
10 "fmt"
11 "hash"
12 "sync"
13 "time"
14
15 "github.com/carlosframework/platform/internal/release"
16)
17
18var errCapped = errors.New("dbreads: the output cap was reached")
19
20// putCreateOnly creates key with body, retrying until it lands or ctx ends.
21// An error or a conflict is resolved by reading the key back: the same
22// bytes mean an earlier attempt landed (spec §5.4). The caller keeps body
23// until this returns, so a retry is the same bytes.
24func putCreateOnly(ctx context.Context, s release.Store, key string, body []byte) error {
25 wait := 250 * time.Millisecond
26 for {
27 _, err := s.Put(ctx, key, body, "")
28 if err == nil {
29 return nil
30 }
31 got, _, gerr := s.Get(ctx, key)
32 switch {
33 case gerr == nil && bytes.Equal(got, body):
34 return nil
35 case gerr == nil:
36 return fmt.Errorf("dbreads: %s holds other bytes: %w", key, release.ErrCASConflict)
37 }
38 select {
39 case <-ctx.Done():
40 return ctx.Err()
41 case <-time.After(wait):
42 }
43 wait = min(wait*2, 5*time.Second)
44 }
45}
46
47// chunkWriter seals a read's output into out/<seq> objects: one every
48// size bytes (ChunkBytes for a query, PullChunkBytes for a pull), and whatever is pending every `every`; close seals what is
49// left, possibly nothing, as the last chunk. Past max nothing more is kept
50// and Write answers errCapped. Each put is create-only and confirmed before
51// the next is sealed, under the lock, so seqs are in order (the run
52// chunker's discipline).
53type chunkWriter struct {
54 ctx context.Context
55 store release.Store
56 id string
57 pub *ecdh.PublicKey
58 stream string
59 max int64
60 size int
61
62 mu sync.Mutex
63 pending []byte
64 seq int64
65 total int64
66 sum hash.Hash
67 err error
68 isCapped bool
69
70 stop chan struct{}
71 done chan struct{}
72}
73
74func newChunkWriter(ctx context.Context, s release.Store, id string, pub *ecdh.PublicKey, stream string, every time.Duration, max int64, size int) *chunkWriter {
75 c := &chunkWriter{ctx: ctx, store: s, id: id, pub: pub, stream: stream, max: max, size: size, sum: sha256.New(),
76 stop: make(chan struct{}), done: make(chan struct{})}
77 go c.tick(every)
78 return c
79}
80
81func (c *chunkWriter) tick(every time.Duration) {
82 defer close(c.done)
83 t := time.NewTicker(every)
84 defer t.Stop()
85 for {
86 select {
87 case <-c.stop:
88 return
89 case <-t.C:
90 c.mu.Lock()
91 if len(c.pending) > 0 && c.err == nil {
92 c.err = c.flushLocked(c.pending, false)
93 c.pending = nil
94 }
95 c.mu.Unlock()
96 }
97 }
98}
99
100func (c *chunkWriter) Write(p []byte) (int, error) {
101 c.mu.Lock()
102 defer c.mu.Unlock()
103 if c.err != nil {
104 return 0, c.err
105 }
106 if c.isCapped {
107 return 0, errCapped
108 }
109 capped := false
110 if room := c.max - c.total; int64(len(p)) > room {
111 p, capped = p[:room], true
112 }
113 c.sum.Write(p)
114 c.total += int64(len(p))
115 c.pending = append(c.pending, p...)
116 for len(c.pending) >= c.size {
117 if err := c.flushLocked(c.pending[:c.size], false); err != nil {
118 c.err = err
119 return 0, err
120 }
121 c.pending = append([]byte(nil), c.pending[c.size:]...)
122 }
123 if capped {
124 c.isCapped = true
125 return len(p), errCapped
126 }
127 return len(p), nil
128}
129
130// flushLocked seals and creates the next chunk. seq moves only once the
131// create is confirmed, so close never reports, and no result ever names, a
132// chunk that was not confirmed (spec §5.4; plan review finding 9). A chunk
133// whose create stayed ambiguous until the context ended may land later at
134// seq+1; no result names it, so no reader relays it.
135func (c *chunkWriter) flushLocked(b []byte, last bool) error {
136 next := c.seq + 1
137 sealed, err := SealChunk(c.pub, c.id, next, c.stream, last, b)
138 if err != nil {
139 return err
140 }
141 if err := putCreateOnly(c.ctx, c.store, ChunkKey(c.id, next), sealed); err != nil {
142 return err
143 }
144 c.seq = next
145 return nil
146}
147
148func (c *chunkWriter) capped() bool {
149 c.mu.Lock()
150 defer c.mu.Unlock()
151 return c.isCapped
152}
153
154// close seals the pending bytes as the last chunk and reports the last seq,
155// the plaintext byte count and its SHA-256.
156func (c *chunkWriter) close() (int64, int64, string, error) {
157 close(c.stop)
158 <-c.done
159 c.mu.Lock()
160 defer c.mu.Unlock()
161 if c.err == nil {
162 c.err = c.flushLocked(c.pending, true)
163 c.pending = nil
164 }
165 return c.seq, c.total, hex.EncodeToString(c.sum.Sum(nil)), c.err
166}
167