Skip to content

Commit 1b08078

Browse files
committed
tests/robustness: add issue72 reproducer
Signed-off-by: Wei Fu <[email protected]>
1 parent 324df9c commit 1b08078

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

tests/failpoint/db_failpoint_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,118 @@ func TestFailpoint_LackOfDiskSpace(t *testing.T) {
155155
require.Error(t, err)
156156
require.ErrorIs(t, err, errors.ErrTxClosed)
157157
}
158+
159+
// TestIssue72 reproduces issue 72.
160+
//
161+
// When bbolt is processing a `Put` invocation, the key might be concurrently
162+
// updated by the application which calls the `Put` API (although it shouldn't).
163+
// It might lead to a situation that bbolt use an old key to find a proper
164+
// position to insert the key/value pair, but actually inserts a new key.
165+
// Eventually it might break the rule that all keys should be sorted. In a
166+
// worse case, it might cause page elements to point to already freed pages.
167+
//
168+
// REF: https://github.com/etcd-io/bbolt/issues/72
169+
func TestIssue72(t *testing.T) {
170+
db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: 4096})
171+
172+
bucketName := []byte(t.Name())
173+
err := db.Update(func(tx *bolt.Tx) error {
174+
_, txerr := tx.CreateBucket(bucketName)
175+
return txerr
176+
})
177+
require.NoError(t, err)
178+
179+
// The layout is like:
180+
//
181+
// +--+--+--+
182+
// +------+1 |3 |10+---+
183+
// | +-++--+--+ |
184+
// | | |
185+
// | | |
186+
// +v-+--+ +v-+--+ +-v+--+--+
187+
// |1 |2 | |3 |4 | |10|11|12|
188+
// +--+--+ +--+--+ +--+--+--+
189+
//
190+
err = db.Update(func(tx *bolt.Tx) error {
191+
bk := tx.Bucket(bucketName)
192+
193+
for _, id := range []int{1, 2, 3, 4, 10, 11, 12} {
194+
if txerr := bk.Put(idToBytes(id), make([]byte, 1000)); txerr != nil {
195+
return txerr
196+
}
197+
}
198+
return nil
199+
})
200+
require.NoError(t, err)
201+
202+
require.NoError(t, gofail.Enable("beforeBucketPut", `sleep(5000)`))
203+
204+
// +--+--+--+
205+
// +------+1 |3 |1 +---+
206+
// | +-++--+--+ |
207+
// | | |
208+
// | | |
209+
// +v-+--+ +v-+--+ +-v+--+--+--+
210+
// |1 |2 | |3 |4 | |1 |10|11|12|
211+
// +--+--+ +--+--+ +--+--+--+--+
212+
//
213+
key := idToBytes(13)
214+
updatedKey := idToBytes(1)
215+
err = db.Update(func(tx *bolt.Tx) error {
216+
bk := tx.Bucket(bucketName)
217+
218+
go func() {
219+
time.Sleep(3 * time.Second)
220+
copy(key, updatedKey)
221+
}()
222+
return bk.Put(key, make([]byte, 100))
223+
})
224+
require.NoError(t, err)
225+
226+
require.NoError(t, gofail.Disable("beforeBucketPut"))
227+
228+
// bbolt inserts 100 into last branch page. Since there are two `1`
229+
// keys in branch, spill operation will update first `1` pointer and
230+
// then last one won't be updated and continues to point to freed page.
231+
//
232+
//
233+
// +--+--+--+
234+
// +---------------+1 |3 |1 +---------+
235+
// | +--++-+--+ |
236+
// | | |
237+
// | | |
238+
// | +--+--+ +v-+--+ +-----v-----+
239+
// | |1 |2 | |3 |4 | |freed page |
240+
// | +--+--+ +--+--+ +-----------+
241+
// |
242+
// +v-+--+--+--+---+
243+
// |1 |10|11|12|100|
244+
// +--+--+--+--+---+
245+
err = db.Update(func(tx *bolt.Tx) error {
246+
return tx.Bucket(bucketName).Put(idToBytes(100), make([]byte, 100))
247+
})
248+
require.NoError(t, err)
249+
250+
defer func() {
251+
if r := recover(); r != nil {
252+
t.Logf("panic info:\n %v", r)
253+
}
254+
}()
255+
256+
// Add more keys to ensure branch node to spill.
257+
err = db.Update(func(tx *bolt.Tx) error {
258+
bk := tx.Bucket(bucketName)
259+
260+
for _, id := range []int{101, 102, 103, 104, 105} {
261+
if txerr := bk.Put(idToBytes(id), make([]byte, 1000)); txerr != nil {
262+
return txerr
263+
}
264+
}
265+
return nil
266+
})
267+
require.NoError(t, err)
268+
}
269+
270+
func idToBytes(id int) []byte {
271+
return []byte(fmt.Sprintf("%010d", id))
272+
}

0 commit comments

Comments
 (0)