This commit is contained in:
2026-06-15 10:47:24 +00:00
parent ca125c99d2
commit 06823ee547
28 changed files with 1243 additions and 1383 deletions

136
storage/cursor.go Normal file
View File

@@ -0,0 +1,136 @@
package storage
import (
"errors"
"fmt"
bin "gordenko.dev/dima/bin/little"
"gordenko.dev/dima/qb"
)
type BackwardCursor struct {
metricType qb.MetricType
fracDigits byte
fetchDataPage func(uint32) ([]byte, error)
releasePage func(uint32)
pageNo uint32
pageData []byte
timestampDecompressor qb.TimestampDecompressor
valueDecompressor qb.ValueDecompressor
}
type BackwardCursorOptions struct {
MetricType qb.MetricType
FracDigits byte
PageNo uint32
PageData []byte
FetchDataPage func(uint32) ([]byte, error)
ReleasePage func(uint32)
}
func NewBackwardCursor(opt BackwardCursorOptions) (*BackwardCursor, error) {
switch opt.MetricType {
case qb.Instant, qb.Cumulative:
// ok
default:
return nil, fmt.Errorf("MetricType option has wrong value: %d", opt.MetricType)
}
if opt.FracDigits > qb.MaxFracDigits {
return nil, errors.New("FracDigits option is required")
}
if opt.FetchDataPage == nil {
return nil, errors.New("FetchDataPage option is required")
}
if opt.ReleasePage == nil {
return nil, errors.New("ReleasePage option is required")
}
if opt.PageNo == 0 {
return nil, errors.New("PageNo option is required")
}
if len(opt.PageData) == 0 {
return nil, errors.New("PageData option is required")
}
s := &BackwardCursor{
metricType: opt.MetricType,
fracDigits: opt.FracDigits,
fetchDataPage: opt.FetchDataPage,
releasePage: opt.ReleasePage,
pageNo: opt.PageNo,
pageData: opt.PageData,
}
var err error
s.timestampDecompressor, err = CreateTimestampDecompressor(s.pageData)
if err != nil {
return nil, fmt.Errorf("CreateTimestampDecompressor: %s", err)
}
s.valueDecompressor, err = CreateValueDecompressor(s.pageData, s.metricType, s.fracDigits)
if err != nil {
return nil, fmt.Errorf("CreateValueDecompressor: %s", err)
}
return s, nil
}
// timestamp, value, done, error
func (s *BackwardCursor) Prev() (uint32, float64, bool, error) {
var (
timestamp uint32
value float64
done bool
err error
)
timestamp, done = s.timestampDecompressor.NextValue()
if !done {
value, done = s.valueDecompressor.NextValue()
if done {
return 0, 0, false,
fmt.Errorf("corrupted data page %d: has timestamp, no value",
s.pageNo)
}
return timestamp, value, false, nil
}
prevPageNo, _ := bin.GetUint32(s.pageData[prevPageIdx:])
if prevPageNo == 0 {
return 0, 0, true, nil
}
s.releasePage(s.pageNo)
s.pageNo = prevPageNo
s.pageData, err = s.fetchDataPage(s.pageNo)
if err != nil {
return 0, 0, false,
fmt.Errorf("fetchDataPage(%d): %s", s.pageNo, err)
}
s.timestampDecompressor, err = CreateTimestampDecompressor(s.pageData)
if err != nil {
return 0, 0, false,
fmt.Errorf("CreateTimestampDecompressor: %s", err)
}
s.valueDecompressor, err = CreateValueDecompressor(s.pageData, s.metricType, s.fracDigits)
if err != nil {
return 0, 0, false,
fmt.Errorf("CreateValueDecompressor: %s", err)
}
//
timestamp, done = s.timestampDecompressor.NextValue()
if done {
return 0, 0, false,
fmt.Errorf("corrupted data page %d: no timestamps", s.pageNo)
}
value, done = s.valueDecompressor.NextValue()
if done {
return 0, 0, false,
fmt.Errorf("corrupted data page %d: no values", s.pageNo)
}
return timestamp, value, false, nil
}
func (s *BackwardCursor) Close() {
s.releasePage(s.pageNo)
}
// HELPER
//func (s *BackwardCursor) makeDecompressors() error {
//}

View File

@@ -6,87 +6,50 @@ import (
bin "gordenko.dev/dima/bin/little"
"gordenko.dev/dima/qb"
"gordenko.dev/dima/qb/enc"
"gordenko.dev/dima/qb/util"
)
// func (s *BackwardCursor) makeDecompressors() error {
// timestampsSize, _ := bin.GetUint16(s.pageData[timestampsSizeIdx:])
// valuesSize, _ := bin.GetUint16(s.pageData[valuesSizeIdx:])
// payloadSize := timestampsSize + valuesSize
// if payloadSize > dataFooterIdx {
// return fmt.Errorf("corrupted data page %d: timestamps + values size %d gt payload size",
// s.pageNo, payloadSize)
// }
// s.timestampDecompressor = enc.NewTimeDeltaDecompressor(
// s.pageData[:timestampsSize],
// )
// vbuf := s.pageData[timestampsSize : timestampsSize+valuesSize]
// switch s.metricType {
// case qb.Instant:
// s.valueDecompressor = enc.NewInstantDeltaDecompressor(
// vbuf, s.fracDigits)
// case qb.Cumulative:
// s.valueDecompressor = enc.NewCumulativeDeltaDecompressor(
// vbuf, s.fracDigits)
// default:
// return fmt.Errorf("bug: wrong metricType %d", s.metricType)
// }
// return nil
// }
func makeDecompressors(pageData []byte, metricType qb.MetricType, fracDigits byte) (
qb.TimestampDecompressor, qb.ValueDecompressor, error,
) {
valuesSize, _ := bin.GetUint16(pageData[valuesSizeIdx:])
payloadSize := timestampsSize + valuesSize
if payloadSize > dataFooterIdx {
return nil, nil, fmt.Errorf("corrupted: timestamps + values size %d > payload size",
payloadSize)
}
timestampDecompressor := enc.NewTimeDeltaDecompressor(
pageData[:timestampsSize],
)
vbuf := pageData[timestampsSize : timestampsSize+valuesSize]
var valueDecompressor qb.ValueDecompressor
switch metricType {
case qb.Instant:
valueDecompressor = enc.NewInstantDeltaDecompressor(
vbuf, fracDigits)
case qb.Cumulative:
valueDecompressor = enc.NewCumulativeDeltaDecompressor(
vbuf, fracDigits)
default:
return nil, nil, fmt.Errorf("bug: wrong metricType %d", metricType)
}
return timestampDecompressor, valueDecompressor, nil
return nil, nil, nil
}
func CreateTimeDeltaDecompressor(page []byte) qb.TimestampDecompressor {
func CreateTimestampDecompressor(page []byte) (qb.TimestampDecompressor, error) {
size, _ := bin.GetUint16(page[timestampsSizeIdx:])
if size > DataPageFooterSize {
return nil, fmt.Errorf("bug: invalid timestamps size %d", size)
}
pos := DataPagePayloadSize - int(size)
d := enc.NewTimeDeltaDecompressor()
d.RestoreFromEnd(page[pos:DataPagePayloadSize])
return d
return d, nil
}
func CreateValueDeltaDecompressor(page []byte, metricType qb.MetricType, fracDigits byte) qb.ValueDecompressor {
func CreateValueDecompressor(page []byte, metricType qb.MetricType, fracDigits byte) (qb.ValueDecompressor, error) {
size, _ := bin.GetUint16(page[valuesSizeIdx:])
if size > DataPageFooterSize {
return nil, fmt.Errorf("bug: invalid timestamps size %d", size)
}
d := enc.NewValueDeltaDecompressor(metricType, fracDigits)
d.RestoreFromEnd(page[:size])
return d
return d, nil
}
func VerifyDataPageCRC32(data []byte) error {
var (
calculatedCRC = util.CalculateCRC32(data[:dataCRC32Idx])
writtenCRC, _ = bin.GetUint32(data[dataCRC32Idx:])
)
if calculatedCRC != writtenCRC {
return fmt.Errorf("calculated CRC32 %d are not equal written CRC32 %d",
calculatedCRC, writtenCRC)
}
return nil
}
func VerifyIndexPageCRC32(data []byte) error {
var (
calculatedCRC = util.CalculateCRC32(data[:indexCRC32Idx])
writtenCRC, _ = bin.GetUint32(data[indexCRC32Idx:])
)
if calculatedCRC != writtenCRC {
return fmt.Errorf("calculated CRC32 %d are not equal written CRC32 %d",
calculatedCRC, writtenCRC)
}
return nil
}

214
storage/navigation.go Normal file
View File

@@ -0,0 +1,214 @@
package storage
import bin "gordenko.dev/dima/bin/little"
const (
PageNoSize = 4
)
type KeyComparator interface {
CompareTo(int) int
}
type ValueAtComparator struct {
buf []byte
timestamp uint32
}
func (s ValueAtComparator) CompareTo(elemIdx int) int {
var (
pos = elemIdx * IndexRecordSize
elem, _ = bin.GetUint32(s.buf[pos:])
)
if s.timestamp < elem {
return -1
} else if s.timestamp > elem {
return 1
} else {
return 0
}
}
func BinarySearch(qty int, keyComparator KeyComparator) (elemIdx int, isFound bool) {
if qty == 0 {
return
}
a := 0
b := qty - 1
for {
var (
elemIdx = (b-a)/2 + a
code = keyComparator.CompareTo(elemIdx)
)
if code == 1 {
a = elemIdx + 1
if a > b {
return elemIdx, false // +1
}
} else if code == -1 {
b = elemIdx - 1
if b < a {
if elemIdx == 0 {
return 0, false
} else {
return elemIdx - 1, false
}
}
} else {
return elemIdx, true
}
}
}
// func getPrevPageNo(buf []byte) uint32 {
// pageNo, _ := bin.GetUint32(buf[prevPageIdx:])
// return pageNo
// }
// func GetIndexRecordsSince(buf []byte) (pageNo uint32) {
// pageNo, _ = bin.GetUint32(buf)
// return
// }
func FindPageOnIndexLevelTail(level IndexLevelTail, timestamp uint32) (pageNo uint32) {
comparator := ValueAtComparator{
buf: level.Buffer,
timestamp: timestamp,
}
elemIdx, _ := BinarySearch(level.RecordsCount, comparator)
pos := elemIdx*IndexRecordSize + 4 // timestamp size
pageNo, _ = bin.GetUint32(level.Buffer[pos:])
return
}
// func FindPageOnRecords(records []byte, timestamp uint32) (pageNo uint32) {
// comparator := ValueAtComparator{
// buf: records,
// timestamp: timestamp,
// }
// count := len(records) / IndexRecordSize
// elemIdx, _ := BinarySearch(count, comparator)
// pageNo, _ = bin.GetUint32(records[elemIdx*IndexRecordSize:])
// return
// }
func FindPageOnIndexPage(page []byte, timestamp uint32) (pageNo uint32) {
comparator := ValueAtComparator{
buf: page,
timestamp: timestamp,
}
count, _ := bin.GetUint16(page[indexRecordsCountIdx:])
elemIdx, _ := BinarySearch(int(count), comparator)
pos := elemIdx*IndexRecordSize + 4 // timestamp size
pageNo, _ = bin.GetUint32(page[pos:])
return
}
func IsZeroLevelPage(buf []byte) bool {
return buf[isZeroLevelIdx] == 1
}
// можна перевірити хвости від 0 до ... Перевіряю since кожного хвоста.
// Якщо timestamp >=, шукаю pageNo бінарним пошуком. І сторінка мені однозначно підходить.
// Якщо timestamp <, піднімаюсь вище. Якщо рівнів більше немає - until вказано за межами Range показань.
func FindPageOnIndexLevelTails(levels []IndexLevelTail, timestamp uint32) (pageNo uint32, isDataPage bool) {
for i, level := range levels {
tailSince, _ := bin.GetUint32(level.Buffer)
if timestamp >= tailSince {
pageNo = FindPageOnIndexLevelTail(level, timestamp)
return pageNo, i == 0
}
}
return
}
// func findPageNoIdx(buf []byte, timestamp uint32) (idx int) {
// comparator := ValueAtComparator{
// buf: buf,
// timestamp: timestamp,
// }
// qty, _ := bin.GetUint16(buf[indexRecordsQtyIdx:])
// elemIdx, _ := BinarySearch(int(qty), comparator)
// return elemIdx
// }
// func findPageNoForDeleteSince(buf []byte, since uint32) (uint32, int, int) {
// comparator := ValueAtComparator{
// buf: buf,
// timestamp: since,
// }
// qty, _ := bin.GetUint16(buf[indexRecordsQtyIdx:])
// elemIdx, _ := BinarySearch(int(qty), comparator)
// pos := elemIdx * timestampSize
// timestamp, _ := bin.GetUint32(buf[pos:])
// if timestamp == since {
// if elemIdx == 0 {
// return 0, int(qty), -1
// }
// elemIdx--
// }
// pos = indexFooterIdx - (elemIdx+1)*PageNoSize
// pageNo, _ := bin.GetUint32(buf[pos:])
// return pageNo, int(qty), elemIdx
// }
// func getLastPageNo(buf []byte) (pageNo uint32) {
// qty, _ := bin.GetUint16(buf[indexRecordsQtyIdx:])
// pos := indexFooterIdx - qty*PageNoSize
// pageNo, _ = bin.GetUint32(buf[pos:])
// return
// }
// func getPageNo(buf []byte, idx int) (pageNo uint32) {
// pos := indexFooterIdx - (idx+1)*PageNoSize
// pageNo, _ = bin.GetUint32(buf[pos:])
// return
// }
// func listPageNumbers(buf []byte) (pageNumbers []uint32) {
// qty, _ := bin.GetUint16(buf[indexRecordsQtyIdx:])
// pos := indexFooterIdx - PageNoSize
// for range qty {
// pageNo, _ := bin.GetUint32(buf[pos:])
// pageNumbers = append(pageNumbers, pageNo)
// pos -= PageNoSize
// }
// return
// }
// // include since timestamp
// func listPageNumbersSince(buf []byte, timestamp uint32) (pageNumbers []uint32) {
// comparator := ValueAtComparator{
// buf: buf,
// timestamp: timestamp,
// }
// qty, _ := bin.GetUint16(buf[indexRecordsQtyIdx:])
// elemIdx, _ := BinarySearch(int(qty), comparator)
// pos := indexFooterIdx - (elemIdx+1)*PageNoSize
// for range qty {
// pageNo, _ := bin.GetUint32(buf[pos:])
// pageNumbers = append(pageNumbers, pageNo)
// pos -= PageNoSize
// }
// return
// }
// func getSince(buf []byte) uint32 {
// return bin.GetUint32(buf[0:])
// }
// func appendPair(buf []byte, timestamp uint32, pageNo uint32) bool {
// qty := bin.GetUint16AsInt(buf[indexRecordsQtyIdx:])
// free := indexFooterIdx - qty*pairSize
// if free < pairSize {
// return false
// }
// pos := qty * timestampSize
// bin.PutUint32(buf[pos:], timestamp)
// pos = indexFooterIdx - (qty+1)*PageNoSize
// bin.PutUint32(buf[pos:], pageNo)
// bin.PutIntAsUint16(buf[indexRecordsQtyIdx:], qty+1)
// return true
// }

View File

@@ -9,7 +9,7 @@ var (
// data page
DataPageSize = 8192
DataPagePayloadSize int = DataPageSize - DataPageFooterSize
DataPagePayloadSize = DataPageSize - DataPageFooterSize
dataCRC32Idx = DataPageSize - 4
timestampsSizeIdx = DataPageSize - 6
@@ -19,6 +19,7 @@ var (
// index page
IndexPageSize = 1024
IndexPagePayloadSize = IndexPageSize - IndexPageFooterSize
//indexPageIncSize = IndexPageIncSize
indexCRC32Idx = IndexPageSize - 4
indexRecordsCountIdx = IndexPageSize - 6
@@ -28,7 +29,6 @@ var (
// timestampSize = 4
// pairSize = timestampSize + PageNoSize
// indexFooterIdx = indexRecordsQtyIdx
// dataFooterIdx = timestampsSizeIdx
)

View File

@@ -1081,3 +1081,74 @@ func TestReplayMetric(t *testing.T) {
fmt.Printf("%d: % x\n", metric.IndexLevelTails[0].RecordsCount, metric.IndexLevelTails[0].Buffer)
fmt.Printf("%d: % x\n", metric.IndexLevelTails[1].RecordsCount, metric.IndexLevelTails[1].Buffer)
}
// func TestComposeHeadIndexPage(t *testing.T) {
// var (
// levelIdx = 0
// level = storage.IndexLevelTail{
// Buffer: []byte{
// 1, 2, 3,
// },
// RecordsCount: 2,
// }
// head = &storage.IndexPageTail{
// PageNo: 100,
// CRC32: 12345,
// Records: []byte{},
// }
// )
// page := composeHeadIndexPage(levelIdx, level, head)
// if page.PageNo != head.PageNo {
// t.Fatalf("PageNo: got %d are not equal expected %v",
// page.PageNo, head.PageNo)
// }
// // fix compare pages
// }
func TestFindPageOnIndexTails(t *testing.T) {
levels := []IndexLevelTail{
{
Buffer: []byte{
0x64, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, // 100 => 10
0x6e, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, // 110 => 11
},
RecordsCount: 2,
},
{
Buffer: []byte{
0x0a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // 10 => 1
0x28, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, // 40 => 4
0x46, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, // 70 => 7
},
RecordsCount: 3,
},
}
testCases := []struct {
Timestamp uint32
PageNo uint32
IsDataPage bool
}{
{Timestamp: 9, PageNo: 0, IsDataPage: false},
{Timestamp: 10, PageNo: 1, IsDataPage: false},
{Timestamp: 20, PageNo: 1, IsDataPage: false},
{Timestamp: 40, PageNo: 4, IsDataPage: false},
{Timestamp: 60, PageNo: 4, IsDataPage: false},
{Timestamp: 70, PageNo: 7, IsDataPage: false},
{Timestamp: 90, PageNo: 7, IsDataPage: false},
{Timestamp: 100, PageNo: 10, IsDataPage: true},
{Timestamp: 105, PageNo: 10, IsDataPage: true},
{Timestamp: 110, PageNo: 11, IsDataPage: true},
{Timestamp: 120, PageNo: 11, IsDataPage: true},
}
for _, testCase := range testCases {
pageNo, isDataPage := FindPageOnIndexLevelTails(levels, testCase.Timestamp)
if pageNo != testCase.PageNo {
t.Fatalf("timestamp %d: got pageNo %d are not equal expected %d",
testCase.Timestamp, pageNo, testCase.PageNo)
}
if isDataPage != testCase.IsDataPage {
t.Fatalf("timestamp %d: got isDataPage %t are not equal expected %t",
testCase.Timestamp, isDataPage, testCase.IsDataPage)
}
}
}

View File

@@ -365,9 +365,6 @@ type PageToWrite struct {
func WriteDataPages(file *os.File, pages []PageToWrite) (err error) {
for _, p := range pages {
fmt.Println("pageNo: %d\n", p.PageNo)
}
for _, p := range pages {
if len(p.Content) != DataPageSize {
return fmt.Errorf("wrong data page size: %d", len(p.Content))
}