Skip to content

Commit c74748e

Browse files
authored
Merge pull request #786 from tjungblu/fl_iface_tests
add freelist interface unit tests
2 parents bb1d9be + 3ce0fd0 commit c74748e

File tree

3 files changed

+317
-0
lines changed

3 files changed

+317
-0
lines changed

internal/freelist/array_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@ func TestFreelistArray_allocate(t *testing.T) {
5353
}
5454
}
5555

56+
func TestInvalidArrayAllocation(t *testing.T) {
57+
f := NewArrayFreelist()
58+
// page 0 and 1 are reserved for meta pages, so they should never be free pages.
59+
ids := []common.Pgid{1}
60+
f.Init(ids)
61+
require.Panics(t, func() {
62+
f.Allocate(common.Txid(1), 1)
63+
})
64+
}
65+
5666
func Test_Freelist_Array_Rollback(t *testing.T) {
5767
f := newTestArrayFreelist()
5868

internal/freelist/freelist_test.go

+299
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package freelist
22

33
import (
4+
"fmt"
5+
"math"
46
"math/rand"
57
"os"
68
"reflect"
9+
"slices"
710
"sort"
811
"testing"
12+
"testing/quick"
913
"unsafe"
1014

1115
"github.com/stretchr/testify/require"
@@ -34,6 +38,55 @@ func TestFreelist_free_overflow(t *testing.T) {
3438
}
3539
}
3640

41+
// Ensure that double freeing a page is causing a panic
42+
func TestFreelist_free_double_free_panics(t *testing.T) {
43+
f := newTestFreelist()
44+
f.Free(100, common.NewPage(12, 0, 0, 3))
45+
require.Panics(t, func() {
46+
f.Free(100, common.NewPage(12, 0, 0, 3))
47+
})
48+
}
49+
50+
// Ensure that attempting to free the meta page panics
51+
func TestFreelist_free_meta_panics(t *testing.T) {
52+
f := newTestFreelist()
53+
require.Panics(t, func() {
54+
f.Free(100, common.NewPage(0, 0, 0, 0))
55+
})
56+
require.Panics(t, func() {
57+
f.Free(100, common.NewPage(1, 0, 0, 0))
58+
})
59+
}
60+
61+
func TestFreelist_free_freelist(t *testing.T) {
62+
f := newTestFreelist()
63+
f.Free(100, common.NewPage(12, common.FreelistPageFlag, 0, 0))
64+
pp := f.pendingPageIds()[100]
65+
require.Equal(t, []common.Pgid{12}, pp.ids)
66+
require.Equal(t, []common.Txid{0}, pp.alloctx)
67+
}
68+
69+
func TestFreelist_free_freelist_alloctx(t *testing.T) {
70+
f := newTestFreelist()
71+
f.Free(100, common.NewPage(12, common.FreelistPageFlag, 0, 0))
72+
f.Rollback(100)
73+
require.Empty(t, f.freePageIds())
74+
require.Empty(t, f.pendingPageIds())
75+
require.False(t, f.Freed(12))
76+
77+
f.Free(101, common.NewPage(12, common.FreelistPageFlag, 0, 0))
78+
require.True(t, f.Freed(12))
79+
if exp := []common.Pgid{12}; !reflect.DeepEqual(exp, f.pendingPageIds()[101].ids) {
80+
t.Fatalf("exp=%v; got=%v", exp, f.pendingPageIds()[101].ids)
81+
}
82+
f.ReleasePendingPages()
83+
require.True(t, f.Freed(12))
84+
require.Empty(t, f.pendingPageIds())
85+
if exp := common.Pgids([]common.Pgid{12}); !reflect.DeepEqual(exp, f.freePageIds()) {
86+
t.Fatalf("exp=%v; got=%v", exp, f.freePageIds())
87+
}
88+
}
89+
3790
// Ensure that a transaction's free pages can be released.
3891
func TestFreelist_release(t *testing.T) {
3992
f := newTestFreelist()
@@ -220,6 +273,30 @@ func TestFreeList_reload(t *testing.T) {
220273
require.Equal(t, []common.Pgid{10, 11, 12}, f2.pendingPageIds()[5].ids)
221274
}
222275

276+
// Ensure that the txIDx swap, less and len are properly implemented
277+
func TestTxidSorting(t *testing.T) {
278+
require.NoError(t, quick.Check(func(a []uint64) bool {
279+
var txids []common.Txid
280+
for _, txid := range a {
281+
txids = append(txids, common.Txid(txid))
282+
}
283+
284+
sort.Sort(txIDx(txids))
285+
286+
var r []uint64
287+
for _, txid := range txids {
288+
r = append(r, uint64(txid))
289+
}
290+
291+
if !slices.IsSorted(r) {
292+
t.Errorf("txids were not sorted correctly=%v", txids)
293+
return false
294+
}
295+
296+
return true
297+
}, nil))
298+
}
299+
223300
// Ensure that a freelist can deserialize from a freelist page.
224301
func TestFreelist_read(t *testing.T) {
225302
// Create a page.
@@ -243,6 +320,18 @@ func TestFreelist_read(t *testing.T) {
243320
}
244321
}
245322

323+
// Ensure that we never read a non-freelist page
324+
func TestFreelist_read_panics(t *testing.T) {
325+
buf := make([]byte, 4096)
326+
page := common.LoadPage(buf)
327+
page.SetFlags(common.BranchPageFlag)
328+
page.SetCount(2)
329+
f := newTestFreelist()
330+
require.Panics(t, func() {
331+
f.Read(page)
332+
})
333+
}
334+
246335
// Ensure that a freelist can serialize into a freelist page.
247336
func TestFreelist_write(t *testing.T) {
248337
// Create a freelist and write it to a page.
@@ -266,6 +355,216 @@ func TestFreelist_write(t *testing.T) {
266355
}
267356
}
268357

358+
func TestFreelist_E2E_HappyPath(t *testing.T) {
359+
f := newTestFreelist()
360+
f.Init([]common.Pgid{})
361+
requirePages(t, f, common.Pgids{}, common.Pgids{})
362+
363+
allocated := f.Allocate(common.Txid(1), 5)
364+
require.Equal(t, common.Pgid(0), allocated)
365+
// tx.go may now allocate more space, and eventually we need to delete a page again
366+
f.Free(common.Txid(2), common.NewPage(5, common.LeafPageFlag, 0, 0))
367+
f.Free(common.Txid(2), common.NewPage(3, common.LeafPageFlag, 0, 0))
368+
f.Free(common.Txid(2), common.NewPage(8, common.LeafPageFlag, 0, 0))
369+
// the above will only mark the pages as pending, so free pages should not return anything
370+
requirePages(t, f, common.Pgids{}, common.Pgids{3, 5, 8})
371+
372+
// someone wants to do a read on top of the next tx id
373+
f.AddReadonlyTXID(common.Txid(3))
374+
// this should free the above pages for tx 2 entirely
375+
f.ReleasePendingPages()
376+
requirePages(t, f, common.Pgids{3, 5, 8}, common.Pgids{})
377+
378+
// no span of two pages available should yield a zero-page result
379+
require.Equal(t, common.Pgid(0), f.Allocate(common.Txid(4), 2))
380+
// we should be able to allocate those pages independently however,
381+
// map and array differ in the order they return the pages
382+
expectedPgids := map[common.Pgid]struct{}{3: {}, 5: {}, 8: {}}
383+
for i := 0; i < 3; i++ {
384+
allocated = f.Allocate(common.Txid(4), 1)
385+
require.Contains(t, expectedPgids, allocated, "expected to find pgid %d", allocated)
386+
require.False(t, f.Freed(allocated))
387+
delete(expectedPgids, allocated)
388+
}
389+
require.Emptyf(t, expectedPgids, "unexpectedly more than one page was still found")
390+
// no more free pages to allocate
391+
require.Equal(t, common.Pgid(0), f.Allocate(common.Txid(4), 1))
392+
}
393+
394+
func TestFreelist_E2E_MultiSpanOverflows(t *testing.T) {
395+
f := newTestFreelist()
396+
f.Init([]common.Pgid{})
397+
f.Free(common.Txid(10), common.NewPage(20, common.LeafPageFlag, 0, 1))
398+
f.Free(common.Txid(10), common.NewPage(25, common.LeafPageFlag, 0, 2))
399+
f.Free(common.Txid(10), common.NewPage(35, common.LeafPageFlag, 0, 3))
400+
f.Free(common.Txid(10), common.NewPage(39, common.LeafPageFlag, 0, 2))
401+
f.Free(common.Txid(10), common.NewPage(45, common.LeafPageFlag, 0, 4))
402+
requirePages(t, f, common.Pgids{}, common.Pgids{20, 21, 25, 26, 27, 35, 36, 37, 38, 39, 40, 41, 45, 46, 47, 48, 49})
403+
f.ReleasePendingPages()
404+
requirePages(t, f, common.Pgids{20, 21, 25, 26, 27, 35, 36, 37, 38, 39, 40, 41, 45, 46, 47, 48, 49}, common.Pgids{})
405+
406+
// that sequence, regardless of implementation, should always yield the same blocks of pages
407+
allocSequence := []int{7, 5, 3, 2}
408+
expectedSpanStarts := []common.Pgid{35, 45, 25, 20}
409+
for i, pageNums := range allocSequence {
410+
allocated := f.Allocate(common.Txid(11), pageNums)
411+
require.Equal(t, expectedSpanStarts[i], allocated)
412+
// ensure all pages in that span are not considered free anymore
413+
for i := 0; i < pageNums; i++ {
414+
require.False(t, f.Freed(allocated+common.Pgid(i)))
415+
}
416+
}
417+
}
418+
419+
func TestFreelist_E2E_Rollbacks(t *testing.T) {
420+
freelist := newTestFreelist()
421+
freelist.Init([]common.Pgid{})
422+
freelist.Free(common.Txid(2), common.NewPage(5, common.LeafPageFlag, 0, 1))
423+
freelist.Free(common.Txid(2), common.NewPage(8, common.LeafPageFlag, 0, 0))
424+
requirePages(t, freelist, common.Pgids{}, common.Pgids{5, 6, 8})
425+
freelist.Rollback(common.Txid(2))
426+
requirePages(t, freelist, common.Pgids{}, common.Pgids{})
427+
428+
// unknown transaction should not trigger anything
429+
freelist.Free(common.Txid(4), common.NewPage(13, common.LeafPageFlag, 0, 3))
430+
requirePages(t, freelist, common.Pgids{}, common.Pgids{13, 14, 15, 16})
431+
freelist.ReleasePendingPages()
432+
requirePages(t, freelist, common.Pgids{13, 14, 15, 16}, common.Pgids{})
433+
freelist.Rollback(common.Txid(1337))
434+
requirePages(t, freelist, common.Pgids{13, 14, 15, 16}, common.Pgids{})
435+
}
436+
437+
func TestFreelist_E2E_RollbackPanics(t *testing.T) {
438+
freelist := newTestFreelist()
439+
freelist.Init([]common.Pgid{5})
440+
requirePages(t, freelist, common.Pgids{5}, common.Pgids{})
441+
442+
_ = freelist.Allocate(common.Txid(5), 1)
443+
require.Panics(t, func() {
444+
// depending on the verification level, either should panic
445+
freelist.Free(common.Txid(5), common.NewPage(5, common.LeafPageFlag, 0, 0))
446+
freelist.Rollback(5)
447+
})
448+
}
449+
450+
// tests the reloading from another physical page
451+
func TestFreelist_E2E_Reload(t *testing.T) {
452+
freelist := newTestFreelist()
453+
freelist.Init([]common.Pgid{})
454+
freelist.Free(common.Txid(2), common.NewPage(5, common.LeafPageFlag, 0, 1))
455+
freelist.Free(common.Txid(2), common.NewPage(8, common.LeafPageFlag, 0, 0))
456+
freelist.ReleasePendingPages()
457+
requirePages(t, freelist, common.Pgids{5, 6, 8}, common.Pgids{})
458+
buf := make([]byte, 4096)
459+
p := common.LoadPage(buf)
460+
freelist.Write(p)
461+
462+
freelist.Free(common.Txid(3), common.NewPage(3, common.LeafPageFlag, 0, 1))
463+
freelist.Free(common.Txid(3), common.NewPage(10, common.LeafPageFlag, 0, 2))
464+
requirePages(t, freelist, common.Pgids{5, 6, 8}, common.Pgids{3, 4, 10, 11, 12})
465+
466+
otherBuf := make([]byte, 4096)
467+
px := common.LoadPage(otherBuf)
468+
freelist.Write(px)
469+
470+
loadFreeList := newTestFreelist()
471+
loadFreeList.Init([]common.Pgid{})
472+
loadFreeList.Read(px)
473+
requirePages(t, loadFreeList, common.Pgids{3, 4, 5, 6, 8, 10, 11, 12}, common.Pgids{})
474+
// restore the original freelist again
475+
loadFreeList.Reload(p)
476+
requirePages(t, loadFreeList, common.Pgids{5, 6, 8}, common.Pgids{})
477+
478+
// reload another page with different free pages to test we are deduplicating the free pages with the pending ones correctly
479+
freelist = newTestFreelist()
480+
freelist.Init([]common.Pgid{})
481+
freelist.Free(common.Txid(5), common.NewPage(5, common.LeafPageFlag, 0, 4))
482+
freelist.Reload(p)
483+
requirePages(t, freelist, common.Pgids{}, common.Pgids{5, 6, 7, 8, 9})
484+
}
485+
486+
// tests the loading and reloading from physical pages
487+
func TestFreelist_E2E_SerDe_HappyPath(t *testing.T) {
488+
freelist := newTestFreelist()
489+
freelist.Init([]common.Pgid{})
490+
freelist.Free(common.Txid(2), common.NewPage(5, common.LeafPageFlag, 0, 1))
491+
freelist.Free(common.Txid(2), common.NewPage(8, common.LeafPageFlag, 0, 0))
492+
freelist.ReleasePendingPages()
493+
requirePages(t, freelist, common.Pgids{5, 6, 8}, common.Pgids{})
494+
495+
freelist.Free(common.Txid(3), common.NewPage(3, common.LeafPageFlag, 0, 1))
496+
freelist.Free(common.Txid(3), common.NewPage(10, common.LeafPageFlag, 0, 2))
497+
requirePages(t, freelist, common.Pgids{5, 6, 8}, common.Pgids{3, 4, 10, 11, 12})
498+
499+
buf := make([]byte, 4096)
500+
p := common.LoadPage(buf)
501+
require.Equal(t, 80, freelist.EstimatedWritePageSize())
502+
freelist.Write(p)
503+
504+
loadFreeList := newTestFreelist()
505+
loadFreeList.Init([]common.Pgid{})
506+
loadFreeList.Read(p)
507+
requirePages(t, loadFreeList, common.Pgids{3, 4, 5, 6, 8, 10, 11, 12}, common.Pgids{})
508+
}
509+
510+
// tests the loading of a freelist against other implementations with various sizes
511+
func TestFreelist_E2E_SerDe_AcrossImplementations(t *testing.T) {
512+
testSizes := []int{0, 1, 10, 100, 1000, math.MaxUint16, math.MaxUint16 + 1, math.MaxUint16 * 2}
513+
for _, size := range testSizes {
514+
t.Run(fmt.Sprintf("n=%d", size), func(t *testing.T) {
515+
freelist := newTestFreelist()
516+
expectedFreePgids := common.Pgids{}
517+
for i := 0; i < size; i++ {
518+
pgid := common.Pgid(i + 2)
519+
freelist.Free(common.Txid(1), common.NewPage(pgid, common.LeafPageFlag, 0, 0))
520+
expectedFreePgids = append(expectedFreePgids, pgid)
521+
}
522+
freelist.ReleasePendingPages()
523+
requirePages(t, freelist, expectedFreePgids, common.Pgids{})
524+
buf := make([]byte, freelist.EstimatedWritePageSize())
525+
p := common.LoadPage(buf)
526+
freelist.Write(p)
527+
528+
for n, loadFreeList := range map[string]Interface{
529+
"hashmap": NewHashMapFreelist(),
530+
"array": NewArrayFreelist(),
531+
} {
532+
t.Run(n, func(t *testing.T) {
533+
loadFreeList.Read(p)
534+
requirePages(t, loadFreeList, expectedFreePgids, common.Pgids{})
535+
})
536+
}
537+
})
538+
}
539+
}
540+
541+
func requirePages(t *testing.T, f Interface, freePageIds common.Pgids, pendingPageIds common.Pgids) {
542+
require.Equal(t, f.FreeCount()+f.PendingCount(), f.Count())
543+
require.Equalf(t, freePageIds, f.freePageIds(), "unexpected free pages")
544+
require.Equal(t, len(freePageIds), f.FreeCount())
545+
546+
pp := allPendingPages(f.pendingPageIds())
547+
require.Equalf(t, pendingPageIds, pp, "unexpected pending pages")
548+
require.Equal(t, len(pp), f.PendingCount())
549+
550+
for _, pgid := range f.freePageIds() {
551+
require.Truef(t, f.Freed(pgid), "expected free page to return true on Freed")
552+
}
553+
554+
for _, pgid := range pp {
555+
require.Truef(t, f.Freed(pgid), "expected pending page to return true on Freed")
556+
}
557+
}
558+
559+
func allPendingPages(p map[common.Txid]*txPending) common.Pgids {
560+
pgids := common.Pgids{}
561+
for _, pending := range p {
562+
pgids = append(pgids, pending.ids...)
563+
}
564+
sort.Sort(pgids)
565+
return pgids
566+
}
567+
269568
func Benchmark_FreelistRelease10K(b *testing.B) { benchmark_FreelistRelease(b, 10000) }
270569
func Benchmark_FreelistRelease100K(b *testing.B) { benchmark_FreelistRelease(b, 100000) }
271570
func Benchmark_FreelistRelease1000K(b *testing.B) { benchmark_FreelistRelease(b, 1000000) }

internal/freelist/hashmap_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import (
1111
"go.etcd.io/bbolt/internal/common"
1212
)
1313

14+
func TestFreelistHashmap_init_panics(t *testing.T) {
15+
f := NewHashMapFreelist()
16+
require.Panics(t, func() {
17+
// init expects sorted input
18+
f.Init([]common.Pgid{25, 5})
19+
})
20+
}
21+
1422
func TestFreelistHashmap_allocate(t *testing.T) {
1523
f := NewHashMapFreelist()
1624

0 commit comments

Comments
 (0)