Commit 29df52af authored by Cameron Ball's avatar Cameron Ball
Browse files

MDL-74548 backup: Refactor course copies

This patch modifies the way copy data is shared in order to mitigate potential race conditions
and ensure that the serialised controller stored in the DB is always in a valid state.

The restore controller is now considered the "source of truth" for all information about the
copy operation. Backup controllers can no longer contain information about course copies.

As copy creation is not atomic, it is still possible for copy controllers to become orphaned or
exist in an invalid state. To mitigate this the backup cleanup task has been modified to call
a new helper method copy_helper::cleanup_orphaned_copy_controllers.

Summary of changes in this patch:

    - Copy data must now be passed through the restore controller's constructor
    - base_controller::get_copy has been deprecated in favour of restore_controller::get_copy
    - base_controller::set_copy has been deprecated without replacement
    - core_backup\copy\copy has been deprecated, use copy_helper.class.php's copy_helper instead
    - backup_cleanup_task will now clean up orphaned controllers from copy operations that went awry

Thanks to Peter Burnett for assiting with testing this patch.
parent ceb41588
...@@ -65,6 +65,13 @@ class restore_controller extends base_controller { ...@@ -65,6 +65,13 @@ class restore_controller extends base_controller {
/** @var int Number of restore_controllers that are currently executing */ /** @var int Number of restore_controllers that are currently executing */
protected static $executing = 0; protected static $executing = 0;
/**
* Holds the relevant destination information for course copy operations.
*
* @var \stdClass.
*/
protected $copy;
/** /**
* Constructor. * Constructor.
* *
...@@ -79,10 +86,17 @@ class restore_controller extends base_controller { ...@@ -79,10 +86,17 @@ class restore_controller extends base_controller {
* @param int $userid * @param int $userid
* @param int $target backup::TARGET_[ NEW_COURSE | CURRENT_ADDING | CURRENT_DELETING | EXISTING_ADDING | EXISTING_DELETING ] * @param int $target backup::TARGET_[ NEW_COURSE | CURRENT_ADDING | CURRENT_DELETING | EXISTING_ADDING | EXISTING_DELETING ]
* @param \core\progress\base $progress Optional progress monitor * @param \core\progress\base $progress Optional progress monitor
* @param \stdClass $copydata Course copy data, required when in MODE_COPY
* @param bool $releasesession Should release the session? backup::RELEASESESSION_YES or backup::RELEASESESSION_NO * @param bool $releasesession Should release the session? backup::RELEASESESSION_YES or backup::RELEASESESSION_NO
*/ */
public function __construct($tempdir, $courseid, $interactive, $mode, $userid, $target, public function __construct($tempdir, $courseid, $interactive, $mode, $userid, $target,
\core\progress\base $progress = null, $releasesession = backup::RELEASESESSION_NO) { \core\progress\base $progress = null, $releasesession = backup::RELEASESESSION_NO, ?\stdClass $copydata = null) {
if ($mode == backup::MODE_COPY && is_null($copydata)) {
throw new restore_controller_exception('cannot_instantiate_missing_copydata');
}
$this->copy = $copydata;
$this->tempdir = $tempdir; $this->tempdir = $tempdir;
$this->courseid = $courseid; $this->courseid = $courseid;
$this->interactive = $interactive; $this->interactive = $interactive;
...@@ -563,6 +577,19 @@ class restore_controller extends base_controller { ...@@ -563,6 +577,19 @@ class restore_controller extends base_controller {
$this->progress->end_progress(); $this->progress->end_progress();
} }
/**
* Get the course copy data.
*
* @return \stdClass
*/
public function get_copy(): \stdClass {
if ($this->mode != backup::MODE_COPY) {
throw new restore_controller_exception('cannot_get_copy_wrong_mode');
}
return $this->copy;
}
// Protected API starts here // Protected API starts here
protected function calculate_restoreid() { protected function calculate_restoreid() {
......
...@@ -72,17 +72,15 @@ class controller_test extends \advanced_testcase { ...@@ -72,17 +72,15 @@ class controller_test extends \advanced_testcase {
} }
/** /**
* Test set copy method. * Test instantiating a restore controller for a course copy without providing copy data.
*
* @covers \restore_controller::__construct
*/ */
public function test_base_controller_set_copy() { public function test_restore_controller_copy_without_copydata() {
$this->expectException(\backup_controller_exception::class); $this->expectException(\restore_controller_exception::class);
$copy = new \stdClass();
// Set up controller as a non-copy operation.
$bc = new \backup_controller(backup::TYPE_1COURSE, $this->courseid, backup::FORMAT_MOODLE,
backup::INTERACTIVE_NO, backup::MODE_GENERAL, $this->userid, backup::RELEASESESSION_YES);
$bc->set_copy($copy); new \restore_controller(1729, $this->courseid, backup::INTERACTIVE_NO, backup::MODE_COPY,
$this->userid, backup::TARGET_NEW_COURSE);
} }
/* /*
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
*/ */
require_once('../config.php'); require_once('../config.php');
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
defined('MOODLE_INTERNAL') || die(); defined('MOODLE_INTERNAL') || die();
...@@ -71,8 +72,8 @@ if ($mform->is_cancelled()) { ...@@ -71,8 +72,8 @@ if ($mform->is_cancelled()) {
} else if ($mdata = $mform->get_data()) { } else if ($mdata = $mform->get_data()) {
// Process the form and create the copy task. // Process the form and create the copy task.
$backupcopy = new \core_backup\copy\copy($mdata); $copydata = \copy_helper::process_formdata($mdata);
$backupcopy->create_copy(); \copy_helper::create_copy($copydata);
if (!empty($mdata->submitdisplay)) { if (!empty($mdata->submitdisplay)) {
// Redirect to the copy progress overview. // Redirect to the copy progress overview.
......
...@@ -388,8 +388,8 @@ class core_backup_external extends external_api { ...@@ -388,8 +388,8 @@ class core_backup_external extends external_api {
if ($mdata) { if ($mdata) {
// Create the copy task. // Create the copy task.
$backupcopy = new \core_backup\copy\copy($mdata); $copydata = \copy_helper::process_formdata($mdata);
$copyids = $backupcopy->create_copy(); $copyids = \copy_helper::create_copy($copydata);
} else { } else {
throw new moodle_exception('copyformfail', 'backup'); throw new moodle_exception('copyformfail', 'backup');
} }
......
...@@ -163,15 +163,14 @@ class course_copy_test extends \advanced_testcase { ...@@ -163,15 +163,14 @@ class course_copy_test extends \advanced_testcase {
$formdata->role_3 = 3; $formdata->role_3 = 3;
$formdata->role_5 = 5; $formdata->role_5 = 5;
$coursecopy = new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
$result = $coursecopy->create_copy(); $result = \copy_helper::create_copy($copydata);
// Load the controllers, to extract the data we need. // Load the controllers, to extract the data we need.
$bc = \backup_controller::load_controller($result['backupid']); $bc = \backup_controller::load_controller($result['backupid']);
$rc = \restore_controller::load_controller($result['restoreid']); $rc = \restore_controller::load_controller($result['restoreid']);
// Check the backup controller. // Check the backup controller.
$this->assertEquals($result, $bc->get_copy()->copyids);
$this->assertEquals(backup::MODE_COPY, $bc->get_mode()); $this->assertEquals(backup::MODE_COPY, $bc->get_mode());
$this->assertEquals($this->course->id, $bc->get_courseid()); $this->assertEquals($this->course->id, $bc->get_courseid());
$this->assertEquals(backup::TYPE_1COURSE, $bc->get_type()); $this->assertEquals(backup::TYPE_1COURSE, $bc->get_type());
...@@ -180,7 +179,6 @@ class course_copy_test extends \advanced_testcase { ...@@ -180,7 +179,6 @@ class course_copy_test extends \advanced_testcase {
$newcourseid = $rc->get_courseid(); $newcourseid = $rc->get_courseid();
$newcourse = get_course($newcourseid); $newcourse = get_course($newcourseid);
$this->assertEquals($result, $rc->get_copy()->copyids);
$this->assertEquals(get_string('copyingcourse', 'backup'), $newcourse->fullname); $this->assertEquals(get_string('copyingcourse', 'backup'), $newcourse->fullname);
$this->assertEquals(get_string('copyingcourseshortname', 'backup'), $newcourse->shortname); $this->assertEquals(get_string('copyingcourseshortname', 'backup'), $newcourse->shortname);
$this->assertEquals(backup::MODE_COPY, $rc->get_mode()); $this->assertEquals(backup::MODE_COPY, $rc->get_mode());
...@@ -222,11 +220,11 @@ class course_copy_test extends \advanced_testcase { ...@@ -222,11 +220,11 @@ class course_copy_test extends \advanced_testcase {
$formdata2->shortname = 'tree'; $formdata2->shortname = 'tree';
// Create some copies. // Create some copies.
$coursecopy = new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
$result = $coursecopy->create_copy(); $result = \copy_helper::create_copy($copydata);
// Backup, awaiting. // Backup, awaiting.
$copies = \core_backup\copy\copy::get_copies($USER->id); $copies = \copy_helper::get_copies($USER->id);
$this->assertEquals($result['backupid'], $copies[0]->backupid); $this->assertEquals($result['backupid'], $copies[0]->backupid);
$this->assertEquals($result['restoreid'], $copies[0]->restoreid); $this->assertEquals($result['restoreid'], $copies[0]->restoreid);
$this->assertEquals(\backup::STATUS_AWAITING, $copies[0]->status); $this->assertEquals(\backup::STATUS_AWAITING, $copies[0]->status);
...@@ -236,7 +234,7 @@ class course_copy_test extends \advanced_testcase { ...@@ -236,7 +234,7 @@ class course_copy_test extends \advanced_testcase {
// Backup, in progress. // Backup, in progress.
$bc->set_status(\backup::STATUS_EXECUTING); $bc->set_status(\backup::STATUS_EXECUTING);
$copies = \core_backup\copy\copy::get_copies($USER->id); $copies = \copy_helper::get_copies($USER->id);
$this->assertEquals($result['backupid'], $copies[0]->backupid); $this->assertEquals($result['backupid'], $copies[0]->backupid);
$this->assertEquals($result['restoreid'], $copies[0]->restoreid); $this->assertEquals($result['restoreid'], $copies[0]->restoreid);
$this->assertEquals(\backup::STATUS_EXECUTING, $copies[0]->status); $this->assertEquals(\backup::STATUS_EXECUTING, $copies[0]->status);
...@@ -244,19 +242,19 @@ class course_copy_test extends \advanced_testcase { ...@@ -244,19 +242,19 @@ class course_copy_test extends \advanced_testcase {
// Restore, ready to process. // Restore, ready to process.
$bc->set_status(\backup::STATUS_FINISHED_OK); $bc->set_status(\backup::STATUS_FINISHED_OK);
$copies = \core_backup\copy\copy::get_copies($USER->id); $copies = \copy_helper::get_copies($USER->id);
$this->assertEquals($result['backupid'], $copies[0]->backupid); $this->assertEquals(null, $copies[0]->backupid);
$this->assertEquals($result['restoreid'], $copies[0]->restoreid); $this->assertEquals($result['restoreid'], $copies[0]->restoreid);
$this->assertEquals(\backup::STATUS_REQUIRE_CONV, $copies[0]->status); $this->assertEquals(\backup::STATUS_REQUIRE_CONV, $copies[0]->status);
$this->assertEquals(\backup::OPERATION_RESTORE, $copies[0]->operation); $this->assertEquals(\backup::OPERATION_RESTORE, $copies[0]->operation);
// No records. // No records.
$bc->set_status(\backup::STATUS_FINISHED_ERR); $bc->set_status(\backup::STATUS_FINISHED_ERR);
$copies = \core_backup\copy\copy::get_copies($USER->id); $copies = \copy_helper::get_copies($USER->id);
$this->assertEmpty($copies); $this->assertEmpty($copies);
$coursecopy2 = new \core_backup\copy\copy($formdata2); $copydata2 = \copy_helper::process_formdata($formdata2);
$result2 = $coursecopy2->create_copy(); $result2 = \copy_helper::create_copy($copydata2);
// Set the second copy to be complete. // Set the second copy to be complete.
$bc = \backup_controller::load_controller($result2['backupid']); $bc = \backup_controller::load_controller($result2['backupid']);
$bc->set_status(\backup::STATUS_FINISHED_OK); $bc->set_status(\backup::STATUS_FINISHED_OK);
...@@ -265,7 +263,7 @@ class course_copy_test extends \advanced_testcase { ...@@ -265,7 +263,7 @@ class course_copy_test extends \advanced_testcase {
$rc->set_status(\backup::STATUS_FINISHED_OK); $rc->set_status(\backup::STATUS_FINISHED_OK);
// No records. // No records.
$copies = \core_backup\copy\copy::get_copies($USER->id); $copies = \copy_helper::get_copies($USER->id);
$this->assertEmpty($copies); $this->assertEmpty($copies);
} }
...@@ -291,11 +289,11 @@ class course_copy_test extends \advanced_testcase { ...@@ -291,11 +289,11 @@ class course_copy_test extends \advanced_testcase {
$formdata->role_5 = 5; $formdata->role_5 = 5;
// Create some copies. // Create some copies.
$coursecopy = new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
$coursecopy->create_copy(); \copy_helper::create_copy($copydata);
// No copies match this course id. // No copies match this course id.
$copies = \core_backup\copy\copy::get_copies($USER->id, ($this->course->id + 1)); $copies = \copy_helper::get_copies($USER->id, ($this->course->id + 1));
$this->assertEmpty($copies); $this->assertEmpty($copies);
} }
...@@ -321,13 +319,13 @@ class course_copy_test extends \advanced_testcase { ...@@ -321,13 +319,13 @@ class course_copy_test extends \advanced_testcase {
$formdata->role_5 = 5; $formdata->role_5 = 5;
// Create some copies. // Create some copies.
$coursecopy = new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
$coursecopy->create_copy(); \copy_helper::create_copy($copydata);
delete_course($this->course->id, false); delete_course($this->course->id, false);
// No copies match this course id as it has been deleted. // No copies match this course id as it has been deleted.
$copies = \core_backup\copy\copy::get_copies($USER->id, ($this->course->id)); $copies = \copy_helper::get_copies($USER->id, ($this->course->id));
$this->assertEmpty($copies); $this->assertEmpty($copies);
} }
...@@ -353,8 +351,8 @@ class course_copy_test extends \advanced_testcase { ...@@ -353,8 +351,8 @@ class course_copy_test extends \advanced_testcase {
$formdata->role_5 = 5; $formdata->role_5 = 5;
// Create the course copy records and associated ad-hoc task. // Create the course copy records and associated ad-hoc task.
$coursecopy = new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
$copyids = $coursecopy->create_copy(); $copyids = \copy_helper::create_copy($copydata);
$courseid = $this->course->id; $courseid = $this->course->id;
...@@ -430,8 +428,8 @@ class course_copy_test extends \advanced_testcase { ...@@ -430,8 +428,8 @@ class course_copy_test extends \advanced_testcase {
$formdata->role_5 = 0; $formdata->role_5 = 0;
// Create the course copy records and associated ad-hoc task. // Create the course copy records and associated ad-hoc task.
$coursecopy = new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
$copyids = $coursecopy->create_copy(); $copyids = \copy_helper::create_copy($copydata);
$courseid = $this->course->id; $courseid = $this->course->id;
...@@ -499,8 +497,8 @@ class course_copy_test extends \advanced_testcase { ...@@ -499,8 +497,8 @@ class course_copy_test extends \advanced_testcase {
$formdata->role_5 = 5; $formdata->role_5 = 5;
// Create the course copy records and associated ad-hoc task. // Create the course copy records and associated ad-hoc task.
$coursecopy = new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
$copyids = $coursecopy->create_copy(); $copyids = \copy_helper::create_copy($copydata);
$courseid = $this->course->id; $courseid = $this->course->id;
...@@ -568,8 +566,8 @@ class course_copy_test extends \advanced_testcase { ...@@ -568,8 +566,8 @@ class course_copy_test extends \advanced_testcase {
$formdata->role_5 = 5; $formdata->role_5 = 5;
// Create the course copy records and associated ad-hoc task. // Create the course copy records and associated ad-hoc task.
$coursecopy = new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
$copyids = $coursecopy->create_copy(); $copyids = \copy_helper::create_copy($copydata);
$courseid = $this->course->id; $courseid = $this->course->id;
...@@ -627,6 +625,7 @@ class course_copy_test extends \advanced_testcase { ...@@ -627,6 +625,7 @@ class course_copy_test extends \advanced_testcase {
// Expect and exception as form data is incomplete. // Expect and exception as form data is incomplete.
$this->expectException(\moodle_exception::class); $this->expectException(\moodle_exception::class);
new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
\copy_helper::create_copy($copydata);
} }
} }
...@@ -82,8 +82,8 @@ class externallib_test extends externallib_advanced_testcase { ...@@ -82,8 +82,8 @@ class externallib_test extends externallib_advanced_testcase {
$formdata->role_3 = 3; $formdata->role_3 = 3;
$formdata->role_5 = 5; $formdata->role_5 = 5;
$coursecopy = new \core_backup\copy\copy($formdata); $copydata = \copy_helper::process_formdata($formdata);
$copydetails = $coursecopy->create_copy(); $copydetails = \copy_helper::create_copy($copydata);
$copydetails['operation'] = \backup::OPERATION_BACKUP; $copydetails['operation'] = \backup::OPERATION_BACKUP;
$params = array('copies' => $copydetails); $params = array('copies' => $copydetails);
......
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
/**
* Copy helper class.
*
* @package core_backup
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Cameron Ball <cameron@cameron1729.xyz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
final class copy_helper {
/**
* Process raw form data from copy_form.
*
* @param \stdClass $formdata Raw formdata
* @return \stdClass Processed data for use with create_copy
*/
public static function process_formdata(\stdClass $formdata): \stdClass {
$requiredfields = [
'courseid', // Course id integer.
'fullname', // Fullname of the destination course.
'shortname', // Shortname of the destination course.
'category', // Category integer ID that contains the destination course.
'visible', // Integer to detrmine of the copied course will be visible.
'startdate', // Integer timestamp of the start of the destination course.
'enddate', // Integer timestamp of the end of the destination course.
'idnumber', // ID of the destination course.
'userdata', // Integer to determine if the copied course will contain user data.
];
$missingfields = array_diff($requiredfields, array_keys((array)$formdata));
if ($missingfields) {
throw new \moodle_exception('copyfieldnotfound', 'backup', '', null, implode(", ", $missingfields));
}
// Remove any extra stuff in the form data.
$processed = (object)array_intersect_key((array)$formdata, array_flip($requiredfields));
$processed->keptroles = [];
// Extract roles from the form data and add to keptroles.
foreach ($formdata as $key => $value) {
if ((substr($key, 0, 5) === 'role_') && ($value != 0)) {
$processed->keptroles[] = $value;
}
}
return $processed;
}
/**
* Creates a course copy.
* Sets up relevant controllers and adhoc task.
*
* @param \stdClass $copydata Course copy data from process_formdata
* @return array $copyids The backup and restore controller ids
*/
public static function create_copy(\stdClass $copydata): array {
global $USER;
$copyids = [];
// Create the initial backupcontoller.
$bc = new \backup_controller(\backup::TYPE_1COURSE, $copydata->courseid, \backup::FORMAT_MOODLE,
\backup::INTERACTIVE_NO, \backup::MODE_COPY, $USER->id, \backup::RELEASESESSION_YES);
$copyids['backupid'] = $bc->get_backupid();
// Create the initial restore contoller.
list($fullname, $shortname) = \restore_dbops::calculate_course_names(
0, get_string('copyingcourse', 'backup'), get_string('copyingcourseshortname', 'backup'));
$newcourseid = \restore_dbops::create_new_course($fullname, $shortname, $copydata->category);
$rc = new \restore_controller($copyids['backupid'], $newcourseid, \backup::INTERACTIVE_NO,
\backup::MODE_COPY, $USER->id, \backup::TARGET_NEW_COURSE, null,
\backup::RELEASESESSION_NO, $copydata);
$copyids['restoreid'] = $rc->get_restoreid();
$bc->set_status(\backup::STATUS_AWAITING);
$bc->get_status();
$rc->save_controller();
// Create the ad-hoc task to perform the course copy.
$asynctask = new \core\task\asynchronous_copy_task();
$asynctask->set_blocking(false);
$asynctask->set_custom_data($copyids);
\core\task\manager::queue_adhoc_task($asynctask);
// Clean up the controller.
$bc->destroy();
return $copyids;
}
/**
* Get the in progress course copy operations for a user.
*
* @param int $userid User id to get the course copies for.
* @param int|null $courseid The optional source course id to get copies for.
* @return array $copies Details of the inprogress copies.
*/
public static function get_copies(int $userid, ?int $courseid = null): array {
global $DB;
$copies = [];
[$insql, $inparams] = $DB->get_in_or_equal([\backup::STATUS_FINISHED_OK, \backup::STATUS_FINISHED_ERR]);
$params = [
$userid,
\backup::EXECUTION_DELAYED,
\backup::MODE_COPY,
\backup::OPERATION_BACKUP,
\backup::STATUS_FINISHED_OK,
\backup::OPERATION_RESTORE
];
// We exclude backups that finished with OK. Therefore if a backup is missing,
// we can assume it finished properly.
//
// We exclude both failed and successful restores because both of those indicate that the whole
// operation has completed.
$sql = 'SELECT backupid, itemid, operation, status, timecreated, purpose
FROM {backup_controllers}
WHERE userid = ?
AND execution = ?
AND purpose = ?
AND ((operation = ? AND status <> ?) OR (operation = ? AND status NOT ' . $insql .'))
ORDER BY timecreated DESC';
$copyrecords = $DB->get_records_sql($sql, array_merge($params, $inparams));
$idtorc = self::map_backupids_to_restore_controller($copyrecords);
// Our SQL only gets controllers that have not finished successfully.
// So, no restores => all restores have finished (either failed or OK) => all backups have too
// Therefore there are no in progress copy operations, return early.
if (empty($idtorc)) {
return [];
}
foreach ($copyrecords as $copyrecord) {
try {
$isbackup = $copyrecord->operation == \backup::OPERATION_BACKUP;
// The mapping is guaranteed to exist for restore controllers, but not
// backup controllers.
//
// When processing backups we don't actually need it, so we just coalesce
// to null.
$rc = $idtorc[$copyrecord->backupid] ?? null;
$cid = $isbackup ? $copyrecord->itemid : $rc->get_copy()->courseid;
$course = get_course($cid);
$copy = clone ($copyrecord);
$copy->backupid = $isbackup ? $copyrecord->backupid : null;
$copy->restoreid = $rc ? $rc->get_restoreid() : null;
$copy->destination = $rc ? $rc->get_copy()->shortname : null;
$copy->source = $course->shortname;
$copy->sourceid = $course->id;
} catch (\Exception $e) {
continue;
}
// Filter out anything that's not relevant.
if ($courseid) {
if ($isbackup && $copyrecord->itemid != $courseid) {
continue;
}
if (!$isbackup && $rc->get_copy()->courseid != $courseid) {
continue;
}
}
// A backup here means that the associated restore controller has not started.
//
// There's a few situations to consider:
//
// 1. The backup is waiting or in progress
// 2. The backup failed somehow
// 3. Something went wrong (e.g., solar flare) and the backup controller saved, but the restore controller didn't
// 4. The restore hasn't been created yet (race condition)
//
// In the case of 1, we add it to the return list. In the case of 2, 3 and 4 we just ignore it and move on.
// The backup cleanup task will take care of updating/deleting invalid controllers.
if ($isbackup) {
if ($copyrecord->status != \backup::STATUS_FINISHED_ERR && !is_null($rc)) {