Skip to content

Commit

Permalink
Move autoremove builds logic to a scheduled job
Browse files Browse the repository at this point in the history
Separate the automatic removal of old builds from CDash's submission parsing
logic. Instead, old builds will now be periodically cleaned up as a scheduled
job. We use a new, lower priority queue to schedule these pruning jobs so they
don't interfere with asychronous submission parsing.

This commit moves the remove_builds function (and related helper functions)
from common.php to DatabaseCleanupUtils.

It also removes the "Cleanup database" from upgrade.php and moves this
functionality to a new Artisan command instead: db:cleanup. We use this new
db:cleanup command to remove shared records, such as testoutput. This allows
us to remove a big chunk of custom logic from our removeBuilds funcion.

While writing this commit, the following tables were already handled by db:cleanup:
  - buildfailuredetails
  - configure
  - configureerror
  - coveragefile
  - test2image

The following tables represent potentially shared data that wasn't already handled
by db:cleanup:
  - note
  - buildupdate
  - testoutput
  - updatefile
  - image
  - uploadfile

The following tables were found to already have cascade-on-delete foreign keys,
and thus their explicit DELETE logic was deemed safe to remove:
  - build2uploadfile
  - dynamicanalysisdefect
  - label2dynamicanalysis
  - label2buildfailure
  • Loading branch information
zackgalbreath committed Nov 15, 2024
1 parent 236cfd4 commit c0ed119
Show file tree
Hide file tree
Showing 52 changed files with 388 additions and 835 deletions.
90 changes: 90 additions & 0 deletions app/Console/Commands/CleanupDatabase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

class CleanupDatabase extends Command
{
/**
* The name and signature of the console command.
*/
protected $signature = 'db:cleanup';

/**
* The console command description.
*/
protected $description = 'Prune unused records from the CDash database';

/**
* Execute the console command.
*/
public function handle(): void
{
DB::delete("DELETE FROM banner WHERE projectid != 0 AND projectid NOT IN (SELECT id FROM project)");
self::delete_unused_rows('dailyupdate', 'projectid', 'project');

self::delete_unused_rows('buildfailuredetails', 'id', 'buildfailure', 'detailsid');
self::delete_unused_rows('configure', 'id', 'build2configure', 'configureid');
self::delete_unused_rows('configureerror', 'configureid', 'configure');
self::delete_unused_rows('dailyupdatefile', 'dailyupdateid', 'dailyupdate');
self::delete_unused_rows('note', 'id', 'build2note', 'noteid');
self::delete_unused_rows('testoutput', 'id', 'build2test', 'outputid');
self::delete_unused_rows('updatefile', 'updateid', 'buildupdate');
self::delete_unused_rows('uploadfile', 'id', 'build2uploadfile', 'fileid');

self::delete_unused_rows('subproject2subproject', 'subprojectid', 'subproject');

self::delete_unused_rows('coveragefile', 'id', 'coverage', 'fileid');

self::delete_unused_rows('test2image', 'outputid', 'testoutput');

DB::delete("DELETE FROM image WHERE
id NOT IN (SELECT imageid FROM project) AND
id NOT IN (SELECT imgid FROM test2image)");
}

/** Delete unused rows in batches */
private static function delete_unused_rows(string $table, string $field, string $targettable, string $selectfield = 'id'): void
{
$start = DB::table($table)->min($field);
$max = DB::table($table)->max($field);
if (!is_numeric($start) || !is_numeric($max)) {
echo "Could not determine min and max for $field on $table\n";
return;
}

$start = intval($start);
$max = intval($max);

$total = $max - $start;
if ($total < 1) {
return;
}
$num_done = 0;
$next_report = 10;
$done = false;
echo "Pruning unused rows from $table\n";
while (!$done) {
$end = $start + 49999;
DB::delete("
DELETE FROM $table
WHERE $field BETWEEN $start AND $end
AND $field NOT IN (SELECT $selectfield FROM $targettable)");
$num_done += 50000;
if ($end >= $max) {
$done = true;
} else {
usleep(1);
$start += 50000;
// Calculate percentage of work completed so far.
$percent = round(($num_done / $total) * 100, -1);
if ($percent > $next_report) {
echo "{$percent}%\n";
$next_report = $next_report + 10;
}
}
}
}
}
14 changes: 12 additions & 2 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace App\Console;

use App\Jobs\PruneAuthTokens;
use App\Jobs\PruneBuilds;
use App\Jobs\PruneDatabase;
use App\Jobs\PruneJobs;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
Expand All @@ -25,11 +27,19 @@ protected function schedule(Schedule $schedule)
->everySixHours()
->sendOutputTo($output_filename);

$schedule->job(new PruneJobs())
$schedule->job(new PruneAuthTokens(), 'low')
->hourly()
->withoutOverlapping();

$schedule->job(new PruneAuthTokens())
$schedule->job(new PruneBuilds(), 'low')
->hourly()
->withoutOverlapping();

$schedule->job(new PruneDatabase(), 'low')
->dailyAt('03:00')
->withoutOverlapping();

$schedule->job(new PruneJobs(), 'low')
->hourly()
->withoutOverlapping();

Expand Down
87 changes: 2 additions & 85 deletions app/Http/Controllers/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace App\Http\Controllers;

use App\Models\User;
use App\Utils\DatabaseCleanupUtils;
use CDash\Model\Project;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\DB;
Expand Down Expand Up @@ -96,7 +97,7 @@ public function removeBuilds(): View|RedirectResponse
$builds[] = (int) $build_array->id;
}

remove_build_chunked($builds);
DatabaseCleanupUtils::removeBuildChunked($builds);
$alert = 'Removed ' . count($builds) . ' builds.';
}

Expand Down Expand Up @@ -325,12 +326,9 @@ public function upgrade()

@$AssignBuildToDefaultGroups = $_POST['AssignBuildToDefaultGroups'];
@$FixBuildBasedOnRule = $_POST['FixBuildBasedOnRule'];
@$DeleteBuildsWrongDate = $_POST['DeleteBuildsWrongDate'];
@$CheckBuildsWrongDate = $_POST['CheckBuildsWrongDate'];
@$ComputeTestTiming = $_POST['ComputeTestTiming'];
@$ComputeUpdateStatistics = $_POST['ComputeUpdateStatistics'];

@$Cleanup = $_POST['Cleanup'];
@$Dependencies = $_POST['Dependencies'];
@$Audit = $_POST['Audit'];
@$ClearAudit = $_POST['Clear'];
Expand Down Expand Up @@ -376,80 +374,6 @@ public function upgrade()
unlink($configFile);
}


/* Cleanup the database */
if ($Cleanup) {
self::delete_unused_rows('banner', 'projectid', 'project');
self::delete_unused_rows('blockbuild', 'projectid', 'project');
self::delete_unused_rows('build', 'projectid', 'project');
self::delete_unused_rows('buildgroup', 'projectid', 'project');
self::delete_unused_rows('labelemail', 'projectid', 'project');
self::delete_unused_rows('project2repositories', 'projectid', 'project');
self::delete_unused_rows('dailyupdate', 'projectid', 'project');
self::delete_unused_rows('subproject', 'projectid', 'project');
self::delete_unused_rows('coveragefilepriority', 'projectid', 'project');
self::delete_unused_rows('user2project', 'projectid', 'project');
self::delete_unused_rows('userstatistics', 'projectid', 'project');

self::delete_unused_rows('build2configure', 'buildid', 'build');
self::delete_unused_rows('build2note', 'buildid', 'build');
self::delete_unused_rows('build2test', 'buildid', 'build');
self::delete_unused_rows('buildemail', 'buildid', 'build');
self::delete_unused_rows('builderror', 'buildid', 'build');
self::delete_unused_rows('builderrordiff', 'buildid', 'build');
self::delete_unused_rows('buildfailure', 'buildid', 'build');
self::delete_unused_rows('buildfailuredetails', 'id', 'buildfailure', 'detailsid');
self::delete_unused_rows('buildtesttime', 'buildid', 'build');
self::delete_unused_rows('configure', 'id', 'build2configure', 'configureid');
self::delete_unused_rows('configureerror', 'configureid', 'configure');
self::delete_unused_rows('configureerrordiff', 'buildid', 'build');
self::delete_unused_rows('coverage', 'buildid', 'build');
self::delete_unused_rows('coveragefilelog', 'buildid', 'build');
self::delete_unused_rows('coveragesummary', 'buildid', 'build');
self::delete_unused_rows('coveragesummarydiff', 'buildid', 'build');
self::delete_unused_rows('dynamicanalysis', 'buildid', 'build');
self::delete_unused_rows('label2build', 'buildid', 'build');
self::delete_unused_rows('subproject2build', 'buildid', 'build');
self::delete_unused_rows('summaryemail', 'buildid', 'build');
self::delete_unused_rows('testdiff', 'buildid', 'build');

self::delete_unused_rows('dynamicanalysisdefect', 'dynamicanalysisid', 'dynamicanalysis');
self::delete_unused_rows('subproject2subproject', 'subprojectid', 'subproject');

self::delete_unused_rows('dailyupdatefile', 'dailyupdateid', 'dailyupdate');
self::delete_unused_rows('coveragefile', 'id', 'coverage', 'fileid');

self::delete_unused_rows('dailyupdatefile', 'dailyupdateid', 'dailyupdate');
self::delete_unused_rows('test2image', 'outputid', 'testoutput');

$xml .= add_XML_value('alert', 'Database cleanup complete.');
}

/* Check the builds with wrong date */
if ($CheckBuildsWrongDate) {
$currentdate = time() + 3600 * 24 * 3; // or 3 days away from now
$forwarddate = date(FMT_DATETIME, $currentdate);

$builds = pdo_query("SELECT id,name,starttime FROM build WHERE starttime<'1975-12-31 23:59:59' OR starttime>'$forwarddate'");
while ($builds_array = pdo_fetch_array($builds)) {
echo $builds_array['name'] . '-' . $builds_array['starttime'] . '<br>';
}
}

/* Delete the builds with wrong date */
if ($DeleteBuildsWrongDate) {
$currentdate = time() + 3600 * 24 * 3; // or 3 days away from now
$forwarddate = date(FMT_DATETIME, $currentdate);

$builds = pdo_query(
"SELECT id FROM build WHERE parentid IN (0, -1) AND
starttime<'1975-12-31 23:59:59' OR starttime>'$forwarddate'");
while ($builds_array = pdo_fetch_array($builds)) {
$buildid = $builds_array['id'];
remove_build($buildid);
}
}

if ($FixBuildBasedOnRule) {
// loop through the list of build2group
$buildgroups = pdo_query('SELECT * from build2group');
Expand Down Expand Up @@ -505,11 +429,4 @@ public function userStatistics(): View
{
return $this->angular_view('userStatistics');
}

/** Delete unused rows */
private static function delete_unused_rows($table, $field, $targettable, $selectfield = 'id'): void
{
DB::delete("DELETE FROM $table WHERE $field NOT IN (SELECT $selectfield AS $field FROM $targettable)");
echo pdo_error();
}
}
3 changes: 2 additions & 1 deletion app/Http/Controllers/BuildController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use App\Models\Comment;
use App\Models\User;
use App\Models\Build as EloquentBuild;
use App\Utils\DatabaseCleanupUtils;
use App\Utils\PageTimer;
use App\Utils\RepositoryUtils;
use App\Utils\TestingDay;
Expand Down Expand Up @@ -1470,7 +1471,7 @@ private function restApiPost(): JsonResponse
private function restApiDelete(): JsonResponse
{
Log::info("Build #{$this->build->Id} removed manually.");
remove_build($this->build->Id);
DatabaseCleanupUtils::removeBuild($this->build->Id);
return response()->json();
}
}
30 changes: 30 additions & 0 deletions app/Jobs/PruneBuilds.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Artisan;

/**
* Removes builds that have expired according to per-project and
* per-buildgroup settings.
*/
class PruneBuilds implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* Execute the job.
*/
public function handle(): void
{
if (!(bool) config('cdash.autoremove_builds')) {
return;
}
Artisan::call('build:remove all');
}
}
26 changes: 26 additions & 0 deletions app/Jobs/PruneDatabase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Artisan;

/**
* Remove unreferenced database rows.
*/
class PruneDatabase implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* Execute the job.
*/
public function handle(): void
{
Artisan::call('db:cleanup');
}
}
Loading

0 comments on commit c0ed119

Please sign in to comment.