Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

1package knotacl 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "slices" 8 "sync" 9 "testing" 10 "time" 11) 12 13var cacheTestBase = time.Unix(1700000000, 0) 14 15type fakeLister struct { 16 mu sync.Mutex 17 memberCalls int 18 members []string 19 err error 20 started chan struct{} 21 block chan struct{} 22} 23 24func (f *fakeLister) GetKnotMembers(ctx context.Context, host string) ([]string, error) { 25 f.mu.Lock() 26 f.memberCalls++ 27 members, err, started, block := f.members, f.err, f.started, f.block 28 f.mu.Unlock() 29 if started != nil { 30 close(started) 31 } 32 if block != nil { 33 <-block 34 } 35 if err != nil { 36 return nil, err 37 } 38 return members, nil 39} 40 41func (f *fakeLister) GetRepoCollaborators(ctx context.Context, host, repoDid string) ([]string, error) { 42 f.mu.Lock() 43 defer f.mu.Unlock() 44 return f.members, f.err 45} 46 47func (f *fakeLister) calls() int { 48 f.mu.Lock() 49 defer f.mu.Unlock() 50 return f.memberCalls 51} 52 53func (f *fakeLister) set(members []string, err error) { 54 f.mu.Lock() 55 defer f.mu.Unlock() 56 f.members, f.err = members, err 57} 58 59type fakeClock struct { 60 mu sync.Mutex 61 t time.Time 62} 63 64func (c *fakeClock) now() time.Time { 65 c.mu.Lock() 66 defer c.mu.Unlock() 67 return c.t 68} 69 70func (c *fakeClock) advance(d time.Duration) { 71 c.mu.Lock() 72 defer c.mu.Unlock() 73 c.t = c.t.Add(d) 74} 75 76func TestCache_TTLCollapsesThenExpires(t *testing.T) { 77 clk := &fakeClock{t: cacheTestBase} 78 f := &fakeLister{members: []string{"did:plc:boltless"}} 79 c := newCache(f, cacheTTL, clk.now) 80 ctx := context.Background() 81 82 if _, err := c.GetKnotMembers(ctx, "knot.nel.pet"); err != nil { 83 t.Fatal(err) 84 } 85 if _, err := c.GetKnotMembers(ctx, "knot.nel.pet"); err != nil { 86 t.Fatal(err) 87 } 88 if f.calls() != 1 { 89 t.Errorf("memberCalls=%d, want 1 within the TTL window", f.calls()) 90 } 91 92 clk.advance(cacheTTL) 93 if _, err := c.GetKnotMembers(ctx, "knot.nel.pet"); err != nil { 94 t.Fatal(err) 95 } 96 if f.calls() != 2 { 97 t.Errorf("memberCalls=%d, want 2 once the entry expired", f.calls()) 98 } 99} 100 101func TestCache_ErrorsNotCached(t *testing.T) { 102 clk := &fakeClock{t: cacheTestBase} 103 f := &fakeLister{err: errors.New("knot unreachable")} 104 c := newCache(f, cacheTTL, clk.now) 105 ctx := context.Background() 106 107 if _, err := c.GetKnotMembers(ctx, "knot.nel.pet"); err == nil { 108 t.Fatal("want error on the first call") 109 } 110 f.set([]string{"did:plc:boltless"}, nil) 111 got, err := c.GetKnotMembers(ctx, "knot.nel.pet") 112 if err != nil { 113 t.Fatal(err) 114 } 115 if !slices.Equal(got, []string{"did:plc:boltless"}) { 116 t.Errorf("got %v after recovery, want the live value", got) 117 } 118 if f.calls() != 2 { 119 t.Errorf("memberCalls=%d, want 2; a failed fetch must not be cached", f.calls()) 120 } 121} 122 123func TestCache_MemoShortCircuitsWithinRequest(t *testing.T) { 124 clk := &fakeClock{t: cacheTestBase} 125 f := &fakeLister{members: []string{"did:plc:boltless"}} 126 c := newCache(f, cacheTTL, clk.now) 127 ctx := WithMemo(context.Background()) 128 129 first, err := c.GetKnotMembers(ctx, "knot.nel.pet") 130 if err != nil { 131 t.Fatal(err) 132 } 133 134 clk.advance(2 * cacheTTL) 135 f.set([]string{"did:plc:akshay"}, nil) 136 137 second, err := c.GetKnotMembers(ctx, "knot.nel.pet") 138 if err != nil { 139 t.Fatal(err) 140 } 141 if !slices.Equal(first, second) { 142 t.Errorf("memo must hold one snapshot per request: first=%v second=%v", first, second) 143 } 144 if f.calls() != 1 { 145 t.Errorf("memberCalls=%d, want 1; the request memo must not re-query even past the TTL", f.calls()) 146 } 147} 148 149func TestCache_ReturnedSliceCannotCorruptCache(t *testing.T) { 150 clk := &fakeClock{t: cacheTestBase} 151 f := &fakeLister{members: []string{"did:plc:boltless", "did:plc:akshay"}} 152 c := newCache(f, cacheTTL, clk.now) 153 ctx := context.Background() 154 155 got, err := c.GetKnotMembers(ctx, "knot.nel.pet") 156 if err != nil { 157 t.Fatal(err) 158 } 159 for i := range got { 160 got[i] = "did:plc:squid" 161 } 162 163 again, err := c.GetKnotMembers(ctx, "knot.nel.pet") 164 if err != nil { 165 t.Fatal(err) 166 } 167 if slices.Contains(again, "did:plc:squid") { 168 t.Errorf("a caller mutating its returned slice corrupted the cached entry: %v", again) 169 } 170 if f.calls() != 1 { 171 t.Errorf("memberCalls=%d, want 1; the second read should be served from cache", f.calls()) 172 } 173} 174 175func TestCache_SingleflightCollapsesConcurrentMisses(t *testing.T) { 176 clk := &fakeClock{t: cacheTestBase} 177 started := make(chan struct{}) 178 release := make(chan struct{}) 179 f := &fakeLister{members: []string{"did:plc:boltless"}, started: started, block: release} 180 c := newCache(f, cacheTTL, clk.now) 181 ctx := context.Background() 182 183 var wg sync.WaitGroup 184 call := func() { 185 wg.Add(1) 186 go func() { 187 defer wg.Done() 188 if _, err := c.GetKnotMembers(ctx, "knot.nel.pet"); err != nil { 189 t.Errorf("GetKnotMembers: %v", err) 190 } 191 }() 192 } 193 194 call() 195 <-started 196 for range make([]struct{}, 8) { 197 call() 198 } 199 time.Sleep(20 * time.Millisecond) 200 close(release) 201 wg.Wait() 202 203 if f.calls() != 1 { 204 t.Errorf("memberCalls=%d, want 1; concurrent misses must collapse into a single knot query", f.calls()) 205 } 206} 207 208func TestCache_CapIsHardUnderFreshFlood(t *testing.T) { 209 clk := &fakeClock{t: cacheTestBase} 210 f := &fakeLister{members: []string{"did:plc:limpet"}} 211 c := newCache(f, cacheTTL, clk.now) 212 ctx := context.Background() 213 214 for i := 0; i < cacheMaxEntries+100; i++ { 215 if _, err := c.GetKnotMembers(ctx, fmt.Sprintf("knot-%d.nel.pet", i)); err != nil { 216 t.Fatal(err) 217 } 218 } 219 220 c.mu.Lock() 221 n := len(c.entries) 222 c.mu.Unlock() 223 if n > cacheMaxEntries { 224 t.Errorf("entries=%d, want <= %d; all-fresh keys must not grow past the cap", n, cacheMaxEntries) 225 } 226}