mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
* Add gRPC update debouncing mechanism Implements backpressure handling for peer network map updates to efficiently handle rapid changes. First update is sent immediately, subsequent rapid updates are coalesced, ensuring only the latest update is sent after a 1-second quiet period. * Enhance unit test to verify peer count synchronization with debouncing and timeout handling * Debounce based on type * Refactor test to validate timer restart after pending update dispatch * Simplify timer reset for Go 1.23+ automatic channel draining Remove manual channel drain in resetTimer() since Go 1.23+ automatically drains the timer channel when Stop() returns false, making the select-case pattern unnecessary.
588 lines
18 KiB
Go
588 lines
18 KiB
Go
package grpc
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/controllers/network_map"
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
)
|
|
|
|
func TestUpdateDebouncer_FirstUpdateSentImmediately(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
update := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
|
|
shouldSend := debouncer.ProcessUpdate(update)
|
|
|
|
if !shouldSend {
|
|
t.Error("First update should be sent immediately")
|
|
}
|
|
|
|
if debouncer.TimerChannel() == nil {
|
|
t.Error("Timer should be started after first update")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_RapidUpdatesCoalesced(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
update1 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
update2 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
update3 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
|
|
// First update should be sent immediately
|
|
if !debouncer.ProcessUpdate(update1) {
|
|
t.Error("First update should be sent immediately")
|
|
}
|
|
|
|
// Rapid subsequent updates should be coalesced
|
|
if debouncer.ProcessUpdate(update2) {
|
|
t.Error("Second rapid update should not be sent immediately")
|
|
}
|
|
|
|
if debouncer.ProcessUpdate(update3) {
|
|
t.Error("Third rapid update should not be sent immediately")
|
|
}
|
|
|
|
// Wait for debounce period
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
if len(pendingUpdates) != 1 {
|
|
t.Errorf("Should get exactly 1 pending update, got %d", len(pendingUpdates))
|
|
}
|
|
if pendingUpdates[0] != update3 {
|
|
t.Error("Should get the last update (update3)")
|
|
}
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_LastUpdateAlwaysSent(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(30 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
update1 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
update2 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
|
|
// Send first update
|
|
debouncer.ProcessUpdate(update1)
|
|
|
|
// Send second update within debounce period
|
|
debouncer.ProcessUpdate(update2)
|
|
|
|
// Wait for timer
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
if len(pendingUpdates) != 1 {
|
|
t.Errorf("Should get exactly 1 pending update, got %d", len(pendingUpdates))
|
|
}
|
|
if pendingUpdates[0] != update2 {
|
|
t.Error("Should get the last update")
|
|
}
|
|
if pendingUpdates[0] == update1 {
|
|
t.Error("Should not get the first update")
|
|
}
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_TimerResetOnNewUpdate(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
update1 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
update2 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
update3 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
|
|
// Send first update
|
|
debouncer.ProcessUpdate(update1)
|
|
|
|
// Wait a bit, but not the full debounce period
|
|
time.Sleep(30 * time.Millisecond)
|
|
|
|
// Send second update - should reset timer
|
|
debouncer.ProcessUpdate(update2)
|
|
|
|
// Wait a bit more
|
|
time.Sleep(30 * time.Millisecond)
|
|
|
|
// Send third update - should reset timer again
|
|
debouncer.ProcessUpdate(update3)
|
|
|
|
// Now wait for the timer (should fire after last update's reset)
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
if len(pendingUpdates) != 1 {
|
|
t.Errorf("Should get exactly 1 pending update, got %d", len(pendingUpdates))
|
|
}
|
|
if pendingUpdates[0] != update3 {
|
|
t.Error("Should get the last update (update3)")
|
|
}
|
|
// Timer should be restarted since there was a pending update
|
|
if debouncer.TimerChannel() == nil {
|
|
t.Error("Timer should be restarted after sending pending update")
|
|
}
|
|
case <-time.After(150 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_TimerRestartsAfterPendingUpdateSent(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(30 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
update1 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
update2 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
update3 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
|
|
// First update sent immediately
|
|
debouncer.ProcessUpdate(update1)
|
|
|
|
// Second update coalesced
|
|
debouncer.ProcessUpdate(update2)
|
|
|
|
// Wait for timer to expire
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
|
|
if len(pendingUpdates) == 0 {
|
|
t.Fatal("Should have pending update")
|
|
}
|
|
|
|
// After sending pending update, timer is restarted, so next update is NOT immediate
|
|
if debouncer.ProcessUpdate(update3) {
|
|
t.Error("Update after debounced send should not be sent immediately (timer restarted)")
|
|
}
|
|
|
|
// Wait for the restarted timer and verify update3 is pending
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
finalUpdates := debouncer.GetPendingUpdates()
|
|
if len(finalUpdates) != 1 || finalUpdates[0] != update3 {
|
|
t.Error("Should get update3 as pending")
|
|
}
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Error("Timer should have fired for restarted timer")
|
|
}
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_StopCleansUp(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
|
|
update := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
|
|
// Send update to start timer
|
|
debouncer.ProcessUpdate(update)
|
|
|
|
// Stop should clean up
|
|
debouncer.Stop()
|
|
|
|
// Multiple stops should be safe
|
|
debouncer.Stop()
|
|
}
|
|
|
|
func TestUpdateDebouncer_HighFrequencyUpdates(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
// Simulate high-frequency updates
|
|
var lastUpdate *network_map.UpdateMessage
|
|
sentImmediately := 0
|
|
for i := 0; i < 100; i++ {
|
|
update := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{
|
|
NetworkMap: &proto.NetworkMap{
|
|
Serial: uint64(i),
|
|
},
|
|
},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
lastUpdate = update
|
|
if debouncer.ProcessUpdate(update) {
|
|
sentImmediately++
|
|
}
|
|
time.Sleep(1 * time.Millisecond) // Very rapid updates
|
|
}
|
|
|
|
// Only first update should be sent immediately
|
|
if sentImmediately != 1 {
|
|
t.Errorf("Expected only 1 update sent immediately, got %d", sentImmediately)
|
|
}
|
|
|
|
// Wait for debounce period
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
if len(pendingUpdates) != 1 {
|
|
t.Errorf("Should get exactly 1 pending update, got %d", len(pendingUpdates))
|
|
}
|
|
if pendingUpdates[0] != lastUpdate {
|
|
t.Error("Should get the very last update")
|
|
}
|
|
if pendingUpdates[0].Update.NetworkMap.Serial != 99 {
|
|
t.Errorf("Expected serial 99, got %d", pendingUpdates[0].Update.NetworkMap.Serial)
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_NoUpdatesAfterFirst(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(30 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
update := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
|
|
// Send first update
|
|
if !debouncer.ProcessUpdate(update) {
|
|
t.Error("First update should be sent immediately")
|
|
}
|
|
|
|
// Wait for timer to expire with no additional updates (true quiet period)
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
if len(pendingUpdates) != 0 {
|
|
t.Error("Should have no pending updates")
|
|
}
|
|
// After true quiet period, timer should be cleared
|
|
if debouncer.TimerChannel() != nil {
|
|
t.Error("Timer should be cleared after quiet period")
|
|
}
|
|
case <-time.After(100 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_IntermediateUpdatesDropped(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
updates := make([]*network_map.UpdateMessage, 5)
|
|
for i := range updates {
|
|
updates[i] = &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{
|
|
NetworkMap: &proto.NetworkMap{
|
|
Serial: uint64(i),
|
|
},
|
|
},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
}
|
|
|
|
// First update sent immediately
|
|
debouncer.ProcessUpdate(updates[0])
|
|
|
|
// Send updates 1, 2, 3, 4 rapidly - only last one should remain pending
|
|
debouncer.ProcessUpdate(updates[1])
|
|
debouncer.ProcessUpdate(updates[2])
|
|
debouncer.ProcessUpdate(updates[3])
|
|
debouncer.ProcessUpdate(updates[4])
|
|
|
|
// Wait for debounce
|
|
<-debouncer.TimerChannel()
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
|
|
if len(pendingUpdates) != 1 {
|
|
t.Errorf("Should get exactly 1 pending update, got %d", len(pendingUpdates))
|
|
}
|
|
if pendingUpdates[0].Update.NetworkMap.Serial != 4 {
|
|
t.Errorf("Expected only the last update (serial 4), got serial %d", pendingUpdates[0].Update.NetworkMap.Serial)
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_TrueQuietPeriodResetsToImmediateMode(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(30 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
update1 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
update2 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
|
|
// First update sent immediately
|
|
if !debouncer.ProcessUpdate(update1) {
|
|
t.Error("First update should be sent immediately")
|
|
}
|
|
|
|
// Wait for timer without sending any more updates (true quiet period)
|
|
<-debouncer.TimerChannel()
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
|
|
if len(pendingUpdates) != 0 {
|
|
t.Error("Should have no pending updates during quiet period")
|
|
}
|
|
|
|
// After true quiet period, next update should be sent immediately
|
|
if !debouncer.ProcessUpdate(update2) {
|
|
t.Error("Update after true quiet period should be sent immediately")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_ContinuousHighFrequencyStaysInDebounceMode(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
// Simulate continuous high-frequency updates
|
|
for i := 0; i < 10; i++ {
|
|
update := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{
|
|
NetworkMap: &proto.NetworkMap{
|
|
Serial: uint64(i),
|
|
},
|
|
},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
|
|
if i == 0 {
|
|
// First one sent immediately
|
|
if !debouncer.ProcessUpdate(update) {
|
|
t.Error("First update should be sent immediately")
|
|
}
|
|
} else {
|
|
// All others should be coalesced (not sent immediately)
|
|
if debouncer.ProcessUpdate(update) {
|
|
t.Errorf("Update %d should not be sent immediately", i)
|
|
}
|
|
}
|
|
|
|
// Wait a bit but send next update before debounce expires
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
|
|
// Now wait for final debounce
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
if len(pendingUpdates) == 0 {
|
|
t.Fatal("Should have the last update pending")
|
|
}
|
|
if pendingUpdates[0].Update.NetworkMap.Serial != 9 {
|
|
t.Errorf("Expected serial 9, got %d", pendingUpdates[0].Update.NetworkMap.Serial)
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_ControlConfigMessagesQueued(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
netmapUpdate := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetworkMap: &proto.NetworkMap{Serial: 1}},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
tokenUpdate1 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetbirdConfig: &proto.NetbirdConfig{}},
|
|
MessageType: network_map.MessageTypeControlConfig,
|
|
}
|
|
tokenUpdate2 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetbirdConfig: &proto.NetbirdConfig{}},
|
|
MessageType: network_map.MessageTypeControlConfig,
|
|
}
|
|
|
|
// First update sent immediately
|
|
debouncer.ProcessUpdate(netmapUpdate)
|
|
|
|
// Send multiple control config updates - they should all be queued
|
|
debouncer.ProcessUpdate(tokenUpdate1)
|
|
debouncer.ProcessUpdate(tokenUpdate2)
|
|
|
|
// Wait for debounce period
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
// Should get both control config updates
|
|
if len(pendingUpdates) != 2 {
|
|
t.Errorf("Expected 2 control config updates, got %d", len(pendingUpdates))
|
|
}
|
|
// Control configs should come first
|
|
if pendingUpdates[0] != tokenUpdate1 {
|
|
t.Error("First pending update should be tokenUpdate1")
|
|
}
|
|
if pendingUpdates[1] != tokenUpdate2 {
|
|
t.Error("Second pending update should be tokenUpdate2")
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_MixedMessageTypes(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
netmapUpdate1 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetworkMap: &proto.NetworkMap{Serial: 1}},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
netmapUpdate2 := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetworkMap: &proto.NetworkMap{Serial: 2}},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
tokenUpdate := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetbirdConfig: &proto.NetbirdConfig{}},
|
|
MessageType: network_map.MessageTypeControlConfig,
|
|
}
|
|
|
|
// First update sent immediately
|
|
debouncer.ProcessUpdate(netmapUpdate1)
|
|
|
|
// Send token update and network map update
|
|
debouncer.ProcessUpdate(tokenUpdate)
|
|
debouncer.ProcessUpdate(netmapUpdate2)
|
|
|
|
// Wait for debounce period
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
// Should get 2 updates in order: token, then network map
|
|
if len(pendingUpdates) != 2 {
|
|
t.Errorf("Expected 2 pending updates, got %d", len(pendingUpdates))
|
|
}
|
|
// Token update should come first (preserves order)
|
|
if pendingUpdates[0] != tokenUpdate {
|
|
t.Error("First pending update should be tokenUpdate")
|
|
}
|
|
// Network map update should come second
|
|
if pendingUpdates[1] != netmapUpdate2 {
|
|
t.Error("Second pending update should be netmapUpdate2")
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|
|
|
|
func TestUpdateDebouncer_OrderPreservation(t *testing.T) {
|
|
debouncer := NewUpdateDebouncer(50 * time.Millisecond)
|
|
defer debouncer.Stop()
|
|
|
|
// Simulate: 50 network maps -> 1 control config -> 50 network maps
|
|
// Expected result: 3 messages (netmap, controlConfig, netmap)
|
|
|
|
// Send first network map immediately
|
|
firstNetmap := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetworkMap: &proto.NetworkMap{Serial: 0}},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
if !debouncer.ProcessUpdate(firstNetmap) {
|
|
t.Error("First update should be sent immediately")
|
|
}
|
|
|
|
// Send 49 more network maps (will be coalesced to last one)
|
|
var lastNetmapBatch1 *network_map.UpdateMessage
|
|
for i := 1; i < 50; i++ {
|
|
lastNetmapBatch1 = &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetworkMap: &proto.NetworkMap{Serial: uint64(i)}},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
debouncer.ProcessUpdate(lastNetmapBatch1)
|
|
}
|
|
|
|
// Send 1 control config
|
|
controlConfig := &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetbirdConfig: &proto.NetbirdConfig{}},
|
|
MessageType: network_map.MessageTypeControlConfig,
|
|
}
|
|
debouncer.ProcessUpdate(controlConfig)
|
|
|
|
// Send 50 more network maps (will be coalesced to last one)
|
|
var lastNetmapBatch2 *network_map.UpdateMessage
|
|
for i := 50; i < 100; i++ {
|
|
lastNetmapBatch2 = &network_map.UpdateMessage{
|
|
Update: &proto.SyncResponse{NetworkMap: &proto.NetworkMap{Serial: uint64(i)}},
|
|
MessageType: network_map.MessageTypeNetworkMap,
|
|
}
|
|
debouncer.ProcessUpdate(lastNetmapBatch2)
|
|
}
|
|
|
|
// Wait for debounce period
|
|
select {
|
|
case <-debouncer.TimerChannel():
|
|
pendingUpdates := debouncer.GetPendingUpdates()
|
|
// Should get exactly 3 updates: netmap, controlConfig, netmap
|
|
if len(pendingUpdates) != 3 {
|
|
t.Errorf("Expected 3 pending updates, got %d", len(pendingUpdates))
|
|
}
|
|
// First should be the last netmap from batch 1
|
|
if pendingUpdates[0] != lastNetmapBatch1 {
|
|
t.Error("First pending update should be last netmap from batch 1")
|
|
}
|
|
if pendingUpdates[0].Update.NetworkMap.Serial != 49 {
|
|
t.Errorf("Expected serial 49, got %d", pendingUpdates[0].Update.NetworkMap.Serial)
|
|
}
|
|
// Second should be the control config
|
|
if pendingUpdates[1] != controlConfig {
|
|
t.Error("Second pending update should be control config")
|
|
}
|
|
// Third should be the last netmap from batch 2
|
|
if pendingUpdates[2] != lastNetmapBatch2 {
|
|
t.Error("Third pending update should be last netmap from batch 2")
|
|
}
|
|
if pendingUpdates[2].Update.NetworkMap.Serial != 99 {
|
|
t.Errorf("Expected serial 99, got %d", pendingUpdates[2].Update.NetworkMap.Serial)
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Error("Timer should have fired")
|
|
}
|
|
}
|