github.com/koko1123/flow-go-1@v0.29.6/engine/common/requester/engine_test.go (about) 1 package requester 2 3 import ( 4 "math/rand" 5 "testing" 6 "time" 7 8 "github.com/rs/zerolog" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/mock" 11 "github.com/stretchr/testify/require" 12 "github.com/vmihailenco/msgpack" 13 "go.uber.org/atomic" 14 15 module "github.com/koko1123/flow-go-1/module/mock" 16 17 "github.com/koko1123/flow-go-1/engine" 18 "github.com/koko1123/flow-go-1/model/flow" 19 "github.com/koko1123/flow-go-1/model/flow/filter" 20 "github.com/koko1123/flow-go-1/model/messages" 21 "github.com/koko1123/flow-go-1/module/metrics" 22 "github.com/koko1123/flow-go-1/network/mocknetwork" 23 protocol "github.com/koko1123/flow-go-1/state/protocol/mock" 24 "github.com/koko1123/flow-go-1/utils/unittest" 25 ) 26 27 func TestEntityByID(t *testing.T) { 28 29 request := Engine{ 30 unit: engine.NewUnit(), 31 items: make(map[flow.Identifier]*Item), 32 rng: rand.New(rand.NewSource(0)), 33 } 34 35 now := time.Now().UTC() 36 37 entityID := unittest.IdentifierFixture() 38 selector := filter.Any 39 request.EntityByID(entityID, selector) 40 41 assert.Len(t, request.items, 1) 42 item, contains := request.items[entityID] 43 if assert.True(t, contains) { 44 assert.Equal(t, item.EntityID, entityID) 45 assert.Equal(t, item.NumAttempts, uint(0)) 46 cutoff := item.LastRequested.Add(item.RetryAfter) 47 assert.True(t, cutoff.Before(now)) // make sure we push out immediately 48 } 49 } 50 51 func TestDispatchRequestVarious(t *testing.T) { 52 53 identities := unittest.IdentityListFixture(16) 54 targetID := identities[0].NodeID 55 56 final := &protocol.Snapshot{} 57 final.On("Identities", mock.Anything).Return( 58 func(selector flow.IdentityFilter) flow.IdentityList { 59 return identities.Filter(selector) 60 }, 61 nil, 62 ) 63 64 state := &protocol.State{} 65 state.On("Final").Return(final) 66 67 cfg := Config{ 68 BatchInterval: 200 * time.Millisecond, 69 BatchThreshold: 999, 70 RetryInitial: 100 * time.Millisecond, 71 RetryFunction: RetryLinear(10 * time.Millisecond), 72 RetryAttempts: 2, 73 RetryMaximum: 300 * time.Millisecond, 74 } 75 76 // item that has just been added, should be included 77 justAdded := &Item{ 78 EntityID: unittest.IdentifierFixture(), 79 NumAttempts: 0, 80 LastRequested: time.Time{}, 81 RetryAfter: cfg.RetryInitial, 82 ExtraSelector: filter.Any, 83 } 84 85 // item was tried long time ago, should be included 86 triedAnciently := &Item{ 87 EntityID: unittest.IdentifierFixture(), 88 NumAttempts: 1, 89 LastRequested: time.Now().UTC().Add(-cfg.RetryMaximum), 90 RetryAfter: cfg.RetryFunction(cfg.RetryInitial), 91 ExtraSelector: filter.Any, 92 } 93 94 // item that was just tried, should be excluded 95 triedRecently := &Item{ 96 EntityID: unittest.IdentifierFixture(), 97 NumAttempts: 1, 98 LastRequested: time.Now().UTC(), 99 RetryAfter: cfg.RetryFunction(cfg.RetryInitial), 100 } 101 102 // item was tried twice, should be excluded 103 triedTwice := &Item{ 104 EntityID: unittest.IdentifierFixture(), 105 NumAttempts: 2, 106 LastRequested: time.Time{}, 107 RetryAfter: cfg.RetryInitial, 108 ExtraSelector: filter.Any, 109 } 110 111 items := make(map[flow.Identifier]*Item) 112 items[justAdded.EntityID] = justAdded 113 items[triedAnciently.EntityID] = triedAnciently 114 items[triedRecently.EntityID] = triedRecently 115 items[triedTwice.EntityID] = triedTwice 116 117 var nonce uint64 118 119 con := &mocknetwork.Conduit{} 120 con.On("Unicast", mock.Anything, mock.Anything).Run( 121 func(args mock.Arguments) { 122 request := args.Get(0).(*messages.EntityRequest) 123 originID := args.Get(1).(flow.Identifier) 124 nonce = request.Nonce 125 assert.Equal(t, originID, targetID) 126 assert.ElementsMatch(t, request.EntityIDs, []flow.Identifier{justAdded.EntityID, triedAnciently.EntityID}) 127 }, 128 ).Return(nil) 129 130 request := Engine{ 131 unit: engine.NewUnit(), 132 metrics: metrics.NewNoopCollector(), 133 cfg: cfg, 134 state: state, 135 con: con, 136 items: items, 137 requests: make(map[uint64]*messages.EntityRequest), 138 selector: filter.HasNodeID(targetID), 139 rng: rand.New(rand.NewSource(0)), 140 } 141 dispatched, err := request.dispatchRequest() 142 require.NoError(t, err) 143 require.True(t, dispatched) 144 145 con.AssertExpectations(t) 146 147 request.unit.Lock() 148 assert.Contains(t, request.requests, nonce) 149 request.unit.Unlock() 150 151 // TODO: racy/slow test 152 time.Sleep(2 * cfg.RetryInitial) 153 154 request.unit.Lock() 155 assert.NotContains(t, request.requests, nonce) 156 request.unit.Unlock() 157 } 158 159 func TestDispatchRequestBatchSize(t *testing.T) { 160 161 batchLimit := uint(16) 162 totalItems := uint(99) 163 164 identities := unittest.IdentityListFixture(16) 165 166 final := &protocol.Snapshot{} 167 final.On("Identities", mock.Anything).Return( 168 func(selector flow.IdentityFilter) flow.IdentityList { 169 return identities.Filter(selector) 170 }, 171 nil, 172 ) 173 174 state := &protocol.State{} 175 state.On("Final").Return(final) 176 177 cfg := Config{ 178 BatchInterval: 24 * time.Hour, 179 BatchThreshold: batchLimit, 180 RetryInitial: 24 * time.Hour, 181 RetryFunction: RetryLinear(1), 182 RetryAttempts: 1, 183 RetryMaximum: 24 * time.Hour, 184 } 185 186 // item that has just been added, should be included 187 items := make(map[flow.Identifier]*Item) 188 for i := uint(0); i < totalItems; i++ { 189 item := &Item{ 190 EntityID: unittest.IdentifierFixture(), 191 NumAttempts: 0, 192 LastRequested: time.Time{}, 193 RetryAfter: cfg.RetryInitial, 194 ExtraSelector: filter.Any, 195 } 196 items[item.EntityID] = item 197 } 198 199 con := &mocknetwork.Conduit{} 200 con.On("Unicast", mock.Anything, mock.Anything).Run( 201 func(args mock.Arguments) { 202 request := args.Get(0).(*messages.EntityRequest) 203 assert.Len(t, request.EntityIDs, int(batchLimit)) 204 }, 205 ).Return(nil) 206 207 request := Engine{ 208 unit: engine.NewUnit(), 209 metrics: metrics.NewNoopCollector(), 210 cfg: cfg, 211 state: state, 212 con: con, 213 items: items, 214 requests: make(map[uint64]*messages.EntityRequest), 215 selector: filter.Any, 216 rng: rand.New(rand.NewSource(0)), 217 } 218 dispatched, err := request.dispatchRequest() 219 require.NoError(t, err) 220 require.True(t, dispatched) 221 222 con.AssertExpectations(t) 223 } 224 225 func TestOnEntityResponseValid(t *testing.T) { 226 227 identities := unittest.IdentityListFixture(16) 228 targetID := identities[0].NodeID 229 230 final := &protocol.Snapshot{} 231 final.On("Identities", mock.Anything).Return( 232 func(selector flow.IdentityFilter) flow.IdentityList { 233 return identities.Filter(selector) 234 }, 235 nil, 236 ) 237 238 state := &protocol.State{} 239 state.On("Final").Return(final) 240 241 nonce := rand.Uint64() 242 243 wanted1 := unittest.CollectionFixture(1) 244 wanted2 := unittest.CollectionFixture(2) 245 unavailable := unittest.CollectionFixture(3) 246 unwanted := unittest.CollectionFixture(4) 247 248 now := time.Now() 249 250 iwanted1 := &Item{ 251 EntityID: wanted1.ID(), 252 LastRequested: now, 253 ExtraSelector: filter.Any, 254 } 255 iwanted2 := &Item{ 256 EntityID: wanted2.ID(), 257 LastRequested: now, 258 ExtraSelector: filter.Any, 259 } 260 iunavailable := &Item{ 261 EntityID: unavailable.ID(), 262 LastRequested: now, 263 ExtraSelector: filter.Any, 264 } 265 266 bwanted1, _ := msgpack.Marshal(wanted1) 267 bwanted2, _ := msgpack.Marshal(wanted2) 268 bunwanted, _ := msgpack.Marshal(unwanted) 269 270 res := &messages.EntityResponse{ 271 Nonce: nonce, 272 EntityIDs: []flow.Identifier{wanted1.ID(), wanted2.ID(), unwanted.ID()}, 273 Blobs: [][]byte{bwanted1, bwanted2, bunwanted}, 274 } 275 276 req := &messages.EntityRequest{ 277 Nonce: nonce, 278 EntityIDs: []flow.Identifier{wanted1.ID(), wanted2.ID(), unavailable.ID()}, 279 } 280 281 done := make(chan struct{}) 282 called := *atomic.NewUint64(0) 283 request := Engine{ 284 unit: engine.NewUnit(), 285 metrics: metrics.NewNoopCollector(), 286 state: state, 287 items: make(map[flow.Identifier]*Item), 288 requests: make(map[uint64]*messages.EntityRequest), 289 selector: filter.HasNodeID(targetID), 290 create: func() flow.Entity { return &flow.Collection{} }, 291 handle: func(flow.Identifier, flow.Entity) { 292 if called.Inc() >= 2 { 293 close(done) 294 } 295 }, 296 rng: rand.New(rand.NewSource(0)), 297 } 298 299 request.items[iwanted1.EntityID] = iwanted1 300 request.items[iwanted2.EntityID] = iwanted2 301 request.items[iunavailable.EntityID] = iunavailable 302 303 request.requests[req.Nonce] = req 304 305 err := request.onEntityResponse(targetID, res) 306 assert.NoError(t, err) 307 308 // check that the request was removed 309 assert.NotContains(t, request.requests, nonce) 310 311 // check that the provided items were removed 312 assert.NotContains(t, request.items, wanted1.ID()) 313 assert.NotContains(t, request.items, wanted2.ID()) 314 315 // check that the missing item is still there 316 assert.Contains(t, request.items, unavailable.ID()) 317 318 // make sure we processed two items 319 unittest.AssertClosesBefore(t, done, time.Second) 320 321 // check that the missing items timestamp was reset 322 assert.Equal(t, iunavailable.LastRequested, time.Time{}) 323 } 324 325 func TestOnEntityIntegrityCheck(t *testing.T) { 326 identities := unittest.IdentityListFixture(16) 327 targetID := identities[0].NodeID 328 329 final := &protocol.Snapshot{} 330 final.On("Identities", mock.Anything).Return( 331 func(selector flow.IdentityFilter) flow.IdentityList { 332 return identities.Filter(selector) 333 }, 334 nil, 335 ) 336 337 state := &protocol.State{} 338 state.On("Final").Return(final) 339 340 nonce := rand.Uint64() 341 342 wanted := unittest.CollectionFixture(1) 343 wanted2 := unittest.CollectionFixture(2) 344 345 now := time.Now() 346 347 iwanted := &Item{ 348 EntityID: wanted.ID(), 349 LastRequested: now, 350 ExtraSelector: filter.Any, 351 checkIntegrity: true, 352 } 353 354 assert.NotEqual(t, wanted, wanted2) 355 356 // prepare payload from different entity 357 bwanted, _ := msgpack.Marshal(wanted2) 358 359 res := &messages.EntityResponse{ 360 Nonce: nonce, 361 EntityIDs: []flow.Identifier{wanted.ID()}, 362 Blobs: [][]byte{bwanted}, 363 } 364 365 req := &messages.EntityRequest{ 366 Nonce: nonce, 367 EntityIDs: []flow.Identifier{wanted.ID()}, 368 } 369 370 called := make(chan struct{}) 371 request := Engine{ 372 unit: engine.NewUnit(), 373 metrics: metrics.NewNoopCollector(), 374 state: state, 375 items: make(map[flow.Identifier]*Item), 376 requests: make(map[uint64]*messages.EntityRequest), 377 selector: filter.HasNodeID(targetID), 378 create: func() flow.Entity { return &flow.Collection{} }, 379 handle: func(flow.Identifier, flow.Entity) { close(called) }, 380 rng: rand.New(rand.NewSource(0)), 381 } 382 383 request.items[iwanted.EntityID] = iwanted 384 385 request.requests[req.Nonce] = req 386 387 err := request.onEntityResponse(targetID, res) 388 assert.NoError(t, err) 389 390 // check that the request was removed 391 assert.NotContains(t, request.requests, nonce) 392 393 // check that the provided item wasn't removed 394 assert.Contains(t, request.items, wanted.ID()) 395 396 iwanted.checkIntegrity = false 397 request.items[iwanted.EntityID] = iwanted 398 request.requests[req.Nonce] = req 399 400 err = request.onEntityResponse(targetID, res) 401 assert.NoError(t, err) 402 403 // make sure we process item without checking integrity 404 unittest.AssertClosesBefore(t, called, time.Second) 405 } 406 407 // Verify that the origin should not be checked when ValidateStaking config is set to false 408 func TestOriginValidation(t *testing.T) { 409 identities := unittest.IdentityListFixture(16) 410 targetID := identities[0].NodeID 411 wrongID := identities[1].NodeID 412 meID := identities[3].NodeID 413 414 final := &protocol.Snapshot{} 415 final.On("Identities", mock.Anything).Return( 416 func(selector flow.IdentityFilter) flow.IdentityList { 417 return identities.Filter(selector) 418 }, 419 nil, 420 ) 421 422 state := &protocol.State{} 423 state.On("Final").Return(final) 424 425 me := &module.Local{} 426 427 me.On("NodeID").Return(meID) 428 429 nonce := rand.Uint64() 430 431 wanted := unittest.CollectionFixture(1) 432 433 now := time.Now() 434 435 iwanted := &Item{ 436 EntityID: wanted.ID(), 437 LastRequested: now, 438 ExtraSelector: filter.HasNodeID(targetID), 439 checkIntegrity: true, 440 } 441 442 // prepare payload 443 bwanted, _ := msgpack.Marshal(wanted) 444 445 res := &messages.EntityResponse{ 446 Nonce: nonce, 447 EntityIDs: []flow.Identifier{wanted.ID()}, 448 Blobs: [][]byte{bwanted}, 449 } 450 451 req := &messages.EntityRequest{ 452 Nonce: nonce, 453 EntityIDs: []flow.Identifier{wanted.ID()}, 454 } 455 456 network := &mocknetwork.Network{} 457 network.On("Register", mock.Anything, mock.Anything).Return(nil, nil) 458 459 e, err := New( 460 zerolog.Nop(), 461 metrics.NewNoopCollector(), 462 network, 463 me, 464 state, 465 "", 466 filter.HasNodeID(targetID), 467 func() flow.Entity { return &flow.Collection{} }, 468 ) 469 assert.NoError(t, err) 470 471 called := make(chan struct{}) 472 473 e.WithHandle(func(origin flow.Identifier, _ flow.Entity) { 474 // we expect wrong origin to propagate here with validation disabled 475 assert.Equal(t, wrongID, origin) 476 close(called) 477 }) 478 479 e.items[iwanted.EntityID] = iwanted 480 e.requests[req.Nonce] = req 481 482 err = e.onEntityResponse(wrongID, res) 483 assert.Error(t, err) 484 assert.IsType(t, engine.InvalidInputError{}, err) 485 486 e.cfg.ValidateStaking = false 487 488 err = e.onEntityResponse(wrongID, res) 489 assert.NoError(t, err) 490 491 // handler are called async, but this should be extremely quick 492 unittest.AssertClosesBefore(t, called, time.Second) 493 }