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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 242 | 242 | 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 | 250 | // stopped returns the failure (which wins over Close, so callers see a | |
| 246 | 251 | // stable error), ErrClosed, or nil while the database accepts batches. | |
| 247 | 252 | 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.err | 242 | 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 a | 250 | // 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) { | |||
| 1181 | 1181 | b.ReportMetric(syncs/secs, "syncs/s") | |
| 1182 | 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 | + | } | |
| @@ -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 | + | } | |||