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 "context"
5 "errors"
6 "fmt"
7 "sync"
8 "time"
9)
10
11var (
12 errBusy = errors.New("dbreads: this box is running its two reads")
13 errOrphans = errors.New("dbreads: a carlos-dbread@ unit this agent is not running is still active")
14 errNoRoom = errors.New("dbreads: not enough disk for the read")
15)
16
17// admission is the agent's one table (spec §5.7, J5): how many worker
18// connections it holds, which reads it is running, and every disk
19// reservation. The agent is the only process that starts workers, so one
20// mutex covers it all.
21type admission struct {
22 units func(context.Context) (int, error)
23 free func() (avail, total uint64, err error)
24
25 mu sync.Mutex
26 conns int
27 running map[string]bool
28 held map[string]int64
29 probing bool
30}
31
32func newAdmission(units func(context.Context) (int, error), free func() (uint64, uint64, error)) *admission {
33 return &admission{units: units, free: free, running: map[string]bool{}, held: map[string]int64{}}
34}
35
36// reconcileLocked is the refusal-based reconciliation (spec §5.7): while any
37// carlos-dbread@ unit is active, activating or deactivating beyond the
38// connections this agent holds, nothing is admitted. A finished read's
39// reservation is released only when no unit is left at all: then every
40// stopped unit's scratch has been removed by systemd.
41func (a *admission) reconcileLocked(ctx context.Context) error {
42 n, err := a.units(ctx)
43 if err != nil {
44 return fmt.Errorf("dbreads: list carlos-dbread@ units: %w", err)
45 }
46 if n > a.conns {
47 return errOrphans
48 }
49 if n == 0 {
50 for id := range a.held {
51 if !a.running[id] {
52 delete(a.held, id)
53 }
54 }
55 }
56 return nil
57}
58
59// fitsLocked: after subtracting every other reservation and need, the
60// filesystem holding /var/tmp keeps DiskFloor and DiskFreePercent free.
61func (a *admission) fitsLocked(id string, need int64) bool {
62 avail, total, err := a.free()
63 if err != nil {
64 return false
65 }
66 left := int64(avail) - need
67 for k, v := range a.held {
68 if k != id {
69 left -= v
70 }
71 }
72 return left >= DiskFloor && left >= int64(total)*DiskFreePercent/100
73}
74
75// admitRead admits a read before it enters the election: a free read slot,
76// no orphan, and room for the floor reservation.
77func (a *admission) admitRead(ctx context.Context, id string) error {
78 a.mu.Lock()
79 defer a.mu.Unlock()
80 if err := a.reconcileLocked(ctx); err != nil {
81 return err
82 }
83 if len(a.running) >= ReadsPerBox {
84 return errBusy
85 }
86 if !a.fitsLocked(id, DiskFloor) {
87 return errNoRoom
88 }
89 a.running[id] = true
90 a.held[id] = DiskFloor
91 return nil
92}
93
94// grow raises a running read's reservation to need (never below the floor)
95// once its plan is known.
96func (a *admission) grow(id string, need int64) error {
97 a.mu.Lock()
98 defer a.mu.Unlock()
99 need = max(need, DiskFloor)
100 if !a.fitsLocked(id, need) {
101 return errNoRoom
102 }
103 a.held[id] = need
104 return nil
105}
106
107// finish ends a read's slot. Its reservation stays until reconcileLocked
108// sees no unit left.
109func (a *admission) finish(id string) {
110 a.mu.Lock()
111 defer a.mu.Unlock()
112 delete(a.running, id)
113}
114
115// abandon gives back a read this agent admitted and then did not run (it
116// lost the election, or stopped before it): no worker was started for it,
117// so its reservation goes at once rather than waiting for the units to drain.
118func (a *admission) abandon(id string) {
119 a.mu.Lock()
120 defer a.mu.Unlock()
121 delete(a.running, id)
122 delete(a.held, id)
123}
124
125// retry is a read starting a fresh plan after a planned file vanished (spec
126// §8.2). Its previous worker may still hold scratch until its unit stops, so
127// that reservation moves to a retired key, held until no unit is left (J5),
128// and the read starts again from the floor (plan review finding 10).
129func (a *admission) retry(id string, attempt int) {
130 a.mu.Lock()
131 defer a.mu.Unlock()
132 if v, ok := a.held[id]; ok {
133 a.held[fmt.Sprintf("%s#%d", id, attempt)] = v
134 }
135 a.held[id] = DiskFloor
136}
137
138// settle waits until no carlos-dbread@ unit is running beyond this agent's
139// open connections, so a retry opens its next worker only once the last one
140// is gone, and never into the probe's connection. It releases retired
141// reservations once no unit is left at all.
142func (a *admission) settle(ctx context.Context, poll time.Duration) error {
143 for {
144 a.mu.Lock()
145 err := a.reconcileLocked(ctx)
146 a.mu.Unlock()
147 if !errors.Is(err, errOrphans) {
148 return err
149 }
150 select {
151 case <-ctx.Done():
152 return ctx.Err()
153 case <-time.After(poll):
154 }
155 }
156}
157
158// admitProbe admits the readiness probe: no orphan and no other probe. The
159// probe's connection is the third (opened checks the count).
160func (a *admission) admitProbe(ctx context.Context) bool {
161 a.mu.Lock()
162 defer a.mu.Unlock()
163 if a.probing || a.reconcileLocked(ctx) != nil {
164 return false
165 }
166 a.probing = true
167 return true
168}
169
170func (a *admission) probeDone() {
171 a.mu.Lock()
172 defer a.mu.Unlock()
173 a.probing = false
174}
175
176// opened takes a connection slot; the agent never opens a fourth.
177func (a *admission) opened() bool {
178 a.mu.Lock()
179 defer a.mu.Unlock()
180 if a.conns >= MaxConnections {
181 return false
182 }
183 a.conns++
184 return true
185}
186
187func (a *admission) closed() {
188 a.mu.Lock()
189 defer a.mu.Unlock()
190 a.conns--
191}
192