Skip to content

Commit

Permalink
Add tests for signal handling during scheduled-tasks:run
Browse files Browse the repository at this point in the history
  • Loading branch information
mneudert committed Sep 2, 2024
1 parent f0a630b commit ff22f4f
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 0 deletions.
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'])),
],
]),
];
}
}
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;
}
}
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();

0 comments on commit ff22f4f

Please sign in to comment.