2026-06-13 22:43:17 +00:00
|
|
|
|
package storage
|
2026-06-09 16:42:17 +00:00
|
|
|
|
|
|
|
|
|
|
import (
|
2026-06-10 06:18:45 +03:00
|
|
|
|
"fmt"
|
2026-06-09 16:42:17 +00:00
|
|
|
|
|
2026-06-10 06:18:45 +03:00
|
|
|
|
"gordenko.dev/dima/qb"
|
2026-06-09 16:42:17 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type ReplayMetric struct {
|
2026-06-14 07:57:01 +03:00
|
|
|
|
MetricType qb.MetricType
|
|
|
|
|
|
FracDigits byte
|
|
|
|
|
|
LastPageNo uint32
|
|
|
|
|
|
Buf []byte
|
|
|
|
|
|
TimestampsSize int
|
|
|
|
|
|
ValuesSize int
|
|
|
|
|
|
IndexLevelTails []IndexLevelTail // root - last element
|
2026-06-09 16:42:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 07:57:01 +03:00
|
|
|
|
// return occupied after write
|
|
|
|
|
|
func writeTimestampsWithRewind(buf []byte, occupied int, payload []byte, rewindOffset int) int {
|
|
|
|
|
|
// timestamps writes right-to-left
|
|
|
|
|
|
pos := DataPagePayloadSize - occupied
|
|
|
|
|
|
// timestamps rewind offset moves pos left-to-right
|
|
|
|
|
|
pos += rewindOffset
|
|
|
|
|
|
pos -= len(payload)
|
|
|
|
|
|
copy(buf[pos:], payload)
|
|
|
|
|
|
return DataPagePayloadSize - pos
|
2026-06-09 16:42:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 07:57:01 +03:00
|
|
|
|
func writeValuesWithRewind(buf []byte, occupied int, payload []byte, rewindOffset int) int {
|
|
|
|
|
|
// values writes left-to-right
|
|
|
|
|
|
pos := occupied
|
|
|
|
|
|
// values rewind offset moves pos right-to-left
|
|
|
|
|
|
pos -= rewindOffset
|
|
|
|
|
|
copy(buf[pos:], payload)
|
|
|
|
|
|
return pos + len(payload)
|
|
|
|
|
|
}
|
2026-06-10 06:18:45 +03:00
|
|
|
|
|
2026-06-13 22:43:17 +00:00
|
|
|
|
func (s *ReplayMetric) MeasuresAppend(rec MeasuresAppendRecord) {
|
2026-06-14 07:57:01 +03:00
|
|
|
|
s.TimestampsSize = writeTimestampsWithRewind(s.Buf, s.TimestampsSize,
|
|
|
|
|
|
rec.Timestamps, rec.TimestampsRewindOffset)
|
|
|
|
|
|
s.ValuesSize = writeValuesWithRewind(s.Buf, s.ValuesSize,
|
|
|
|
|
|
rec.Values, rec.ValuesRewindOffset)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 16:42:17 +00:00
|
|
|
|
type AppendMeasuresResult struct {
|
2026-06-14 07:57:01 +03:00
|
|
|
|
IndexPagesToRewrite []PageToWrite
|
|
|
|
|
|
DataPagesToRewrite []PageToWrite
|
2026-06-09 16:42:17 +00:00
|
|
|
|
ReusedIndexPagesCount int
|
|
|
|
|
|
ReusedDataPagesCount int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 22:43:17 +00:00
|
|
|
|
func (s *ReplayMetric) MeasuresAppendWithGrow(rec MeasuresAppendWithGrowRecord, isLastPacket bool) (_ AppendMeasuresResult) {
|
2026-06-10 06:18:45 +03:00
|
|
|
|
var (
|
|
|
|
|
|
reusedIndexPages int
|
|
|
|
|
|
reusedDataPages int
|
2026-06-13 22:43:17 +00:00
|
|
|
|
indexPages []PageToWrite
|
|
|
|
|
|
dataPages []PageToWrite
|
2026-06-10 06:18:45 +03:00
|
|
|
|
)
|
2026-06-09 16:42:17 +00:00
|
|
|
|
// Додати в index level tails недостаючі дані, або замінити
|
|
|
|
|
|
for levelIdx, change := range rec.ChangedIndexLevels {
|
2026-06-10 06:18:45 +03:00
|
|
|
|
var (
|
2026-06-13 08:01:42 +03:00
|
|
|
|
head = change.IndexPageTail
|
2026-06-13 22:43:17 +00:00
|
|
|
|
newRecordsCount = len(change.TailRecords) / IndexRecordSize
|
2026-06-10 06:18:45 +03:00
|
|
|
|
)
|
|
|
|
|
|
// 1. набиваю сторінки для перезапису в index файлі
|
|
|
|
|
|
if isLastPacket {
|
|
|
|
|
|
if head != nil {
|
2026-06-14 07:57:01 +03:00
|
|
|
|
indexPages = append(indexPages, s.composeHeadIndexPage(levelIdx, head))
|
2026-06-10 06:18:45 +03:00
|
|
|
|
}
|
|
|
|
|
|
for _, x := range change.IndexPages {
|
2026-06-13 22:43:17 +00:00
|
|
|
|
indexPages = append(indexPages, PageToWrite{
|
2026-06-10 06:18:45 +03:00
|
|
|
|
PageNo: x.PageNo,
|
|
|
|
|
|
Content: x.Content,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Вношу зміни в поточні індексні хвости
|
2026-06-14 07:57:01 +03:00
|
|
|
|
if levelIdx < len(s.IndexLevelTails) {
|
|
|
|
|
|
level := s.IndexLevelTails[levelIdx]
|
2026-06-10 06:18:45 +03:00
|
|
|
|
if head != nil {
|
|
|
|
|
|
// попередній хвіст індексного рівня перетворився на head сторінку,
|
2026-06-09 16:42:17 +00:00
|
|
|
|
// отже TailRecords - це новий хвіст
|
|
|
|
|
|
copy(level.Buffer, change.TailRecords)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
level.RecordsCount = newRecordsCount
|
2026-06-09 16:42:17 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// TailRecords - це нові дані, які треба додати
|
2026-06-13 22:43:17 +00:00
|
|
|
|
pos := level.RecordsCount * IndexRecordSize
|
2026-06-09 16:42:17 +00:00
|
|
|
|
copy(level.Buffer[pos:], change.TailRecords)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
level.RecordsCount += newRecordsCount
|
2026-06-09 16:42:17 +00:00
|
|
|
|
}
|
2026-06-14 07:57:01 +03:00
|
|
|
|
s.IndexLevelTails[levelIdx] = level
|
2026-06-09 16:42:17 +00:00
|
|
|
|
} else {
|
2026-06-10 06:18:45 +03:00
|
|
|
|
// додаю новий індексний рівень
|
2026-06-13 22:43:17 +00:00
|
|
|
|
buf := make([]byte, IndexPageSize)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
copy(buf, change.TailRecords)
|
|
|
|
|
|
//
|
2026-06-14 07:57:01 +03:00
|
|
|
|
s.IndexLevelTails = append(s.IndexLevelTails, IndexLevelTail{
|
2026-06-10 06:18:45 +03:00
|
|
|
|
Buffer: buf,
|
|
|
|
|
|
RecordsCount: newRecordsCount,
|
2026-06-09 16:42:17 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-06-10 06:18:45 +03:00
|
|
|
|
|
|
|
|
|
|
// 3. рахую кількість reused індексних сторінок
|
|
|
|
|
|
if head != nil {
|
|
|
|
|
|
if head.Reused {
|
|
|
|
|
|
reusedIndexPages++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, x := range change.IndexPages {
|
|
|
|
|
|
if x.Reused {
|
|
|
|
|
|
reusedIndexPages++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-09 16:42:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 06:18:45 +03:00
|
|
|
|
// 4. набиваю сторінки для перезапису в data файлі
|
|
|
|
|
|
if isLastPacket {
|
2026-06-14 07:57:01 +03:00
|
|
|
|
dataPages = append(dataPages, s.composeHeadDataPage(rec.DataPageTail))
|
2026-06-09 16:42:17 +00:00
|
|
|
|
|
2026-06-10 06:18:45 +03:00
|
|
|
|
for _, x := range rec.DataPages {
|
2026-06-13 22:43:17 +00:00
|
|
|
|
dataPages = append(dataPages, PageToWrite{
|
2026-06-10 06:18:45 +03:00
|
|
|
|
PageNo: x.PageNo,
|
|
|
|
|
|
Content: x.Content,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-09 16:42:17 +00:00
|
|
|
|
|
2026-06-10 06:18:45 +03:00
|
|
|
|
// рахую кількість reused дата сторінок
|
2026-06-13 08:01:42 +03:00
|
|
|
|
if rec.DataPageTail.Reused {
|
2026-06-10 06:18:45 +03:00
|
|
|
|
reusedDataPages++
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, x := range rec.DataPages {
|
|
|
|
|
|
if x.Reused {
|
|
|
|
|
|
reusedDataPages++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// HeadDataPage є полюбому, оскільки це транзакція із мінімум однією заповненою
|
|
|
|
|
|
// data сторінкою. Отже TailTimestamps і TailValues - це нові хвости data рівня.
|
2026-06-14 07:57:01 +03:00
|
|
|
|
copy(s.Buf, rec.TailValues)
|
|
|
|
|
|
s.ValuesSize = len(rec.TailValues)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
|
2026-06-14 07:57:01 +03:00
|
|
|
|
pos := DataPagePayloadSize - len(rec.TailTimestamps)
|
|
|
|
|
|
copy(s.Buf[pos:], rec.TailTimestamps)
|
|
|
|
|
|
s.TimestampsSize = len(rec.TailTimestamps)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
|
|
|
|
|
|
if len(rec.DataPages) == 0 {
|
2026-06-14 07:57:01 +03:00
|
|
|
|
s.LastPageNo = rec.DataPageTail.PageNo
|
2026-06-09 16:42:17 +00:00
|
|
|
|
} else {
|
2026-06-14 07:57:01 +03:00
|
|
|
|
s.LastPageNo = rec.DataPages[len(rec.DataPages)-1].PageNo
|
2026-06-10 06:18:45 +03:00
|
|
|
|
}
|
2026-06-09 16:42:17 +00:00
|
|
|
|
|
2026-06-10 06:18:45 +03:00
|
|
|
|
return AppendMeasuresResult{
|
2026-06-14 07:57:01 +03:00
|
|
|
|
IndexPagesToRewrite: indexPages,
|
|
|
|
|
|
DataPagesToRewrite: dataPages,
|
2026-06-10 06:18:45 +03:00
|
|
|
|
ReusedIndexPagesCount: reusedIndexPages,
|
|
|
|
|
|
ReusedDataPagesCount: reusedDataPages,
|
2026-06-09 16:42:17 +00:00
|
|
|
|
}
|
2026-06-10 06:18:45 +03:00
|
|
|
|
}
|
2026-06-09 16:42:17 +00:00
|
|
|
|
|
2026-06-10 06:18:45 +03:00
|
|
|
|
// HELPERS
|
|
|
|
|
|
|
2026-06-14 07:57:01 +03:00
|
|
|
|
func (s *ReplayMetric) composeHeadIndexPage(levelIdx int, head *IndexPageTail) PageToWrite {
|
|
|
|
|
|
level := s.IndexLevelTails[levelIdx]
|
2026-06-10 06:18:45 +03:00
|
|
|
|
// розраховую pos, з якого буду дописувати хвіст
|
2026-06-13 22:43:17 +00:00
|
|
|
|
pos := level.RecordsCount * IndexRecordSize
|
2026-06-10 06:18:45 +03:00
|
|
|
|
// створюю копію сторінки
|
2026-06-13 22:43:17 +00:00
|
|
|
|
page := make([]byte, IndexPageSize)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
copy(page, level.Buffer[:pos]) // поточні дані
|
|
|
|
|
|
copy(page[pos:], head.Records)
|
|
|
|
|
|
// запечатати сторінку
|
2026-06-13 22:43:17 +00:00
|
|
|
|
calculatedCRC := SealIndexPage(SealIndexPageIn{
|
2026-06-10 06:18:45 +03:00
|
|
|
|
Content: page,
|
2026-06-13 22:43:17 +00:00
|
|
|
|
RecordsCount: level.RecordsCount + len(head.Records)/IndexRecordSize,
|
2026-06-10 06:18:45 +03:00
|
|
|
|
ZeroLevel: levelIdx == 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
// перевірка CRC
|
2026-06-13 08:01:42 +03:00
|
|
|
|
if calculatedCRC != head.CRC32 {
|
2026-06-10 06:18:45 +03:00
|
|
|
|
qb.Abort(qb.WALReplayFailed,
|
|
|
|
|
|
fmt.Errorf("calculated CRC %d not equal expected %d of head page %d on index level %d",
|
2026-06-13 08:01:42 +03:00
|
|
|
|
calculatedCRC, head.CRC32, head.PageNo, levelIdx))
|
2026-06-10 06:18:45 +03:00
|
|
|
|
}
|
2026-06-13 22:43:17 +00:00
|
|
|
|
return PageToWrite{
|
2026-06-10 06:18:45 +03:00
|
|
|
|
PageNo: head.PageNo,
|
|
|
|
|
|
Content: page,
|
|
|
|
|
|
}
|
2026-06-09 16:42:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 07:57:01 +03:00
|
|
|
|
func (s *ReplayMetric) composeHeadDataPage(head DataPageTail) PageToWrite {
|
|
|
|
|
|
// copy page
|
|
|
|
|
|
page := make([]byte, DataPageSize)
|
|
|
|
|
|
copy(page, s.Buf)
|
|
|
|
|
|
// append data
|
|
|
|
|
|
timestampsSize := writeTimestampsWithRewind(s.Buf, s.TimestampsSize,
|
|
|
|
|
|
head.Timestamps, head.TimestampsRewindOffset)
|
|
|
|
|
|
valuesSize := writeValuesWithRewind(s.Buf, s.ValuesSize,
|
|
|
|
|
|
head.Values, head.ValuesRewindOffset)
|
2026-06-10 06:18:45 +03:00
|
|
|
|
// запечатати сторінку
|
2026-06-13 22:43:17 +00:00
|
|
|
|
calculatedCRC := SealDataPage(SealDataPageIn{
|
2026-06-10 06:18:45 +03:00
|
|
|
|
Content: page,
|
|
|
|
|
|
PrevPageNo: head.PrevPageNo,
|
|
|
|
|
|
TimestampsSize: timestampsSize,
|
2026-06-14 07:57:01 +03:00
|
|
|
|
ValuesSize: valuesSize,
|
2026-06-10 06:18:45 +03:00
|
|
|
|
})
|
|
|
|
|
|
// перевірка CRC
|
2026-06-13 08:01:42 +03:00
|
|
|
|
if calculatedCRC != head.CRC32 {
|
2026-06-10 06:18:45 +03:00
|
|
|
|
qb.Abort(qb.WALReplayFailed,
|
|
|
|
|
|
fmt.Errorf("calculated CRC %d not equal expected %d of head data page %d",
|
2026-06-13 08:01:42 +03:00
|
|
|
|
calculatedCRC, head.CRC32, head.PageNo))
|
2026-06-09 16:42:17 +00:00
|
|
|
|
}
|
2026-06-13 22:43:17 +00:00
|
|
|
|
return PageToWrite{
|
2026-06-10 06:18:45 +03:00
|
|
|
|
PageNo: head.PageNo,
|
|
|
|
|
|
Content: page,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|