Skip to content

Commit 155a03d

Browse files
committed
roachtest: requeue preempted benchmarks for one non-spot retry
A benchmark's value is the per-nightly time-series data point it produces. When a benchmark runs on a spot VM and the VM is preempted, the runner today marks the failure as a flake (routed to test-eng, no GitHub issue) and moves on: --count=1 means no in-run retry, and the test selector only forces re-selection on the *next* nightly. The data point is silently lost. Requeue a benchmark exactly once when a preemption is detected post-test. The retry is queued with Cluster.UseSpotVMs=false, which also serves as the loop-breaker: only spot runs can preempt, so the retry can't itself qualify for another retry. Non-benchmark tests are unaffected. Release note: None Epic: none
1 parent cd9b33d commit 155a03d

4 files changed

Lines changed: 81 additions & 0 deletions

File tree

pkg/cmd/roachtest/test_impl.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ type testImpl struct {
148148
// githubIpToNodeMapping contains the ip to node map that will be passed to
149149
// github.MaybePost
150150
githubIpToNodeMapping string
151+
152+
// preempted is set when the test's failure was attributed to a VM
153+
// preemption by runTest's post-failure checks. Read by the test runner
154+
// after runTest returns to decide whether to requeue (e.g. retry a
155+
// benchmark on non-spot VMs).
156+
preempted bool
151157
}
152158
// Map from version to path to the cockroach binary to be used when
153159
// mixed-version test wants a binary for that binary. If a particular version
@@ -524,6 +530,21 @@ func (t *testImpl) resetFailures() {
524530
t.mu.failuresSuppressed = false
525531
}
526532

533+
// markPreempted records that this test's failure was attributed to a VM
534+
// preemption. See the comment on the preempted field for details.
535+
func (t *testImpl) markPreempted() {
536+
t.mu.Lock()
537+
defer t.mu.Unlock()
538+
t.mu.preempted = true
539+
}
540+
541+
// wasPreempted reports whether markPreempted was called.
542+
func (t *testImpl) wasPreempted() bool {
543+
t.mu.RLock()
544+
defer t.mu.RUnlock()
545+
return t.mu.preempted
546+
}
547+
527548
// We take the "squashed" error that contains information of all the errors for each failure.
528549
func formatFailure(b *strings.Builder, reportFailures ...failure) {
529550
for i, failure := range reportFailures {

pkg/cmd/roachtest/test_runner.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,20 @@ func (r *testRunner) runWorker(
11951195
}
11961196
}
11971197

1198+
// A preempted benchmark loses its per-nightly perf data point: the
1199+
// runner uses --count=1 and the test selector only forces re-selection
1200+
// on the *next* nightly. Requeue one retry on a non-spot cluster (which
1201+
// is also the loop-breaker, since only spot runs can preempt). Skip
1202+
// when --count>1: workPool.decTestLocked identifies entries by name,
1203+
// so a duplicate-named entry would race with the original's remaining
1204+
// runs.
1205+
if t.wasPreempted() && testToRun.spec.Benchmark &&
1206+
testToRun.spec.Cluster.UseSpotVMs && testToRun.runCount == 1 {
1207+
retrySpec := testToRun.spec
1208+
retrySpec.Cluster.UseSpotVMs = false
1209+
work.requeueForPreemptionRetry(ctx, workerL, retrySpec)
1210+
}
1211+
11981212
msg := "test passed: %s (run %d)"
11991213
if t.Failed() {
12001214
msg = "test failed: %s (run %d)"
@@ -1367,6 +1381,7 @@ func (r *testRunner) runTest(
13671381
// want to propagate the preemption error and avoid creating an issue.
13681382
t.resetFailures()
13691383
t.Error(vmPreemptionError(preemptedVMNames))
1384+
t.markPreempted()
13701385
}
13711386
hostErrorVMNames := getHostErrorVMNames(ctx, c, l)
13721387
if hostErrorVMNames != "" {

pkg/cmd/roachtest/test_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,36 @@ func TestVMPreemptionPolling(t *testing.T) {
11561156

11571157
require.NoError(t, err)
11581158
})
1159+
1160+
// Test that a benchmark whose spot VM gets preempted is automatically
1161+
// requeued for one retry. Without this, a single preemption silently
1162+
// loses the nightly's perf data point for that benchmark (the runner
1163+
// uses --count=1 and the test selector only forces re-selection on
1164+
// the *next* nightly). The retry runs on a non-spot cluster, which is
1165+
// also the loop-breaker: only spot runs can preempt.
1166+
t.Run("benchmark preempted on spot is requeued for retry", func(t *testing.T) {
1167+
// Drive preemption via the polling path: both the original and the
1168+
// retry's Run will block on ctx.Done() and be cancelled when
1169+
// monitorForPreemptedVMs observes the (always-on) preemption hook.
1170+
setPollPreemptionInterval(50 * time.Millisecond)
1171+
getPreemptedVMsHook = func(c cluster.Cluster, ctx context.Context, l *logger.Logger) ([]vm.PreemptedVM, error) {
1172+
return []vm.PreemptedVM{{Name: "test_node", PreemptedAt: time.Now()}}, nil
1173+
}
1174+
1175+
var calls atomic.Int32
1176+
benchmarkTest := mockTest
1177+
benchmarkTest.Benchmark = true
1178+
benchmarkTest.Run = func(ctx context.Context, t test.Test, c cluster.Cluster) {
1179+
calls.Add(1)
1180+
<-ctx.Done()
1181+
}
1182+
1183+
err := runner.Run(ctx, []registry.TestSpec{benchmarkTest}, 1, /* count */
1184+
defaultParallelism, copt, testOpts{}, lopt, github)
1185+
require.NoError(t, err)
1186+
require.Equal(t, int32(2), calls.Load(),
1187+
"benchmark should run twice: original (spot, preempted) + one non-spot retry")
1188+
})
11591189
}
11601190

11611191
// TestRunnerFailureAfterTimeout checks that a test has a failure added

pkg/cmd/roachtest/work_pool.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,21 @@ func (p *workPool) findCompatibleTestsLocked(
253253
return tests
254254
}
255255

256+
// requeueForPreemptionRetry adds spec back to the pool for exactly one
257+
// additional run. The entry is inserted at the head so workers pick it up
258+
// promptly. The caller is responsible for any spec adjustments needed to
259+
// break a retry loop (e.g. setting Cluster.UseSpotVMs=false so the retry
260+
// can't itself be preempted into another retry).
261+
func (p *workPool) requeueForPreemptionRetry(
262+
ctx context.Context, l *logger.Logger, spec registry.TestSpec,
263+
) {
264+
p.mu.Lock()
265+
defer p.mu.Unlock()
266+
l.PrintfCtx(ctx, "requeueing %s for preemption retry (UseSpotVMs=%t)",
267+
spec.Name, spec.Cluster.UseSpotVMs)
268+
p.mu.tests = append([]testWithCount{{spec: spec, count: 1}}, p.mu.tests...)
269+
}
270+
256271
// decTestLocked decrements a test's remaining count and removes it
257272
// from the workPool if it was exhausted.
258273
func (p *workPool) decTestLocked(ctx context.Context, l *logger.Logger, name string) {

0 commit comments

Comments
 (0)