Skip to content

Commit 2d4d03d

Browse files
ivanmatmatioktalz
authored andcommitted
MINOR: gate: re-upsert only the Gateways whose conflict verdict changed
A listener conflict is a relational property: it depends on the listeners of the other Gateways. A Gateway can therefore enter or leave the conflict set without any event of its own -- adding a listener to an older Gateway invalidates a younger Gateway's listener on the same port, and deleting the older one frees it again. The conflict table is rebuilt for the whole tree on every cycle, but the transfer of a verdict onto a listener (checkConflict -> CheckConflict -> BuildConditions) only runs for Gateways marked StatusUpserted. Re-upserting an untouched Gateway is what routes it through that single transfer point, so the selection of Gateways to re-upsert must cover every verdict change. Until now that selection was the union of the Gateways having a conflicting listener in the previous cycle and those having one in the current cycle. It was correct but coarse in one direction and blind in another: - a lasting misconfiguration keeps its Gateways in both sets, so they were re-upserted on every single cycle, whatever the event that triggered it. Each re-upsert replays all five listener checks, down to re-reading the Secrets and PEM-decoding the certificates in checkCertificateRefs, to conclude that nothing changed; - conversely, a listener staying conflicted while its reason changes (for instance ProtocolConflict becoming HostnameConflict) does not change its membership of either set, so the stale reason was never refreshed. Replace the two set computations with a value diff of the previous and current conflict tables. Both tables hold an entry for every listener of the tree, conflicting or not, so comparing the listenerConflictCondition values yields exactly the listeners whose verdict changed -- entering, leaving, or keeping a conflict with a different reason -- and nothing else. The diff works at listener granularity and derives the Gateway keys from the listener keys, which is also the granularity needed to extend the mechanism to resources other than Gateways.
1 parent 8e523a0 commit 2d4d03d

2 files changed

Lines changed: 62 additions & 32 deletions

File tree

.aspell.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ allowed:
111111
- misconfiguration
112112
- upsert
113113
- upserted
114+
- upserting
114115
- CEL
115116
- httpfilters
116117
- allowlist

k8s/gate/tree/gateway_builder.go

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -202,24 +202,29 @@ func (b *GatewayBuilderImpl) resetListenerConflicts() {
202202
b.ControllerStore.mapPort2Listeners = make(map[gatewayv1.PortNumber]listenerConflict)
203203
}
204204

205-
// checkListenerConflicts checks the conflicts between all Gateway listeners
206-
// See computeListenerConflicts to see how the conflicts are detected
207-
// For now, as there are only a few number of Gateways, we do this check on all Gateway/ all listeners
205+
// checkListenerConflicts recomputes the listener conflict table, then re-upserts
206+
// the Gateways whose conflict verdict changed since the previous cycle.
207+
//
208+
// A conflict is a relational property: it depends on the listeners of the other
209+
// Gateways, so a Gateway can enter or leave the conflict set without any event of
210+
// its own — adding a listener to an older Gateway silently invalidates a younger
211+
// Gateway's listener on the same port. The conflict table is rebuilt for the whole
212+
// tree on every cycle, but the transfer of a verdict onto a listener
213+
// (listener.checkConflict -> CheckConflict -> BuildConditions) only runs for
214+
// Gateways marked StatusUpserted. Re-upserting is what routes an untouched Gateway
215+
// through that single transfer point.
216+
//
217+
// Only Gateways whose verdict actually changed are re-upserted. Selecting every
218+
// Gateway that merely *has* a conflict would re-run all five listener checks — down
219+
// to re-reading the Secrets and PEM-decoding the certificates in
220+
// checkCertificateRefs — on every single cycle for as long as a misconfiguration
221+
// lasts, whatever the event that triggered the cycle.
208222
func (b *GatewayBuilderImpl) checkListenerConflicts() {
209-
oldGwWithPortConflicts := b.previousGatewaysWithPortConflicts()
210-
211-
// Detect new conflicts
223+
// Detect conflicts. The table is rebuilt from scratch and holds an entry for
224+
// every listener of the tree, conflicting or not.
212225
b.computeListenerConflicts()
213226

214-
newGwWithPortConflict := b.gatewaysWithPortConflicts()
215-
oldAndNewGwWithPortConflicts := map[client.ObjectKey]struct{}{}
216-
for gwKey := range newGwWithPortConflict {
217-
oldAndNewGwWithPortConflicts[gwKey] = struct{}{}
218-
}
219-
for gwKey := range oldGwWithPortConflicts {
220-
oldAndNewGwWithPortConflicts[gwKey] = struct{}{}
221-
}
222-
for gwKey := range oldAndNewGwWithPortConflicts {
227+
for gwKey := range b.gatewaysWithChangedConflictVerdict() {
223228
treeGw, ok := b.ControllerStore.GateTree.Gateways[gwKey]
224229
if !ok {
225230
continue
@@ -232,29 +237,53 @@ func (b *GatewayBuilderImpl) checkListenerConflicts() {
232237
}
233238
}
234239

235-
// gatewaysWithPortConflicts returns a map of Gateway keys which have a conflict
236-
func (b *GatewayBuilderImpl) gatewaysWithPortConflicts() map[client.ObjectKey]struct{} {
237-
return gatewaysWithPortConflicts(b.ControllerStore.mapPort2Listeners)
240+
// gatewaysWithChangedConflictVerdict returns the Gateway keys whose conflict
241+
// verdict changed between the previous and the current cycle.
242+
func (b *GatewayBuilderImpl) gatewaysWithChangedConflictVerdict() map[client.ObjectKey]struct{} {
243+
return gatewaysWithChangedConflictVerdict(
244+
b.ControllerStore.previousMapPort2Listeners,
245+
b.ControllerStore.mapPort2Listeners,
246+
)
238247
}
239248

240-
// previousGatewaysWithPortConflicts returns a map of Gateway keys which have a conflict
241-
func (b *GatewayBuilderImpl) previousGatewaysWithPortConflicts() map[client.ObjectKey]struct{} {
242-
return gatewaysWithPortConflicts(b.ControllerStore.previousMapPort2Listeners)
243-
}
244-
245-
// gatewaysWithPortConflicts returns a set of Gateway keys that have at least one listener with a conflict.
246-
func gatewaysWithPortConflicts(mapPort2ListenerConflict map[gatewayv1.PortNumber]listenerConflict) map[client.ObjectKey]struct{} {
249+
// gatewaysWithChangedConflictVerdict returns the set of Gateway keys owning at
250+
// least one listener whose conflict verdict differs between the previous and the
251+
// current conflict table.
252+
//
253+
// The comparison is by value, not by membership of the conflicting set, so it also
254+
// catches a listener that stays conflicted while its reason changes (e.g.
255+
// ProtocolConflict becoming HostnameConflict). Both tables hold an entry for every
256+
// listener, so an absent verdict is a real difference and not a gap: a listener
257+
// that appeared or disappeared belongs to a Gateway that was created, deleted or
258+
// edited, hence already present in ClusterStore.Updates — selecting it is a no-op
259+
// filtered out by the StatusUpserted guard in checkListenerConflicts.
260+
func gatewaysWithChangedConflictVerdict(
261+
previous, current map[gatewayv1.PortNumber]listenerConflict,
262+
) map[client.ObjectKey]struct{} {
247263
gwKeys := map[client.ObjectKey]struct{}{}
248-
for _, conflictMap := range mapPort2ListenerConflict {
249-
for glk, v := range conflictMap {
250-
// If there is a conflict for this listener
251-
if v.hasConflict {
252-
// Compute the Gw key from the listener key
253-
gwKey := ConvertListenerKeyToGatewayKey(glk)
254-
gwKeys[gwKey] = struct{}{}
264+
265+
for port, currentConflicts := range current {
266+
previousConflicts := previous[port]
267+
for listenerKey, currentCondition := range currentConflicts {
268+
previousCondition, existed := previousConflicts[listenerKey]
269+
if existed && previousCondition == currentCondition {
270+
continue
271+
}
272+
gwKeys[ConvertListenerKeyToGatewayKey(listenerKey)] = struct{}{}
273+
}
274+
}
275+
276+
// Listeners that were in the previous table and are gone from the current one.
277+
for port, previousConflicts := range previous {
278+
currentConflicts := current[port]
279+
for listenerKey := range previousConflicts {
280+
if _, stillPresent := currentConflicts[listenerKey]; stillPresent {
281+
continue
255282
}
283+
gwKeys[ConvertListenerKeyToGatewayKey(listenerKey)] = struct{}{}
256284
}
257285
}
286+
258287
return gwKeys
259288
}
260289

0 commit comments

Comments
 (0)