package freelist import ( "fmt" "math" "math/rand" "os" "reflect" "slices" "sort" "testing" "testing/quick" "unsafe" "github.com/stretchr/testify/require" "github.com/tutus-one/tutus-bolt/internal/common" ) // TestFreelistType is used as a env variable for test to indicate the backend type const TestFreelistType = "TEST_FREELIST_TYPE" // Ensure that a page is added to a transaction's freelist. func TestFreelist_free(t *testing.T) { f := newTestFreelist() f.Free(100, common.NewPage(12, 0, 0, 0)) if !reflect.DeepEqual([]common.Pgid{12}, f.pendingPageIds()[100].ids) { t.Fatalf("exp=%v; got=%v", []common.Pgid{12}, f.pendingPageIds()[100].ids) } } // Ensure that a page and its overflow is added to a transaction's freelist. func TestFreelist_free_overflow(t *testing.T) { f := newTestFreelist() f.Free(100, common.NewPage(12, 0, 0, 3)) if exp := []common.Pgid{12, 13, 14, 15}; !reflect.DeepEqual(exp, f.pendingPageIds()[100].ids) { t.Fatalf("exp=%v; got=%v", exp, f.pendingPageIds()[100].ids) } } // Ensure that double freeing a page is causing a panic func TestFreelist_free_double_free_panics(t *testing.T) { f := newTestFreelist() f.Free(100, common.NewPage(12, 0, 0, 3)) require.Panics(t, func() { f.Free(100, common.NewPage(12, 0, 0, 3)) }) } // Ensure that attempting to free the meta page panics func TestFreelist_free_meta_panics(t *testing.T) { f := newTestFreelist() require.Panics(t, func() { f.Free(100, common.NewPage(0, 0, 0, 0)) }) require.Panics(t, func() { f.Free(100, common.NewPage(1, 0, 0, 0)) }) } func TestFreelist_free_freelist(t *testing.T) { f := newTestFreelist() f.Free(100, common.NewPage(12, common.FreelistPageFlag, 0, 0)) pp := f.pendingPageIds()[100] require.Equal(t, []common.Pgid{12}, pp.ids) require.Equal(t, []common.Txid{0}, pp.alloctx) } func TestFreelist_free_freelist_alloctx(t *testing.T) { f := newTestFreelist() f.Free(100, common.NewPage(12, common.FreelistPageFlag, 0, 0)) f.Rollback(100) require.Empty(t, f.freePageIds()) require.Empty(t, f.pendingPageIds()) require.False(t, f.Freed(12)) f.Free(101, common.NewPage(12, common.FreelistPageFlag, 0, 0)) require.True(t, f.Freed(12)) if exp := []common.Pgid{12}; !reflect.DeepEqual(exp, f.pendingPageIds()[101].ids) { t.Fatalf("exp=%v; got=%v", exp, f.pendingPageIds()[101].ids) } f.ReleasePendingPages() require.True(t, f.Freed(12)) require.Empty(t, f.pendingPageIds()) if exp := common.Pgids([]common.Pgid{12}); !reflect.DeepEqual(exp, f.freePageIds()) { t.Fatalf("exp=%v; got=%v", exp, f.freePageIds()) } } // Ensure that a transaction's free pages can be released. func TestFreelist_release(t *testing.T) { f := newTestFreelist() f.Free(100, common.NewPage(12, 0, 0, 1)) f.Free(100, common.NewPage(9, 0, 0, 0)) f.Free(102, common.NewPage(39, 0, 0, 0)) f.release(100) f.release(101) if exp := common.Pgids([]common.Pgid{9, 12, 13}); !reflect.DeepEqual(exp, f.freePageIds()) { t.Fatalf("exp=%v; got=%v", exp, f.freePageIds()) } f.release(102) if exp := common.Pgids([]common.Pgid{9, 12, 13, 39}); !reflect.DeepEqual(exp, f.freePageIds()) { t.Fatalf("exp=%v; got=%v", exp, f.freePageIds()) } } // Ensure that releaseRange handles boundary conditions correctly func TestFreelist_releaseRange(t *testing.T) { type testRange struct { begin, end common.Txid } type testPage struct { id common.Pgid n int allocTxn common.Txid freeTxn common.Txid } var releaseRangeTests = []struct { title string pagesIn []testPage releaseRanges []testRange wantFree []common.Pgid }{ { title: "Single pending in range", pagesIn: []testPage{{id: 3, n: 1, allocTxn: 100, freeTxn: 200}}, releaseRanges: []testRange{{1, 300}}, wantFree: []common.Pgid{3}, }, { title: "Single pending with minimum end range", pagesIn: []testPage{{id: 3, n: 1, allocTxn: 100, freeTxn: 200}}, releaseRanges: []testRange{{1, 200}}, wantFree: []common.Pgid{3}, }, { title: "Single pending outsize minimum end range", pagesIn: []testPage{{id: 3, n: 1, allocTxn: 100, freeTxn: 200}}, releaseRanges: []testRange{{1, 199}}, wantFree: []common.Pgid{}, }, { title: "Single pending with minimum begin range", pagesIn: []testPage{{id: 3, n: 1, allocTxn: 100, freeTxn: 200}}, releaseRanges: []testRange{{100, 300}}, wantFree: []common.Pgid{3}, }, { title: "Single pending outside minimum begin range", pagesIn: []testPage{{id: 3, n: 1, allocTxn: 100, freeTxn: 200}}, releaseRanges: []testRange{{101, 300}}, wantFree: []common.Pgid{}, }, { title: "Single pending in minimum range", pagesIn: []testPage{{id: 3, n: 1, allocTxn: 199, freeTxn: 200}}, releaseRanges: []testRange{{199, 200}}, wantFree: []common.Pgid{3}, }, { title: "Single pending and read transaction at 199", pagesIn: []testPage{{id: 3, n: 1, allocTxn: 199, freeTxn: 200}}, releaseRanges: []testRange{{100, 198}, {200, 300}}, wantFree: []common.Pgid{}, }, { title: "Adjacent pending and read transactions at 199, 200", pagesIn: []testPage{ {id: 3, n: 1, allocTxn: 199, freeTxn: 200}, {id: 4, n: 1, allocTxn: 200, freeTxn: 201}, }, releaseRanges: []testRange{ {100, 198}, {200, 199}, // Simulate the ranges db.freePages might produce. {201, 300}, }, wantFree: []common.Pgid{}, }, { title: "Out of order ranges", pagesIn: []testPage{ {id: 3, n: 1, allocTxn: 199, freeTxn: 200}, {id: 4, n: 1, allocTxn: 200, freeTxn: 201}, }, releaseRanges: []testRange{ {201, 199}, {201, 200}, {200, 200}, }, wantFree: []common.Pgid{}, }, { title: "Multiple pending, read transaction at 150", pagesIn: []testPage{ {id: 3, n: 1, allocTxn: 100, freeTxn: 200}, {id: 4, n: 1, allocTxn: 100, freeTxn: 125}, {id: 5, n: 1, allocTxn: 125, freeTxn: 150}, {id: 6, n: 1, allocTxn: 125, freeTxn: 175}, {id: 7, n: 2, allocTxn: 150, freeTxn: 175}, {id: 9, n: 2, allocTxn: 175, freeTxn: 200}, }, releaseRanges: []testRange{{50, 149}, {151, 300}}, wantFree: []common.Pgid{4, 9, 10}, }, } for _, c := range releaseRangeTests { t.Run(c.title, func(t *testing.T) { f := newTestFreelist() var ids []common.Pgid for _, p := range c.pagesIn { for i := uint64(0); i < uint64(p.n); i++ { ids = append(ids, common.Pgid(uint64(p.id)+i)) } } f.Init(ids) for _, p := range c.pagesIn { f.Allocate(p.allocTxn, p.n) } for _, p := range c.pagesIn { f.Free(p.freeTxn, common.NewPage(p.id, 0, 0, uint32(p.n-1))) } for _, r := range c.releaseRanges { f.releaseRange(r.begin, r.end) } require.Equal(t, common.Pgids(c.wantFree), f.freePageIds()) }) } } func TestFreeList_init(t *testing.T) { buf := make([]byte, 4096) f := newTestFreelist() f.Init(common.Pgids{5, 6, 8}) p := common.LoadPage(buf) f.Write(p) f2 := newTestFreelist() f2.Read(p) require.Equal(t, common.Pgids{5, 6, 8}, f2.freePageIds()) // When initializing the freelist with an empty list of page ID, // it should reset the freelist page IDs. f2.Init([]common.Pgid{}) require.Equal(t, common.Pgids{}, f2.freePageIds()) } func TestFreeList_reload(t *testing.T) { buf := make([]byte, 4096) f := newTestFreelist() f.Init(common.Pgids{5, 6, 8}) p := common.LoadPage(buf) f.Write(p) f2 := newTestFreelist() f2.Read(p) require.Equal(t, common.Pgids{5, 6, 8}, f2.freePageIds()) f2.Free(common.Txid(5), common.NewPage(10, common.LeafPageFlag, 0, 2)) // reload shouldn't affect the pending list f2.Reload(p) require.Equal(t, common.Pgids{5, 6, 8}, f2.freePageIds()) require.Equal(t, []common.Pgid{10, 11, 12}, f2.pendingPageIds()[5].ids) } // Ensure that the txIDx swap, less and len are properly implemented func TestTxidSorting(t *testing.T) { require.NoError(t, quick.Check(func(a []uint64) bool { var txids []common.Txid for _, txid := range a { txids = append(txids, common.Txid(txid)) } sort.Sort(txIDx(txids)) var r []uint64 for _, txid := range txids { r = append(r, uint64(txid)) } if !slices.IsSorted(r) { t.Errorf("txids were not sorted correctly=%v", txids) return false } return true }, nil)) } // Ensure that a freelist can deserialize from a freelist page. func TestFreelist_read(t *testing.T) { // Create a page. var buf [4096]byte page := (*common.Page)(unsafe.Pointer(&buf[0])) page.SetFlags(common.FreelistPageFlag) page.SetCount(2) // Insert 2 page ids. ids := (*[3]common.Pgid)(unsafe.Pointer(uintptr(unsafe.Pointer(page)) + unsafe.Sizeof(*page))) ids[0] = 23 ids[1] = 50 // Deserialize page into a freelist. f := newTestFreelist() f.Read(page) // Ensure that there are two page ids in the freelist. if exp := common.Pgids([]common.Pgid{23, 50}); !reflect.DeepEqual(exp, f.freePageIds()) { t.Fatalf("exp=%v; got=%v", exp, f.freePageIds()) } } // Ensure that we never read a non-freelist page func TestFreelist_read_panics(t *testing.T) { buf := make([]byte, 4096) page := common.LoadPage(buf) page.SetFlags(common.BranchPageFlag) page.SetCount(2) f := newTestFreelist() require.Panics(t, func() { f.Read(page) }) } // Ensure that a freelist can serialize into a freelist page. func TestFreelist_write(t *testing.T) { // Create a freelist and write it to a page. var buf [4096]byte f := newTestFreelist() f.Init([]common.Pgid{12, 39}) f.pendingPageIds()[100] = &txPending{ids: []common.Pgid{28, 11}} f.pendingPageIds()[101] = &txPending{ids: []common.Pgid{3}} p := (*common.Page)(unsafe.Pointer(&buf[0])) f.Write(p) // Read the page back out. f2 := newTestFreelist() f2.Read(p) // Ensure that the freelist is correct. // All pages should be present and in reverse order. if exp := common.Pgids([]common.Pgid{3, 11, 12, 28, 39}); !reflect.DeepEqual(exp, f2.freePageIds()) { t.Fatalf("exp=%v; got=%v", exp, f2.freePageIds()) } } func TestFreelist_E2E_HappyPath(t *testing.T) { f := newTestFreelist() f.Init([]common.Pgid{}) requirePages(t, f, common.Pgids{}, common.Pgids{}) allocated := f.Allocate(common.Txid(1), 5) require.Equal(t, common.Pgid(0), allocated) // tx.go may now allocate more space, and eventually we need to delete a page again f.Free(common.Txid(2), common.NewPage(5, common.LeafPageFlag, 0, 0)) f.Free(common.Txid(2), common.NewPage(3, common.LeafPageFlag, 0, 0)) f.Free(common.Txid(2), common.NewPage(8, common.LeafPageFlag, 0, 0)) // the above will only mark the pages as pending, so free pages should not return anything requirePages(t, f, common.Pgids{}, common.Pgids{3, 5, 8}) // someone wants to do a read on top of the next tx id f.AddReadonlyTXID(common.Txid(3)) // this should free the above pages for tx 2 entirely f.ReleasePendingPages() requirePages(t, f, common.Pgids{3, 5, 8}, common.Pgids{}) // no span of two pages available should yield a zero-page result require.Equal(t, common.Pgid(0), f.Allocate(common.Txid(4), 2)) // we should be able to allocate those pages independently however, // map and array differ in the order they return the pages expectedPgids := map[common.Pgid]struct{}{3: {}, 5: {}, 8: {}} for i := 0; i < 3; i++ { allocated = f.Allocate(common.Txid(4), 1) require.Contains(t, expectedPgids, allocated, "expected to find pgid %d", allocated) require.False(t, f.Freed(allocated)) delete(expectedPgids, allocated) } require.Emptyf(t, expectedPgids, "unexpectedly more than one page was still found") // no more free pages to allocate require.Equal(t, common.Pgid(0), f.Allocate(common.Txid(4), 1)) } func TestFreelist_E2E_MultiSpanOverflows(t *testing.T) { f := newTestFreelist() f.Init([]common.Pgid{}) f.Free(common.Txid(10), common.NewPage(20, common.LeafPageFlag, 0, 1)) f.Free(common.Txid(10), common.NewPage(25, common.LeafPageFlag, 0, 2)) f.Free(common.Txid(10), common.NewPage(35, common.LeafPageFlag, 0, 3)) f.Free(common.Txid(10), common.NewPage(39, common.LeafPageFlag, 0, 2)) f.Free(common.Txid(10), common.NewPage(45, common.LeafPageFlag, 0, 4)) requirePages(t, f, common.Pgids{}, common.Pgids{20, 21, 25, 26, 27, 35, 36, 37, 38, 39, 40, 41, 45, 46, 47, 48, 49}) f.ReleasePendingPages() requirePages(t, f, common.Pgids{20, 21, 25, 26, 27, 35, 36, 37, 38, 39, 40, 41, 45, 46, 47, 48, 49}, common.Pgids{}) // that sequence, regardless of implementation, should always yield the same blocks of pages allocSequence := []int{7, 5, 3, 2} expectedSpanStarts := []common.Pgid{35, 45, 25, 20} for i, pageNums := range allocSequence { allocated := f.Allocate(common.Txid(11), pageNums) require.Equal(t, expectedSpanStarts[i], allocated) // ensure all pages in that span are not considered free anymore for i := 0; i < pageNums; i++ { require.False(t, f.Freed(allocated+common.Pgid(i))) } } } func TestFreelist_E2E_Rollbacks(t *testing.T) { freelist := newTestFreelist() freelist.Init([]common.Pgid{}) freelist.Free(common.Txid(2), common.NewPage(5, common.LeafPageFlag, 0, 1)) freelist.Free(common.Txid(2), common.NewPage(8, common.LeafPageFlag, 0, 0)) requirePages(t, freelist, common.Pgids{}, common.Pgids{5, 6, 8}) freelist.Rollback(common.Txid(2)) requirePages(t, freelist, common.Pgids{}, common.Pgids{}) // unknown transaction should not trigger anything freelist.Free(common.Txid(4), common.NewPage(13, common.LeafPageFlag, 0, 3)) requirePages(t, freelist, common.Pgids{}, common.Pgids{13, 14, 15, 16}) freelist.ReleasePendingPages() requirePages(t, freelist, common.Pgids{13, 14, 15, 16}, common.Pgids{}) freelist.Rollback(common.Txid(1337)) requirePages(t, freelist, common.Pgids{13, 14, 15, 16}, common.Pgids{}) } func TestFreelist_E2E_RollbackPanics(t *testing.T) { freelist := newTestFreelist() freelist.Init([]common.Pgid{5}) requirePages(t, freelist, common.Pgids{5}, common.Pgids{}) _ = freelist.Allocate(common.Txid(5), 1) require.Panics(t, func() { // depending on the verification level, either should panic freelist.Free(common.Txid(5), common.NewPage(5, common.LeafPageFlag, 0, 0)) freelist.Rollback(5) }) } // tests the reloading from another physical page func TestFreelist_E2E_Reload(t *testing.T) { freelist := newTestFreelist() freelist.Init([]common.Pgid{}) freelist.Free(common.Txid(2), common.NewPage(5, common.LeafPageFlag, 0, 1)) freelist.Free(common.Txid(2), common.NewPage(8, common.LeafPageFlag, 0, 0)) freelist.ReleasePendingPages() requirePages(t, freelist, common.Pgids{5, 6, 8}, common.Pgids{}) buf := make([]byte, 4096) p := common.LoadPage(buf) freelist.Write(p) freelist.Free(common.Txid(3), common.NewPage(3, common.LeafPageFlag, 0, 1)) freelist.Free(common.Txid(3), common.NewPage(10, common.LeafPageFlag, 0, 2)) requirePages(t, freelist, common.Pgids{5, 6, 8}, common.Pgids{3, 4, 10, 11, 12}) otherBuf := make([]byte, 4096) px := common.LoadPage(otherBuf) freelist.Write(px) loadFreeList := newTestFreelist() loadFreeList.Init([]common.Pgid{}) loadFreeList.Read(px) requirePages(t, loadFreeList, common.Pgids{3, 4, 5, 6, 8, 10, 11, 12}, common.Pgids{}) // restore the original freelist again loadFreeList.Reload(p) requirePages(t, loadFreeList, common.Pgids{5, 6, 8}, common.Pgids{}) // reload another page with different free pages to test we are deduplicating the free pages with the pending ones correctly freelist = newTestFreelist() freelist.Init([]common.Pgid{}) freelist.Free(common.Txid(5), common.NewPage(5, common.LeafPageFlag, 0, 4)) freelist.Reload(p) requirePages(t, freelist, common.Pgids{}, common.Pgids{5, 6, 7, 8, 9}) } // tests the loading and reloading from physical pages func TestFreelist_E2E_SerDe_HappyPath(t *testing.T) { freelist := newTestFreelist() freelist.Init([]common.Pgid{}) freelist.Free(common.Txid(2), common.NewPage(5, common.LeafPageFlag, 0, 1)) freelist.Free(common.Txid(2), common.NewPage(8, common.LeafPageFlag, 0, 0)) freelist.ReleasePendingPages() requirePages(t, freelist, common.Pgids{5, 6, 8}, common.Pgids{}) freelist.Free(common.Txid(3), common.NewPage(3, common.LeafPageFlag, 0, 1)) freelist.Free(common.Txid(3), common.NewPage(10, common.LeafPageFlag, 0, 2)) requirePages(t, freelist, common.Pgids{5, 6, 8}, common.Pgids{3, 4, 10, 11, 12}) buf := make([]byte, 4096) p := common.LoadPage(buf) require.Equal(t, 80, freelist.EstimatedWritePageSize()) freelist.Write(p) loadFreeList := newTestFreelist() loadFreeList.Init([]common.Pgid{}) loadFreeList.Read(p) requirePages(t, loadFreeList, common.Pgids{3, 4, 5, 6, 8, 10, 11, 12}, common.Pgids{}) } // tests the loading of a freelist against other implementations with various sizes func TestFreelist_E2E_SerDe_AcrossImplementations(t *testing.T) { testSizes := []int{0, 1, 10, 100, 1000, math.MaxUint16, math.MaxUint16 + 1, math.MaxUint16 * 2} for _, size := range testSizes { t.Run(fmt.Sprintf("n=%d", size), func(t *testing.T) { freelist := newTestFreelist() expectedFreePgids := common.Pgids{} for i := 0; i < size; i++ { pgid := common.Pgid(i + 2) freelist.Free(common.Txid(1), common.NewPage(pgid, common.LeafPageFlag, 0, 0)) expectedFreePgids = append(expectedFreePgids, pgid) } freelist.ReleasePendingPages() requirePages(t, freelist, expectedFreePgids, common.Pgids{}) buf := make([]byte, freelist.EstimatedWritePageSize()) p := common.LoadPage(buf) freelist.Write(p) for n, loadFreeList := range map[string]Interface{ "hashmap": NewHashMapFreelist(), "array": NewArrayFreelist(), } { t.Run(n, func(t *testing.T) { loadFreeList.Read(p) requirePages(t, loadFreeList, expectedFreePgids, common.Pgids{}) }) } }) } } func requirePages(t *testing.T, f Interface, freePageIds common.Pgids, pendingPageIds common.Pgids) { require.Equal(t, f.FreeCount()+f.PendingCount(), f.Count()) require.Equalf(t, freePageIds, f.freePageIds(), "unexpected free pages") require.Equal(t, len(freePageIds), f.FreeCount()) pp := allPendingPages(f.pendingPageIds()) require.Equalf(t, pendingPageIds, pp, "unexpected pending pages") require.Equal(t, len(pp), f.PendingCount()) for _, pgid := range f.freePageIds() { require.Truef(t, f.Freed(pgid), "expected free page to return true on Freed") } for _, pgid := range pp { require.Truef(t, f.Freed(pgid), "expected pending page to return true on Freed") } } func allPendingPages(p map[common.Txid]*txPending) common.Pgids { pgids := common.Pgids{} for _, pending := range p { pgids = append(pgids, pending.ids...) } sort.Sort(pgids) return pgids } func Benchmark_FreelistRelease10K(b *testing.B) { benchmark_FreelistRelease(b, 10000) } func Benchmark_FreelistRelease100K(b *testing.B) { benchmark_FreelistRelease(b, 100000) } func Benchmark_FreelistRelease1000K(b *testing.B) { benchmark_FreelistRelease(b, 1000000) } func Benchmark_FreelistRelease10000K(b *testing.B) { benchmark_FreelistRelease(b, 10000000) } func benchmark_FreelistRelease(b *testing.B, size int) { ids := randomPgids(size) pending := randomPgids(len(ids) / 400) b.ResetTimer() for i := 0; i < b.N; i++ { txp := &txPending{ids: pending} f := newTestFreelist() f.pendingPageIds()[1] = txp f.Init(ids) f.release(1) } } func randomPgids(n int) []common.Pgid { pgids := make(common.Pgids, n) for i := range pgids { pgids[i] = common.Pgid(rand.Int63()) } sort.Sort(pgids) return pgids } func Test_freelist_ReadIDs_and_getFreePageIDs(t *testing.T) { f := newTestFreelist() exp := common.Pgids([]common.Pgid{3, 4, 5, 6, 7, 9, 12, 13, 18}) f.Init(exp) if got := f.freePageIds(); !reflect.DeepEqual(exp, got) { t.Fatalf("exp=%v; got=%v", exp, got) } f2 := newTestFreelist() exp2 := []common.Pgid{} f2.Init(exp2) if got2 := f2.freePageIds(); !reflect.DeepEqual(got2, common.Pgids(exp2)) { t.Fatalf("exp2=%#v; got2=%#v", exp2, got2) } } // newTestFreelist get the freelist type from env and initial the freelist func newTestFreelist() Interface { if env := os.Getenv(TestFreelistType); env == "hashmap" { return NewHashMapFreelist() } return NewArrayFreelist() }