2026-06-13 22:43:17 +00:00
|
|
|
package storage
|
2026-06-09 08:16:16 +03:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
|
|
|
|
bin "gordenko.dev/dima/bin/little"
|
|
|
|
|
"gordenko.dev/dima/qb/util"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
Формат:
|
|
|
|
|
metricsQty - varsize
|
|
|
|
|
[metric]*
|
|
|
|
|
де metric - це:
|
|
|
|
|
|
|
|
|
|
index free list frozen pages - varsize
|
|
|
|
|
indexFreeList size - varsize
|
|
|
|
|
indexFreeList - Nb
|
|
|
|
|
|
|
|
|
|
data free list frozen pages - varsize
|
|
|
|
|
dataFreeList size - varsize
|
|
|
|
|
dataFreeList - Nb
|
|
|
|
|
|
|
|
|
|
CRC32 - 4b
|
|
|
|
|
*/
|
|
|
|
|
|
2026-06-13 22:43:17 +00:00
|
|
|
const ReadBufferSize = 8 * 1024 * 1024 // 8mb
|
2026-06-09 08:16:16 +03:00
|
|
|
|
2026-06-13 22:43:17 +00:00
|
|
|
type WritableMetric interface {
|
|
|
|
|
WriteTo(io.Writer) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type WriteSnapshotIn struct {
|
2026-06-09 08:16:16 +03:00
|
|
|
SnapshotNumber int
|
|
|
|
|
Dir string
|
|
|
|
|
WriteBufferSize int
|
2026-06-13 22:43:17 +00:00
|
|
|
Metrics map[uint32]WritableMetric
|
2026-06-09 08:16:16 +03:00
|
|
|
FrozenIndexPagesCount int
|
|
|
|
|
IndexPageNumbers []uint32
|
|
|
|
|
FrozenDataPagesCount int
|
|
|
|
|
DataPageNumbers []uint32
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 22:43:17 +00:00
|
|
|
func WriteSnapshot(in WriteSnapshotIn) (err error) {
|
2026-06-09 08:16:16 +03:00
|
|
|
var (
|
|
|
|
|
fileName = filepath.Join(in.Dir, fmt.Sprintf("%d.snapshot", in.SnapshotNumber))
|
|
|
|
|
hasher = util.NewHasher()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY, 0666) //0770)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dst := io.MultiWriter(bufio.NewWriterSize(file, in.WriteBufferSize), hasher)
|
|
|
|
|
|
|
|
|
|
_, err = bin.WriteVarSize(dst, len(in.Metrics))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for metricID, metric := range in.Metrics {
|
|
|
|
|
err = bin.WriteUint32(dst, metricID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
err = metric.WriteTo(dst)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// free index pages
|
2026-06-09 16:42:17 +00:00
|
|
|
err = writeFreePages(dst, in.FrozenIndexPagesCount, in.IndexPageNumbers)
|
2026-06-09 08:16:16 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// free data pages
|
2026-06-09 16:42:17 +00:00
|
|
|
err = writeFreePages(dst, in.FrozenDataPagesCount, in.DataPageNumbers)
|
2026-06-09 08:16:16 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// CRC32
|
|
|
|
|
bin.WriteUint32(file, hasher.Sum32())
|
|
|
|
|
|
|
|
|
|
err = file.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// prevLogNumber := logNumber - 1
|
|
|
|
|
// prevChanges := filepath.Join(s.dir, fmt.Sprintf("%d.changes", prevLogNumber))
|
|
|
|
|
// prevSnapshot := filepath.Join(s.dir, fmt.Sprintf("%d.snapshot", prevLogNumber))
|
|
|
|
|
|
|
|
|
|
// isExist, err := isFileExist(prevChanges)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// return
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// if isExist {
|
|
|
|
|
// err = os.Remove(prevChanges)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// qb.Abort(qb.DeletePrevChangesFileFailed, err)
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// isExist, err = isFileExist(prevSnapshot)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// return
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// if isExist {
|
|
|
|
|
// err = os.Remove(prevSnapshot)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// qb.Abort(qb.DeletePrevSnapshotFileFailed, err)
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 22:43:17 +00:00
|
|
|
type ReadSnapshotOut struct {
|
2026-06-09 08:16:16 +03:00
|
|
|
Metrics map[uint32]*ReplayMetric
|
|
|
|
|
FrozenIndexPagesCount int
|
|
|
|
|
IndexPageNumbers []uint32
|
|
|
|
|
FrozenDataPagesCount int
|
|
|
|
|
DataPageNumbers []uint32
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 22:43:17 +00:00
|
|
|
func ReadSnapshot(fileName string) (out ReadSnapshotOut, err error) {
|
2026-06-09 08:16:16 +03:00
|
|
|
var (
|
|
|
|
|
metrics = make(map[uint32]*ReplayMetric)
|
|
|
|
|
frozenIndexPagesCount int
|
|
|
|
|
indexPageNumbers []uint32
|
|
|
|
|
frozenDataPagesCount int
|
|
|
|
|
dataPageNumbers []uint32
|
|
|
|
|
hasher = util.NewHasher()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
file, err := os.Open(fileName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
stat, err := file.Stat()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
fileSize := stat.Size()
|
|
|
|
|
|
|
|
|
|
if fileSize == 0 {
|
2026-06-13 22:43:17 +00:00
|
|
|
return ReadSnapshotOut{
|
2026-06-09 08:16:16 +03:00
|
|
|
Metrics: metrics,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if fileSize < 4 {
|
|
|
|
|
err = fmt.Errorf("%s is corrupted", fileName)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
payloadSize := fileSize - 4
|
|
|
|
|
|
|
|
|
|
// 2. Створюємо великий буфер для NVMe (4 MB)
|
|
|
|
|
bufferedReader := bufio.NewReaderSize(file, readBufferSize)
|
|
|
|
|
|
|
|
|
|
// 3. Обмежуємо читання лише розміром Payload
|
|
|
|
|
limitReader := io.LimitReader(bufferedReader, payloadSize)
|
|
|
|
|
|
|
|
|
|
// 4. Створюємо хеш та обгортку TeeReader, яка рахує CRC32 на льоту
|
|
|
|
|
src := io.TeeReader(limitReader, hasher)
|
|
|
|
|
|
|
|
|
|
// читаю payload
|
|
|
|
|
metricsQty, err := bin.ReadVarSize(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for range metricsQty {
|
|
|
|
|
var (
|
|
|
|
|
metricID uint32
|
2026-06-13 22:43:17 +00:00
|
|
|
buf = make([]byte, DataPageSize)
|
2026-06-09 16:42:17 +00:00
|
|
|
// для простоти створюю буфер максимального розміру
|
|
|
|
|
metric = &ReplayMetric{
|
|
|
|
|
buf: buf,
|
2026-06-13 22:43:17 +00:00
|
|
|
databuf: buf[:DataPagePayloadSize],
|
2026-06-09 08:16:16 +03:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
metricID, err = bin.ReadUint32(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
err = metric.ReadFrom(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
metrics[metricID] = metric
|
|
|
|
|
}
|
|
|
|
|
// index pages
|
2026-06-09 16:42:17 +00:00
|
|
|
frozenIndexPagesCount, indexPageNumbers, err = readFreePages(src)
|
2026-06-09 08:16:16 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// data pages
|
2026-06-09 16:42:17 +00:00
|
|
|
frozenDataPagesCount, dataPageNumbers, err = readFreePages(src)
|
2026-06-09 08:16:16 +03:00
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// verify CRC32
|
|
|
|
|
expectedCRC, err := bin.ReadUint32(bufferedReader)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
calculatedCRC := hasher.Sum32()
|
|
|
|
|
|
|
|
|
|
if expectedCRC != calculatedCRC {
|
|
|
|
|
err = fmt.Errorf("%s is corrupted. Calculated CRC %d not equal expected CRC %d",
|
|
|
|
|
fileName, calculatedCRC, expectedCRC)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 22:43:17 +00:00
|
|
|
return ReadSnapshotOut{
|
2026-06-09 08:16:16 +03:00
|
|
|
Metrics: metrics,
|
|
|
|
|
FrozenIndexPagesCount: frozenIndexPagesCount,
|
|
|
|
|
IndexPageNumbers: indexPageNumbers,
|
|
|
|
|
FrozenDataPagesCount: frozenDataPagesCount,
|
|
|
|
|
DataPageNumbers: dataPageNumbers,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
2026-06-09 16:42:17 +00:00
|
|
|
|
|
|
|
|
// HELPERS
|
|
|
|
|
|
|
|
|
|
func writeFreePages(dst io.Writer, frozenPagesCount int, pageNumbers []uint32) (err error) {
|
|
|
|
|
_, err = bin.WriteVarSize(dst, frozenPagesCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, err = bin.WriteVarSize(dst, len(pageNumbers))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, pageNo := range pageNumbers {
|
|
|
|
|
err = bin.WriteUint32(dst, pageNo)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func readFreePages(src io.Reader) (_ int, _ []uint32, err error) {
|
|
|
|
|
var (
|
|
|
|
|
frozenPagesCount int
|
|
|
|
|
pageNumbers []uint32
|
|
|
|
|
)
|
|
|
|
|
frozenPagesCount, err = bin.ReadVarSize(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
pageNumbersCount, err := bin.ReadVarSize(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for range pageNumbersCount {
|
|
|
|
|
var pageNo uint32
|
|
|
|
|
pageNo, err = bin.ReadUint32(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
pageNumbers = append(pageNumbers, pageNo)
|
|
|
|
|
}
|
|
|
|
|
return frozenPagesCount, pageNumbers, nil
|
|
|
|
|
}
|