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