package storage import ( "errors" bin "gordenko.dev/dima/bin/little" "gordenko.dev/dima/qb" "gordenko.dev/dima/qb/util" ) // Задача метода: // - для data сторінок отримати pageNo та reused, задати sizes та prevPageNo, порахувати CRC32 // - додати пари до index, згенерувати нові index сторінки // levels []*IndexLevel, dataPages []*DataPage // fix - reduce leves type PagePreparer struct { getDataPageNumber func() (uint32, bool, error) getIndexPageNumber func() (uint32, bool, error) } type PagePreparerOptions struct { GetDataPageNumber func() (uint32, bool, error) GetIndexPageNumber func() (uint32, bool, error) } func NewPagePreparer(opt PagePreparerOptions) (*PagePreparer, error) { if opt.GetIndexPageNumber == nil { return nil, errors.New("missing required option: GetIndexPageNumber") } if opt.GetDataPageNumber == nil { return nil, errors.New("missing required option: GetDataPageNumber") } s := &PagePreparer{ getIndexPageNumber: opt.GetIndexPageNumber, getDataPageNumber: opt.GetDataPageNumber, } return s, nil } type appendIndexRecordIn struct { Records []byte RecordsCount int Timestamp uint32 PageNo uint32 } func (s *PagePreparer) appendIndexRecord(in appendIndexRecordIn) []byte { if in.RecordsCount < maxRecordsOnIndexPage { var ( pos int buf = in.Records ) if in.RecordsCount > 0 { pos = in.RecordsCount * indexRecordSize } else { buf = make([]byte, IndexPageSize) // IndexPageIncSize } bin.PutUint32(buf[pos:], in.Timestamp) bin.PutUint32(buf[pos+4:], in.PageNo) return buf } qb.Abort(qb.NoSpaceOnIndexPage, nil) return nil } type IndexLevelTail struct { Buffer []byte // розмір більший за кількість RecordsCount int } type DataPayload struct { Since uint32 Content []byte TimestampsSize int ValuesSize int } type SealPagesIn struct { LastPageNo uint32 IndexLevelTails []IndexLevelTail DataPages []DataPayload } type SealedDataPage struct { PageNo uint32 Reused bool PrevPageNo uint32 Checksum uint32 Content []byte } type SealedIndexPage struct { Content []byte PageNo uint32 Reused bool Checksum uint32 } // SealedIndexLevel - як зрозуміти чи потрібно щось писати в WAL чи index файл, // чи додавання нових data сторінок не зачепило індексний рівень? // Якщо є IndexPages - пишемо весь рівень в WAL і готові сторінки в index файл. // Якщо IndexPages пустий - порівнюємо Offset і len(Payload) - якщо довжина // більша за змішення - дані додані на незаповнену сторінку рівня, // отже різницю пишемо в WAL. type SealedIndexLevel struct { // Якщо є IndexPages, offset - це зміщення із якого починаються дані першої сторінки, // які ще не зписані в WAL. // Якщо IndexPages пустий, offset - це зміщення із якого починаються дані payload, // які ще не зписані в WAL. // В payload в будь-якому разі дані останньої індексної сторінки рівня (незаповненої). SkipRecords int IndexPages []SealedIndexPage TailRecords []byte TailRecordsCount int } type SealResult struct { DataPages []SealedDataPage IndexLevels []*SealedIndexLevel } func (s *PagePreparer) SealPages(in SealPagesIn) (result SealResult) { var ( sealedDataPages []SealedDataPage sealedIndexLevels []*SealedIndexLevel ) for _, tail := range in.IndexLevelTails { sealedIndexLevels = append(sealedIndexLevels, &SealedIndexLevel{ SkipRecords: tail.RecordsCount, TailRecords: tail.Buffer, TailRecordsCount: tail.RecordsCount, }) } for dataPageIdx, d := range in.DataPages { var ( upTimestamp = d.Since upPageNo uint32 levelIdx = 0 prevPageNo uint32 ) if dataPageIdx == 0 { prevPageNo = in.LastPageNo } else { prevPageNo = result.DataPages[dataPageIdx-1].PageNo } pageNo, reused, err := s.getDataPageNumber() if err != nil { qb.Abort(qb.FailedGetPageNumber, err) } sealedDataPages = append(sealedDataPages, SealedDataPage{ Content: d.Content, PrevPageNo: prevPageNo, PageNo: pageNo, Reused: reused, Checksum: SealDataPage(SealDataPageIn{ Content: d.Content, PrevPageNo: prevPageNo, TimestampsSize: d.TimestampsSize, ValuesSize: d.ValuesSize, }), }) upPageNo = pageNo for { if levelIdx == len(sealedIndexLevels) { // новий root sealedIndexLevels = append(sealedIndexLevels, &SealedIndexLevel{ TailRecords: s.appendIndexRecord(appendIndexRecordIn{ Records: nil, Timestamp: upTimestamp, PageNo: upPageNo, }), TailRecordsCount: 1, }) break } level := sealedIndexLevels[levelIdx] if level.TailRecordsCount < maxRecordsOnIndexPage { level.TailRecords = s.appendIndexRecord(appendIndexRecordIn{ Records: level.TailRecords, Timestamp: upTimestamp, PageNo: upPageNo, }) level.TailRecordsCount++ break } // в буфері немає місця для додавання нової пари, отже це // заповнена сторінка pageNo, reused, err := s.getIndexPageNumber() if err != nil { qb.Abort(qb.FailedGetPageNumber, err) } filled := SealedIndexPage{ Content: level.TailRecords, PageNo: pageNo, Reused: reused, Checksum: SealIndexPage(SealIndexPageIn{ Content: level.TailRecords, RecordsCount: level.TailRecordsCount, ZeroLevel: levelIdx == 0, }), } level.IndexPages = append(level.IndexPages, filled) // новий tail на індексному рівні level.TailRecords = s.appendIndexRecord(appendIndexRecordIn{ Records: nil, Timestamp: upTimestamp, PageNo: upPageNo, }) level.TailRecordsCount = 1 // upPageNo = pageNo upTimestamp, _ = bin.GetUint32(filled.Content) levelIdx++ } } return SealResult{ DataPages: sealedDataPages, IndexLevels: sealedIndexLevels, } } // HELPERS type SealDataPageIn struct { Content []byte PrevPageNo uint32 TimestampsSize int ValuesSize int } func SealDataPage(in SealDataPageIn) (checksum uint32) { bin.PutUint16(in.Content[timestampsSizeIdx:], uint16(in.TimestampsSize)) bin.PutUint16(in.Content[valuesSizeIdx:], uint16(in.ValuesSize)) bin.PutUint32(in.Content[prevPageIdx:], in.PrevPageNo) checksum = util.CalcChecksum(in.Content[:dataCRC32Idx]) bin.PutUint32(in.Content[dataCRC32Idx:], checksum) return } type SealIndexPageIn struct { Content []byte RecordsCount int ZeroLevel bool } func SealIndexPage(in SealIndexPageIn) (checksum uint32) { bin.PutUint16(in.Content[indexRecordsCountIdx:], uint16(in.RecordsCount)) if in.ZeroLevel { in.Content[isZeroLevelIdx] = 1 } checksum = util.CalcChecksum(in.Content[:indexCRC32Idx]) bin.PutUint32(in.Content[indexCRC32Idx:], checksum) return }