Skip to content

Commit ff22f4f

Browse files
committed
Add tests for signal handling during scheduled-tasks:run
1 parent f0a630b commit ff22f4f

File tree

3 files changed

+298
-0
lines changed

3 files changed

+298
-0
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
/**
4+
* Matomo - free/libre analytics platform
5+
*
6+
* @link https://matomo.org
7+
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Piwik\Plugins\CoreAdminHome\tests\Fixtures;
13+
14+
use Closure;
15+
use Piwik\Container\Container;
16+
use Piwik\DI;
17+
use Piwik\Plugins\CoreAdminHome\tests\Fixtures\RunScheduledTasksProcessSignal\StepControl;
18+
use Piwik\Plugins\Monolog\Handler\EchoHandler;
19+
use Piwik\Tests\Framework\Fixture;
20+
21+
/**
22+
* Provides container configuration and helpers to run process signal tests.
23+
*/
24+
class RunScheduledTasksProcessSignal extends Fixture
25+
{
26+
public const ENV_TRIGGER = 'MATOMO_TEST_RUN_SCHEDULED_TASKS_PROCESS_SIGNAL';
27+
28+
/**
29+
* @var int
30+
*/
31+
public $idSite = 1;
32+
33+
/**
34+
* @var StepControl
35+
*/
36+
public $stepControl;
37+
38+
/**
39+
* @var bool
40+
*/
41+
private $inTestEnv;
42+
43+
public function __construct()
44+
{
45+
$this->inTestEnv = (bool) getenv(self::ENV_TRIGGER);
46+
47+
$this->stepControl = new StepControl();
48+
}
49+
50+
public function setUp(): void
51+
{
52+
Fixture::createSuperUser();
53+
54+
if (!self::siteCreated($this->idSite)) {
55+
self::createWebsite('2021-01-01');
56+
}
57+
}
58+
59+
public function tearDown(): void
60+
{
61+
// empty
62+
}
63+
64+
public function provideContainerConfig(): array
65+
{
66+
if (!$this->inTestEnv) {
67+
return [];
68+
}
69+
70+
return [
71+
'ini.tests.enable_logging' => 1,
72+
'log.handlers' => static function (Container $c) {
73+
return [$c->get(EchoHandler::class)];
74+
},
75+
'observers.global' => DI::add([
76+
[
77+
'ScheduledTasks.execute',
78+
DI::value(Closure::fromCallable([$this->stepControl, 'handleScheduledTasksExecute'])),
79+
],
80+
]),
81+
];
82+
}
83+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/**
4+
* Matomo - free/libre analytics platform
5+
*
6+
* @link https://matomo.org
7+
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8+
*/
9+
10+
declare(strict_types=1);
11+
12+
namespace Piwik\Plugins\CoreAdminHome\tests\Fixtures\RunScheduledTasksProcessSignal;
13+
14+
use Piwik\Option;
15+
use RuntimeException;
16+
17+
class StepControl
18+
{
19+
public const OPTION_PREFIX = 'RunScheduledTasksProcessSignal.';
20+
21+
private const OPTION_SCHEDULED_TASKS_BLOCKED = self::OPTION_PREFIX . 'ScheduledTasksBlocked';
22+
23+
/**
24+
* Block proceeding from the "ScheduledTasks.execute" event.
25+
*/
26+
public function blockScheduledTasks(): void
27+
{
28+
Option::set(self::OPTION_SCHEDULED_TASKS_BLOCKED, true);
29+
}
30+
31+
/**
32+
* DI hook intercepting the "ScheduledTasks.execute" event.
33+
*/
34+
public function handleScheduledTasksExecute(): void
35+
{
36+
$continue = $this->waitForSuccess(static function (): bool {
37+
// force reading from database
38+
Option::clearCachedOption(self::OPTION_SCHEDULED_TASKS_BLOCKED);
39+
40+
return false === Option::get(self::OPTION_SCHEDULED_TASKS_BLOCKED);
41+
});
42+
43+
if (!$continue) {
44+
throw new RuntimeException('Waiting for ScheduledTask option took too long!');
45+
}
46+
}
47+
48+
/**
49+
* Remove all internal blocks.
50+
*/
51+
public function reset(): void
52+
{
53+
Option::deleteLike(self::OPTION_PREFIX . '%');
54+
}
55+
56+
/**
57+
* Allow proceeding past the "ScheduledTasks.execute" event.
58+
*/
59+
public function unblockScheduledTasks(): void
60+
{
61+
Option::delete(self::OPTION_SCHEDULED_TASKS_BLOCKED);
62+
}
63+
64+
/**
65+
* Wait until a callable returns true or a timeout is reached.
66+
*/
67+
public function waitForSuccess(callable $check, int $timeoutInSeconds = 10): bool
68+
{
69+
$start = time();
70+
71+
do {
72+
$now = time();
73+
74+
if ($check()) {
75+
return true;
76+
}
77+
78+
// 250 millisecond sleep
79+
usleep(250 * 1000);
80+
} while ($timeoutInSeconds > $now - $start);
81+
82+
return false;
83+
}
84+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
/**
4+
* Matomo - free/libre analytics platform
5+
*
6+
* @link https://matomo.org
7+
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8+
*/
9+
10+
namespace Piwik\Plugins\CoreAdminHome\tests\Integration\Commands;
11+
12+
use Piwik\CliMulti\ProcessSymfony;
13+
use Piwik\Plugins\CoreAdminHome\Tasks;
14+
use Piwik\Plugins\CoreAdminHome\tests\Fixtures\RunScheduledTasksProcessSignal as RunScheduledTasksProcessSignalFixture;
15+
use Piwik\Scheduler\Task;
16+
use Piwik\Tests\Framework\Fixture;
17+
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
18+
19+
/**
20+
* @group Core
21+
* @group CoreAdminHome
22+
* @group RunScheduledTasksProcessSignal
23+
*/
24+
class RunScheduledTasksProcessSignalTest extends IntegrationTestCase
25+
{
26+
/**
27+
* @var RunScheduledTasksProcessSignalFixture
28+
*/
29+
public static $fixture;
30+
31+
public function setUp(): void
32+
{
33+
if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) {
34+
$this->markTestSkipped('signal test cannot run without ext-pcntl');
35+
}
36+
37+
parent::setUp();
38+
39+
self::$fixture->stepControl->reset();
40+
}
41+
42+
/**
43+
* @dataProvider getScheduledTasksStoppedData
44+
*/
45+
public function testScheduledTasksStopped(int $signal): void
46+
{
47+
self::$fixture->stepControl->blockScheduledTasks();
48+
49+
$process = $this->startScheduledTasks();
50+
51+
// wait until scheduled tasks are running
52+
$result = self::$fixture->stepControl->waitForSuccess(static function () use ($process): bool {
53+
return false !== strpos($process->getOutput(), 'Scheduler: executing task');
54+
}, $timeoutInSeconds = 30);
55+
56+
self::assertTrue($result, 'Scheduled tasks did not start');
57+
58+
$this->sendSignalToProcess($process, $signal);
59+
60+
self::$fixture->stepControl->unblockScheduledTasks();
61+
62+
$this->waitForProcessToStop($process);
63+
64+
$processOutput = $process->getOutput();
65+
$expectedExecutedTask = Task::getTaskName(Tasks::class, 'invalidateOutdatedArchives', null);
66+
$expectedSkippedTask = Task::getTaskName(Tasks::class, 'purgeOutdatedArchives', null);
67+
68+
self::assertStringContainsString('executing task ' . $expectedExecutedTask, $processOutput);
69+
self::assertStringNotContainsString('executing task ' . $expectedSkippedTask, $processOutput);
70+
71+
self::assertStringContainsString(
72+
'Received system signal to stop scheduled tasks: ' . $signal,
73+
$processOutput
74+
);
75+
76+
self::assertStringContainsString('Scheduler: Aborting due to received signal', $processOutput);
77+
}
78+
79+
public function getScheduledTasksStoppedData(): iterable
80+
{
81+
yield 'stop using sigint' => [\SIGINT];
82+
yield 'stop using sigterm' => [\SIGTERM];
83+
}
84+
85+
private function sendSignalToProcess(ProcessSymfony $process, int $signal): void
86+
{
87+
$process->signal($signal);
88+
89+
$result = self::$fixture->stepControl->waitForSuccess(
90+
static function () use ($process, $signal): bool {
91+
return false !== strpos(
92+
$process->getOutput(),
93+
'Received system signal to stop scheduled tasks: ' . $signal
94+
);
95+
}
96+
);
97+
98+
self::assertTrue($result, 'Process did not acknowledge signal');
99+
}
100+
101+
private function startScheduledTasks(): ProcessSymfony
102+
{
103+
// exec is mandatory to send signals to the process
104+
// not using array notation because "Fixture::getCliCommandBase" contains parameters
105+
$process = ProcessSymfony::fromShellCommandline(sprintf(
106+
'exec %s scheduled-tasks:run -vvv --force',
107+
Fixture::getCliCommandBase()
108+
));
109+
110+
$process->setEnv([RunScheduledTasksProcessSignalFixture::ENV_TRIGGER => '1']);
111+
$process->setTimeout(null);
112+
$process->start();
113+
114+
self::assertTrue($process->isRunning());
115+
self::assertNotNull($process->getPid());
116+
117+
return $process;
118+
}
119+
120+
private function waitForProcessToStop(ProcessSymfony $process): void
121+
{
122+
$result = self::$fixture->stepControl->waitForSuccess(static function () use ($process): bool {
123+
return !$process->isRunning();
124+
});
125+
126+
self::assertTrue($result, 'Archiving process did not stop');
127+
self::assertSame(0, $process->getExitCode());
128+
}
129+
}
130+
131+
RunScheduledTasksProcessSignalTest::$fixture = new RunScheduledTasksProcessSignalFixture();

0 commit comments

Comments
 (0)