mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-07 00:55:55 +00:00
These commits are hard to disentagle, and doing them separately means re-modifying the same chunks of code several times before removing it, and similar things. Basically: (1) Drop the bolt backend storage. (2) Drop the blue-green wrapper that compares two backends. (3) Drop unused or barely-used Tx API components from all the remaining backends. (4) Minor related cleanup to simplify things related to these. The boltdb backend existed only to verify RBF. The blue-green wrapper was mostly used to verify RBF, but in practice we had to do a lot of working around that, and it introduced a lot of special cases. Types removed: IteratorFinder: Used only to implement the roaring iterator on top of boltdb, and to complicate the way it worked in roaring. Reverted the complications. Also unexport NewSliceContainers which is used only for that outside of roaring's internals. PortMapper from cluster_internal_test.go: Used only for a test we removed early this year. Never used for anything else. RawRoaringData: Totally unused. TxStore: Totally unused. Functions removed from Tx API, and sometimes corresponding members were removed from structs: * Dump: debugging code, I don't think I found any actually reachable paths to it. * Group: only used for debugging TxGroup stuff * IncrementOpN: only used by fragment, fragment can increment its own opN. * Options: unused? * Pointer: debugging only * Readonly: used only to decide how to handle Tx in a TxGrp, but we never add a non-readonly Tx to a TxGrp. Removed also all the corresponding write-aware stuff. * RoaringBitmapReader: Used exactly once, can just be a bm.WriteTo. * Sn (and OpenSnList): Unused * UnionInPlace: unused and conceptually-invalid; it didn't write to storage and shouldn't have, and was just "create a bitmap then call union-in-place", which we can do directly. * UseRowCache: just checked storage.UseRowCache. Other things removed: The SetRequiredForAtomicWriteTx and ClearRequiredForAtomicWriteTx functions go away, since nothing now seems to be using them? Same for holder_internal_test's `testHasBit` and `testMustNotHaveBit`, which were unused. The DBPerShard "DeleteDBPath" and "HasData" functions and related parts were mostly unused; took out the parts that were never actually being reached. Changed the API of one function to simplify special cases and remove things: * ImportRoaringBits had a special "data" argument which gave it subtly different semantics for RBF and roaring (for roaring, it could produce a roaring bitmap *with ops log*), didn't seem to be adding much. Removed corresponding "readStorageFromArchive" which is not otherwise used. Also took out various debugging/dumping functions that were unused and may have bitrotted. Dropped a test from txfactory_internal_test, and the "pjobs" code, because those two were the only things that needed Barrier and thus idem, which lets us drop two more dependencies. We already have errgroup for grouping things which want to terminate as soon as one of them errors, approximately. To do better we'd have to have context-threading, really. Unbroke the WriteFragment test for non-roaring tests and made it not roaring-only.
608 lines
15 KiB
Go
608 lines
15 KiB
Go
// Copyright 2017 Pilosa Corp.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package rbf
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"github.com/benbjohnson/immutable"
|
|
rbfcfg "github.com/molecula/featurebase/v2/rbf/cfg"
|
|
"github.com/molecula/featurebase/v2/syswrap"
|
|
)
|
|
|
|
var (
|
|
ErrClosed = errors.New("rbf: database closed")
|
|
)
|
|
|
|
// shared cursor pool across all DB instances.
|
|
// Cursors are returned on Cursor.Close().
|
|
var cursorSyncPool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return &Cursor{}
|
|
},
|
|
}
|
|
|
|
// DB options like MaxSize, FsyncEnabled, DoAllocZero
|
|
// can be set before calling DB.Open().
|
|
type DB struct {
|
|
cfg rbfcfg.Config
|
|
|
|
data []byte // database mmap
|
|
file *os.File // database file descriptor
|
|
rootRecords *immutable.SortedMap // cached root records
|
|
pageMap *PageMap // pgno-to-WALID mapping
|
|
txs map[*Tx]struct{} // active transactions
|
|
opened bool // true if open
|
|
|
|
wal []byte // wal mmap
|
|
walFile *os.File // wal file descriptor
|
|
walPageN int // wal page count
|
|
|
|
mu sync.RWMutex // general mutex
|
|
rwmu sync.Mutex // mutex for restricting single writer
|
|
haltCond *sync.Cond // condition for resuming txs after checkpoint
|
|
|
|
// Path represents the path to the database file.
|
|
Path string
|
|
}
|
|
|
|
// NewDB returns a new instance of DB.
|
|
// If cfg is nil we will use the rbfcfg.DefaultConfig().
|
|
func NewDB(path string, cfg *rbfcfg.Config) *DB {
|
|
if cfg == nil {
|
|
cfg = rbfcfg.NewDefaultConfig()
|
|
}
|
|
db := &DB{
|
|
cfg: *cfg,
|
|
txs: make(map[*Tx]struct{}),
|
|
pageMap: NewPageMap(),
|
|
Path: path,
|
|
}
|
|
db.haltCond = sync.NewCond(&db.mu)
|
|
|
|
return db
|
|
}
|
|
|
|
// DataPath returns the path to the data file for the DB.
|
|
func (db *DB) DataPath() string {
|
|
return filepath.Join(db.Path, "data")
|
|
}
|
|
|
|
// WALPath returns the path to the WAL file.
|
|
func (db *DB) WALPath() string {
|
|
return filepath.Join(db.Path, "wal")
|
|
}
|
|
|
|
func CreateDirIfNotExist(path string) {
|
|
dir := filepath.Dir(path)
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
err = os.MkdirAll(dir, 0755)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TxN returns the number of active transactions.
|
|
func (db *DB) TxN() int {
|
|
db.mu.RLock()
|
|
defer db.mu.RUnlock()
|
|
return len(db.txs)
|
|
}
|
|
|
|
// Open opens a database with the file specified in Path.
|
|
// Creates a new file if one does not already exist.
|
|
func (db *DB) Open() (err error) {
|
|
db.mu.Lock()
|
|
defer db.mu.Unlock()
|
|
|
|
if err := os.MkdirAll(db.Path, 0755); err != nil {
|
|
return err
|
|
} else if db.file, err = os.OpenFile(db.DataPath(), os.O_WRONLY|os.O_CREATE, 0666); err != nil {
|
|
return fmt.Errorf("open file: %w", err)
|
|
}
|
|
|
|
// Open read-only database mmap.
|
|
if f, err := os.OpenFile(db.DataPath(), os.O_RDONLY, 0666); err != nil {
|
|
return fmt.Errorf("open mmap file: %w", err)
|
|
} else if db.data, err = syswrap.Mmap(int(f.Fd()), 0, int(db.cfg.MaxSize), syscall.PROT_READ, syscall.MAP_SHARED); err != nil {
|
|
f.Close()
|
|
return fmt.Errorf("open mmap file: %w", err)
|
|
} else if err := f.Close(); err != nil {
|
|
return fmt.Errorf("cannot close mmap file: %w", err)
|
|
}
|
|
|
|
// Initialize file if it is too small.
|
|
if fi, err := db.file.Stat(); err != nil {
|
|
return fmt.Errorf("stat: %w", err)
|
|
} else if fi.Size() < PageSize {
|
|
if err := db.init(); err != nil {
|
|
return fmt.Errorf("init: %w", err)
|
|
}
|
|
}
|
|
|
|
// TODO(BBJ): Obtain advisory lock on file.
|
|
|
|
db.opened = true
|
|
|
|
// Open write-ahead log & checkpoint to the end since no transactions are open.
|
|
if err := db.openWAL(); err != nil {
|
|
return fmt.Errorf("wal open: %w", err)
|
|
} else if err := db.checkpoint(); err != nil {
|
|
return fmt.Errorf("checkpoint: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (db *DB) openWAL() (err error) {
|
|
// Open WAL file writer.
|
|
if db.walFile, err = os.OpenFile(db.WALPath(), os.O_WRONLY|os.O_CREATE, 0666); err != nil {
|
|
return fmt.Errorf("open wal file: %w", err)
|
|
}
|
|
|
|
// Open read-only mmap.
|
|
if f, err := os.OpenFile(db.WALPath(), os.O_RDONLY, 0666); err != nil {
|
|
return fmt.Errorf("open wal mmap file: %w", err)
|
|
} else if db.wal, err = syswrap.Mmap(int(f.Fd()), 0, int(db.cfg.MaxWALSize), syscall.PROT_READ, syscall.MAP_SHARED); err != nil {
|
|
f.Close()
|
|
return fmt.Errorf("map wal mmap file: %w", err)
|
|
} else if err := f.Close(); err != nil {
|
|
return fmt.Errorf("cannot close wal mmap file: %w", err)
|
|
}
|
|
|
|
// Determine the number of whole pages in the WAL.
|
|
var pageN int
|
|
if fi, err := db.walFile.Stat(); err != nil {
|
|
return fmt.Errorf("wal stat: %w", err)
|
|
} else {
|
|
pageN = int(fi.Size() / PageSize)
|
|
}
|
|
|
|
// Read backwards through the WAL to find the last valid meta page.
|
|
for ; pageN > 0; pageN-- {
|
|
if page, err := db.readWALPageAt(pageN - 1); err != nil {
|
|
return err
|
|
} else if IsMetaPage(page) {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Truncate WAL to the last valid meta page.
|
|
if err := db.walFile.Truncate(int64(pageN * PageSize)); err != nil {
|
|
return fmt.Errorf("wal truncate: %w", err)
|
|
} else if _, err := db.walFile.Seek(int64(pageN*PageSize), io.SeekStart); err != nil {
|
|
return fmt.Errorf("wal seek: %w", err)
|
|
}
|
|
db.walPageN = pageN
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkpoint moves all WAL pages to the main DB file.
|
|
// Must be called by a write transaction while under db.mu lock.
|
|
func (db *DB) checkpoint() error {
|
|
if !db.opened {
|
|
return nil
|
|
} else if len(db.txs) > 0 {
|
|
return nil // skip if transactions open
|
|
}
|
|
|
|
for i := 0; i < db.walPageN; i++ {
|
|
page, err := db.readWALPageAt(i)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Determine page number. Meta pages are always on zero & bitmap
|
|
// headers specify the page number of the next page in the WAL.
|
|
// All other pages have their page number in the page data.
|
|
var pgno uint32
|
|
if IsBitmapHeader(page) {
|
|
pgno = readPageNo(page)
|
|
if page, err = db.readWALPageAt(i + 1); err != nil {
|
|
return err
|
|
}
|
|
i++ // bitmaps in WAL are two pages
|
|
} else if !IsMetaPage(page) {
|
|
pgno = readPageNo(page)
|
|
}
|
|
|
|
// Write data to the data file.
|
|
if err := db.writeDBPage(pgno, page); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Ensure database file is synced and then truncate the WAL file.
|
|
if err := db.fsync(db.file); err != nil {
|
|
return fmt.Errorf("db file sync: %w", err)
|
|
} else if err := db.walFile.Truncate(0); err != nil {
|
|
return fmt.Errorf("truncate wal file: %w", err)
|
|
} else if err := db.fsync(db.walFile); err != nil {
|
|
return fmt.Errorf("wal file sync: %w", err)
|
|
} else if _, err := db.walFile.Seek(0, io.SeekStart); err != nil {
|
|
return fmt.Errorf("seek wal file: %w", err)
|
|
}
|
|
db.walPageN = 0
|
|
db.pageMap = NewPageMap()
|
|
|
|
// Notify halted tranactions that the WAL has been checkpointed.
|
|
db.haltCond.Broadcast()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Close closes the database.
|
|
func (db *DB) Close() (err error) {
|
|
// TODO(bbj): Add wait group to hang until last Tx is complete.
|
|
|
|
// Wait for writer lock.
|
|
db.rwmu.Lock()
|
|
defer db.rwmu.Unlock()
|
|
|
|
db.mu.Lock()
|
|
defer db.mu.Unlock()
|
|
|
|
db.opened = false
|
|
|
|
// Close mmap handle.
|
|
if db.data != nil {
|
|
if e := syswrap.Munmap(db.data); e != nil && err == nil {
|
|
err = e
|
|
}
|
|
db.data = nil
|
|
}
|
|
|
|
// Close writer handler.
|
|
if db.file != nil {
|
|
err = db.fsync(db.file)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if e := db.file.Close(); e != nil && err == nil {
|
|
err = e
|
|
}
|
|
db.file = nil
|
|
}
|
|
|
|
// Close WAL mmap handle.
|
|
if db.wal != nil {
|
|
if e := syswrap.Munmap(db.wal); e != nil && err == nil {
|
|
err = e
|
|
}
|
|
db.wal = nil
|
|
}
|
|
|
|
// Close wal writer handler.
|
|
if db.walFile != nil {
|
|
if e := db.walFile.Close(); e != nil && err == nil {
|
|
err = e
|
|
}
|
|
db.walFile = nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// HasData with requireOneHotBit=false returns
|
|
// hasAnyRecords true if any record has been stored,
|
|
// even if the value for that bitmap record turned out to have
|
|
// no bits hot (be all zeroes).
|
|
//
|
|
// In this case, we are taking the attempted storage
|
|
// of any named bitmap into the database as evidence
|
|
// that the db is in use, and we return hasAnyRecords true.
|
|
//
|
|
// Conversely, if requireOneHotBit is true, then a
|
|
// database consisting of only a named bitmap with
|
|
// an all zeroes (no bits hot)
|
|
// will return hasAnyRecords false. We must find at
|
|
// least a single hot bit inside the db
|
|
// in order to return hasAnyRecords true.
|
|
//
|
|
// HasData is used by backend migration.
|
|
//
|
|
// If there is a disk error we return (false, error), so always
|
|
// check the error before deciding if hasAnyRecords is valid.
|
|
//
|
|
// We will internally create and rollback a read-only
|
|
// transaction to answer this query.
|
|
func (db *DB) HasData(requireOneHotBit bool) (hasAnyRecords bool, err error) {
|
|
|
|
// Read a list of all bitmaps in Tx.
|
|
tx, err := db.Begin(false)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
records, err := tx.RootRecords()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
// Loop over each bitmap and attempt to move to the first cell.
|
|
// If we can move to a cell then we have at least one record.
|
|
|
|
for itr := records.Iterator(); !itr.Done(); {
|
|
name, _ := itr.Next()
|
|
// Fetch cursor for bitmap.
|
|
cur, err := tx.Cursor(name.(string))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer cur.Close()
|
|
|
|
if !requireOneHotBit {
|
|
return true, nil
|
|
}
|
|
// INVAR: requireOneHotBit true
|
|
|
|
// Check if we can move to the first cell.
|
|
if err := cur.First(); err == io.EOF {
|
|
continue // no data in bitmap
|
|
} else if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// Size returns the size of the database & WAL, in bytes.
|
|
func (db *DB) Size() (int64, error) {
|
|
db.mu.RLock()
|
|
defer db.mu.RUnlock()
|
|
|
|
fi, err := os.Stat(db.Path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return db.walSize() + fi.Size(), nil
|
|
}
|
|
|
|
// WALSize returns the size of the WAL, in bytes.
|
|
func (db *DB) WALSize() int64 {
|
|
db.mu.RLock()
|
|
defer db.mu.RUnlock()
|
|
return db.walSize()
|
|
}
|
|
|
|
func (db *DB) walSize() int64 {
|
|
return int64(db.walPageN * PageSize)
|
|
}
|
|
|
|
// init initializes a new database file.
|
|
func (db *DB) init() error {
|
|
if err := db.initMetaPage(); err != nil {
|
|
return fmt.Errorf("meta: %w", err)
|
|
} else if err := db.initRootRecordPage(); err != nil {
|
|
return fmt.Errorf("root record page: %w", err)
|
|
} else if err := db.initFreelistPage(); err != nil {
|
|
return fmt.Errorf("freelist page: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// initMetaPage initializes the meta page.
|
|
func (db *DB) initMetaPage() error {
|
|
|
|
page := make([]byte, PageSize)
|
|
writeMetaMagic(page)
|
|
writeMetaPageN(page, 3)
|
|
writeMetaRootRecordPageNo(page, 1)
|
|
writeMetaFreelistPageNo(page, 2)
|
|
_, err := db.file.WriteAt(page, 0*PageSize)
|
|
return err
|
|
}
|
|
|
|
// initRootRecordPage initializes the initial root record page.
|
|
func (db *DB) initRootRecordPage() error {
|
|
|
|
page := make([]byte, PageSize)
|
|
writePageNo(page, 1)
|
|
writeFlags(page, PageTypeRootRecord)
|
|
_, err := db.file.WriteAt(page, 1*PageSize)
|
|
return err
|
|
}
|
|
|
|
// initFreelistPage initializes the initial freelist btree page.
|
|
func (db *DB) initFreelistPage() error {
|
|
|
|
page := make([]byte, PageSize)
|
|
writePageNo(page, 2)
|
|
writeFlags(page, PageTypeLeaf)
|
|
_, err := db.file.WriteAt(page, 2*PageSize)
|
|
return err
|
|
}
|
|
|
|
// Begin starts a new transaction.
|
|
func (db *DB) Begin(writable bool) (_ *Tx, err error) {
|
|
// Ensure only one writable transaction at a time.
|
|
if writable {
|
|
db.rwmu.Lock()
|
|
}
|
|
|
|
// This local function is called at exit points that occur before we can
|
|
// call Rollback() which would normally release these locks.
|
|
cleanup := func() {
|
|
if writable {
|
|
db.rwmu.Unlock()
|
|
}
|
|
}
|
|
|
|
db.mu.Lock()
|
|
// note: We cannot defer db.mu.Unlock() here because
|
|
// we call tx.Rollback() before if db.readMetaPage
|
|
// returns an error, and thus we will deadlock against
|
|
// ourselves when the Rollback tries to acquire the db.mu.
|
|
// This is why db.mu.Unlock() is done manually below.
|
|
|
|
if !db.opened {
|
|
cleanup()
|
|
db.mu.Unlock()
|
|
return nil, ErrClosed
|
|
}
|
|
|
|
// Wait for WAL size to be below threshold.
|
|
for int64(db.walPageN*PageSize) > db.cfg.MaxWALCheckpointSize {
|
|
db.haltCond.Wait()
|
|
}
|
|
|
|
tx := &Tx{
|
|
db: db,
|
|
rootRecords: db.rootRecords,
|
|
pageMap: db.pageMap,
|
|
walPageN: db.walPageN,
|
|
writable: writable,
|
|
|
|
DeleteEmptyContainer: true,
|
|
}
|
|
|
|
if writable {
|
|
tx.dirtyPages = make(map[uint32][]byte)
|
|
tx.dirtyBitmapPages = make(map[uint32][]byte)
|
|
}
|
|
|
|
// Copy meta page into transaction's buffer.
|
|
// This page is only written at the end of a dirty transaction.
|
|
page, err := db.readMetaPage()
|
|
if err != nil {
|
|
// we will deadlock in tx.Rollback()
|
|
// on db.mu.Lock unless we manually db.mu.Unlock first.
|
|
db.mu.Unlock()
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
copy(tx.meta[:], page)
|
|
|
|
// Attach starting WAL ID to transaction.
|
|
tx.walID = readMetaWALID(tx.meta[:])
|
|
|
|
// Track transaction with the DB.
|
|
db.txs[tx] = struct{}{}
|
|
|
|
// If no root records are cached, build the cache the first time.
|
|
// Normally the cache is updated by successful write transactions but
|
|
// this avoids recomputing the cache if there are no write txs for a while.
|
|
if db.rootRecords == nil {
|
|
if db.rootRecords, err = tx.RootRecords(); err != nil {
|
|
db.mu.Unlock()
|
|
tx.Rollback()
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
db.mu.Unlock()
|
|
return tx, nil
|
|
}
|
|
|
|
// removeTx removes an active transaction from the database.
|
|
func (db *DB) removeTx(tx *Tx) error {
|
|
// Release writer lock if tx is writable.
|
|
if tx.writable {
|
|
tx.db.rwmu.Unlock()
|
|
}
|
|
|
|
delete(tx.db.txs, tx)
|
|
|
|
// Disassociate from db.
|
|
tx.db = nil
|
|
|
|
// Write pages from WAL to DB.
|
|
// TODO(bbj): Move this to an async goroutine.
|
|
if len(db.txs) == 0 && db.walSize() > db.cfg.MinWALCheckpointSize {
|
|
if err := db.checkpoint(); err != nil {
|
|
return fmt.Errorf("checkpoint: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Check performs an integrity check.
|
|
func (db *DB) Check() error {
|
|
|
|
tx, err := db.Begin(false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
return tx.Check()
|
|
}
|
|
|
|
// writeDBPage writes a page to the data file.
|
|
func (db *DB) writeDBPage(pgno uint32, page []byte) error {
|
|
_, err := db.file.WriteAt(page, int64(pgno)*PageSize)
|
|
return err
|
|
}
|
|
|
|
func (db *DB) readDBPage(pgno uint32) ([]byte, error) {
|
|
offset := int64(pgno) * PageSize
|
|
return db.data[offset : offset+PageSize], nil
|
|
}
|
|
|
|
// baseWALID returns the WAL ID stored in the database file meta page.
|
|
func (db *DB) baseWALID() int64 {
|
|
return readMetaWALID(db.data)
|
|
}
|
|
|
|
// readWALPageByID reads a WAL page by WAL ID.
|
|
func (db *DB) readWALPageByID(id int64) ([]byte, error) {
|
|
return db.readWALPageAt(int(id - db.baseWALID() - 1))
|
|
}
|
|
|
|
// readWALPageAt reads the i-th page in the WAL file.
|
|
func (db *DB) readWALPageAt(i int) ([]byte, error) {
|
|
offset := int64(i) * PageSize
|
|
return db.wal[offset : offset+PageSize], nil
|
|
}
|
|
|
|
func (db *DB) readMetaPage() ([]byte, error) {
|
|
if walID, ok := db.pageMap.Get(uint32(0)); ok {
|
|
return db.readWALPageByID(walID)
|
|
}
|
|
return db.readDBPage(0)
|
|
}
|
|
|
|
func (db *DB) getCursor(tx *Tx) *Cursor {
|
|
c := cursorSyncPool.Get().(*Cursor)
|
|
c.tx = tx
|
|
return c
|
|
}
|
|
|
|
// Shared pool for in-memory database pages.
|
|
// These are used before being flushed to disk.
|
|
var pagePool = &sync.Pool{
|
|
New: func() interface{} {
|
|
page := make([]byte, PageSize)
|
|
return &page
|
|
},
|
|
}
|
|
|
|
func allocPage() []byte {
|
|
page := pagePool.Get().(*[]byte)
|
|
return *page
|
|
}
|
|
|
|
func freePage(page []byte) {
|
|
pagePool.Put(&page)
|
|
}
|