diff --git a/app/code/Magento/Cron/Cron/CleanOldJobs.php b/app/code/Magento/Cron/Cron/CleanOldJobs.php new file mode 100644 index 0000000000000..d198ba8ec7ff8 --- /dev/null +++ b/app/code/Magento/Cron/Cron/CleanOldJobs.php @@ -0,0 +1,89 @@ +config->get(System::CONFIG_TYPE); + $maxLifetime = 0; + + array_walk_recursive( + $fullConfig, + static function ($value, $key) use (&$maxLifetime) { + if ($key === ProcessCronQueueObserver::XML_PATH_HISTORY_SUCCESS + || $key === ProcessCronQueueObserver::XML_PATH_HISTORY_FAILURE + ) { + $maxLifetime = max($maxLifetime, (int) $value); + } + } + ); + + if ($maxLifetime === 0) { + // Something has gone wrong. Why are there no configuration values? + // Drop out now to avoid doing any damage to this already-broken installation. + return; + } + + // The value stored in XML is in minutes, we want seconds. + $maxLifetime *= 60; + + // Add one day to avoid removing items which are near their natural expiry anyway. + $maxLifetime += 86400; + + /** @var Schedule $scheduleResource */ + $scheduleResource = $this->scheduleFactory->create()->getResource(); + + $currentTime = $this->dateTime->gmtTimestamp(); + $deleteBefore = $scheduleResource->getConnection()->formatDate($currentTime - $maxLifetime); + + $this->retrier->execute( + function () use ($scheduleResource, $deleteBefore) { + $scheduleResource->getConnection()->delete( + $scheduleResource->getTable('cron_schedule'), + [ + 'scheduled_at < ?' => $deleteBefore, + ] + ); + }, + $scheduleResource->getConnection() + ); + } +} diff --git a/app/code/Magento/Cron/Test/Unit/Cron/CleanOldJobsTest.php b/app/code/Magento/Cron/Test/Unit/Cron/CleanOldJobsTest.php new file mode 100644 index 0000000000000..4078fae26e25d --- /dev/null +++ b/app/code/Magento/Cron/Test/Unit/Cron/CleanOldJobsTest.php @@ -0,0 +1,141 @@ +configMock = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->dateTimeMock = $this->getMockBuilder(DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dateTimeMock->method('gmtTimestamp') + ->willReturn($this->time); + + $this->retrierMock = $this->getMockForAbstractClass(DeadlockRetrierInterface::class); + + $this->scheduleFactoryMock = $this->getMockBuilder(ScheduleFactory::class) + ->onlyMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + + $this->scheduleMock = $this->getMockBuilder(Schedule::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scheduleResourceMock = $this->getMockBuilder(ScheduleResourceModel::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->scheduleMock + ->method('getResource') + ->willReturn($this->scheduleResourceMock); + + $this->scheduleFactoryMock + ->method('create') + ->willReturn($this->scheduleMock); + + $this->cleanOldJobs = new CleanOldJobs( + $this->configMock, + $this->dateTimeMock, + $this->retrierMock, + $this->scheduleFactoryMock + ); + } + + public function testSuccess(): void + { + $tableName = 'cron_schedule'; + + $this->configMock->expects($this->once()) + ->method('get') + ->with('system') + ->willReturn([ + 'history_success_lifetime' => 100, + 'history_failure_lifetime' => 200, + ]); + + $connectionMock = $this->getMockForAbstractClass(AdapterInterface::class); + + $connectionMock->expects($this->once()) + ->method('delete') + ->with($tableName, [ + 'scheduled_at < ?' => '$this->time - (86400 + (200 * 60))', + ]); + + $connectionMock->method('formatDate') + ->willReturnMap([ + [1501538400, true, '$this->time'], + [1501538200, true, '$this->time - 200'], + [1501526400, true, '$this->time - (200 * 60)'], + [1501452000, true, '$this->time - 86400'], + [1501451800, true, '$this->time - (86400 + 200)'], + [1501440000, true, '$this->time - (86400 + (200 * 60))'], + ]); + + $this->scheduleResourceMock->expects($this->once()) + ->method('getTable') + ->with($tableName) + ->willReturn($tableName); + $this->scheduleResourceMock->expects($this->exactly(3)) + ->method('getConnection') + ->willReturn($connectionMock); + + $this->retrierMock->expects($this->once()) + ->method('execute') + ->willReturnCallback( + function ($callback) { + return $callback(); + } + ); + + $this->cleanOldJobs->execute(); + } + + public function testNoActionWhenEmptyConfig(): void + { + $this->configMock->expects($this->once()) + ->method('get') + ->with('system') + ->willReturn([]); + + $this->scheduleFactoryMock->expects($this->never())->method('create'); + + $this->cleanOldJobs->execute(); + } +} diff --git a/app/code/Magento/Cron/composer.json b/app/code/Magento/Cron/composer.json index 02f55e9de4597..c6e366227f688 100644 --- a/app/code/Magento/Cron/composer.json +++ b/app/code/Magento/Cron/composer.json @@ -7,11 +7,9 @@ "require": { "php": "~8.1.0||~8.2.0||~8.3.0", "magento/framework": "*", + "magento/module-config": "^100.1.2 || ^101.0", "magento/module-store": "*" }, - "suggest": { - "magento/module-config": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", diff --git a/app/code/Magento/Cron/etc/crontab.xml b/app/code/Magento/Cron/etc/crontab.xml new file mode 100644 index 0000000000000..82e84e06979b3 --- /dev/null +++ b/app/code/Magento/Cron/etc/crontab.xml @@ -0,0 +1,17 @@ + + + + + + 0 0 * * * + + + diff --git a/app/code/Magento/Cron/etc/module.xml b/app/code/Magento/Cron/etc/module.xml index 8112b9e8c46db..9babe267f0023 100644 --- a/app/code/Magento/Cron/etc/module.xml +++ b/app/code/Magento/Cron/etc/module.xml @@ -8,6 +8,7 @@ +