Files
qb/txlog/writer.go

505 lines
12 KiB
Go
Raw Normal View History

2026-02-10 14:02:11 +00:00
package txlog
import (
"bytes"
"errors"
"fmt"
"hash/crc32"
2026-05-21 23:38:52 +03:00
"log"
2026-02-10 14:02:11 +00:00
"os"
"path/filepath"
"sync"
2026-05-10 00:59:47 +00:00
bin "gordenko.dev/dima/bin/little"
2026-05-21 23:38:52 +03:00
"gordenko.dev/dima/qb"
2026-05-10 00:59:47 +00:00
"gordenko.dev/dima/qb/atree"
2026-02-10 14:02:11 +00:00
)
const (
lsnSize = 4
packetPrefixSize = 12 // 4 lsn + 4 packet length + 4 crc32
lengthIdx = 0
checksumIdx = 4
lsnIdx = 8
filePerm = 0770
dumpSnapshotAfterNBytes = 1024 * 1024 * 1024 // 1 GB
)
const (
2026-05-10 00:59:47 +00:00
CodeAddedMetric byte = 1
CodeDeletedMetric byte = 2
CodeAppendedMeasure byte = 4
CodeAppendedMeasures byte = 5
CodeAppendedPages byte = 6
CodeDeletedMeasures byte = 7
2026-02-10 14:02:11 +00:00
)
2026-05-10 00:59:47 +00:00
type FreeList interface {
// використовується в allocPage
2026-05-21 23:38:52 +03:00
GetPageNumber() (uint32, error)
2026-05-10 00:59:47 +00:00
}
2026-02-10 14:02:11 +00:00
func JoinChangesFileName(dir string, logNumber int) string {
return filepath.Join(dir, fmt.Sprintf("%d.changes", logNumber))
}
type Changes struct {
Records []any
LogNumber int
ForceSnapshot bool
ExitWaitGroup *sync.WaitGroup
WaitCh chan struct{}
}
type Writer struct {
2026-05-10 00:59:47 +00:00
mutex sync.Mutex
freelist FreeList
atree *atree.Atree
logNumber int
dir string
2026-05-21 23:38:52 +03:00
w *bytes.Buffer // wal буфер для упаковки даних перед записом на диск
wal *os.File
2026-05-10 00:59:47 +00:00
dataFile *os.File
2026-05-21 23:38:52 +03:00
indexFile *os.File
dataPage []byte
indexPage []byte
2026-05-10 00:59:47 +00:00
input []any
//buf *bytes.Buffer
2026-05-21 23:38:52 +03:00
//pagesToWrite []atree.PageToWrite
workerReqs []any
//waitCh chan struct{}
2026-02-10 14:02:11 +00:00
appendToWorkerQueue func(any)
2026-05-21 23:38:52 +03:00
//lsn uint32
written int64
isExited bool
exitCh chan struct{}
waitGroup *sync.WaitGroup
signalCh chan struct{}
2026-02-10 14:02:11 +00:00
}
type WriterOptions struct {
Dir string
LogNumber int // номер журнала
AppendToWorkerQueue func(any)
2026-05-10 00:59:47 +00:00
FreeList FreeList
Atree *atree.Atree
2026-02-10 14:02:11 +00:00
ExitCh chan struct{}
WaitGroup *sync.WaitGroup
}
func NewWriter(opt WriterOptions) (*Writer, error) {
if opt.Dir == "" {
return nil, errors.New("Dir option is required")
}
if opt.AppendToWorkerQueue == nil {
return nil, errors.New("AppendToWorkerQueue option is required")
}
2026-05-10 00:59:47 +00:00
if opt.FreeList == nil {
return nil, errors.New("FreeList option is required")
}
if opt.Atree == nil {
return nil, errors.New("Atree option is required")
}
2026-02-10 14:02:11 +00:00
if opt.ExitCh == nil {
return nil, errors.New("ExitCh option is required")
}
if opt.WaitGroup == nil {
return nil, errors.New("WaitGroup option is required")
}
s := &Writer{
dir: opt.Dir,
appendToWorkerQueue: opt.AppendToWorkerQueue,
2026-05-10 00:59:47 +00:00
freelist: opt.FreeList,
atree: opt.Atree,
2026-02-10 14:02:11 +00:00
logNumber: opt.LogNumber,
exitCh: opt.ExitCh,
waitGroup: opt.WaitGroup,
signalCh: make(chan struct{}, 1),
}
var err error
if opt.LogNumber > 0 {
2026-05-21 23:38:52 +03:00
s.wal, err = os.OpenFile(
2026-02-10 14:02:11 +00:00
JoinChangesFileName(opt.Dir, s.logNumber),
os.O_APPEND|os.O_WRONLY,
filePerm,
)
if err != nil {
return nil, err
}
} else {
s.logNumber = 1
2026-05-21 23:38:52 +03:00
s.wal, err = os.OpenFile(
2026-02-10 14:02:11 +00:00
JoinChangesFileName(opt.Dir, s.logNumber),
os.O_CREATE|os.O_WRONLY,
filePerm,
)
if err != nil {
return nil, err
}
}
return s, nil
}
func (s *Writer) Run() {
for {
select {
case <-s.signalCh:
2026-05-10 00:59:47 +00:00
if err := s.packAndWrite(); err != nil {
2026-05-21 23:38:52 +03:00
qb.Abort(qb.FailedWriteToTxLog, err)
2026-02-10 14:02:11 +00:00
}
case <-s.exitCh:
s.exit()
return
}
}
}
2026-05-21 23:38:52 +03:00
func (s *Writer) packAndWrite() (err error) {
2026-05-10 00:59:47 +00:00
s.mutex.Lock()
input := s.input
s.input = nil
s.mutex.Unlock()
w := bytes.NewBuffer(nil)
2026-05-21 23:38:52 +03:00
// 1. Пакую всі дані в WAL буфер запису
2026-05-10 00:59:47 +00:00
for _, untyped := range input {
switch x := untyped.(type) {
case AddedMetric:
2026-05-21 23:38:52 +03:00
x.Pack(w)
// case DeletedMetric:
// if len(x.FreePageNumbers) > 0 {
// s.freeList.AddPageNumbers(x.FreePageNumbers)
// }
2026-05-10 00:59:47 +00:00
case AppendedMeasures:
2026-05-21 23:38:52 +03:00
//
s.packMeasuresIntoWALBuffer(x)
//case DeletedMeasures:
2026-05-10 00:59:47 +00:00
//case DeletedMeasuresSince:
}
}
2026-05-21 23:38:52 +03:00
// 2. Додаю розмір пакету і чексуму
// 3. Пишу на диск WAL (append)
// 4. Пишу в atree сторінки
for range 1 {
err = s.writePagesToAtree(nil, nil)
if err != nil {
return
}
}
// 5. Пишу зміни в free list
// 6. відправляю input - worker-у
2026-05-10 00:59:47 +00:00
// flush
2026-05-21 23:38:52 +03:00
err = s.flush(w)
if err != nil {
return err
}
// FIX send to worker workerReqs
return nil
2026-02-10 14:02:11 +00:00
}
2026-05-21 23:38:52 +03:00
func (s *Writer) Append(req any) {
2026-02-10 14:02:11 +00:00
s.mutex.Lock()
2026-05-10 00:59:47 +00:00
s.input = append(s.input, req)
s.workerReqs = append(s.workerReqs, req)
s.mutex.Unlock()
select {
case s.signalCh <- struct{}{}:
default:
}
}
// func (s *Writer) reset() {
// s.buf.Reset()
// s.buf.Write([]byte{
// 0, 0, 0, 0, // packet length
// 0, 0, 0, 0, // crc32
// 0, 0, 0, 0, // lsn
// })
// s.pagesToWrite = nil
// s.workerReqs = nil
// s.waitCh = make(chan struct{})
// }
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
func (s *Writer) flush(w *bytes.Buffer) error {
s.mutex.Lock()
2026-05-21 23:38:52 +03:00
//pagesToWrite := s.pagesToWrite
2026-02-10 14:02:11 +00:00
workerReqs := s.workerReqs
isExited := s.isExited
var exitWaitGroup *sync.WaitGroup
if s.isExited {
exitWaitGroup = s.waitGroup
}
2026-05-10 00:59:47 +00:00
if w.Len() > packetPrefixSize {
2026-05-21 23:38:52 +03:00
//s.lsn++
//lsn := s.lsn
2026-05-10 00:59:47 +00:00
packet := make([]byte, w.Len())
copy(packet, w.Bytes())
//s.reset() fix
2026-02-10 14:02:11 +00:00
s.written += int64(len(packet)) + 12
s.mutex.Unlock()
bin.PutUint32(packet[lengthIdx:], uint32(len(packet)-packetPrefixSize))
2026-05-21 23:38:52 +03:00
//bin.PutUint32(packet[lsnIdx:], lsn)
2026-02-10 14:02:11 +00:00
bin.PutUint32(packet[checksumIdx:], crc32.ChecksumIEEE(packet[8:]))
2026-05-21 23:38:52 +03:00
n, err := s.wal.Write(packet)
2026-02-10 14:02:11 +00:00
if err != nil {
return fmt.Errorf("TxLog write: %s", err)
}
if n != len(packet) {
return fmt.Errorf("TxLog written %d != packet size %d", n, len(packet))
}
2026-05-21 23:38:52 +03:00
if err := s.wal.Sync(); err != nil {
2026-02-10 14:02:11 +00:00
return fmt.Errorf("TxLog sync: %s", err)
}
2026-05-10 00:59:47 +00:00
2026-05-21 23:38:52 +03:00
// err = s.writePagesToAtree(pagesToWrite)
// if err != nil {
// return fmt.Errorf("TxLog writePagesToAtree: %s", err)
// }
2026-02-10 14:02:11 +00:00
} else {
s.mutex.Unlock()
}
var forceSnapshot bool
if s.written > dumpSnapshotAfterNBytes {
forceSnapshot = true
}
if isExited && s.written > 0 {
forceSnapshot = true
}
if forceSnapshot {
2026-05-21 23:38:52 +03:00
if err := s.wal.Close(); err != nil {
2026-02-10 14:02:11 +00:00
return fmt.Errorf("close changes file: %s", err)
}
s.logNumber++
var err error
2026-05-21 23:38:52 +03:00
s.wal, err = os.OpenFile(
2026-02-10 14:02:11 +00:00
JoinChangesFileName(s.dir, s.logNumber),
os.O_CREATE|os.O_WRONLY,
filePerm,
)
if err != nil {
return fmt.Errorf("create new changes file: %s", err)
}
s.written = 0
}
s.appendToWorkerQueue(Changes{
Records: workerReqs,
ForceSnapshot: forceSnapshot,
LogNumber: s.logNumber,
ExitWaitGroup: exitWaitGroup,
})
return nil
}
2026-05-21 23:38:52 +03:00
func (s *Writer) exit() {
s.mutex.Lock()
s.isExited = true
s.mutex.Unlock()
if err := s.packAndWrite(); err != nil {
qb.Abort(qb.FailedWriteToTxLog, err)
}
}
2026-02-10 14:02:11 +00:00
2026-05-21 23:38:52 +03:00
// writePagesToAtree - записує сторінки в .data та .index файли
func (s *Writer) writePagesToAtree(levels []*atree.IndexLevel, dataPages []*atree.DataPage) (err error) {
for _, p := range dataPages {
p.Checksum = atree.ChunksToDataPage(s.dataPage, atree.ChunksToDataPageReq{
PrevPageNo: p.PrevPageNo,
Timestamps: p.Timestamps,
TimestampsSize: p.TimestampsSize,
Values: p.Values,
ValuesSize: p.ValuesSize,
})
off := (p.PageNo - 1) * atree.DataPageSize
n, err := s.dataFile.WriteAt(s.dataPage, int64(off))
2026-05-10 00:59:47 +00:00
if err != nil {
return err
}
2026-05-21 23:38:52 +03:00
if n != atree.DataPageSize {
return fmt.Errorf("write %d instead of %d", n, atree.DataPageSize)
}
}
for levelIdx, level := range levels {
for _, p := range level.Filled {
// if len(p.Data) != atree.PageSize {
// return fmt.Errorf("wrong page %d size: %d",
// p.PageNo, len(p.Data))
// }
p.Checksum = atree.DataToIndexPage(s.indexPage, p.Data, levelIdx == 0)
off := (p.PageNo - 1) * atree.IndexPageSize
n, err := s.indexFile.WriteAt(s.indexPage, int64(off))
if err != nil {
return err
}
if n != atree.IndexPageSize {
return fmt.Errorf("write %d instead of %d", n, atree.IndexPageSize)
}
2026-05-10 00:59:47 +00:00
}
2026-02-10 14:02:11 +00:00
}
2026-05-10 00:59:47 +00:00
return nil
2026-02-10 14:02:11 +00:00
}
2026-05-21 23:38:52 +03:00
type AppendedMeasures struct {
MetricID uint32
TimestampsOffset int // (заповнені одразу)
ValuesOffset int // (заповнені одразу)
Timestamps [][]byte
TimestampsSize int
Values [][]byte
ValuesSize int
IndexLevels []*atree.IndexLevel
DataPages []*atree.DataPage // fix - prevPageNo for the 1st data page
}
2026-02-10 14:02:11 +00:00
2026-05-21 23:38:52 +03:00
func (s *Writer) packMeasuresIntoWALBuffer(req AppendedMeasures) (err error) {
// fix write code
bin.WriteUint32(s.w, req.MetricID)
bin.WriteVarSize(s.w, len(req.DataPages))
for idx, p := range req.DataPages {
bin.WriteUint32(s.w, p.PageNo)
var reused byte
if p.IsReused {
reused = 1
}
s.w.WriteByte(reused)
bin.WriteUint32(s.w, p.PrevPageNo)
bin.WriteUint32(s.w, p.Checksum)
var (
timestampsOffset int
valuesOffset int
)
if idx == 0 {
timestampsOffset = req.TimestampsOffset
valuesOffset = req.ValuesOffset
}
copyPayloadFromChunks(s.w, p.Timestamps, p.TimestampsSize, timestampsOffset)
copyPayloadFromChunks(s.w, p.Values, p.ValuesSize, valuesOffset)
}
bin.WriteUint16(s.w, uint16(req.TimestampsOffset))
bin.WriteUint16(s.w, uint16(req.ValuesOffset))
var (
timestampsOffset int
valuesOffset int
)
if len(req.DataPages) == 0 {
timestampsOffset = req.TimestampsOffset
valuesOffset = req.ValuesOffset
2026-02-10 14:02:11 +00:00
}
2026-05-21 23:38:52 +03:00
copyPayloadFromChunks(s.w, req.Timestamps, req.TimestampsSize, timestampsOffset)
copyPayloadFromChunks(s.w, req.Values, req.ValuesSize, valuesOffset)
if len(req.DataPages) == 0 {
return
}
// fix - levels reduced
bin.WriteVarSize(s.w, len(req.IndexLevels))
for _, level := range req.IndexLevels {
bin.WriteVarSize(s.w, len(level.Filled))
for idx, p := range level.Filled {
bin.WriteUint32(s.w, p.PageNo)
var reused byte
if p.IsReused {
reused = 1
}
s.w.WriteByte(reused)
bin.WriteUint32(s.w, p.Checksum)
records := p.Data
if idx == 0 {
records = p.Data[level.Offset:]
}
if (len(records) % recordSize) != 0 {
log.Fatalf("wrong records length: %d", len(records))
}
bin.WriteVarSize(s.w, len(records)/recordSize)
s.w.Write(records)
}
records := level.Data
if len(level.Filled) == 0 {
records = level.Data[level.Offset:]
}
if (len(records) % recordSize) != 0 {
log.Fatalf("wrong unfilled records length: %d", len(records))
}
bin.WriteVarSize(s.w, len(records)/recordSize)
s.w.Write(records)
}
return
2026-02-10 14:02:11 +00:00
}
2026-05-10 00:59:47 +00:00
// API
2026-02-10 14:02:11 +00:00
2026-05-10 00:59:47 +00:00
// FIX - add
// type DeletedMeasuresSince struct {
// MetricID uint32
// LastPageNo uint32
// IsRootChanged bool
// RootPageNo uint32
// FreeDataPages []uint32
// FreeIndexPages []uint32
// TimestampsBuf []byte
// ValuesBuf []byte
// }
2026-02-10 14:02:11 +00:00
func (s *Writer) sendSignal() {
select {
case s.signalCh <- struct{}{}:
default:
}
}
2026-05-10 00:59:47 +00:00
// Якщо (Pages) > 0 - незаповнену сторінку кодую в стисненому вигляді.
// Якщо (Pages) = 0 - кодую просто пари timestamp / value
2026-05-21 23:38:52 +03:00
// func (s *Writer) packWriteAppended(w *bytes.Buffer, req AppendedPagesReq) {
// // завантажений path. Отже просто додаємо в дерево data сторінки, створюємо нові індексні
// // без звернення до диску.
// report := s.atree.AppendDataPages(atree.AppendDataPagesReq{
// LastPageNo: req.LastPageNo,
// Legs: req.Legs,
// DataPages: req.Pages,
// })
2026-02-10 14:02:11 +00:00
2026-05-21 23:38:52 +03:00
// m := AppendedPages{
// MetricID: req.MetricID,
// Timestamp: req.Timestamp,
// Value: req.Value,
// NewRootPageNo: report.NewRootPageNo,
// LastPageNo: report.LastPageNo,
// Pages: report.Pages,
// TimestampsChunks: req.TimestampsChunks,
// TimestampsSize: int(req.TimestampsSize),
// ValuesChunks: req.ValuesChunks,
// ValuesSize: int(req.ValuesSize),
// }
// m.Pack(w)
// }
2026-05-10 00:59:47 +00:00
// helpers