mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-05 16:15:56 +00:00
267 lines
6.1 KiB
Go
267 lines
6.1 KiB
Go
// Copyright 2023 Molecula Corp. All rights reserved.
|
|
package bufferpool
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type FileShardID struct {
|
|
ObjectId int32
|
|
Shard int32
|
|
}
|
|
|
|
// OnDiskDiskManager is a on disk implementation for a DiskManager interface
|
|
type OnDiskDiskManager struct {
|
|
mu sync.Mutex
|
|
|
|
files map[FileShardID]*os.File
|
|
}
|
|
|
|
// NewInMemDiskSpillingDiskManager returns a in-memory version of disk manager
|
|
func NewOnDiskDiskManager() *OnDiskDiskManager {
|
|
dm := &OnDiskDiskManager{
|
|
files: make(map[FileShardID]*os.File),
|
|
}
|
|
return dm
|
|
}
|
|
|
|
func (d *OnDiskDiskManager) CreateOrOpenShard(objectId int32, shard int32, dataFile string) error {
|
|
// serialize access here
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
fileID := FileShardID{objectId, shard}
|
|
|
|
// do we have the file already open?
|
|
_, ok := d.files[fileID]
|
|
if ok {
|
|
return nil
|
|
}
|
|
|
|
var fd *os.File
|
|
|
|
// see if the file for this shard exists
|
|
_, err := os.Stat(dataFile)
|
|
if err != nil {
|
|
//create file
|
|
fd, err = os.OpenFile(dataFile, os.O_RDWR|os.O_CREATE, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("open file: %w", err)
|
|
}
|
|
// write the root page
|
|
err = d.writeRootPage(fd, objectId, shard)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// open or create the file
|
|
fd, err = os.OpenFile(dataFile, os.O_RDWR, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("open file: %w", err)
|
|
}
|
|
}
|
|
d.files[fileID] = fd
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *OnDiskDiskManager) writeRootPage(fd *os.File, objectId int32, shard int32) error {
|
|
headerPage := NewPage(PageID{objectId, shard, 0}, 0)
|
|
headerPage.WritePageNumber(0)
|
|
headerPage.WriteFreeSpaceOffset(int16(PAGE_SIZE))
|
|
headerPage.WriteNextPointer(PageID{0, 0, INVALID_PAGE})
|
|
headerPage.WritePrevPointer(PageID{0, 0, INVALID_PAGE})
|
|
headerPage.WritePageType(PAGE_TYPE_BTREE_HEADER)
|
|
|
|
// write a slot that points to the initial root page
|
|
rootPageID := PageID{objectId, shard, 1}
|
|
|
|
// get the free space offset
|
|
freeSpaceOffset := headerPage.ReadFreeSpaceOffset()
|
|
|
|
keyBytes := []byte{0, 0, 0, 0}
|
|
|
|
// build a chunk
|
|
chunk := InternalPageChunk{
|
|
KeyLength: int16(len(keyBytes)),
|
|
KeyBytes: keyBytes,
|
|
PtrValue: 1,
|
|
}
|
|
|
|
// compute the new free space offset
|
|
freeSpaceOffset -= int16(chunk.Length())
|
|
|
|
headerPage.WriteInternalPageChunk(freeSpaceOffset, chunk)
|
|
|
|
// update the free space offset
|
|
headerPage.WriteFreeSpaceOffset(int16(freeSpaceOffset))
|
|
|
|
// make a slot
|
|
slot := PageSlot{
|
|
PayloadOffset: freeSpaceOffset,
|
|
}
|
|
// write the slot
|
|
headerPage.WritePageSlot(0, slot)
|
|
|
|
// update the slot count
|
|
headerPage.WriteSlotCount(int16(1))
|
|
|
|
_, err := fd.WriteAt(headerPage.data[:], 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rootPage := NewPage(rootPageID, 0)
|
|
rootPage.WritePageNumber(1)
|
|
rootPage.WriteFreeSpaceOffset(int16(PAGE_SIZE))
|
|
rootPage.WriteNextPointer(PageID{0, 0, INVALID_PAGE})
|
|
rootPage.WritePrevPointer(PageID{0, 0, INVALID_PAGE})
|
|
rootPage.WritePageType(PAGE_TYPE_BTREE_LEAF)
|
|
|
|
_, err = fd.WriteAt(rootPage.data[:], PAGE_SIZE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReadPage reads a page from disk
|
|
func (d *OnDiskDiskManager) ReadPage(pageID PageID) (*Page, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
// do we have the file open
|
|
fileID := FileShardID{pageID.ObjectID, pageID.Shard}
|
|
|
|
fd, ok := d.files[fileID]
|
|
if !ok {
|
|
return nil, errors.Errorf("file id '%d' not open", pageID.ObjectID)
|
|
}
|
|
|
|
info, err := os.Stat(fd.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
numPages := info.Size() / int64(PAGE_SIZE)
|
|
|
|
// check we're not asking for page out of range
|
|
if pageID.Page < 0 || pageID.Page >= numPages {
|
|
return nil, errors.Errorf("requested page number '%d' out of range", pageID.Page)
|
|
}
|
|
offset := pageID.Page * int64(PAGE_SIZE)
|
|
|
|
var page = pageSyncPool.Get().(*Page)
|
|
// we have to do this stupid check because if -cpuprofile is set for go test, this
|
|
// the previous line return a weird nil-ish thing...
|
|
if page == (*Page)(nil) {
|
|
page = pageSyncPool.New().(*Page)
|
|
}
|
|
page.id = pageID
|
|
|
|
// do the read
|
|
_, err = fd.ReadAt(page.data[:], offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return page, nil
|
|
}
|
|
|
|
// WritePage writes a page in memory to pages
|
|
func (d *OnDiskDiskManager) WritePage(page *Page) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
// do we have the file open
|
|
fileID := FileShardID{page.id.ObjectID, page.id.Shard}
|
|
|
|
fd, ok := d.files[fileID]
|
|
if !ok {
|
|
return errors.Errorf("file id '%d' not open", page.id.ObjectID)
|
|
}
|
|
|
|
info, err := os.Stat(fd.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
numPages := info.Size() / int64(PAGE_SIZE)
|
|
|
|
// check we're not asking for page out of range
|
|
if page.id.Page < 0 || page.id.Page >= numPages {
|
|
return errors.Errorf("requested page number '%d' out of range", page.id.Page)
|
|
}
|
|
offset := page.id.Page * int64(PAGE_SIZE)
|
|
|
|
// do the write
|
|
_, err = fd.WriteAt(page.data[:], offset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// yeah, nah
|
|
// err = d.fd.Sync()
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
return nil
|
|
}
|
|
|
|
// AllocatePage allocates a page and returns the page number
|
|
func (d *OnDiskDiskManager) AllocatePage(objectId int32, shard int32) (PageID, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
// do we have the file open
|
|
fileID := FileShardID{objectId, shard}
|
|
|
|
fd, ok := d.files[fileID]
|
|
if !ok {
|
|
return PageID{objectId, shard, INVALID_PAGE}, errors.Errorf("file id '%d' not open", objectId)
|
|
}
|
|
|
|
info, err := os.Stat(fd.Name())
|
|
if err != nil {
|
|
return PageID{objectId, shard, INVALID_PAGE}, err
|
|
}
|
|
|
|
numPages := info.Size() / int64(PAGE_SIZE)
|
|
numPages = numPages + 1
|
|
|
|
pageID := PageID{objectId, shard, numPages - 1}
|
|
size := numPages * PAGE_SIZE
|
|
_, err = fd.WriteAt([]byte{0}, size-1)
|
|
if err != nil {
|
|
return PageID{objectId, shard, INVALID_PAGE}, err
|
|
}
|
|
return pageID, nil
|
|
}
|
|
|
|
// DeallocatePage removes page from disk
|
|
func (d *OnDiskDiskManager) DeallocatePage(pageID PageID) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
// nothing to do right now
|
|
return nil
|
|
}
|
|
|
|
func (d *OnDiskDiskManager) FileSize(objectId int32, shard int32) int64 {
|
|
// TODO(pok) return correct file size
|
|
return 0
|
|
}
|
|
|
|
func (d *OnDiskDiskManager) Close() {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
for _, fd := range d.files {
|
|
fd.Close()
|
|
}
|
|
// empty the efiles
|
|
d.files = make(map[FileShardID]*os.File, 0)
|
|
}
|