feat: initialize project structure and error types
- go.mod with github.com/dailz/go-kv, Go 1.26.3, testify - config/config.go with WalConfig, Validate() with checked arithmetic - errors.go with sentinel errors (ErrCommitUnknown, ErrWriteStopped, etc.) - wal/constants.go with all WAL format constants and enums - wal/header.go with WAL File Header encode/decode (CRC32 IEEE) - wal/record.go with Physical Record codec, block boundary, SplitIntoRecords - wal/entry.go with WAL Entry codec (varint keys/values, OpType, ValueKind) - wal/sequence.go with SequenceManager (atomic, CAS, overflow-safe) - manifest/manifest.go with MANIFEST stub (Load/Save atomic) - manifest/current.go with CURRENT file (WriteCurrent/ReadCurrent) - Comprehensive tests for all modules - .golangci.yml configuration
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WriteCurrent writes the CURRENT file to dir with a best-effort atomic rename.
|
||||
// The file contains the active WAL segment filename (e.g. "segment-5.wal").
|
||||
// CURRENT is only a write-side hint; it may be missing or stale after a crash.
|
||||
func WriteCurrent(dir string, segmentID uint64) error {
|
||||
content := fmt.Sprintf("segment-%d.wal\n", segmentID)
|
||||
tmpPath := dir + "/CURRENT.tmp"
|
||||
if err := os.WriteFile(tmpPath, []byte(content), 0o644); err != nil {
|
||||
return fmt.Errorf("write current tmp: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, dir+"/CURRENT"); err != nil {
|
||||
return fmt.Errorf("rename current: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadCurrent reads the CURRENT file from dir and returns the segment ID.
|
||||
// If the file does not exist or cannot be parsed, it returns 0, false with no error.
|
||||
func ReadCurrent(dir string) (segmentID uint64, ok bool) {
|
||||
data, err := os.ReadFile(dir + "/CURRENT")
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
line := strings.TrimSpace(string(data))
|
||||
// Expected format: "segment-N.wal"
|
||||
if !strings.HasPrefix(line, "segment-") || !strings.HasSuffix(line, ".wal") {
|
||||
return 0, false
|
||||
}
|
||||
idStr := strings.TrimSuffix(strings.TrimPrefix(line, "segment-"), ".wal")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package manifest manages database metadata and checkpoint information.
|
||||
package manifest
|
||||
@@ -0,0 +1,52 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Manifest holds database metadata used for recovery.
|
||||
// The MANIFEST file stores the recovery checkpoint so that
|
||||
// recovery knows which segments are already confirmed durable.
|
||||
type Manifest struct {
|
||||
RecoverySegmentID uint64
|
||||
}
|
||||
|
||||
// Load reads the MANIFEST file from dir.
|
||||
// If the file does not exist, it returns a zero-value Manifest with no error (fresh DB).
|
||||
func Load(dir string) (*Manifest, error) {
|
||||
path := dir + "/MANIFEST"
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &Manifest{RecoverySegmentID: 0}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read manifest: %w", err)
|
||||
}
|
||||
|
||||
line := strings.TrimSpace(string(data))
|
||||
if !strings.HasPrefix(line, "recovery_segment_id:") {
|
||||
return nil, fmt.Errorf("manifest: invalid format: %q", line)
|
||||
}
|
||||
idStr := strings.TrimPrefix(line, "recovery_segment_id:")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("manifest: parse recovery_segment_id: %w", err)
|
||||
}
|
||||
return &Manifest{RecoverySegmentID: id}, nil
|
||||
}
|
||||
|
||||
// Save atomically writes the MANIFEST file to dir with the given recoverySegmentID.
|
||||
func Save(dir string, recoverySegmentID uint64) error {
|
||||
content := fmt.Sprintf("recovery_segment_id:%d\n", recoverySegmentID)
|
||||
tmpPath := dir + "/MANIFEST.tmp"
|
||||
if err := os.WriteFile(tmpPath, []byte(content), 0o644); err != nil {
|
||||
return fmt.Errorf("write manifest tmp: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, dir+"/MANIFEST"); err != nil {
|
||||
return fmt.Errorf("rename manifest: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestManifestRoundtrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
err := Save(dir, 42)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := Load(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(42), m.RecoverySegmentID)
|
||||
}
|
||||
|
||||
func TestManifestMissing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
m, err := Load(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(0), m.RecoverySegmentID)
|
||||
}
|
||||
|
||||
func TestCurrentRoundtrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
err := WriteCurrent(dir, 5)
|
||||
require.NoError(t, err)
|
||||
|
||||
segmentID, ok := ReadCurrent(dir)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, uint64(5), segmentID)
|
||||
}
|
||||
|
||||
func TestCurrentMissing(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
_, ok := ReadCurrent(dir)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
Reference in New Issue
Block a user