wp
This commit is contained in:
369
atree/atree.go
369
atree/atree.go
@@ -3,41 +3,64 @@ package atree
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
diploma "gordenko.dev/dima/qb"
|
||||
"gordenko.dev/dima/qb/atree/redo"
|
||||
"gordenko.dev/dima/qb/bin"
|
||||
)
|
||||
|
||||
const (
|
||||
PageTypeData = 1
|
||||
PageTypeIndex = 2
|
||||
|
||||
filePerm = 0770
|
||||
|
||||
// common
|
||||
crc32Idx = PageSize - 4
|
||||
pageType = PageSize - 5
|
||||
|
||||
// index page
|
||||
indexRecordsQtyIdx = IndexPageSize - 7
|
||||
isDataPageNumbersIdx = IndexPageSize - 5
|
||||
indexCRC32Idx = IndexPageSize - 4
|
||||
indexRecordsQtyIdx = PageSize - 8
|
||||
isDataPageNumbersIdx = PageSize - 6
|
||||
|
||||
// data page
|
||||
timestampsSizeIdx = DataPageSize - 12
|
||||
valuesSizeIdx = DataPageSize - 10
|
||||
prevPageIdx = DataPageSize - 8
|
||||
dataCRC32Idx = DataPageSize - 4
|
||||
timestampsSizeIdx = PageSize - 13
|
||||
valuesSizeIdx = PageSize - 11
|
||||
prevPageIdx = PageSize - 9
|
||||
|
||||
timestampSize = 4
|
||||
pairSize = timestampSize + PageNoSize
|
||||
indexFooterIdx = indexRecordsQtyIdx
|
||||
dataFooterIdx = timestampsSizeIdx
|
||||
|
||||
DataPageSize = 8192
|
||||
IndexPageSize = 1024
|
||||
PageNoSize = 4
|
||||
PageSize = 8192
|
||||
PageNoSize = 4
|
||||
//
|
||||
DataPagePayloadSize int = dataFooterIdx
|
||||
)
|
||||
|
||||
const (
|
||||
FlagReused byte = 1 // сторінка із FreeList
|
||||
FlagNewRoot byte = 2 // новая страница
|
||||
)
|
||||
|
||||
var (
|
||||
castagnoliTable = crc32.MakeTable(crc32.Castagnoli)
|
||||
)
|
||||
|
||||
func calcChecksum(page []byte) uint32 {
|
||||
return crc32.Checksum(page, castagnoliTable)
|
||||
}
|
||||
|
||||
type PageToWrite struct {
|
||||
PageNo uint32
|
||||
Data []byte
|
||||
IsReused bool
|
||||
}
|
||||
|
||||
type FreeList interface {
|
||||
// використовується в allocPage
|
||||
ReservePage() uint32
|
||||
@@ -50,120 +73,71 @@ type _page struct {
|
||||
}
|
||||
|
||||
type Atree struct {
|
||||
redoDir string
|
||||
indexFreelist FreeList
|
||||
dataFreelist FreeList
|
||||
dataFile *os.File
|
||||
indexFile *os.File
|
||||
mutex sync.Mutex
|
||||
allocatedIndexPagesQty uint32
|
||||
allocatedDataPagesQty uint32
|
||||
indexPages map[uint32]*_page
|
||||
dataPages map[uint32]*_page
|
||||
indexWaits map[uint32][]chan readResult
|
||||
dataWaits map[uint32][]chan readResult
|
||||
indexPagesToRead []uint32
|
||||
dataPagesToRead []uint32
|
||||
readSignalCh chan struct{}
|
||||
writeSignalCh chan struct{}
|
||||
writeTasksQueue []WriteTask
|
||||
freelist FreeList
|
||||
file *os.File
|
||||
mutex sync.Mutex
|
||||
allocatedPagesQty uint32
|
||||
pages map[uint32]*_page
|
||||
pageWaits map[uint32][]chan readResult
|
||||
pagesToRead []uint32
|
||||
readSignalCh chan struct{}
|
||||
writeSignalCh chan struct{}
|
||||
writeTasksQueue []WriteTask
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Dir string
|
||||
RedoDir string
|
||||
DatabaseName string
|
||||
DataFreeList FreeList
|
||||
IndexFreeList FreeList
|
||||
Dir string
|
||||
DatabaseName string
|
||||
FreeList FreeList
|
||||
}
|
||||
|
||||
func New(opt Options) (*Atree, error) {
|
||||
if opt.Dir == "" {
|
||||
return nil, errors.New("Dir option is required")
|
||||
}
|
||||
if opt.RedoDir == "" {
|
||||
return nil, errors.New("RedoDir option is required")
|
||||
}
|
||||
// if opt.RedoDir == "" {
|
||||
// return nil, errors.New("RedoDir option is required")
|
||||
// }
|
||||
if opt.DatabaseName == "" {
|
||||
return nil, errors.New("DatabaseName option is required")
|
||||
}
|
||||
if opt.DataFreeList == nil {
|
||||
return nil, errors.New("DataFreeList option is required")
|
||||
}
|
||||
if opt.IndexFreeList == nil {
|
||||
return nil, errors.New("IndexFreeList option is required")
|
||||
if opt.FreeList == nil {
|
||||
return nil, errors.New("FreeList option is required")
|
||||
}
|
||||
// открываю или создаю dbName.data и dbName.index файлы
|
||||
var (
|
||||
indexFileName = filepath.Join(opt.Dir, opt.DatabaseName+".index")
|
||||
dataFileName = filepath.Join(opt.Dir, opt.DatabaseName+".data")
|
||||
|
||||
indexFile *os.File
|
||||
dataFile *os.File
|
||||
allocatedIndexPagesQty uint32
|
||||
allocatedDataPagesQty uint32
|
||||
fileName = filepath.Join(opt.Dir, opt.DatabaseName+".db")
|
||||
file *os.File
|
||||
allocatedPagesQty uint32
|
||||
)
|
||||
|
||||
// При создании data файла сразу создается индекс, поэтому корректное
|
||||
// состояние БД: либо оба файла есть, либо ни одного файла нет.
|
||||
isIndexExist, err := isFileExist(indexFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check index file is exist: %s", err)
|
||||
}
|
||||
|
||||
isDataExist, err := isFileExist(dataFileName)
|
||||
isDataExist, err := isFileExist(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check data file is exist: %s", err)
|
||||
}
|
||||
|
||||
if isIndexExist {
|
||||
if isDataExist {
|
||||
// открываю оба файла
|
||||
indexFile, allocatedIndexPagesQty, err = openFile(indexFileName, IndexPageSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open index file: %s", err)
|
||||
}
|
||||
|
||||
dataFile, allocatedDataPagesQty, err = openFile(dataFileName, DataPageSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open data file: %s", err)
|
||||
}
|
||||
} else {
|
||||
// нет data файла
|
||||
return nil, errors.New("not found data file")
|
||||
if isDataExist {
|
||||
file, allocatedPagesQty, err = openFile(fileName, PageSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open data file: %s", err)
|
||||
}
|
||||
} else {
|
||||
if isDataExist {
|
||||
// index файла нет
|
||||
return nil, errors.New("not found index file")
|
||||
} else {
|
||||
// нет обоих файлов
|
||||
indexFile, err = os.OpenFile(indexFileName, os.O_CREATE|os.O_RDWR, filePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataFile, err = os.OpenFile(dataFileName, os.O_CREATE|os.O_RDWR, filePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// нет файла
|
||||
file, err = os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, filePerm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tree := &Atree{
|
||||
redoDir: opt.RedoDir,
|
||||
indexFreelist: opt.IndexFreeList,
|
||||
dataFreelist: opt.DataFreeList,
|
||||
indexFile: indexFile,
|
||||
dataFile: dataFile,
|
||||
allocatedIndexPagesQty: allocatedIndexPagesQty,
|
||||
allocatedDataPagesQty: allocatedDataPagesQty,
|
||||
indexPages: make(map[uint32]*_page),
|
||||
dataPages: make(map[uint32]*_page),
|
||||
indexWaits: make(map[uint32][]chan readResult),
|
||||
dataWaits: make(map[uint32][]chan readResult),
|
||||
readSignalCh: make(chan struct{}, 1),
|
||||
writeSignalCh: make(chan struct{}, 1),
|
||||
//freelist: opt.FreeList,
|
||||
file: file,
|
||||
allocatedPagesQty: allocatedPagesQty,
|
||||
pages: make(map[uint32]*_page),
|
||||
pageWaits: make(map[uint32][]chan readResult),
|
||||
readSignalCh: make(chan struct{}, 1),
|
||||
writeSignalCh: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
@@ -185,7 +159,7 @@ func (s *Atree) findDataPage(rootPageNo uint32, timestamp uint32) (uint32, []byt
|
||||
}
|
||||
|
||||
foundPageNo := findPageNo(buf, timestamp)
|
||||
s.releaseIndexPage(indexPageNo)
|
||||
s.releasePage(indexPageNo)
|
||||
|
||||
if buf[isDataPageNumbersIdx] == 1 {
|
||||
buf, err := s.fetchDataPage(foundPageNo)
|
||||
@@ -256,16 +230,37 @@ type AppendDataPageReq struct {
|
||||
ValuesChunks [][]byte
|
||||
ValuesSize uint16
|
||||
}
|
||||
type Report struct {
|
||||
//IsDataPageReused bool
|
||||
//DataPageNo uint32
|
||||
IsRootChanged bool
|
||||
NewRootPageNo uint32
|
||||
//ReusedIndexPages []uint32
|
||||
Pages []PageToWrite
|
||||
}
|
||||
|
||||
func (s *Atree) AppendDataPage(req AppendDataPageReq) (_ redo.Report, err error) {
|
||||
// type ChangedPage struct {
|
||||
// PageNo uint32
|
||||
// Data []byte
|
||||
// IsReused bool
|
||||
// }
|
||||
|
||||
// AppendDataPage - метод не записує дані в data-файл, а лише змінює дані
|
||||
// в page cache та freeList і повертає звіт що змінено.
|
||||
// Цей звіт txlog має записати в transaction log і лише потім можна змінювати data файл.
|
||||
// Є ідея - записати у index файли заглушки 255,255,255,255 замість номерів сторінок і зберегти зміщення.
|
||||
// А потім одним викликом отримати із FreeList список вільних сторінок.
|
||||
// Тому що є проблема із відновленням FreeList після збою, якщо з нього будуть паралельно
|
||||
// забирати та добавляти номери сторінок інші потоки.
|
||||
// Це буде працювати, якщо додавання в txlog і маніпуляції із freeList будуть відбуватись в одному потоці
|
||||
func (s *Atree) AppendDataPage(req AppendDataPageReq) (_ Report, err error) {
|
||||
var (
|
||||
flags byte
|
||||
dataPagesToRelease []uint32
|
||||
indexPagesToRelease []uint32
|
||||
pagesToRelease []uint32
|
||||
report Report
|
||||
)
|
||||
|
||||
newDataPage := s.allocDataPage()
|
||||
dataPagesToRelease = append(dataPagesToRelease, newDataPage.PageNo)
|
||||
newDataPage := s.allocPage()
|
||||
pagesToRelease = append(pagesToRelease, newDataPage.PageNo)
|
||||
|
||||
chunksToDataPage(newDataPage.Data, chunksToDataPageReq{
|
||||
PrevPageNo: req.PrevPageNo,
|
||||
@@ -275,18 +270,23 @@ func (s *Atree) AppendDataPage(req AppendDataPageReq) (_ redo.Report, err error)
|
||||
ValuesSize: req.ValuesSize,
|
||||
})
|
||||
|
||||
redoWriter, err := redo.NewWriter(redo.WriterOptions{
|
||||
Dir: s.redoDir,
|
||||
MetricID: req.MetricID,
|
||||
Timestamp: req.Timestamp,
|
||||
Value: req.Value,
|
||||
IsDataPageReused: newDataPage.IsReused,
|
||||
DataPageNo: newDataPage.PageNo,
|
||||
Page: newDataPage.Data,
|
||||
report.Pages = append(report.Pages, PageToWrite{
|
||||
PageNo: newDataPage.PageNo,
|
||||
Data: newDataPage.Data,
|
||||
IsReused: newDataPage.IsReused,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// redoWriter, err := NewWriter(WriterOptions{
|
||||
// MetricID: req.MetricID,
|
||||
// Timestamp: req.Timestamp,
|
||||
// Value: req.Value,
|
||||
// IsDataPageReused: newDataPage.IsReused,
|
||||
// DataPageNo: newDataPage.PageNo,
|
||||
// Page: newDataPage.Data,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
|
||||
if req.RootPageNo > 0 {
|
||||
var path pathToDataPage
|
||||
@@ -295,7 +295,7 @@ func (s *Atree) AppendDataPage(req AppendDataPageReq) (_ redo.Report, err error)
|
||||
return
|
||||
}
|
||||
for _, leg := range path.Legs {
|
||||
indexPagesToRelease = append(indexPagesToRelease, leg.PageNo)
|
||||
pagesToRelease = append(pagesToRelease, leg.PageNo)
|
||||
}
|
||||
|
||||
if path.LastPageNo != req.PrevPageNo {
|
||||
@@ -314,97 +314,116 @@ func (s *Atree) AppendDataPage(req AppendDataPageReq) (_ redo.Report, err error)
|
||||
|
||||
ok := appendPair(leg.Data, req.Since, newPageNo)
|
||||
if ok {
|
||||
err = redoWriter.AppendIndexPage(leg.PageNo, leg.Data, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// index
|
||||
report.Pages = append(report.Pages, PageToWrite{
|
||||
PageNo: leg.PageNo,
|
||||
Data: leg.Data,
|
||||
})
|
||||
// err = redoWriter.AppendIndexPage(leg.PageNo, leg.Data, 0)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
break
|
||||
}
|
||||
|
||||
newIndexPage := s.allocIndexPage()
|
||||
indexPagesToRelease = append(indexPagesToRelease, newIndexPage.PageNo)
|
||||
newIndexPage := s.allocPage()
|
||||
pagesToRelease = append(pagesToRelease, newIndexPage.PageNo)
|
||||
appendPair(newIndexPage.Data, req.Since, newPageNo)
|
||||
// ставлю мітку що всі pageNo на сторінці - це data pageNo
|
||||
if legIdx == lastIdx {
|
||||
newIndexPage.Data[isDataPageNumbersIdx] = 1
|
||||
}
|
||||
|
||||
flags = 0
|
||||
if newIndexPage.IsReused {
|
||||
flags |= redo.FlagReused
|
||||
}
|
||||
err = redoWriter.AppendIndexPage(newIndexPage.PageNo, newIndexPage.Data, flags)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// flags = 0
|
||||
// if newIndexPage.IsReused {
|
||||
// flags |= FlagReused
|
||||
// }
|
||||
report.Pages = append(report.Pages, PageToWrite{
|
||||
PageNo: newIndexPage.PageNo,
|
||||
Data: newIndexPage.Data,
|
||||
IsReused: newIndexPage.IsReused,
|
||||
})
|
||||
// err = redoWriter.AppendIndexPage(newIndexPage.PageNo, newIndexPage.Data, flags)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
newPageNo = newIndexPage.PageNo
|
||||
|
||||
if legIdx == 0 {
|
||||
newRoot := s.allocIndexPage()
|
||||
indexPagesToRelease = append(indexPagesToRelease, newRoot.PageNo)
|
||||
newRoot := s.allocPage()
|
||||
pagesToRelease = append(pagesToRelease, newRoot.PageNo)
|
||||
appendPair(newRoot.Data, getSince(leg.Data), leg.PageNo) // old rootPageNo
|
||||
appendPair(newRoot.Data, req.Since, newIndexPage.PageNo)
|
||||
|
||||
// Фиксирую новый root в REDO логе
|
||||
flags = redo.FlagNewRoot
|
||||
if newRoot.IsReused {
|
||||
flags |= redo.FlagReused
|
||||
}
|
||||
err = redoWriter.AppendIndexPage(newRoot.PageNo, newRoot.Data, flags)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
report.Pages = append(report.Pages, PageToWrite{
|
||||
PageNo: newRoot.PageNo,
|
||||
Data: newRoot.Data,
|
||||
IsReused: newRoot.IsReused,
|
||||
})
|
||||
report.NewRootPageNo = newRoot.PageNo
|
||||
// flags = FlagNewRoot
|
||||
// if newRoot.IsReused {
|
||||
// flags |= FlagReused
|
||||
// }
|
||||
// err = redoWriter.AppendIndexPage(newRoot.PageNo, newRoot.Data, flags)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newRoot := s.allocIndexPage()
|
||||
indexPagesToRelease = append(indexPagesToRelease, newRoot.PageNo)
|
||||
newRoot := s.allocPage()
|
||||
pagesToRelease = append(pagesToRelease, newRoot.PageNo)
|
||||
newRoot.Data[isDataPageNumbersIdx] = 1
|
||||
appendPair(newRoot.Data, req.Since, newDataPage.PageNo)
|
||||
|
||||
flags = redo.FlagNewRoot
|
||||
if newRoot.IsReused {
|
||||
flags |= redo.FlagReused
|
||||
}
|
||||
err = redoWriter.AppendIndexPage(newRoot.PageNo, newRoot.Data, flags)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
report.Pages = append(report.Pages, PageToWrite{
|
||||
PageNo: newRoot.PageNo,
|
||||
Data: newRoot.Data,
|
||||
IsReused: newRoot.IsReused,
|
||||
})
|
||||
report.NewRootPageNo = newRoot.PageNo
|
||||
// flags = FlagNewRoot
|
||||
// if newRoot.IsReused {
|
||||
// flags |= FlagReused
|
||||
// }
|
||||
// err = redoWriter.AppendIndexPage(newRoot.PageNo, newRoot.Data, flags)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
}
|
||||
|
||||
err = redoWriter.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// err = redoWriter.Close()
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
|
||||
// На данний момен схема - наступна. Всі сторінки - data та index - зафіксовані в кеші.
|
||||
// Отже запис на диск пройде максимально швидко. Після цього ReferenceCount кожної
|
||||
// сторінки зменшиться на 1. Оскільки на метрику утримується XLock, сторінки мають
|
||||
// ReferenceCount = 1 (немає інших читачів).
|
||||
waitCh := make(chan struct{})
|
||||
// waitCh := make(chan struct{})
|
||||
|
||||
task := WriteTask{
|
||||
WaitCh: waitCh,
|
||||
DataPage: redo.PageToWrite{
|
||||
PageNo: newDataPage.PageNo,
|
||||
Data: newDataPage.Data,
|
||||
},
|
||||
IndexPages: redoWriter.IndexPagesToWrite(),
|
||||
}
|
||||
// task := WriteTask{
|
||||
// WaitCh: waitCh,
|
||||
// Pages: report.Pages,
|
||||
// }
|
||||
|
||||
s.appendWriteTaskToQueue(task)
|
||||
// s.appendWriteTaskToQueue(task)
|
||||
|
||||
<-waitCh
|
||||
// <-waitCh
|
||||
|
||||
for _, pageNo := range dataPagesToRelease {
|
||||
s.releaseDataPage(pageNo)
|
||||
}
|
||||
for _, pageNo := range indexPagesToRelease {
|
||||
s.releaseIndexPage(pageNo)
|
||||
}
|
||||
return redoWriter.GetReport(), nil
|
||||
// for _, pageNo := range dataPagesToRelease {
|
||||
// s.releasePage(pageNo)
|
||||
// }
|
||||
// for _, pageNo := range indexPagesToRelease {
|
||||
// s.releasePage(pageNo)
|
||||
// }
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// DELETE
|
||||
@@ -439,7 +458,7 @@ func (s *Atree) GetAllPages(rootPageNo uint32) (_ PageLists, err error) {
|
||||
pageNumbers := listPageNumbers(buf)
|
||||
dataPages = append(dataPages, pageNumbers...)
|
||||
|
||||
s.releaseIndexPage(rootPageNo)
|
||||
s.releasePage(rootPageNo)
|
||||
|
||||
return PageLists{
|
||||
DataPages: dataPages,
|
||||
@@ -481,7 +500,7 @@ func (s *Atree) GetAllPages(rootPageNo uint32) (_ PageLists, err error) {
|
||||
pageNumbers := listPageNumbers(buf)
|
||||
dataPages = append(dataPages, pageNumbers...)
|
||||
|
||||
s.releaseIndexPage(pageNo)
|
||||
s.releasePage(pageNo)
|
||||
} else {
|
||||
levels = append(levels, &Level{
|
||||
PageNo: pageNo,
|
||||
@@ -491,7 +510,7 @@ func (s *Atree) GetAllPages(rootPageNo uint32) (_ PageLists, err error) {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
s.releaseIndexPage(level.PageNo)
|
||||
s.releasePage(level.PageNo)
|
||||
levels = levels[:lastIdx]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user