-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests for signal handling during scheduled-tasks:run
- Loading branch information
Showing
3 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
83 changes: 83 additions & 0 deletions
83
plugins/CoreAdminHome/tests/Fixtures/RunScheduledTasksProcessSignal.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<?php | ||
|
||
/** | ||
* Matomo - free/libre analytics platform | ||
* | ||
* @link https://matomo.org | ||
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Piwik\Plugins\CoreAdminHome\tests\Fixtures; | ||
|
||
use Closure; | ||
use Piwik\Container\Container; | ||
use Piwik\DI; | ||
use Piwik\Plugins\CoreAdminHome\tests\Fixtures\RunScheduledTasksProcessSignal\StepControl; | ||
use Piwik\Plugins\Monolog\Handler\EchoHandler; | ||
use Piwik\Tests\Framework\Fixture; | ||
|
||
/** | ||
* Provides container configuration and helpers to run process signal tests. | ||
*/ | ||
class RunScheduledTasksProcessSignal extends Fixture | ||
{ | ||
public const ENV_TRIGGER = 'MATOMO_TEST_RUN_SCHEDULED_TASKS_PROCESS_SIGNAL'; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
public $idSite = 1; | ||
|
||
/** | ||
* @var StepControl | ||
*/ | ||
public $stepControl; | ||
|
||
/** | ||
* @var bool | ||
*/ | ||
private $inTestEnv; | ||
|
||
public function __construct() | ||
{ | ||
$this->inTestEnv = (bool) getenv(self::ENV_TRIGGER); | ||
|
||
$this->stepControl = new StepControl(); | ||
} | ||
|
||
public function setUp(): void | ||
{ | ||
Fixture::createSuperUser(); | ||
|
||
if (!self::siteCreated($this->idSite)) { | ||
self::createWebsite('2021-01-01'); | ||
} | ||
} | ||
|
||
public function tearDown(): void | ||
{ | ||
// empty | ||
} | ||
|
||
public function provideContainerConfig(): array | ||
{ | ||
if (!$this->inTestEnv) { | ||
return []; | ||
} | ||
|
||
return [ | ||
'ini.tests.enable_logging' => 1, | ||
'log.handlers' => static function (Container $c) { | ||
return [$c->get(EchoHandler::class)]; | ||
}, | ||
'observers.global' => DI::add([ | ||
[ | ||
'ScheduledTasks.execute', | ||
DI::value(Closure::fromCallable([$this->stepControl, 'handleScheduledTasksExecute'])), | ||
], | ||
]), | ||
]; | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
plugins/CoreAdminHome/tests/Fixtures/RunScheduledTasksProcessSignal/StepControl.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
<?php | ||
|
||
/** | ||
* Matomo - free/libre analytics platform | ||
* | ||
* @link https://matomo.org | ||
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Piwik\Plugins\CoreAdminHome\tests\Fixtures\RunScheduledTasksProcessSignal; | ||
|
||
use Piwik\Option; | ||
use RuntimeException; | ||
|
||
class StepControl | ||
{ | ||
public const OPTION_PREFIX = 'RunScheduledTasksProcessSignal.'; | ||
|
||
private const OPTION_SCHEDULED_TASKS_BLOCKED = self::OPTION_PREFIX . 'ScheduledTasksBlocked'; | ||
|
||
/** | ||
* Block proceeding from the "ScheduledTasks.execute" event. | ||
*/ | ||
public function blockScheduledTasks(): void | ||
{ | ||
Option::set(self::OPTION_SCHEDULED_TASKS_BLOCKED, true); | ||
} | ||
|
||
/** | ||
* DI hook intercepting the "ScheduledTasks.execute" event. | ||
*/ | ||
public function handleScheduledTasksExecute(): void | ||
{ | ||
$continue = $this->waitForSuccess(static function (): bool { | ||
// force reading from database | ||
Option::clearCachedOption(self::OPTION_SCHEDULED_TASKS_BLOCKED); | ||
|
||
return false === Option::get(self::OPTION_SCHEDULED_TASKS_BLOCKED); | ||
}); | ||
|
||
if (!$continue) { | ||
throw new RuntimeException('Waiting for ScheduledTask option took too long!'); | ||
} | ||
} | ||
|
||
/** | ||
* Remove all internal blocks. | ||
*/ | ||
public function reset(): void | ||
{ | ||
Option::deleteLike(self::OPTION_PREFIX . '%'); | ||
} | ||
|
||
/** | ||
* Allow proceeding past the "ScheduledTasks.execute" event. | ||
*/ | ||
public function unblockScheduledTasks(): void | ||
{ | ||
Option::delete(self::OPTION_SCHEDULED_TASKS_BLOCKED); | ||
} | ||
|
||
/** | ||
* Wait until a callable returns true or a timeout is reached. | ||
*/ | ||
public function waitForSuccess(callable $check, int $timeoutInSeconds = 10): bool | ||
{ | ||
$start = time(); | ||
|
||
do { | ||
$now = time(); | ||
|
||
if ($check()) { | ||
return true; | ||
} | ||
|
||
// 250 millisecond sleep | ||
usleep(250 * 1000); | ||
} while ($timeoutInSeconds > $now - $start); | ||
|
||
return false; | ||
} | ||
} |
131 changes: 131 additions & 0 deletions
131
plugins/CoreAdminHome/tests/Integration/Commands/RunScheduledTasksProcessSignalTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
<?php | ||
|
||
/** | ||
* Matomo - free/libre analytics platform | ||
* | ||
* @link https://matomo.org | ||
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later | ||
*/ | ||
|
||
namespace Piwik\Plugins\CoreAdminHome\tests\Integration\Commands; | ||
|
||
use Piwik\CliMulti\ProcessSymfony; | ||
use Piwik\Plugins\CoreAdminHome\Tasks; | ||
use Piwik\Plugins\CoreAdminHome\tests\Fixtures\RunScheduledTasksProcessSignal as RunScheduledTasksProcessSignalFixture; | ||
use Piwik\Scheduler\Task; | ||
use Piwik\Tests\Framework\Fixture; | ||
use Piwik\Tests\Framework\TestCase\IntegrationTestCase; | ||
|
||
/** | ||
* @group Core | ||
* @group CoreAdminHome | ||
* @group RunScheduledTasksProcessSignal | ||
*/ | ||
class RunScheduledTasksProcessSignalTest extends IntegrationTestCase | ||
{ | ||
/** | ||
* @var RunScheduledTasksProcessSignalFixture | ||
*/ | ||
public static $fixture; | ||
|
||
public function setUp(): void | ||
{ | ||
if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) { | ||
$this->markTestSkipped('signal test cannot run without ext-pcntl'); | ||
} | ||
|
||
parent::setUp(); | ||
|
||
self::$fixture->stepControl->reset(); | ||
} | ||
|
||
/** | ||
* @dataProvider getScheduledTasksStoppedData | ||
*/ | ||
public function testScheduledTasksStopped(int $signal): void | ||
{ | ||
self::$fixture->stepControl->blockScheduledTasks(); | ||
|
||
$process = $this->startScheduledTasks(); | ||
|
||
// wait until scheduled tasks are running | ||
$result = self::$fixture->stepControl->waitForSuccess(static function () use ($process): bool { | ||
return false !== strpos($process->getOutput(), 'Scheduler: executing task'); | ||
}, $timeoutInSeconds = 30); | ||
|
||
self::assertTrue($result, 'Scheduled tasks did not start'); | ||
|
||
$this->sendSignalToProcess($process, $signal); | ||
|
||
self::$fixture->stepControl->unblockScheduledTasks(); | ||
|
||
$this->waitForProcessToStop($process); | ||
|
||
$processOutput = $process->getOutput(); | ||
$expectedExecutedTask = Task::getTaskName(Tasks::class, 'invalidateOutdatedArchives', null); | ||
$expectedSkippedTask = Task::getTaskName(Tasks::class, 'purgeOutdatedArchives', null); | ||
|
||
self::assertStringContainsString('executing task ' . $expectedExecutedTask, $processOutput); | ||
self::assertStringNotContainsString('executing task ' . $expectedSkippedTask, $processOutput); | ||
|
||
self::assertStringContainsString( | ||
'Received system signal to stop scheduled tasks: ' . $signal, | ||
$processOutput | ||
); | ||
|
||
self::assertStringContainsString('Scheduler: Aborting due to received signal', $processOutput); | ||
} | ||
|
||
public function getScheduledTasksStoppedData(): iterable | ||
{ | ||
yield 'stop using sigint' => [\SIGINT]; | ||
yield 'stop using sigterm' => [\SIGTERM]; | ||
} | ||
|
||
private function sendSignalToProcess(ProcessSymfony $process, int $signal): void | ||
{ | ||
$process->signal($signal); | ||
|
||
$result = self::$fixture->stepControl->waitForSuccess( | ||
static function () use ($process, $signal): bool { | ||
return false !== strpos( | ||
$process->getOutput(), | ||
'Received system signal to stop scheduled tasks: ' . $signal | ||
); | ||
} | ||
); | ||
|
||
self::assertTrue($result, 'Process did not acknowledge signal'); | ||
} | ||
|
||
private function startScheduledTasks(): ProcessSymfony | ||
{ | ||
// exec is mandatory to send signals to the process | ||
// not using array notation because "Fixture::getCliCommandBase" contains parameters | ||
$process = ProcessSymfony::fromShellCommandline(sprintf( | ||
'exec %s scheduled-tasks:run -vvv --force', | ||
Fixture::getCliCommandBase() | ||
)); | ||
|
||
$process->setEnv([RunScheduledTasksProcessSignalFixture::ENV_TRIGGER => '1']); | ||
$process->setTimeout(null); | ||
$process->start(); | ||
|
||
self::assertTrue($process->isRunning()); | ||
self::assertNotNull($process->getPid()); | ||
|
||
return $process; | ||
} | ||
|
||
private function waitForProcessToStop(ProcessSymfony $process): void | ||
{ | ||
$result = self::$fixture->stepControl->waitForSuccess(static function () use ($process): bool { | ||
return !$process->isRunning(); | ||
}); | ||
|
||
self::assertTrue($result, 'Archiving process did not stop'); | ||
self::assertSame(0, $process->getExitCode()); | ||
} | ||
} | ||
|
||
RunScheduledTasksProcessSignalTest::$fixture = new RunScheduledTasksProcessSignalFixture(); |