carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostactors
2
3import (
4 "context"
5 "errors"
6 "sync"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/release"
11)
12
13// dsClock is a clock the store underneath and the test both move.
14type dsClock struct {
15 mu sync.Mutex
16 t time.Time
17}
18
19func (c *dsClock) now() time.Time {
20 c.mu.Lock()
21 defer c.mu.Unlock()
22 return c.t
23}
24
25func (c *dsClock) add(d time.Duration) {
26 c.mu.Lock()
27 defer c.mu.Unlock()
28 c.t = c.t.Add(d)
29}
30
31// dsStore is a store underneath the bound: put runs in place of each Put,
32// and every call it receives is counted.
33type dsStore struct {
34 release.Store
35 mu sync.Mutex
36 calls int
37 put func(ctx context.Context) (string, error)
38}
39
40func (s *dsStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
41 s.mu.Lock()
42 s.calls++
43 s.mu.Unlock()
44 return s.put(ctx)
45}
46
47func (s *dsStore) count() int {
48 s.mu.Lock()
49 defer s.mu.Unlock()
50 return s.calls
51}
52
53// No call is issued at or after the deadline, by the read's clock, and the
54// store underneath never sees it.
55func TestDeadlineStoreIssuesNothingAtTheDeadline(t *testing.T) {
56 clock := &dsClock{t: time.Now()}
57 inner := &dsStore{put: func(context.Context) (string, error) { return "e1", nil }}
58 bd, bctx, cancel := writeBound(context.Background(), Directory{Store: inner}, membershipConfirmation{observedAt: clock.now()}, clock.now)
59 defer cancel()
60 clock.add(membershipFreshness)
61 if _, err := bd.Store.Put(bctx, Key, []byte("x"), "e0"); !errors.Is(err, ErrWriteDeadline) || inner.count() != 0 {
62 t.Fatalf("a Put at the deadline: %v, %d calls reached the store", err, inner.count())
63 }
64}
65
66// A store that ignores its context is cut off by the wrapper at
67// storeCallTimeout, and the context it was given is cancelled: the outcome
68// is unknown.
69func TestDeadlineStoreCutsOffAStoreThatNeverAnswers(t *testing.T) {
70 old := storeCallTimeout
71 storeCallTimeout = 50 * time.Millisecond
72 defer func() { storeCallTimeout = old }()
73 unblock := make(chan struct{})
74 defer close(unblock)
75 given := make(chan context.Context, 1)
76 inner := &dsStore{put: func(ctx context.Context) (string, error) {
77 given <- ctx
78 <-unblock
79 return "e1", nil
80 }}
81 now := time.Now
82 bd, bctx, cancel := writeBound(context.Background(), Directory{Store: inner}, membershipConfirmation{observedAt: now()}, now)
83 defer cancel()
84 start := time.Now()
85 _, err := bd.Store.Put(bctx, Key, []byte("x"), "e0")
86 took := time.Since(start)
87 if !errors.Is(err, ErrWriteDeadline) || took > 5*time.Second {
88 t.Fatalf("a store that never answers: %v after %s", err, took)
89 }
90 ctx := <-given
91 if dl, ok := ctx.Deadline(); !ok || dl.Sub(start) > storeCallTimeout+time.Second {
92 t.Fatalf("the call's context had deadline %v (%v), want within %s of the call", dl, ok, storeCallTimeout)
93 }
94 if ctx.Err() == nil {
95 t.Fatal("the call's context was not cancelled when the wrapper gave up")
96 }
97}
98
99// A success that answers only after the deadline is unknown, never a
100// success.
101func TestDeadlineStoreCountsALateSuccessAsUnknown(t *testing.T) {
102 clock := &dsClock{t: time.Now()}
103 inner := &dsStore{}
104 inner.put = func(context.Context) (string, error) {
105 clock.add(membershipFreshness + time.Second)
106 return "e1", nil
107 }
108 bd, bctx, cancel := writeBound(context.Background(), Directory{Store: inner}, membershipConfirmation{observedAt: clock.now()}, clock.now)
109 defer cancel()
110 if etag, err := bd.Store.Put(bctx, Key, []byte("x"), "e0"); !errors.Is(err, ErrWriteDeadline) || etag != "" || inner.count() != 1 {
111 t.Fatalf("a late success: %q %v, %d calls", etag, err, inner.count())
112 }
113}
114
115// An unknown read, or a write the bound could not confirm, is never one of
116// the answers that count against the attempt, and leaves the attempt as it
117// was: same ids, not consumed, the roster unwritten.
118func TestAnUnknownOutcomeKeepsTheAttempt(t *testing.T) {
119 check := func(t *testing.T, w *memberWorld, err error, want error) {
120 t.Helper()
121 if !errors.Is(err, want) || errors.Is(err, ErrNotMember) || errors.Is(err, ErrMembershipStale) {
122 t.Fatalf("got %v, want only %v", err, want)
123 }
124 o, rerr := w.d.Read(context.Background())
125 if rerr != nil {
126 t.Fatal(rerr)
127 }
128 a := o.Roster.EnrollmentAttempts[newRef]
129 if o.Roster.Revision != w.o.Roster.Revision || a.Consumed || a.AttemptID != w.in.AttemptID || a.ConfirmationID != w.in.ConfirmationID {
130 t.Fatalf("the attempt moved: revision %d (was %d), %+v", o.Roster.Revision, w.o.Roster.Revision, a)
131 }
132 }
133 t.Run("the read fails", func(t *testing.T) {
134 w := newMemberWorld(t, member("routers-ie"))
135 w.cloud.err = errors.New("RequestTimeout")
136 _, ev, err := w.enroll()
137 check(t, w, err, ErrMembershipUnknown)
138 if len(ev.RequestIDs) != 0 {
139 t.Fatalf("a failed read carried request ids %v", ev.RequestIDs)
140 }
141 })
142 t.Run("the write is cut off", func(t *testing.T) {
143 w := newMemberWorld(t, member("routers-ie"))
144 clock := &dsClock{t: w.clock}
145 inner := w.d.Store
146 w.d.Store = &dsStore{Store: inner, put: func(context.Context) (string, error) {
147 // The write answers success, but only at the deadline.
148 clock.add(membershipFreshness)
149 return "e-late", nil
150 }}
151 _, _, err := w.d.EnrollAutomatic(context.Background(), w.sg, w.o, w.v, w.in, w.raw, "i-new", w.cloud, clock.now)
152 w.d.Store = inner
153 check(t, w, err, ErrWriteDeadline)
154 })
155}
156