@samitouri / QOSamiQemu / commits / 438ee08bc9

tests/unit/test-blockjob: cover keeping a job paused while a pause is pending

Add a regression test for the previous commit. A job that has reached its pause point is spuriously re-entered (job_enter()) while a pause is still pending (pause_count > 0), reproducing what an overlapping drain does: one drain's job_resume() wakes the job while the next drain's job_pause() is already counted. The job must stay parked - it must not run job code or clear job->paused, or job_set_aio_context() could observe paused == false and abort. The test counts the job's run-loop iterations: without the fix the re-entered job clears job->paused, runs one iteration and re-pauses, so the counter advances; with the fix it stays parked and the counter is unchanged. It runs in the main AioContext, so job_enter() is synchronous and the check is deterministic. Signed-off-by: Denis V. Lunev <den@openvz.org> Message-ID: <20260623152406.1180235-3-den@openvz.org> Reviewed-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> Tested-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru> Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>

Denis V. Lunev committed Jun 23, 2026 at 17:24 UTC 438ee08bc97af48ade6365133f2f5b43d4d788ae
1 file changed +100
tests/unit/test-blockjob.c
+100
@@ -388,6 +388,105 @@ static void test_cancel_concluded(void)
388 cancel_common(s);
389 }
390
391 +typedef struct PauseCountJob {
392 + BlockJob common;
393 + int n;
394 + bool should_complete;
395 +} PauseCountJob;
396 +
397 +static void pause_count_job_complete(Job *job, Error **errp)
398 +{
399 + PauseCountJob *s = container_of(job, PauseCountJob, common.job);
400 + s->should_complete = true;
401 +}
402 +
403 +static int coroutine_fn pause_count_job_run(Job *job, Error **errp)
404 +{
405 + PauseCountJob *s = container_of(job, PauseCountJob, common.job);
406 +
407 + while (!s->should_complete) {
408 + if (job_is_cancelled(&s->common.job)) {
409 + return 0;
410 + }
411 + s->n++;
412 + /*
413 + * Yields; while a pause is pending the yield is skipped and the job
414 + * parks in job_pause_point() instead.
415 + */
416 + job_sleep_ns(&s->common.job, 10 * 1000 * 1000);
417 + }
418 +
419 + return 0;
420 +}
421 +
422 +static const BlockJobDriver pause_count_job_driver = {
423 + .job_driver = {
424 + .instance_size = sizeof(PauseCountJob),
425 + .free = block_job_free,
426 + .user_resume = block_job_user_resume,
427 + .run = pause_count_job_run,
428 + .complete = pause_count_job_complete,
429 + },
430 +};
431 +
432 +/*
433 + * A job that has reached its pause point must stay paused while a pause is
434 + * still pending (pause_count > 0). An overlapping drain re-enters the job (one
435 + * drain's job_resume() wakes it while the next drain's job_pause() is already
436 + * counted); the job must not run or clear job->paused, otherwise
437 + * job_set_aio_context() can observe paused == false and abort.
438 + */
439 +static void test_pause_keeps_paused(void)
440 +{
441 + BlockBackend *blk;
442 + BlockJob *bjob;
443 + PauseCountJob *s;
444 + Job *job;
445 + int n0;
446 +
447 + blk = create_blk(NULL);
448 + bjob = mk_job(blk, "job0", &pause_count_job_driver, true,
449 + JOB_MANUAL_FINALIZE | JOB_MANUAL_DISMISS);
450 + s = container_of(bjob, PauseCountJob, common);
451 + job = &bjob->job;
452 + WITH_JOB_LOCK_GUARD() {
453 + job_ref_locked(job);
454 + }
455 +
456 + job_start(job);
457 +
458 + /* Pause the running job; it parks in job_pause_point() with paused set. */
459 + WITH_JOB_LOCK_GUARD() {
460 + job_pause_locked(job);
461 + g_assert_true(job->paused);
462 + g_assert_cmpint(job->status, ==, JOB_STATUS_PAUSED);
463 + }
464 + n0 = s->n;
465 +
466 + /*
467 + * Spurious wake while the pause is still pending. The job must stay parked:
468 + * the bug clears job->paused, runs an iteration (s->n advances) and
469 + * re-pauses, exposing a paused == false window.
470 + */
471 + job_enter(job);
472 + WITH_JOB_LOCK_GUARD() {
473 + g_assert_true(job->paused);
474 + }
475 + g_assert_cmpint(s->n, ==, n0);
476 +
477 + /* Resume and tear down. */
478 + WITH_JOB_LOCK_GUARD() {
479 + job_resume_locked(job);
480 + }
481 + job_cancel_sync(job, true);
482 + WITH_JOB_LOCK_GUARD() {
483 + Job *dummy = job;
484 + job_dismiss_locked(&dummy, &error_abort);
485 + job_unref_locked(job);
486 + }
487 + destroy_blk(blk);
488 +}
489 +
490 int main(int argc, char **argv)
491 {
492 qemu_init_main_loop(&error_abort);
@@ -402,5 +501,6 @@ int main(int argc, char **argv)
501 g_test_add_func("/blockjob/cancel/standby", test_cancel_standby);
502 g_test_add_func("/blockjob/cancel/pending", test_cancel_pending);
503 g_test_add_func("/blockjob/cancel/concluded", test_cancel_concluded);
504 + g_test_add_func("/blockjob/pause/keep_paused", test_pause_keeps_paused);
505 return g_test_run();
506 }