mirror of
https://github.com/openziti/ziti.git
synced 2026-10-24 20:53:03 +00:00
444999bc5b
- adds RemoveTerminatorsV2, an asynchronous control-channel removal, so a router no longer holds a request slot open waiting for a synchronous reply; gated on a new capability so routers fall back to the synchronous path against controllers that do not advertise it - paces establish and remove retries with a per-terminator exponential backoff after a controller rejection, so a rejected operation is not re-sent on the next event-loop tick, which is what let a rate-limited removal sustain itself - routes v2 removes to the leader and skips the ordered delete for a terminator the router confirms it created that no longer exists, so a storm of no-op retries cannot consume the command rate limiter - gates that skip on the leader having applied everything committed when it took leadership, since a leader reports itself elected before those applies finish and would otherwise read a committed create as absent and drop its delete - guards the leader term and every write to the caught-up flag with a dedicated lock, so a barrier completing alongside a leadership change cannot record a result for a term that has already ended - scopes v2 removals to the requesting router, so a router can only remove terminators it owns on the asynchronous path as well as the synchronous one - tracks in-flight removal batches on the router, resolving or requeueing them when the response arrives; lost responses self-heal via rate-limiter expiry and the retry scan - waits for an outstanding operation before sending a terminator delete, and checks that before dequeuing, so a delete neither races a create the controller has committed but not applied nor falls out of the queue until the retry scan finds it minutes later - matches a create response to the attempt that produced it, by control channel and reply sequence, since attempts overlap once one overruns the establishment timeout and every attempt carries the same terminator id; only the outstanding attempt's response may confirm the create or report the terminator idle - records the attempt as the sender assigns its sequence, so a response cannot be processed before the request it answers is known, and withdraws an attempt whose send never queued - reports a removal that succeeds past the establishment timeout as congestion rather than success, matching how the establish path classifies its own latency, since both resolve into the same limiter - extracts the transient-error test both remove handlers share, so the v2 path treats a leaderless cluster as busy like v1 does instead of as a permanent failure, and the two cannot drift again - applies the same delete sequencing and response correlation to the router-hosted (ER/T) registry, which cleared the same in-flight marker unguarded and dropped a deferred delete from its queue rather than leaving it there - bounds the v2 response send with a timeout, so a router that stops draining its send queue cannot accumulate blocked responder goroutines on the controller
133 lines
5.2 KiB
Go
133 lines
5.2 KiB
Go
/*
|
|
Copyright NetFoundry Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package handler_ctrl
|
|
|
|
import (
|
|
"github.com/michaelquigley/pfxlog"
|
|
"github.com/openziti/channel/v5"
|
|
"github.com/openziti/ziti/v2/controller/change"
|
|
"github.com/openziti/ziti/v2/controller/command"
|
|
"github.com/openziti/ziti/v2/controller/model"
|
|
"github.com/openziti/ziti/v2/controller/network"
|
|
"github.com/openziti/ziti/v2/controller/storage/boltz"
|
|
)
|
|
|
|
type baseHandler struct {
|
|
router *model.Router
|
|
network *network.Network
|
|
}
|
|
|
|
func (self *baseHandler) newChangeContext(ch channel.Channel, method string) *change.Context {
|
|
return change.NewControlChannelChange(self.router.Id, self.router.Name, method, ch)
|
|
}
|
|
|
|
// ownsTerminator reports whether terminator belongs to the router on the other end of this handler's
|
|
// control channel. Terminator operations arriving on the fabric control channel are scoped to the
|
|
// requesting router, mirroring the edge control channel's verifyTerminator.
|
|
func (self *baseHandler) ownsTerminator(terminator *model.Terminator) bool {
|
|
return terminator.Router == self.router.Id
|
|
}
|
|
|
|
// lookupTerminatorOwner returns the id of the router that owns terminator id and whether the
|
|
// terminator currently exists. A not-found terminator returns ("", false, nil); other read errors
|
|
// are returned so the caller can default to keeping the id rather than acting on incomplete state.
|
|
func (self *baseHandler) lookupTerminatorOwner(id string) (routerId string, present bool, err error) {
|
|
terminator, err := self.network.Terminator.Read(id)
|
|
if err != nil {
|
|
if boltz.IsErrNotFoundErr(err) {
|
|
return "", false, nil
|
|
}
|
|
return "", false, err
|
|
}
|
|
return terminator.Router, true, nil
|
|
}
|
|
|
|
// wasControllerBusy reports whether err is a transient controller condition, meaning the requester
|
|
// should back off and retry rather than treat the operation as having permanently failed. Rate
|
|
// limiting and a leaderless cluster during a membership change both qualify.
|
|
func wasControllerBusy(err error) bool {
|
|
return command.WasRateLimited(err) || command.WasLeaderless(err)
|
|
}
|
|
|
|
// selectRemovableTerminators returns the terminator ids from a remove request that should be sent
|
|
// through the ordered delete. It drops ids whose terminator is owned by a different router, so a
|
|
// router can only remove terminators it owns, and confirmed-created ids that are already gone, so a
|
|
// storm of no-op retries doesn't consume the raft rate limiter. createConfirmed may be nil when the
|
|
// request carries no confirmation, in which case no id is treated as confirmed.
|
|
func (self *baseHandler) selectRemovableTerminators(ids []string, createConfirmed []bool) []string {
|
|
// Not IsLeader: a leader mid-catch-up can read a committed create as absent.
|
|
leaderCaughtUp := self.network.Dispatcher.IsLeaderAndCaughtUp()
|
|
|
|
kept, rejected, skipped := filterRemovableTerminators(
|
|
ids,
|
|
createConfirmed,
|
|
self.router.Id,
|
|
leaderCaughtUp,
|
|
self.lookupTerminatorOwner,
|
|
)
|
|
|
|
if rejected > 0 {
|
|
pfxlog.Logger().
|
|
WithField("routerId", self.router.Id).
|
|
WithField("rejected", rejected).
|
|
Warn("router attempted to remove terminators it does not own; rejected")
|
|
}
|
|
|
|
if skipped > 0 {
|
|
pfxlog.Logger().
|
|
WithField("routerId", self.router.Id).
|
|
WithField("skipped", skipped).
|
|
WithField("remaining", len(kept)).
|
|
Debug("leader fast-path skipped confirmed already-removed terminators")
|
|
}
|
|
|
|
return kept
|
|
}
|
|
|
|
// filterRemovableTerminators selects which requested terminator ids to send through the ordered
|
|
// (raft) delete for a remove request from requestingRouterId, using lookup to resolve each id's
|
|
// owning router and whether it exists. It drops:
|
|
// - rejected: ids owned by a different router; a router may only remove terminators it owns.
|
|
// - skipped: ids the router confirmed it created (createConfirmed, index-aligned with ids) that no
|
|
// longer exist, so the delete is a confirmed no-op.
|
|
//
|
|
// Skipping requires leaderCaughtUp rather than merely being leader: until a new leader has applied
|
|
// what it committed, absence does not prove the terminator is gone. Everything else is kept,
|
|
// including absent unconfirmed ids and lookup errors, so no delete is dropped on an uncertain read.
|
|
func filterRemovableTerminators(ids []string, createConfirmed []bool, requestingRouterId string, leaderCaughtUp bool, lookup func(string) (routerId string, present bool, err error)) (kept []string, rejected int, skipped int) {
|
|
kept = make([]string, 0, len(ids))
|
|
for i, id := range ids {
|
|
routerId, present, err := lookup(id)
|
|
|
|
if err == nil && present && routerId != requestingRouterId {
|
|
rejected++
|
|
continue
|
|
}
|
|
|
|
if leaderCaughtUp && err == nil && !present {
|
|
if i < len(createConfirmed) && createConfirmed[i] {
|
|
skipped++
|
|
continue
|
|
}
|
|
}
|
|
|
|
kept = append(kept, id)
|
|
}
|
|
|
|
return kept, rejected, skipped
|
|
}
|