Files
qb/atree/atree.go
2026-05-31 20:01:28 +00:00

469 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package atree
import (
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"gordenko.dev/dima/qb/bin"
)
const (
PageTypeData = 1
PageTypeIndex = 2
filePerm = 0770
// common
crc32Idx = PageSize - 4
pageTypeIdx = PageSize - 5
// index page
indexCRC32Idx = IndexPageSize - 4
isLastLevelIdx = IndexPageSize - 5
indexRecordsQtyIdx = IndexPageSize - 7
indexRecordSize = 8
// data page
dataCRC32Idx = DataPageSize - 4
prevPageIdx = DataPageSize - 8
valuesSizeIdx = DataPageSize - 10
timestampsSizeIdx = DataPageSize - 12
timestampSize = 4
pairSize = timestampSize + PageNoSize
indexFooterIdx = indexRecordsQtyIdx
dataFooterIdx = timestampsSizeIdx
PageSize = 8192
DataPageSize = 8192
IndexPageSize = 1024
PageNoSize = 4
//
DataPagePayloadSize int = dataFooterIdx
)
type _page struct {
PageNo uint32
Buf []byte
ReferenceCount int
}
type Atree struct {
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
DatabaseName string
}
func New(opt Options) (*Atree, error) {
if opt.Dir == "" {
return nil, errors.New("Dir option is required")
}
if opt.DatabaseName == "" {
return nil, errors.New("DatabaseName option is required")
}
// открываю или создаю dbName.data и dbName.index файлы
var (
fileName = filepath.Join(opt.Dir, opt.DatabaseName+".db")
file *os.File
allocatedPagesQty uint32
)
// При создании data файла сразу создается индекс, поэтому корректное
// состояние БД: либо оба файла есть, либо ни одного файла нет.
isDataExist, err := isFileExist(fileName)
if err != nil {
return nil, fmt.Errorf("check data file is exist: %s", err)
}
if isDataExist {
file, allocatedPagesQty, err = openFile(fileName, PageSize)
if err != nil {
return nil, fmt.Errorf("open data file: %s", err)
}
} else {
// нет файла
file, err = os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, filePerm)
if err != nil {
return nil, err
}
}
tree := &Atree{
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
}
func (s *Atree) Run() {
//go s.pageWriter()
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)
s.releasePage(indexPageNo)
// fix
if buf[isLastLevelIdx] == 1 {
buf, err := s.fetchDataPage(foundPageNo)
if err != nil {
return 0, nil, fmt.Errorf("fetchDataPage(%d): %s", foundPageNo, err)
}
return foundPageNo, buf, nil
}
// вглубь
indexPageNo = foundPageNo
}
}
type PathLeg struct {
PageNo uint32
Data []byte
}
type PathToDataPage struct {
Legs []PathLeg
LastPageNo uint32
}
func (s *Atree) FindPathToLastPage(rootPageNo uint32) (_ PathToDataPage, err error) {
var (
pageNo = rootPageNo
legs []PathLeg
)
for {
var buf []byte
buf, err = s.fetchIndexPage(pageNo)
if err != nil {
err = fmt.Errorf("FetchIndexPage(%d): %s", pageNo, err)
return
}
legs = append(legs, PathLeg{
PageNo: pageNo,
Data: buf,
// childIdx не нужен
})
foundPageNo := getLastPageNo(buf)
// fix
if buf[isLastLevelIdx] == 1 {
return PathToDataPage{
Legs: legs,
LastPageNo: foundPageNo,
}, nil
}
// вглубь
pageNo = foundPageNo
}
}
// DELETE
type Level struct {
PageNo uint32
PageData []byte
Idx int
ChildQty int
}
func (s *Atree) GetAllPages(rootPageNo uint32) (_ []uint32, err error) {
var (
pageNumbers []uint32
levels []*Level
)
buf, err := s.fetchIndexPage(rootPageNo)
if err != nil {
return nil, fmt.Errorf("fetchIndexPage(%d): %s", rootPageNo, err)
}
pageNumbers = append(pageNumbers, rootPageNo)
// if buf[isDataPageNumbersIdx] == 1 {
// pageNumbers := listPageNumbers(buf)
// dataPages = append(dataPages, pageNumbers...)
// s.releasePage(rootPageNo)
// return PageLists{
// DataPages: dataPages,
// IndexPages: indexPages,
// }, nil
// }
levels = append(levels, &Level{
PageNo: rootPageNo,
PageData: buf,
Idx: 0,
ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]),
})
for {
if len(levels) == 0 {
return pageNumbers, nil
}
lastIdx := len(levels) - 1
level := levels[lastIdx]
if level.Idx < level.ChildQty {
pageNo := getPageNo(level.PageData, level.Idx)
level.Idx++
var buf []byte
buf, err = s.fetchPage(pageNo)
if err != nil {
return nil, fmt.Errorf("fetchPage(%d): %s", pageNo, err)
}
pageNumbers = append(pageNumbers, pageNo)
if buf[pageTypeIdx] == PageTypeData {
//pageNumbers := listPageNumbers(buf)
//dataPages = append(dataPages, pageNumbers...)
s.releasePage(pageNo)
} else {
levels = append(levels, &Level{
PageNo: pageNo,
PageData: buf,
Idx: 0,
ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]),
})
}
} else {
s.releasePage(level.PageNo)
levels = levels[:lastIdx]
}
}
}
// const (
// FlagReused byte = 1 // сторінка із FreeList
// FlagNewRoot byte = 2 // новая страница
// )
// type PageToWrite struct {
// PageNo uint32
// Data []byte
// IsReused bool
// }
//type AppendDataPagesReq struct {
// LastPageNo uint32
// Legs []PathLeg
// DataPages []NotLinkedDataPage
// }
// type Report struct {
// NewRootPageNo uint32
// LastPageNo uint32
// Pages []PageToWrite
// }
// // Можливо 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)
// )
// for _, p := range req.DataPages {
// newDataPageNo, isReused := s.allocPageNumber() // alloc only number
// // pagesToRelease = append(pagesToRelease, newDataPage.PageNo)
// // set prevPageNo
// p.SetPrevPageNo(lastPageNo)
// pages = append(pages, PageToWrite{
// PageNo: newDataPageNo,
// Data: p.Data,
// IsReused: isReused,
// })
// // 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
// // }
// pages = append(pages, PageToWrite{
// PageNo: newIndexPage.PageNo,
// Data: newIndexPage.Data,
// IsReused: newIndexPage.IsReused,
// })
// // замінюю крок в 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
// }
// }
// } 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...)
// }
// // На данний момен схема - наступна. Всі сторінки - data та index - зафіксовані в кеші.
// // Отже запис на диск пройде максимально швидко. Після цього ReferenceCount кожної
// // сторінки зменшиться на 1. Оскільки на метрику утримується XLock, сторінки мають
// // ReferenceCount = 1 (немає інших читачів).
// // for _, pageNo := range indexPagesToRelease {
// // s.releasePage(pageNo)
// // }
// 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,
// }
// }
// APPEND DATA PAGE
//MetricID uint32
//Timestamp uint32
//Value float64
//RootPageNo uint32
//PrevPageNo uint32
// type AppendDataPageReq struct {
// Since uint32
// TimestampsChunks [][]byte
// TimestampsSize uint16
// ValuesChunks [][]byte
// ValuesSize uint16
// }
// 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 будуть відбуватись в одному потоці
// NotLinkedDataPage - це data сторінка із payload, але без встановленого prevPageNo та без розрахованого CRC32
// type NotLinkedDataPage struct {
// Since uint32
// Data []byte
// }
// func (s NotLinkedDataPage) SetPrevPageNo(prevPageNo uint32) {
// bin.PutUint32(s.Data[prevPageIdx:], prevPageNo)
// bin.PutUint32(s.Data[crc32Idx:], util.CalcChecksum(s.Data[:crc32Idx]))
// }
//