detached 0355b83

feat(db): add DB.Err to report why the database stopped without touching the writer

commit
0355b83e3455af7b87098cd1ec994b5e09d12bd9
author
brice · · 2026-10-11 20:41 UTC
parent
21d0a80
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +45 -0
M db.go +5 -0
@@ -242,6 +242,11 @@ func (db *DB) Apply(ctx context.Context, b *Batch) (uint64, error) {
242242 return res.seq, res.err
243243 }
244244
245+// Err reports why the database stopped: ErrClosed after Close, ErrFailed
246+// (wrapped as Apply returns it) after a fatal write error, nil while it runs.
247+// It does not touch the writer.
248+func (db *DB) Err() error { return db.stopped() }
249+
245250 // stopped returns the failure (which wins over Close, so callers see a
246251 // stable error), ErrClosed, or nil while the database accepts batches.
247252 func (db *DB) stopped() error {
@@ -242,6 +242,11 @@ func (db *DB) Apply(ctx context.Context, b *Batch) (uint64, error) {
242 return res.seq, res.err242 return res.seq, res.err
243 }243 }
244 244
245+// Err reports why the database stopped: ErrClosed after Close, ErrFailed
246+// (wrapped as Apply returns it) after a fatal write error, nil while it runs.
247+// It does not touch the writer.
248+func (db *DB) Err() error { return db.stopped() }
249+
245 // stopped returns the failure (which wins over Close, so callers see a250 // stopped returns the failure (which wins over Close, so callers see a
246 // stable error), ErrClosed, or nil while the database accepts batches.251 // stable error), ErrClosed, or nil while the database accepts batches.
247 func (db *DB) stopped() error {252 func (db *DB) stopped() error {
M db_test.go +40 -0
@@ -1181,3 +1181,43 @@ func BenchmarkApply(b *testing.B) {
11811181 b.ReportMetric(syncs/secs, "syncs/s")
11821182 b.ReportMetric(float64(b.N)/syncs, "batches/sync")
11831183 }
1184+
1185+func TestErr(t *testing.T) {
1186+ tests := []struct {
1187+ name string
1188+ stop func(t *testing.T, db *DB, fx *faultfs.FS)
1189+ want error
1190+ }{
1191+ {"running", func(*testing.T, *DB, *faultfs.FS) {}, nil},
1192+ {"closed", func(t *testing.T, db *DB, _ *faultfs.FS) {
1193+ if err := db.Close(bg); err != nil {
1194+ t.Fatalf("Close: %v", err)
1195+ }
1196+ }, ErrClosed},
1197+ {"failed", func(t *testing.T, db *DB, fx *faultfs.FS) {
1198+ fx.FailWrite(1, nil)
1199+ var b Batch
1200+ b.Put([]byte("k"), []byte("v"))
1201+ if _, err := db.Apply(bg, &b); !errors.Is(err, ErrFailed) {
1202+ t.Fatalf("Apply: %v, want ErrFailed", err)
1203+ }
1204+ }, ErrFailed},
1205+ }
1206+ for _, tt := range tests {
1207+ t.Run(tt.name, func(t *testing.T) {
1208+ fx := faultfs.New()
1209+ db := mustOpen(t, fx, Options{})
1210+ tt.stop(t, db, fx)
1211+ err := db.Err()
1212+ if tt.want == nil {
1213+ if err != nil {
1214+ t.Fatalf("Err = %v, want nil", err)
1215+ }
1216+ return
1217+ }
1218+ if !errors.Is(err, tt.want) {
1219+ t.Fatalf("Err = %v, want %v", err, tt.want)
1220+ }
1221+ })
1222+ }
1223+}
@@ -1181,3 +1181,43 @@ func BenchmarkApply(b *testing.B) {
1181 b.ReportMetric(syncs/secs, "syncs/s")1181 b.ReportMetric(syncs/secs, "syncs/s")
1182 b.ReportMetric(float64(b.N)/syncs, "batches/sync")1182 b.ReportMetric(float64(b.N)/syncs, "batches/sync")
1183 }1183 }
1184+
1185+func TestErr(t *testing.T) {
1186+ tests := []struct {
1187+ name string
1188+ stop func(t *testing.T, db *DB, fx *faultfs.FS)
1189+ want error
1190+ }{
1191+ {"running", func(*testing.T, *DB, *faultfs.FS) {}, nil},
1192+ {"closed", func(t *testing.T, db *DB, _ *faultfs.FS) {
1193+ if err := db.Close(bg); err != nil {
1194+ t.Fatalf("Close: %v", err)
1195+ }
1196+ }, ErrClosed},
1197+ {"failed", func(t *testing.T, db *DB, fx *faultfs.FS) {
1198+ fx.FailWrite(1, nil)
1199+ var b Batch
1200+ b.Put([]byte("k"), []byte("v"))
1201+ if _, err := db.Apply(bg, &b); !errors.Is(err, ErrFailed) {
1202+ t.Fatalf("Apply: %v, want ErrFailed", err)
1203+ }
1204+ }, ErrFailed},
1205+ }
1206+ for _, tt := range tests {
1207+ t.Run(tt.name, func(t *testing.T) {
1208+ fx := faultfs.New()
1209+ db := mustOpen(t, fx, Options{})
1210+ tt.stop(t, db, fx)
1211+ err := db.Err()
1212+ if tt.want == nil {
1213+ if err != nil {
1214+ t.Fatalf("Err = %v, want nil", err)
1215+ }
1216+ return
1217+ }
1218+ if !errors.Is(err, tt.want) {
1219+ t.Fatalf("Err = %v, want %v", err, tt.want)
1220+ }
1221+ })
1222+ }
1223+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 2

↑↓ moveenter openesc close