Skip to content

fix: attempt to close all resources in DB.Close() even on error#65

Merged
feichai0017 merged 4 commits intofeichai0017:mainfrom
ByteByteUp:fix/db-close-short-circuits-cleanup
Feb 15, 2026
Merged

fix: attempt to close all resources in DB.Close() even on error#65
feichai0017 merged 4 commits intofeichai0017:mainfrom
ByteByteUp:fix/db-close-short-circuits-cleanup

Conversation

@ByteByteUp
Copy link
Copy Markdown
Contributor

@ByteByteUp ByteByteUp commented Feb 15, 2026

Previously, DB.Close() would return immediately on the first error (e.g. from lsm.Close()),
skipping cleanup of vlog, WAL, and dirLock. This could lead to resource leaks or stale locks.

Now, all close operations are attempted regardless of individual errors,
and any errors are aggregated using errors.Join.

Add test to verify that other components are closed even when one fails.

Fix #55

Summary by CodeRabbit

  • Bug Fixes

    • Improved database shutdown: all component close errors are now collected and reported together, and shutdown proceeds through each component to ensure full cleanup.
  • Tests

    • Added a test that simulates multiple close-time errors, verifies all errors are surfaced, and confirms the shutdown call sequence is preserved.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

DB.Close was changed to always attempt closing LSM, VLog, WAL, and dir lock, recording call order and aggregating any returned errors. A test-only hook container (testCloseHooks) was added and a unit test validates that all close calls run and errors are joined.

Changes

Cohort / File(s) Summary
DB close logic & test hooks
db.go
Adds testCloseHooks type and testCloseHooks *testCloseHooks field on DB. Modifies DB.Close() to invoke each close (lsm, vlog, wal, dirLock) even if prior steps fail, call test hooks when set, record call order, and aggregate errors rather than returning early. Adds (*testCloseHooks).record(name string).
Close behavior unit test
db_test.go
Adds TestCloseWithErrors which installs test hooks that simulate errors for lsm, vlog, wal, and dirLock release, calls db.Close(), asserts the aggregated error unwraps to four errors in order, and verifies the recorded call sequence.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test / Caller
    participant DB as DB.Close()
    participant LSM as LSM
    participant VLOG as VLog
    participant WAL as WAL
    participant LOCK as DirLock
    participant AGG as ErrorAggregator

    Test->>DB: call Close()
    DB->>LSM: close (or test hook)
    alt error?
        LSM-->>AGG: add "lsm" error
    end
    DB->>VLOG: close (or test hook)
    alt error?
        VLOG-->>AGG: add "vlog" error
    end
    DB->>WAL: close (or test hook)
    alt error?
        WAL-->>AGG: add "wal" error
    end
    DB->>LOCK: release (or test hook)
    alt error?
        LOCK-->>AGG: add "dir lock" error
    end
    AGG-->>DB: joined error (nil or aggregated)
    DB-->>Test: return aggregated error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through every close and peered,

Each latch and log I gently cleared.
If errors came, I gathered four,
Then sent them off behind the door.
Now every burrow’s tidy, assured.

🚥 Pre-merge checks | ✅ 4 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (4 files):

⚔️ db.go (content)
⚔️ db_test.go (content)
⚔️ txn.go (content)
⚔️ txn_test.go (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing DB.Close() to attempt closing all resources even when errors occur, directly addressing the short-circuit issue.
Linked Issues check ✅ Passed The implementation satisfies all objectives from issue #55: all four close operations are invoked regardless of errors, errors are aggregated (via test hooks), and deterministic shutdown is maintained with test verification.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the DB.Close() short-circuit issue; no unrelated modifications to other components or features are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix/db-close-short-circuits-cleanup
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
db.go (1)

353-355: ⚠️ Potential issue | 🟠 Major

stats.close() still short-circuits, bypassing error aggregation for downstream resources.

If db.stats.close() fails, the function returns immediately (line 354), and lsm/vlog/wal/dirLock are never closed. This contradicts the PR objective of best-effort cleanup for all resources. Consider collecting this error into errs as well.

🐛 Proposed fix
-	if err := db.stats.close(); err != nil {
-		return err
-	}
+	if err := db.stats.close(); err != nil {
+		errs = append(errs, fmt.Errorf("stats close: %w", err))
+	}

Note: you'd need to move var errs []error above this block (before line 353).

🤖 Fix all issues with AI agents
In `@db_test.go`:
- Around line 996-1025: The test TestCloseWithErrors replaces real closers with
testCloseHooks and never releases the real resources opened by Open(opt),
leaking descriptors and the directory lock; fix by capturing the real resource
handles before overriding testCloseHooks and ensuring they are closed/released
(e.g., save db.lsm, db.vlog, db.wal, db.dirLock into locals and defer/explicitly
call their Close/Release after the assert or before setting testCloseHooks) so
the real LSM/vlog/WAL/dirLock are always closed even while you assert the hook
behavior.

In `@db.go`:
- Around line 422-428: The Close() method currently returns early when errs is
non-empty and never sets db.isClosed, breaking idempotence; fix it by ensuring
atomic.StoreUint32(&db.isClosed, 1) is always executed before any return from
Close() (e.g. set it at the start of Close() when shutdown begins or use a defer
that sets it regardless of errors), then continue to aggregate and return
stderrors.Join(errs...) so callers still receive the error list; reference
Close(), the errs slice, IsClosed(), and atomic.StoreUint32(&db.isClosed, 1)
when making the change.
🧹 Nitpick comments (1)
db.go (1)

111-117: Test hooks struct adds test-only plumbing to production code.

An alternative is to define an interface for the closeable components (or use function fields on DB set only during tests via an _test.go file using internal access). That said, the current approach is a common pragmatic pattern in Go — consider refactoring in a follow-up if the hook surface grows.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 72.41379% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
db.go 72.41% 4 Missing and 4 partials ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes a resource leak issue where DB.Close() would short-circuit on the first error, preventing subsequent cleanup operations from running. The fix introduces aggregated error handling using errors.Join to ensure all close operations are attempted even when individual operations fail.

Changes:

  • Modified DB.Close() to collect and aggregate errors from lsm, vlog, wal, and dirLock close operations instead of returning immediately on first error
  • Added testCloseHooks struct to inject close errors in tests without affecting real resource cleanup
  • Added TestCloseWithErrors to verify that all close operations are attempted and errors are properly aggregated

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
db.go Implemented error aggregation in Close() method, added testCloseHooks for testing, and record() helper method
db_test.go Added TestCloseWithErrors to verify all close operations execute even when they fail and errors are aggregated

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +996 to +1010
func TestCloseWithErrors(t *testing.T) {
clearDir()
db := Open(opt)
lsmErr := errors.New("lsm close error")
vlogErr := errors.New("vlog close error")
walErr := errors.New("wal close error")
dirLockErr := errors.New("dir lock release error")
db.testCloseHooks = &testCloseHooks{
lsmClose: func() error { return lsmErr },
vlogClose: func() error { return vlogErr },
walClose: func() error { return walErr },
dirLockRelease: func() error { return dirLockErr },
calls: []string{},
}
err := db.Close()
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The test opens a real database with Open(opt) which initializes real lsm, vlog, wal, and dirLock resources, but then immediately replaces their close methods with test hooks that only return errors. This means the actual resources (file handles, locks, etc.) may not be properly cleaned up when the test ends. Consider either: (1) creating a mock/stub DB instance instead of using Open(), or (2) ensuring the test hooks still call the real close methods after recording the call, or (3) adding explicit cleanup in a defer statement.

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +117
type testCloseHooks struct {
lsmClose func() error
vlogClose func() error
walClose func() error
dirLockRelease func() error
calls []string
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The testCloseHooks struct lacks a documentation comment explaining its purpose and usage. Consider adding a comment like: "testCloseHooks allows tests to intercept and simulate errors during DB.Close() operations without affecting real resource cleanup. Only for testing purposes." This would help other developers understand when and how to use this testing facility.

Copilot uses AI. Check for mistakes.
Comment on lines +1010 to +1025
err := db.Close()

var gotErrs []error
if err != nil {
if multiErr, ok := err.(interface{ Unwrap() []error }); ok {
gotErrs = multiErr.Unwrap()
}
}
require.Len(t, gotErrs, 4)
require.True(t, errors.Is(gotErrs[0], lsmErr))
require.True(t, errors.Is(gotErrs[1], vlogErr))
require.True(t, errors.Is(gotErrs[2], walErr))
require.True(t, errors.Is(gotErrs[3], dirLockErr))
expectCalls := []string{"lsm close", "vlog close", "wal close", "dir lock release"}
require.Equal(t, expectCalls, db.testCloseHooks.calls)
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The test doesn't verify the state of db.IsClosed() after Close() returns errors. Given the bug where isClosed is only set on success (line 427), the test should assert that either: (1) db.IsClosed() returns true even when Close() fails (if that's the intended behavior), or (2) db.IsClosed() returns false when Close() fails (if that matches the current buggy behavior). This would help document the expected behavior and catch regressions.

Copilot uses AI. Check for mistakes.
db.go Outdated
Comment on lines 423 to 427
if len(errs) > 0 {
return stderrors.Join(errs...)
}

atomic.StoreUint32(&db.isClosed, 1)
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The isClosed flag is only set to 1 when there are no errors in the aggregated close operations. This means if any of lsm, vlog, wal, or dirLock close operations fail, the database will remain in a "not closed" state (isClosed will still be 0). This could lead to issues if Close() is called again or if other code checks IsClosed() to determine whether operations can proceed. The isClosed flag should be set regardless of whether errors occurred during cleanup, since the close attempt has been made and the database should not be used further.

Suggested change
if len(errs) > 0 {
return stderrors.Join(errs...)
}
atomic.StoreUint32(&db.isClosed, 1)
atomic.StoreUint32(&db.isClosed, 1)
if len(errs) > 0 {
return stderrors.Join(errs...)
}

Copilot uses AI. Check for mistakes.
Comment on lines +1012 to +1017
var gotErrs []error
if err != nil {
if multiErr, ok := err.(interface{ Unwrap() []error }); ok {
gotErrs = multiErr.Unwrap()
}
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The error checking logic has a flaw: if err is nil or if it doesn't implement the Unwrap() []error interface, gotErrs will be empty/nil but the test will still try to access gotErrs[0] through gotErrs[3] in the assertions below. This could lead to index out of bounds panics. Consider adding a check like 'require.NotNil(t, err)' before the type assertion, or failing the test if gotErrs is not populated as expected.

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +117
type testCloseHooks struct {
lsmClose func() error
vlogClose func() error
walClose func() error
dirLockRelease func() error
calls []string
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The testCloseHooks struct is not thread-safe. The 'calls' slice is accessed by the record() method without any synchronization. If Close() were ever called concurrently (though it shouldn't be in typical usage), this could lead to race conditions. Consider adding a mutex to protect the calls slice, or document that testCloseHooks is not thread-safe and should only be used in single-threaded test scenarios.

Copilot uses AI. Check for mistakes.
Comment on lines +384 to 421
if db.testCloseHooks != nil && db.testCloseHooks.lsmClose != nil {
db.testCloseHooks.record("lsm close")
if err := db.testCloseHooks.lsmClose(); err != nil {
errs = append(errs, fmt.Errorf("lsm close: %w", err))
}
} else if err := db.lsm.Close(); err != nil {
errs = append(errs, fmt.Errorf("lsm close: %w", err))
}
if err := db.vlog.close(); err != nil {
return err

if db.testCloseHooks != nil && db.testCloseHooks.vlogClose != nil {
db.testCloseHooks.record("vlog close")
if err := db.testCloseHooks.vlogClose(); err != nil {
errs = append(errs, fmt.Errorf("vlog close: %w", err))
}
} else if err := db.vlog.close(); err != nil {
errs = append(errs, fmt.Errorf("vlog close: %w", err))
}
if err := db.wal.Close(); err != nil {
return err

if db.testCloseHooks != nil && db.testCloseHooks.walClose != nil {
db.testCloseHooks.record("wal close")
if err := db.testCloseHooks.walClose(); err != nil {
errs = append(errs, fmt.Errorf("wal close: %w", err))
}
} else if err := db.wal.Close(); err != nil {
errs = append(errs, fmt.Errorf("wal close: %w", err))
}

if db.dirLock != nil {
if err := db.dirLock.Release(); err != nil {
return err
if db.testCloseHooks != nil && db.testCloseHooks.dirLockRelease != nil {
db.testCloseHooks.record("dir lock release")
if err := db.testCloseHooks.dirLockRelease(); err != nil {
errs = append(errs, fmt.Errorf("dir lock release: %w", err))
}
} else if err := db.dirLock.Release(); err != nil {
errs = append(errs, fmt.Errorf("dir lock release: %w", err))
}
db.dirLock = nil
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The close hook implementation is verbose and repetitive. Each close operation requires 8-9 lines of nearly identical if-else logic. Consider refactoring to a helper method that handles the hook/real operation pattern, which would reduce code duplication and make the logic easier to maintain. For example: a method like 'closeWithHook(name string, hookFn func() error, realFn func() error) error' that returns the error to append.

Copilot uses AI. Check for mistakes.
@ByteByteUp
Copy link
Copy Markdown
Contributor Author

@feichai0017 I've updated the code based on your suggestions. Please review again—thanks!

@feichai0017
Copy link
Copy Markdown
Owner

LGTM,Thanks

@feichai0017 feichai0017 merged commit cc8dccc into feichai0017:main Feb 15, 2026
4 of 5 checks passed
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.

[BUG] DB.Close short-circuits cleanup on first close error

4 participants