Skip to content

Commit 2f71077

Browse files
ARR4Nalarso16
andauthored
refactor: remove default 1-minute timeout on Database.Close() and set 1-sec in tests (#1458)
Closes #1457 --------- Co-authored-by: Austin Larson <[email protected]>
1 parent 20786f9 commit 2f71077

File tree

2 files changed

+31
-72
lines changed

2 files changed

+31
-72
lines changed

ffi/firewood.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import (
3131
"fmt"
3232
"runtime"
3333
"sync"
34-
"time"
3534
)
3635

3736
const RootLength = C.sizeof_HashKey
@@ -246,18 +245,14 @@ func (db *Database) Revision(root Hash) (*Revision, error) {
246245
return rev, nil
247246
}
248247

249-
// defaultCloseTimeout is the duration by which the [context.Context] passed to
250-
// [Database.Close] is limited. A minute is arbitrary but well above what is
251-
// reasonably required, and is chosen simply to avoid permanently blocking.
252-
var defaultCloseTimeout = time.Minute
253-
254248
// Close releases the memory associated with the Database.
255249
//
256-
// This blocks until all outstanding keep-alive handles are disowned. That is,
257-
// until all Revisions and Proposals created from this Database are either
258-
// unreachable or one of [Proposal.Commit], [Proposal.Drop], or [Revision.Drop]
259-
// has been called on them. Unreachable objects will be automatically dropped
260-
// before Close returns, unless an alternate GC finalizer is set on them.
250+
// This blocks until all outstanding keep-alive handles are disowned or the
251+
// [context.Context] is cancelled. That is, until all Revisions and Proposals
252+
// created from this Database are either unreachable or one of
253+
// [Proposal.Commit], [Proposal.Drop], or [Revision.Drop] has been called on
254+
// them. Unreachable objects will be automatically dropped before Close returns,
255+
// unless an alternate GC finalizer is set on them.
261256
//
262257
// This is safe to call if the handle pointer is nil, in which case it does
263258
// nothing. The pointer will be set to nil after freeing to prevent double free.
@@ -276,12 +271,10 @@ func (db *Database) Close(ctx context.Context) error {
276271
close(done)
277272
}()
278273

279-
ctx, cancel := context.WithTimeout(ctx, defaultCloseTimeout)
280-
defer cancel()
281274
select {
282275
case <-done:
283276
case <-ctx.Done():
284-
return ErrActiveKeepAliveHandles
277+
return fmt.Errorf("%w: %w", ctx.Err(), ErrActiveKeepAliveHandles)
285278
}
286279

287280
if err := getErrorFromVoidResult(C.fwd_close_db(db.handle)); err != nil {

ffi/firewood_test.go

Lines changed: 24 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ func inferHashingMode(ctx context.Context) (string, error) {
7575
return "", err
7676
}
7777
defer func() {
78+
ctx, cancel := context.WithTimeout(ctx, time.Second)
79+
defer cancel()
7880
_ = db.Close(ctx)
7981
_ = os.Remove(dbFile)
8082
}()
@@ -137,6 +139,16 @@ func TestMain(m *testing.M) {
137139
os.Exit(m.Run())
138140
}
139141

142+
// oneSecCtx returns `tb.Context()` with a 1-second timeout added. Any existing
143+
// cancellation on `tb.Context()` is removed, which allows this function to be
144+
// used inside a `tb.Cleanup()`
145+
func oneSecCtx(tb testing.TB) context.Context {
146+
ctx := context.WithoutCancel(tb.Context())
147+
ctx, cancel := context.WithTimeout(ctx, time.Second)
148+
tb.Cleanup(cancel)
149+
return ctx
150+
}
151+
140152
func newTestDatabase(t *testing.T, configureFns ...func(*Config)) *Database {
141153
t.Helper()
142154
r := require.New(t)
@@ -145,7 +157,7 @@ func newTestDatabase(t *testing.T, configureFns ...func(*Config)) *Database {
145157
db, err := newDatabase(dbFile, configureFns...)
146158
r.NoError(err)
147159
t.Cleanup(func() {
148-
r.NoError(db.Close(context.Background())) //nolint:usetesting // t.Context() will already be cancelled
160+
r.NoError(db.Close(oneSecCtx(t)))
149161
})
150162
return db
151163
}
@@ -205,7 +217,7 @@ func TestTruncateDatabase(t *testing.T) {
205217
r.NoError(err)
206218

207219
// Close the database.
208-
r.NoError(db.Close(t.Context()))
220+
r.NoError(db.Close(oneSecCtx(t)))
209221

210222
// Reopen the database with truncate enabled.
211223
db, err = New(dbFile, config)
@@ -217,7 +229,7 @@ func TestTruncateDatabase(t *testing.T) {
217229
expectedHash := stringToHash(t, expectedRoots[emptyKey])
218230
r.Equal(expectedHash, hash, "Root hash mismatch after truncation")
219231

220-
r.NoError(db.Close(t.Context()))
232+
r.NoError(db.Close(oneSecCtx(t)))
221233
}
222234

223235
func TestClosedDatabase(t *testing.T) {
@@ -226,7 +238,7 @@ func TestClosedDatabase(t *testing.T) {
226238
db, err := newDatabase(dbFile)
227239
r.NoError(err)
228240

229-
r.NoError(db.Close(t.Context()))
241+
r.NoError(db.Close(oneSecCtx(t)))
230242

231243
_, err = db.Root()
232244
r.ErrorIs(err, errDBClosed)
@@ -238,7 +250,7 @@ func TestClosedDatabase(t *testing.T) {
238250
r.Empty(root)
239251
r.ErrorIs(err, errDBClosed)
240252

241-
r.NoError(db.Close(t.Context()))
253+
r.NoError(db.Close(oneSecCtx(t)))
242254
}
243255

244256
func keyForTest(i int) []byte {
@@ -1214,7 +1226,7 @@ func TestHandlesFreeImplicitly(t *testing.T) {
12141226

12151227
done := make(chan struct{})
12161228
go func() {
1217-
require.NoErrorf(t, db.Close(t.Context()), "%T.Close()", db)
1229+
require.NoErrorf(t, db.Close(oneSecCtx(t)), "%T.Close()", db)
12181230
close(done)
12191231
}()
12201232

@@ -1594,49 +1606,13 @@ func TestCloseWithCancelledContext(t *testing.T) {
15941606
wg.Wait()
15951607

15961608
r.ErrorIs(err, ErrActiveKeepAliveHandles, "Close should return ErrActiveKeepAliveHandles when context is cancelled")
1609+
r.ErrorIs(err, context.Canceled, "Close error should wrap context.Canceled")
15971610

15981611
// Drop the proposal
15991612
r.NoError(proposal.Drop())
16001613

16011614
// Now Close should succeed
1602-
r.NoError(db.Close(t.Context()))
1603-
}
1604-
1605-
// TestCloseWithShortTimeout verifies that Database.Close returns
1606-
// ErrActiveKeepAliveHandles when the context times out before handles are dropped.
1607-
func TestCloseWithShortTimeout(t *testing.T) {
1608-
r := require.New(t)
1609-
dbFile := filepath.Join(t.TempDir(), "test.db")
1610-
db, err := newDatabase(dbFile)
1611-
r.NoError(err)
1612-
1613-
// Create a revision to keep a handle active
1614-
keys, vals := kvForTest(1)
1615-
root, err := db.Update(keys, vals)
1616-
r.NoError(err)
1617-
1618-
revision, err := db.Revision(root)
1619-
r.NoError(err)
1620-
1621-
// Create a context with a very short timeout (100ms)
1622-
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
1623-
defer cancel()
1624-
1625-
// Record start time to verify timeout occurred quickly
1626-
start := time.Now()
1627-
1628-
// Close should return ErrActiveKeepAliveHandles after timeout
1629-
err = db.Close(ctx)
1630-
elapsed := time.Since(start)
1631-
1632-
r.ErrorIs(err, ErrActiveKeepAliveHandles, "Close should return ErrActiveKeepAliveHandles when context times out")
1633-
r.Less(elapsed, defaultCloseTimeout, "Close should timeout quickly, not wait for default timeout")
1634-
1635-
// Drop the revision
1636-
r.NoError(revision.Drop())
1637-
1638-
// Now Close should succeed
1639-
r.NoError(db.Close(t.Context()))
1615+
r.NoError(db.Close(oneSecCtx(t)))
16401616
}
16411617

16421618
// TestCloseWithMultipleActiveHandles verifies that Database.Close returns
@@ -1674,6 +1650,7 @@ func TestCloseWithMultipleActiveHandles(t *testing.T) {
16741650
// Close should return ErrActiveKeepAliveHandles
16751651
err = db.Close(ctx)
16761652
r.ErrorIs(err, ErrActiveKeepAliveHandles, "Close should return ErrActiveKeepAliveHandles with multiple active handles")
1653+
r.ErrorIs(err, context.Canceled, "Close error should wrap context.Canceled")
16771654

16781655
// Drop all handles
16791656
r.NoError(proposal1.Drop())
@@ -1683,7 +1660,7 @@ func TestCloseWithMultipleActiveHandles(t *testing.T) {
16831660
r.NoError(revision2.Drop())
16841661

16851662
// Now Close should succeed
1686-
r.NoError(db.Close(t.Context()))
1663+
r.NoError(db.Close(oneSecCtx(t)))
16871664
}
16881665

16891666
// TestCloseSucceedsWhenHandlesDroppedInTime verifies that Database.Close succeeds
@@ -1701,28 +1678,17 @@ func TestCloseSucceedsWhenHandlesDroppedInTime(t *testing.T) {
17011678
proposal2, err := db.Propose(keys[1:2], vals[1:2])
17021679
r.NoError(err)
17031680

1704-
// Create a context with a reasonable timeout
1705-
ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
1706-
defer cancel()
1707-
17081681
// Channel to receive Close result
17091682
closeDone := make(chan error, 1)
17101683

17111684
// Start Close in a goroutine
17121685
go func() {
1713-
closeDone <- db.Close(ctx)
1686+
closeDone <- db.Close(oneSecCtx(t))
17141687
}()
17151688

17161689
// Drop handles after a short delay (before timeout)
17171690
time.Sleep(100 * time.Millisecond)
17181691
r.NoError(proposal1.Drop())
17191692
r.NoError(proposal2.Drop())
1720-
1721-
// Close should succeed (not timeout)
1722-
select {
1723-
case err := <-closeDone:
1724-
r.NoError(err, "Close should succeed when handles are dropped before timeout")
1725-
case <-time.After(defaultCloseTimeout):
1726-
r.Fail("Close did not complete in time")
1727-
}
1693+
r.NoError(<-closeDone, "Close should succeed when handles are dropped before timeout")
17281694
}

0 commit comments

Comments
 (0)