diff --git a/internal/freelist/array.go b/internal/freelist/array.go index 93ccc5edc..0cc1ba715 100644 --- a/internal/freelist/array.go +++ b/internal/freelist/array.go @@ -101,6 +101,7 @@ func (f *array) mergeSpans(ids common.Pgids) { func NewArrayFreelist() Interface { a := &array{ shared: newShared(), + ids: []common.Pgid{}, } a.Interface = a return a diff --git a/internal/freelist/freelist.go b/internal/freelist/freelist.go index 3d77d8f94..25160726a 100644 --- a/internal/freelist/freelist.go +++ b/internal/freelist/freelist.go @@ -11,7 +11,7 @@ type ReadWriter interface { // Write writes the freelist into the given page. Write(page *common.Page) - // EstimatedWritePageSize returns the size of the freelist after serialization in Write. + // EstimatedWritePageSize returns the size in bytes of the freelist after serialization in Write. // This should never underestimate the size. EstimatedWritePageSize() int } @@ -46,13 +46,14 @@ type Interface interface { ReleasePendingPages() // Free releases a page and its overflow for a given transaction id. - // If the page is already free then a panic will occur. + // If the page is already free or is one of the meta pages, then a panic will occur. Free(txId common.Txid, p *common.Page) // Freed returns whether a given page is in the free list. Freed(pgId common.Pgid) bool // Rollback removes the pages from a given pending tx. + // Should always be followed by Reload or NoSyncReload from last freelist state. Rollback(txId common.Txid) // Copyall copies a list of all free ids and all pending ids in one sorted list. @@ -65,7 +66,7 @@ type Interface interface { // NoSyncReload reads the freelist from Pgids and filters out pending items. NoSyncReload(pgIds common.Pgids) - // freePageIds returns the IDs of all free pages. + // freePageIds returns the IDs of all free pages. No free pages returns an empty slice. freePageIds() common.Pgids // pendingPageIds returns all pending pages by transaction id. diff --git a/internal/freelist/freelist_test.go b/internal/freelist/freelist_test.go index df7c7697e..67cef9b49 100644 --- a/internal/freelist/freelist_test.go +++ b/internal/freelist/freelist_test.go @@ -1,13 +1,19 @@ package freelist import ( + "fmt" + "math" "math/rand" "os" "reflect" + "slices" "sort" "testing" + "testing/quick" "unsafe" + "github.com/stretchr/testify/require" + "go.etcd.io/bbolt/internal/common" ) @@ -32,6 +38,56 @@ func TestFreelist_free_overflow(t *testing.T) { } } +// 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)) + + // we still hold an allotx reference to page 12 through txid 99 - let's try to free it again + 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() @@ -85,7 +141,7 @@ func TestFreelist_releaseRange(t *testing.T) { title: "Single pending outsize minimum end range", pagesIn: []testPage{{id: 3, n: 1, allocTxn: 100, freeTxn: 200}}, releaseRanges: []testRange{{1, 199}}, - wantFree: nil, + wantFree: []common.Pgid{}, }, { title: "Single pending with minimum begin range", @@ -97,7 +153,7 @@ func TestFreelist_releaseRange(t *testing.T) { title: "Single pending outside minimum begin range", pagesIn: []testPage{{id: 3, n: 1, allocTxn: 100, freeTxn: 200}}, releaseRanges: []testRange{{101, 300}}, - wantFree: nil, + wantFree: []common.Pgid{}, }, { title: "Single pending in minimum range", @@ -109,7 +165,7 @@ func TestFreelist_releaseRange(t *testing.T) { 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: nil, + wantFree: []common.Pgid{}, }, { title: "Adjacent pending and read transactions at 199, 200", @@ -122,7 +178,7 @@ func TestFreelist_releaseRange(t *testing.T) { {200, 199}, // Simulate the ranges db.freePages might produce. {201, 300}, }, - wantFree: nil, + wantFree: []common.Pgid{}, }, { title: "Out of order ranges", @@ -135,7 +191,7 @@ func TestFreelist_releaseRange(t *testing.T) { {201, 200}, {200, 200}, }, - wantFree: nil, + wantFree: []common.Pgid{}, }, { title: "Multiple pending, read transaction at 150", @@ -179,6 +235,30 @@ func TestFreelist_releaseRange(t *testing.T) { } } +// 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. @@ -202,6 +282,18 @@ func TestFreelist_read(t *testing.T) { } } +// 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. @@ -225,6 +317,267 @@ func TestFreelist_write(t *testing.T) { } } +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) + } +} + +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{}) + + // allocate, free and rollback + allocate := freelist.Allocate(common.Txid(5), 2) + require.NotEqual(t, 0, allocate) + freelist.Free(common.Txid(5), common.NewPage(allocate, common.LeafPageFlag, 0, 1)) + requirePages(t, freelist, common.Pgids{15, 16}, common.Pgids{13, 14}) + freelist.Rollback(common.Txid(5)) + // this is a bit of cheating, as we basically set what we expect later + // FIXME(thomas): without the reload we would have a divergent cache and freelist + freelist.NoSyncReload(common.Pgids{13, 14, 15, 16}) + requirePages(t, freelist, common.Pgids{13, 14, 15, 16}, common.Pgids{}) +} + +func TestFreelist_E2E_ReadOnlyTxTracking(t *testing.T) { + freelist := newTestFreelist() + freelist.Init([]common.Pgid{}) + freelist.Free(common.Txid(10), common.NewPage(10, common.LeafPageFlag, 0, 2)) + freelist.Free(common.Txid(11), common.NewPage(20, common.LeafPageFlag, 0, 2)) + freelist.Free(common.Txid(12), common.NewPage(30, common.LeafPageFlag, 0, 2)) + requirePages(t, freelist, common.Pgids{}, common.Pgids{10, 11, 12, 20, 21, 22, 30, 31, 32}) + + freelist.AddReadonlyTXID(11) + freelist.ReleasePendingPages() + requirePages(t, freelist, common.Pgids{10, 11, 12}, common.Pgids{20, 21, 22, 30, 31, 32}) + + // this should be a no-op, as we still have a read TX open with id 11 + freelist.AddReadonlyTXID(12) + freelist.ReleasePendingPages() + requirePages(t, freelist, common.Pgids{10, 11, 12}, common.Pgids{20, 21, 22, 30, 31, 32}) + + // now 12 should be the latest + freelist.RemoveReadonlyTXID(11) + freelist.AddReadonlyTXID(12) + freelist.ReleasePendingPages() + requirePages(t, freelist, common.Pgids{10, 11, 12, 20, 21, 22}, common.Pgids{30, 31, 32}) + + // 12 was registered twice, so we also have to remove it twice to have an effect + freelist.RemoveReadonlyTXID(12) + freelist.AddReadonlyTXID(13) + freelist.ReleasePendingPages() + requirePages(t, freelist, common.Pgids{10, 11, 12, 20, 21, 22}, common.Pgids{30, 31, 32}) + + freelist.RemoveReadonlyTXID(12) + freelist.ReleasePendingPages() + requirePages(t, freelist, common.Pgids{10, 11, 12, 20, 21, 22, 30, 31, 32}, common.Pgids{}) +} + +// 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) } @@ -252,26 +605,6 @@ func randomPgids(n int) []common.Pgid { 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() - var 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" { diff --git a/internal/freelist/hashmap.go b/internal/freelist/hashmap.go index a6bad8976..213364e32 100644 --- a/internal/freelist/hashmap.go +++ b/internal/freelist/hashmap.go @@ -117,7 +117,7 @@ func (f *hashMap) FreeCount() int { func (f *hashMap) freePageIds() common.Pgids { count := f.FreeCount() if count == 0 { - return nil + return []common.Pgid{} } m := make([]common.Pgid, 0, count) diff --git a/internal/freelist/hashmap_test.go b/internal/freelist/hashmap_test.go index 32cc5dfa0..55060fa47 100644 --- a/internal/freelist/hashmap_test.go +++ b/internal/freelist/hashmap_test.go @@ -6,9 +6,19 @@ import ( "sort" "testing" + "github.com/stretchr/testify/require" + "go.etcd.io/bbolt/internal/common" ) +func TestFreelistHashmap_init_panics(t *testing.T) { + f := NewHashMapFreelist() + require.Panics(t, func() { + // init expects sorted input + f.Init([]common.Pgid{25, 5}) + }) +} + func TestFreelistHashmap_allocate(t *testing.T) { f := NewHashMapFreelist() diff --git a/internal/freelist/shared.go b/internal/freelist/shared.go index 9914cc7af..0a7526a34 100644 --- a/internal/freelist/shared.go +++ b/internal/freelist/shared.go @@ -87,7 +87,7 @@ func (t *shared) Rollback(txid common.Txid) { if txp == nil { return } - var m common.Pgids + m := common.Pgids{} for i, pgid := range txp.ids { delete(t.cache, pgid) tx := txp.alloctx[i] @@ -164,7 +164,7 @@ func (t *shared) releaseRange(begin, end common.Txid) { if begin > end { return } - var m common.Pgids + m := common.Pgids{} for tid, txp := range t.pending { if tid < begin || tid > end { continue @@ -205,25 +205,7 @@ func (t *shared) Copyall(dst []common.Pgid) { func (t *shared) Reload(p *common.Page) { t.Read(p) - - // Build a cache of only pending pages. - pcache := make(map[common.Pgid]bool) - for _, txp := range t.pending { - for _, pendingID := range txp.ids { - pcache[pendingID] = true - } - } - - // Check each page in the freelist and build a new available freelist - // with any pages not in the pending lists. - var a []common.Pgid - for _, id := range t.freePageIds() { - if !pcache[id] { - a = append(a, id) - } - } - - t.Init(a) + t.NoSyncReload(t.freePageIds()) } func (t *shared) NoSyncReload(pgIds common.Pgids) { @@ -237,7 +219,7 @@ func (t *shared) NoSyncReload(pgIds common.Pgids) { // Check each page in the freelist and build a new available freelist // with any pages not in the pending lists. - var a []common.Pgid + a := []common.Pgid{} for _, id := range pgIds { if !pcache[id] { a = append(a, id) @@ -271,7 +253,7 @@ func (t *shared) Read(p *common.Page) { // Copy the list of page ids from the freelist. if len(ids) == 0 { - t.Init(nil) + t.Init([]common.Pgid{}) } else { // copy the ids, so we don't modify on the freelist page directly idsCopy := make([]common.Pgid, len(ids))