Files
qb/atree/atree.go

490 lines
13 KiB
Go
Raw Normal View History

2026-02-10 14:02:11 +00:00
package atree
import (
"errors"
"fmt"
2026-02-23 19:37:05 +00:00
"hash/crc32"
2026-02-10 14:02:11 +00:00
"os"
"path/filepath"
"sync"
"gordenko.dev/dima/qb/bin"
)
const (
2026-02-23 19:37:05 +00:00
PageTypeData = 1
PageTypeIndex = 2
2026-02-10 14:02:11 +00:00
filePerm = 0770
2026-02-23 19:37:05 +00:00
// common
2026-05-10 00:59:47 +00:00
crc32Idx = PageSize - 4
pageTypeIdx = PageSize - 5
2026-02-23 19:37:05 +00:00
2026-02-10 14:02:11 +00:00
// index page
2026-05-17 12:58:46 +00:00
indexCRC32Idx = IndexPageSize - 4
isLastLevelIdx = IndexPageSize - 5
indexRecordsQtyIdx = IndexPageSize - 7
indexRecordSize = 8
2026-02-10 14:02:11 +00:00
// data page
2026-05-17 12:58:46 +00:00
dataCRC32Idx = DataPageSize - 4
prevPageIdx = DataPageSize - 8
valuesSizeIdx = DataPageSize - 10
timestampsSizeIdx = DataPageSize - 12
2026-02-10 14:02:11 +00:00
timestampSize = 4
pairSize = timestampSize + PageNoSize
indexFooterIdx = indexRecordsQtyIdx
dataFooterIdx = timestampsSizeIdx
2026-05-17 12:58:46 +00:00
PageSize = 8192
DataPageSize = 8192
IndexPageSize = 1024
PageNoSize = 4
2026-02-10 14:02:11 +00:00
//
DataPagePayloadSize int = dataFooterIdx
)
2026-02-23 19:37:05 +00:00
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
}
2026-02-10 14:02:11 +00:00
type FreeList interface {
2026-02-23 11:02:29 +00:00
// використовується в allocPage
2026-02-10 14:02:11 +00:00
ReservePage() uint32
}
type _page struct {
PageNo uint32
Buf []byte
ReferenceCount int
}
type Atree struct {
2026-02-23 19:37:05 +00:00
file *os.File
mutex sync.Mutex
2026-05-10 00:59:47 +00:00
freelist FreeList
2026-02-23 19:37:05 +00:00
allocatedPagesQty uint32
pages map[uint32]*_page
pageWaits map[uint32][]chan readResult
pagesToRead []uint32
readSignalCh chan struct{}
writeSignalCh chan struct{}
2026-05-10 00:59:47 +00:00
//writeTasksQueue []WriteTask
2026-02-10 14:02:11 +00:00
}
type Options struct {
2026-02-23 19:37:05 +00:00
Dir string
DatabaseName string
FreeList FreeList
2026-02-10 14:02:11 +00:00
}
func New(opt Options) (*Atree, error) {
if opt.Dir == "" {
return nil, errors.New("Dir option is required")
}
2026-02-23 19:37:05 +00:00
// if opt.RedoDir == "" {
// return nil, errors.New("RedoDir option is required")
// }
2026-02-10 14:02:11 +00:00
if opt.DatabaseName == "" {
return nil, errors.New("DatabaseName option is required")
}
2026-02-23 19:37:05 +00:00
if opt.FreeList == nil {
return nil, errors.New("FreeList option is required")
2026-02-10 14:02:11 +00:00
}
// открываю или создаю dbName.data и dbName.index файлы
var (
2026-02-23 19:37:05 +00:00
fileName = filepath.Join(opt.Dir, opt.DatabaseName+".db")
file *os.File
allocatedPagesQty uint32
2026-02-10 14:02:11 +00:00
)
// При создании data файла сразу создается индекс, поэтому корректное
// состояние БД: либо оба файла есть, либо ни одного файла нет.
2026-02-23 19:37:05 +00:00
isDataExist, err := isFileExist(fileName)
2026-02-10 14:02:11 +00:00
if err != nil {
return nil, fmt.Errorf("check data file is exist: %s", err)
}
2026-02-23 19:37:05 +00:00
if isDataExist {
file, allocatedPagesQty, err = openFile(fileName, PageSize)
if err != nil {
return nil, fmt.Errorf("open data file: %s", err)
2026-02-10 14:02:11 +00:00
}
} else {
2026-02-23 19:37:05 +00:00
// нет файла
file, err = os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, filePerm)
if err != nil {
return nil, err
2026-02-10 14:02:11 +00:00
}
}
tree := &Atree{
2026-05-10 00:59:47 +00:00
freelist: opt.FreeList,
2026-02-23 19:37:05 +00:00
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),
2026-02-10 14:02:11 +00:00
}
return tree, nil
}
func (s *Atree) Run() {
2026-05-10 00:59:47 +00:00
//go s.pageWriter()
2026-02-10 14:02:11 +00:00
go s.pageReader()
}
// FIND
func (s *Atree) findDataPage(rootPageNo uint32, timestamp uint32) (uint32, []byte, error) {
indexPageNo := rootPageNo
for {
buf, err := s.fetchIndexPage(indexPageNo)
if err != nil {
return 0, nil, fmt.Errorf("fetchIndexPage(%d): %s", indexPageNo, err)
}
foundPageNo := findPageNo(buf, timestamp)
2026-02-23 19:37:05 +00:00
s.releasePage(indexPageNo)
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
// fix
2026-05-17 12:58:46 +00:00
if buf[isLastLevelIdx] == 1 {
2026-02-10 14:02:11 +00:00
buf, err := s.fetchDataPage(foundPageNo)
if err != nil {
return 0, nil, fmt.Errorf("fetchDataPage(%d): %s", foundPageNo, err)
}
return foundPageNo, buf, nil
}
// вглубь
indexPageNo = foundPageNo
}
}
2026-05-10 00:59:47 +00:00
type PathLeg struct {
2026-02-10 14:02:11 +00:00
PageNo uint32
Data []byte
}
2026-05-10 00:59:47 +00:00
type PathToDataPage struct {
Legs []PathLeg
2026-02-10 14:02:11 +00:00
LastPageNo uint32
}
2026-05-10 00:59:47 +00:00
func (s *Atree) FindPathToLastPage(rootPageNo uint32) (_ PathToDataPage, err error) {
2026-02-10 14:02:11 +00:00
var (
pageNo = rootPageNo
2026-05-10 00:59:47 +00:00
legs []PathLeg
2026-02-10 14:02:11 +00:00
)
for {
var buf []byte
buf, err = s.fetchIndexPage(pageNo)
if err != nil {
err = fmt.Errorf("FetchIndexPage(%d): %s", pageNo, err)
return
}
2026-05-10 00:59:47 +00:00
legs = append(legs, PathLeg{
2026-02-10 14:02:11 +00:00
PageNo: pageNo,
Data: buf,
// childIdx не нужен
})
foundPageNo := getLastPageNo(buf)
2026-05-10 00:59:47 +00:00
// fix
2026-05-17 12:58:46 +00:00
if buf[isLastLevelIdx] == 1 {
2026-05-10 00:59:47 +00:00
return PathToDataPage{
2026-02-10 14:02:11 +00:00
Legs: legs,
LastPageNo: foundPageNo,
}, nil
}
// вглубь
pageNo = foundPageNo
}
}
// APPEND DATA PAGE
2026-05-10 00:59:47 +00:00
//MetricID uint32
//Timestamp uint32
//Value float64
//RootPageNo uint32
//PrevPageNo uint32
2026-02-10 14:02:11 +00:00
type AppendDataPageReq struct {
Since uint32
TimestampsChunks [][]byte
TimestampsSize uint16
ValuesChunks [][]byte
ValuesSize uint16
}
2026-02-23 19:37:05 +00:00
// 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 будуть відбуватись в одному потоці
2026-05-10 00:59:47 +00:00
// NotLinkedDataPage - це data сторінка із payload, але без встановленого prevPageNo та без розрахованого CRC32
type NotLinkedDataPage struct {
Since uint32
Data []byte
}
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
func (s NotLinkedDataPage) SetPrevPageNo(prevPageNo uint32) {
bin.PutUint32(s.Data[prevPageIdx:], prevPageNo)
bin.PutUint32(s.Data[crc32Idx:], calcChecksum(s.Data[:crc32Idx]))
}
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
type AppendDataPagesReq struct {
LastPageNo uint32
Legs []PathLeg
DataPages []NotLinkedDataPage
}
2026-02-23 19:37:05 +00:00
2026-05-10 00:59:47 +00:00
type Report struct {
NewRootPageNo uint32
LastPageNo uint32
Pages []PageToWrite
}
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
// Можливо atree не треба блокувати при читанні. Я можу створити копії index сторінок,
// а потім під 1 мутексом замінити в pages.
func (s *Atree) AppendDataPages(req AppendDataPagesReq) Report {
var (
//pagesToRelease []uint32
newRootPageNo uint32
lastPageNo = req.LastPageNo
pages []PageToWrite
legs = req.Legs
changed = make(map[uint32]PathLeg)
)
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
for _, p := range req.DataPages {
newDataPageNo, isReused := s.allocPageNumber() // alloc only number
// pagesToRelease = append(pagesToRelease, newDataPage.PageNo)
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
// set prevPageNo
p.SetPrevPageNo(lastPageNo)
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
pages = append(pages, PageToWrite{
PageNo: newDataPageNo,
Data: p.Data,
IsReused: isReused,
})
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
// FIX - після додавання index page потрібно модифікувати path,
// або будувати щось типу дерева знизу вверх
if len(legs) > 0 {
newPageNo := newDataPageNo
lastIdx := len(legs) - 1
for legIdx := lastIdx; legIdx >= 0; legIdx-- {
leg := req.Legs[legIdx]
ok := appendPair(leg.Data, p.Since, newPageNo)
if ok {
// index FIX
// потрібно запам'ятати змінені сторінки, але їх можуть змінювати
// кілька ітерацій, тому додавати в Pages не можна.
// на індексній сторінці достатньо місця. Запис вставлено.
changed[leg.PageNo] = leg
break
}
// на індексній сторінці НЕ достатньо місця. Створюю нову.
newIndexPage := s.allocPage()
//pagesToRelease = append(pagesToRelease, newIndexPage.PageNo)
appendPair(newIndexPage.Data, p.Since, newPageNo)
// ставлю мітку що всі pageNo на сторінці - це data pageNo
// fix - єдина оптимізація від існування isDataPageNumbersIdx - getAllPages не завантажує data pages
// if legIdx == lastIdx {
// newIndexPage.Data[isDataPageNumbersIdx] = 1
2026-02-23 19:37:05 +00:00
// }
2026-05-10 00:59:47 +00:00
pages = append(pages, PageToWrite{
PageNo: newIndexPage.PageNo,
Data: newIndexPage.Data,
IsReused: newIndexPage.IsReused,
2026-02-23 19:37:05 +00:00
})
2026-05-10 00:59:47 +00:00
// замінюю крок в path на новий
legs[legIdx] = PathLeg{
PageNo: newIndexPage.PageNo,
Data: newIndexPage.Data,
}
//
newPageNo = newIndexPage.PageNo
if legIdx == 0 {
newRoot := s.allocPage()
//pagesToRelease = append(pagesToRelease, newRoot.PageNo)
appendPair(newRoot.Data, getSince(leg.Data), leg.PageNo) // old rootPageNo
appendPair(newRoot.Data, p.Since, newIndexPage.PageNo)
// Фиксирую новый root в REDO логе
pages = append(pages, PageToWrite{
PageNo: newRoot.PageNo,
Data: newRoot.Data,
IsReused: newRoot.IsReused,
})
newRootPageNo = newRoot.PageNo
// додаю в початок списку кроків нову root сторінку
legs = append([]PathLeg{
{
PageNo: newRoot.PageNo,
Data: newRoot.Data,
},
}, legs...)
break
}
2026-02-10 14:02:11 +00:00
}
2026-05-10 00:59:47 +00:00
} else {
// індексну root сторінку створюю одразу для першої data сторінки,
// root data сторінки, як в B+Tree не буває.
newRoot := s.allocPage()
//pagesToRelease = append(pagesToRelease, newRoot.PageNo)
//newRoot.Data[isDataPageNumbersIdx] = 1
appendPair(newRoot.Data, p.Since, newDataPageNo)
pages = append(pages, PageToWrite{
PageNo: newRoot.PageNo,
Data: newRoot.Data,
IsReused: newRoot.IsReused,
})
newRootPageNo = newRoot.PageNo
// додаю в початок списку кроків нову root сторінку
legs = append([]PathLeg{
{
PageNo: newRoot.PageNo,
Data: newRoot.Data,
},
}, legs...)
2026-02-10 14:02:11 +00:00
}
2026-05-10 00:59:47 +00:00
// На данний момен схема - наступна. Всі сторінки - data та index - зафіксовані в кеші.
// Отже запис на диск пройде максимально швидко. Після цього ReferenceCount кожної
// сторінки зменшиться на 1. Оскільки на метрику утримується XLock, сторінки мають
// ReferenceCount = 1 (немає інших читачів).
// for _, pageNo := range indexPagesToRelease {
// s.releasePage(pageNo)
2026-02-23 19:37:05 +00:00
// }
2026-05-10 00:59:47 +00:00
lastPageNo = newDataPageNo
}
for _, leg := range changed {
// fix recalc checksum
pages = append(pages, PageToWrite{
PageNo: leg.PageNo,
Data: leg.Data,
})
}
return Report{
NewRootPageNo: newRootPageNo,
LastPageNo: lastPageNo,
Pages: pages,
2026-02-10 14:02:11 +00:00
}
}
// DELETE
type Level struct {
PageNo uint32
PageData []byte
Idx int
ChildQty int
}
2026-05-10 00:59:47 +00:00
func (s *Atree) GetAllPages(rootPageNo uint32) (_ []uint32, err error) {
2026-02-10 14:02:11 +00:00
var (
2026-05-10 00:59:47 +00:00
pageNumbers []uint32
levels []*Level
2026-02-10 14:02:11 +00:00
)
buf, err := s.fetchIndexPage(rootPageNo)
if err != nil {
2026-05-10 00:59:47 +00:00
return nil, fmt.Errorf("fetchIndexPage(%d): %s", rootPageNo, err)
2026-02-10 14:02:11 +00:00
}
2026-05-10 00:59:47 +00:00
pageNumbers = append(pageNumbers, rootPageNo)
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
// if buf[isDataPageNumbersIdx] == 1 {
// pageNumbers := listPageNumbers(buf)
// dataPages = append(dataPages, pageNumbers...)
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
// s.releasePage(rootPageNo)
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
// return PageLists{
// DataPages: dataPages,
// IndexPages: indexPages,
// }, nil
// }
2026-02-10 14:02:11 +00:00
levels = append(levels, &Level{
PageNo: rootPageNo,
PageData: buf,
Idx: 0,
ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]),
})
for {
if len(levels) == 0 {
2026-05-10 00:59:47 +00:00
return pageNumbers, nil
2026-02-10 14:02:11 +00:00
}
lastIdx := len(levels) - 1
level := levels[lastIdx]
if level.Idx < level.ChildQty {
pageNo := getPageNo(level.PageData, level.Idx)
level.Idx++
var buf []byte
2026-05-10 00:59:47 +00:00
buf, err = s.fetchPage(pageNo)
2026-02-10 14:02:11 +00:00
if err != nil {
2026-05-10 00:59:47 +00:00
return nil, fmt.Errorf("fetchPage(%d): %s", pageNo, err)
2026-02-10 14:02:11 +00:00
}
2026-05-10 00:59:47 +00:00
pageNumbers = append(pageNumbers, pageNo)
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
if buf[pageTypeIdx] == PageTypeData {
//pageNumbers := listPageNumbers(buf)
//dataPages = append(dataPages, pageNumbers...)
2026-02-23 19:37:05 +00:00
s.releasePage(pageNo)
2026-02-10 14:02:11 +00:00
} else {
levels = append(levels, &Level{
PageNo: pageNo,
PageData: buf,
Idx: 0,
ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]),
})
}
} else {
2026-02-23 19:37:05 +00:00
s.releasePage(level.PageNo)
2026-02-10 14:02:11 +00:00
levels = levels[:lastIdx]
}
}
}