This commit is contained in:
2026-06-13 08:01:42 +03:00
parent bf22e9a85e
commit aca0252cfd
19 changed files with 2001 additions and 1240 deletions

247
storage/page_ingester.go Normal file
View File

@@ -0,0 +1,247 @@
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 PageIngester struct {
getDataPageNumber func() (uint32, bool, error)
getIndexPageNumber func() (uint32, bool, error)
}
type PageIngesterOptions struct {
GetDataPageNumber func() (uint32, bool, error)
GetIndexPageNumber func() (uint32, bool, error)
}
func NewPageIngester(opt PageIngesterOptions) (*PageIngester, 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 := &PageIngester{
getIndexPageNumber: opt.GetIndexPageNumber,
getDataPageNumber: opt.GetDataPageNumber,
}
return s, nil
}
type IndexLevelTail struct {
Buffer []byte // розмір більший за кількість
RecordsCount int
}
type DataPayload struct {
Since uint32
Content []byte
TimestampsSize int
ValuesSize int
}
type IngestIn struct {
LastPageNo uint32
IndexLevelTails []IndexLevelTail
DataPages []DataPayload
}
type SealedDataPage struct {
PageNo uint32
Reused bool
Content []byte
}
type SealedIndexPage struct {
PageNo uint32
Reused bool
Content []byte
}
// 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
}
func (s *PageIngester) Ingest(in IngestIn) ([]SealedDataPage, []*SealedIndexLevel) {
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 = sealedDataPages[dataPageIdx-1].PageNo
}
pageNo, reused, err := s.getDataPageNumber()
if err != nil {
qb.Abort(qb.FailedGetPageNumber, err)
}
SealDataPage(SealDataPageIn{
Content: d.Content,
PrevPageNo: prevPageNo,
TimestampsSize: d.TimestampsSize,
ValuesSize: d.ValuesSize,
})
sealedDataPages = append(sealedDataPages, SealedDataPage{
Content: d.Content,
PageNo: pageNo,
Reused: reused,
})
upPageNo = pageNo
for {
if levelIdx == len(sealedIndexLevels) {
// новий root
sealedIndexLevels = append(sealedIndexLevels, &SealedIndexLevel{
TailRecords: appendIndexRecord(appendIndexRecordIn{
Timestamp: upTimestamp,
PageNo: upPageNo,
}),
TailRecordsCount: 1,
})
break
}
level := sealedIndexLevels[levelIdx]
if level.TailRecordsCount < maxRecordsOnIndexPage {
level.TailRecords = appendIndexRecord(appendIndexRecordIn{
Records: level.TailRecords,
RecordsCount: level.TailRecordsCount,
Timestamp: upTimestamp,
PageNo: upPageNo,
})
level.TailRecordsCount++
break
}
// в буфері немає місця для додавання нової пари, отже це
// заповнена сторінка
pageNo, reused, err := s.getIndexPageNumber()
if err != nil {
qb.Abort(qb.FailedGetPageNumber, err)
}
SealIndexPage(SealIndexPageIn{
Content: level.TailRecords,
RecordsCount: level.TailRecordsCount,
ZeroLevel: levelIdx == 0,
})
filled := SealedIndexPage{
Content: level.TailRecords,
PageNo: pageNo,
Reused: reused,
}
level.IndexPages = append(level.IndexPages, filled)
// новий tail на індексному рівні
level.TailRecords = appendIndexRecord(appendIndexRecordIn{
Timestamp: upTimestamp,
PageNo: upPageNo,
})
level.TailRecordsCount = 1
//
upPageNo = pageNo
upTimestamp, _ = bin.GetUint32(filled.Content)
levelIdx++
}
}
return sealedDataPages, 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.CalculateCRC32(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.CalculateCRC32(in.Content[:indexCRC32Idx])
bin.PutUint32(in.Content[indexCRC32Idx:], checksum)
return
}
type appendIndexRecordIn struct {
Records []byte
RecordsCount int
Timestamp uint32
PageNo uint32
}
func 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
}