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])) // } //