Back to list
Development Update — May 3
Skywire: TPD Panic Loop — Two Sources
Production TPD was restarting every 30-40s (RestartCount 556 over 6h on the prod host) because two distinct panics tear down the process. Today’s PRs catch both, plus the residual nil-deref races they exposed.
80fef7159 Fix tpd panic loop: cxo Finc-to-negative + httputil short-write discriminator
pkg/cxo/skyobject/cache.go (*Cache).Finc:1189,1216— panic:"Finc to negative for: <hash>". The filling-refcount went below zero, likely a duplicateFincon aFiller.incsmap or anInc/Fincmismatch across overlapping fillers. Hard process kill via panic.Filler.apply/Filler.rejectalready consume Finc’s error return and just log; surface the inconsistency through that path instead. Clampfcto 0, log the condition with key and the offending inc, and continue. Worst case is a leaked filling-item slot — orders of magnitude better than killing the service.pkg/httputil/httputil.go WriteJSON:50— panic:"short write: i/o deadline reached".isIOErrorcheckserrors.Is(err, io.ErrShortWrite), butnet/http’stimeoutWriterreturns its own error value with the same message string when a write deadline expires mid-response. The earlier sweep (PR #2402) caught the explicit timeout-writer wrapping, butWriteJSONwas building its own short-write path onEncodefailure that bypassed the helper. Routes the path throughisIOErrorconsistently so the shape is logged and discarded, not panicked.
36cf86077 cxo/node: guard fillHead nil-deref races (closeFiller + handleDelConn) — PR #2422 silenced the two dominant TPD panics. RestartCount went from ~556/6h to 2/10min. The remaining two panics are nil-derefs in pkg/cxo/node/head.go from torn-down fillHead state:
createFillerline 338:cr.c.String()panics whencr.cis nil.handleDelConn(line 517) setsf.p.c = nilwhen the source connection is removed.f.p.rremains non-zero, so thef.p == (connRoot{})gate athandleFillingResultdoes NOT catch the nil-c case, andcreateFiller(f.p)runs with a nil source. Fix: nil-c short-circuit at the top ofcreateFiller. The fill can’t proceed without a source connection — log and return.handleSuccessline 228 (and the parallel sites inhandleRequest,handleRequestFailure,handleReceivedRoot):f.fc.PushBackon a nil*list.List.closeFiller(line 384) setsf.rqo, f.fc, f.rq = nil, nil, nil, so any in-flight Filler goroutine messages that drain after close land on a nil list. Fix: nil-guard eachPushBack/PushFrontsite (4 sites total).
Skywire: CXO Cache Eviction Bug
**f0a45db52 cxo: fix Cache.cleanDown eviction bug and skip re-encoding unchanged sub-trees`
pkg/cxo/skyobject/cache.go — the filter loop’s “skip wanted” / “skip filling” comments said skip, but the code returned, aborting the entire ranking on the first non-evictable entry encountered during map iteration. On a busy visor there is virtually always something filling or being awaited, so the cache never evicted anything: putItem invoked cleanDown once the budget was exceeded, cleanDown built a len(c.is)-sized rank slice and immediately discarded it, and putItem then inserted the new item on top of an already-over-budget cache. The discarded slice is what the alloc profile pinned at 95% of total bytes allocated under cleanDown, and the un-evicted backlog is what pinned the live heap under putItem.
continue, not return. While here, switches the rank slice from []*rankItem to []rankItem so we don’t allocate a tiny heap object per cache entry.
pkg/cxo/treestore/publisher.go — caches encoded sub-node hashes across publishes. The previous publish flow re-encoded every sub-tree on every cycle, even when the underlying nodes hadn’t changed. CXO’s hash is content-addressed — equal content always produces equal hash — so caching the previous-cycle hash and skipping the encode when the in-memory tree is unchanged is safe. Production effect on the TPD aggregator: encode cost on the per-visor publish loop dropped from O(tree size) to O(changed nodes).
Skywire: Outlier RTT Samples
**0a7e12b2d transport: drop outlier RTT samples above MaxReasonableRTTMs (30s)— production observation: three sudph transports persisted withmin/avg ~190msandmax ~33-35 SECONDSin/metrics. The min/avg are correct; the max is a single straggler pong arriving long after its ping (delayed packet, host suspended, NIC queue stall). Once it lands, Maxonly ever grows, so a single bad sample pins the transport's Max for the life of thelat:
handleTransportPong correlates pongs to pings only by timestamp — there’s no in-flight tracking that would discard a pong arriving N seconds after timeout. Real intercontinental RTT plus heavy queueing is comfortably under 5s, so 30s is a generous upper bound for “obviously bogus.”
Mirrors the existing zero-clobber lower-bound guard with an upper-bound at every layer:
transport.MaxReasonableRTTMs(exported, 30,000.0).SetLatencydropslatencyMs > MaxReasonableRTTMs(alongside<=0).SetLatencyStatsrejects the whole snapshot if any of min/max/avg exceeds the cap.- The TPD-side latency-aware mirror gets the same upper-bound check, so a visor on an old build that still ships the bad snapshot can’t poison the discovery either. PR #2425 (May 4) extends the same cap to TPD’s
UpdateLatencywrite path.
Skywire: UI Latency Display
96ee3806c UI/transport latency display (PR #2419) — the visor’s TransportSummary carries a smoothed RTT (LatencyMS, populated by the transport-level ping/pong landed in PR #2401); the UI just wasn’t reading it.
app.datatypesTransportgains an optionallatencyMsfield.node.servicemapstransport.latency_ms(snake_case from the REST API) into the typed model. Both fetch sites in the service get the wire-up.transport-listadds a “Latency” column on the desktop table and a row on the small-screen card view; sortable like the other columns. Visible on/nodes/<pk>/routing(short list) and/nodes/<pk>/transports/<page>(full list).