package atree import ( "errors" "fmt" "hash/crc32" "os" "path/filepath" "sync" diploma "gordenko.dev/dima/qb" "gordenko.dev/dima/qb/bin" ) const ( PageTypeData = 1 PageTypeIndex = 2 filePerm = 0770 // common crc32Idx = PageSize - 4 pageType = PageSize - 5 // index page indexRecordsQtyIdx = PageSize - 8 isDataPageNumbersIdx = PageSize - 6 // data page timestampsSizeIdx = PageSize - 13 valuesSizeIdx = PageSize - 11 prevPageIdx = PageSize - 9 timestampSize = 4 pairSize = timestampSize + PageNoSize indexFooterIdx = indexRecordsQtyIdx dataFooterIdx = timestampsSizeIdx 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 } type _page struct { PageNo uint32 Buf []byte ReferenceCount int } type Atree struct { 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 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.DatabaseName == "" { return nil, errors.New("DatabaseName option is required") } if opt.FreeList == nil { return nil, errors.New("FreeList 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{ //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 } 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) if buf[isDataPageNumbersIdx] == 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) if buf[isDataPageNumbersIdx] == 1 { return pathToDataPage{ Legs: legs, LastPageNo: foundPageNo, }, nil } // вглубь pageNo = foundPageNo } } // APPEND DATA PAGE type AppendDataPageReq struct { MetricID uint32 Timestamp uint32 Value float64 Since uint32 RootPageNo uint32 PrevPageNo uint32 TimestampsChunks [][]byte TimestampsSize uint16 ValuesChunks [][]byte ValuesSize uint16 } type Report struct { //IsDataPageReused bool //DataPageNo uint32 IsRootChanged bool NewRootPageNo uint32 //ReusedIndexPages []uint32 Pages []PageToWrite } // 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 ( pagesToRelease []uint32 report Report ) newDataPage := s.allocPage() pagesToRelease = append(pagesToRelease, newDataPage.PageNo) chunksToDataPage(newDataPage.Data, chunksToDataPageReq{ PrevPageNo: req.PrevPageNo, TimestampsChunks: req.TimestampsChunks, TimestampsSize: req.TimestampsSize, ValuesChunks: req.ValuesChunks, ValuesSize: req.ValuesSize, }) report.Pages = append(report.Pages, PageToWrite{ PageNo: newDataPage.PageNo, Data: newDataPage.Data, IsReused: newDataPage.IsReused, }) // 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 path, err = s.findPathToLastPage(req.RootPageNo) if err != nil { return } for _, leg := range path.Legs { pagesToRelease = append(pagesToRelease, leg.PageNo) } if path.LastPageNo != req.PrevPageNo { diploma.Abort( diploma.WrongPrevPageNo, fmt.Errorf("bug: last pageNo %d in tree != prev pageNo %d in _metric", path.LastPageNo, req.PrevPageNo), ) } newPageNo := newDataPage.PageNo lastIdx := len(path.Legs) - 1 for legIdx := lastIdx; legIdx >= 0; legIdx-- { leg := path.Legs[legIdx] ok := appendPair(leg.Data, req.Since, newPageNo) if ok { // 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.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 |= 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.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 логе 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.allocPage() pagesToRelease = append(pagesToRelease, newRoot.PageNo) newRoot.Data[isDataPageNumbersIdx] = 1 appendPair(newRoot.Data, req.Since, newDataPage.PageNo) 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 // } // На данний момен схема - наступна. Всі сторінки - data та index - зафіксовані в кеші. // Отже запис на диск пройде максимально швидко. Після цього ReferenceCount кожної // сторінки зменшиться на 1. Оскільки на метрику утримується XLock, сторінки мають // ReferenceCount = 1 (немає інших читачів). // waitCh := make(chan struct{}) // task := WriteTask{ // WaitCh: waitCh, // Pages: report.Pages, // } // s.appendWriteTaskToQueue(task) // <-waitCh // for _, pageNo := range dataPagesToRelease { // s.releasePage(pageNo) // } // for _, pageNo := range indexPagesToRelease { // s.releasePage(pageNo) // } return report, nil } // DELETE type PageLists struct { DataPages []uint32 IndexPages []uint32 } type Level struct { PageNo uint32 PageData []byte Idx int ChildQty int } func (s *Atree) GetAllPages(rootPageNo uint32) (_ PageLists, err error) { var ( dataPages []uint32 indexPages []uint32 levels []*Level ) buf, err := s.fetchIndexPage(rootPageNo) if err != nil { err = fmt.Errorf("fetchIndexPage(%d): %s", rootPageNo, err) return } indexPages = append(indexPages, 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 PageLists{ DataPages: dataPages, IndexPages: indexPages, }, 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.fetchIndexPage(pageNo) if err != nil { err = fmt.Errorf("fetchIndexPage(%d): %s", pageNo, err) return } indexPages = append(indexPages, pageNo) if buf[isDataPageNumbersIdx] == 1 { 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] } } }