A stalled log sink deadlocks peer connectivity — node reports healthy with zero peers and never recovers
Maintainers usually reply within 2 days
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- go
- Domain
- backend, networking
Research direction
Start with pkg/log/logger.go and pkg/pricing/pricing.go, then trace the connection and manage-loop paths in pkg/p2p/libp2p/libp2p.go and pkg/topology/kademlia/kademlia.go. First check whether the reported blocking behavior still occurs on the current version; the issue provides a pipe-based logger reproduction and an end-to-end procedure using bee start | cat. Done means a stalled log consumer no longer prevents peer dialing and the relevant behavior is covered by tests.
Written by the indexing model from the issue text.
Description
Context
- Bee 2.8.1 (
2.8.1-7cf53193), light node, macOS on arm64, launched by Swarm Desktop - Observed and diagnosed on 2.8.1. I have not reproduced it on a later release, so I am reporting what I saw. The code involved is
pkg/log/logger.goandpkg/pricing/pricing.go, and neither looked to have changed since — worth checking whether it still applies to current versions. - Node uptime at the time of capture: the fault had been in place 105 minutes
Summary
On 2.8.1, if whatever consumes bee's log output stops reading, Logger.Write
blocks indefinitely. Because the peer connection path logs synchronously, that block
propagates into kademlia: the manage loop parks on wg.Wait() and never dials
again, never runs its zero-peer bootnode fallback, and never updates its
gauges.
The node stays up, serves its API and reports {"status":"ok"} while holding
zero peers. Only a restart recovers it.
This is not specific to Swarm Desktop. Any stalled consumer produces it — a full
disk, a paused container, an unread pipe, a stalled journal. Swarm Desktop is
just where I hit it, because it pipes bee's stdout and stderr over unix sockets
and stopped draining them.
Expected behavior
A log consumer that stops reading should cost log lines, not connectivity. A
node should keep dialling peers regardless of whether anything is listening to
its logs, and if it cannot log it should degrade by dropping messages.
Actual behavior
The node held zero peers and made no dial attempts at all. Three samples,
twenty seconds apart:
/peers 0
/topology .connected 0
bee_kademlia_currently_connected_peers 88 <- stale
bee_kademlia_total_outbound_connection_attempts 699 <- frozen
bee_kademlia_total_outbound_connection_failed 262 <- frozen
/health {"status":"ok"}
The gauge disagreeing with reality is itself diagnostic:
CurrentlyConnectedPeers is set inside the manage loop, so 88 is the value from
the last iteration that completed.
From /debug/pprof/goroutine?debug=2:
goroutine 2148 [sync.WaitGroup.Wait, 105 minutes]:
sync.(*WaitGroup).Wait(...)
github.com/ethersphere/bee/v2/pkg/topology/kademlia.(*Kad).manage(...)
pkg/topology/kademlia/kademlia.go:629
created by ...kademlia.(*Kad).Start
and seven of these:
goroutine 2177 [semacquire, 105 minutes]:
internal/poll.runtime_Semacquire(...)
internal/poll.(*fdMutex).rwlock(...)
internal/poll.(*FD).Write(...) internal/poll/fd_unix.go:361
os.(*File).Write(...) os/file.go:215
github.com/ethersphere/bee/v2/pkg/log.(*logger).log(...) pkg/log/logger.go:245
github.com/ethersphere/bee/v2/pkg/log.(*logger).Warning(...) pkg/log/logger.go:199
github.com/ethersphere/bee/v2/pkg/pricing.(*Service).init(...) pkg/pricing/pricing.go:119
github.com/ethersphere/bee/v2/pkg/p2p/libp2p.(*Service).Connect(...) pkg/p2p/libp2p/libp2p.go:1201
github.com/ethersphere/bee/v2/pkg/topology/kademlia.(*Kad).connect(...) pkg/topology/kademlia/kademlia.go:975
Checking the process's file descriptors confirms where that write goes: file
descriptors 1 and 2 were both unix sockets held open by the parent process, not
a terminal or a file. The parent had stopped reading them.
The chain:
- The consumer stops reading; the socket buffer fills.
pricing.(*Service).initlogs a Warning duringConnect— it fires whenever
AnnouncePaymentThresholdreturns an error, which is common as peers go bad.logger.go:245does a barel.sink.Write(buf)with no timeout, no bounded
buffer and no drop path, so it blocks for ever.- The dial goroutine never returns.
manageis insidewg.Wait()atkademlia.go:629, so the iteration never
completes.- No further dials, and the
connectedPeers.Length() == 0bootnode fallback is
never reached.
Worth ruling out explicitly: this is not the connection breaker.
TotalOutboundConnectionAttempts.Inc() at kademlia.go:973 runs before
p2p.Connect, so a breaker-refused dial would still increment it. That counter
is frozen, so the code never reaches the breaker.
Steps to reproduce
The logger half reproduces on its own, with no node and no network:
func TestLoggerBlocksOnAStalledSink(t *testing.T) {
r, w, _ := os.Pipe()
t.Cleanup(func() { r.Close(); w.Close() })
lg := log.NewLogger("demo", log.WithSink(w), log.WithVerbosity(log.VerbosityAll))
var completed atomic.Int64
go func() {
for {
lg.Warning("could not send payment threshold announcement to peer",
"peer_address", "0000000000000000000000000000000000000000000000000000000000000000")
completed.Add(1)
}
}()
time.Sleep(2 * time.Second)
a := completed.Load()
time.Sleep(5 * time.Second)
b := completed.Load()
t.Logf("log calls completed: %d after 2s, %d after 7s", a, b)
}
Result — nothing reads r, the pipe fills, and the logger stops for good:
log calls completed: 368 after 2s, 368 after 7s
End to end on a real node, without Swarm Desktop:
bee start | cat- Once it has peers,
kill -STOPthecat. - Wait for the pipe to fill (a few hundred log lines).
curl localhost:1633/peersdrains to zero and stays there;/healthkeeps
reporting ok;bee_kademlia_total_outbound_connection_attemptsstops moving.
Possible solution
The logger is the right place to fix it. Fixing only the one call site leaves
every other log statement able to do the same thing.
- A bounded buffered sink with an explicit drop policy, so a stalled consumer
costs log lines rather than the node. Counting dropped lines matters, or the
loss is silent. - Failing that, a write deadline on the sink when it is a pipe or socket.
Narrower and cheaper, if the above is too invasive: make the connect path not
log synchronously. That fixes this instance and leaves the class open.
A separate hardening that would have limited the blast radius: manage's
wg.Wait() at kademlia.go:629 has no bound, so one stuck dial parks the whole
loop indefinitely. A timeout there would keep a node dialling even when
something below it misbehaves.
AI Disclosure
- This issue contains suggestions and text generated by an LLM.
- I have reviewed the AI generated content thoroughly.
- I possess the technical expertise to responsibly review the AI generated content mentioned in this issue.
Generated with help of AI.
- Dominant language
- Go
- Stars
- 1.5k
- Forks
- 390
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 23
Getting set up
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ethersphere/bee
-
Chequebook deployment uses a fixed 175,000 gas limit; it needs about 630,000 under Glamsterdam (live on Sepolia since 2026-10-06)Possibly taken @sbackend123 claimed this 9 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
ethersphere/bee#5650 · 1 assignee ·
Maintainers usually reply within 2 days
-
Add a transaction inbox for outbound chain transactionsPossibly taken @sbackend123 claimed this 3 days ago. Openseen
Difficulty 5/5 Over a week Newbie friendliness 35/100
ethersphere/bee#5660 · 1 assignee ·
Maintainers usually reply within 2 days
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
ethersphere/bee#5659 ·
Maintainers usually reply within 2 days
-
accepted seen
Difficulty 4/5 3-5 days Newbie friendliness 40/100
ethersphere/bee#5648 · 1 comment ·
Maintainers usually reply within 2 days
-
pusher: chunksWorker deadlocks on a deferred chunk that fails IdentityAddress (send on nil op.Err), halting all push-syncPossibly taken @sbackend123 claimed this 11 days ago. Openseen
Difficulty 3/5 1-2 days Newbie friendliness 75/100
ethersphere/bee#5641 · 1 assignee ·
Maintainers usually reply within 2 days
Similar issues
-
documentation good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
Maintainers usually reply within 1 day
-
install-path local lint proofs will exit 5 on roster-less machines once #3263 release is pinnedOpenraised-by:reviewer
Difficulty 1/5 1-3 hours Newbie friendliness 90/100
medici-finance/assay#3333 ·
Maintainers usually reply within 1 day
-
enhancement good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
onurbaydas/VirtoMir#9 ·
-
Add --version flagOpenenhancement good first issue
Difficulty 1/5 Under an hour Newbie friendliness 95/100
syncronix-labs/aegora#12 ·
-
auto-fixable pkg:api
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
tphakala/birdnet-go#4650 · 2 comments · 1 reaction ·
Maintainers usually reply within 1 day