Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions pkg/router/route_group_mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,51 @@ func TestRouteMuxRemoveLegsPromotesPrimary(t *testing.T) {
require.True(t, m.ready[1], "former leg 2 keeps its ready bit")
}

// TestRemoveMuxRouteByTransportRemovesPrimary proves the router-level guard
// against removing the primary leg (index 0) is gone: removing the tps[0]
// transport re-homes the primary onto a surviving leg — the ping/SACK/latency
// probes read rg.tps[0] LIVE, so the promoted leg transparently takes over, the
// same way pruneDeadTransports/pruneLegByConsumeRule already re-home index 0.
// This is the primitive that lets exact route pinning, live direct<->multihop
// route switching and single-leg pinning reach the operator's exact leg-set
// with no un-prunable auto primary left behind.
func TestRemoveMuxRouteByTransportRemovesPrimary(t *testing.T) {
rg, mts, _ := createMuxRouteGroup(t, 3)
r := &router{
logger: logging.MustGetLogger("remove_primary_test"),
rt: rg.rt,
rgsNs: make(map[routing.RouteDescriptor]*NoiseRouteGroup),
}
r.rgsNs[rg.desc] = &NoiseRouteGroup{rg: rg, Conn: rg}

primaryID := mts[0].Entry.ID
secondID := mts[1].Entry.ID

require.NoError(t, r.RemoveMuxRouteByTransport(rg.desc, primaryID),
"removing the primary leg (index 0) must be allowed and re-home")

rg.mu.Lock()
defer rg.mu.Unlock()
require.Len(t, rg.tps, 2, "exactly one leg removed")
require.Equal(t, secondID, rg.tps[0].Entry.ID,
"former leg 1 must be promoted to primary at index 0")
}

// TestRemoveMuxRouteByTransportKeepsLastLeg confirms the last-leg floor still
// holds after relaxing the primary guard: the sole remaining leg is never removed.
func TestRemoveMuxRouteByTransportKeepsLastLeg(t *testing.T) {
rg, mts, _ := createMuxRouteGroup(t, 1)
r := &router{
logger: logging.MustGetLogger("keep_last_test"),
rt: rg.rt,
rgsNs: make(map[routing.RouteDescriptor]*NoiseRouteGroup),
}
r.rgsNs[rg.desc] = &NoiseRouteGroup{rg: rg, Conn: rg}

require.Error(t, r.RemoveMuxRouteByTransport(rg.desc, mts[0].Entry.ID),
"removing the only remaining leg must still be refused")
}

// TestRouteMuxRemoveLegsEmpty is the no-op guard.
func TestRouteMuxRemoveLegsEmpty(t *testing.T) {
m := &routeMux{}
Expand Down
18 changes: 11 additions & 7 deletions pkg/router/router_mux_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,13 +496,17 @@ func (r *router) RemoveMuxRouteByTransport(desc routing.RouteDescriptor, tpID uu
if idx < 0 {
return fmt.Errorf("transport %s not found in route group", tpID)
}
// The primary leg (index 0) is privileged throughout the route group:
// ping/pong/SACK and latency attribution hardcode tps[0], and the mux
// selector treats leg 0 as always-ready. Removing it silently breaks the
// group, so refuse it (the last-leg guard above does not cover this).
if idx == 0 {
return errors.New("cannot remove the primary leg (index 0) of a route group")
}
// Removing the primary leg (index 0) is ALLOWED and re-homes the primary.
// ping/pong/SACK and latency attribution read rg.tps[0] LIVE on each call
// (they hold no cached leg reference), and the mux selector is rebuilt
// below, so the survivor compacted into index 0 transparently takes over —
// the exact re-home pruneDeadTransports (index-0 death) and
// pruneLegByConsumeRule (remote leg retire) already perform in production.
// The last-leg guard above is the only real floor. Allowing this is what
// makes full manual route control work: exact route pinning, live
// direct<->multihop route switching, and single-leg pinning — the leg-set
// becomes exactly what the operator asks for (`proxy start --route`,
// `proxy mux set --prune`), with no un-prunable auto primary left behind.

// Collect rule IDs to delete
var deadRuleIDs []routing.RouteID
Expand Down
Loading