flow_budget.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. package nebula
  2. import (
  3. "sync/atomic"
  4. "time"
  5. "github.com/slackhq/nebula/overlay/tio"
  6. )
  7. // txBudget is the reservation credit of one tunnel, shared by every staged reader sending on it. The low 24 bits of
  8. // state are outstanding counters, the next 24 spent credit, and the top 16 the sole ordered reader (q+1); owner zero
  9. // means shared. Readers beyond the owner field's range always take the conservative shared path.
  10. //
  11. // A cap on outstanding counters alone is not enough: a fast reader could repeatedly send and reacquire credit,
  12. // overtaking a paused reader by arbitrarily far. So once readers overlap, or another reader is blocked, spent credit
  13. // is only recycled when ALL reservations have drained. A sole reader instead gets each ordered completion refunded,
  14. // its later batches cannot overtake its earlier ones. All transitions are one CAS, without locks.
  15. //
  16. // The credit is half the peer's replay window. The other half is left for uncredited sends and transport reordering:
  17. // inline and control traffic, and relay forwarding through SendVia, which takes counters on the relay tunnel without
  18. // credit. Unlimited bypass traffic or network delay can still exceed it. The window is not negotiated, so
  19. // ci.window.length, from the build-time ReplayWindow, is also the assumed peer window.
  20. type txBudget struct{ state atomic.Uint64 }
  21. const txBudgetMask = uint64(1<<24 - 1)
  22. // Fail compilation for unsupported build-time windows: a power of two, at least 8192, fitting the credit fields (up
  23. // to 16777216).
  24. const (
  25. _ = uint64(txBudgetMask) - ReplayWindow/2
  26. _ = uint64(ReplayWindow) - 8192
  27. _ = uint64(0) - (ReplayWindow & (ReplayWindow - 1))
  28. )
  29. func txBudgetOwner(q int) uint64 {
  30. if q < 0 || q >= 65535 {
  31. return 0
  32. }
  33. return uint64(q + 1)
  34. }
  35. func (b *txBudget) acquire(n, limit, owner uint64) bool {
  36. for {
  37. old := b.state.Load()
  38. pending, spent, who := old&txBudgetMask, (old>>24)&txBudgetMask, old>>48
  39. if n > limit || spent > limit-n {
  40. // A blocked reader ends sole ownership so ordered completions stop refunding credit. Otherwise a busy
  41. // owner could keep a larger waiter blocked forever without ever sharing a reservation.
  42. if who != 0 && who != owner && !b.state.CompareAndSwap(old, old&^(uint64(0xffff)<<48)) {
  43. continue
  44. }
  45. return false
  46. }
  47. if pending == 0 {
  48. who = owner
  49. } else if who != owner {
  50. who = 0
  51. }
  52. next := who<<48 | (spent+n)<<24 | (pending + n)
  53. if b.state.CompareAndSwap(old, next) {
  54. return true
  55. }
  56. }
  57. }
  58. // release runs only after the socket flush or an explicit drop. A rollback returns credit before any message counter
  59. // was allocated, as when a later tunnel of a relay or multi-tunnel batch could not acquire its own.
  60. func (b *txBudget) release(n uint64, rollback bool) {
  61. for {
  62. old := b.state.Load()
  63. pending, spent, owner := old&txBudgetMask, (old>>24)&txBudgetMask, old>>48
  64. pending -= n
  65. if rollback || owner != 0 {
  66. spent -= n
  67. }
  68. next := owner<<48 | spent<<24 | pending
  69. if pending == 0 {
  70. next = 0
  71. }
  72. if b.state.CompareAndSwap(old, next) {
  73. return
  74. }
  75. }
  76. }
  77. func (ci *ConnectionState) txBudgetLimit() uint64 { return ci.window.length / 2 }
  78. func (r *txReserver) oversized() bool {
  79. for _, t := range r.tunnels {
  80. if t.n > t.ci.txBudgetLimit() {
  81. return true
  82. }
  83. }
  84. return false
  85. }
  86. func (r *txReserver) tryBudget() bool {
  87. for i, t := range r.tunnels {
  88. if !t.ci.txBudget.acquire(t.n, t.ci.txBudgetLimit(), r.owner) {
  89. for _, acquired := range r.tunnels[:i] {
  90. acquired.ci.txBudget.release(acquired.n, true)
  91. }
  92. return false
  93. }
  94. }
  95. return true
  96. }
  97. // waitBudget acquires credit for every tunnel of the batch, never holding a partial acquisition while it waits. Each
  98. // reader first flushes its own oldest batch, flushes need no credit; with none, another reader holds all outstanding
  99. // work and can flush it independently. So there is no circular wait, even with relay tunnels acquired in opposite
  100. // orders. The sleep is only for readers with no local work, not the uncontended hot path. Acquisition is
  101. // all-or-nothing per read, so pressure on one tunnel also delays packets for idle tunnels in the read; splitting
  102. // would need separate slot ownership and coupled inner/relay reservations. Reader fairness is not guaranteed.
  103. func (p *stagedTx) waitBudget() (time.Duration, bool) {
  104. if p.tryBudget() {
  105. return 0, true
  106. }
  107. t := time.Now()
  108. for {
  109. if p.f.closed.Load() {
  110. return time.Since(t), false
  111. }
  112. if !p.flushOldest() {
  113. time.Sleep(50 * time.Microsecond)
  114. }
  115. if p.tryBudget() {
  116. return time.Since(t), true
  117. }
  118. }
  119. }
  120. func (s *txSlot) releaseBudget() {
  121. for _, t := range s.reservations {
  122. t.ci.txBudget.release(t.n, false)
  123. }
  124. clear(s.reservations)
  125. s.reservations = s.reservations[:0]
  126. }
  127. // sendLarge handles the unusual reads that exceed a whole tunnel's credit, such as tiny-MSS superpackets. It drains
  128. // the pipeline, then segments and sends them synchronously with credit for each segment, never exceeding the cap and
  129. // never filtering or running connection manager side effects twice. This deliberately slow path avoids a lone-batch
  130. // exception to the shared invariant.
  131. func (p *stagedTx) sendLarge(items []txItem) time.Duration {
  132. t := time.Now()
  133. for p.flushOldest() {
  134. }
  135. stall := time.Since(t)
  136. for _, it := range items {
  137. err := tio.SegmentSuperpacket(it.pkt, func(seg []byte) error {
  138. if p.f.closed.Load() {
  139. return nil
  140. }
  141. part := it
  142. part.pkt, part.segs = tio.Packet{Bytes: seg}, 1
  143. p.tunnels = p.tunnels[:0]
  144. part.counter = p.need(part.hostinfo, part.ci, 1)
  145. if part.relayHI != nil {
  146. part.relayCounter = p.need(part.relayHI, part.relayHI.ConnectionState, 1)
  147. }
  148. d, ok := p.waitBudget()
  149. stall += d
  150. if !ok {
  151. return nil
  152. }
  153. one := [1]txItem{part}
  154. if reserved := p.reserve(one[:]); len(reserved) != 0 {
  155. p.f.encryptTxItem(&reserved[0], p.nb, p.sb)
  156. p.f.flushSendBatch(p.sb, p.q)
  157. }
  158. for _, t := range p.tunnels {
  159. t.ci.txBudget.release(t.n, false)
  160. }
  161. return nil
  162. })
  163. if err != nil {
  164. it.hostinfo.logger(p.f.l).Error("Failed to segment superpacket for send", "error", err)
  165. }
  166. }
  167. clear(items)
  168. return stall
  169. }