This commit is contained in:
2026-02-23 19:37:05 +00:00
parent 6011535509
commit 271046231c
15 changed files with 948 additions and 816 deletions

View File

@@ -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]
}
}