Skip to content

Commit 0e89e14

Browse files
committed
Bound gateway informer startup and honor forwarding path MTUs
Cancel obsolete unsynced generations, bound required-cache synchronization, preserve adjacent-hop MTU caps, and use the active generation in route summaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ab9b78ee-42b8-4841-8d74-cd78d002d48b
1 parent 7543f98 commit 0e89e14

7 files changed

Lines changed: 251 additions & 37 deletions

‎cmd/unbounded-net-node/gateway_informer_lifecycle.go‎

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,26 @@ type gatewayInformerGeneration struct {
3333
}
3434

3535
type gatewayInformerLifecycle struct {
36-
client dynamic.Interface
37-
resync time.Duration
38-
39-
mu sync.RWMutex
40-
required bool
41-
starting bool
42-
retiring bool
43-
generation uint64
44-
active *gatewayInformerGeneration
45-
onEvent func()
46-
lastErr error
36+
client dynamic.Interface
37+
resync time.Duration
38+
syncTimeout time.Duration
39+
40+
mu sync.RWMutex
41+
required bool
42+
starting bool
43+
retiring bool
44+
generation uint64
45+
active *gatewayInformerGeneration
46+
onEvent func()
47+
lastErr error
48+
startCancel context.CancelFunc
4749
}
4850

4951
func newGatewayInformerLifecycle(client dynamic.Interface, resync time.Duration) *gatewayInformerLifecycle {
5052
return &gatewayInformerLifecycle{
51-
client: client,
52-
resync: resync,
53+
client: client,
54+
resync: resync,
55+
syncTimeout: 30 * time.Second,
5356
}
5457
}
5558

@@ -75,7 +78,11 @@ func (l *gatewayInformerLifecycle) setRequired(required bool) {
7578
defer l.mu.Unlock()
7679

7780
l.required = required
81+
7882
l.retiring = !required && l.active != nil
83+
if !required && l.startCancel != nil {
84+
l.startCancel()
85+
}
7986
}
8087

8188
func (l *gatewayInformerLifecycle) readiness() (bool, string) {
@@ -155,6 +162,20 @@ func (l *gatewayInformerLifecycle) start(ctx context.Context) (gatewayInformerSn
155162
return snapshot, false, nil
156163
}
157164

165+
if !l.required {
166+
l.mu.Unlock()
167+
168+
return gatewayInformerSnapshot{}, false, nil
169+
}
170+
171+
if l.starting {
172+
l.mu.Unlock()
173+
174+
return gatewayInformerSnapshot{}, false, fmt.Errorf("gateway informer caches are already starting")
175+
}
176+
177+
runCtx, cancel := context.WithCancel(ctx)
178+
l.startCancel = cancel
158179
l.starting = true
159180
l.lastErr = nil
160181
l.generation++
@@ -168,12 +189,12 @@ func (l *gatewayInformerLifecycle) start(ctx context.Context) (gatewayInformerSn
168189

169190
cleanups, err := addGatewayInformerEventHandlers(pools, nodes, poolPeerings, l.requestUpdate)
170191
if err != nil {
192+
cancel()
171193
l.recordStartFailure(err)
172194

173195
return gatewayInformerSnapshot{}, false, err
174196
}
175197

176-
runCtx, cancel := context.WithCancel(ctx)
177198
factory.Start(runCtx.Done())
178199

179200
snapshot := gatewayInformerSnapshot{
@@ -183,7 +204,11 @@ func (l *gatewayInformerLifecycle) start(ctx context.Context) (gatewayInformerSn
183204
nodes: nodes,
184205
poolPeerings: poolPeerings,
185206
}
186-
if !cache.WaitForCacheSync(runCtx.Done(), pools.HasSynced, nodes.HasSynced, poolPeerings.HasSynced) {
207+
208+
syncCtx, stopSync := context.WithTimeout(runCtx, l.syncTimeout)
209+
defer stopSync()
210+
211+
if !cache.WaitForCacheSync(syncCtx.Done(), pools.HasSynced, nodes.HasSynced, poolPeerings.HasSynced) {
187212
cancel()
188213
runGatewayInformerCleanups(cleanups)
189214

@@ -204,7 +229,18 @@ func (l *gatewayInformerLifecycle) start(ctx context.Context) (gatewayInformerSn
204229
}
205230

206231
l.mu.Lock()
232+
if !l.required || runCtx.Err() != nil {
233+
l.starting = false
234+
l.startCancel = nil
235+
l.mu.Unlock()
236+
cancel()
237+
runGatewayInformerCleanups(cleanups)
238+
239+
return gatewayInformerSnapshot{}, false, fmt.Errorf("gateway informer generation %d start canceled", generation)
240+
}
241+
207242
l.starting = false
243+
l.startCancel = nil
208244
l.lastErr = nil
209245
l.active = activeGeneration
210246
l.retiring = !l.required
@@ -218,6 +254,7 @@ func (l *gatewayInformerLifecycle) recordStartFailure(err error) {
218254
defer l.mu.Unlock()
219255

220256
l.starting = false
257+
l.startCancel = nil
221258
l.lastErr = err
222259
}
223260

@@ -244,7 +281,11 @@ func (l *gatewayInformerLifecycle) close() {
244281
active := l.active
245282
l.active = nil
246283
l.required = false
284+
247285
l.retiring = false
286+
if l.startCancel != nil {
287+
l.startCancel()
288+
}
248289
l.mu.Unlock()
249290

250291
if active != nil {

‎cmd/unbounded-net-node/gateway_informer_lifecycle_test.go‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55

66
import (
77
"context"
8+
"errors"
89
"sync"
910
"sync/atomic"
1011
"testing"
@@ -15,10 +16,76 @@ import (
1516
"k8s.io/apimachinery/pkg/runtime"
1617
"k8s.io/apimachinery/pkg/runtime/schema"
1718
dynamicfake "k8s.io/client-go/dynamic/fake"
19+
k8stesting "k8s.io/client-go/testing"
1820

1921
unboundednetv1alpha1 "github.com/Azure/unbounded/api/net/v1alpha1"
2022
)
2123

24+
func TestGatewayInformerFailedListCanBeAbandoned(t *testing.T) {
25+
for _, mode := range []string{"assignment removed", "sync timeout", "close"} {
26+
t.Run(mode, func(t *testing.T) {
27+
client := newGatewayLifecycleClient()
28+
listed := make(chan struct{}, 1)
29+
30+
client.PrependReactor("list", "*", func(k8stesting.Action) (bool, runtime.Object, error) {
31+
select {
32+
case listed <- struct{}{}:
33+
default:
34+
}
35+
36+
return true, nil, errors.New("list unavailable")
37+
})
38+
lifecycle := newGatewayInformerLifecycle(client, 0)
39+
t.Cleanup(lifecycle.close)
40+
41+
if mode == "sync timeout" {
42+
lifecycle.syncTimeout = 50 * time.Millisecond
43+
}
44+
45+
done := make(chan error, 1)
46+
47+
go func() {
48+
_, _, err := lifecycle.prepare(t.Context(), true)
49+
done <- err
50+
}()
51+
52+
select {
53+
case <-listed:
54+
case <-time.After(2 * time.Second):
55+
t.Fatal("informers did not attempt LIST")
56+
}
57+
58+
switch mode {
59+
case "assignment removed":
60+
lifecycle.setRequired(false)
61+
case "close":
62+
lifecycle.close()
63+
}
64+
65+
select {
66+
case err := <-done:
67+
if err == nil {
68+
t.Fatal("failed LIST unexpectedly produced synced caches")
69+
}
70+
case <-time.After(2 * time.Second):
71+
t.Fatal("cache sync blocked reconciliation")
72+
}
73+
74+
if lifecycle.snapshot().available {
75+
t.Fatal("unsynced generation was published")
76+
}
77+
78+
if mode != "sync timeout" {
79+
if _, retire, err := lifecycle.prepare(t.Context(), false); err != nil || retire {
80+
t.Fatalf("failed to abandon caches: retire=%v err=%v", retire, err)
81+
}
82+
} else if ready, _ := lifecycle.readiness(); ready {
83+
t.Fatal("timed-out required caches reported ready")
84+
}
85+
})
86+
}
87+
}
88+
2289
func TestGatewayInformerLifecycleLateEventHandler(t *testing.T) {
2390
client := newGatewayLifecycleClient()
2491
lifecycle := newGatewayInformerLifecycle(client, 0)

‎cmd/unbounded-net-node/reconciliation_helpers.go‎

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package main
66
import (
77
"encoding/json"
88
"fmt"
9+
"slices"
910
"strconv"
1011
"strings"
1112
"time"
@@ -276,6 +277,10 @@ func gatewayPoolPeeringTunnelMTU(
276277
remotePool = strings.TrimSpace(remotePool)
277278

278279
for _, peering := range poolPeerings {
280+
if !unboundednetv1alpha1.SpecEnabled(peering.Spec.Enabled) {
281+
continue
282+
}
283+
279284
hasLocal := false
280285
hasRemote := false
281286

@@ -294,36 +299,51 @@ func gatewayPoolPeeringTunnelMTU(
294299
}
295300

296301
func learnedGatewayTransitTunnelMTU(
297-
peerPool string,
298302
learnedRoutes map[string]unboundednetv1alpha1.GatewayNodeRoute,
303+
siteMTUs, poolMTUs map[string]int,
304+
assignments []unboundednetv1alpha1.SiteGatewayPoolAssignment,
299305
poolPeerings []unboundednetv1alpha1.GatewayPoolPeering,
300306
) int {
301307
result := 0
302-
peerPool = strings.TrimSpace(peerPool)
303308

304309
for _, route := range learnedRoutes {
305310
for _, path := range route.Paths {
306-
pathPools := map[string]bool{peerPool: true}
311+
for index, hop := range path {
312+
switch hop.Type {
313+
case "Site":
314+
result = resolveTunnelMTU(result, siteMTUs[hop.Name])
315+
case "GatewayPool":
316+
result = resolveTunnelMTU(result, poolMTUs[hop.Name])
317+
}
307318

308-
for _, hop := range path {
309-
if hop.Type == "GatewayPool" {
310-
if poolName := strings.TrimSpace(hop.Name); poolName != "" {
311-
pathPools[poolName] = true
312-
}
319+
if index == 0 {
320+
continue
313321
}
314-
}
315322

316-
for _, peering := range poolPeerings {
317-
matchedPools := 0
323+
// Paths run from origin to advertiser. Only adjacent hops
324+
// identify a forwarding edge; co-occurrence is not enough.
325+
previous := path[index-1]
326+
if previous.Type == "GatewayPool" && hop.Type == "GatewayPool" {
327+
result = resolveTunnelMTU(result, gatewayPoolPeeringTunnelMTU([]string{previous.Name}, hop.Name, poolPeerings))
318328

319-
for _, poolName := range peering.Spec.GatewayPools {
320-
if pathPools[strings.TrimSpace(poolName)] {
321-
matchedPools++
322-
}
329+
continue
330+
}
331+
332+
site, pool := previous, hop
333+
if site.Type == "GatewayPool" && pool.Type == "Site" {
334+
site, pool = pool, site
323335
}
324336

325-
if matchedPools >= 2 {
326-
result = resolveTunnelMTU(result, tunnelMTUFromSpec(peering.Spec.TunnelMTU))
337+
if site.Type != "Site" || pool.Type != "GatewayPool" {
338+
continue
339+
}
340+
341+
for _, assignment := range assignments {
342+
if unboundednetv1alpha1.SpecEnabled(assignment.Spec.Enabled) &&
343+
slices.Contains(assignment.Spec.Sites, site.Name) &&
344+
slices.Contains(assignment.Spec.GatewayPools, pool.Name) {
345+
result = resolveTunnelMTU(result, tunnelMTUFromSpec(assignment.Spec.TunnelMTU))
346+
}
327347
}
328348
}
329349
}

‎cmd/unbounded-net-node/reconciliation_helpers_test.go‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,49 @@ func TestMergeLowestTunnelMTU(t *testing.T) {
250250
}
251251
}
252252

253+
func TestLearnedGatewayTransitPathCaps(t *testing.T) {
254+
path := []unboundednetv1alpha1.GatewayNodePathHop{
255+
{Type: "Site", Name: "site-b"},
256+
{Type: "GatewayPool", Name: "pool-h"},
257+
{Type: "GatewayPool", Name: "pool-j"},
258+
{Type: "GatewayPool", Name: "pool-g"},
259+
}
260+
routes := map[string]unboundednetv1alpha1.GatewayNodeRoute{
261+
"10.0.0.0/24": {Paths: [][]unboundednetv1alpha1.GatewayNodePathHop{path}},
262+
}
263+
264+
for _, tc := range []struct {
265+
name string
266+
sites map[string]int
267+
pools map[string]int
268+
assignments []unboundednetv1alpha1.SiteGatewayPoolAssignment
269+
peerings []unboundednetv1alpha1.GatewayPoolPeering
270+
want int
271+
}{
272+
{name: "origin Site", sites: map[string]int{"site-b": 1200, "unrelated": 900}, want: 1200},
273+
{name: "downstream pool", pools: map[string]int{"pool-h": 1250, "unrelated": 900}, want: 1250},
274+
{name: "assignment edge", assignments: []unboundednetv1alpha1.SiteGatewayPoolAssignment{
275+
{Spec: unboundednetv1alpha1.SiteGatewayPoolAssignmentSpec{Sites: []string{"site-b"}, GatewayPools: []string{"pool-h"}, TunnelMTU: ptrInt32(1100)}},
276+
}, want: 1100},
277+
{name: "assignment nonadjacent", assignments: []unboundednetv1alpha1.SiteGatewayPoolAssignment{
278+
{Spec: unboundednetv1alpha1.SiteGatewayPoolAssignmentSpec{Sites: []string{"site-b"}, GatewayPools: []string{"pool-g"}, TunnelMTU: ptrInt32(1100)}},
279+
}},
280+
{name: "adjacent peering", peerings: []unboundednetv1alpha1.GatewayPoolPeering{
281+
{Spec: unboundednetv1alpha1.GatewayPoolPeeringSpec{GatewayPools: []string{"pool-h", "pool-j"}, TunnelMTU: ptrInt32(1300)}},
282+
}, want: 1300},
283+
{name: "nonadjacent peering", peerings: []unboundednetv1alpha1.GatewayPoolPeering{
284+
{Spec: unboundednetv1alpha1.GatewayPoolPeeringSpec{GatewayPools: []string{"pool-h", "pool-g"}, TunnelMTU: ptrInt32(1000)}},
285+
}},
286+
} {
287+
t.Run(tc.name, func(t *testing.T) {
288+
got := learnedGatewayTransitTunnelMTU(routes, tc.sites, tc.pools, tc.assignments, tc.peerings)
289+
if got != tc.want {
290+
t.Fatalf("transit MTU=%d want=%d", got, tc.want)
291+
}
292+
})
293+
}
294+
}
295+
253296
func TestForwardingAwareTunnelMTU(t *testing.T) {
254297
siteMTUs := map[string]int{
255298
"site-a": 1450,
@@ -296,11 +339,11 @@ func TestForwardingAwareTunnelMTU(t *testing.T) {
296339
}},
297340
},
298341
}
299-
if got := learnedGatewayTransitTunnelMTU("pool-g", learnedRoutes, []unboundednetv1alpha1.GatewayPoolPeering{peering}); got != 1200 {
342+
if got := learnedGatewayTransitTunnelMTU(learnedRoutes, nil, nil, nil, []unboundednetv1alpha1.GatewayPoolPeering{peering}); got != 1200 {
300343
t.Fatalf("worker learned gateway transit MTU = %d, want 1200", got)
301344
}
302345

303-
if got := learnedGatewayTransitTunnelMTU("pool-g", nil, []unboundednetv1alpha1.GatewayPoolPeering{peering}); got != 0 {
346+
if got := learnedGatewayTransitTunnelMTU(nil, nil, nil, nil, []unboundednetv1alpha1.GatewayPoolPeering{peering}); got != 0 {
304347
t.Fatalf("gateway peering without a learned forwarding path constrained worker MTU: %d", got)
305348
}
306349

‎cmd/unbounded-net-node/site_watch_reconcile.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ func watchSiteAndConfigureWireGuard(ctx context.Context, clientset kubernetes.In
283283
gatewaySnapshot gatewayInformerSnapshot
284284
retireGatewayInformers bool
285285
gatewayStateStable bool
286+
err error
286287
)
287288

288289
for range 3 {
@@ -1871,9 +1872,9 @@ func updateWireGuardFromSlices(ctx context.Context, dynamicClient dynamic.Interf
18711872
excludedNodeCIDRSites := networkPeeredSites
18721873
routedCidrs, routeDistances, learnedRoutes := routedCIDRsForGatewayPeer(peerGatewayNode, fallbackRoutedCIDRs, mySiteName, localGatewayPools, excludedNodeCIDRSites, now, staleAfter)
18731874

1874-
transitTunnelMTU := learnedGatewayTransitTunnelMTU(pool.Name, learnedRoutes, poolPeerings)
1875+
transitTunnelMTU := learnedGatewayTransitTunnelMTU(learnedRoutes, siteTunnelMTUs, poolTunnelMTUs, assignments, poolPeerings)
18751876
if isGatewayNode {
1876-
transitTunnelMTU = gatewayPoolPeeringTunnelMTU(localGatewayPools, pool.Name, poolPeerings)
1877+
transitTunnelMTU = resolveTunnelMTU(transitTunnelMTU, gatewayPoolPeeringTunnelMTU(localGatewayPools, pool.Name, poolPeerings))
18771878
}
18781879

18791880
peeringTunnelProtocol := poolPeeringTunnelProtocols[pool.Name]

0 commit comments

Comments
 (0)