Initial commit

This commit is contained in:
jdl
2023-10-13 11:43:27 +02:00
commit 71eb6b0c7e
121 changed files with 11493 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
package atomicheader
import (
"encoding/binary"
"hash/crc32"
"git.crumpington.com/public/jldb/lib/errs"
"os"
"sync"
)
const (
PageSize = 512
AvailabePageSize = 508
ReservedBytes = PageSize * 4
offsetSwitch = 1 * PageSize
offset1 = 2 * PageSize
offset2 = 3 * PageSize
)
type Handler struct {
lock sync.Mutex
switchPage []byte // At offsetSwitch.
page []byte // Page buffer is re-used for reading and writing.
currentPage int64 // Either 0 or 1.
f *os.File
}
func Init(f *os.File) error {
if err := f.Truncate(ReservedBytes); err != nil {
return errs.IO.WithErr(err)
}
switchPage := make([]byte, PageSize)
switchPage[0] = 2
if _, err := f.WriteAt(switchPage, offsetSwitch); err != nil {
return errs.IO.WithErr(err)
}
return nil
}
func Open(f *os.File) (*Handler, error) {
switchPage := make([]byte, PageSize)
if _, err := f.ReadAt(switchPage, offsetSwitch); err != nil {
return nil, errs.IO.WithErr(err)
}
h := &Handler{
switchPage: switchPage,
page: make([]byte, PageSize),
currentPage: int64(switchPage[0]),
f: f,
}
if h.currentPage != 1 && h.currentPage != 2 {
return nil, errs.Corrupt.WithMsg("invalid page id: %d", h.currentPage)
}
return h, nil
}
// Read reads the currently active header page.
func (h *Handler) Read(read func(page []byte) error) error {
h.lock.Lock()
defer h.lock.Unlock()
if _, err := h.f.ReadAt(h.page, h.currentOffset()); err != nil {
return errs.IO.WithErr(err)
}
computedCRC := crc32.ChecksumIEEE(h.page[:PageSize-4])
storedCRC := binary.LittleEndian.Uint32(h.page[PageSize-4:])
if computedCRC != storedCRC {
return errs.Corrupt.WithMsg("checksum mismatch")
}
return read(h.page)
}
// Write writes the currently active header page. The page buffer given to the
// function may contain old data, so the caller may need to zero some bytes if
// necessary.
func (h *Handler) Write(update func(page []byte) error) error {
h.lock.Lock()
defer h.lock.Unlock()
if err := update(h.page); err != nil {
return err
}
crc := crc32.ChecksumIEEE(h.page[:PageSize-4])
binary.LittleEndian.PutUint32(h.page[PageSize-4:], crc)
newPageNum := 1 + h.currentPage%2
newOffset := h.getOffset(newPageNum)
if _, err := h.f.WriteAt(h.page, newOffset); err != nil {
return errs.IO.WithErr(err)
}
if err := h.f.Sync(); err != nil {
return errs.IO.WithErr(err)
}
h.switchPage[0] = byte(newPageNum)
if _, err := h.f.WriteAt(h.switchPage, offsetSwitch); err != nil {
return errs.IO.WithErr(err)
}
if err := h.f.Sync(); err != nil {
return errs.IO.WithErr(err)
}
h.currentPage = newPageNum
return nil
}
// ----------------------------------------------------------------------------
func (h *Handler) currentOffset() int64 {
return h.getOffset(h.currentPage)
}
func (h *Handler) getOffset(pageNum int64) int64 {
switch pageNum {
case 1:
return offset1
case 2:
return offset2
default:
panic("Invalid page number.")
}
}

View File

@@ -0,0 +1,121 @@
package atomicheader
import (
"errors"
"os"
"path/filepath"
"sync"
"testing"
"time"
)
func NewForTesting(t *testing.T) (*Handler, func()) {
tmpDir := t.TempDir()
f, err := os.Create(filepath.Join(tmpDir, "h"))
if err != nil {
t.Fatal(err)
}
if err := Init(f); err != nil {
t.Fatal(err)
}
h, err := Open(f)
if err != nil {
t.Fatal(err)
}
return h, func() {
f.Close()
os.RemoveAll(tmpDir)
}
}
func TestAtomicHeaderSimple(t *testing.T) {
h, cleanup := NewForTesting(t)
defer cleanup()
err := h.Write(func(page []byte) error {
for i := range page[:AvailabePageSize] {
page[i] = byte(i) % 11
}
return nil
})
if err != nil {
t.Fatal(err)
}
err = h.Read(func(page []byte) error {
for i := range page[:AvailabePageSize] {
if page[i] != byte(i)%11 {
t.Fatal(i, page[i], byte(i)%11)
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
func TestAtomicHeaderThreaded(t *testing.T) {
h, cleanup := NewForTesting(t)
defer cleanup()
expectedValue := byte(0)
writeErr := make(chan error, 1)
stop := make(chan struct{})
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
writeErr <- nil
return
default:
}
err := h.Write(func(page []byte) error {
if page[0] != expectedValue {
return errors.New("Unexpected current value.")
}
expectedValue++
page[0] = expectedValue
return nil
})
if err != nil {
writeErr <- err
return
}
time.Sleep(time.Millisecond / 13)
}
}()
for i := 0; i < 2000; i++ {
time.Sleep(time.Millisecond)
err := h.Read(func(page []byte) error {
if page[0] != expectedValue {
t.Fatal(page[0], expectedValue)
}
return nil
})
if err != nil {
t.Fatal(err)
}
}
close(stop)
wg.Wait()
if err := <-writeErr; err != nil {
t.Fatal(err)
}
}