docs(kv): document guarantees in the package doc and add a README example
- commit
- 3755228d8f6ab2036e0adb78dcfe4bf8b120d884
- author
- brice · · 2026-10-10 02:56 UTC
- parent
- aa8d6de
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +49 -3
M README.md +25 -3
| @@ -1,17 +1,39 @@ | |||
| 1 | 1 | # ermite-kv | |
| 2 | 2 | ||
| 3 | - | An embedded key-value store for Go. Data lives in memory and is persisted to an append-only log. One writer goroutine groups commits into a single `fsync`, and readers use immutable snapshots without locks. Standard library only. | |
| 3 | + | An embedded key-value store for Go. Data lives in memory and is persisted to an append-only log with snapshots. One writer goroutine groups commits into a single `fsync`, and readers use immutable snapshots without locks. Standard library only, plus [zerowrap](https://github.com/bnema/zerowrap) for logs. | |
| 4 | 4 | ||
| 5 | - | Status: early development, no stable API. | |
| 5 | + | Status: early development, no stable API. Linux only. | |
| 6 | 6 | ||
| 7 | 7 | ```go | |
| 8 | 8 | import kv "bnema.dev/ermite-kv" | |
| 9 | + | ||
| 10 | + | db, err := kv.Open(ctx, "/var/lib/app/kv", kv.Options{}) | |
| 11 | + | if err != nil { | |
| 12 | + | return err | |
| 13 | + | } | |
| 14 | + | defer db.Close(ctx) | |
| 15 | + | ||
| 16 | + | var b kv.Batch | |
| 17 | + | b.RequireAbsent([]byte("user/alice")) | |
| 18 | + | b.Put([]byte("user/alice"), []byte(`{"name":"Alice"}`)) | |
| 19 | + | seq, err := db.Apply(ctx, &b) // durable and visible when it returns | |
| 20 | + | if errors.Is(err, kv.ErrConflict) { | |
| 21 | + | // the key already exists | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | for key, item := range db.Snapshot(ctx).Prefix(ctx, []byte("user/")) { | |
| 25 | + | fmt.Printf("%s = %s (version %d)\n", key, item.Value, item.Version) | |
| 26 | + | } | |
| 9 | 27 | ``` | |
| 10 | 28 | ||
| 29 | + | The package documentation describes the durability and visibility guarantees. | |
| 30 | + | ||
| 11 | 31 | ## Test | |
| 12 | 32 | ||
| 13 | 33 | ```sh | |
| 14 | - | go test ./... | |
| 34 | + | go test -race ./... | |
| 35 | + | ERMITE_KV_CRASH_ITERATIONS=200 go test -run TestCrash . # SIGKILL crash test | |
| 36 | + | ERMITE_KV_LOG_LEVEL=debug go test -run TestPowerLoss . # with logs | |
| 15 | 37 | ``` | |
| 16 | 38 | ||
| 17 | 39 | ## License | |
| @@ -1,17 +1,39 @@ | |||||
| 1 | # ermite-kv | 1 | # ermite-kv | ||
| 2 | 2 | ||||
| 3 | - | An embedded key-value store for Go. Data lives in memory and is persisted to an append-only log. One writer goroutine groups commits into a single `fsync`, and readers use immutable snapshots without locks. Standard library only. | 3 | + | An embedded key-value store for Go. Data lives in memory and is persisted to an append-only log with snapshots. One writer goroutine groups commits into a single `fsync`, and readers use immutable snapshots without locks. Standard library only, plus [zerowrap](https://github.com/bnema/zerowrap) for logs. |
| 4 | 4 | ||||
| 5 | - | Status: early development, no stable API. | 5 | + | Status: early development, no stable API. Linux only. |
| 6 | 6 | ||||
| 7 | ```go | 7 | ```go | ||
| 8 | import kv "bnema.dev/ermite-kv" | 8 | import kv "bnema.dev/ermite-kv" | ||
| 9 | + | ||||
| 10 | + | db, err := kv.Open(ctx, "/var/lib/app/kv", kv.Options{}) | |||
| 11 | + | if err != nil { | |||
| 12 | + | return err | |||
| 13 | + | } | |||
| 14 | + | defer db.Close(ctx) | |||
| 15 | + | ||||
| 16 | + | var b kv.Batch | |||
| 17 | + | b.RequireAbsent([]byte("user/alice")) | |||
| 18 | + | b.Put([]byte("user/alice"), []byte(`{"name":"Alice"}`)) | |||
| 19 | + | seq, err := db.Apply(ctx, &b) // durable and visible when it returns | |||
| 20 | + | if errors.Is(err, kv.ErrConflict) { | |||
| 21 | + | // the key already exists | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | for key, item := range db.Snapshot(ctx).Prefix(ctx, []byte("user/")) { | |||
| 25 | + | fmt.Printf("%s = %s (version %d)\n", key, item.Value, item.Version) | |||
| 26 | + | } | |||
| 9 | ``` | 27 | ``` | ||
| 10 | 28 | ||||
| 29 | + | The package documentation describes the durability and visibility guarantees. | |||
| 30 | + | ||||
| 11 | ## Test | 31 | ## Test | ||
| 12 | 32 | ||||
| 13 | ```sh | 33 | ```sh | ||
| 14 | - | go test ./... | 34 | + | go test -race ./... |
| 35 | + | ERMITE_KV_CRASH_ITERATIONS=200 go test -run TestCrash . # SIGKILL crash test | |||
| 36 | + | ERMITE_KV_LOG_LEVEL=debug go test -run TestPowerLoss . # with logs | |||
| 15 | ``` | 37 | ``` | ||
| 16 | 38 | ||||
| 17 | ## License | 39 | ## License | ||
M doc.go +24 -0
| @@ -4,4 +4,28 @@ | |||
| 4 | 4 | // memory as immutable snapshots and is persisted to an append-only log. | |
| 5 | 5 | // A single writer goroutine applies batches and groups their fsync calls; | |
| 6 | 6 | // readers load the current snapshot without locks. | |
| 7 | + | // | |
| 8 | + | // # Guarantees | |
| 9 | + | // | |
| 10 | + | // - Durability: DB.Apply returns only after the batch is written and | |
| 11 | + | // synced (fdatasync), unless Options.NoSync is set. After a crash or a | |
| 12 | + | // power loss, Open recovers every acknowledged batch, and the state is | |
| 13 | + | // exactly a prefix of the applied batches. | |
| 14 | + | // - Atomicity: a Batch is applied entirely or not at all. Its | |
| 15 | + | // preconditions (RequireAbsent, RequireVersion) are checked against the | |
| 16 | + | // latest state before any op; a failed precondition returns ErrConflict. | |
| 17 | + | // - Visibility: a successful Apply is visible to the next DB.Snapshot or | |
| 18 | + | // DB.Get (read-your-writes). Nothing is visible before it is durable. | |
| 19 | + | // - Snapshots are immutable and safe for concurrent use. Keys yielded by | |
| 20 | + | // iterators and Item.Value are owned by the snapshot and must not be | |
| 21 | + | // modified. | |
| 22 | + | // - ErrFailed means a write or sync failed. The outcome of the batches in | |
| 23 | + | // that commit group is unknown (they may be replayed by the next Open); | |
| 24 | + | // every later Apply fails, while reads keep serving the last snapshot. | |
| 25 | + | // Close the DB and open it again to recover. | |
| 26 | + | // - ErrCorrupt means Open found damage that is not a torn tail of the last | |
| 27 | + | // log. Open never silently drops acknowledged data. | |
| 28 | + | // | |
| 29 | + | // One process at a time may open a directory (ErrLocked otherwise). Linux | |
| 30 | + | // only. Logs go through the zerowrap logger carried by the ctx given to Open. | |
| 7 | 31 | package kv | |
| @@ -4,4 +4,28 @@ | |||||
| 4 | // memory as immutable snapshots and is persisted to an append-only log. | 4 | // memory as immutable snapshots and is persisted to an append-only log. | ||
| 5 | // A single writer goroutine applies batches and groups their fsync calls; | 5 | // A single writer goroutine applies batches and groups their fsync calls; | ||
| 6 | // readers load the current snapshot without locks. | 6 | // readers load the current snapshot without locks. | ||
| 7 | + | // | |||
| 8 | + | // # Guarantees | |||
| 9 | + | // | |||
| 10 | + | // - Durability: DB.Apply returns only after the batch is written and | |||
| 11 | + | // synced (fdatasync), unless Options.NoSync is set. After a crash or a | |||
| 12 | + | // power loss, Open recovers every acknowledged batch, and the state is | |||
| 13 | + | // exactly a prefix of the applied batches. | |||
| 14 | + | // - Atomicity: a Batch is applied entirely or not at all. Its | |||
| 15 | + | // preconditions (RequireAbsent, RequireVersion) are checked against the | |||
| 16 | + | // latest state before any op; a failed precondition returns ErrConflict. | |||
| 17 | + | // - Visibility: a successful Apply is visible to the next DB.Snapshot or | |||
| 18 | + | // DB.Get (read-your-writes). Nothing is visible before it is durable. | |||
| 19 | + | // - Snapshots are immutable and safe for concurrent use. Keys yielded by | |||
| 20 | + | // iterators and Item.Value are owned by the snapshot and must not be | |||
| 21 | + | // modified. | |||
| 22 | + | // - ErrFailed means a write or sync failed. The outcome of the batches in | |||
| 23 | + | // that commit group is unknown (they may be replayed by the next Open); | |||
| 24 | + | // every later Apply fails, while reads keep serving the last snapshot. | |||
| 25 | + | // Close the DB and open it again to recover. | |||
| 26 | + | // - ErrCorrupt means Open found damage that is not a torn tail of the last | |||
| 27 | + | // log. Open never silently drops acknowledged data. | |||
| 28 | + | // | |||
| 29 | + | // One process at a time may open a directory (ErrLocked otherwise). Linux | |||
| 30 | + | // only. Logs go through the zerowrap logger carried by the ctx given to Open. | |||
| 7 | package kv | 31 | package kv | ||