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 "math/rand/v2"
7 "sync"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// The identity check (spec §8.2): the record as the envelope named it, or a
16// code. T9's stages are Task 11's; this is the check itself.
17func TestCheckIsTheRecordAsTheEnvelopeNamedIt(t *testing.T) {
18 t.Parallel()
19 no := false
20 for name, c := range map[string]struct {
21 change func(fx *fixture)
22 want string
23 }{
24 "unchanged": {func(*fixture) {}, ""},
25 "moved to another region": {func(fx *fixture) { r := fx.record(); r.Region = "ie"; fx.putRecord(r) }, CodeRecordChanged},
26 "moving": {func(fx *fixture) {
27 r := fx.record()
28 r.Move = &provision.MoveState{ID: "mv_x", To: "ie", State: "requested"}
29 fx.putRecord(r)
30 }, CodeMoving},
31 "recreated in the account": {func(fx *fixture) { r := fx.record(); r.CreatedAt = r.CreatedAt.Add(time.Hour); fx.putRecord(r) }, CodeRecordChanged},
32 "recreated in another account": {func(fx *fixture) {
33 created := fx.record().CreatedAt
34 fx.store.Delete(context.Background(), provision.RecordKey(fxAccount, fxApp, fxHost))
35 fx.putRecordUnder("bac", provision.Record{Region: "uk", CreatedAt: created})
36 }, CodeRecordChanged},
37 "no longer sleeps": {func(fx *fixture) { r := fx.record(); r.Hibernate = &no; fx.putRecord(r) }, CodeRecordChanged},
38 "pooled": {func(fx *fixture) { r := fx.record(); r.Pool = "shared"; fx.putRecord(r) }, CodeRecordChanged},
39 "placed on a fleet": {func(fx *fixture) { r := fx.record(); r.Region = "fleet/acme"; fx.putRecord(r) }, CodeRecordChanged},
40 } {
41 t.Run(name, func(t *testing.T) {
42 t.Parallel()
43 fx := newFixture(t, 0)
44 req := fx.request("SELECT 1", "")
45 c.change(fx)
46 a, _ := fx.agent("uk-1")
47 _, code, err := a.check(context.Background(), req)
48 if err != nil || code != c.want {
49 t.Fatalf("check = %q, %v; want %q", code, err, c.want)
50 }
51 })
52 }
53}
54
55// T5: two capable boxes in one region; exactly one owns each read, across
56// 1,000 reads with random jitter.
57func TestElectionOneOwnerAcrossTwoAgents(t *testing.T) {
58 t.Parallel()
59 fx := newFixture(t, 0)
60 a, _ := fx.agent("uk-1")
61 b, _ := fx.agent("uk-2")
62 var mu sync.Mutex
63 owners := map[string]int{}
64 var wg sync.WaitGroup
65 for i := 0; i < 1000; i++ {
66 id, err := MintID()
67 if err != nil {
68 t.Fatal(err)
69 }
70 for _, ag := range []*Agent{a, b} {
71 wg.Add(1)
72 go func(ag *Agent) {
73 defer wg.Done()
74 time.Sleep(rand.N(2 * time.Millisecond))
75 if _, ok := ag.elect(context.Background(), id); ok {
76 mu.Lock()
77 owners[id]++
78 mu.Unlock()
79 }
80 }(ag)
81 }
82 }
83 wg.Wait()
84 if len(owners) != 1000 {
85 t.Fatalf("%d reads have an owner, want 1000", len(owners))
86 }
87 for id, n := range owners {
88 if n != 1 {
89 t.Fatalf("%s has %d owners", id, n)
90 }
91 }
92}
93
94// lossyStore commits some Puts and then answers an error: a response lost
95// on the way back (T5, codex 16).
96type lossyStore struct {
97 release.Store
98 lose func(key string) bool
99 refuse func(key string) bool
100}
101
102func (s lossyStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
103 if s.refuse != nil && s.refuse(key) {
104 return "", errors.New("connection reset before the write")
105 }
106 etag, err := s.Store.Put(ctx, key, body, ifMatch)
107 if err == nil && s.lose != nil && s.lose(key) {
108 return "", errors.New("connection reset after the write")
109 }
110 return etag, err
111}
112
113// A box whose picked create commits but answers an error reads it back and
114// owns the read.
115func TestElectionReadsBackALostAnswer(t *testing.T) {
116 t.Parallel()
117 fx := newFixture(t, 0)
118 a, _ := fx.agent("uk-1", func(c *AgentConfig, _ *inProcWorkers) {
119 c.Store = lossyStore{Store: fx.store, lose: func(string) bool { return true }}
120 })
121 if _, ok := a.elect(context.Background(), "dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa"); !ok {
122 t.Fatal("a lost answer to a create that landed was not owned")
123 }
124}
125
126// A box whose create answers an error while another box owns the read steps
127// away; so does one whose read-back fails.
128func TestElectionStepsAwayFromAnotherOwner(t *testing.T) {
129 t.Parallel()
130 fx := newFixture(t, 0)
131 a, _ := fx.agent("uk-1")
132 id := "dbr_aaaaaaaaaaaaaaaaaaaaaaaaaa"
133 if _, ok := a.elect(context.Background(), id); !ok {
134 t.Fatal("the first create did not own")
135 }
136 b, _ := fx.agent("uk-2", func(c *AgentConfig, _ *inProcWorkers) {
137 c.Store = lossyStore{Store: fx.store, refuse: func(string) bool { return true }}
138 })
139 if _, ok := b.elect(context.Background(), id); ok {
140 t.Fatal("a box owned a read another box had picked")
141 }
142 c, _ := fx.agent("uk-3", func(cfg *AgentConfig, _ *inProcWorkers) {
143 cfg.Store = failingGets{lossyStore{Store: fx.store, refuse: func(string) bool { return true }}}
144 })
145 if _, ok := c.elect(context.Background(), "dbr_bbbbbbbbbbbbbbbbbbbbbbbbbb"); ok {
146 t.Fatal("a box owned a read it could not read back")
147 }
148}
149
150type failingGets struct{ release.Store }
151
152func (failingGets) Get(context.Context, string) ([]byte, string, error) {
153 return nil, "", errors.New("timeout")
154}
155