Files
zt/tests/router_data_model_order_test.go
zeekay 75fa3d51e0 Deliver control traffic in the order it was sent
Only the dialing side recorded which underlays it held, so on the
controller the high-priority underlay drained the default queue too,
and back-to-back default messages to a router crossed two connections
and could arrive swapped.  The router data model applies change sets
by index and discards one older than what it holds, so an overtaken
change set was lost for good: an identity could reach the router
without the policy that lets it dial.

Only default underlays drain the default queue now, a high-priority
underlay carries high-priority traffic alone, and a failed write is
reported instead of requeued behind later messages.  High-priority
traffic still rides the default underlay while no high-priority one is
connected, and losing the last one wakes the default underlay at once.

Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
2026-09-23 20:54:23 -07:00

127 lines
4.3 KiB
Go

//go:build apitests
package tests
import (
"fmt"
"sync"
"testing"
"time"
"github.com/hanzozt/channel/v4"
"github.com/hanzozt/zt/v2/common/ctrlchan"
"github.com/hanzozt/zt/v2/common/pb/edge_ctrl_pb"
"github.com/hanzozt/zt/v2/controller/xt_smartrouting"
)
// Change sets the controller publishes back to back reach the router in order, so it holds
// every one, while its control channel holds a high-priority underlay beside the default one.
// The router applies change sets by index and drops one that arrives after a later one, so
// a burst split across the two underlays loses whatever is overtaken. A router renewing its
// subscription is sent every change set since its index in one such burst.
func Test_RouterDataModel_ChangeSetBurstArrivesInOrder(t *testing.T) {
ctx := NewTestContext(t)
defer ctx.Teardown()
ctx.StartServer()
ctx.RequireAdminManagementApiLogin()
router := ctx.CreateEnrollAndStartEdgeRouter()
requireHighPriorityUnderlay(ctx, router.GetNetworkControllers().AnyChannel())
sender := ctx.EdgeController.AppEnv.Broker.GetRouterDataModel()
const count = 200
index := sender.CurrentIndex()
for i := range count {
index++
sender.ApplyChangeSet(&edge_ctrl_pb.DataState_ChangeSet{
Index: index,
Changes: []*edge_ctrl_pb.DataState_Event{{
Action: edge_ctrl_pb.DataState_Create,
Model: &edge_ctrl_pb.DataState_Event_Identity{
Identity: &edge_ctrl_pb.DataState_Identity{Id: fmt.Sprintf("burst-%d", i), Name: fmt.Sprintf("burst-%d", i)},
},
}},
})
}
rdm := router.GetRouterDataModel()
ctx.Req.Eventually(func() bool {
return rdm.CurrentIndex() == index
}, 5*time.Second, 20*time.Millisecond, "router should reach index %d", index)
for i := range count {
_, found := rdm.Identities.Get(fmt.Sprintf("burst-%d", i))
ctx.Req.True(found, "router lost change set %d of %d", i+1, count)
}
}
// Identities created back to back reach the router with their dial policy, and each dials
// within seconds, while the router's control channel holds a high-priority underlay beside
// the default one. The router applies data model change sets by index and drops one that
// arrives after a later one, so a change set that overtakes its predecessor loses it.
func Test_RouterDataModel_NewIdentitiesDial(t *testing.T) {
ctx := NewTestContext(t)
defer ctx.Teardown()
ctx.StartServer()
ctx.RequireAdminManagementApiLogin()
service := ctx.AdminManagementSession.RequireNewServiceAccessibleToAll(xt_smartrouting.Name)
router := ctx.CreateEnrollAndStartEdgeRouter()
requireHighPriorityUnderlay(ctx, router.GetNetworkControllers().AnyChannel())
_, host := ctx.AdminManagementSession.RequireCreateSdkContext()
defer host.Close()
listener, err := host.Listen(service.Name)
ctx.Req.NoError(err)
defer func() { _ = listener.Close() }()
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
_ = conn.Close()
}
}()
const count = 20
ids := make(chan string, count)
var wg sync.WaitGroup
for range count {
wg.Add(1)
go func() {
defer wg.Done()
ids <- ctx.AdminManagementSession.RequireNewIdentityWithOtt(false).Id
}()
}
wg.Wait()
close(ids)
rdm := router.GetRouterDataModel()
for id := range ids {
ctx.Req.Eventually(func() bool {
access, err := rdm.GetServiceAccessPolicies(id, service.Id, edge_ctrl_pb.PolicyType_DialPolicy)
return err == nil && len(access.Policies) > 0
}, 2*time.Second, 20*time.Millisecond, "identity %s should reach the router with its dial policy", id)
}
for range 3 {
_, dialer := ctx.AdminManagementSession.RequireCreateSdkContext()
start := time.Now()
conn, err := dialer.Dial(service.Name)
ctx.Req.NoError(err, "a new identity should dial at once")
t.Logf("new identity dialed in %v", time.Since(start))
_ = conn.Close()
dialer.Close()
}
}
// requireHighPriorityUnderlay waits until the router's control channel holds a high-priority
// underlay beside its default one, which it dials a few seconds after connecting.
func requireHighPriorityUnderlay(ctx *TestContext, ch channel.Channel) {
ctx.Req.Eventually(func() bool {
multi, ok := ch.(interface{ GetUnderlayCountsByType() map[string]int })
return ok && multi.GetUnderlayCountsByType()[ctrlchan.ChannelTypeHighPriority] > 0
}, 15*time.Second, 100*time.Millisecond, "router should hold a high-priority control underlay")
}