mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 09:56:52 +01:00
178 lines
5.8 KiB
Go
178 lines
5.8 KiB
Go
package nebula
|
|
|
|
import (
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/slackhq/nebula/overlay/tio"
|
|
)
|
|
|
|
// txBudget is the reservation credit of one tunnel, shared by every staged reader sending on it. The low 24 bits of
|
|
// state are outstanding counters, the next 24 spent credit, and the top 16 the sole ordered reader (q+1); owner zero
|
|
// means shared. Readers beyond the owner field's range always take the conservative shared path.
|
|
//
|
|
// A cap on outstanding counters alone is not enough: a fast reader could repeatedly send and reacquire credit,
|
|
// overtaking a paused reader by arbitrarily far. So once readers overlap, or another reader is blocked, spent credit
|
|
// is only recycled when ALL reservations have drained. A sole reader instead gets each ordered completion refunded,
|
|
// its later batches cannot overtake its earlier ones. All transitions are one CAS, without locks.
|
|
//
|
|
// The credit is half the peer's replay window. The other half is left for uncredited sends and transport reordering:
|
|
// inline and control traffic, and relay forwarding through SendVia, which takes counters on the relay tunnel without
|
|
// credit. Unlimited bypass traffic or network delay can still exceed it. The window is not negotiated, so
|
|
// ci.window.length, from the build-time ReplayWindow, is also the assumed peer window.
|
|
type txBudget struct{ state atomic.Uint64 }
|
|
|
|
const txBudgetMask = uint64(1<<24 - 1)
|
|
|
|
// Fail compilation for unsupported build-time windows: a power of two, at least 8192, fitting the credit fields (up
|
|
// to 16777216).
|
|
const (
|
|
_ = uint64(txBudgetMask) - ReplayWindow/2
|
|
_ = uint64(ReplayWindow) - 8192
|
|
_ = uint64(0) - (ReplayWindow & (ReplayWindow - 1))
|
|
)
|
|
|
|
func txBudgetOwner(q int) uint64 {
|
|
if q < 0 || q >= 65535 {
|
|
return 0
|
|
}
|
|
return uint64(q + 1)
|
|
}
|
|
|
|
func (b *txBudget) acquire(n, limit, owner uint64) bool {
|
|
for {
|
|
old := b.state.Load()
|
|
pending, spent, who := old&txBudgetMask, (old>>24)&txBudgetMask, old>>48
|
|
if n > limit || spent > limit-n {
|
|
// A blocked reader ends sole ownership so ordered completions stop refunding credit. Otherwise a busy
|
|
// owner could keep a larger waiter blocked forever without ever sharing a reservation.
|
|
if who != 0 && who != owner && !b.state.CompareAndSwap(old, old&^(uint64(0xffff)<<48)) {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
if pending == 0 {
|
|
who = owner
|
|
} else if who != owner {
|
|
who = 0
|
|
}
|
|
next := who<<48 | (spent+n)<<24 | (pending + n)
|
|
if b.state.CompareAndSwap(old, next) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// release runs only after the socket flush or an explicit drop. A rollback returns credit before any message counter
|
|
// was allocated, as when a later tunnel of a relay or multi-tunnel batch could not acquire its own.
|
|
func (b *txBudget) release(n uint64, rollback bool) {
|
|
for {
|
|
old := b.state.Load()
|
|
pending, spent, owner := old&txBudgetMask, (old>>24)&txBudgetMask, old>>48
|
|
pending -= n
|
|
if rollback || owner != 0 {
|
|
spent -= n
|
|
}
|
|
next := owner<<48 | spent<<24 | pending
|
|
if pending == 0 {
|
|
next = 0
|
|
}
|
|
if b.state.CompareAndSwap(old, next) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ci *ConnectionState) txBudgetLimit() uint64 { return ci.window.length / 2 }
|
|
|
|
func (r *txReserver) oversized() bool {
|
|
for _, t := range r.tunnels {
|
|
if t.n > t.ci.txBudgetLimit() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *txReserver) tryBudget() bool {
|
|
for i, t := range r.tunnels {
|
|
if !t.ci.txBudget.acquire(t.n, t.ci.txBudgetLimit(), r.owner) {
|
|
for _, acquired := range r.tunnels[:i] {
|
|
acquired.ci.txBudget.release(acquired.n, true)
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// waitBudget acquires credit for every tunnel of the batch, never holding a partial acquisition while it waits. Each
|
|
// reader first flushes its own oldest batch, flushes need no credit; with none, another reader holds all outstanding
|
|
// work and can flush it independently. So there is no circular wait, even with relay tunnels acquired in opposite
|
|
// orders. The sleep is only for readers with no local work, not the uncontended hot path. Acquisition is
|
|
// all-or-nothing per read, so pressure on one tunnel also delays packets for idle tunnels in the read; splitting
|
|
// would need separate slot ownership and coupled inner/relay reservations. Reader fairness is not guaranteed.
|
|
func (p *stagedTx) waitBudget() bool {
|
|
if p.tryBudget() {
|
|
return true
|
|
}
|
|
for {
|
|
if p.f.closed.Load() {
|
|
return false
|
|
}
|
|
if !p.flushOldest() {
|
|
time.Sleep(50 * time.Microsecond)
|
|
}
|
|
if p.tryBudget() {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *txSlot) releaseBudget() {
|
|
for _, t := range s.reservations {
|
|
t.ci.txBudget.release(t.n, false)
|
|
}
|
|
clear(s.reservations)
|
|
s.reservations = s.reservations[:0]
|
|
}
|
|
|
|
// sendLarge handles the unusual reads that exceed a whole tunnel's credit, such as tiny-MSS superpackets. It drains
|
|
// the pipeline, then segments and sends them synchronously with credit for each segment, never exceeding the cap and
|
|
// never filtering or running connection manager side effects twice. This deliberately slow path avoids a lone-batch
|
|
// exception to the shared invariant.
|
|
func (p *stagedTx) sendLarge(items []txItem) {
|
|
for p.flushOldest() {
|
|
}
|
|
for _, it := range items {
|
|
err := tio.SegmentSuperpacket(it.pkt, func(seg []byte) error {
|
|
if p.f.closed.Load() {
|
|
return nil
|
|
}
|
|
part := it
|
|
part.pkt, part.segs = tio.Packet{Bytes: seg}, 1
|
|
p.tunnels = p.tunnels[:0]
|
|
part.counter = p.need(part.hostinfo, part.ci, 1)
|
|
if part.relayHI != nil {
|
|
part.relayCounter = p.need(part.relayHI, part.relayHI.ConnectionState, 1)
|
|
}
|
|
if !p.waitBudget() {
|
|
return nil
|
|
}
|
|
one := [1]txItem{part}
|
|
if reserved := p.reserve(one[:]); len(reserved) != 0 {
|
|
p.f.encryptTxItem(&reserved[0], p.nb, p.sb)
|
|
p.f.flushSendBatch(p.sb, p.q)
|
|
}
|
|
for _, t := range p.tunnels {
|
|
t.ci.txBudget.release(t.n, false)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
it.hostinfo.logger(p.f.l).Error("Failed to segment superpacket for send", "error", err)
|
|
}
|
|
}
|
|
clear(items)
|
|
}
|