Skip to content

fix(baseapp): make BaseApp.Close idempotent - #26786

Open
pucedoteth wants to merge 1 commit into
cosmos:mainfrom
pucedoteth:fix/baseapp-close-idempotent
Open

fix(baseapp): make BaseApp.Close idempotent#26786
pucedoteth wants to merge 1 commit into
cosmos:mainfrom
pucedoteth:fix/baseapp-close-idempotent

Conversation

@pucedoteth

Copy link
Copy Markdown

Description

Closes #26558

server/start.go defers two cleanups that each call app.Close():

  • startCmtNode returns a cleanupFn that stops the CometBFT node and then calls app.Close() — deferred at server/start.go:352
  • startApp returns an appCleanupFn that shuts telemetry down and then calls app.Close() — deferred at server/start.go:252

Defers run LIFO, so on the ordinary in-process shutdown path both fire and Close runs twice. Close closed app.db and app.snapshotManager unconditionally, so the second call re-closed an already-closed database.

Backends disagree about what that means. goleveldb returns an error, which is merely logged. pebble panics with pebble: closed, so a pebble-backed node panics at the tail end of an otherwise clean stop and exits non-zero.

Fix

Guard the shutdown with a sync.Once and replay the first call's result. The underlying resources are closed once regardless of how many cleanup paths call Close, and a genuine close error is still surfaced to every caller rather than being swallowed.

This is deliberately fixed in BaseApp.Close rather than by deleting one of the two defers: Close is exported, and the SDK itself calls it from four separate places (server/start.go lines 328, 424, 649, 830). Making the method idempotent fixes all of them, and any chain doing the same in its own start command.

Testing

baseapp/close_test.go wraps a dbm.DB so the second Close panics the way pebble does, and asserts the backend is only closed once:

  • TestBaseAppCloseIsIdempotent — three Close calls, one backend close, no panic
  • TestBaseAppCloseReplaysError — an error from the first close is still returned by later calls, not lost

Both fail on main (panic: pebble: closed) and pass with this change. Full ./baseapp/ suite, go vet, and gofmt are clean.


Author Checklist

  • included the correct type prefix in the PR title
  • confirmed ! in the type prefix if API or client breaking change
  • targeted the correct branch
  • provided a link to the relevant issue or specification
  • reviewed "Files changed" and left comments if necessary
  • included the necessary unit and integration tests
  • added a changelog entry to CHANGELOG.md
  • updated the relevant documentation or specification, including comments for documenting Go code
  • confirmed all CI checks have passed

`server/start.go` defers two cleanups that each call `app.Close()`:
`startCmtNode`'s `cleanupFn` (which stops the CometBFT node and then closes
the app) and `startApp`'s `appCleanupFn`. Defers run LIFO, so on the normal
in-process shutdown path both fire and `Close` runs twice.

`Close` closed `app.db` and `app.snapshotManager` unconditionally, so the
second call re-closed an already-closed database. Backends differ in how they
react: goleveldb returns an error, but pebble panics with `pebble: closed`.
The result is that a pebble-backed node panics at the tail end of an otherwise
clean stop and exits non-zero.

Guard the shutdown with a `sync.Once` and replay the first call's result, so
the underlying resources are only ever closed once no matter how many cleanup
paths call `Close`.

Fixes cosmos#26558

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR author is not in the allowed authors list.

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.

baseapp: BaseApp.Close is not idempotent — double app.Close() on shutdown panics with PebbleDB ("pebble: closed")

1 participant