Files
ziti/controller/handler_ctrl/bind.go
T
Paul Lorenz 9595e10cfa Replicate link state over gossip. Fixes #3726
A router reported its links to every controller, and each controller kept its own
picture built only from what routers told it directly. That does not survive
routers being connected to a subset of controllers: a controller learns nothing
about links whose routers it does not hold a connection to.

Link state now lives in the replicated store: a router reports to one controller,
that controller writes the entry it owns, and the mesh carries it to the rest.
Each link entry is owned by the router that dialled it, so two controllers never
contend for the same key, and a controller that has never spoken to a router
still converges on its links.

- registers a link state type on the gossip store and carries link add, update
  and removal through it
- makes a link's source router an atomic and repoints it when the router
  connects, since a link can be built from a gossiped entry before its router has
  connected here, leaving a database-loaded placeholder as the endpoint
- reconciles a reconnecting router's gossip entries, marking its links usable
  again rather than removing them, since a disconnect sets them down instead of
  deleting them
- tombstones a link on disconnect in single-controller mode, where there is no
  peer to learn the removal from
- adds the gossip transport: peer handlers on the controller mesh, router-facing
  gossip handlers, digest exchange off the receive goroutine, and the pools that
  bound apply and I/O work
- has the digest exchange restamp a key the controller holds a higher version
  for, above that version, and send the live value. A router's Lamport clock is
  in memory, so a restart returns it to zero while the controller still holds
  versions from the previous incarnation under the same key. Link metrics are
  keyed by link id alone, and that id belongs to the dialer, so an acceptor's
  restart leaves the key unchanged and its republishes are refused as older.
  Keeping the stored version sends nothing, and every later digest reaches the
  same answer, so the exchange that exists to repair divergence would instead
  hold it in place. Safe because the router is the sole writer of the keys it
  advertises: it takes the clock from a digest but never a value
- advertises a gossip capability so a router reports to one controller only once
  every controller can replicate, and falls back to reporting to all until then
- adds canaries, a per-router sequence carried over the same path, so a router
  can tell that a controller has stopped applying its state
- carries link metrics over gossip alongside the state
- keeps the disconnect teardown's reroute ordering: the currency guard wraps it,
  and inside, the link snapshot and MarkDisconnected stay ahead of the cascade so
  reroute cannot path through the router being removed
2026-09-03 12:39:57 -04:00

197 lines
8.1 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 (
"sync/atomic"
"time"
"github.com/openziti/ziti/v2/common/pb/ctrl_pb"
"github.com/openziti/ziti/v2/controller/model"
"github.com/sirupsen/logrus"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/channel/v5"
"github.com/openziti/channel/v5/latency"
"github.com/openziti/foundation/v2/concurrenz"
"github.com/openziti/metrics"
"github.com/openziti/ziti/v2/common/trace"
"github.com/openziti/ziti/v2/controller/network"
"github.com/openziti/ziti/v2/controller/xctrl"
metrics2 "github.com/openziti/ziti/v2/router/metrics"
)
type bindHandler struct {
heartbeatOptions *channel.HeartbeatOptions
router *model.Router
network *network.Network
xctrls []xctrl.Xctrl
}
func newBindHandler(heartbeatOptions *channel.HeartbeatOptions, router *model.Router, network *network.Network, xctrls []xctrl.Xctrl) channel.BindHandler {
return &bindHandler{
heartbeatOptions: heartbeatOptions,
router: router,
network: network,
xctrls: xctrls,
}
}
func (self *bindHandler) BindChannel(binding channel.Binding) error {
log := pfxlog.Logger().WithFields(map[string]interface{}{
"routerId": self.router.Id,
"routerVersion": self.router.VersionInfo.Version,
})
log.Debug("binding router channel")
channel.AddReceiveHandlers(binding, newAlertHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newCircuitRequestHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newRouteResultHandler(self.network, self.router))
channel.AddReceiveHandlers(binding, newCircuitConfirmationHandler(self.network, self.router))
channel.AddReceiveHandlers(binding, newCreateTerminatorHandler(self.network, self.router))
channel.AddReceiveHandlers(binding, newRemoveTerminatorHandler(self.network, self.router))
channel.AddReceiveHandlers(binding, newRemoveTerminatorsHandler(self.network, self.router))
channel.AddReceiveHandlers(binding, newUpdateTerminatorHandler(self.network, self.router))
channel.AddReceiveHandlers(binding, newLinkStateHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newRouterLinkHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newUpdateLinkListenersHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newVerifyRouterHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newFaultHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newMetricsHandler(self.network))
channel.AddReceiveHandlers(binding, newTraceHandler(self.network.GetTraceController()))
channel.AddReceiveHandlers(binding, newInspectHandler(self.network))
channel.AddReceiveHandlers(binding, newQuiesceRouterHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newDequiesceRouterHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newDecommissionRouterHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newUpdateRouterInterfacesHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newPingHandler())
channel.AddReceiveHandlers(binding, &channel.AsyncFunctionReceiveAdapter{
Type: int32(ctrl_pb.ContentType_ValidateTerminatorsV2ResponseType),
Handler: self.network.RouterMessaging.NewValidationResponseHandler(self.network, self.router),
})
channel.AddReceiveHandlers(binding, newSendClusterMembersHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newCanaryHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newCtrlGossipDeltaHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newCtrlGossipDigestResponseHandler(self.router, self.network))
channel.AddReceiveHandlers(binding, newCtrlGossipDigestRequestHandler(self.router, self.network))
binding.AddPeekHandler(trace.NewChannelPeekHandler(self.network.GetAppId(), binding.GetChannel(), self.network.GetTraceController()))
binding.AddPeekHandler(metrics2.NewCtrlChannelPeekHandler(self.router.Id, self.network.GetMetricsRegistry()))
roundTripHistogram := self.network.GetMetricsRegistry().Histogram("ctrl.latency:" + self.router.Id)
queueTimeHistogram := self.network.GetMetricsRegistry().Histogram("ctrl.queue_time:" + self.router.Id)
binding.AddCloseHandler(channel.CloseHandlerF(func(ch channel.Channel) {
roundTripHistogram.Dispose()
queueTimeHistogram.Dispose()
}))
cb := &heartbeatCallback{
latencyMetric: roundTripHistogram,
queueTimeMetric: queueTimeHistogram,
ch: binding.GetChannel(),
latencySemaphore: concurrenz.NewSemaphore(2),
closeUnresponsiveTimeout: self.heartbeatOptions.CloseUnresponsiveTimeout,
}
cb.lastResponse.Store(time.Now().Add(self.heartbeatOptions.CloseUnresponsiveTimeout * 2).UnixMilli()) // wait at least 2x timeout before closing
channel.ConfigureHeartbeat(binding, self.heartbeatOptions.SendInterval, self.heartbeatOptions.CheckInterval, cb)
xctrlDone := make(chan struct{})
for _, x := range self.xctrls {
if err := x.BindChannel(binding); err != nil {
return err
}
if err := x.Run(binding.GetChannel(), self.network.GetDb(), xctrlDone); err != nil {
return err
}
}
if len(self.xctrls) > 0 {
binding.AddCloseHandler(newXctrlCloseHandler(xctrlDone))
}
startCanaryStatusSender(binding.GetChannel(), self.router, self.network)
binding.AddCloseHandler(newCloseHandler(self.router, self.network))
// Send a gossip digest to the router for each registered store type so it
// can reconcile on reconnect. Run async — old routers won't respond, and
// that's fine.
for _, storeType := range self.network.GossipStoreTypes() {
go sendRouterGossipDigest(binding.GetChannel(), self.router, self.network, storeType)
}
return nil
}
type heartbeatCallback struct {
latencyMetric metrics.Histogram
queueTimeMetric metrics.Histogram
lastResponse atomic.Int64
ch channel.Channel
latencySemaphore concurrenz.Semaphore
closeUnresponsiveTimeout time.Duration
}
func (self *heartbeatCallback) HeartbeatTx(int64) {}
func (self *heartbeatCallback) HeartbeatRx(int64) {}
func (self *heartbeatCallback) HeartbeatRespTx(int64) {}
func (self *heartbeatCallback) HeartbeatRespRx(ts int64) {
now := time.Now()
self.lastResponse.Store(now.UnixMilli())
self.latencyMetric.Update(now.UnixNano() - ts)
}
func (self *heartbeatCallback) timeSinceLastResponse(nowUnixMillis int64) time.Duration {
return time.Duration(nowUnixMillis-self.lastResponse.Load()) * time.Millisecond
}
func (self *heartbeatCallback) CheckHeartBeat() {
now := time.Now().UnixMilli()
if self.timeSinceLastResponse(now) > self.closeUnresponsiveTimeout {
log := self.logger()
log.Error("heartbeat not received in time, closing control channel connection")
if err := self.ch.Close(); err != nil {
log.WithError(err).Error("error while closing control channel connection")
}
}
go self.checkQueueTime()
}
func (self *heartbeatCallback) checkQueueTime() {
if !self.latencySemaphore.TryAcquire() {
self.logger().Warn("unable to check queue time, too many check already running")
return
}
defer self.latencySemaphore.Release()
sendTracker := &latency.SendTimeTracker{
Handler: func(latencyType latency.Type, latency time.Duration) {
self.queueTimeMetric.Update(latency.Nanoseconds())
},
StartTime: time.Now(),
}
if err := self.ch.Send(sendTracker); err != nil && !self.ch.IsClosed() {
self.logger().WithError(err).Error("unable to send queue time tracer")
}
}
func (self *heartbeatCallback) logger() *logrus.Entry {
return pfxlog.Logger().WithField("channelType", "router").WithField("channelId", self.ch.Id())
}