Skip to content

feat: add safe package for unified goroutine recovery#5528

Open
janos wants to merge 2 commits into
masterfrom
safe-goroutines
Open

feat: add safe package for unified goroutine recovery#5528
janos wants to merge 2 commits into
masterfrom
safe-goroutines

Conversation

@janos

@janos janos commented Jul 7, 2026

Copy link
Copy Markdown
Member

Checklist

  • I have read the coding guide.
  • My change requires a documentation update, and I have done it.
  • I have added tests to cover my changes.
  • I have filled out the description and linked the related issues.

Description

This PR introduces a unified panic recovery approach to protect the Bee node from unexpected crashes in goroutines.

By catching panics inside worker routines, checker loops, and handler callbacks, it is ensures that unexpected panics are safely recovered and converted into either structured log messages or standard Go errors, rather than causing the entire node process to crash.

I have added a new small package in ./pkg/safe for functions that unify the recovery for the bee node.

Other changes are the usage of these functions in various goroutines in the code. Waith groups and errors groups create goroutines internally.

Open API Spec Version Changes (if applicable)

Motivation and Context (Optional)

Prevent bee node from crashing on panics.

Related Issue (Optional)

Screenshots (if appropriate):

AI Disclosure

  • This PR contains code that has been generated by an LLM.
  • I have reviewed the AI generated code thoroughly.
  • I possess the technical expertise to responsibly review the code generated in this PR.

@acud

acud commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

if goroutines that are supposed to panic are recovered gracefully and do not cause the application to crash, how do we recover from a state where:
a. application is an inconsistent state?
b. database is in an inconsistent state? i.e. if a db write failed because the disk is full and we can no longer assume that the node is consistent
c. node setup is done in a wrong way by us (devs) and now the "feedback" (panic) is swallowed into logs

@janos

janos commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

if goroutines that are supposed to panic are recovered gracefully and do not cause the application to crash, how do we recover from a state where: a. application is an inconsistent state? b. database is in an inconsistent state? i.e. if a db write failed because the disk is full and we can no longer assume that the node is consistent c. node setup is done in a wrong way by us (devs) and now the "feedback" (panic) is swallowed into logs

That is per goroutine for us to decide. Some goroutines are contained for a specific operation that should not terminate the application. For example, the joiner slice out of bounds panic was related only to one resource to be loaded and had no impact on the operations of the whole application and that panic could have been be recovered. And for the goroutines related to the database should be evaluated if they should be recovered. That is why I have added protection in goroutines that I could find so that they can be evaluated, case per case, as are visible in this PR changeset.

@gacevicljubisa gacevicljubisa left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, the PR is well scoped and with clear idea. I would suggest adding some metrics that could track total recovered panics.

Comment thread pkg/storer/validate.go Outdated
Comment on lines +155 to +160
safe.Run(logger, "reserve-validation-worker", func() {
buf := make([]byte, swarm.SocMaxChunkSize)
for item := range iteratateItemsC {
validChunk(item, buf[:item.Location.Length])
}
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better if we recover per item? That way one corrupt item will not tear down the worker?

Comment thread pkg/storer/validate.go
Comment on lines +337 to +339
if !validChunk(item, buf[:item.Location.Length]) {
invalid.Add(1)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe here as well we could recover per item?

Comment thread pkg/safe/safe.go Outdated
func Run(logger log.Logger, name string, fn func()) {
defer func() {
if r := recover(); r != nil {
logger.Error(nil, "panic recovered", "name", name, "panic", fmt.Sprintf("%v", r), "stack", string(debug.Stack()))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is a minor thing, but maybe we could check if logger is nil?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then you completely lose sight of the fact that there was a panic...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then you completely lose sight of the fact that there was a panic...

actually, it will panic if the logger is nil, as the recover is already called, but the check for nil logger is a good suggestion

@acud acud Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes so what i meant it... if it panicked and there is no logger, at least you get a panic now (so you know there was even a panic in the first place).
if you check if the logger is nil and don't even call it - then you will: a. never know that a panic was recovered (unless you add metrics), and b. never know that the logger was not set (which is a severe engineering human error, because someone wrote code that did not set the logger where it should have been set). so my claim is that when we do all of these defensive programming we actually lose feedback over the code.
if the choice is between: 1. having a panic that propagates because of a real error, or 2. having a panic that does not show up at all (and be silently recovered, potentially putting the process into an inconsistent state because someone forgot to set a logger up properly then i prefer to have a panic crash the process honestly but that's just me.
or to conclude: i like panics :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, you will get the panic that the logger is nil as it was a method call on the nil logger. That does mean that the original panic is lost, but because of that I have added the check on the nil logger as Ljubisa suggested.

We do not need to recover from panics. This PR is just mu suggestion, it depends on your preference. My opinion is that recovering from panics depends on the actual logic in the goroutine. Recent panics in the code show that there are localized problems that could be errors, not crashing the node (joiner bounds and nil bzz address).

The defensive programming is mostly related to the user experience. Where an error can be logged instead of crashing the node, it appears to me like a better user experience.

Comment thread pkg/salud/salud.go
return
}

mtx.Lock()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe to defer mtx.Unlock() here if panic happens in IsWithinStorageRadius

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants