mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 16:55:41 +00:00
99ca242ecc
The controller decided which of two racing connections for a router was current
by comparing router instances, but loaded one per connect by evicting the router
cache and reading back through it. Two connects could both evict, and whichever
read second was handed the instance the first had just published. A shared
instance makes the two connections indistinguishable: the connect path cannot
reject the second into an occupied slot, and when either channel dies the
disconnect path finds itself current and tears down the registration the other is
still using. The surviving channel is never re-bound, so the router stays
connected at the transport layer while absent from the model, unable to recover.
Connect and disconnect were also unserialized, so a stale or superseded
disconnect could interleave with a live connection and take its links with it.
- serializes a router's connect and disconnect with a per-router striped lock
- keeps at most one connection per router: a connect into an occupied slot is
rejected via an error from ConnectRouter, so the bind fails and NewChannel
closes it without starting rx or registering it, and the occupant is displaced;
the router redials into the freed slot
- displaces an occupant by closing its channel and also invoking the teardown
directly, since a channel that is already closed never fires its close handler
again; without this a dead but still registered connection holds the slot
forever and every redial is rejected against a slot nothing can free
- refuses a connect whose control channel is already closed rather than
registering it, so a connection no disconnect could ever remove is never
published
- gives every connection its own router instance via RouterManager.NewCtrlChanRouter,
read through readUncached so the cache neither supplies nor receives it, which
is what makes comparing instances meaningful
- moves recording the channel and connect time out of the accept path, so a
caller cannot attach the wrong channel or forget to attach one
- serializes link publication with that teardown on the same per-router stripe.
Validating currency and then publishing without it is a check-then-act: a
report can find the connection current and, by the time it reaches the link
manager, the teardown has already snapshotted and cleared the router's links,
so the link is recreated after everything that would have removed it. It is
then absent from the router's own index while still in the link table with a
disconnected source, and a reconnect reporting the same iteration can adopt
that stale source instead of rebuilding the link
- guards the entire DisconnectRouter teardown by connection currency, all or
nothing, and clears the connected flag and link index only when the
registration was actually given up, with the flag cleared under the same shard
lock as the map removal so the two cannot be observed disagreeing; the connected flag decides whether the
controller accepts a router's link reports, so clearing it for the wrong
connection silences a router that is up and reporting
- reduces MarkConnected to publishing the connection; the takeover-close moves
into ConnectRouter's reject path
- makes the per-router unlock idempotent so callers can defer it as a leak-safety
net and still unlock early before closing a channel outside the lock
- stops the replaced RouterSender in routerTxMap.Add so a takeover does not leak
the old sender's goroutine when the broker's asynchronous RouterDisconnected
loses the race to the redial's RouterConnected
- discards pending peer state changes for a router whose channel has closed,
since sending on one fails immediately and the failed send is retried as soon
as the event loop turns, spinning the loop and flooding the log
- queues the peer-state send-done event on every path, so a missing channel can
no longer leave sendInProgress set and stall that router's updates permanently
- resolves a router's version from its connected instance when validating link
conn info, since the version arrives in the hello and so is absent from an
instance loaded from the database
- normalizes both endpoints to the connected instance in shortestPath, which is
keyed and compared by pointer and so treated an endpoint held as any other
instance of the same router as absent from the graph, reporting a router as
unroutable from itself. That worked before only because the connect path
published its instance into the router cache, so a cache read and the connected
map returned the same object; nothing stated the requirement
- configures test logging once per package in TestMain, so a test no longer
writes global logger state while a previous test's shutdown logging reads it
Original issue: #4196.
(cherry picked from commit 647c4daa1e)
223 lines
6.6 KiB
Go
223 lines
6.6 KiB
Go
package network
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/openziti/ziti/v2/common/pb/ctrl_pb"
|
|
"github.com/openziti/ziti/v2/controller/idgen"
|
|
"github.com/openziti/ziti/v2/controller/model"
|
|
"github.com/openziti/ziti/v2/controller/xt"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
func (network *Network) CreateRouteMessages(path *model.Path, attempt uint32, circuitId string, terminator xt.Terminator, deadline time.Time) []*ctrl_pb.Route {
|
|
var routeMessages []*ctrl_pb.Route
|
|
remainingTime := time.Until(deadline)
|
|
if len(path.Links) == 0 {
|
|
// single router path
|
|
routeMessage := &ctrl_pb.Route{CircuitId: circuitId, Attempt: attempt, Timeout: uint64(remainingTime)}
|
|
routeMessage.Forwards = append(routeMessage.Forwards, &ctrl_pb.Route_Forward{
|
|
SrcAddress: path.IngressId,
|
|
DstAddress: path.EgressId,
|
|
DstType: ctrl_pb.DestType_End,
|
|
})
|
|
routeMessage.Forwards = append(routeMessage.Forwards, &ctrl_pb.Route_Forward{
|
|
SrcAddress: path.EgressId,
|
|
DstAddress: path.IngressId,
|
|
DstType: ctrl_pb.DestType_Start,
|
|
})
|
|
routeMessage.Egress = &ctrl_pb.Route_Egress{
|
|
Binding: terminator.GetBinding(),
|
|
Address: path.EgressId,
|
|
Destination: terminator.GetAddress(),
|
|
}
|
|
routeMessages = append(routeMessages, routeMessage)
|
|
}
|
|
|
|
for i, link := range path.Links {
|
|
if i == 0 {
|
|
// ingress
|
|
routeMessage := &ctrl_pb.Route{CircuitId: circuitId, Attempt: attempt, Timeout: uint64(remainingTime)}
|
|
routeMessage.Forwards = append(routeMessage.Forwards, &ctrl_pb.Route_Forward{
|
|
SrcAddress: path.IngressId,
|
|
DstAddress: link.Id,
|
|
DstType: ctrl_pb.DestType_Link,
|
|
})
|
|
routeMessage.Forwards = append(routeMessage.Forwards, &ctrl_pb.Route_Forward{
|
|
SrcAddress: link.Id,
|
|
DstAddress: path.IngressId,
|
|
DstType: ctrl_pb.DestType_Start,
|
|
})
|
|
routeMessages = append(routeMessages, routeMessage)
|
|
}
|
|
if i >= 0 && i < len(path.Links)-1 {
|
|
// transit
|
|
nextLink := path.Links[i+1]
|
|
routeMessage := &ctrl_pb.Route{CircuitId: circuitId, Attempt: attempt, Timeout: uint64(remainingTime)}
|
|
routeMessage.Forwards = append(routeMessage.Forwards, &ctrl_pb.Route_Forward{
|
|
SrcAddress: link.Id,
|
|
DstAddress: nextLink.Id,
|
|
DstType: ctrl_pb.DestType_Link,
|
|
})
|
|
routeMessage.Forwards = append(routeMessage.Forwards, &ctrl_pb.Route_Forward{
|
|
SrcAddress: nextLink.Id,
|
|
DstAddress: link.Id,
|
|
DstType: ctrl_pb.DestType_Link,
|
|
})
|
|
routeMessages = append(routeMessages, routeMessage)
|
|
}
|
|
if i == len(path.Links)-1 {
|
|
// egress
|
|
routeMessage := &ctrl_pb.Route{CircuitId: circuitId, Attempt: attempt, Timeout: uint64(remainingTime)}
|
|
if attempt != SmartRerouteAttempt {
|
|
routeMessage.Egress = &ctrl_pb.Route_Egress{
|
|
Binding: terminator.GetBinding(),
|
|
Address: path.EgressId,
|
|
Destination: terminator.GetAddress(),
|
|
}
|
|
}
|
|
routeMessage.Forwards = append(routeMessage.Forwards, &ctrl_pb.Route_Forward{
|
|
SrcAddress: path.EgressId,
|
|
DstAddress: link.Id,
|
|
DstType: ctrl_pb.DestType_Link,
|
|
})
|
|
routeMessage.Forwards = append(routeMessage.Forwards, &ctrl_pb.Route_Forward{
|
|
SrcAddress: link.Id,
|
|
DstAddress: path.EgressId,
|
|
DstType: ctrl_pb.DestType_End,
|
|
})
|
|
routeMessages = append(routeMessages, routeMessage)
|
|
}
|
|
}
|
|
return routeMessages
|
|
}
|
|
|
|
func (network *Network) CreatePathWithNodes(nodes []*model.Router) (*model.Path, CircuitError) {
|
|
ingressId, err := idgen.NewUUIDString()
|
|
if err != nil {
|
|
return nil, newCircuitErrWrap(CircuitFailureIdGenerationError, err)
|
|
}
|
|
|
|
egressId, err := idgen.NewUUIDString()
|
|
if err != nil {
|
|
return nil, newCircuitErrWrap(CircuitFailureIdGenerationError, err)
|
|
}
|
|
|
|
path := &model.Path{
|
|
Nodes: nodes,
|
|
IngressId: ingressId,
|
|
EgressId: egressId,
|
|
}
|
|
if err := network.setLinks(path); err != nil {
|
|
return nil, newCircuitErrWrap(CircuitFailurePathMissingLink, err)
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (network *Network) UpdatePath(path *model.Path) (*model.Path, error) {
|
|
srcR := path.Nodes[0]
|
|
dstR := path.Nodes[len(path.Nodes)-1]
|
|
nodes, _, err := network.shortestPath(srcR, dstR)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
path2 := &model.Path{
|
|
Nodes: nodes,
|
|
IngressId: path.IngressId,
|
|
EgressId: path.EgressId,
|
|
InitiatorLocalAddr: path.InitiatorLocalAddr,
|
|
InitiatorRemoteAddr: path.InitiatorRemoteAddr,
|
|
TerminatorLocalAddr: path.TerminatorLocalAddr,
|
|
TerminatorRemoteAddr: path.TerminatorRemoteAddr,
|
|
}
|
|
if err := network.setLinks(path2); err != nil {
|
|
return nil, err
|
|
}
|
|
return path2, nil
|
|
}
|
|
|
|
func (network *Network) shortestPath(srcR *model.Router, dstR *model.Router) ([]*model.Router, int64, error) {
|
|
if srcR == nil || dstR == nil {
|
|
return nil, 0, errors.New("not routable (!srcR||!dstR)")
|
|
}
|
|
|
|
// The graph below is pointer-keyed, so an endpoint held as a different instance of the same router is
|
|
// not a node in it and the search reports the router unroutable from itself. Callers legitimately hold
|
|
// other instances: each connection has its own, and the router cache holds a database-loaded one.
|
|
if connected := network.Router.GetConnected(srcR.Id); connected != nil {
|
|
srcR = connected
|
|
}
|
|
if connected := network.Router.GetConnected(dstR.Id); connected != nil {
|
|
dstR = connected
|
|
}
|
|
|
|
if srcR == dstR {
|
|
return []*model.Router{srcR}, 0, nil
|
|
}
|
|
|
|
dist := make(map[*model.Router]int64)
|
|
prev := make(map[*model.Router]*model.Router)
|
|
unvisited := make(map[*model.Router]bool)
|
|
|
|
for _, r := range network.Router.AllConnected() {
|
|
dist[r] = math.MaxInt32
|
|
unvisited[r] = true
|
|
}
|
|
dist[srcR] = 0
|
|
|
|
minRouterCost := network.options.MinRouterCost
|
|
|
|
for len(unvisited) > 0 {
|
|
u := minCost(unvisited, dist)
|
|
if u == dstR { // if the dest router is the lowest cost next link, we can stop evaluating
|
|
break
|
|
}
|
|
delete(unvisited, u)
|
|
|
|
neighbors := network.Link.ConnectedNeighborsOfRouter(u)
|
|
for _, r := range neighbors {
|
|
if _, found := unvisited[r]; found {
|
|
var cost int64 = math.MaxInt32 + 1
|
|
if l, found := network.Link.LeastExpensiveLink(r, u); found {
|
|
if !r.NoTraversal || r == srcR || r == dstR {
|
|
cost = l.GetCost() + int64(max(r.Cost, minRouterCost))
|
|
}
|
|
}
|
|
|
|
alt := dist[u] + cost
|
|
if alt < dist[r] {
|
|
dist[r] = alt
|
|
prev[r] = u
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* dist: (r2->r1->r0)
|
|
* r0 = 2 <- r1
|
|
* r1 = 1 <- r2
|
|
* r2 = 0 <- nil
|
|
*/
|
|
|
|
routerPath := make([]*model.Router, 0)
|
|
p := prev[dstR]
|
|
for p != nil {
|
|
routerPath = append([]*model.Router{p}, routerPath...)
|
|
p = prev[p]
|
|
}
|
|
routerPath = append(routerPath, dstR)
|
|
|
|
if routerPath[0] != srcR {
|
|
return nil, 0, fmt.Errorf("can't route from %v -> %v", srcR.Id, dstR.Id)
|
|
}
|
|
if routerPath[len(routerPath)-1] != dstR {
|
|
return nil, 0, fmt.Errorf("can't route from %v -> %v. destination unreachable", srcR.Id, dstR.Id)
|
|
}
|
|
|
|
return routerPath, dist[dstR], nil
|
|
}
|