2026-06-10 06:18:45 +03:00
|
|
|
|
package storage
|
2026-06-05 19:43:01 +00:00
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
|
2026-06-10 06:18:45 +03:00
|
|
|
|
bin "gordenko.dev/dima/bin/little"
|
2026-06-05 19:43:01 +00:00
|
|
|
|
"gordenko.dev/dima/qb"
|
|
|
|
|
|
"gordenko.dev/dima/qb/util"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Задача метода:
|
|
|
|
|
|
// - для data сторінок отримати pageNo та reused, задати sizes та prevPageNo, порахувати CRC32
|
|
|
|
|
|
// - додати пари до index, згенерувати нові index сторінки
|
|
|
|
|
|
// levels []*IndexLevel, dataPages []*DataPage
|
|
|
|
|
|
// fix - reduce leves
|
|
|
|
|
|
|
2026-06-13 08:01:42 +03:00
|
|
|
|
type PageIngester struct {
|
2026-06-12 06:11:57 +00:00
|
|
|
|
getDataPageNumber func() (uint32, bool, error)
|
|
|
|
|
|
getIndexPageNumber func() (uint32, bool, error)
|
2026-06-05 19:43:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 08:01:42 +03:00
|
|
|
|
type PageIngesterOptions struct {
|
2026-06-05 19:43:01 +00:00
|
|
|
|
GetDataPageNumber func() (uint32, bool, error)
|
|
|
|
|
|
GetIndexPageNumber func() (uint32, bool, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 08:01:42 +03:00
|
|
|
|
func NewPageIngester(opt PageIngesterOptions) (*PageIngester, error) {
|
2026-06-05 19:43:01 +00:00
|
|
|
|
if opt.GetIndexPageNumber == nil {
|
|
|
|
|
|
return nil, errors.New("missing required option: GetIndexPageNumber")
|
|
|
|
|
|
}
|
|
|
|
|
|
if opt.GetDataPageNumber == nil {
|
|
|
|
|
|
return nil, errors.New("missing required option: GetDataPageNumber")
|
|
|
|
|
|
}
|
2026-06-13 08:01:42 +03:00
|
|
|
|
s := &PageIngester{
|
2026-06-12 06:11:57 +00:00
|
|
|
|
getIndexPageNumber: opt.GetIndexPageNumber,
|
|
|
|
|
|
getDataPageNumber: opt.GetDataPageNumber,
|
2026-06-05 19:43:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
return s, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type IndexLevelTail struct {
|
2026-06-09 08:16:16 +03:00
|
|
|
|
Buffer []byte // розмір більший за кількість
|
2026-06-05 19:43:01 +00:00
|
|
|
|
RecordsCount int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type DataPayload struct {
|
|
|
|
|
|
Since uint32
|
|
|
|
|
|
Content []byte
|
|
|
|
|
|
TimestampsSize int
|
|
|
|
|
|
ValuesSize int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 08:01:42 +03:00
|
|
|
|
type IngestIn struct {
|
2026-06-05 19:43:01 +00:00
|
|
|
|
LastPageNo uint32
|
|
|
|
|
|
IndexLevelTails []IndexLevelTail
|
|
|
|
|
|
DataPages []DataPayload
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type SealedDataPage struct {
|
2026-06-13 08:01:42 +03:00
|
|
|
|
PageNo uint32
|
|
|
|
|
|
Reused bool
|
|
|
|
|
|
Content []byte
|
2026-06-05 19:43:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type SealedIndexPage struct {
|
2026-06-13 08:01:42 +03:00
|
|
|
|
PageNo uint32
|
|
|
|
|
|
Reused bool
|
|
|
|
|
|
Content []byte
|
2026-06-05 19:43:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 08:01:42 +03:00
|
|
|
|
func (s *PageIngester) Ingest(in IngestIn) ([]SealedDataPage, []*SealedIndexLevel) {
|
2026-06-05 19:43:01 +00:00
|
|
|
|
var (
|
|
|
|
|
|
sealedDataPages []SealedDataPage
|
|
|
|
|
|
sealedIndexLevels []*SealedIndexLevel
|
|
|
|
|
|
)
|
|
|
|
|
|
for _, tail := range in.IndexLevelTails {
|
|
|
|
|
|
sealedIndexLevels = append(sealedIndexLevels, &SealedIndexLevel{
|
|
|
|
|
|
SkipRecords: tail.RecordsCount,
|
2026-06-09 08:16:16 +03:00
|
|
|
|
TailRecords: tail.Buffer,
|
2026-06-05 19:43:01 +00:00
|
|
|
|
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 {
|
2026-06-13 08:01:42 +03:00
|
|
|
|
prevPageNo = sealedDataPages[dataPageIdx-1].PageNo
|
2026-06-05 19:43:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
pageNo, reused, err := s.getDataPageNumber()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
qb.Abort(qb.FailedGetPageNumber, err)
|
|
|
|
|
|
}
|
2026-06-13 08:01:42 +03:00
|
|
|
|
|
|
|
|
|
|
SealDataPage(SealDataPageIn{
|
|
|
|
|
|
Content: d.Content,
|
|
|
|
|
|
PrevPageNo: prevPageNo,
|
|
|
|
|
|
TimestampsSize: d.TimestampsSize,
|
|
|
|
|
|
ValuesSize: d.ValuesSize,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-05 19:43:01 +00:00
|
|
|
|
sealedDataPages = append(sealedDataPages, SealedDataPage{
|
2026-06-13 08:01:42 +03:00
|
|
|
|
Content: d.Content,
|
|
|
|
|
|
PageNo: pageNo,
|
|
|
|
|
|
Reused: reused,
|
2026-06-05 19:43:01 +00:00
|
|
|
|
})
|
|
|
|
|
|
upPageNo = pageNo
|
|
|
|
|
|
for {
|
|
|
|
|
|
if levelIdx == len(sealedIndexLevels) {
|
|
|
|
|
|
// новий root
|
|
|
|
|
|
sealedIndexLevels = append(sealedIndexLevels, &SealedIndexLevel{
|
2026-06-13 08:01:42 +03:00
|
|
|
|
TailRecords: appendIndexRecord(appendIndexRecordIn{
|
2026-06-05 19:43:01 +00:00
|
|
|
|
Timestamp: upTimestamp,
|
|
|
|
|
|
PageNo: upPageNo,
|
|
|
|
|
|
}),
|
|
|
|
|
|
TailRecordsCount: 1,
|
|
|
|
|
|
})
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
level := sealedIndexLevels[levelIdx]
|
2026-06-12 06:11:57 +00:00
|
|
|
|
if level.TailRecordsCount < maxRecordsOnIndexPage {
|
2026-06-13 08:01:42 +03:00
|
|
|
|
level.TailRecords = appendIndexRecord(appendIndexRecordIn{
|
|
|
|
|
|
Records: level.TailRecords,
|
|
|
|
|
|
RecordsCount: level.TailRecordsCount,
|
|
|
|
|
|
Timestamp: upTimestamp,
|
|
|
|
|
|
PageNo: upPageNo,
|
2026-06-05 19:43:01 +00:00
|
|
|
|
})
|
|
|
|
|
|
level.TailRecordsCount++
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
// в буфері немає місця для додавання нової пари, отже це
|
|
|
|
|
|
// заповнена сторінка
|
|
|
|
|
|
pageNo, reused, err := s.getIndexPageNumber()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
qb.Abort(qb.FailedGetPageNumber, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 08:01:42 +03:00
|
|
|
|
SealIndexPage(SealIndexPageIn{
|
|
|
|
|
|
Content: level.TailRecords,
|
|
|
|
|
|
RecordsCount: level.TailRecordsCount,
|
|
|
|
|
|
ZeroLevel: levelIdx == 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-05 19:43:01 +00:00
|
|
|
|
filled := SealedIndexPage{
|
|
|
|
|
|
Content: level.TailRecords,
|
|
|
|
|
|
PageNo: pageNo,
|
|
|
|
|
|
Reused: reused,
|
|
|
|
|
|
}
|
|
|
|
|
|
level.IndexPages = append(level.IndexPages, filled)
|
|
|
|
|
|
// новий tail на індексному рівні
|
2026-06-13 08:01:42 +03:00
|
|
|
|
level.TailRecords = appendIndexRecord(appendIndexRecordIn{
|
2026-06-05 19:43:01 +00:00
|
|
|
|
Timestamp: upTimestamp,
|
|
|
|
|
|
PageNo: upPageNo,
|
|
|
|
|
|
})
|
|
|
|
|
|
level.TailRecordsCount = 1
|
|
|
|
|
|
//
|
|
|
|
|
|
upPageNo = pageNo
|
2026-06-10 06:18:45 +03:00
|
|
|
|
upTimestamp, _ = bin.GetUint32(filled.Content)
|
2026-06-05 19:43:01 +00:00
|
|
|
|
levelIdx++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-13 08:01:42 +03:00
|
|
|
|
return sealedDataPages, sealedIndexLevels
|
2026-06-05 19:43:01 +00:00
|
|
|
|
}
|
2026-06-10 06:18:45 +03:00
|
|
|
|
|
|
|
|
|
|
// HELPERS
|
|
|
|
|
|
|
|
|
|
|
|
type SealDataPageIn struct {
|
|
|
|
|
|
Content []byte
|
|
|
|
|
|
PrevPageNo uint32
|
|
|
|
|
|
TimestampsSize int
|
|
|
|
|
|
ValuesSize int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func SealDataPage(in SealDataPageIn) (checksum uint32) {
|
2026-06-12 06:11:57 +00:00
|
|
|
|
bin.PutUint16(in.Content[timestampsSizeIdx:], uint16(in.TimestampsSize))
|
|
|
|
|
|
bin.PutUint16(in.Content[valuesSizeIdx:], uint16(in.ValuesSize))
|
|
|
|
|
|
bin.PutUint32(in.Content[prevPageIdx:], in.PrevPageNo)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
|
2026-06-13 08:01:42 +03:00
|
|
|
|
checksum = util.CalculateCRC32(in.Content[:dataCRC32Idx])
|
2026-06-12 06:11:57 +00:00
|
|
|
|
bin.PutUint32(in.Content[dataCRC32Idx:], checksum)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type SealIndexPageIn struct {
|
|
|
|
|
|
Content []byte
|
|
|
|
|
|
RecordsCount int
|
|
|
|
|
|
ZeroLevel bool
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func SealIndexPage(in SealIndexPageIn) (checksum uint32) {
|
2026-06-12 06:11:57 +00:00
|
|
|
|
bin.PutUint16(in.Content[indexRecordsCountIdx:], uint16(in.RecordsCount))
|
|
|
|
|
|
if in.ZeroLevel {
|
|
|
|
|
|
in.Content[isZeroLevelIdx] = 1
|
|
|
|
|
|
}
|
2026-06-13 08:01:42 +03:00
|
|
|
|
checksum = util.CalculateCRC32(in.Content[:indexCRC32Idx])
|
2026-06-12 06:11:57 +00:00
|
|
|
|
bin.PutUint32(in.Content[indexCRC32Idx:], checksum)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-13 08:01:42 +03:00
|
|
|
|
|
|
|
|
|
|
type appendIndexRecordIn struct {
|
|
|
|
|
|
Records []byte
|
|
|
|
|
|
RecordsCount int
|
|
|
|
|
|
Timestamp uint32
|
|
|
|
|
|
PageNo uint32
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func appendIndexRecord(in appendIndexRecordIn) []byte {
|
2026-06-13 07:13:03 +00:00
|
|
|
|
//fmt.Printf("appendIndexRecord: % x\n", in.Records)
|
|
|
|
|
|
//fmt.Printf("timestamp: %d, pageNo: %d\n", in.Timestamp, in.PageNo)
|
2026-06-13 08:01:42 +03:00
|
|
|
|
if in.RecordsCount < maxRecordsOnIndexPage {
|
|
|
|
|
|
var (
|
|
|
|
|
|
pos int
|
|
|
|
|
|
buf = in.Records
|
|
|
|
|
|
)
|
|
|
|
|
|
if in.RecordsCount > 0 {
|
2026-06-13 22:43:17 +00:00
|
|
|
|
pos = in.RecordsCount * IndexRecordSize
|
2026-06-13 07:13:03 +00:00
|
|
|
|
//fmt.Printf("pos: %d\n", pos)
|
2026-06-13 08:01:42 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
buf = make([]byte, IndexPageSize) // IndexPageIncSize
|
|
|
|
|
|
}
|
|
|
|
|
|
bin.PutUint32(buf[pos:], in.Timestamp)
|
|
|
|
|
|
bin.PutUint32(buf[pos+4:], in.PageNo)
|
2026-06-13 07:13:03 +00:00
|
|
|
|
//fmt.Printf("appendIndexRecord after: % x\n", buf)
|
2026-06-13 08:01:42 +03:00
|
|
|
|
return buf
|
|
|
|
|
|
}
|
|
|
|
|
|
qb.Abort(qb.NoSpaceOnIndexPage, nil)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|