fix(writer): allocate the Apply reply channel per call so tests can use synctest
The pooled request kept a reply channel created inside one synctest bubble, which crashed the next bubble that reused it. Apply now allocates its request and channel each call (about 3-5% on BenchmarkApply, small next to fdatasync). Timing-based tests (group window, context before dequeue, compaction, close, faultfs sync delay) now run in synctest bubbles instead of polling and sleeping.
- commit
- 21d0a80b779c2edfee0a91ec22b0ee93d96c38bd
- author
- brice · · 2026-10-11 18:37 UTC
- parent
- 5e09b48
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +487 -451
M compact_test.go +300 -283
| @@ -14,6 +14,7 @@ import ( | |||
| 14 | 14 | "sync" | |
| 15 | 15 | "sync/atomic" | |
| 16 | 16 | "testing" | |
| 17 | + | "testing/synctest" | |
| 17 | 18 | "time" | |
| 18 | 19 | ||
| 19 | 20 | "github.com/bnema/zerowrap" | |
| @@ -239,14 +240,34 @@ func settled(t testing.TB, fx fsys.FS, s uint64) bool { | |||
| 239 | 240 | return slices.Equal(snaps, []uint64{s}) && slices.Equal(logs, []uint64{s}) | |
| 240 | 241 | } | |
| 241 | 242 | ||
| 242 | - | func waitFor(t testing.TB, what string, cond func() bool) { | |
| 243 | + | // settle returns once every goroutine of the bubble is durably blocked: the | |
| 244 | + | // writer and any compactor have finished all the work they can do. It must be | |
| 245 | + | // called inside a synctest bubble. | |
| 246 | + | func settle() { synctest.Wait() } | |
| 247 | + | ||
| 248 | + | // applyUntil applies batches i, i+1, … until cond holds, settling after each | |
| 249 | + | // one so that a compaction the batch triggered has finished. It returns the | |
| 250 | + | // next unused i. | |
| 251 | + | func applyUntil(t testing.TB, db *DB, i int, what string, cond func() bool) int { | |
| 243 | 252 | t.Helper() | |
| 244 | - | deadline := time.Now().Add(10 * time.Second) | |
| 245 | - | for !cond() { | |
| 246 | - | if time.Now().After(deadline) { | |
| 247 | - | t.Fatalf("timeout waiting for %s", what) | |
| 253 | + | settle() | |
| 254 | + | for ; !cond(); i++ { | |
| 255 | + | if i > 1000 { | |
| 256 | + | t.Fatalf("%s did not happen", what) | |
| 248 | 257 | } | |
| 249 | - | time.Sleep(time.Millisecond) | |
| 258 | + | mustApply(t, db, i) | |
| 259 | + | settle() | |
| 260 | + | } | |
| 261 | + | return i | |
| 262 | + | } | |
| 263 | + | ||
| 264 | + | // failedDB reports whether the database is in the failed state. | |
| 265 | + | func failedDB(db *DB) bool { | |
| 266 | + | select { | |
| 267 | + | case <-db.failed: | |
| 268 | + | return true | |
| 269 | + | default: | |
| 270 | + | return false | |
| 250 | 271 | } | |
| 251 | 272 | } | |
| 252 | 273 | ||
| @@ -262,43 +283,48 @@ func logged(buf *syncBuffer, msg string) int { | |||
| 262 | 283 | } | |
| 263 | 284 | ||
| 264 | 285 | func TestCompactTrigger(t *testing.T) { | |
| 265 | - | fx := faultfs.New() | |
| 266 | - | ctx, logs := debugCtx() | |
| 267 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 268 | - | ||
| 269 | - | // Exactly CompactAfter bytes after the header: not above the threshold. | |
| 270 | - | for i := 1; i <= 5; i++ { | |
| 271 | - | mustApply(t, db, i) | |
| 272 | - | } | |
| 273 | - | barrier(t, db) | |
| 274 | - | if snaps, files := listFiles(t, fx); len(snaps) != 0 || !slices.Equal(files, []uint64{0}) { | |
| 275 | - | t.Fatalf("below the threshold: snapshots %v, logs %v", snaps, files) | |
| 276 | - | } | |
| 286 | + | synctest.Test(t, func(t *testing.T) { | |
| 287 | + | fx := faultfs.New() | |
| 288 | + | ctx, logs := debugCtx() | |
| 289 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 290 | + | ||
| 291 | + | // Exactly CompactAfter bytes after the header: not above the threshold. | |
| 292 | + | for i := 1; i <= 5; i++ { | |
| 293 | + | mustApply(t, db, i) | |
| 294 | + | } | |
| 295 | + | barrier(t, db) | |
| 296 | + | if snaps, files := listFiles(t, fx); len(snaps) != 0 || !slices.Equal(files, []uint64{0}) { | |
| 297 | + | t.Fatalf("below the threshold: snapshots %v, logs %v", snaps, files) | |
| 298 | + | } | |
| 277 | 299 | ||
| 278 | - | mustApply(t, db, 6) | |
| 279 | - | waitFor(t, "compaction", func() bool { return logged(logs, "compaction done") == 1 }) | |
| 280 | - | if !settled(t, fx, 6) { | |
| 281 | - | snaps, files := listFiles(t, fx) | |
| 282 | - | t.Fatalf("after compaction: snapshots %v, logs %v; want snapshot-6 and log-6 only", snaps, files) | |
| 283 | - | } | |
| 284 | - | if n := logged(logs, "log switch"); n != 1 { | |
| 285 | - | t.Fatalf("%d log switch messages, want 1", n) | |
| 286 | - | } | |
| 287 | - | for _, key := range []string{`"seq":6`, `"keys":6`, `"bytes":`, `"duration":`} { | |
| 288 | - | if !strings.Contains(logs.String(), key) { | |
| 289 | - | t.Errorf("compaction message lacks %s: %s", key, logs.String()) | |
| 300 | + | mustApply(t, db, 6) | |
| 301 | + | settle() // compaction: the background work has finished | |
| 302 | + | if logged(logs, "compaction done") != 1 { | |
| 303 | + | t.Fatalf("compaction did not happen: %s", logs.String()) | |
| 304 | + | } | |
| 305 | + | if !settled(t, fx, 6) { | |
| 306 | + | snaps, files := listFiles(t, fx) | |
| 307 | + | t.Fatalf("after compaction: snapshots %v, logs %v; want snapshot-6 and log-6 only", snaps, files) | |
| 308 | + | } | |
| 309 | + | if n := logged(logs, "log switch"); n != 1 { | |
| 310 | + | t.Fatalf("%d log switch messages, want 1", n) | |
| 311 | + | } | |
| 312 | + | for _, key := range []string{`"seq":6`, `"keys":6`, `"bytes":`, `"duration":`} { | |
| 313 | + | if !strings.Contains(logs.String(), key) { | |
| 314 | + | t.Errorf("compaction message lacks %s: %s", key, logs.String()) | |
| 315 | + | } | |
| 290 | 316 | } | |
| 291 | - | } | |
| 292 | 317 | ||
| 293 | - | // The new log takes the following batches; the old files stay gone. | |
| 294 | - | mustApply(t, db, 7) | |
| 295 | - | if err := db.Close(bg); err != nil { | |
| 296 | - | t.Fatal(err) | |
| 297 | - | } | |
| 298 | - | db = mustOpen(t, fx, Options{}) | |
| 299 | - | if n := mustCheck(t, db, 7); n != 7 { | |
| 300 | - | t.Fatalf("recovered %d batches, want 7", n) | |
| 301 | - | } | |
| 318 | + | // The new log takes the following batches; the old files stay gone. | |
| 319 | + | mustApply(t, db, 7) | |
| 320 | + | if err := db.Close(bg); err != nil { | |
| 321 | + | t.Fatal(err) | |
| 322 | + | } | |
| 323 | + | db = mustOpen(t, fx, Options{}) | |
| 324 | + | if n := mustCheck(t, db, 7); n != 7 { | |
| 325 | + | t.Fatalf("recovered %d batches, want 7", n) | |
| 326 | + | } | |
| 327 | + | }) | |
| 302 | 328 | } | |
| 303 | 329 | ||
| 304 | 330 | func TestCompactDisabled(t *testing.T) { | |
| @@ -314,63 +340,62 @@ func TestCompactDisabled(t *testing.T) { | |||
| 314 | 340 | } | |
| 315 | 341 | ||
| 316 | 342 | func TestCompactSingleFlight(t *testing.T) { | |
| 317 | - | fx := faultfs.New() | |
| 318 | - | started, release := make(chan struct{}), make(chan struct{}) | |
| 319 | - | var once sync.Once | |
| 320 | - | h := &hookFS{FS: fx, hook: func(op, path string) error { | |
| 321 | - | if op == "open" && strings.Contains(path, snapshotPrefix) { | |
| 322 | - | once.Do(func() { close(started); <-release }) | |
| 323 | - | } | |
| 324 | - | return nil | |
| 325 | - | }} | |
| 326 | - | ctx, logs := debugCtx() | |
| 327 | - | db := mustOpenCtx(t, ctx, h, compactOpts()) | |
| 328 | - | for i := 1; i <= 6; i++ { | |
| 329 | - | mustApply(t, db, i) | |
| 330 | - | } | |
| 331 | - | select { | |
| 332 | - | case <-started: | |
| 333 | - | case <-time.After(10 * time.Second): | |
| 334 | - | t.Fatal("the compactor did not start") | |
| 335 | - | } | |
| 343 | + | synctest.Test(t, func(t *testing.T) { | |
| 344 | + | fx := faultfs.New() | |
| 345 | + | started, release := make(chan struct{}), make(chan struct{}) | |
| 346 | + | var once sync.Once | |
| 347 | + | h := &hookFS{FS: fx, hook: func(op, path string) error { | |
| 348 | + | if op == "open" && strings.Contains(path, snapshotPrefix) { | |
| 349 | + | once.Do(func() { close(started); <-release }) | |
| 350 | + | } | |
| 351 | + | return nil | |
| 352 | + | }} | |
| 353 | + | ctx, logs := debugCtx() | |
| 354 | + | db := mustOpenCtx(t, ctx, h, compactOpts()) | |
| 355 | + | for i := 1; i <= 6; i++ { | |
| 356 | + | mustApply(t, db, i) | |
| 357 | + | } | |
| 358 | + | settle() // the compactor is parked in the hook | |
| 359 | + | select { | |
| 360 | + | case <-started: | |
| 361 | + | default: | |
| 362 | + | t.Fatal("the compactor did not start") | |
| 363 | + | } | |
| 336 | 364 | ||
| 337 | - | // The compactor is stuck. The writer is not: it goes on far past the | |
| 338 | - | // threshold without starting a second compaction. | |
| 339 | - | for i := 7; i <= 60; i++ { | |
| 340 | - | mustApply(t, db, i) | |
| 341 | - | } | |
| 342 | - | barrier(t, db) | |
| 343 | - | if snaps, files := listFiles(t, fx); len(snaps) != 0 || !slices.Equal(files, []uint64{0, 6}) { | |
| 344 | - | t.Fatalf("while compacting: snapshots %v, logs %v", snaps, files) | |
| 345 | - | } | |
| 346 | - | if n := logged(logs, "log switch"); n != 1 { | |
| 347 | - | t.Fatalf("%d log switches while a compaction was running, want 1", n) | |
| 348 | - | } | |
| 365 | + | // The compactor is stuck. The writer is not: it goes on far past the | |
| 366 | + | // threshold without starting a second compaction. | |
| 367 | + | for i := 7; i <= 60; i++ { | |
| 368 | + | mustApply(t, db, i) | |
| 369 | + | } | |
| 370 | + | barrier(t, db) | |
| 371 | + | if snaps, files := listFiles(t, fx); len(snaps) != 0 || !slices.Equal(files, []uint64{0, 6}) { | |
| 372 | + | t.Fatalf("while compacting: snapshots %v, logs %v", snaps, files) | |
| 373 | + | } | |
| 374 | + | if n := logged(logs, "log switch"); n != 1 { | |
| 375 | + | t.Fatalf("%d log switches while a compaction was running, want 1", n) | |
| 376 | + | } | |
| 349 | 377 | ||
| 350 | - | close(release) | |
| 351 | - | waitFor(t, "first compaction", func() bool { return logged(logs, "compaction done") == 1 }) | |
| 352 | - | // The log is still over the threshold: the next commit compacts again. | |
| 353 | - | i := 61 | |
| 354 | - | waitFor(t, "second compaction", func() bool { | |
| 355 | - | if logged(logs, "compaction done") >= 2 { | |
| 356 | - | return true | |
| 378 | + | close(release) | |
| 379 | + | settle() // first compaction: the background work has finished | |
| 380 | + | if logged(logs, "compaction done") != 1 { | |
| 381 | + | t.Fatalf("first compaction did not happen: %s", logs.String()) | |
| 357 | 382 | } | |
| 358 | - | mustApply(t, db, i) | |
| 359 | - | i++ | |
| 360 | - | return false | |
| 383 | + | // The log is still over the threshold: the next commit compacts again. | |
| 384 | + | i := 61 | |
| 385 | + | i = applyUntil(t, db, i, "second compaction", func() bool { return logged(logs, "compaction done") >= 2 }) | |
| 386 | + | if n := logged(logs, "log switch"); n != 2 { | |
| 387 | + | t.Fatalf("%d log switches, want 2", n) | |
| 388 | + | } | |
| 389 | + | if err := db.Close(bg); err != nil { | |
| 390 | + | t.Fatal(err) | |
| 391 | + | } | |
| 392 | + | snaps, files := listFiles(t, fx) | |
| 393 | + | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |
| 394 | + | t.Fatalf("after the second compaction: snapshots %v, logs %v", snaps, files) | |
| 395 | + | } | |
| 396 | + | db = mustOpen(t, fx, Options{}) | |
| 397 | + | mustCheck(t, db, i-1) | |
| 361 | 398 | }) | |
| 362 | - | if n := logged(logs, "log switch"); n != 2 { | |
| 363 | - | t.Fatalf("%d log switches, want 2", n) | |
| 364 | - | } | |
| 365 | - | if err := db.Close(bg); err != nil { | |
| 366 | - | t.Fatal(err) | |
| 367 | - | } | |
| 368 | - | snaps, files := listFiles(t, fx) | |
| 369 | - | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |
| 370 | - | t.Fatalf("after the second compaction: snapshots %v, logs %v", snaps, files) | |
| 371 | - | } | |
| 372 | - | db = mustOpen(t, fx, Options{}) | |
| 373 | - | mustCheck(t, db, i-1) | |
| 374 | 399 | } | |
| 375 | 400 | ||
| 376 | 401 | func TestCompactRemovesOldFiles(t *testing.T) { | |
| @@ -460,76 +485,72 @@ var compactorFaults = []struct { | |||
| 460 | 485 | func TestCompactorFailure(t *testing.T) { | |
| 461 | 486 | for _, f := range compactorFaults { | |
| 462 | 487 | t.Run(f.name, func(t *testing.T) { | |
| 463 | - | fx := faultfs.New() | |
| 464 | - | ctx, logs := debugCtx() | |
| 465 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 466 | - | for i := 1; i <= 5; i++ { | |
| 467 | - | mustApply(t, db, i) | |
| 468 | - | } | |
| 469 | - | f.inject(fx) | |
| 470 | - | mustApply(t, db, 6) | |
| 471 | - | waitFor(t, "compaction failure", func() bool { return logged(logs, "compaction failed") == 1 }) | |
| 472 | - | if !strings.Contains(logs.String(), `"level":"warn"`) { | |
| 473 | - | t.Errorf("failure not logged as a warning: %s", logs.String()) | |
| 474 | - | } | |
| 475 | - | mustApply(t, db, 7) // the database is not failed | |
| 476 | - | if err := db.Close(bg); err != nil { | |
| 477 | - | t.Fatalf("Close: %v", err) | |
| 478 | - | } | |
| 488 | + | synctest.Test(t, func(t *testing.T) { | |
| 489 | + | fx := faultfs.New() | |
| 490 | + | ctx, logs := debugCtx() | |
| 491 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 492 | + | for i := 1; i <= 5; i++ { | |
| 493 | + | mustApply(t, db, i) | |
| 494 | + | } | |
| 495 | + | f.inject(fx) | |
| 496 | + | mustApply(t, db, 6) | |
| 497 | + | settle() // compaction failure: the background work has finished | |
| 498 | + | if logged(logs, "compaction failed") != 1 { | |
| 499 | + | t.Fatalf("compaction failure did not happen: %s", logs.String()) | |
| 500 | + | } | |
| 501 | + | if !strings.Contains(logs.String(), `"level":"warn"`) { | |
| 502 | + | t.Errorf("failure not logged as a warning: %s", logs.String()) | |
| 503 | + | } | |
| 504 | + | mustApply(t, db, 7) // the database is not failed | |
| 505 | + | if err := db.Close(bg); err != nil { | |
| 506 | + | t.Fatalf("Close: %v", err) | |
| 507 | + | } | |
| 479 | 508 | ||
| 480 | - | // Reopen the partial state: it recovers and compacts again. | |
| 481 | - | ctx, logs = debugCtx() | |
| 482 | - | db = mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 483 | - | mustCheck(t, db, 7) | |
| 484 | - | i := 8 | |
| 485 | - | waitFor(t, "retried compaction", func() bool { | |
| 486 | - | if logged(logs, "compaction done") >= 1 { | |
| 487 | - | return true | |
| 509 | + | // Reopen the partial state: it recovers and compacts again. | |
| 510 | + | ctx, logs = debugCtx() | |
| 511 | + | db = mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 512 | + | mustCheck(t, db, 7) | |
| 513 | + | i := 8 | |
| 514 | + | i = applyUntil(t, db, i, "retried compaction", func() bool { return logged(logs, "compaction done") >= 1 }) | |
| 515 | + | if err := db.Close(bg); err != nil { | |
| 516 | + | t.Fatal(err) | |
| 488 | 517 | } | |
| 489 | - | mustApply(t, db, i) | |
| 490 | - | i++ | |
| 491 | - | return false | |
| 518 | + | snaps, files := listFiles(t, fx) | |
| 519 | + | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |
| 520 | + | t.Fatalf("after the retry: snapshots %v, logs %v", snaps, files) | |
| 521 | + | } | |
| 522 | + | db = mustOpen(t, fx, Options{}) | |
| 523 | + | mustCheck(t, db, i-1) | |
| 492 | 524 | }) | |
| 493 | - | if err := db.Close(bg); err != nil { | |
| 494 | - | t.Fatal(err) | |
| 495 | - | } | |
| 496 | - | snaps, files := listFiles(t, fx) | |
| 497 | - | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |
| 498 | - | t.Fatalf("after the retry: snapshots %v, logs %v", snaps, files) | |
| 499 | - | } | |
| 500 | - | db = mustOpen(t, fx, Options{}) | |
| 501 | - | mustCheck(t, db, i-1) | |
| 502 | 525 | }) | |
| 503 | 526 | } | |
| 504 | 527 | } | |
| 505 | 528 | ||
| 506 | 529 | // TestCompactorRetryWithoutReopen checks the retry in the same process. | |
| 507 | 530 | func TestCompactorRetryWithoutReopen(t *testing.T) { | |
| 508 | - | fx := faultfs.New() | |
| 509 | - | ctx, logs := debugCtx() | |
| 510 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 511 | - | for i := 1; i <= 5; i++ { | |
| 512 | - | mustApply(t, db, i) | |
| 513 | - | } | |
| 514 | - | fx.FailOp(faultfs.OpRename, 2, nil) // the snapshot | |
| 515 | - | mustApply(t, db, 6) | |
| 516 | - | waitFor(t, "compaction failure", func() bool { return logged(logs, "compaction failed") == 1 }) | |
| 517 | - | i := 7 | |
| 518 | - | waitFor(t, "retried compaction", func() bool { | |
| 519 | - | if logged(logs, "compaction done") >= 1 { | |
| 520 | - | return true | |
| 531 | + | synctest.Test(t, func(t *testing.T) { | |
| 532 | + | fx := faultfs.New() | |
| 533 | + | ctx, logs := debugCtx() | |
| 534 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 535 | + | for i := 1; i <= 5; i++ { | |
| 536 | + | mustApply(t, db, i) | |
| 537 | + | } | |
| 538 | + | fx.FailOp(faultfs.OpRename, 2, nil) // the snapshot | |
| 539 | + | mustApply(t, db, 6) | |
| 540 | + | settle() // compaction failure: the background work has finished | |
| 541 | + | if logged(logs, "compaction failed") != 1 { | |
| 542 | + | t.Fatalf("compaction failure did not happen: %s", logs.String()) | |
| 543 | + | } | |
| 544 | + | i := 7 | |
| 545 | + | i = applyUntil(t, db, i, "retried compaction", func() bool { return logged(logs, "compaction done") >= 1 }) | |
| 546 | + | if err := db.Close(bg); err != nil { | |
| 547 | + | t.Fatal(err) | |
| 548 | + | } | |
| 549 | + | snaps, files := listFiles(t, fx) | |
| 550 | + | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |
| 551 | + | t.Fatalf("snapshots %v, logs %v", snaps, files) | |
| 521 | 552 | } | |
| 522 | - | mustApply(t, db, i) | |
| 523 | - | i++ | |
| 524 | - | return false | |
| 525 | 553 | }) | |
| 526 | - | if err := db.Close(bg); err != nil { | |
| 527 | - | t.Fatal(err) | |
| 528 | - | } | |
| 529 | - | snaps, files := listFiles(t, fx) | |
| 530 | - | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |
| 531 | - | t.Fatalf("snapshots %v, logs %v", snaps, files) | |
| 532 | - | } | |
| 533 | 554 | } | |
| 534 | 555 | ||
| 535 | 556 | // switchFaults fail the creation of log-6 in the writer. The numbering is | |
| @@ -551,49 +572,44 @@ var switchFaults = []struct { | |||
| 551 | 572 | func TestLogSwitchFailure(t *testing.T) { | |
| 552 | 573 | for _, f := range switchFaults { | |
| 553 | 574 | t.Run(f.name, func(t *testing.T) { | |
| 554 | - | fx := faultfs.New() | |
| 555 | - | ctx, logs := debugCtx() | |
| 556 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 557 | - | for i := 1; i <= 5; i++ { | |
| 558 | - | mustApply(t, db, i) | |
| 559 | - | } | |
| 560 | - | f.inject(fx) | |
| 561 | - | mustApply(t, db, 6) | |
| 562 | - | barrier(t, db) | |
| 563 | - | if logged(logs, "log switch failed") != 1 { | |
| 564 | - | t.Fatalf("switch failure not logged: %s", logs.String()) | |
| 565 | - | } | |
| 566 | - | snaps, files := listFiles(t, fx) | |
| 567 | - | if len(snaps) != 0 || !slices.Equal(files, []uint64{0}) { | |
| 568 | - | t.Fatalf("after the failure: snapshots %v, logs %v", snaps, files) | |
| 569 | - | } | |
| 570 | - | names, _ := fx.ReadDir(testDir) | |
| 571 | - | for _, n := range names { | |
| 572 | - | if strings.HasSuffix(n, tmpSuffix) { | |
| 573 | - | t.Errorf("leftover %s", n) | |
| 575 | + | synctest.Test(t, func(t *testing.T) { | |
| 576 | + | fx := faultfs.New() | |
| 577 | + | ctx, logs := debugCtx() | |
| 578 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 579 | + | for i := 1; i <= 5; i++ { | |
| 580 | + | mustApply(t, db, i) | |
| 581 | + | } | |
| 582 | + | f.inject(fx) | |
| 583 | + | mustApply(t, db, 6) | |
| 584 | + | barrier(t, db) | |
| 585 | + | if logged(logs, "log switch failed") != 1 { | |
| 586 | + | t.Fatalf("switch failure not logged: %s", logs.String()) | |
| 587 | + | } | |
| 588 | + | snaps, files := listFiles(t, fx) | |
| 589 | + | if len(snaps) != 0 || !slices.Equal(files, []uint64{0}) { | |
| 590 | + | t.Fatalf("after the failure: snapshots %v, logs %v", snaps, files) | |
| 591 | + | } | |
| 592 | + | names, _ := fx.ReadDir(testDir) | |
| 593 | + | for _, n := range names { | |
| 594 | + | if strings.HasSuffix(n, tmpSuffix) { | |
| 595 | + | t.Errorf("leftover %s", n) | |
| 596 | + | } | |
| 574 | 597 | } | |
| 575 | - | } | |
| 576 | 598 | ||
| 577 | - | // No retry on every commit: a failure costs another CompactAfter bytes. | |
| 578 | - | mustApply(t, db, 7) | |
| 579 | - | barrier(t, db) | |
| 580 | - | if n := logged(logs, "log switch failed"); n != 1 { | |
| 581 | - | t.Fatalf("%d switch attempts for 2 commits, want 1", n) | |
| 582 | - | } | |
| 583 | - | i := 8 | |
| 584 | - | waitFor(t, "compaction after the failed switch", func() bool { | |
| 585 | - | if logged(logs, "compaction done") >= 1 { | |
| 586 | - | return true | |
| 599 | + | // No retry on every commit: a failure costs another CompactAfter bytes. | |
| 600 | + | mustApply(t, db, 7) | |
| 601 | + | barrier(t, db) | |
| 602 | + | if n := logged(logs, "log switch failed"); n != 1 { | |
| 603 | + | t.Fatalf("%d switch attempts for 2 commits, want 1", n) | |
| 604 | + | } | |
| 605 | + | i := 8 | |
| 606 | + | i = applyUntil(t, db, i, "compaction after the failed switch", func() bool { return logged(logs, "compaction done") >= 1 }) | |
| 607 | + | if err := db.Close(bg); err != nil { | |
| 608 | + | t.Fatal(err) | |
| 587 | 609 | } | |
| 588 | - | mustApply(t, db, i) | |
| 589 | - | i++ | |
| 590 | - | return false | |
| 610 | + | db = mustOpen(t, fx, Options{}) | |
| 611 | + | mustCheck(t, db, i-1) | |
| 591 | 612 | }) | |
| 592 | - | if err := db.Close(bg); err != nil { | |
| 593 | - | t.Fatal(err) | |
| 594 | - | } | |
| 595 | - | db = mustOpen(t, fx, Options{}) | |
| 596 | - | mustCheck(t, db, i-1) | |
| 597 | 613 | }) | |
| 598 | 614 | } | |
| 599 | 615 | } | |
| @@ -613,35 +629,33 @@ func TestLogSwitchFailsDatabase(t *testing.T) { | |||
| 613 | 629 | } | |
| 614 | 630 | for _, c := range cases { | |
| 615 | 631 | t.Run(c.name, func(t *testing.T) { | |
| 616 | - | fx := faultfs.New() | |
| 617 | - | ctx, logs := debugCtx() | |
| 618 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 619 | - | for i := 1; i <= 5; i++ { | |
| 620 | - | mustApply(t, db, i) | |
| 621 | - | } | |
| 622 | - | c.inject(fx) | |
| 623 | - | mustApply(t, db, 6) // acknowledged: the failure comes after the reply | |
| 624 | - | waitFor(t, "failed state", func() bool { | |
| 625 | - | select { | |
| 626 | - | case <-db.failed: | |
| 627 | - | return true | |
| 628 | - | default: | |
| 629 | - | return false | |
| 632 | + | synctest.Test(t, func(t *testing.T) { | |
| 633 | + | fx := faultfs.New() | |
| 634 | + | ctx, logs := debugCtx() | |
| 635 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 636 | + | for i := 1; i <= 5; i++ { | |
| 637 | + | mustApply(t, db, i) | |
| 638 | + | } | |
| 639 | + | c.inject(fx) | |
| 640 | + | mustApply(t, db, 6) // acknowledged: the failure comes after the reply | |
| 641 | + | settle() // the writer has seen the failure | |
| 642 | + | if !failedDB(db) { | |
| 643 | + | t.Fatal("the database did not fail") | |
| 644 | + | } | |
| 645 | + | var b Batch | |
| 646 | + | b.Put([]byte("later"), nil) | |
| 647 | + | if _, err := db.Apply(bg, &b); !errors.Is(err, ErrFailed) { | |
| 648 | + | t.Fatalf("Apply after the failure: %v", err) | |
| 649 | + | } | |
| 650 | + | if !strings.Contains(logs.String(), "database failed") { | |
| 651 | + | t.Errorf("failure not logged: %s", logs.String()) | |
| 630 | 652 | } | |
| 653 | + | if err := db.Close(bg); !errors.Is(err, ErrFailed) { | |
| 654 | + | t.Fatalf("Close = %v, want the failure", err) | |
| 655 | + | } | |
| 656 | + | db = mustOpen(t, fx, Options{}) | |
| 657 | + | mustCheck(t, db, 6) | |
| 631 | 658 | }) | |
| 632 | - | var b Batch | |
| 633 | - | b.Put([]byte("later"), nil) | |
| 634 | - | if _, err := db.Apply(bg, &b); !errors.Is(err, ErrFailed) { | |
| 635 | - | t.Fatalf("Apply after the failure: %v", err) | |
| 636 | - | } | |
| 637 | - | if !strings.Contains(logs.String(), "database failed") { | |
| 638 | - | t.Errorf("failure not logged: %s", logs.String()) | |
| 639 | - | } | |
| 640 | - | if err := db.Close(bg); !errors.Is(err, ErrFailed) { | |
| 641 | - | t.Fatalf("Close = %v, want the failure", err) | |
| 642 | - | } | |
| 643 | - | db = mustOpen(t, fx, Options{}) | |
| 644 | - | mustCheck(t, db, 6) | |
| 645 | 659 | }) | |
| 646 | 660 | } | |
| 647 | 661 | } | |
| @@ -652,79 +666,82 @@ func TestLogSwitchFailsDatabase(t *testing.T) { | |||
| 652 | 666 | // sync are gone; the database must fail rather than continue, and what is | |
| 653 | 667 | // left must open as a prefix. | |
| 654 | 668 | func TestLogSwitchSyncFailureDropsUnsyncedData(t *testing.T) { | |
| 655 | - | fx := faultfs.New() | |
| 656 | - | fx.DropOnSyncFailure(true) | |
| 657 | - | opts := compactOpts() | |
| 658 | - | opts.NoSync = true | |
| 659 | - | db := mustOpen(t, fx, opts) | |
| 660 | - | for i := 1; i <= 5; i++ { | |
| 661 | - | mustApply(t, db, i) | |
| 662 | - | } | |
| 663 | - | fx.FailSync(1, nil) | |
| 664 | - | mustApply(t, db, 6) | |
| 665 | - | waitFor(t, "failed state", func() bool { | |
| 666 | - | select { | |
| 667 | - | case <-db.failed: | |
| 668 | - | return true | |
| 669 | - | default: | |
| 670 | - | return false | |
| 669 | + | synctest.Test(t, func(t *testing.T) { | |
| 670 | + | fx := faultfs.New() | |
| 671 | + | fx.DropOnSyncFailure(true) | |
| 672 | + | opts := compactOpts() | |
| 673 | + | opts.NoSync = true | |
| 674 | + | db := mustOpen(t, fx, opts) | |
| 675 | + | for i := 1; i <= 5; i++ { | |
| 676 | + | mustApply(t, db, i) | |
| 677 | + | } | |
| 678 | + | fx.FailSync(1, nil) | |
| 679 | + | mustApply(t, db, 6) | |
| 680 | + | settle() // the writer has seen the failure | |
| 681 | + | if !failedDB(db) { | |
| 682 | + | t.Fatal("the database did not fail") | |
| 683 | + | } | |
| 684 | + | if err := db.Close(bg); !errors.Is(err, ErrFailed) { | |
| 685 | + | t.Fatalf("Close = %v, want the failure", err) | |
| 686 | + | } | |
| 687 | + | db = mustOpen(t, fx, Options{}) | |
| 688 | + | if n := mustCheck(t, db, 0); n != 0 { | |
| 689 | + | t.Fatalf("recovered %d batches, want 0 (all were dropped)", n) | |
| 671 | 690 | } | |
| 672 | 691 | }) | |
| 673 | - | if err := db.Close(bg); !errors.Is(err, ErrFailed) { | |
| 674 | - | t.Fatalf("Close = %v, want the failure", err) | |
| 675 | - | } | |
| 676 | - | db = mustOpen(t, fx, Options{}) | |
| 677 | - | if n := mustCheck(t, db, 0); n != 0 { | |
| 678 | - | t.Fatalf("recovered %d batches, want 0 (all were dropped)", n) | |
| 679 | - | } | |
| 680 | 692 | } | |
| 681 | 693 | ||
| 682 | 694 | // TestCloseWaitsForCompaction checks that Close does not cancel a running | |
| 683 | 695 | // compaction, waits for it, and leaves a directory that opens. | |
| 684 | 696 | func TestCloseWaitsForCompaction(t *testing.T) { | |
| 685 | - | fx := faultfs.New() | |
| 686 | - | started, release := make(chan struct{}), make(chan struct{}) | |
| 687 | - | var once sync.Once | |
| 688 | - | h := &hookFS{FS: fx, hook: func(op, path string) error { | |
| 689 | - | if op == "open" && strings.Contains(path, snapshotPrefix) { | |
| 690 | - | once.Do(func() { close(started); <-release }) | |
| 691 | - | } | |
| 692 | - | return nil | |
| 693 | - | }} | |
| 694 | - | ctx, logs := debugCtx() | |
| 695 | - | db := mustOpenCtx(t, ctx, h, compactOpts()) | |
| 696 | - | for i := 1; i <= 6; i++ { | |
| 697 | - | mustApply(t, db, i) | |
| 698 | - | } | |
| 699 | - | select { | |
| 700 | - | case <-started: | |
| 701 | - | case <-time.After(10 * time.Second): | |
| 702 | - | t.Fatal("the compactor did not start") | |
| 703 | - | } | |
| 704 | - | closed := make(chan error, 1) | |
| 705 | - | go func() { closed <- db.Close(bg) }() | |
| 706 | - | select { | |
| 707 | - | case err := <-closed: | |
| 708 | - | t.Fatalf("Close returned during the compaction: %v", err) | |
| 709 | - | case <-time.After(100 * time.Millisecond): | |
| 710 | - | } | |
| 711 | - | if _, err := db.Apply(bg, new(Batch)); !errors.Is(err, ErrClosed) { | |
| 712 | - | t.Fatalf("Apply while closing: %v", err) | |
| 713 | - | } | |
| 714 | - | close(release) | |
| 715 | - | select { | |
| 716 | - | case err := <-closed: | |
| 717 | - | if err != nil { | |
| 718 | - | t.Fatalf("Close: %v", err) | |
| 697 | + | synctest.Test(t, func(t *testing.T) { | |
| 698 | + | fx := faultfs.New() | |
| 699 | + | started, release := make(chan struct{}), make(chan struct{}) | |
| 700 | + | var once sync.Once | |
| 701 | + | h := &hookFS{FS: fx, hook: func(op, path string) error { | |
| 702 | + | if op == "open" && strings.Contains(path, snapshotPrefix) { | |
| 703 | + | once.Do(func() { close(started); <-release }) | |
| 704 | + | } | |
| 705 | + | return nil | |
| 706 | + | }} | |
| 707 | + | ctx, logs := debugCtx() | |
| 708 | + | db := mustOpenCtx(t, ctx, h, compactOpts()) | |
| 709 | + | for i := 1; i <= 6; i++ { | |
| 710 | + | mustApply(t, db, i) | |
| 719 | 711 | } | |
| 720 | - | case <-time.After(10 * time.Second): | |
| 721 | - | t.Fatal("Close did not return after the compaction was released") | |
| 722 | - | } | |
| 723 | - | if logged(logs, "compaction done") != 1 || !settled(t, fx, 6) { | |
| 724 | - | t.Fatalf("the compaction did not complete: %s", logs.String()) | |
| 725 | - | } | |
| 726 | - | db = mustOpen(t, fx, Options{}) | |
| 727 | - | mustCheck(t, db, 6) | |
| 712 | + | settle() // the compactor is parked in the hook | |
| 713 | + | select { | |
| 714 | + | case <-started: | |
| 715 | + | default: | |
| 716 | + | t.Fatal("the compactor did not start") | |
| 717 | + | } | |
| 718 | + | closed := make(chan error, 1) | |
| 719 | + | go func() { closed <- db.Close(bg) }() | |
| 720 | + | settle() // Close is blocked waiting for the compactor, which is parked in the hook | |
| 721 | + | select { | |
| 722 | + | case err := <-closed: | |
| 723 | + | t.Fatalf("Close returned during the compaction: %v", err) | |
| 724 | + | default: | |
| 725 | + | } | |
| 726 | + | if _, err := db.Apply(bg, new(Batch)); !errors.Is(err, ErrClosed) { | |
| 727 | + | t.Fatalf("Apply while closing: %v", err) | |
| 728 | + | } | |
| 729 | + | close(release) | |
| 730 | + | settle() // the compaction finishes and Close returns | |
| 731 | + | select { | |
| 732 | + | case err := <-closed: | |
| 733 | + | if err != nil { | |
| 734 | + | t.Fatalf("Close: %v", err) | |
| 735 | + | } | |
| 736 | + | default: | |
| 737 | + | t.Fatal("Close did not return after the compaction was released") | |
| 738 | + | } | |
| 739 | + | if logged(logs, "compaction done") != 1 || !settled(t, fx, 6) { | |
| 740 | + | t.Fatalf("the compaction did not complete: %s", logs.String()) | |
| 741 | + | } | |
| 742 | + | db = mustOpen(t, fx, Options{}) | |
| 743 | + | mustCheck(t, db, 6) | |
| 744 | + | }) | |
| 728 | 745 | } | |
| 729 | 746 | ||
| 730 | 747 | // TestCloseDuringCompaction (AC9) closes the database while writers run and | |
| @@ -14,6 +14,7 @@ import ( | |||||
| 14 | "sync" | 14 | "sync" | ||
| 15 | "sync/atomic" | 15 | "sync/atomic" | ||
| 16 | "testing" | 16 | "testing" | ||
| 17 | + | "testing/synctest" | |||
| 17 | "time" | 18 | "time" | ||
| 18 | 19 | ||||
| 19 | "github.com/bnema/zerowrap" | 20 | "github.com/bnema/zerowrap" | ||
| @@ -239,14 +240,34 @@ func settled(t testing.TB, fx fsys.FS, s uint64) bool { | |||||
| 239 | return slices.Equal(snaps, []uint64{s}) && slices.Equal(logs, []uint64{s}) | 240 | return slices.Equal(snaps, []uint64{s}) && slices.Equal(logs, []uint64{s}) | ||
| 240 | } | 241 | } | ||
| 241 | 242 | ||||
| 242 | - | func waitFor(t testing.TB, what string, cond func() bool) { | 243 | + | // settle returns once every goroutine of the bubble is durably blocked: the |
| 244 | + | // writer and any compactor have finished all the work they can do. It must be | |||
| 245 | + | // called inside a synctest bubble. | |||
| 246 | + | func settle() { synctest.Wait() } | |||
| 247 | + | ||||
| 248 | + | // applyUntil applies batches i, i+1, … until cond holds, settling after each | |||
| 249 | + | // one so that a compaction the batch triggered has finished. It returns the | |||
| 250 | + | // next unused i. | |||
| 251 | + | func applyUntil(t testing.TB, db *DB, i int, what string, cond func() bool) int { | |||
| 243 | t.Helper() | 252 | t.Helper() | ||
| 244 | - | deadline := time.Now().Add(10 * time.Second) | 253 | + | settle() |
| 245 | - | for !cond() { | 254 | + | for ; !cond(); i++ { |
| 246 | - | if time.Now().After(deadline) { | 255 | + | if i > 1000 { |
| 247 | - | t.Fatalf("timeout waiting for %s", what) | 256 | + | t.Fatalf("%s did not happen", what) |
| 248 | } | 257 | } | ||
| 249 | - | time.Sleep(time.Millisecond) | 258 | + | mustApply(t, db, i) |
| 259 | + | settle() | |||
| 260 | + | } | |||
| 261 | + | return i | |||
| 262 | + | } | |||
| 263 | + | ||||
| 264 | + | // failedDB reports whether the database is in the failed state. | |||
| 265 | + | func failedDB(db *DB) bool { | |||
| 266 | + | select { | |||
| 267 | + | case <-db.failed: | |||
| 268 | + | return true | |||
| 269 | + | default: | |||
| 270 | + | return false | |||
| 250 | } | 271 | } | ||
| 251 | } | 272 | } | ||
| 252 | 273 | ||||
| @@ -262,43 +283,48 @@ func logged(buf *syncBuffer, msg string) int { | |||||
| 262 | } | 283 | } | ||
| 263 | 284 | ||||
| 264 | func TestCompactTrigger(t *testing.T) { | 285 | func TestCompactTrigger(t *testing.T) { | ||
| 265 | - | fx := faultfs.New() | 286 | + | synctest.Test(t, func(t *testing.T) { |
| 266 | - | ctx, logs := debugCtx() | 287 | + | fx := faultfs.New() |
| 267 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | 288 | + | ctx, logs := debugCtx() |
| 268 | - | 289 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) | |
| 269 | - | // Exactly CompactAfter bytes after the header: not above the threshold. | 290 | + | |
| 270 | - | for i := 1; i <= 5; i++ { | 291 | + | // Exactly CompactAfter bytes after the header: not above the threshold. |
| 271 | - | mustApply(t, db, i) | 292 | + | for i := 1; i <= 5; i++ { |
| 272 | - | } | 293 | + | mustApply(t, db, i) |
| 273 | - | barrier(t, db) | 294 | + | } |
| 274 | - | if snaps, files := listFiles(t, fx); len(snaps) != 0 || !slices.Equal(files, []uint64{0}) { | 295 | + | barrier(t, db) |
| 275 | - | t.Fatalf("below the threshold: snapshots %v, logs %v", snaps, files) | 296 | + | if snaps, files := listFiles(t, fx); len(snaps) != 0 || !slices.Equal(files, []uint64{0}) { |
| 276 | - | } | 297 | + | t.Fatalf("below the threshold: snapshots %v, logs %v", snaps, files) |
| 298 | + | } | |||
| 277 | 299 | ||||
| 278 | - | mustApply(t, db, 6) | 300 | + | mustApply(t, db, 6) |
| 279 | - | waitFor(t, "compaction", func() bool { return logged(logs, "compaction done") == 1 }) | 301 | + | settle() // compaction: the background work has finished |
| 280 | - | if !settled(t, fx, 6) { | 302 | + | if logged(logs, "compaction done") != 1 { |
| 281 | - | snaps, files := listFiles(t, fx) | 303 | + | t.Fatalf("compaction did not happen: %s", logs.String()) |
| 282 | - | t.Fatalf("after compaction: snapshots %v, logs %v; want snapshot-6 and log-6 only", snaps, files) | 304 | + | } |
| 283 | - | } | 305 | + | if !settled(t, fx, 6) { |
| 284 | - | if n := logged(logs, "log switch"); n != 1 { | 306 | + | snaps, files := listFiles(t, fx) |
| 285 | - | t.Fatalf("%d log switch messages, want 1", n) | 307 | + | t.Fatalf("after compaction: snapshots %v, logs %v; want snapshot-6 and log-6 only", snaps, files) |
| 286 | - | } | 308 | + | } |
| 287 | - | for _, key := range []string{`"seq":6`, `"keys":6`, `"bytes":`, `"duration":`} { | 309 | + | if n := logged(logs, "log switch"); n != 1 { |
| 288 | - | if !strings.Contains(logs.String(), key) { | 310 | + | t.Fatalf("%d log switch messages, want 1", n) |
| 289 | - | t.Errorf("compaction message lacks %s: %s", key, logs.String()) | 311 | + | } |
| 312 | + | for _, key := range []string{`"seq":6`, `"keys":6`, `"bytes":`, `"duration":`} { | |||
| 313 | + | if !strings.Contains(logs.String(), key) { | |||
| 314 | + | t.Errorf("compaction message lacks %s: %s", key, logs.String()) | |||
| 315 | + | } | |||
| 290 | } | 316 | } | ||
| 291 | - | } | |||
| 292 | 317 | ||||
| 293 | - | // The new log takes the following batches; the old files stay gone. | 318 | + | // The new log takes the following batches; the old files stay gone. |
| 294 | - | mustApply(t, db, 7) | 319 | + | mustApply(t, db, 7) |
| 295 | - | if err := db.Close(bg); err != nil { | 320 | + | if err := db.Close(bg); err != nil { |
| 296 | - | t.Fatal(err) | 321 | + | t.Fatal(err) |
| 297 | - | } | 322 | + | } |
| 298 | - | db = mustOpen(t, fx, Options{}) | 323 | + | db = mustOpen(t, fx, Options{}) |
| 299 | - | if n := mustCheck(t, db, 7); n != 7 { | 324 | + | if n := mustCheck(t, db, 7); n != 7 { |
| 300 | - | t.Fatalf("recovered %d batches, want 7", n) | 325 | + | t.Fatalf("recovered %d batches, want 7", n) |
| 301 | - | } | 326 | + | } |
| 327 | + | }) | |||
| 302 | } | 328 | } | ||
| 303 | 329 | ||||
| 304 | func TestCompactDisabled(t *testing.T) { | 330 | func TestCompactDisabled(t *testing.T) { | ||
| @@ -314,63 +340,62 @@ func TestCompactDisabled(t *testing.T) { | |||||
| 314 | } | 340 | } | ||
| 315 | 341 | ||||
| 316 | func TestCompactSingleFlight(t *testing.T) { | 342 | func TestCompactSingleFlight(t *testing.T) { | ||
| 317 | - | fx := faultfs.New() | 343 | + | synctest.Test(t, func(t *testing.T) { |
| 318 | - | started, release := make(chan struct{}), make(chan struct{}) | 344 | + | fx := faultfs.New() |
| 319 | - | var once sync.Once | 345 | + | started, release := make(chan struct{}), make(chan struct{}) |
| 320 | - | h := &hookFS{FS: fx, hook: func(op, path string) error { | 346 | + | var once sync.Once |
| 321 | - | if op == "open" && strings.Contains(path, snapshotPrefix) { | 347 | + | h := &hookFS{FS: fx, hook: func(op, path string) error { |
| 322 | - | once.Do(func() { close(started); <-release }) | 348 | + | if op == "open" && strings.Contains(path, snapshotPrefix) { |
| 323 | - | } | 349 | + | once.Do(func() { close(started); <-release }) |
| 324 | - | return nil | 350 | + | } |
| 325 | - | }} | 351 | + | return nil |
| 326 | - | ctx, logs := debugCtx() | 352 | + | }} |
| 327 | - | db := mustOpenCtx(t, ctx, h, compactOpts()) | 353 | + | ctx, logs := debugCtx() |
| 328 | - | for i := 1; i <= 6; i++ { | 354 | + | db := mustOpenCtx(t, ctx, h, compactOpts()) |
| 329 | - | mustApply(t, db, i) | 355 | + | for i := 1; i <= 6; i++ { |
| 330 | - | } | 356 | + | mustApply(t, db, i) |
| 331 | - | select { | 357 | + | } |
| 332 | - | case <-started: | 358 | + | settle() // the compactor is parked in the hook |
| 333 | - | case <-time.After(10 * time.Second): | 359 | + | select { |
| 334 | - | t.Fatal("the compactor did not start") | 360 | + | case <-started: |
| 335 | - | } | 361 | + | default: |
| 362 | + | t.Fatal("the compactor did not start") | |||
| 363 | + | } | |||
| 336 | 364 | ||||
| 337 | - | // The compactor is stuck. The writer is not: it goes on far past the | 365 | + | // The compactor is stuck. The writer is not: it goes on far past the |
| 338 | - | // threshold without starting a second compaction. | 366 | + | // threshold without starting a second compaction. |
| 339 | - | for i := 7; i <= 60; i++ { | 367 | + | for i := 7; i <= 60; i++ { |
| 340 | - | mustApply(t, db, i) | 368 | + | mustApply(t, db, i) |
| 341 | - | } | 369 | + | } |
| 342 | - | barrier(t, db) | 370 | + | barrier(t, db) |
| 343 | - | if snaps, files := listFiles(t, fx); len(snaps) != 0 || !slices.Equal(files, []uint64{0, 6}) { | 371 | + | if snaps, files := listFiles(t, fx); len(snaps) != 0 || !slices.Equal(files, []uint64{0, 6}) { |
| 344 | - | t.Fatalf("while compacting: snapshots %v, logs %v", snaps, files) | 372 | + | t.Fatalf("while compacting: snapshots %v, logs %v", snaps, files) |
| 345 | - | } | 373 | + | } |
| 346 | - | if n := logged(logs, "log switch"); n != 1 { | 374 | + | if n := logged(logs, "log switch"); n != 1 { |
| 347 | - | t.Fatalf("%d log switches while a compaction was running, want 1", n) | 375 | + | t.Fatalf("%d log switches while a compaction was running, want 1", n) |
| 348 | - | } | 376 | + | } |
| 349 | 377 | ||||
| 350 | - | close(release) | 378 | + | close(release) |
| 351 | - | waitFor(t, "first compaction", func() bool { return logged(logs, "compaction done") == 1 }) | 379 | + | settle() // first compaction: the background work has finished |
| 352 | - | // The log is still over the threshold: the next commit compacts again. | 380 | + | if logged(logs, "compaction done") != 1 { |
| 353 | - | i := 61 | 381 | + | t.Fatalf("first compaction did not happen: %s", logs.String()) |
| 354 | - | waitFor(t, "second compaction", func() bool { | |||
| 355 | - | if logged(logs, "compaction done") >= 2 { | |||
| 356 | - | return true | |||
| 357 | } | 382 | } | ||
| 358 | - | mustApply(t, db, i) | 383 | + | // The log is still over the threshold: the next commit compacts again. |
| 359 | - | i++ | 384 | + | i := 61 |
| 360 | - | return false | 385 | + | i = applyUntil(t, db, i, "second compaction", func() bool { return logged(logs, "compaction done") >= 2 }) |
| 386 | + | if n := logged(logs, "log switch"); n != 2 { | |||
| 387 | + | t.Fatalf("%d log switches, want 2", n) | |||
| 388 | + | } | |||
| 389 | + | if err := db.Close(bg); err != nil { | |||
| 390 | + | t.Fatal(err) | |||
| 391 | + | } | |||
| 392 | + | snaps, files := listFiles(t, fx) | |||
| 393 | + | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |||
| 394 | + | t.Fatalf("after the second compaction: snapshots %v, logs %v", snaps, files) | |||
| 395 | + | } | |||
| 396 | + | db = mustOpen(t, fx, Options{}) | |||
| 397 | + | mustCheck(t, db, i-1) | |||
| 361 | }) | 398 | }) | ||
| 362 | - | if n := logged(logs, "log switch"); n != 2 { | |||
| 363 | - | t.Fatalf("%d log switches, want 2", n) | |||
| 364 | - | } | |||
| 365 | - | if err := db.Close(bg); err != nil { | |||
| 366 | - | t.Fatal(err) | |||
| 367 | - | } | |||
| 368 | - | snaps, files := listFiles(t, fx) | |||
| 369 | - | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |||
| 370 | - | t.Fatalf("after the second compaction: snapshots %v, logs %v", snaps, files) | |||
| 371 | - | } | |||
| 372 | - | db = mustOpen(t, fx, Options{}) | |||
| 373 | - | mustCheck(t, db, i-1) | |||
| 374 | } | 399 | } | ||
| 375 | 400 | ||||
| 376 | func TestCompactRemovesOldFiles(t *testing.T) { | 401 | func TestCompactRemovesOldFiles(t *testing.T) { | ||
| @@ -460,76 +485,72 @@ var compactorFaults = []struct { | |||||
| 460 | func TestCompactorFailure(t *testing.T) { | 485 | func TestCompactorFailure(t *testing.T) { | ||
| 461 | for _, f := range compactorFaults { | 486 | for _, f := range compactorFaults { | ||
| 462 | t.Run(f.name, func(t *testing.T) { | 487 | t.Run(f.name, func(t *testing.T) { | ||
| 463 | - | fx := faultfs.New() | 488 | + | synctest.Test(t, func(t *testing.T) { |
| 464 | - | ctx, logs := debugCtx() | 489 | + | fx := faultfs.New() |
| 465 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | 490 | + | ctx, logs := debugCtx() |
| 466 | - | for i := 1; i <= 5; i++ { | 491 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) |
| 467 | - | mustApply(t, db, i) | 492 | + | for i := 1; i <= 5; i++ { |
| 468 | - | } | 493 | + | mustApply(t, db, i) |
| 469 | - | f.inject(fx) | 494 | + | } |
| 470 | - | mustApply(t, db, 6) | 495 | + | f.inject(fx) |
| 471 | - | waitFor(t, "compaction failure", func() bool { return logged(logs, "compaction failed") == 1 }) | 496 | + | mustApply(t, db, 6) |
| 472 | - | if !strings.Contains(logs.String(), `"level":"warn"`) { | 497 | + | settle() // compaction failure: the background work has finished |
| 473 | - | t.Errorf("failure not logged as a warning: %s", logs.String()) | 498 | + | if logged(logs, "compaction failed") != 1 { |
| 474 | - | } | 499 | + | t.Fatalf("compaction failure did not happen: %s", logs.String()) |
| 475 | - | mustApply(t, db, 7) // the database is not failed | 500 | + | } |
| 476 | - | if err := db.Close(bg); err != nil { | 501 | + | if !strings.Contains(logs.String(), `"level":"warn"`) { |
| 477 | - | t.Fatalf("Close: %v", err) | 502 | + | t.Errorf("failure not logged as a warning: %s", logs.String()) |
| 478 | - | } | 503 | + | } |
| 504 | + | mustApply(t, db, 7) // the database is not failed | |||
| 505 | + | if err := db.Close(bg); err != nil { | |||
| 506 | + | t.Fatalf("Close: %v", err) | |||
| 507 | + | } | |||
| 479 | 508 | ||||
| 480 | - | // Reopen the partial state: it recovers and compacts again. | 509 | + | // Reopen the partial state: it recovers and compacts again. |
| 481 | - | ctx, logs = debugCtx() | 510 | + | ctx, logs = debugCtx() |
| 482 | - | db = mustOpenCtx(t, ctx, fx, compactOpts()) | 511 | + | db = mustOpenCtx(t, ctx, fx, compactOpts()) |
| 483 | - | mustCheck(t, db, 7) | 512 | + | mustCheck(t, db, 7) |
| 484 | - | i := 8 | 513 | + | i := 8 |
| 485 | - | waitFor(t, "retried compaction", func() bool { | 514 | + | i = applyUntil(t, db, i, "retried compaction", func() bool { return logged(logs, "compaction done") >= 1 }) |
| 486 | - | if logged(logs, "compaction done") >= 1 { | 515 | + | if err := db.Close(bg); err != nil { |
| 487 | - | return true | 516 | + | t.Fatal(err) |
| 488 | } | 517 | } | ||
| 489 | - | mustApply(t, db, i) | 518 | + | snaps, files := listFiles(t, fx) |
| 490 | - | i++ | 519 | + | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { |
| 491 | - | return false | 520 | + | t.Fatalf("after the retry: snapshots %v, logs %v", snaps, files) |
| 521 | + | } | |||
| 522 | + | db = mustOpen(t, fx, Options{}) | |||
| 523 | + | mustCheck(t, db, i-1) | |||
| 492 | }) | 524 | }) | ||
| 493 | - | if err := db.Close(bg); err != nil { | |||
| 494 | - | t.Fatal(err) | |||
| 495 | - | } | |||
| 496 | - | snaps, files := listFiles(t, fx) | |||
| 497 | - | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |||
| 498 | - | t.Fatalf("after the retry: snapshots %v, logs %v", snaps, files) | |||
| 499 | - | } | |||
| 500 | - | db = mustOpen(t, fx, Options{}) | |||
| 501 | - | mustCheck(t, db, i-1) | |||
| 502 | }) | 525 | }) | ||
| 503 | } | 526 | } | ||
| 504 | } | 527 | } | ||
| 505 | 528 | ||||
| 506 | // TestCompactorRetryWithoutReopen checks the retry in the same process. | 529 | // TestCompactorRetryWithoutReopen checks the retry in the same process. | ||
| 507 | func TestCompactorRetryWithoutReopen(t *testing.T) { | 530 | func TestCompactorRetryWithoutReopen(t *testing.T) { | ||
| 508 | - | fx := faultfs.New() | 531 | + | synctest.Test(t, func(t *testing.T) { |
| 509 | - | ctx, logs := debugCtx() | 532 | + | fx := faultfs.New() |
| 510 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | 533 | + | ctx, logs := debugCtx() |
| 511 | - | for i := 1; i <= 5; i++ { | 534 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) |
| 512 | - | mustApply(t, db, i) | 535 | + | for i := 1; i <= 5; i++ { |
| 513 | - | } | 536 | + | mustApply(t, db, i) |
| 514 | - | fx.FailOp(faultfs.OpRename, 2, nil) // the snapshot | 537 | + | } |
| 515 | - | mustApply(t, db, 6) | 538 | + | fx.FailOp(faultfs.OpRename, 2, nil) // the snapshot |
| 516 | - | waitFor(t, "compaction failure", func() bool { return logged(logs, "compaction failed") == 1 }) | 539 | + | mustApply(t, db, 6) |
| 517 | - | i := 7 | 540 | + | settle() // compaction failure: the background work has finished |
| 518 | - | waitFor(t, "retried compaction", func() bool { | 541 | + | if logged(logs, "compaction failed") != 1 { |
| 519 | - | if logged(logs, "compaction done") >= 1 { | 542 | + | t.Fatalf("compaction failure did not happen: %s", logs.String()) |
| 520 | - | return true | 543 | + | } |
| 544 | + | i := 7 | |||
| 545 | + | i = applyUntil(t, db, i, "retried compaction", func() bool { return logged(logs, "compaction done") >= 1 }) | |||
| 546 | + | if err := db.Close(bg); err != nil { | |||
| 547 | + | t.Fatal(err) | |||
| 548 | + | } | |||
| 549 | + | snaps, files := listFiles(t, fx) | |||
| 550 | + | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |||
| 551 | + | t.Fatalf("snapshots %v, logs %v", snaps, files) | |||
| 521 | } | 552 | } | ||
| 522 | - | mustApply(t, db, i) | |||
| 523 | - | i++ | |||
| 524 | - | return false | |||
| 525 | }) | 553 | }) | ||
| 526 | - | if err := db.Close(bg); err != nil { | |||
| 527 | - | t.Fatal(err) | |||
| 528 | - | } | |||
| 529 | - | snaps, files := listFiles(t, fx) | |||
| 530 | - | if len(snaps) != 1 || !slices.Equal(files, snaps) || snaps[0] <= 6 { | |||
| 531 | - | t.Fatalf("snapshots %v, logs %v", snaps, files) | |||
| 532 | - | } | |||
| 533 | } | 554 | } | ||
| 534 | 555 | ||||
| 535 | // switchFaults fail the creation of log-6 in the writer. The numbering is | 556 | // switchFaults fail the creation of log-6 in the writer. The numbering is | ||
| @@ -551,49 +572,44 @@ var switchFaults = []struct { | |||||
| 551 | func TestLogSwitchFailure(t *testing.T) { | 572 | func TestLogSwitchFailure(t *testing.T) { | ||
| 552 | for _, f := range switchFaults { | 573 | for _, f := range switchFaults { | ||
| 553 | t.Run(f.name, func(t *testing.T) { | 574 | t.Run(f.name, func(t *testing.T) { | ||
| 554 | - | fx := faultfs.New() | 575 | + | synctest.Test(t, func(t *testing.T) { |
| 555 | - | ctx, logs := debugCtx() | 576 | + | fx := faultfs.New() |
| 556 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | 577 | + | ctx, logs := debugCtx() |
| 557 | - | for i := 1; i <= 5; i++ { | 578 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) |
| 558 | - | mustApply(t, db, i) | 579 | + | for i := 1; i <= 5; i++ { |
| 559 | - | } | 580 | + | mustApply(t, db, i) |
| 560 | - | f.inject(fx) | 581 | + | } |
| 561 | - | mustApply(t, db, 6) | 582 | + | f.inject(fx) |
| 562 | - | barrier(t, db) | 583 | + | mustApply(t, db, 6) |
| 563 | - | if logged(logs, "log switch failed") != 1 { | 584 | + | barrier(t, db) |
| 564 | - | t.Fatalf("switch failure not logged: %s", logs.String()) | 585 | + | if logged(logs, "log switch failed") != 1 { |
| 565 | - | } | 586 | + | t.Fatalf("switch failure not logged: %s", logs.String()) |
| 566 | - | snaps, files := listFiles(t, fx) | 587 | + | } |
| 567 | - | if len(snaps) != 0 || !slices.Equal(files, []uint64{0}) { | 588 | + | snaps, files := listFiles(t, fx) |
| 568 | - | t.Fatalf("after the failure: snapshots %v, logs %v", snaps, files) | 589 | + | if len(snaps) != 0 || !slices.Equal(files, []uint64{0}) { |
| 569 | - | } | 590 | + | t.Fatalf("after the failure: snapshots %v, logs %v", snaps, files) |
| 570 | - | names, _ := fx.ReadDir(testDir) | 591 | + | } |
| 571 | - | for _, n := range names { | 592 | + | names, _ := fx.ReadDir(testDir) |
| 572 | - | if strings.HasSuffix(n, tmpSuffix) { | 593 | + | for _, n := range names { |
| 573 | - | t.Errorf("leftover %s", n) | 594 | + | if strings.HasSuffix(n, tmpSuffix) { |
| 595 | + | t.Errorf("leftover %s", n) | |||
| 596 | + | } | |||
| 574 | } | 597 | } | ||
| 575 | - | } | |||
| 576 | 598 | ||||
| 577 | - | // No retry on every commit: a failure costs another CompactAfter bytes. | 599 | + | // No retry on every commit: a failure costs another CompactAfter bytes. |
| 578 | - | mustApply(t, db, 7) | 600 | + | mustApply(t, db, 7) |
| 579 | - | barrier(t, db) | 601 | + | barrier(t, db) |
| 580 | - | if n := logged(logs, "log switch failed"); n != 1 { | 602 | + | if n := logged(logs, "log switch failed"); n != 1 { |
| 581 | - | t.Fatalf("%d switch attempts for 2 commits, want 1", n) | 603 | + | t.Fatalf("%d switch attempts for 2 commits, want 1", n) |
| 582 | - | } | 604 | + | } |
| 583 | - | i := 8 | 605 | + | i := 8 |
| 584 | - | waitFor(t, "compaction after the failed switch", func() bool { | 606 | + | i = applyUntil(t, db, i, "compaction after the failed switch", func() bool { return logged(logs, "compaction done") >= 1 }) |
| 585 | - | if logged(logs, "compaction done") >= 1 { | 607 | + | if err := db.Close(bg); err != nil { |
| 586 | - | return true | 608 | + | t.Fatal(err) |
| 587 | } | 609 | } | ||
| 588 | - | mustApply(t, db, i) | 610 | + | db = mustOpen(t, fx, Options{}) |
| 589 | - | i++ | 611 | + | mustCheck(t, db, i-1) |
| 590 | - | return false | |||
| 591 | }) | 612 | }) | ||
| 592 | - | if err := db.Close(bg); err != nil { | |||
| 593 | - | t.Fatal(err) | |||
| 594 | - | } | |||
| 595 | - | db = mustOpen(t, fx, Options{}) | |||
| 596 | - | mustCheck(t, db, i-1) | |||
| 597 | }) | 613 | }) | ||
| 598 | } | 614 | } | ||
| 599 | } | 615 | } | ||
| @@ -613,35 +629,33 @@ func TestLogSwitchFailsDatabase(t *testing.T) { | |||||
| 613 | } | 629 | } | ||
| 614 | for _, c := range cases { | 630 | for _, c := range cases { | ||
| 615 | t.Run(c.name, func(t *testing.T) { | 631 | t.Run(c.name, func(t *testing.T) { | ||
| 616 | - | fx := faultfs.New() | 632 | + | synctest.Test(t, func(t *testing.T) { |
| 617 | - | ctx, logs := debugCtx() | 633 | + | fx := faultfs.New() |
| 618 | - | db := mustOpenCtx(t, ctx, fx, compactOpts()) | 634 | + | ctx, logs := debugCtx() |
| 619 | - | for i := 1; i <= 5; i++ { | 635 | + | db := mustOpenCtx(t, ctx, fx, compactOpts()) |
| 620 | - | mustApply(t, db, i) | 636 | + | for i := 1; i <= 5; i++ { |
| 621 | - | } | 637 | + | mustApply(t, db, i) |
| 622 | - | c.inject(fx) | 638 | + | } |
| 623 | - | mustApply(t, db, 6) // acknowledged: the failure comes after the reply | 639 | + | c.inject(fx) |
| 624 | - | waitFor(t, "failed state", func() bool { | 640 | + | mustApply(t, db, 6) // acknowledged: the failure comes after the reply |
| 625 | - | select { | 641 | + | settle() // the writer has seen the failure |
| 626 | - | case <-db.failed: | 642 | + | if !failedDB(db) { |
| 627 | - | return true | 643 | + | t.Fatal("the database did not fail") |
| 628 | - | default: | 644 | + | } |
| 629 | - | return false | 645 | + | var b Batch |
| 646 | + | b.Put([]byte("later"), nil) | |||
| 647 | + | if _, err := db.Apply(bg, &b); !errors.Is(err, ErrFailed) { | |||
| 648 | + | t.Fatalf("Apply after the failure: %v", err) | |||
| 649 | + | } | |||
| 650 | + | if !strings.Contains(logs.String(), "database failed") { | |||
| 651 | + | t.Errorf("failure not logged: %s", logs.String()) | |||
| 630 | } | 652 | } | ||
| 653 | + | if err := db.Close(bg); !errors.Is(err, ErrFailed) { | |||
| 654 | + | t.Fatalf("Close = %v, want the failure", err) | |||
| 655 | + | } | |||
| 656 | + | db = mustOpen(t, fx, Options{}) | |||
| 657 | + | mustCheck(t, db, 6) | |||
| 631 | }) | 658 | }) | ||
| 632 | - | var b Batch | |||
| 633 | - | b.Put([]byte("later"), nil) | |||
| 634 | - | if _, err := db.Apply(bg, &b); !errors.Is(err, ErrFailed) { | |||
| 635 | - | t.Fatalf("Apply after the failure: %v", err) | |||
| 636 | - | } | |||
| 637 | - | if !strings.Contains(logs.String(), "database failed") { | |||
| 638 | - | t.Errorf("failure not logged: %s", logs.String()) | |||
| 639 | - | } | |||
| 640 | - | if err := db.Close(bg); !errors.Is(err, ErrFailed) { | |||
| 641 | - | t.Fatalf("Close = %v, want the failure", err) | |||
| 642 | - | } | |||
| 643 | - | db = mustOpen(t, fx, Options{}) | |||
| 644 | - | mustCheck(t, db, 6) | |||
| 645 | }) | 659 | }) | ||
| 646 | } | 660 | } | ||
| 647 | } | 661 | } | ||
| @@ -652,79 +666,82 @@ func TestLogSwitchFailsDatabase(t *testing.T) { | |||||
| 652 | // sync are gone; the database must fail rather than continue, and what is | 666 | // sync are gone; the database must fail rather than continue, and what is | ||
| 653 | // left must open as a prefix. | 667 | // left must open as a prefix. | ||
| 654 | func TestLogSwitchSyncFailureDropsUnsyncedData(t *testing.T) { | 668 | func TestLogSwitchSyncFailureDropsUnsyncedData(t *testing.T) { | ||
| 655 | - | fx := faultfs.New() | 669 | + | synctest.Test(t, func(t *testing.T) { |
| 656 | - | fx.DropOnSyncFailure(true) | 670 | + | fx := faultfs.New() |
| 657 | - | opts := compactOpts() | 671 | + | fx.DropOnSyncFailure(true) |
| 658 | - | opts.NoSync = true | 672 | + | opts := compactOpts() |
| 659 | - | db := mustOpen(t, fx, opts) | 673 | + | opts.NoSync = true |
| 660 | - | for i := 1; i <= 5; i++ { | 674 | + | db := mustOpen(t, fx, opts) |
| 661 | - | mustApply(t, db, i) | 675 | + | for i := 1; i <= 5; i++ { |
| 662 | - | } | 676 | + | mustApply(t, db, i) |
| 663 | - | fx.FailSync(1, nil) | 677 | + | } |
| 664 | - | mustApply(t, db, 6) | 678 | + | fx.FailSync(1, nil) |
| 665 | - | waitFor(t, "failed state", func() bool { | 679 | + | mustApply(t, db, 6) |
| 666 | - | select { | 680 | + | settle() // the writer has seen the failure |
| 667 | - | case <-db.failed: | 681 | + | if !failedDB(db) { |
| 668 | - | return true | 682 | + | t.Fatal("the database did not fail") |
| 669 | - | default: | 683 | + | } |
| 670 | - | return false | 684 | + | if err := db.Close(bg); !errors.Is(err, ErrFailed) { |
| 685 | + | t.Fatalf("Close = %v, want the failure", err) | |||
| 686 | + | } | |||
| 687 | + | db = mustOpen(t, fx, Options{}) | |||
| 688 | + | if n := mustCheck(t, db, 0); n != 0 { | |||
| 689 | + | t.Fatalf("recovered %d batches, want 0 (all were dropped)", n) | |||
| 671 | } | 690 | } | ||
| 672 | }) | 691 | }) | ||
| 673 | - | if err := db.Close(bg); !errors.Is(err, ErrFailed) { | |||
| 674 | - | t.Fatalf("Close = %v, want the failure", err) | |||
| 675 | - | } | |||
| 676 | - | db = mustOpen(t, fx, Options{}) | |||
| 677 | - | if n := mustCheck(t, db, 0); n != 0 { | |||
| 678 | - | t.Fatalf("recovered %d batches, want 0 (all were dropped)", n) | |||
| 679 | - | } | |||
| 680 | } | 692 | } | ||
| 681 | 693 | ||||
| 682 | // TestCloseWaitsForCompaction checks that Close does not cancel a running | 694 | // TestCloseWaitsForCompaction checks that Close does not cancel a running | ||
| 683 | // compaction, waits for it, and leaves a directory that opens. | 695 | // compaction, waits for it, and leaves a directory that opens. | ||
| 684 | func TestCloseWaitsForCompaction(t *testing.T) { | 696 | func TestCloseWaitsForCompaction(t *testing.T) { | ||
| 685 | - | fx := faultfs.New() | 697 | + | synctest.Test(t, func(t *testing.T) { |
| 686 | - | started, release := make(chan struct{}), make(chan struct{}) | 698 | + | fx := faultfs.New() |
| 687 | - | var once sync.Once | 699 | + | started, release := make(chan struct{}), make(chan struct{}) |
| 688 | - | h := &hookFS{FS: fx, hook: func(op, path string) error { | 700 | + | var once sync.Once |
| 689 | - | if op == "open" && strings.Contains(path, snapshotPrefix) { | 701 | + | h := &hookFS{FS: fx, hook: func(op, path string) error { |
| 690 | - | once.Do(func() { close(started); <-release }) | 702 | + | if op == "open" && strings.Contains(path, snapshotPrefix) { |
| 691 | - | } | 703 | + | once.Do(func() { close(started); <-release }) |
| 692 | - | return nil | 704 | + | } |
| 693 | - | }} | 705 | + | return nil |
| 694 | - | ctx, logs := debugCtx() | 706 | + | }} |
| 695 | - | db := mustOpenCtx(t, ctx, h, compactOpts()) | 707 | + | ctx, logs := debugCtx() |
| 696 | - | for i := 1; i <= 6; i++ { | 708 | + | db := mustOpenCtx(t, ctx, h, compactOpts()) |
| 697 | - | mustApply(t, db, i) | 709 | + | for i := 1; i <= 6; i++ { |
| 698 | - | } | 710 | + | mustApply(t, db, i) |
| 699 | - | select { | |||
| 700 | - | case <-started: | |||
| 701 | - | case <-time.After(10 * time.Second): | |||
| 702 | - | t.Fatal("the compactor did not start") | |||
| 703 | - | } | |||
| 704 | - | closed := make(chan error, 1) | |||
| 705 | - | go func() { closed <- db.Close(bg) }() | |||
| 706 | - | select { | |||
| 707 | - | case err := <-closed: | |||
| 708 | - | t.Fatalf("Close returned during the compaction: %v", err) | |||
| 709 | - | case <-time.After(100 * time.Millisecond): | |||
| 710 | - | } | |||
| 711 | - | if _, err := db.Apply(bg, new(Batch)); !errors.Is(err, ErrClosed) { | |||
| 712 | - | t.Fatalf("Apply while closing: %v", err) | |||
| 713 | - | } | |||
| 714 | - | close(release) | |||
| 715 | - | select { | |||
| 716 | - | case err := <-closed: | |||
| 717 | - | if err != nil { | |||
| 718 | - | t.Fatalf("Close: %v", err) | |||
| 719 | } | 711 | } | ||
| 720 | - | case <-time.After(10 * time.Second): | 712 | + | settle() // the compactor is parked in the hook |
| 721 | - | t.Fatal("Close did not return after the compaction was released") | 713 | + | select { |
| 722 | - | } | 714 | + | case <-started: |
| 723 | - | if logged(logs, "compaction done") != 1 || !settled(t, fx, 6) { | 715 | + | default: |
| 724 | - | t.Fatalf("the compaction did not complete: %s", logs.String()) | 716 | + | t.Fatal("the compactor did not start") |
| 725 | - | } | 717 | + | } |
| 726 | - | db = mustOpen(t, fx, Options{}) | 718 | + | closed := make(chan error, 1) |
| 727 | - | mustCheck(t, db, 6) | 719 | + | go func() { closed <- db.Close(bg) }() |
| 720 | + | settle() // Close is blocked waiting for the compactor, which is parked in the hook | |||
| 721 | + | select { | |||
| 722 | + | case err := <-closed: | |||
| 723 | + | t.Fatalf("Close returned during the compaction: %v", err) | |||
| 724 | + | default: | |||
| 725 | + | } | |||
| 726 | + | if _, err := db.Apply(bg, new(Batch)); !errors.Is(err, ErrClosed) { | |||
| 727 | + | t.Fatalf("Apply while closing: %v", err) | |||
| 728 | + | } | |||
| 729 | + | close(release) | |||
| 730 | + | settle() // the compaction finishes and Close returns | |||
| 731 | + | select { | |||
| 732 | + | case err := <-closed: | |||
| 733 | + | if err != nil { | |||
| 734 | + | t.Fatalf("Close: %v", err) | |||
| 735 | + | } | |||
| 736 | + | default: | |||
| 737 | + | t.Fatal("Close did not return after the compaction was released") | |||
| 738 | + | } | |||
| 739 | + | if logged(logs, "compaction done") != 1 || !settled(t, fx, 6) { | |||
| 740 | + | t.Fatalf("the compaction did not complete: %s", logs.String()) | |||
| 741 | + | } | |||
| 742 | + | db = mustOpen(t, fx, Options{}) | |||
| 743 | + | mustCheck(t, db, 6) | |||
| 744 | + | }) | |||
| 728 | } | 745 | } | ||
| 729 | 746 | ||||
| 730 | // TestCloseDuringCompaction (AC9) closes the database while writers run and | 747 | // TestCloseDuringCompaction (AC9) closes the database while writers run and | ||
M db.go +4 -6
| @@ -228,19 +228,17 @@ func (db *DB) Apply(ctx context.Context, b *Batch) (uint64, error) { | |||
| 228 | 228 | return 0, err | |
| 229 | 229 | } | |
| 230 | 230 | ||
| 231 | - | r := requestPool.Get().(*request) | |
| 232 | - | r.b = b | |
| 231 | + | r := &request{b: b, reply: make(chan result, 1)} | |
| 233 | 232 | select { | |
| 234 | 233 | case db.reqCh <- r: | |
| 235 | 234 | case <-ctx.Done(): | |
| 236 | - | return 0, r.release(ctx.Err()) | |
| 235 | + | return 0, ctx.Err() | |
| 237 | 236 | case <-db.closing: | |
| 238 | - | return 0, r.release(db.stopped()) | |
| 237 | + | return 0, db.stopped() | |
| 239 | 238 | case <-db.failed: | |
| 240 | - | return 0, r.release(db.stopped()) | |
| 239 | + | return 0, db.stopped() | |
| 241 | 240 | } | |
| 242 | 241 | res := <-r.reply // the writer always answers a request it has taken | |
| 243 | - | _ = r.release(nil) | |
| 244 | 242 | return res.seq, res.err | |
| 245 | 243 | } | |
| 246 | 244 | ||
| @@ -228,19 +228,17 @@ func (db *DB) Apply(ctx context.Context, b *Batch) (uint64, error) { | |||||
| 228 | return 0, err | 228 | return 0, err | ||
| 229 | } | 229 | } | ||
| 230 | 230 | ||||
| 231 | - | r := requestPool.Get().(*request) | 231 | + | r := &request{b: b, reply: make(chan result, 1)} |
| 232 | - | r.b = b | |||
| 233 | select { | 232 | select { | ||
| 234 | case db.reqCh <- r: | 233 | case db.reqCh <- r: | ||
| 235 | case <-ctx.Done(): | 234 | case <-ctx.Done(): | ||
| 236 | - | return 0, r.release(ctx.Err()) | 235 | + | return 0, ctx.Err() |
| 237 | case <-db.closing: | 236 | case <-db.closing: | ||
| 238 | - | return 0, r.release(db.stopped()) | 237 | + | return 0, db.stopped() |
| 239 | case <-db.failed: | 238 | case <-db.failed: | ||
| 240 | - | return 0, r.release(db.stopped()) | 239 | + | return 0, db.stopped() |
| 241 | } | 240 | } | ||
| 242 | res := <-r.reply // the writer always answers a request it has taken | 241 | res := <-r.reply // the writer always answers a request it has taken | ||
| 243 | - | _ = r.release(nil) | |||
| 244 | return res.seq, res.err | 242 | return res.seq, res.err | ||
| 245 | } | 243 | } | ||
| 246 | 244 | ||||
M db_test.go +157 -138
| @@ -16,6 +16,7 @@ import ( | |||
| 16 | 16 | "sync" | |
| 17 | 17 | "sync/atomic" | |
| 18 | 18 | "testing" | |
| 19 | + | "testing/synctest" | |
| 19 | 20 | "time" | |
| 20 | 21 | ||
| 21 | 22 | "github.com/bnema/zerowrap" | |
| @@ -354,7 +355,9 @@ func TestPreconditionsBeforeOps(t *testing.T) { | |||
| 354 | 355 | // holdWriter applies a batch whose sync is slow and returns once the writer | |
| 355 | 356 | // is inside that sync, so batches applied next queue up and form one group | |
| 356 | 357 | // (GroupWindow 0 drains everything already queued). wait returns when the | |
| 357 | - | // held batch is acknowledged; the sync delay stays set until then. | |
| 358 | + | // held batch is acknowledged; the sync delay stays set until then. It must be | |
| 359 | + | // called inside a synctest bubble: the writer sleeps in the sync on the fake | |
| 360 | + | // clock, so no real time passes. | |
| 358 | 361 | func holdWriter(t *testing.T, db *DB, fx *faultfs.FS) (wait func()) { | |
| 359 | 362 | t.Helper() | |
| 360 | 363 | base := fx.Syncs() | |
| @@ -366,8 +369,9 @@ func holdWriter(t *testing.T, db *DB, fx *faultfs.FS) (wait func()) { | |||
| 366 | 369 | _, err := db.Apply(bg, &b) | |
| 367 | 370 | done <- err | |
| 368 | 371 | }() | |
| 369 | - | for fx.Syncs() == base { | |
| 370 | - | time.Sleep(time.Millisecond) | |
| 372 | + | synctest.Wait() // the writer is parked in the delayed sync | |
| 373 | + | if fx.Syncs() == base { | |
| 374 | + | t.Fatal("the writer did not reach the sync") | |
| 371 | 375 | } | |
| 372 | 376 | return func() { | |
| 373 | 377 | t.Helper() | |
| @@ -381,28 +385,30 @@ func holdWriter(t *testing.T, db *DB, fx *faultfs.FS) (wait func()) { | |||
| 381 | 385 | // TestGroupPreconditions checks that preconditions see earlier batches of | |
| 382 | 386 | // the same group: the second batch of the group must conflict. | |
| 383 | 387 | func TestGroupPreconditions(t *testing.T) { | |
| 384 | - | fx := faultfs.New() | |
| 385 | - | db := mustOpen(t, fx, Options{}) | |
| 386 | - | syncs := fx.Syncs() | |
| 387 | - | wait := holdWriter(t, db, fx) | |
| 388 | - | errs := make([]error, 2) | |
| 389 | - | var wg sync.WaitGroup | |
| 390 | - | for i := range errs { | |
| 391 | - | wg.Go(func() { | |
| 392 | - | var b Batch | |
| 393 | - | b.RequireAbsent([]byte("k")) | |
| 394 | - | b.Put([]byte("k"), []byte{byte('a' + i)}) | |
| 395 | - | _, errs[i] = db.Apply(bg, &b) | |
| 396 | - | }) | |
| 397 | - | } | |
| 398 | - | wg.Wait() | |
| 399 | - | wait() | |
| 400 | - | if (errs[0] == nil) == (errs[1] == nil) { | |
| 401 | - | t.Fatalf("errors %v, want exactly one success", errs) | |
| 402 | - | } | |
| 403 | - | if got := fx.Syncs() - syncs; got != 2 { | |
| 404 | - | t.Fatalf("%d syncs, want 2 (held batch, then one group)", got) | |
| 405 | - | } | |
| 388 | + | synctest.Test(t, func(t *testing.T) { | |
| 389 | + | fx := faultfs.New() | |
| 390 | + | db := mustOpen(t, fx, Options{}) | |
| 391 | + | syncs := fx.Syncs() | |
| 392 | + | wait := holdWriter(t, db, fx) | |
| 393 | + | errs := make([]error, 2) | |
| 394 | + | var wg sync.WaitGroup | |
| 395 | + | for i := range errs { | |
| 396 | + | wg.Go(func() { | |
| 397 | + | var b Batch | |
| 398 | + | b.RequireAbsent([]byte("k")) | |
| 399 | + | b.Put([]byte("k"), []byte{byte('a' + i)}) | |
| 400 | + | _, errs[i] = db.Apply(bg, &b) | |
| 401 | + | }) | |
| 402 | + | } | |
| 403 | + | wg.Wait() | |
| 404 | + | wait() | |
| 405 | + | if (errs[0] == nil) == (errs[1] == nil) { | |
| 406 | + | t.Fatalf("errors %v, want exactly one success", errs) | |
| 407 | + | } | |
| 408 | + | if got := fx.Syncs() - syncs; got != 2 { | |
| 409 | + | t.Fatalf("%d syncs, want 2 (held batch, then one group)", got) | |
| 410 | + | } | |
| 411 | + | }) | |
| 406 | 412 | } | |
| 407 | 413 | ||
| 408 | 414 | func TestEmptyAndConflictOnlyBatches(t *testing.T) { | |
| @@ -462,25 +468,27 @@ func TestEmptyAndConflictOnlyBatches(t *testing.T) { | |||
| 462 | 468 | // TestNoWriteForSkippedGroup checks that a group of only conflicts and empty | |
| 463 | 469 | // batches neither writes nor syncs. | |
| 464 | 470 | func TestNoWriteForSkippedGroup(t *testing.T) { | |
| 465 | - | fx := faultfs.New() | |
| 466 | - | db := mustOpen(t, fx, Options{GroupWindow: 100 * time.Millisecond}) | |
| 467 | - | putKV(t, db, "a", "1") | |
| 468 | - | writes, syncs := fx.Writes(), fx.Syncs() | |
| 469 | - | var wg sync.WaitGroup | |
| 470 | - | for range 4 { | |
| 471 | - | wg.Go(func() { | |
| 472 | - | var b Batch | |
| 473 | - | b.RequireAbsent([]byte("a")) | |
| 474 | - | b.Put([]byte("x"), nil) | |
| 475 | - | if _, err := db.Apply(bg, &b); !errors.Is(err, ErrConflict) { | |
| 476 | - | t.Errorf("err = %v, want ErrConflict", err) | |
| 477 | - | } | |
| 478 | - | }) | |
| 479 | - | } | |
| 480 | - | wg.Wait() | |
| 481 | - | if fx.Writes() != writes || fx.Syncs() != syncs { | |
| 482 | - | t.Fatalf("writes %d→%d syncs %d→%d", writes, fx.Writes(), syncs, fx.Syncs()) | |
| 483 | - | } | |
| 471 | + | synctest.Test(t, func(t *testing.T) { | |
| 472 | + | fx := faultfs.New() | |
| 473 | + | db := mustOpen(t, fx, Options{GroupWindow: 100 * time.Millisecond}) | |
| 474 | + | putKV(t, db, "a", "1") | |
| 475 | + | writes, syncs := fx.Writes(), fx.Syncs() | |
| 476 | + | var wg sync.WaitGroup | |
| 477 | + | for range 4 { | |
| 478 | + | wg.Go(func() { | |
| 479 | + | var b Batch | |
| 480 | + | b.RequireAbsent([]byte("a")) | |
| 481 | + | b.Put([]byte("x"), nil) | |
| 482 | + | if _, err := db.Apply(bg, &b); !errors.Is(err, ErrConflict) { | |
| 483 | + | t.Errorf("err = %v, want ErrConflict", err) | |
| 484 | + | } | |
| 485 | + | }) | |
| 486 | + | } | |
| 487 | + | wg.Wait() | |
| 488 | + | if fx.Writes() != writes || fx.Syncs() != syncs { | |
| 489 | + | t.Fatalf("writes %d→%d syncs %d→%d", writes, fx.Writes(), syncs, fx.Syncs()) | |
| 490 | + | } | |
| 491 | + | }) | |
| 484 | 492 | } | |
| 485 | 493 | ||
| 486 | 494 | func TestGroupCommitSyncs(t *testing.T) { | |
| @@ -527,33 +535,35 @@ func TestNoSync(t *testing.T) { | |||
| 527 | 535 | // TestLargeBatches covers a batch above the 4 MiB group cap (a group of one) | |
| 528 | 536 | // and a burst of batches that does not fit in one group. | |
| 529 | 537 | func TestLargeBatches(t *testing.T) { | |
| 530 | - | fx := faultfs.New() | |
| 531 | - | db := mustOpen(t, fx, Options{GroupWindow: 50 * time.Millisecond}) | |
| 532 | - | big := strings.Repeat("B", 5<<20) | |
| 533 | - | putKV(t, db, "big", big) | |
| 534 | - | ||
| 535 | - | const n = 6 | |
| 536 | - | chunk := strings.Repeat("c", 3<<19) // 1.5 MiB: three do not fit in 4 MiB | |
| 537 | - | seqs := make([]uint64, n) | |
| 538 | - | var wg sync.WaitGroup | |
| 539 | - | for i := range n { | |
| 540 | - | wg.Go(func() { seqs[i] = putKV(t, db, fmt.Sprintf("chunk%d", i), chunk) }) | |
| 541 | - | } | |
| 542 | - | wg.Wait() | |
| 543 | - | slices.Sort(seqs) | |
| 544 | - | for i, s := range seqs { | |
| 545 | - | if s != uint64(i+2) { | |
| 546 | - | t.Fatalf("seqs = %v", seqs) | |
| 538 | + | synctest.Test(t, func(t *testing.T) { | |
| 539 | + | fx := faultfs.New() | |
| 540 | + | db := mustOpen(t, fx, Options{GroupWindow: 50 * time.Millisecond}) | |
| 541 | + | big := strings.Repeat("B", 5<<20) | |
| 542 | + | putKV(t, db, "big", big) | |
| 543 | + | ||
| 544 | + | const n = 6 | |
| 545 | + | chunk := strings.Repeat("c", 3<<19) // 1.5 MiB: three do not fit in 4 MiB | |
| 546 | + | seqs := make([]uint64, n) | |
| 547 | + | var wg sync.WaitGroup | |
| 548 | + | for i := range n { | |
| 549 | + | wg.Go(func() { seqs[i] = putKV(t, db, fmt.Sprintf("chunk%d", i), chunk) }) | |
| 547 | 550 | } | |
| 548 | - | } | |
| 549 | - | if err := db.Close(bg); err != nil { | |
| 550 | - | t.Fatal(err) | |
| 551 | - | } | |
| 552 | - | s := mustOpen(t, fx, Options{}).Snapshot(bg) | |
| 553 | - | mustGet(t, s, "big", big) | |
| 554 | - | for i := range n { | |
| 555 | - | mustGet(t, s, fmt.Sprintf("chunk%d", i), chunk) | |
| 556 | - | } | |
| 551 | + | wg.Wait() | |
| 552 | + | slices.Sort(seqs) | |
| 553 | + | for i, s := range seqs { | |
| 554 | + | if s != uint64(i+2) { | |
| 555 | + | t.Fatalf("seqs = %v", seqs) | |
| 556 | + | } | |
| 557 | + | } | |
| 558 | + | if err := db.Close(bg); err != nil { | |
| 559 | + | t.Fatal(err) | |
| 560 | + | } | |
| 561 | + | s := mustOpen(t, fx, Options{}).Snapshot(bg) | |
| 562 | + | mustGet(t, s, "big", big) | |
| 563 | + | for i := range n { | |
| 564 | + | mustGet(t, s, fmt.Sprintf("chunk%d", i), chunk) | |
| 565 | + | } | |
| 566 | + | }) | |
| 557 | 567 | } | |
| 558 | 568 | ||
| 559 | 569 | func TestInvalidInputs(t *testing.T) { | |
| @@ -675,58 +685,65 @@ func TestApplyAfterClose(t *testing.T) { | |||
| 675 | 685 | // TestContextBeforeDequeue blocks the writer in a slow sync so that a second | |
| 676 | 686 | // batch cannot be dequeued, then cancels its context. | |
| 677 | 687 | func TestContextBeforeDequeue(t *testing.T) { | |
| 678 | - | fx := faultfs.New() | |
| 679 | - | db := mustOpen(t, fx, Options{}) | |
| 688 | + | synctest.Test(t, func(t *testing.T) { | |
| 689 | + | fx := faultfs.New() | |
| 690 | + | db := mustOpen(t, fx, Options{}) | |
| 680 | 691 | ||
| 681 | - | done := make(chan error, 1) | |
| 682 | - | base := fx.Syncs() | |
| 683 | - | fx.SetSyncDelay(300 * time.Millisecond) | |
| 684 | - | go func() { | |
| 685 | - | var b Batch | |
| 686 | - | b.Put([]byte("first"), []byte("1")) | |
| 687 | - | _, err := db.Apply(bg, &b) | |
| 688 | - | done <- err | |
| 689 | - | }() | |
| 690 | - | for fx.Syncs() == base { // the writer is now inside the delayed sync | |
| 691 | - | time.Sleep(time.Millisecond) | |
| 692 | - | } | |
| 692 | + | done := make(chan error, 1) | |
| 693 | + | base := fx.Syncs() | |
| 694 | + | fx.SetSyncDelay(300 * time.Millisecond) | |
| 695 | + | go func() { | |
| 696 | + | var b Batch | |
| 697 | + | b.Put([]byte("first"), []byte("1")) | |
| 698 | + | _, err := db.Apply(bg, &b) | |
| 699 | + | done <- err | |
| 700 | + | }() | |
| 701 | + | synctest.Wait() // the writer is now inside the delayed sync | |
| 702 | + | if fx.Syncs() == base { | |
| 703 | + | t.Fatal("the writer did not reach the sync") | |
| 704 | + | } | |
| 705 | + | start := time.Now() | |
| 693 | 706 | ||
| 694 | - | ctx, cancel := context.WithCancel(bg) | |
| 695 | - | second := make(chan error, 1) | |
| 696 | - | go func() { | |
| 697 | - | var b Batch | |
| 698 | - | b.Put([]byte("second"), []byte("2")) | |
| 699 | - | _, err := db.Apply(ctx, &b) | |
| 700 | - | second <- err | |
| 701 | - | }() | |
| 702 | - | time.Sleep(50 * time.Millisecond) // let Apply reach its blocking select | |
| 703 | - | cancel() | |
| 704 | - | if err := <-second; !errors.Is(err, context.Canceled) { | |
| 705 | - | t.Fatalf("Apply = %v, want context.Canceled", err) | |
| 706 | - | } | |
| 707 | - | if err := <-done; err != nil { | |
| 708 | - | t.Fatalf("first Apply: %v", err) | |
| 709 | - | } | |
| 710 | - | fx.SetSyncDelay(0) | |
| 707 | + | ctx, cancel := context.WithCancel(bg) | |
| 708 | + | second := make(chan error, 1) | |
| 709 | + | go func() { | |
| 710 | + | var b Batch | |
| 711 | + | b.Put([]byte("second"), []byte("2")) | |
| 712 | + | _, err := db.Apply(ctx, &b) | |
| 713 | + | second <- err | |
| 714 | + | }() | |
| 715 | + | synctest.Wait() // Apply is blocked in its select: the writer cannot dequeue | |
| 716 | + | cancel() | |
| 717 | + | if err := <-second; !errors.Is(err, context.Canceled) { | |
| 718 | + | t.Fatalf("Apply = %v, want context.Canceled", err) | |
| 719 | + | } | |
| 720 | + | if d := time.Since(start); d != 0 { | |
| 721 | + | t.Fatalf("the cancelled Apply returned after %v, it must not wait for the sync", d) | |
| 722 | + | } | |
| 723 | + | if err := <-done; err != nil { | |
| 724 | + | t.Fatalf("first Apply: %v", err) | |
| 725 | + | } | |
| 726 | + | fx.SetSyncDelay(0) | |
| 711 | 727 | ||
| 712 | - | // An already cancelled context never reaches the writer either. | |
| 713 | - | var b Batch | |
| 714 | - | b.Put([]byte("third"), nil) | |
| 715 | - | if _, err := db.Apply(ctx, &b); !errors.Is(err, context.Canceled) { | |
| 716 | - | t.Fatalf("Apply with cancelled ctx = %v", err) | |
| 717 | - | } | |
| 718 | - | putKV(t, db, "fourth", "4") // the writer is still healthy | |
| 719 | - | if err := db.Close(bg); err != nil { | |
| 720 | - | t.Fatal(err) | |
| 721 | - | } | |
| 722 | - | s := mustOpen(t, fx, Options{}).Snapshot(bg) | |
| 723 | - | mustGet(t, s, "first", "1") | |
| 724 | - | mustGet(t, s, "fourth", "4") | |
| 725 | - | mustMissing(t, s, "second") | |
| 726 | - | mustMissing(t, s, "third") | |
| 727 | - | if s.Seq() != 2 { | |
| 728 | - | t.Fatalf("Seq = %d, want 2", s.Seq()) | |
| 729 | - | } | |
| 728 | + | // An already cancelled context never reaches the writer either. | |
| 729 | + | var b Batch | |
| 730 | + | b.Put([]byte("third"), nil) | |
| 731 | + | if _, err := db.Apply(ctx, &b); !errors.Is(err, context.Canceled) { | |
| 732 | + | t.Fatalf("Apply with cancelled ctx = %v", err) | |
| 733 | + | } | |
| 734 | + | putKV(t, db, "fourth", "4") // the writer is still healthy | |
| 735 | + | if err := db.Close(bg); err != nil { | |
| 736 | + | t.Fatal(err) | |
| 737 | + | } | |
| 738 | + | s := mustOpen(t, fx, Options{}).Snapshot(bg) | |
| 739 | + | mustGet(t, s, "first", "1") | |
| 740 | + | mustGet(t, s, "fourth", "4") | |
| 741 | + | mustMissing(t, s, "second") | |
| 742 | + | mustMissing(t, s, "third") | |
| 743 | + | if s.Seq() != 2 { | |
| 744 | + | t.Fatalf("Seq = %d, want 2", s.Seq()) | |
| 745 | + | } | |
| 746 | + | }) | |
| 730 | 747 | } | |
| 731 | 748 | ||
| 732 | 749 | func TestCloseDuringApplies(t *testing.T) { | |
| @@ -804,7 +821,7 @@ func TestInjectedFailures(t *testing.T) { | |||
| 804 | 821 | for _, f := range failures { | |
| 805 | 822 | for _, powerLoss := range []bool{false, true} { | |
| 806 | 823 | t.Run(fmt.Sprintf("%s/powerloss=%v", f.name, powerLoss), func(t *testing.T) { | |
| 807 | - | testInjectedFailure(t, f, powerLoss) | |
| 824 | + | synctest.Test(t, func(t *testing.T) { testInjectedFailure(t, f, powerLoss) }) | |
| 808 | 825 | }) | |
| 809 | 826 | } | |
| 810 | 827 | } | |
| @@ -894,25 +911,27 @@ func testInjectedFailure(t *testing.T, f failure, powerLoss bool) { | |||
| 894 | 911 | // TestFailedWriterAnswersOverflow checks that no Apply hangs when the group | |
| 895 | 912 | // that fails leaves a dequeued request behind. | |
| 896 | 913 | func TestFailedWriterAnswersOverflow(t *testing.T) { | |
| 897 | - | fx := faultfs.New() | |
| 898 | - | db := mustOpen(t, fx, Options{GroupWindow: 100 * time.Millisecond}) | |
| 899 | - | fx.FailWrite(1, nil) | |
| 900 | - | chunk := strings.Repeat("c", 3<<20) // two do not fit in one group | |
| 901 | - | errs := make([]error, 4) | |
| 902 | - | var wg sync.WaitGroup | |
| 903 | - | for i := range errs { | |
| 904 | - | wg.Go(func() { | |
| 905 | - | var b Batch | |
| 906 | - | b.Put(fmt.Appendf(nil, "k%d", i), []byte(chunk)) | |
| 907 | - | _, errs[i] = db.Apply(bg, &b) | |
| 908 | - | }) | |
| 909 | - | } | |
| 910 | - | wg.Wait() | |
| 911 | - | for i, err := range errs { | |
| 912 | - | if !errors.Is(err, ErrFailed) { | |
| 913 | - | t.Errorf("batch %d: %v", i, err) | |
| 914 | + | synctest.Test(t, func(t *testing.T) { | |
| 915 | + | fx := faultfs.New() | |
| 916 | + | db := mustOpen(t, fx, Options{GroupWindow: 100 * time.Millisecond}) | |
| 917 | + | fx.FailWrite(1, nil) | |
| 918 | + | chunk := strings.Repeat("c", 3<<20) // two do not fit in one group | |
| 919 | + | errs := make([]error, 4) | |
| 920 | + | var wg sync.WaitGroup | |
| 921 | + | for i := range errs { | |
| 922 | + | wg.Go(func() { | |
| 923 | + | var b Batch | |
| 924 | + | b.Put(fmt.Appendf(nil, "k%d", i), []byte(chunk)) | |
| 925 | + | _, errs[i] = db.Apply(bg, &b) | |
| 926 | + | }) | |
| 914 | 927 | } | |
| 915 | - | } | |
| 928 | + | wg.Wait() | |
| 929 | + | for i, err := range errs { | |
| 930 | + | if !errors.Is(err, ErrFailed) { | |
| 931 | + | t.Errorf("batch %d: %v", i, err) | |
| 932 | + | } | |
| 933 | + | } | |
| 934 | + | }) | |
| 916 | 935 | } | |
| 917 | 936 | ||
| 918 | 937 | type syncBuffer struct { | |
| @@ -16,6 +16,7 @@ import ( | |||||
| 16 | "sync" | 16 | "sync" | ||
| 17 | "sync/atomic" | 17 | "sync/atomic" | ||
| 18 | "testing" | 18 | "testing" | ||
| 19 | + | "testing/synctest" | |||
| 19 | "time" | 20 | "time" | ||
| 20 | 21 | ||||
| 21 | "github.com/bnema/zerowrap" | 22 | "github.com/bnema/zerowrap" | ||
| @@ -354,7 +355,9 @@ func TestPreconditionsBeforeOps(t *testing.T) { | |||||
| 354 | // holdWriter applies a batch whose sync is slow and returns once the writer | 355 | // holdWriter applies a batch whose sync is slow and returns once the writer | ||
| 355 | // is inside that sync, so batches applied next queue up and form one group | 356 | // is inside that sync, so batches applied next queue up and form one group | ||
| 356 | // (GroupWindow 0 drains everything already queued). wait returns when the | 357 | // (GroupWindow 0 drains everything already queued). wait returns when the | ||
| 357 | - | // held batch is acknowledged; the sync delay stays set until then. | 358 | + | // held batch is acknowledged; the sync delay stays set until then. It must be |
| 359 | + | // called inside a synctest bubble: the writer sleeps in the sync on the fake | |||
| 360 | + | // clock, so no real time passes. | |||
| 358 | func holdWriter(t *testing.T, db *DB, fx *faultfs.FS) (wait func()) { | 361 | func holdWriter(t *testing.T, db *DB, fx *faultfs.FS) (wait func()) { | ||
| 359 | t.Helper() | 362 | t.Helper() | ||
| 360 | base := fx.Syncs() | 363 | base := fx.Syncs() | ||
| @@ -366,8 +369,9 @@ func holdWriter(t *testing.T, db *DB, fx *faultfs.FS) (wait func()) { | |||||
| 366 | _, err := db.Apply(bg, &b) | 369 | _, err := db.Apply(bg, &b) | ||
| 367 | done <- err | 370 | done <- err | ||
| 368 | }() | 371 | }() | ||
| 369 | - | for fx.Syncs() == base { | 372 | + | synctest.Wait() // the writer is parked in the delayed sync |
| 370 | - | time.Sleep(time.Millisecond) | 373 | + | if fx.Syncs() == base { |
| 374 | + | t.Fatal("the writer did not reach the sync") | |||
| 371 | } | 375 | } | ||
| 372 | return func() { | 376 | return func() { | ||
| 373 | t.Helper() | 377 | t.Helper() | ||
| @@ -381,28 +385,30 @@ func holdWriter(t *testing.T, db *DB, fx *faultfs.FS) (wait func()) { | |||||
| 381 | // TestGroupPreconditions checks that preconditions see earlier batches of | 385 | // TestGroupPreconditions checks that preconditions see earlier batches of | ||
| 382 | // the same group: the second batch of the group must conflict. | 386 | // the same group: the second batch of the group must conflict. | ||
| 383 | func TestGroupPreconditions(t *testing.T) { | 387 | func TestGroupPreconditions(t *testing.T) { | ||
| 384 | - | fx := faultfs.New() | 388 | + | synctest.Test(t, func(t *testing.T) { |
| 385 | - | db := mustOpen(t, fx, Options{}) | 389 | + | fx := faultfs.New() |
| 386 | - | syncs := fx.Syncs() | 390 | + | db := mustOpen(t, fx, Options{}) |
| 387 | - | wait := holdWriter(t, db, fx) | 391 | + | syncs := fx.Syncs() |
| 388 | - | errs := make([]error, 2) | 392 | + | wait := holdWriter(t, db, fx) |
| 389 | - | var wg sync.WaitGroup | 393 | + | errs := make([]error, 2) |
| 390 | - | for i := range errs { | 394 | + | var wg sync.WaitGroup |
| 391 | - | wg.Go(func() { | 395 | + | for i := range errs { |
| 392 | - | var b Batch | 396 | + | wg.Go(func() { |
| 393 | - | b.RequireAbsent([]byte("k")) | 397 | + | var b Batch |
| 394 | - | b.Put([]byte("k"), []byte{byte('a' + i)}) | 398 | + | b.RequireAbsent([]byte("k")) |
| 395 | - | _, errs[i] = db.Apply(bg, &b) | 399 | + | b.Put([]byte("k"), []byte{byte('a' + i)}) |
| 396 | - | }) | 400 | + | _, errs[i] = db.Apply(bg, &b) |
| 397 | - | } | 401 | + | }) |
| 398 | - | wg.Wait() | 402 | + | } |
| 399 | - | wait() | 403 | + | wg.Wait() |
| 400 | - | if (errs[0] == nil) == (errs[1] == nil) { | 404 | + | wait() |
| 401 | - | t.Fatalf("errors %v, want exactly one success", errs) | 405 | + | if (errs[0] == nil) == (errs[1] == nil) { |
| 402 | - | } | 406 | + | t.Fatalf("errors %v, want exactly one success", errs) |
| 403 | - | if got := fx.Syncs() - syncs; got != 2 { | 407 | + | } |
| 404 | - | t.Fatalf("%d syncs, want 2 (held batch, then one group)", got) | 408 | + | if got := fx.Syncs() - syncs; got != 2 { |
| 405 | - | } | 409 | + | t.Fatalf("%d syncs, want 2 (held batch, then one group)", got) |
| 410 | + | } | |||
| 411 | + | }) | |||
| 406 | } | 412 | } | ||
| 407 | 413 | ||||
| 408 | func TestEmptyAndConflictOnlyBatches(t *testing.T) { | 414 | func TestEmptyAndConflictOnlyBatches(t *testing.T) { | ||
| @@ -462,25 +468,27 @@ func TestEmptyAndConflictOnlyBatches(t *testing.T) { | |||||
| 462 | // TestNoWriteForSkippedGroup checks that a group of only conflicts and empty | 468 | // TestNoWriteForSkippedGroup checks that a group of only conflicts and empty | ||
| 463 | // batches neither writes nor syncs. | 469 | // batches neither writes nor syncs. | ||
| 464 | func TestNoWriteForSkippedGroup(t *testing.T) { | 470 | func TestNoWriteForSkippedGroup(t *testing.T) { | ||
| 465 | - | fx := faultfs.New() | 471 | + | synctest.Test(t, func(t *testing.T) { |
| 466 | - | db := mustOpen(t, fx, Options{GroupWindow: 100 * time.Millisecond}) | 472 | + | fx := faultfs.New() |
| 467 | - | putKV(t, db, "a", "1") | 473 | + | db := mustOpen(t, fx, Options{GroupWindow: 100 * time.Millisecond}) |
| 468 | - | writes, syncs := fx.Writes(), fx.Syncs() | 474 | + | putKV(t, db, "a", "1") |
| 469 | - | var wg sync.WaitGroup | 475 | + | writes, syncs := fx.Writes(), fx.Syncs() |
| 470 | - | for range 4 { | 476 | + | var wg sync.WaitGroup |
| 471 | - | wg.Go(func() { | 477 | + | for range 4 { |
| 472 | - | var b Batch | 478 | + | wg.Go(func() { |
| 473 | - | b.RequireAbsent([]byte("a")) | 479 | + | var b Batch |
| 474 | - | b.Put([]byte("x"), nil) | 480 | + | b.RequireAbsent([]byte("a")) |
| 475 | - | if _, err := db.Apply(bg, &b); !errors.Is(err, ErrConflict) { | 481 | + | b.Put([]byte("x"), nil) |
| 476 | - | t.Errorf("err = %v, want ErrConflict", err) | 482 | + | if _, err := db.Apply(bg, &b); !errors.Is(err, ErrConflict) { |
| 477 | - | } | 483 | + | t.Errorf("err = %v, want ErrConflict", err) |
| 478 | - | }) | 484 | + | } |
| 479 | - | } | 485 | + | }) |
| 480 | - | wg.Wait() | 486 | + | } |
| 481 | - | if fx.Writes() != writes || fx.Syncs() != syncs { | 487 | + | wg.Wait() |
| 482 | - | t.Fatalf("writes %d→%d syncs %d→%d", writes, fx.Writes(), syncs, fx.Syncs()) | 488 | + | if fx.Writes() != writes || fx.Syncs() != syncs { |
| 483 | - | } | 489 | + | t.Fatalf("writes %d→%d syncs %d→%d", writes, fx.Writes(), syncs, fx.Syncs()) |
| 490 | + | } | |||
| 491 | + | }) | |||
| 484 | } | 492 | } | ||
| 485 | 493 | ||||
| 486 | func TestGroupCommitSyncs(t *testing.T) { | 494 | func TestGroupCommitSyncs(t *testing.T) { | ||
| @@ -527,33 +535,35 @@ func TestNoSync(t *testing.T) { | |||||
| 527 | // TestLargeBatches covers a batch above the 4 MiB group cap (a group of one) | 535 | // TestLargeBatches covers a batch above the 4 MiB group cap (a group of one) | ||
| 528 | // and a burst of batches that does not fit in one group. | 536 | // and a burst of batches that does not fit in one group. | ||
| 529 | func TestLargeBatches(t *testing.T) { | 537 | func TestLargeBatches(t *testing.T) { | ||
| 530 | - | fx := faultfs.New() | 538 | + | synctest.Test(t, func(t *testing.T) { |
| 531 | - | db := mustOpen(t, fx, Options{GroupWindow: 50 * time.Millisecond}) | 539 | + | fx := faultfs.New() |
| 532 | - | big := strings.Repeat("B", 5<<20) | 540 | + | db := mustOpen(t, fx, Options{GroupWindow: 50 * time.Millisecond}) |
| 533 | - | putKV(t, db, "big", big) | 541 | + | big := strings.Repeat("B", 5<<20) |
| 534 | - | 542 | + | putKV(t, db, "big", big) | |
| 535 | - | const n = 6 | 543 | + | |
| 536 | - | chunk := strings.Repeat("c", 3<<19) // 1.5 MiB: three do not fit in 4 MiB | 544 | + | const n = 6 |
| 537 | - | seqs := make([]uint64, n) | 545 | + | chunk := strings.Repeat("c", 3<<19) // 1.5 MiB: three do not fit in 4 MiB |
| 538 | - | var wg sync.WaitGroup | 546 | + | seqs := make([]uint64, n) |
| 539 | - | for i := range n { | 547 | + | var wg sync.WaitGroup |
| 540 | - | wg.Go(func() { seqs[i] = putKV(t, db, fmt.Sprintf("chunk%d", i), chunk) }) | 548 | + | for i := range n { |
| 541 | - | } | 549 | + | wg.Go(func() { seqs[i] = putKV(t, db, fmt.Sprintf("chunk%d", i), chunk) }) |
| 542 | - | wg.Wait() | |||
| 543 | - | slices.Sort(seqs) | |||
| 544 | - | for i, s := range seqs { | |||
| 545 | - | if s != uint64(i+2) { | |||
| 546 | - | t.Fatalf("seqs = %v", seqs) | |||
| 547 | } | 550 | } | ||
| 548 | - | } | 551 | + | wg.Wait() |
| 549 | - | if err := db.Close(bg); err != nil { | 552 | + | slices.Sort(seqs) |
| 550 | - | t.Fatal(err) | 553 | + | for i, s := range seqs { |
| 551 | - | } | 554 | + | if s != uint64(i+2) { |
| 552 | - | s := mustOpen(t, fx, Options{}).Snapshot(bg) | 555 | + | t.Fatalf("seqs = %v", seqs) |
| 553 | - | mustGet(t, s, "big", big) | 556 | + | } |
| 554 | - | for i := range n { | 557 | + | } |
| 555 | - | mustGet(t, s, fmt.Sprintf("chunk%d", i), chunk) | 558 | + | if err := db.Close(bg); err != nil { |
| 556 | - | } | 559 | + | t.Fatal(err) |
| 560 | + | } | |||
| 561 | + | s := mustOpen(t, fx, Options{}).Snapshot(bg) | |||
| 562 | + | mustGet(t, s, "big", big) | |||
| 563 | + | for i := range n { | |||
| 564 | + | mustGet(t, s, fmt.Sprintf("chunk%d", i), chunk) | |||
| 565 | + | } | |||
| 566 | + | }) | |||
| 557 | } | 567 | } | ||
| 558 | 568 | ||||
| 559 | func TestInvalidInputs(t *testing.T) { | 569 | func TestInvalidInputs(t *testing.T) { | ||
| @@ -675,58 +685,65 @@ func TestApplyAfterClose(t *testing.T) { | |||||
| 675 | // TestContextBeforeDequeue blocks the writer in a slow sync so that a second | 685 | // TestContextBeforeDequeue blocks the writer in a slow sync so that a second | ||
| 676 | // batch cannot be dequeued, then cancels its context. | 686 | // batch cannot be dequeued, then cancels its context. | ||
| 677 | func TestContextBeforeDequeue(t *testing.T) { | 687 | func TestContextBeforeDequeue(t *testing.T) { | ||
| 678 | - | fx := faultfs.New() | 688 | + | synctest.Test(t, func(t *testing.T) { |
| 679 | - | db := mustOpen(t, fx, Options{}) | 689 | + | fx := faultfs.New() |
| 690 | + | db := mustOpen(t, fx, Options{}) | |||
| 680 | 691 | ||||
| 681 | - | done := make(chan error, 1) | 692 | + | done := make(chan error, 1) |
| 682 | - | base := fx.Syncs() | 693 | + | base := fx.Syncs() |
| 683 | - | fx.SetSyncDelay(300 * time.Millisecond) | 694 | + | fx.SetSyncDelay(300 * time.Millisecond) |
| 684 | - | go func() { | 695 | + | go func() { |
| 685 | - | var b Batch | 696 | + | var b Batch |
| 686 | - | b.Put([]byte("first"), []byte("1")) | 697 | + | b.Put([]byte("first"), []byte("1")) |
| 687 | - | _, err := db.Apply(bg, &b) | 698 | + | _, err := db.Apply(bg, &b) |
| 688 | - | done <- err | 699 | + | done <- err |
| 689 | - | }() | 700 | + | }() |
| 690 | - | for fx.Syncs() == base { // the writer is now inside the delayed sync | 701 | + | synctest.Wait() // the writer is now inside the delayed sync |
| 691 | - | time.Sleep(time.Millisecond) | 702 | + | if fx.Syncs() == base { |
| 692 | - | } | 703 | + | t.Fatal("the writer did not reach the sync") |
| 704 | + | } | |||
| 705 | + | start := time.Now() | |||
| 693 | 706 | ||||
| 694 | - | ctx, cancel := context.WithCancel(bg) | 707 | + | ctx, cancel := context.WithCancel(bg) |
| 695 | - | second := make(chan error, 1) | 708 | + | second := make(chan error, 1) |
| 696 | - | go func() { | 709 | + | go func() { |
| 697 | - | var b Batch | 710 | + | var b Batch |
| 698 | - | b.Put([]byte("second"), []byte("2")) | 711 | + | b.Put([]byte("second"), []byte("2")) |
| 699 | - | _, err := db.Apply(ctx, &b) | 712 | + | _, err := db.Apply(ctx, &b) |
| 700 | - | second <- err | 713 | + | second <- err |
| 701 | - | }() | 714 | + | }() |
| 702 | - | time.Sleep(50 * time.Millisecond) // let Apply reach its blocking select | 715 | + | synctest.Wait() // Apply is blocked in its select: the writer cannot dequeue |
| 703 | - | cancel() | 716 | + | cancel() |
| 704 | - | if err := <-second; !errors.Is(err, context.Canceled) { | 717 | + | if err := <-second; !errors.Is(err, context.Canceled) { |
| 705 | - | t.Fatalf("Apply = %v, want context.Canceled", err) | 718 | + | t.Fatalf("Apply = %v, want context.Canceled", err) |
| 706 | - | } | 719 | + | } |
| 707 | - | if err := <-done; err != nil { | 720 | + | if d := time.Since(start); d != 0 { |
| 708 | - | t.Fatalf("first Apply: %v", err) | 721 | + | t.Fatalf("the cancelled Apply returned after %v, it must not wait for the sync", d) |
| 709 | - | } | 722 | + | } |
| 710 | - | fx.SetSyncDelay(0) | 723 | + | if err := <-done; err != nil { |
| 724 | + | t.Fatalf("first Apply: %v", err) | |||
| 725 | + | } | |||
| 726 | + | fx.SetSyncDelay(0) | |||
| 711 | 727 | ||||
| 712 | - | // An already cancelled context never reaches the writer either. | 728 | + | // An already cancelled context never reaches the writer either. |
| 713 | - | var b Batch | 729 | + | var b Batch |
| 714 | - | b.Put([]byte("third"), nil) | 730 | + | b.Put([]byte("third"), nil) |
| 715 | - | if _, err := db.Apply(ctx, &b); !errors.Is(err, context.Canceled) { | 731 | + | if _, err := db.Apply(ctx, &b); !errors.Is(err, context.Canceled) { |
| 716 | - | t.Fatalf("Apply with cancelled ctx = %v", err) | 732 | + | t.Fatalf("Apply with cancelled ctx = %v", err) |
| 717 | - | } | 733 | + | } |
| 718 | - | putKV(t, db, "fourth", "4") // the writer is still healthy | 734 | + | putKV(t, db, "fourth", "4") // the writer is still healthy |
| 719 | - | if err := db.Close(bg); err != nil { | 735 | + | if err := db.Close(bg); err != nil { |
| 720 | - | t.Fatal(err) | 736 | + | t.Fatal(err) |
| 721 | - | } | 737 | + | } |
| 722 | - | s := mustOpen(t, fx, Options{}).Snapshot(bg) | 738 | + | s := mustOpen(t, fx, Options{}).Snapshot(bg) |
| 723 | - | mustGet(t, s, "first", "1") | 739 | + | mustGet(t, s, "first", "1") |
| 724 | - | mustGet(t, s, "fourth", "4") | 740 | + | mustGet(t, s, "fourth", "4") |
| 725 | - | mustMissing(t, s, "second") | 741 | + | mustMissing(t, s, "second") |
| 726 | - | mustMissing(t, s, "third") | 742 | + | mustMissing(t, s, "third") |
| 727 | - | if s.Seq() != 2 { | 743 | + | if s.Seq() != 2 { |
| 728 | - | t.Fatalf("Seq = %d, want 2", s.Seq()) | 744 | + | t.Fatalf("Seq = %d, want 2", s.Seq()) |
| 729 | - | } | 745 | + | } |
| 746 | + | }) | |||
| 730 | } | 747 | } | ||
| 731 | 748 | ||||
| 732 | func TestCloseDuringApplies(t *testing.T) { | 749 | func TestCloseDuringApplies(t *testing.T) { | ||
| @@ -804,7 +821,7 @@ func TestInjectedFailures(t *testing.T) { | |||||
| 804 | for _, f := range failures { | 821 | for _, f := range failures { | ||
| 805 | for _, powerLoss := range []bool{false, true} { | 822 | for _, powerLoss := range []bool{false, true} { | ||
| 806 | t.Run(fmt.Sprintf("%s/powerloss=%v", f.name, powerLoss), func(t *testing.T) { | 823 | t.Run(fmt.Sprintf("%s/powerloss=%v", f.name, powerLoss), func(t *testing.T) { | ||
| 807 | - | testInjectedFailure(t, f, powerLoss) | 824 | + | synctest.Test(t, func(t *testing.T) { testInjectedFailure(t, f, powerLoss) }) |
| 808 | }) | 825 | }) | ||
| 809 | } | 826 | } | ||
| 810 | } | 827 | } | ||
| @@ -894,25 +911,27 @@ func testInjectedFailure(t *testing.T, f failure, powerLoss bool) { | |||||
| 894 | // TestFailedWriterAnswersOverflow checks that no Apply hangs when the group | 911 | // TestFailedWriterAnswersOverflow checks that no Apply hangs when the group | ||
| 895 | // that fails leaves a dequeued request behind. | 912 | // that fails leaves a dequeued request behind. | ||
| 896 | func TestFailedWriterAnswersOverflow(t *testing.T) { | 913 | func TestFailedWriterAnswersOverflow(t *testing.T) { | ||
| 897 | - | fx := faultfs.New() | 914 | + | synctest.Test(t, func(t *testing.T) { |
| 898 | - | db := mustOpen(t, fx, Options{GroupWindow: 100 * time.Millisecond}) | 915 | + | fx := faultfs.New() |
| 899 | - | fx.FailWrite(1, nil) | 916 | + | db := mustOpen(t, fx, Options{GroupWindow: 100 * time.Millisecond}) |
| 900 | - | chunk := strings.Repeat("c", 3<<20) // two do not fit in one group | 917 | + | fx.FailWrite(1, nil) |
| 901 | - | errs := make([]error, 4) | 918 | + | chunk := strings.Repeat("c", 3<<20) // two do not fit in one group |
| 902 | - | var wg sync.WaitGroup | 919 | + | errs := make([]error, 4) |
| 903 | - | for i := range errs { | 920 | + | var wg sync.WaitGroup |
| 904 | - | wg.Go(func() { | 921 | + | for i := range errs { |
| 905 | - | var b Batch | 922 | + | wg.Go(func() { |
| 906 | - | b.Put(fmt.Appendf(nil, "k%d", i), []byte(chunk)) | 923 | + | var b Batch |
| 907 | - | _, errs[i] = db.Apply(bg, &b) | 924 | + | b.Put(fmt.Appendf(nil, "k%d", i), []byte(chunk)) |
| 908 | - | }) | 925 | + | _, errs[i] = db.Apply(bg, &b) |
| 909 | - | } | 926 | + | }) |
| 910 | - | wg.Wait() | |||
| 911 | - | for i, err := range errs { | |||
| 912 | - | if !errors.Is(err, ErrFailed) { | |||
| 913 | - | t.Errorf("batch %d: %v", i, err) | |||
| 914 | } | 927 | } | ||
| 915 | - | } | 928 | + | wg.Wait() |
| 929 | + | for i, err := range errs { | |||
| 930 | + | if !errors.Is(err, ErrFailed) { | |||
| 931 | + | t.Errorf("batch %d: %v", i, err) | |||
| 932 | + | } | |||
| 933 | + | } | |||
| 934 | + | }) | |||
| 916 | } | 935 | } | ||
| 917 | 936 | ||||
| 918 | type syncBuffer struct { | 937 | type syncBuffer struct { | ||
M internal/fsys/faultfs/faultfs_test.go +23 -10
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "os" | |
| 11 | 11 | "sync" | |
| 12 | 12 | "testing" | |
| 13 | + | "testing/synctest" | |
| 13 | 14 | "time" | |
| 14 | 15 | ||
| 15 | 16 | "bnema.dev/ermite-kv/internal/fsys" | |
| @@ -140,16 +141,28 @@ func TestFailSyncAndCounters(t *testing.T) { | |||
| 140 | 141 | } | |
| 141 | 142 | ||
| 142 | 143 | func TestSyncDelay(t *testing.T) { | |
| 143 | - | f := New() | |
| 144 | - | h := create(t, f, "/a") | |
| 145 | - | f.SetSyncDelay(20 * time.Millisecond) | |
| 146 | - | start := time.Now() | |
| 147 | - | if err := h.Sync(); err != nil { | |
| 148 | - | t.Fatal(err) | |
| 149 | - | } | |
| 150 | - | if time.Since(start) < 20*time.Millisecond { | |
| 151 | - | t.Fatal("sync returned before the delay") | |
| 152 | - | } | |
| 144 | + | synctest.Test(t, func(t *testing.T) { | |
| 145 | + | f := New() | |
| 146 | + | h := create(t, f, "/a") | |
| 147 | + | f.SetSyncDelay(20 * time.Millisecond) | |
| 148 | + | for name, sync := range map[string]func() error{"Sync": h.Sync, "SyncAll": h.SyncAll} { | |
| 149 | + | start := time.Now() | |
| 150 | + | if err := sync(); err != nil { | |
| 151 | + | t.Fatal(err) | |
| 152 | + | } | |
| 153 | + | if got := time.Since(start); got != 20*time.Millisecond { | |
| 154 | + | t.Fatalf("%s took %v, want exactly 20ms", name, got) | |
| 155 | + | } | |
| 156 | + | } | |
| 157 | + | f.SetSyncDelay(0) | |
| 158 | + | start := time.Now() | |
| 159 | + | if err := h.Sync(); err != nil { | |
| 160 | + | t.Fatal(err) | |
| 161 | + | } | |
| 162 | + | if got := time.Since(start); got != 0 { | |
| 163 | + | t.Fatalf("Sync without delay took %v", got) | |
| 164 | + | } | |
| 165 | + | }) | |
| 153 | 166 | } | |
| 154 | 167 | ||
| 155 | 168 | func TestPowerLossDropsUnsyncedBytes(t *testing.T) { | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "os" | 10 | "os" | ||
| 11 | "sync" | 11 | "sync" | ||
| 12 | "testing" | 12 | "testing" | ||
| 13 | + | "testing/synctest" | |||
| 13 | "time" | 14 | "time" | ||
| 14 | 15 | ||||
| 15 | "bnema.dev/ermite-kv/internal/fsys" | 16 | "bnema.dev/ermite-kv/internal/fsys" | ||
| @@ -140,16 +141,28 @@ func TestFailSyncAndCounters(t *testing.T) { | |||||
| 140 | } | 141 | } | ||
| 141 | 142 | ||||
| 142 | func TestSyncDelay(t *testing.T) { | 143 | func TestSyncDelay(t *testing.T) { | ||
| 143 | - | f := New() | 144 | + | synctest.Test(t, func(t *testing.T) { |
| 144 | - | h := create(t, f, "/a") | 145 | + | f := New() |
| 145 | - | f.SetSyncDelay(20 * time.Millisecond) | 146 | + | h := create(t, f, "/a") |
| 146 | - | start := time.Now() | 147 | + | f.SetSyncDelay(20 * time.Millisecond) |
| 147 | - | if err := h.Sync(); err != nil { | 148 | + | for name, sync := range map[string]func() error{"Sync": h.Sync, "SyncAll": h.SyncAll} { |
| 148 | - | t.Fatal(err) | 149 | + | start := time.Now() |
| 149 | - | } | 150 | + | if err := sync(); err != nil { |
| 150 | - | if time.Since(start) < 20*time.Millisecond { | 151 | + | t.Fatal(err) |
| 151 | - | t.Fatal("sync returned before the delay") | 152 | + | } |
| 152 | - | } | 153 | + | if got := time.Since(start); got != 20*time.Millisecond { |
| 154 | + | t.Fatalf("%s took %v, want exactly 20ms", name, got) | |||
| 155 | + | } | |||
| 156 | + | } | |||
| 157 | + | f.SetSyncDelay(0) | |||
| 158 | + | start := time.Now() | |||
| 159 | + | if err := h.Sync(); err != nil { | |||
| 160 | + | t.Fatal(err) | |||
| 161 | + | } | |||
| 162 | + | if got := time.Since(start); got != 0 { | |||
| 163 | + | t.Fatalf("Sync without delay took %v", got) | |||
| 164 | + | } | |||
| 165 | + | }) | |||
| 153 | } | 166 | } | ||
| 154 | 167 | ||||
| 155 | func TestPowerLossDropsUnsyncedBytes(t *testing.T) { | 168 | func TestPowerLossDropsUnsyncedBytes(t *testing.T) { | ||
M writer.go +3 -14
| @@ -5,7 +5,6 @@ package kv | |||
| 5 | 5 | import ( | |
| 6 | 6 | "fmt" | |
| 7 | 7 | "io" | |
| 8 | - | "sync" | |
| 9 | 8 | "time" | |
| 10 | 9 | ) | |
| 11 | 10 | ||
| @@ -14,7 +13,9 @@ import ( | |||
| 14 | 13 | const maxGroupBytes = 4 << 20 | |
| 15 | 14 | ||
| 16 | 15 | // request is a batch waiting for the writer. The writer answers on reply | |
| 17 | - | // exactly once. | |
| 16 | + | // exactly once. Requests are allocated per Apply and never pooled: a pooled | |
| 17 | + | // channel would be shared across testing/synctest bubbles, and receiving on a | |
| 18 | + | // bubbled channel from outside its bubble is fatal. | |
| 18 | 19 | type request struct { | |
| 19 | 20 | b *Batch | |
| 20 | 21 | reply chan result // capacity 1 | |
| @@ -25,18 +26,6 @@ type result struct { | |||
| 25 | 26 | err error | |
| 26 | 27 | } | |
| 27 | 28 | ||
| 28 | - | var requestPool = sync.Pool{New: func() any { | |
| 29 | - | return &request{reply: make(chan result, 1)} | |
| 30 | - | }} | |
| 31 | - | ||
| 32 | - | // release returns r to the pool (it must not be referenced by the writer) | |
| 33 | - | // and passes err through. | |
| 34 | - | func (r *request) release(err error) error { | |
| 35 | - | r.b = nil | |
| 36 | - | requestPool.Put(r) | |
| 37 | - | return err | |
| 38 | - | } | |
| 39 | - | ||
| 40 | 29 | // writer is the single goroutine that owns the log file and builds the new | |
| 41 | 30 | // state. It exits on Close or when the database fails. | |
| 42 | 31 | func (db *DB) writer() { | |
| @@ -5,7 +5,6 @@ package kv | |||||
| 5 | import ( | 5 | import ( | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | "io" | 7 | "io" | ||
| 8 | - | "sync" | |||
| 9 | "time" | 8 | "time" | ||
| 10 | ) | 9 | ) | ||
| 11 | 10 | ||||
| @@ -14,7 +13,9 @@ import ( | |||||
| 14 | const maxGroupBytes = 4 << 20 | 13 | const maxGroupBytes = 4 << 20 | ||
| 15 | 14 | ||||
| 16 | // request is a batch waiting for the writer. The writer answers on reply | 15 | // request is a batch waiting for the writer. The writer answers on reply | ||
| 17 | - | // exactly once. | 16 | + | // exactly once. Requests are allocated per Apply and never pooled: a pooled |
| 17 | + | // channel would be shared across testing/synctest bubbles, and receiving on a | |||
| 18 | + | // bubbled channel from outside its bubble is fatal. | |||
| 18 | type request struct { | 19 | type request struct { | ||
| 19 | b *Batch | 20 | b *Batch | ||
| 20 | reply chan result // capacity 1 | 21 | reply chan result // capacity 1 | ||
| @@ -25,18 +26,6 @@ type result struct { | |||||
| 25 | err error | 26 | err error | ||
| 26 | } | 27 | } | ||
| 27 | 28 | ||||
| 28 | - | var requestPool = sync.Pool{New: func() any { | |||
| 29 | - | return &request{reply: make(chan result, 1)} | |||
| 30 | - | }} | |||
| 31 | - | ||||
| 32 | - | // release returns r to the pool (it must not be referenced by the writer) | |||
| 33 | - | // and passes err through. | |||
| 34 | - | func (r *request) release(err error) error { | |||
| 35 | - | r.b = nil | |||
| 36 | - | requestPool.Put(r) | |||
| 37 | - | return err | |||
| 38 | - | } | |||
| 39 | - | ||||
| 40 | // writer is the single goroutine that owns the log file and builds the new | 29 | // writer is the single goroutine that owns the log file and builds the new | ||
| 41 | // state. It exits on Close or when the database fails. | 30 | // state. It exits on Close or when the database fails. | ||
| 42 | func (db *DB) writer() { | 31 | func (db *DB) writer() { | ||