github.com/celestiaorg/celestia-node@v0.15.0-beta.1/share/eds/cache/accessor_cache_test.go (about) 1 package cache 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "io" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/filecoin-project/dagstore" 13 "github.com/filecoin-project/dagstore/shard" 14 blocks "github.com/ipfs/go-block-format" 15 "github.com/ipfs/go-cid" 16 "github.com/stretchr/testify/require" 17 ) 18 19 func TestAccessorCache(t *testing.T) { 20 t.Run("add / get item from cache", func(t *testing.T) { 21 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 22 defer cancel() 23 cache, err := NewAccessorCache("test", 1) 24 require.NoError(t, err) 25 26 // add accessor to the cache 27 key := shard.KeyFromString("key") 28 mock := &mockAccessor{ 29 data: []byte("test_data"), 30 } 31 loaded, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { 32 return mock, nil 33 }) 34 require.NoError(t, err) 35 36 // check if item exists 37 got, err := cache.Get(key) 38 require.NoError(t, err) 39 40 l, err := io.ReadAll(loaded.Reader()) 41 require.NoError(t, err) 42 require.Equal(t, mock.data, l) 43 g, err := io.ReadAll(got.Reader()) 44 require.NoError(t, err) 45 require.Equal(t, mock.data, g) 46 }) 47 48 t.Run("get blockstore from accessor", func(t *testing.T) { 49 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 50 defer cancel() 51 cache, err := NewAccessorCache("test", 1) 52 require.NoError(t, err) 53 54 // add accessor to the cache 55 key := shard.KeyFromString("key") 56 mock := &mockAccessor{} 57 accessor, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { 58 return mock, nil 59 }) 60 require.NoError(t, err) 61 62 // check if item exists 63 _, err = cache.Get(key) 64 require.NoError(t, err) 65 66 // blockstore should be created only after first request 67 require.Equal(t, 0, mock.returnedBs) 68 69 // try to get blockstore 70 _, err = accessor.Blockstore() 71 require.NoError(t, err) 72 73 // second call to blockstore should return same blockstore 74 _, err = accessor.Blockstore() 75 require.NoError(t, err) 76 require.Equal(t, 1, mock.returnedBs) 77 }) 78 79 t.Run("remove an item", func(t *testing.T) { 80 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 81 defer cancel() 82 cache, err := NewAccessorCache("test", 1) 83 require.NoError(t, err) 84 85 // add accessor to the cache 86 key := shard.KeyFromString("key") 87 mock := &mockAccessor{} 88 ac, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { 89 return mock, nil 90 }) 91 require.NoError(t, err) 92 err = ac.Close() 93 require.NoError(t, err) 94 95 err = cache.Remove(key) 96 require.NoError(t, err) 97 98 // accessor should be closed on removal 99 mock.checkClosed(t, true) 100 101 // check if item exists 102 _, err = cache.Get(key) 103 require.ErrorIs(t, err, errCacheMiss) 104 }) 105 106 t.Run("successive reads should read the same data", func(t *testing.T) { 107 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 108 defer cancel() 109 cache, err := NewAccessorCache("test", 1) 110 require.NoError(t, err) 111 112 // add accessor to the cache 113 key := shard.KeyFromString("key") 114 mock := &mockAccessor{data: []byte("test")} 115 accessor, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { 116 return mock, nil 117 }) 118 require.NoError(t, err) 119 120 loaded, err := io.ReadAll(accessor.Reader()) 121 require.NoError(t, err) 122 require.Equal(t, mock.data, loaded) 123 124 for i := 0; i < 2; i++ { 125 accessor, err = cache.Get(key) 126 require.NoError(t, err) 127 got, err := io.ReadAll(accessor.Reader()) 128 require.NoError(t, err) 129 require.Equal(t, mock.data, got) 130 } 131 }) 132 133 t.Run("removed by eviction", func(t *testing.T) { 134 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 135 defer cancel() 136 cache, err := NewAccessorCache("test", 1) 137 require.NoError(t, err) 138 139 // add accessor to the cache 140 key := shard.KeyFromString("key") 141 mock := &mockAccessor{} 142 ac1, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { 143 return mock, nil 144 }) 145 require.NoError(t, err) 146 err = ac1.Close() 147 require.NoError(t, err) 148 149 // add second item 150 key2 := shard.KeyFromString("key2") 151 ac2, err := cache.GetOrLoad(ctx, key2, func(ctx context.Context, key shard.Key) (Accessor, error) { 152 return mock, nil 153 }) 154 require.NoError(t, err) 155 err = ac2.Close() 156 require.NoError(t, err) 157 158 // accessor should be closed on removal by eviction 159 mock.checkClosed(t, true) 160 161 // check if item evicted 162 _, err = cache.Get(key) 163 require.ErrorIs(t, err, errCacheMiss) 164 }) 165 166 t.Run("close on accessor is not closing underlying accessor", func(t *testing.T) { 167 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 168 defer cancel() 169 cache, err := NewAccessorCache("test", 1) 170 require.NoError(t, err) 171 172 // add accessor to the cache 173 key := shard.KeyFromString("key") 174 mock := &mockAccessor{} 175 _, err = cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { 176 return mock, nil 177 }) 178 require.NoError(t, err) 179 180 // check if item exists 181 accessor, err := cache.Get(key) 182 require.NoError(t, err) 183 require.NotNil(t, accessor) 184 185 // close on returned accessor should not close inner accessor 186 err = accessor.Close() 187 require.NoError(t, err) 188 189 // check that close was not performed on inner accessor 190 mock.checkClosed(t, false) 191 }) 192 193 t.Run("close on accessor should wait all readers to finish", func(t *testing.T) { 194 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 195 defer cancel() 196 cache, err := NewAccessorCache("test", 1) 197 require.NoError(t, err) 198 199 // add accessor to the cache 200 key := shard.KeyFromString("key") 201 mock := &mockAccessor{} 202 accessor1, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { 203 return mock, nil 204 }) 205 require.NoError(t, err) 206 207 // create second readers 208 accessor2, err := cache.Get(key) 209 require.NoError(t, err) 210 211 // initialize close 212 done := make(chan struct{}) 213 go func() { 214 err := cache.Remove(key) 215 require.NoError(t, err) 216 close(done) 217 }() 218 219 // close on first reader and check that it is not enough to release the inner accessor 220 err = accessor1.Close() 221 require.NoError(t, err) 222 mock.checkClosed(t, false) 223 224 // second close from same reader should not release accessor either 225 err = accessor1.Close() 226 require.NoError(t, err) 227 mock.checkClosed(t, false) 228 229 // reads for item that is being evicted should result in errCacheMiss 230 _, err = cache.Get(key) 231 require.ErrorIs(t, err, errCacheMiss) 232 233 // close second reader and wait for accessor to be closed 234 err = accessor2.Close() 235 require.NoError(t, err) 236 // wait until close is performed on accessor 237 select { 238 case <-done: 239 case <-ctx.Done(): 240 t.Fatal("timeout reached") 241 } 242 243 // item will be removed 244 mock.checkClosed(t, true) 245 }) 246 247 t.Run("slow reader should not block eviction", func(t *testing.T) { 248 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 249 defer cancel() 250 cache, err := NewAccessorCache("test", 1) 251 require.NoError(t, err) 252 253 // add accessor to the cache 254 key1 := shard.KeyFromString("key1") 255 mock1 := &mockAccessor{} 256 accessor1, err := cache.GetOrLoad(ctx, key1, func(ctx context.Context, key shard.Key) (Accessor, error) { 257 return mock1, nil 258 }) 259 require.NoError(t, err) 260 261 // add second accessor, to trigger eviction of the first one 262 key2 := shard.KeyFromString("key2") 263 mock2 := &mockAccessor{} 264 accessor2, err := cache.GetOrLoad(ctx, key2, func(ctx context.Context, key shard.Key) (Accessor, error) { 265 return mock2, nil 266 }) 267 require.NoError(t, err) 268 269 // first accessor should be evicted from cache 270 _, err = cache.Get(key1) 271 require.ErrorIs(t, err, errCacheMiss) 272 273 // first accessor should not be closed before all refs are released by Close() is calls. 274 mock1.checkClosed(t, false) 275 276 // after Close() is called on first accessor, it is free to get closed 277 err = accessor1.Close() 278 require.NoError(t, err) 279 mock1.checkClosed(t, true) 280 281 // after Close called on second accessor, it should stay in cache (not closed) 282 err = accessor2.Close() 283 require.NoError(t, err) 284 mock2.checkClosed(t, false) 285 }) 286 } 287 288 type mockAccessor struct { 289 m sync.Mutex 290 data []byte 291 isClosed bool 292 returnedBs int 293 } 294 295 func (m *mockAccessor) Reader() io.Reader { 296 m.m.Lock() 297 defer m.m.Unlock() 298 return bytes.NewBuffer(m.data) 299 } 300 301 func (m *mockAccessor) Blockstore() (dagstore.ReadBlockstore, error) { 302 m.m.Lock() 303 defer m.m.Unlock() 304 if m.returnedBs > 0 { 305 return nil, errors.New("blockstore already returned") 306 } 307 m.returnedBs++ 308 return rbsMock{}, nil 309 } 310 311 func (m *mockAccessor) Close() error { 312 m.m.Lock() 313 defer m.m.Unlock() 314 if m.isClosed { 315 return errors.New("already closed") 316 } 317 m.isClosed = true 318 return nil 319 } 320 321 func (m *mockAccessor) checkClosed(t *testing.T, expected bool) { 322 // item will be removed in background, so give it some time to settle 323 time.Sleep(time.Millisecond * 100) 324 m.m.Lock() 325 defer m.m.Unlock() 326 require.Equal(t, expected, m.isClosed) 327 } 328 329 // rbsMock is a dagstore.ReadBlockstore mock 330 type rbsMock struct{} 331 332 func (r rbsMock) Has(context.Context, cid.Cid) (bool, error) { 333 panic("implement me") 334 } 335 336 func (r rbsMock) Get(_ context.Context, _ cid.Cid) (blocks.Block, error) { 337 panic("implement me") 338 } 339 340 func (r rbsMock) GetSize(context.Context, cid.Cid) (int, error) { 341 panic("implement me") 342 } 343 344 func (r rbsMock) AllKeysChan(context.Context) (<-chan cid.Cid, error) { 345 panic("implement me") 346 } 347 348 func (r rbsMock) HashOnRead(bool) { 349 panic("implement me") 350 }