Skip to content

Commit 0c482c5

Browse files
author
Oliver Stark
committed
Fix & improve RateLimiter
1 parent abab77c commit 0c482c5

File tree

11 files changed

+4678
-21
lines changed

11 files changed

+4678
-21
lines changed

src/BackgroundProcess.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,10 @@ public function __construct(QueueCommand $command = null)
3535
public function start()
3636
{
3737
$cmd = $this->command->getPreparedCommand();
38-
$cwd = realpath(CRAFT_BASE_PATH);
39-
40-
$process = Process::fromShellCommandline($cmd, $cwd);
38+
$process = Process::fromShellCommandline($cmd);
4139

4240
try {
43-
$process->run();
41+
$process->start();
4442
} catch (\Symfony\Component\Process\Exception\RuntimeException $runtimeException) {
4543
$runtimeException = new RuntimeException($runtimeException->getMessage());
4644
$runtimeException->setProcess($process);

src/Handlers/BackgroundQueueHandler.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ public function __invoke(PushEvent $event): void
5454
'async-queue'
5555
);
5656
}
57-
5857
}
5958

6059
// Log what's going on

src/QueueCommand.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ public function getPreparedCommand(callable $wrapper = null): string
4545
throw new PhpExecutableNotFound('Unable to find php executable.');
4646
}
4747

48-
$commandLine = implode(" ", [$php, $this->scriptName, $this->scriptArgs]);
48+
$path = realpath(CRAFT_BASE_PATH);
49+
$script = $path . DIRECTORY_SEPARATOR . $this->scriptName;
50+
$commandLine = implode(" ", [$php, $script, $this->scriptArgs]);
4951

5052
return $this->decorate($commandLine);
5153
}
@@ -66,7 +68,7 @@ protected function decorate(string $commandLine): string
6668
}
6769

6870
// default decoration
69-
return "nice -n 15 {$commandLine} > /dev/null 2>&1 &";
71+
return "nice -n 15 {$commandLine}"; // > /tmp/null.txt 2>&1 &
7072
}
7173

7274
}

src/RateLimiter.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ public function __construct(Queue $queue, Settings $settings)
3636
*/
3737
public function canIUse(string $context = null): bool
3838
{
39+
if ($this->internalCount >= $this->maxItems) {
40+
return false;
41+
}
42+
3943
try {
44+
$this->queue->channel = 'queue';
4045
$reserved = $this->queue->getTotalReserved();
4146
} catch (\Exception) {
4247
$reserved = 0;
@@ -54,6 +59,11 @@ public function increment(): void
5459
$this->internalCount++;
5560
}
5661

62+
public function getInternalCount(): int
63+
{
64+
return $this->internalCount;
65+
}
66+
5767

5868
protected function logAttempt(int $currentUsage, string $context = null): void
5969
{

tests/BackgroundProcessRunTest.php

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,36 @@ public function test_start_default_dummy_script_success(): void
3030
$bgProcess = new BackgroundProcess($command);
3131
$process = $bgProcess->start();
3232

33+
$process->wait();
34+
3335
$this->assertEquals(0, $process->getExitCode());
3436
$this->assertTrue($process->isSuccessful());
3537
$this->assertEquals(\Symfony\Component\Process\Process::STATUS_TERMINATED, $process->getStatus());
3638

37-
// Wait 0.25 seconds
38-
usleep(250000);
39+
$this->assertFileExists(TEST_FILE);
3940

4041
$content = json_decode(file_get_contents(TEST_FILE), true);
41-
4242
$this->assertTrue(is_array($content), 'Unable to read and json_decode test file.');
43-
$this->assertContains('craft.php', $content['$argv']);
44-
$this->assertContains('queue/run', $content['$argv']);
43+
$this->assertStringContainsString('craft.php', $content['$argv'][0]);
44+
$this->assertStringContainsString('queue/run', $content['$argv'][1]);
4545
$this->assertGreaterThanOrEqual($content['timestamp'], time());
46+
}
47+
48+
/**
49+
* @covers \ostark\AsyncQueue\BackgroundProcess::start
50+
*/
51+
public function test_process_does_not_block(): void
52+
{
53+
$command = new \ostark\AsyncQueue\QueueCommand('craft.php', '--sleep');
54+
$bgProcess = new BackgroundProcess($command);
55+
$process = $bgProcess->start();
56+
usleep(150000);
4657

58+
$this->assertFileExists(TEST_FILE);
4759

48-
}
60+
$content = json_decode(file_get_contents(TEST_FILE), true);
61+
$this->assertGreaterThanOrEqual($content['timestamp'], time());
4962

63+
}
5064

5165
}

tests/RateLimiterTest.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
use ostark\AsyncQueue\BackgroundProcess;
4+
use PHPUnit\Framework\TestCase;
5+
use yii\queue\PushEvent;
6+
7+
/**
8+
* @covers \ostark\AsyncQueue\RateLimiter
9+
* @covers \ostark\AsyncQueue\Handlers\BackgroundQueueHandler
10+
*/
11+
class RateLimiterTest extends TestCase
12+
{
13+
public $plugin;
14+
15+
public function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
$dummyCommand = new \ostark\AsyncQueue\QueueCommand('craft.php', '--sleep');
20+
$this->plugin = new \ostark\AsyncQueue\Plugin('async-queue', null, []);
21+
$this->plugin->set('async_process', new BackgroundProcess($dummyCommand));
22+
}
23+
24+
public function tearDown(): void
25+
{
26+
parent::tearDown();
27+
@unlink(TEST_FILE);
28+
}
29+
30+
31+
public function test_can_setup_handler_and_invoke_event(): void
32+
{
33+
$this->plugin = new \ostark\AsyncQueue\Plugin('async-queue', null, []);
34+
$handler = new \ostark\AsyncQueue\Handlers\BackgroundQueueHandler($this->plugin);
35+
36+
$handler->__invoke(new PushEvent());
37+
38+
$this->assertSame(1, $this->plugin->getRateLimiter()->getInternalCount());
39+
}
40+
41+
public function test_respect_the_limit_when_adding_multiple_jobs_in_one_request(): void
42+
{
43+
$handler = new \ostark\AsyncQueue\Handlers\BackgroundQueueHandler($this->plugin);
44+
45+
$handler->__invoke(new PushEvent());
46+
$handler->__invoke(new PushEvent());
47+
$handler->__invoke(new PushEvent());
48+
$handler->__invoke(new PushEvent());
49+
50+
$this->assertSame(
51+
$this->plugin->getRateLimiter()->maxItems,
52+
$this->plugin->getRateLimiter()->getInternalCount()
53+
);
54+
}
55+
56+
public function test_stop_spawning_processes_when_too_many_jobs_are_reserved(): void
57+
{
58+
// Fake the reserved count
59+
$queue = new class extends \craft\queue\Queue {
60+
public function getTotalReserved(): int
61+
{
62+
return 5;
63+
}
64+
};
65+
66+
$this->plugin->set('async_rate_limiter', new \ostark\AsyncQueue\RateLimiter($queue, $this->plugin->getSettings()));
67+
$handler = new \ostark\AsyncQueue\Handlers\BackgroundQueueHandler($this->plugin);
68+
69+
$handler->__invoke(new PushEvent());
70+
71+
$this->assertSame(0, $this->plugin->getRateLimiter()->getInternalCount());
72+
}
73+
}

tests/_craft/storage/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)