◐ Off-By-One · answer catalog

go-20261007-clock-skewed-multipath-congestion-recovery

1 answer(s)gogo1.26

A naive multipath transport that estimates per-path delivery rate and RTT from raw ACK timing fails in five ways:

📦 Source in repository (JSON)

Answer

The complete, tested solution is written to ~/multipath-scheduler/SOLUTION.md and reproduced below.

# Fix: Multipath transport scheduler with robust per-path estimation and burst-free migration

## Root-cause analysis

A naive multipath transport that estimates per-path delivery rate and RTT from raw ACK timing fails in five ways:

1. **Asymmetric clock skew.** If RTT or delivery rate are computed by subtracting a remote (receiver) timestamp from a local (sender) timestamp, the result contains an unbounded offset and a drifting rate error. The error differs per path because each path has its own remote timestamp stream.
2. **Intermittent ACK compression.** When several ACKs are delivered in a burst, per-ACK inter-arrival rate samples can claim a bandwidth far above the true path rate. A single compressed burst can make a slow path look fast and push traffic onto it, creating a queue and, on migration, a congestion burst.
3. **Packet reordering and stale samples.** ACKs can arrive out of order. Late ACKs can carry old send timestamps, and duplicates can double-count delivered bytes. Naive estimators accept these and corrupt `minRTT`, `SRTT`, and the delivered counter.
4. **Counter wraparound.** Packet sequence numbers and cumulative-delivered counters are finite. Comparing them with `<` or subtracting them with signed arithmetic produces huge, wrong deltas after wrap.
5. **Migration burst.** When a path fails, its in-flight bytes are lost. When it recovers, a scheduler that immediately gives it the full share of traffic can inject an unacknowledged burst larger than the path's congestion window.

## Exact fix

The fix has five parts, all present in the code below.

1. **Use only the sender's monotonic clock for RTT and rate.** The ACK echoes the sender's `SendTime`; the estimator never subtracts receiver timestamps. This makes the estimator immune to asymmetric clock skew without a separate skew filter.
2. **Packet-timed delivery-rate samples.** `rate = delivered_bytes_since_send / time_since_send`. The denominator comes from the local send clock, so ACK compression cannot make the denominator artificially small. A one-sample-per-`MinSampleInterval` gate and a 75th-percentile window with a 1.5x clamp make the estimate robust to intermittent compression bursts.
3. **Reordering and stale-sample filters.** Duplicate ACKs are ignored by the scheduler's packet map. Karn's algorithm skips retransmitted packets for RTT. Samples older than `MaxSampleAge` are rejected. RTT uses the RFC 6298 SRTT/RTTVAR filter.
4. **Wrap-safe math.** `wrapDiff`, `seqAfter`, and `seqAfterEq` use unsigned arithmetic with an `int64` comparison so modulo-2^64 counters and sequence numbers behave correctly.
5. **ACK-clocked recovery credit.** A failed path drops out of service and releases its in-flight budget. On recovery it starts with `InitialCwnd` and a `migrationCredit` budget; each ACK adds back at most one segment (capped by `MigrationBurst`). The scheduler also ramps its WFQ weight from 0 to 1 over `MigrationRamp`. Traffic therefore migrates to the recovered path only as ACKs open the window, never as a burst.

## Source

### `go.mod`

```go
module example.com/multipath

go 1.26
```

### `wrap.go`

```go
package multipath

// wrapDiff returns the forward difference between two monotonically
// increasing counters that may wrap at 2^64.  The caller must ensure the
// distance is smaller than 2^63; this is true for all sane ACK/sequence
// windows.
func wrapDiff(newer, older uint64) uint64 {
    return newer - older
}

// seqAfter reports whether a is strictly newer than b in modulo-2^64
// sequence space.  Comparing int64(a-b) > 0 is the standard wrap-safe
// comparison used by TCP-like protocols.
func seqAfter(a, b uint64) bool {
    return int64(a-b) > 0
}

// seqAfterEq is the inclusive variant of seqAfter.
func seqAfterEq(a, b uint64) bool {
    return int64(a-b) >= 0
}
```

### `estimator.go`

```go
package multipath

import (
    "math"
    "sort"
    "time"
)

// EstimatorConfig controls the robustness filters of the per-path estimator.
type EstimatorConfig struct {
    // MinSampleInterval prevents a burst of compressed ACKs from creating
    // many delivery-rate samples.  At most one rate sample is accepted per
    // interval.
    MinSampleInterval time.Duration

    // MaxSampleAge rejects samples whose originating packet is too old
    // (lost/reordered for a long time, or a stale ACK after recovery).
    MaxSampleAge time.Duration

    // RateWindow is the sliding window over which the robust delivery rate
    // is computed.
    RateWindow time.Duration

    // MaxRTT rejects implausibly large RTT samples.
    MaxRTT time.Duration

    // MinSampleBytes rejects tiny delivery-rate samples (e.g. delayed ACKs
    // of one small packet) that would be dominated by timer noise.
    MinSampleBytes uint64
}

// DefaultEstimatorConfig returns conservative defaults suitable for a
// datacenter/WAN transport.
func DefaultEstimatorConfig() EstimatorConfig {
    return EstimatorConfig{
        MinSampleInterval: time.Millisecond,
        MaxSampleAge:      10 * time.Second,
        RateWindow:        time.Second,
        MaxRTT:            10 * time.Second,
        MinSampleBytes:    1,
    }
}

// SentPacket is the immutable sender-side record needed to compute robust
// RTT and delivery-rate samples.  SendTime is always taken from the local
// monotonic sender clock.  Echoing this timestamp in the ACK is what makes
// both estimators immune to asymmetric remote clock skew: we never subtract
// two timestamps produced by different clocks.
type SentPacket struct {
    PathID          string
    Seq             uint64
    Size            uint64
    SendTime        time.Time
    DeliveredAtSend uint64
    Retransmitted   bool
}

type rateSample struct {
    at   time.Time
    rate float64
}

// Estimator tracks RTT and delivery rate for one path.
//
// RTT follows the RFC 6298 SRTT/RTTVAR filter.  Delivery rate follows the
// "packet-timed" sampling idea from BBR: rate = delivered bytes / time
// since the *send* time of the packet that was just acknowledged.  Because
// the denominator is built from local send timestamps, ACK compression
// cannot inflate the sample, and because we add an explicit accept-one-per
// interval gate, intermittent ACK compression cannot create a flood of
// samples.
type Estimator struct {
    cfg EstimatorConfig

    // RFC 6298 RTT state.
    srtt    time.Duration
    rttvar  time.Duration
    minRTT  time.Duration
    haveRTT bool

    // Delivery-rate state.
    delivered      uint64
    rate           float64
    haveRate       bool
    samples        []rateSample
    lastRateSample time.Time
}

// NewEstimator returns an Estimator with the supplied configuration.
func NewEstimator(cfg EstimatorConfig) *Estimator {
    if cfg.MinSampleInterval <= 0 {
        cfg.MinSampleInterval = DefaultEstimatorConfig().MinSampleInterval
    }
    if cfg.MaxSampleAge <= 0 {
        cfg.MaxSampleAge = DefaultEstimatorConfig().MaxSampleAge
    }
    if cfg.RateWindow <= 0 {
        cfg.RateWindow = DefaultEstimatorConfig().RateWindow
    }
    if cfg.MaxRTT <= 0 {
        cfg.MaxRTT = DefaultEstimatorConfig().MaxRTT
    }
    if cfg.MinSampleBytes == 0 {
        cfg.MinSampleBytes = 1
    }
    return &Estimator{cfg: cfg}
}

// SRTT returns the smoothed RTT estimate, or 0 when unknown.
func (e *Estimator) SRTT() time.Duration { return e.srtt }

// RTTVar returns the RTT variation estimate.
func (e *Estimator) RTTVar() time.Duration { return e.rttvar }

// MinRTT returns the minimum observed RTT, or 0 when unknown.
func (e *Estimator) MinRTT() time.Duration { return e.minRTT }

// Rate returns the robust delivery rate in bytes per second, or 0 when
// unknown.
func (e *Estimator) Rate() float64 { return e.rate }

// Delivered returns the cumulative bytes delivered on the path.  It is a
// local counter and therefore does not depend on receiver clock or receiver
// counter representation.
func (e *Estimator) Delivered() uint64 { return e.delivered }

// SampleCount returns the number of rate samples currently retained.
func (e *Estimator) SampleCount() int { return len(e.samples) }

// OnAck feeds one newly acknowledged packet to the estimator.
//
// The caller must only call this for a *new* ACK (duplicate ACKs and
// retransmission ACKs are ignored) and must pass a timestamp from the same
// monotonic sender clock that was used for SendTime.
func (e *Estimator) OnAck(now time.Time, pkt *SentPacket) {
    if pkt == nil {
        return
    }
    rtt := now.Sub(pkt.SendTime)

    // ---- RTT: Karn's algorithm + RFC 6298 ----
    // Stale RTT samples (e.g. an old packet finally ACKed after a path
    // recovered) are rejected so they cannot pull minRTT/SRTT backwards.
    if !pkt.Retransmitted && rtt > 0 && rtt <= e.cfg.MaxRTT && rtt <= e.cfg.MaxSampleAge {
        if !e.haveRTT {
            e.srtt = rtt
            e.rttvar = rtt / 2
            e.minRTT = rtt
            e.haveRTT = true
        } else {
            delta := e.srtt - rtt
            if delta < 0 {
                delta = -delta
            }
            e.rttvar = (3*e.rttvar + delta) / 4
            e.srtt = (7*e.srtt + rtt) / 8
            if rtt < e.minRTT {
                e.minRTT = rtt
            }
        }
    }

    // ---- Delivery rate ----
    e.delivered += pkt.Size // uint64 arithmetic intentionally wraps.

    age := now.Sub(pkt.SendTime)
    if age > e.cfg.MaxSampleAge || pkt.Size == 0 {
        return // stale sample: do not let it pollute the rate window
    }

    // Never accept more than one rate sample per MinSampleInterval.  When
    // several ACKs are compressed together, the first one is the most
    // conservative (it covers the fewest delivered bytes).
    if !e.lastRateSample.IsZero() && now.Sub(e.lastRateSample) < e.cfg.MinSampleInterval {
        return
    }

    bytes := wrapDiff(e.delivered, pkt.DeliveredAtSend)
    if bytes < e.cfg.MinSampleBytes {
        return
    }
    if age < e.cfg.MinSampleInterval {
        return
    }

    sample := float64(bytes) / age.Seconds()
    if math.IsInf(sample, 0) || math.IsNaN(sample) || sample < 0 {
        return
    }
    e.samples = append(e.samples, rateSample{at: now, rate: sample})
    e.lastRateSample = now
    e.prune(now)
    e.rate = e.robustRate()
    e.haveRate = true
}

func (e *Estimator) prune(now time.Time) {
    cutoff := now.Add(-e.cfg.RateWindow)
    keep := e.samples[:0]
    for _, s := range e.samples {
        if !s.at.Before(cutoff) {
            keep = append(keep, s)
        }
    }
    e.samples = keep
}

// robustRate returns the 75th percentile of the samples in the current
// window, clamped so that a single compressed-ACK sample cannot increase
// the previous estimate by more than 1.5x.  Percentile rather than max is
// deliberate: it makes intermittent compression outliers visible but not
// dominant.
func (e *Estimator) robustRate() float64 {
    if len(e.samples) == 0 {
        return e.rate
    }
    rates := make([]float64, len(e.samples))
    best := 0.0
    for i, s := range e.samples {
        rates[i] = s.rate
        if s.rate > best {
            best = s.rate
        }
    }
    if !e.haveRate || e.rate <= 0 {
        return best
    }
    sort.Float64s(rates)
    idx := int(float64(len(rates)-1) * 0.75)
    v := rates[idx]
    if v > e.rate*1.5 {
        v = e.rate * 1.5
    }
    return v
}
```

### `scheduler.go`

```go
package multipath

import (
    "sync"
    "time"
)

// SchedulerConfig configures the multipath scheduler and its safety limits.
type SchedulerConfig struct {
    // MaxAggregateInflight is the hard aggregate cap across all paths.
    MaxAggregateInflight uint64

    // MaxPathInflight is the default per-path hard cap.  A path added with
    // a non-zero limit overrides it.
    MaxPathInflight uint64

    // InitialCwnd is the congestion window used for a new/recovered path.
    InitialCwnd uint64

    // MinCwnd is the lower bound after a loss.
    MinCwnd uint64

    // MaxCwnd is the upper bound for a path.
    MaxCwnd uint64

    // MSS is used for congestion-window arithmetic and as the default WFQ
    // weight when no delivery-rate estimate exists.
    MSS uint64

    // MigrationRamp is how long a recovered path takes to regain full
    // scheduler weight.
    MigrationRamp time.Duration

    // MigrationBurst caps the extra ACK-clocked in-flight credit granted
    // while a recovered path is ramping.
    MigrationBurst uint64

    // Estimator configures each path's estimator.
    Estimator EstimatorConfig
}

// DefaultSchedulerConfig returns a usable set of defaults.
func DefaultSchedulerConfig() SchedulerConfig {
    mss := uint64(1200)
    return SchedulerConfig{
        MaxAggregateInflight: 1 << 20,
        MaxPathInflight:      1 << 20,
        InitialCwnd:          2 * mss,
        MinCwnd:              mss,
        MaxCwnd:              1 << 20,
        MSS:                  mss,
        MigrationRamp:        100 * time.Millisecond,
        MigrationBurst:       4 * mss,
        Estimator:            DefaultEstimatorConfig(),
    }
}

// Path is one transport path tracked by the scheduler.
type Path struct {
    ID string

    est Estimator

    sent    map[uint64]*SentPacket
    nextSeq uint64

    cwnd      uint64
    ssthresh  uint64
    slowStart bool
    inflight  uint64
    limit     uint64

    available bool

    // Recovery/migration state.  While ramping, migrationCredit is an
    // additional ACK-clocked in-flight budget, and the WFQ weight is
    // linearly ramped from 0 to 1 so that the recovered path does not
    // immediately attract a large share of new packets.
    ramping         bool
    rampStart       time.Time
    migrationCredit uint64

    // Weighted fair queueing virtual time.  Weight = estimated delivery
    // rate, so a higher-rate path is selected more often.
    vtime float64
}

// SendToken identifies a packet granted by Next.  It is the only object the
// caller needs to ACK or mark lost.
type SendToken struct {
    PathID   string
    Seq      uint64
    Size     uint64
    SendTime time.Time
}

// Scheduler owns all paths, the aggregate in-flight budget, and the
// per-path estimators.
type Scheduler struct {
    mu  sync.Mutex
    cfg SchedulerConfig
    now func() time.Time

    paths  []*Path
    byID   map[string]*Path
    agg    uint64 // aggregate bytes currently in flight
    forced int
}

// NewScheduler returns a scheduler using time.Now as its clock.
func NewScheduler(cfg SchedulerConfig) *Scheduler {
    return newScheduler(cfg, time.Now)
}

func newScheduler(cfg SchedulerConfig, now func() time.Time) *Scheduler {
    if cfg.MSS == 0 {
        cfg.MSS = DefaultSchedulerConfig().MSS
    }
    if cfg.InitialCwnd == 0 {
        cfg.InitialCwnd = DefaultSchedulerConfig().InitialCwnd
    }
    if cfg.MinCwnd == 0 {
        cfg.MinCwnd = DefaultSchedulerConfig().MinCwnd
    }
    if cfg.MaxCwnd == 0 {
        cfg.MaxCwnd = DefaultSchedulerConfig().MaxCwnd
    }
    if cfg.MaxAggregateInflight == 0 {
        cfg.MaxAggregateInflight = DefaultSchedulerConfig().MaxAggregateInflight
    }
    if cfg.MaxPathInflight == 0 {
        cfg.MaxPathInflight = DefaultSchedulerConfig().MaxPathInflight
    }
    if cfg.MigrationBurst == 0 {
        cfg.MigrationBurst = cfg.InitialCwnd
    }
    return &Scheduler{
        cfg:  cfg,
        now:  now,
        byID: make(map[string]*Path),
    }
}

// AddPath registers a path.  A zero limit means use MaxPathInflight.
func (s *Scheduler) AddPath(id string, limit uint64) *Path {
    s.mu.Lock()
    defer s.mu.Unlock()
    if p, ok := s.byID[id]; ok {
        return p
    }
    if limit == 0 {
        limit = s.cfg.MaxPathInflight
    }
    p := &Path{
        ID:        id,
        est:       *NewEstimator(s.cfg.Estimator),
        sent:      make(map[uint64]*SentPacket),
        cwnd:      s.cfg.InitialCwnd,
        ssthresh:  s.cfg.MaxCwnd,
        slowStart: true,
        limit:     limit,
        available: true,
    }
    s.paths = append(s.paths, p)
    s.byID[id] = p
    return p
}

// RemovePath unregisters a path.
func (s *Scheduler) RemovePath(id string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    p, ok := s.byID[id]
    if !ok {
        return
    }
    delete(s.byID, id)
    out := s.paths[:0]
    for _, q := range s.paths {
        if q != p {
            out = append(out, q)
        }
    }
    s.paths = out
    // Treat outstanding bytes as lost so the aggregate budget is not
    // permanently reduced.
    for seq, pkt := range p.sent {
        p.release(pkt.Size)
        s.agg = release(s.agg, pkt.Size)
        delete(p.sent, seq)
    }
}

// MarkDown takes a path out of service.  Outstanding bytes are dropped
// immediately (a failed path cannot deliver them) so aggregate in-flight is
// not stuck.
func (s *Scheduler) MarkDown(id string) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    p, ok := s.byID[id]
    if !ok {
        return false
    }
    p.available = false
    p.ramping = false
    p.vtime = 0
    for seq, pkt := range p.sent {
        s.agg = release(s.agg, pkt.Size)
        delete(p.sent, seq)
    }
    p.inflight = 0
    return true
}

// MarkUp returns a path to service and starts the anti-burst ramp.
func (s *Scheduler) MarkUp(id string) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    p, ok := s.byID[id]
    if !ok {
        return false
    }
    p.available = true
    p.cwnd = s.cfg.InitialCwnd
    p.ssthresh = s.cfg.MaxCwnd
    p.slowStart = true
    p.ramping = true
    p.rampStart = s.now()
    p.migrationCredit = s.cfg.InitialCwnd
    if p.migrationCredit > s.cfg.MigrationBurst {
        p.migrationCredit = s.cfg.MigrationBurst
    }
    p.vtime = 0
    return true
}

// SetAggregateLimit changes the aggregate in-flight cap.
func (s *Scheduler) SetAggregateLimit(n uint64) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.cfg.MaxAggregateInflight = n
}

// SetPathLimit changes a path's hard in-flight cap.
func (s *Scheduler) SetPathLimit(id string, n uint64) {
    s.mu.Lock()
    defer s.mu.Unlock()
    if p, ok := s.byID[id]; ok {
        p.limit = n
    }
}

// AggregateInflight returns current aggregate bytes in flight.
func (s *Scheduler) AggregateInflight() uint64 {
    s.mu.Lock()
    defer s.mu.Unlock()
    return s.agg
}

// PathInflight returns a path's current in-flight bytes.
func (s *Scheduler) PathInflight(id string) uint64 {
    s.mu.Lock()
    defer s.mu.Unlock()
    if p, ok := s.byID[id]; ok {
        return p.inflight
    }
    return 0
}

// Next chooses a path and reserves up to want bytes of in-flight budget.
// The returned token must later be passed to OnAck or OnLoss.
func (s *Scheduler) Next(now time.Time, want uint64) (*SendToken, bool) {
    if want == 0 {
        return nil, false
    }
    s.mu.Lock()
    defer s.mu.Unlock()

    if s.agg >= s.cfg.MaxAggregateInflight {
        return nil, false
    }
    if room := s.cfg.MaxAggregateInflight - s.agg; want > room {
        want = room
    }

    var best *Path
    for _, p := range s.paths {
        if !p.available {
            continue
        }
        p.refresh(now, s.cfg)
        if p.allowance(s.cfg, s.agg) == 0 {
            continue
        }
        if best == nil || p.vtime < best.vtime ||
            (p.vtime == best.vtime && p.ID < best.ID) {
            best = p
        }
    }
    if best == nil {
        return nil, false
    }

    n := best.allowance(s.cfg, s.agg)
    if n > want {
        n = want
    }
    if n == 0 {
        return nil, false
    }

    best.inflight += n
    s.agg += n
    if best.ramping {
        if n >= best.migrationCredit {
            best.migrationCredit = 0
        } else {
            best.migrationCredit -= n
        }
    }

    seq := best.nextSeq
    best.nextSeq++ // wraps naturally
    pkt := &SentPacket{
        PathID:          best.ID,
        Seq:             seq,
        Size:            n,
        SendTime:        now,
        DeliveredAtSend: best.est.Delivered(),
    }
    best.sent[seq] = pkt

    // Weighted fair queueing: update virtual time using the current rate
    // estimate.  During recovery, ramp the weight from 0 to 1.
    w := best.est.Rate()
    if w <= 0 {
        w = float64(s.cfg.MSS)
    }
    if best.ramping {
        frac := 1.0
        if s.cfg.MigrationRamp > 0 {
            frac = float64(now.Sub(best.rampStart)) / float64(s.cfg.MigrationRamp)
            if frac > 1 {
                frac = 1
            }
            if frac < 0 {
                frac = 0
            }
        }
        w *= frac
    }
    if w <= 0 {
        w = 1
    }
    best.vtime += float64(n) / w

    return &SendToken{PathID: pkt.PathID, Seq: pkt.Seq, Size: pkt.Size, SendTime: pkt.SendTime}, true
}

// OnAck processes a new ACK for a granted token.  It returns false for
// duplicate/stale ACKs (token not found), which is important when a path
// was failed and later recovered.
func (s *Scheduler) OnAck(now time.Time, tok *SendToken) bool {
    if tok == nil {
        return false
    }
    s.mu.Lock()
    defer s.mu.Unlock()
    p, ok := s.byID[tok.PathID]
    if !ok {
        return false
    }
    pkt, ok := p.sent[tok.Seq]
    if !ok {
        return false
    }
    delete(p.sent, tok.Seq)
    p.release(pkt.Size)
    s.agg = release(s.agg, pkt.Size)

    // Estimator uses only sender-side monotonic timestamps.
    p.est.OnAck(now, pkt)

    // Anti-burst recovery credit: only ACK-clocked bytes are added back.
    if p.ramping {
        p.migrationCredit += pkt.Size
        if p.migrationCredit > s.cfg.MigrationBurst {
            p.migrationCredit = s.cfg.MigrationBurst
        }
        if now.Sub(p.rampStart) >= s.cfg.MigrationRamp {
            p.ramping = false
        }
    }

    p.onAckCwnd(s.cfg, pkt.Size)
    return true
}

// OnLoss releases in-flight budget and applies multiplicative decrease.
func (s *Scheduler) OnLoss(tok *SendToken) bool {
    if tok == nil {
        return false
    }
    s.mu.Lock()
    defer s.mu.Unlock()
    p, ok := s.byID[tok.PathID]
    if !ok {
        return false
    }
    pkt, ok := p.sent[tok.Seq]
    if !ok {
        return false
    }
    delete(p.sent, tok.Seq)
    p.release(pkt.Size)
    s.agg = release(s.agg, pkt.Size)

    p.slowStart = false
    p.ssthresh = p.cwnd / 2
    if p.ssthresh < s.cfg.MinCwnd {
        p.ssthresh = s.cfg.MinCwnd
    }
    p.cwnd = p.ssthresh
    return true
}

func (p *Path) refresh(now time.Time, cfg SchedulerConfig) {
    if p.ramping && now.Sub(p.rampStart) >= cfg.MigrationRamp {
        p.ramping = false
    }
}

func (p *Path) allowance(cfg SchedulerConfig, agg uint64) uint64 {
    if p.inflight >= p.limit || p.inflight >= p.cwnd {
        return 0
    }
    allow := p.cwnd - p.inflight
    if lim := p.limit - p.inflight; allow > lim {
        allow = lim
    }
    if p.ramping {
        if p.migrationCredit == 0 {
            return 0
        }
        if p.migrationCredit < allow {
            allow = p.migrationCredit
        }
    }
    if agg >= cfg.MaxAggregateInflight {
        return 0
    }
    if a := cfg.MaxAggregateInflight - agg; allow > a {
        allow = a
    }
    return allow
}

func (p *Path) release(n uint64) {
    if n >= p.inflight {
        p.inflight = 0
    } else {
        p.inflight -= n
    }
}

func (p *Path) onAckCwnd(cfg SchedulerConfig, acked uint64) {
    if p.slowStart && p.cwnd < p.ssthresh {
        p.cwnd += acked
        if p.cwnd > cfg.MaxCwnd {
            p.cwnd = cfg.MaxCwnd
        }
        return
    }
    p.slowStart = false
    inc := cfg.MSS * cfg.MSS / p.cwnd
    if inc == 0 {
        inc = 1
    }
    p.cwnd += inc
    if p.cwnd > cfg.MaxCwnd {
        p.cwnd = cfg.MaxCwnd
    }
}

func release(cur, n uint64) uint64 {
    if n >= cur {
        return 0
    }
    return cur - n
}
```

## Verification

Run the full suite (the commands were executed in this environment):

```bash
cd ~/multipath-scheduler
go vet ./...
go test -v ./...
go test -race ./...
```

Observed output:

```text
$ go test ./...
ok      example.com/multipath   0.002s

$ go test -v ./...
=== RUN   TestEstimatorRTTSkewImmune
--- PASS: TestEstimatorRTTSkewImmune (0.00s)
=== RUN   TestEstimatorReorderingAndDuplicates
--- PASS: TestEstimatorReorderingAndDuplicates (0.00s)
=== RUN   TestEstimatorStaleSampleRejected
--- PASS: TestEstimatorStaleSampleRejected (0.00s)
=== RUN   TestEstimatorACKCompression
--- PASS: TestEstimatorACKCompression (0.00s)
=== RUN   TestCounterWraparound
--- PASS: TestCounterWraparound (0.00s)
=== RUN   TestRobustRateClamp
--- PASS: TestRobustRateClamp (0.00s)
=== RUN   TestSchedulerAggregateAndPerPathLimits
--- PASS: TestSchedulerAggregateAndPerPathLimits (0.00s)
=== RUN   TestSchedulerPerPathLimitAlone
--- PASS: TestSchedulerPerPathLimitAlone (0.00s)
=== RUN   TestPathFailureAndRecoveryDoesNotBurst
--- PASS: TestPathFailureAndRecoveryDoesNotBurst (0.00s)
=== RUN   TestStaleACKAfterFailureIgnored
--- PASS: TestStaleACKAfterFailureIgnored (0.00s)
=== RUN   TestSequenceWraparound
--- PASS: TestSequenceWraparound (0.00s)
=== RUN   TestSchedulerUsesDeliveryRateForWFQ
--- PASS: TestSchedulerUsesDeliveryRateForWFQ (0.00s)
PASS
ok      example.com/multipath   0.002s
```

Race detector:

```text
ok      example.com/multipath   1.012s
```

The tests exercise the required failure modes directly:

| Test | What it proves |
| --- | --- |
| `TestEstimatorRTTSkewImmune` | RTT is exactly 100 ms even though a simulated remote clock has a 1-hour offset and a 0.1% drift. |
| `TestEstimatorReorderingAndDuplicates` | Out-of-order ACKs still produce `Delivered=3000` and the correct `MinRTT`. |
| `TestEstimatorStaleSampleRejected` | An ACK older than `MaxSampleAge` updates neither `Rate` nor `SRTT`. |
| `TestEstimatorACKCompression` | Ten ACKs delivered at once yield the one-segment rate, not 10x. |
| `TestCounterWraparound` | `wrapDiff` and sequence comparison are correct across `2^64` wrap; the estimator keeps a correct delta. |
| `TestRobustRateClamp` | A single outlier sample cannot raise the rate more than 1.5x. |
| `TestSchedulerAggregateAndPerPathLimits` | Aggregate and per-path in-flight limits are never exceeded. |
| `TestSchedulerPerPathLimitAlone` | The per-path cap binds even when the aggregate cap is large. |
| `TestPathFailureAndRecoveryDoesNotBurst` | Recovery sends at most `InitialCwnd` before the first ACK and is ACK-clocked afterwards. |
| `TestStaleACKAfterFailureIgnored` | ACKs for packets on a failed path do not resurrect in-flight budget. |
| `TestSequenceWraparound` | Scheduler sequence numbers wrap cleanly and ACKs still match. |
| `TestSchedulerUsesDeliveryRateForWFQ` | A path with a 10x delivery rate receives roughly 10x the scheduling opportunities. |

Evidence & signatures

# Evidence
- Problem class: go-20261007-clock-skewed-multipath-congestion-recovery
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-10-08T04:11:45.649Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement a multipath transport scheduler that estimates per-path delivery rate and RTT under asymmetric clock skew, packet reordering, and intermittent ACK compression, then migrates traffic without creating a congestion burst. Enforce per-path and aggregate in-flight limits while handling path failure and recovery, and make the estimator robust to stale samples and counter wraparound.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-20261007-clock-skewed-multipath-congestion-recovery", "provider": "openrouter", "solved_at": "2026-10-08T04:11:45.649Z", "version": "1.26"}
Generated from the verified corpus · MIT licensedBack to the catalog