diff --git a/backup/tests/async_backup_test.php b/backup/tests/async_backup_test.php index 509c83cf25f9..abf6aabf4346 100644 --- a/backup/tests/async_backup_test.php +++ b/backup/tests/async_backup_test.php @@ -41,7 +41,7 @@ class core_backup_async_backup_testcase extends \core_privacy\tests\provider_tes * Tests the asynchronous backup. */ public function test_async_backup() { - global $DB, $CFG, $USER; + global $CFG, $DB, $USER; $this->resetAfterTest(true); $this->setAdminUser(); @@ -79,8 +79,6 @@ public function test_async_backup() { $DB->set_field('course_sections', 'availability', $availability, array( 'course' => $course->id, 'section' => 1)); - // Start backup process. - // Make the backup controller for an async backup. $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_YES, backup::MODE_ASYNC, $USER->id); @@ -97,7 +95,7 @@ public function test_async_backup() { // Create the adhoc task. $asynctask = new \core\task\asynchronous_backup_task(); $asynctask->set_blocking(false); - $asynctask->set_custom_data(array('backupid' => $backupid)); + $asynctask->set_custom_data(['backupid' => $backupid]); \core\task\manager::queue_adhoc_task($asynctask); // We are expecting trace output during this test. @@ -116,4 +114,84 @@ public function test_async_backup() { $this->assertEquals(backup::STATUS_FINISHED_OK, $postbackuprec->status); $this->assertEquals(1.0, $postbackuprec->progress); } + + /** + * Tests the asynchronous backup will resolve in duplicate cases. + */ + public function test_complete_async_backup() { + global $CFG, $DB, $USER; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $CFG->enableavailability = true; + $CFG->enablecompletion = true; + + // Create a course with some availability data set. + $generator = $this->getDataGenerator(); + $course = $generator->create_course( + array('format' => 'topics', 'numsections' => 3, + 'enablecompletion' => COMPLETION_ENABLED), + array('createsections' => true)); + $forum = $generator->create_module('forum', array( + 'course' => $course->id)); + $forum2 = $generator->create_module('forum', array( + 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); + + // We need a grade, easiest is to add an assignment. + $assignrow = $generator->create_module('assign', array( + 'course' => $course->id)); + $assign = new assign(context_module::instance($assignrow->cmid), false, false); + $item = $assign->get_grade_item(); + + // Make a test grouping as well. + $grouping = $generator->create_grouping(array('courseid' => $course->id, + 'name' => 'Grouping!')); + + $availability = '{"op":"|","show":false,"c":[' . + '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . + '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . + '{"type":"grouping","id":' . $grouping->id . '}' . + ']}'; + $DB->set_field('course_modules', 'availability', $availability, array( + 'id' => $forum->cmid)); + $DB->set_field('course_sections', 'availability', $availability, array( + 'course' => $course->id, 'section' => 1)); + + // Make the backup controller for an async backup. + $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_YES, backup::MODE_ASYNC, $USER->id); + $bc->finish_ui(); + $backupid = $bc->get_backupid(); + $bc->destroy(); + + // Now hack the record to remove the controller and set the status fields to complete. + // This emulates a duplicate run for an already finished controller. + $id = $DB->get_field('backup_controllers', 'id', ['backupid' => $backupid]); + $data = [ + 'id' => $id, + 'controller' => '', + 'progress' => 1.0, + 'status' => backup::STATUS_FINISHED_OK + ]; + $DB->update_record('backup_controllers', $data); + + // Now queue an adhoc task and check it handles and completes gracefully. + $asynctask = new \core\task\asynchronous_backup_task(); + $asynctask->set_blocking(false); + $asynctask->set_custom_data(array('backupid' => $backupid)); + \core\task\manager::queue_adhoc_task($asynctask); + + // We are expecting a specific message output during this test. + $this->expectOutputRegex('/invalid controller/'); + + // Execute adhoc task. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertInstanceOf('\\core\\task\\asynchronous_backup_task', $task); + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + + // Check the task record is removed. + $this->assertEquals(0, $DB->count_records('task_adhoc')); + } } diff --git a/backup/tests/async_restore_test.php b/backup/tests/async_restore_test.php index 1121e371ad51..7bf826653e49 100644 --- a/backup/tests/async_restore_test.php +++ b/backup/tests/async_restore_test.php @@ -41,7 +41,7 @@ class core_backup_async_restore_testcase extends \core_privacy\tests\provider_te * Tests the asynchronous backup. */ public function test_async_restore() { - global $DB, $CFG, $USER; + global $CFG, $USER, $DB; $this->resetAfterTest(true); $this->setAdminUser(); @@ -106,7 +106,6 @@ public function test_async_restore() { $rc = new restore_controller($backupdir, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_ASYNC, $USER->id, backup::TARGET_NEW_COURSE); - $this->assertTrue($rc->execute_precheck()); $restoreid = $rc->get_restoreid(); @@ -137,4 +136,128 @@ public function test_async_restore() { $this->assertEquals(backup::STATUS_FINISHED_OK, $postrestorerec->status); $this->assertEquals(1.0, $postrestorerec->progress); } + + /** + * Tests the asynchronous restore will resolve in duplicate cases where the controller is already removed. + */ + public function test_async_restore_missing_controller() { + global $CFG, $USER, $DB; + + $this->resetAfterTest(true); + $this->setAdminUser(); + $CFG->enableavailability = true; + $CFG->enablecompletion = true; + + // Create a course with some availability data set. + $generator = $this->getDataGenerator(); + $course = $generator->create_course( + array('format' => 'topics', 'numsections' => 3, + 'enablecompletion' => COMPLETION_ENABLED), + array('createsections' => true)); + $forum = $generator->create_module('forum', array( + 'course' => $course->id)); + $forum2 = $generator->create_module('forum', array( + 'course' => $course->id, 'completion' => COMPLETION_TRACKING_MANUAL)); + + // We need a grade, easiest is to add an assignment. + $assignrow = $generator->create_module('assign', array( + 'course' => $course->id)); + $assign = new assign(context_module::instance($assignrow->cmid), false, false); + $item = $assign->get_grade_item(); + + // Make a test grouping as well. + $grouping = $generator->create_grouping(array('courseid' => $course->id, + 'name' => 'Grouping!')); + + $availability = '{"op":"|","show":false,"c":[' . + '{"type":"completion","cm":' . $forum2->cmid .',"e":1},' . + '{"type":"grade","id":' . $item->id . ',"min":4,"max":94},' . + '{"type":"grouping","id":' . $grouping->id . '}' . + ']}'; + $DB->set_field('course_modules', 'availability', $availability, array( + 'id' => $forum->cmid)); + $DB->set_field('course_sections', 'availability', $availability, array( + 'course' => $course->id, 'section' => 1)); + + // Backup the course. + $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_YES, backup::MODE_GENERAL, $USER->id); + $bc->finish_ui(); + $bc->execute_plan(); + $bc->destroy(); + + // Get the backup file. + $coursecontext = context_course::instance($course->id); + $fs = get_file_storage(); + $files = $fs->get_area_files($coursecontext->id, 'backup', 'course', false, 'id ASC'); + $backupfile = reset($files); + + // Extract backup file. + $backupdir = "restore_" . uniqid(); + $path = $CFG->tempdir . DIRECTORY_SEPARATOR . "backup" . DIRECTORY_SEPARATOR . $backupdir; + + $fp = get_file_packer('application/vnd.moodle.backup'); + $fp->extract_to_pathname($backupfile, $path); + + // Create restore controller. + $newcourseid = restore_dbops::create_new_course( + $course->fullname, $course->shortname . '_2', $course->category); + $rc = new restore_controller($backupdir, $newcourseid, + backup::INTERACTIVE_NO, backup::MODE_ASYNC, $USER->id, + backup::TARGET_NEW_COURSE); + $restoreid = $rc->get_restoreid(); + $controllerid = $DB->get_field('backup_controllers', 'id', ['backupid' => $restoreid]); + + // Now hack the record to remove the controller and set the status fields to complete. + // This emulates a duplicate run for an already finished controller. + $data = [ + 'id' => $controllerid, + 'controller' => '', + 'progress' => 1.0, + 'status' => backup::STATUS_FINISHED_OK + ]; + $DB->update_record('backup_controllers', $data); + $rc->destroy(); + + // Create the adhoc task. + $asynctask = new \core\task\asynchronous_restore_task(); + $asynctask->set_blocking(false); + $asynctask->set_custom_data(['backupid' => $restoreid]); + \core\task\manager::queue_adhoc_task($asynctask); + + // We are expecting a specific message output during this test. + $this->expectOutputRegex('/invalid controller/'); + + // Execute adhoc task. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task); + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + + // Check the task record is removed. + $this->assertEquals(0, $DB->count_records('task_adhoc')); + + // Now delete the record and confirm an entirely missing controller is handled. + $DB->delete_records('backup_controllers'); + + // Create the adhoc task. + $asynctask = new \core\task\asynchronous_restore_task(); + $asynctask->set_blocking(false); + $asynctask->set_custom_data(['backupid' => $restoreid]); + \core\task\manager::queue_adhoc_task($asynctask); + + // We are expecting a specific message output during this test. + $this->expectOutputRegex('/Unable to find restore controller/'); + + // Execute adhoc task. + $now = time(); + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertInstanceOf('\\core\\task\\asynchronous_restore_task', $task); + $task->execute(); + \core\task\manager::adhoc_task_complete($task); + + // Check the task record is removed. + $this->assertEquals(0, $DB->count_records('task_adhoc')); + } } diff --git a/lib/classes/task/asynchronous_backup_task.php b/lib/classes/task/asynchronous_backup_task.php index 73554a3ae401..3df9f51a31e4 100644 --- a/lib/classes/task/asynchronous_backup_task.php +++ b/lib/classes/task/asynchronous_backup_task.php @@ -48,12 +48,16 @@ public function execute() { $started = time(); $backupid = $this->get_custom_data()->backupid; - $backuprecordid = $DB->get_field('backup_controllers', 'id', array('backupid' => $backupid), MUST_EXIST); + $backuprecord = $DB->get_record('backup_controllers', array('backupid' => $backupid), 'id, controller', MUST_EXIST); mtrace('Processing asynchronous backup for backup: ' . $backupid); - // Get the backup controller by backup id. + // Get the backup controller by backup id. If controller is invalid, this task can never complete. + if ($backuprecord->controller === '') { + mtrace('Bad backup controller status, invalid controller, ending backup execution.'); + return; + } $bc = \backup_controller::load_controller($backupid); - $bc->set_progress(new \core\progress\db_updater($backuprecordid, 'backup_controllers', 'progress')); + $bc->set_progress(new \core\progress\db_updater($backuprecord->id, 'backup_controllers', 'progress')); // Do some preflight checks on the backup. $status = $bc->get_status(); @@ -87,4 +91,3 @@ public function execute() { mtrace('Backup completed in: ' . $duration . ' seconds'); } } - diff --git a/lib/classes/task/asynchronous_restore_task.php b/lib/classes/task/asynchronous_restore_task.php index 9572ba05ed3a..fcd3b2a5482f 100644 --- a/lib/classes/task/asynchronous_restore_task.php +++ b/lib/classes/task/asynchronous_restore_task.php @@ -47,12 +47,22 @@ public function execute() { $started = time(); $restoreid = $this->get_custom_data()->backupid; - $restorerecordid = $DB->get_field('backup_controllers', 'id', array('backupid' => $restoreid), MUST_EXIST); + $restorerecord = $DB->get_record('backup_controllers', array('backupid' => $restoreid), 'id, controller', IGNORE_MISSING); + // If the record doesn't exist, the backup controller failed to create. Unable to proceed. + if (empty($restorerecord)) { + mtrace('Unable to find restore controller, ending restore execution.'); + return; + } + mtrace('Processing asynchronous restore for id: ' . $restoreid); - // Get the restore controller by backup id. + // Get the backup controller by backup id. If controller is invalid, this task can never complete. + if ($restorerecord->controller === '') { + mtrace('Bad restore controller status, invalid controller, ending restore execution.'); + return; + } $rc = \restore_controller::load_controller($restoreid); - $rc->set_progress(new \core\progress\db_updater($restorerecordid, 'backup_controllers', 'progress')); + $rc->set_progress(new \core\progress\db_updater($restorerecord->id, 'backup_controllers', 'progress')); // Do some preflight checks on the restore. $status = $rc->get_status(); @@ -86,4 +96,3 @@ public function execute() { mtrace('Restore completed in: ' . $duration . ' seconds'); } } -