2026-02-10 14:02:11 +00:00
|
|
|
|
package freelist
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-02-23 11:02:29 +00:00
|
|
|
|
"bytes"
|
2026-02-10 14:02:11 +00:00
|
|
|
|
"fmt"
|
2026-05-31 20:01:28 +00:00
|
|
|
|
"io"
|
2026-02-23 11:02:29 +00:00
|
|
|
|
"os"
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-02-23 11:02:29 +00:00
|
|
|
|
"gordenko.dev/dima/qb/bin"
|
2026-05-21 23:38:52 +03:00
|
|
|
|
"gordenko.dev/dima/qb/util"
|
2026-02-23 11:02:29 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-31 20:01:28 +00:00
|
|
|
|
/*
|
|
|
|
|
|
Ідея - я фіксую кількість сторінок записаних в снапшоті.
|
|
|
|
|
|
Додаю і видаляю сторінки із основного файла, поки їх кількість > кількості зафіксованих.
|
|
|
|
|
|
Якщо pageNumbers видалено і кількість сторінок треба зменшити - нові сторінки пишу в delta файл.
|
|
|
|
|
|
При створенні снапшота - сторінки із delta файла записую в основний файл і фіксую в снапшоті
|
|
|
|
|
|
їх кількість. PageNumbers із буфера пишу в снапшот.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-02-23 11:02:29 +00:00
|
|
|
|
const (
|
2026-05-21 23:38:52 +03:00
|
|
|
|
crcSize = 4
|
|
|
|
|
|
ptrSize = 4
|
2026-02-10 14:02:11 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type FreeList struct {
|
2026-05-31 20:01:28 +00:00
|
|
|
|
baseFile *os.File
|
|
|
|
|
|
deltaFile *os.File
|
|
|
|
|
|
pageSize int
|
|
|
|
|
|
pointersOnPage int
|
|
|
|
|
|
flushTreshold int // кількість вільних сторінок коли вже треба писати на диск
|
|
|
|
|
|
frozenPageCount int
|
|
|
|
|
|
basePagesCount int
|
|
|
|
|
|
deltaPagesCount int
|
|
|
|
|
|
free []uint32
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:38:52 +03:00
|
|
|
|
type Options struct {
|
2026-05-31 20:01:28 +00:00
|
|
|
|
PageSize int
|
|
|
|
|
|
BaseFilePath string
|
|
|
|
|
|
DeltaFilePath string
|
|
|
|
|
|
FrozenPageCount int
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:38:52 +03:00
|
|
|
|
func New(opt Options) (*FreeList, error) {
|
|
|
|
|
|
s := &FreeList{
|
2026-05-31 20:01:28 +00:00
|
|
|
|
pageSize: opt.PageSize,
|
|
|
|
|
|
pointersOnPage: (opt.PageSize - crcSize) / ptrSize,
|
|
|
|
|
|
frozenPageCount: opt.FrozenPageCount,
|
|
|
|
|
|
basePagesCount: opt.FrozenPageCount,
|
2026-02-23 11:02:29 +00:00
|
|
|
|
}
|
2026-05-21 23:38:52 +03:00
|
|
|
|
s.flushTreshold = int(float64(s.pointersOnPage) * 1.3)
|
2026-05-31 20:01:28 +00:00
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
|
s.baseFile, err = os.OpenFile(opt.BaseFilePath, os.O_RDWR|os.O_CREATE, 0666)
|
2026-05-21 23:38:52 +03:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
2026-05-31 20:01:28 +00:00
|
|
|
|
s.deltaFile, err = os.OpenFile(opt.DeltaFilePath, os.O_RDWR|os.O_CREATE, 0666)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
2026-05-21 23:38:52 +03:00
|
|
|
|
}
|
2026-05-31 20:01:28 +00:00
|
|
|
|
// info, err := s.baseFile.Stat()
|
|
|
|
|
|
// if err != nil {
|
|
|
|
|
|
// return nil, err
|
|
|
|
|
|
// }
|
|
|
|
|
|
// fileSize := info.Size()
|
|
|
|
|
|
// if fileSize > 0 {
|
|
|
|
|
|
// if (fileSize % int64(s.pageSize)) > 0 {
|
|
|
|
|
|
// return nil, fmt.Errorf("file size %d is not a multiple of page size %d",
|
|
|
|
|
|
// fileSize, s.pageSize)
|
|
|
|
|
|
// }
|
|
|
|
|
|
// s.basePagesCount = int(fileSize) / s.pageSize
|
|
|
|
|
|
// }
|
2026-05-21 23:38:52 +03:00
|
|
|
|
return s, nil
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 20:01:28 +00:00
|
|
|
|
func (s *FreeList) Pages() int {
|
|
|
|
|
|
return s.basePagesCount + s.deltaPagesCount
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:38:52 +03:00
|
|
|
|
func (s *FreeList) AddPageNumbers(pageNumbers []uint32) (err error) {
|
2026-02-10 14:02:11 +00:00
|
|
|
|
if len(pageNumbers) == 0 {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-23 11:02:29 +00:00
|
|
|
|
s.free = append(s.free, pageNumbers...)
|
2026-05-21 23:38:52 +03:00
|
|
|
|
|
|
|
|
|
|
for len(s.free) > s.flushTreshold {
|
|
|
|
|
|
err = s.save()
|
2026-02-23 11:02:29 +00:00
|
|
|
|
if err != nil {
|
2026-05-21 23:38:52 +03:00
|
|
|
|
return
|
2026-02-23 11:02:29 +00:00
|
|
|
|
}
|
2026-05-21 23:38:52 +03:00
|
|
|
|
// викидаю номери сторінок, які записав на диск
|
|
|
|
|
|
copy(s.free, s.free[s.pointersOnPage:])
|
|
|
|
|
|
s.free = s.free[:len(s.free)-s.pointersOnPage]
|
2026-05-31 20:01:28 +00:00
|
|
|
|
fmt.Println("free:", s.free)
|
2026-02-23 11:02:29 +00:00
|
|
|
|
}
|
2026-05-21 23:38:52 +03:00
|
|
|
|
return
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 11:02:29 +00:00
|
|
|
|
func (s *FreeList) save() error {
|
2026-05-31 20:01:28 +00:00
|
|
|
|
fmt.Println("save:", s.free[:s.pointersOnPage])
|
2026-05-21 23:38:52 +03:00
|
|
|
|
buf := make([]byte, s.pageSize)
|
2026-02-23 11:02:29 +00:00
|
|
|
|
i := crcSize
|
2026-05-21 23:38:52 +03:00
|
|
|
|
for _, pageNo := range s.free[:s.pointersOnPage] {
|
2026-02-23 11:02:29 +00:00
|
|
|
|
bin.PutUint32(buf[i:], pageNo)
|
|
|
|
|
|
i += ptrSize
|
|
|
|
|
|
}
|
2026-05-21 23:38:52 +03:00
|
|
|
|
bin.PutUint32(buf, util.CalcChecksum(buf[crcSize:]))
|
|
|
|
|
|
//
|
2026-05-31 20:01:28 +00:00
|
|
|
|
var (
|
|
|
|
|
|
file *os.File
|
|
|
|
|
|
off int64
|
|
|
|
|
|
)
|
|
|
|
|
|
if s.deltaPagesCount > 0 || s.basePagesCount < s.frozenPageCount {
|
|
|
|
|
|
file = s.deltaFile
|
|
|
|
|
|
off = int64(s.deltaPagesCount * s.pageSize)
|
|
|
|
|
|
s.deltaPagesCount++
|
|
|
|
|
|
} else {
|
|
|
|
|
|
file = s.baseFile
|
|
|
|
|
|
off = int64(s.basePagesCount * s.pageSize)
|
|
|
|
|
|
s.basePagesCount++
|
|
|
|
|
|
}
|
|
|
|
|
|
fmt.Println("write at:", off)
|
|
|
|
|
|
n, err := file.WriteAt(buf, off)
|
2026-02-23 11:02:29 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-05-21 23:38:52 +03:00
|
|
|
|
if n != s.pageSize {
|
|
|
|
|
|
return fmt.Errorf("written size %d bytes not equal page size %d", n, s.pageSize)
|
|
|
|
|
|
}
|
2026-05-31 20:01:28 +00:00
|
|
|
|
err = file.Sync()
|
2026-05-21 23:38:52 +03:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
2026-02-23 11:02:29 +00:00
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *FreeList) loadLastPage() error {
|
|
|
|
|
|
var (
|
2026-05-31 20:01:28 +00:00
|
|
|
|
file *os.File
|
|
|
|
|
|
off int64
|
|
|
|
|
|
buf = make([]byte, s.pageSize)
|
2026-02-23 11:02:29 +00:00
|
|
|
|
)
|
2026-05-31 20:01:28 +00:00
|
|
|
|
if s.deltaPagesCount > 0 {
|
|
|
|
|
|
file = s.deltaFile
|
|
|
|
|
|
off = int64((s.deltaPagesCount - 1) * s.pageSize)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
file = s.baseFile
|
|
|
|
|
|
off = int64((s.basePagesCount - 1) * s.pageSize)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
n, err := file.ReadAt(buf, off)
|
2026-02-23 11:02:29 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("file.Seek: %s", err)
|
|
|
|
|
|
}
|
2026-05-21 23:38:52 +03:00
|
|
|
|
if n != s.pageSize {
|
|
|
|
|
|
return fmt.Errorf("read size %d bytes not equal page size %d", n, s.pageSize)
|
2026-02-23 11:02:29 +00:00
|
|
|
|
}
|
|
|
|
|
|
// check crc
|
2026-05-21 23:38:52 +03:00
|
|
|
|
checksum := bin.GetUint32(buf[0:])
|
|
|
|
|
|
if util.CalcChecksum(buf[crcSize:]) != checksum {
|
2026-05-31 20:01:28 +00:00
|
|
|
|
return fmt.Errorf("page %d is corrupted", s.basePagesCount)
|
2026-05-21 23:38:52 +03:00
|
|
|
|
}
|
2026-02-23 11:02:29 +00:00
|
|
|
|
for i := crcSize; i < len(buf); i += ptrSize {
|
|
|
|
|
|
s.free = append(s.free, bin.GetUint32(buf[i:]))
|
|
|
|
|
|
}
|
2026-05-31 20:01:28 +00:00
|
|
|
|
if s.deltaPagesCount > 0 {
|
|
|
|
|
|
s.deltaPagesCount--
|
|
|
|
|
|
err = file.Truncate(off)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return file.Sync()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
s.basePagesCount--
|
|
|
|
|
|
if s.basePagesCount >= s.frozenPageCount {
|
|
|
|
|
|
err = s.baseFile.Truncate(off)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return file.Sync()
|
|
|
|
|
|
}
|
2026-05-21 23:38:52 +03:00
|
|
|
|
}
|
2026-02-23 11:02:29 +00:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:38:52 +03:00
|
|
|
|
func (s *FreeList) GetPageNumber() (pageNo uint32, err error) {
|
2026-02-23 11:02:29 +00:00
|
|
|
|
if len(s.free) == 0 {
|
2026-05-31 20:01:28 +00:00
|
|
|
|
if s.basePagesCount > 0 || s.deltaPagesCount > 0 {
|
|
|
|
|
|
// якщо немає вільних сторінок у буфері, але є у freelist файлах -
|
|
|
|
|
|
// завантажую сторінку із диску
|
|
|
|
|
|
err = s.loadLastPage()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-02-23 11:02:29 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
2026-05-31 20:01:28 +00:00
|
|
|
|
if len(s.free) > 0 {
|
|
|
|
|
|
lastIdx := len(s.free) - 1
|
|
|
|
|
|
pageNo = s.free[lastIdx]
|
|
|
|
|
|
s.free = s.free[:lastIdx]
|
|
|
|
|
|
}
|
2026-02-10 14:02:11 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:38:52 +03:00
|
|
|
|
// для відновлення недозаповненнох сторінки із снапшота
|
|
|
|
|
|
func (s *FreeList) Restore(serialized []byte) error {
|
|
|
|
|
|
if (len(serialized) % ptrSize) != 0 {
|
|
|
|
|
|
return fmt.Errorf("wrong size")
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := 0; i < len(serialized); i += ptrSize {
|
|
|
|
|
|
s.free = append(s.free, bin.GetUint32(serialized[i:]))
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
2026-05-31 20:01:28 +00:00
|
|
|
|
fmt.Println(s.free)
|
2026-05-21 23:38:52 +03:00
|
|
|
|
return nil
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:38:52 +03:00
|
|
|
|
// для запису в snapshot
|
2026-02-10 14:02:11 +00:00
|
|
|
|
func (s *FreeList) Serialize() ([]byte, error) {
|
2026-02-23 11:02:29 +00:00
|
|
|
|
w := bytes.NewBuffer(nil)
|
|
|
|
|
|
for _, pageNo := range s.free {
|
|
|
|
|
|
bin.WriteUint32(w, pageNo)
|
|
|
|
|
|
}
|
|
|
|
|
|
return w.Bytes(), nil
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
2026-05-31 20:01:28 +00:00
|
|
|
|
|
2026-06-09 08:16:16 +03:00
|
|
|
|
func (s *FreeList) Cached() []uint32 {
|
|
|
|
|
|
return s.free
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 20:01:28 +00:00
|
|
|
|
func (s *FreeList) Merge() (err error) {
|
|
|
|
|
|
var (
|
|
|
|
|
|
off = int64(s.basePagesCount * s.pageSize)
|
|
|
|
|
|
deltaSize = int64(s.deltaPagesCount * s.pageSize)
|
|
|
|
|
|
)
|
|
|
|
|
|
if s.deltaPagesCount > 0 {
|
|
|
|
|
|
var (
|
|
|
|
|
|
ret int64
|
|
|
|
|
|
copied int64
|
|
|
|
|
|
)
|
|
|
|
|
|
ret, err = s.baseFile.Seek(off, 0)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if ret != off {
|
|
|
|
|
|
return fmt.Errorf("Seek failed")
|
|
|
|
|
|
}
|
|
|
|
|
|
copied, err = io.Copy(s.baseFile, s.deltaFile)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if copied != deltaSize {
|
|
|
|
|
|
return fmt.Errorf("copy failed")
|
|
|
|
|
|
}
|
|
|
|
|
|
err = s.baseFile.Sync()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
err = s.deltaFile.Truncate(0)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
s.basePagesCount += s.deltaPagesCount
|
|
|
|
|
|
s.deltaPagesCount = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.basePagesCount < s.frozenPageCount {
|
|
|
|
|
|
off = int64(s.basePagesCount * s.pageSize)
|
|
|
|
|
|
err = s.baseFile.Truncate(off)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
s.frozenPageCount = s.basePagesCount
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|