Initial commit
This commit is contained in:
136
lib/atomicheader/atomicheader.go
Normal file
136
lib/atomicheader/atomicheader.go
Normal 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.")
|
||||
}
|
||||
}
|
||||
121
lib/atomicheader/atomicheader_test.go
Normal file
121
lib/atomicheader/atomicheader_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user