Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(octane): add support for ticks and intervals with FrankenPHP #974

Draft
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions src/Concerns/RegistersTickHandlers.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,43 @@
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Facades\Cache;
use Laravel\Octane\Events\TickReceived;
use Laravel\Octane\Swoole\InvokeTickCallable;
use Laravel\Octane\FrankenPhp\InvokeTickCallable as FrankenPhpInvokeTickCallable;
use Laravel\Octane\FrankenPhp\ServerProcessInspector as FrankenPhpServerProcessInspector;
use Laravel\Octane\Swoole\InvokeTickCallable as SwooleInvokeTickCallable;
use Laravel\Octane\Swoole\ServerProcessInspector as SwooleServerProcessInspector;
use RuntimeException;

trait RegistersTickHandlers
{
/**
* Register a callback to be called every N seconds.
*
* @return \Laravel\Octane\Swoole\InvokeTickCallable
* @return \Laravel\Octane\Swoole\InvokeTickCallable|\Laravel\Octane\FrankenPhp\InvokeTickCallable
*/
public function tick(string $key, callable $callback, int $seconds = 1, bool $immediate = true)
{
$listener = new InvokeTickCallable(
$key,
$callback,
$seconds,
$immediate,
Cache::store('octane'),
app(ExceptionHandler::class)
);
$store = Cache::store('octane');
$exceptionHandler = app(ExceptionHandler::class);

$listener = match (true) {
$this->isSwooleServerRunning() => new SwooleInvokeTickCallable(
$key,
$callback,
$seconds,
$immediate,
$store,
$exceptionHandler
),
$this->isFrankenPhpServerRunning() => new FrankenPhpInvokeTickCallable(
$key,
$callback,
$seconds,
$immediate,
$store,
$exceptionHandler
),
default => throw new RuntimeException('Tick functionality is not supported in this environment.'),
};

app(Dispatcher::class)->listen(
TickReceived::class,
Expand All @@ -33,4 +51,22 @@ public function tick(string $key, callable $callback, int $seconds = 1, bool $im

return $listener;
}

/**
* Check if the Swoole server is running.
*/
protected function isSwooleServerRunning(): bool
{
return app(SwooleServerProcessInspector::class)
->serverIsRunning();
}

/**
* Check if the FrankenPHP server is running.
*/
protected function isFrankenPhpServerRunning(): bool
{
return app(FrankenPhpServerProcessInspector::class)
->serverIsRunning();
}
}
2 changes: 1 addition & 1 deletion src/Facades/Octane.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Illuminate\Support\Facades\Facade;

/**
* @method static \Laravel\Octane\Swoole\InvokeTickCallable tick(string $key, callable $callback, int $seconds = 1, bool $immediate = true)
* @method static \Laravel\Octane\Swoole\InvokeTickCallable|\Laravel\Octane\FrankenPhp\InvokeTickCallable tick(string $key, callable $callback, int $seconds = 1, bool $immediate = true)
* @method static \Swoole\Table table(string $name)
* @method static \Symfony\Component\HttpFoundation\Response invokeRoute(\Illuminate\Http\Request $request, string $method, string $uri)
* @method static array concurrently(array $tasks, int $waitMilliseconds = 3000)
Expand Down
67 changes: 67 additions & 0 deletions src/FrankenPhp/InvokeTickCallable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace Laravel\Octane\FrankenPhp;

use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Support\Carbon;
use Throwable;

class InvokeTickCallable
{
public function __construct(
protected string $key,
protected $callback,
protected int $seconds,
protected bool $immediate,
protected $cache,
protected ExceptionHandler $exceptionHandler
) {
}

/**
* Invoke the tick listener.
*
* @throws Throwable
*/
public function __invoke(): void
{
$lastInvokedAt = $this->cache->get('tick-'.$this->key);

if (! is_null($lastInvokedAt) &&
(Carbon::now()->getTimestamp() - $lastInvokedAt) < $this->seconds) {
return;
}

$this->cache->forever('tick-'.$this->key, Carbon::now()->getTimestamp());

if (is_null($lastInvokedAt) && ! $this->immediate) {
return;
}

try {
call_user_func($this->callback);
} catch (Throwable $e) {
$this->exceptionHandler->report($e);
}
}

/**
* Indicate how often the listener should be invoked.
*/
public function seconds(int $seconds): static
{
$this->seconds = $seconds;

return $this;
}

/**
* Indicate that the listener should be invoked on the first tick after the server starts.
*/
public function immediate(): static
{
$this->immediate = true;

return $this;
}
}
106 changes: 106 additions & 0 deletions tests/FrankenPhpInvokeTickCallableTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace Laravel\Octane\Tests;

use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Support\Carbon;
use Laravel\Octane\FrankenPhp\InvokeTickCallable;
use Mockery;

class FrankenPhpInvokeTickCallableTest extends TestCase
{
/**
* @throws \Throwable
*/
public function test_callable_is_invoked_when_due(): void
{
Carbon::setTestNow($now = now());

$instance = new InvokeTickCallable(
'key', fn () => $_SERVER['__test.invokeTickCallable'] = true, 1, true,
$cache = Mockery::mock('stdClass'), Mockery::mock(ExceptionHandler::class)
);

$cache->shouldReceive('get')->with('tick-key')->andReturn(time() - 100);

$cache->shouldReceive('forever')->once()->with('tick-key', $now->getTimestamp());

$instance();

$this->assertTrue($_SERVER['__test.invokeTickCallable'] ?? false);

Carbon::setTestNow();
unset($_SERVER['__test.invokeTickCallable']);
}

/**
* @throws \Throwable
*/
public function test_callable_is_not_invoked_when_not_due(): void
{
Carbon::setTestNow(now());

$_SERVER['__test.invokeTickCallable'] = false;

$instance = new InvokeTickCallable(
'key', fn () => $_SERVER['__test.invokeTickCallable'] = true, 30, true,
$cache = Mockery::mock('stdClass'), Mockery::mock(ExceptionHandler::class)
);

$cache->shouldReceive('get')->with('tick-key')->andReturn(time() - 10);

$cache->shouldReceive('forever')->never();

$instance();

$this->assertFalse($_SERVER['__test.invokeTickCallable'] ?? false);

Carbon::setTestNow();
unset($_SERVER['__test.invokeTickCallable']);
}

/**
* @throws \Throwable
*/
public function test_callable_is_invoked_when_first_run_and_immediate(): void
{
Carbon::setTestNow($now = now());

$instance = new InvokeTickCallable(
'key', fn () => $_SERVER['__test.invokeTickCallable'] = true, 1, true,
$cache = Mockery::mock('stdClass'), Mockery::mock(ExceptionHandler::class)
);

$cache->shouldReceive('get')->with('tick-key')->andReturn(null);

$cache->shouldReceive('forever')->once()->with('tick-key', $now->getTimestamp());

$instance();

$this->assertTrue($_SERVER['__test.invokeTickCallable'] ?? false);

Carbon::setTestNow();
unset($_SERVER['__test.invokeTickCallable']);
}

public function test_callable_is_not_invoked_when_first_run_and_not_immediate()
{
Carbon::setTestNow($now = now());

$instance = new InvokeTickCallable(
'key', fn () => $_SERVER['__test.invokeTickCallable'] = true, 1, false,
$cache = Mockery::mock('stdClass'), Mockery::mock(ExceptionHandler::class)
);

$cache->shouldReceive('get')->with('tick-key')->andReturn(null);

$cache->shouldReceive('forever')->once()->with('tick-key', $now->getTimestamp());

$instance();

$this->assertFalse($_SERVER['__test.invokeTickCallable'] ?? false);

Carbon::setTestNow();
unset($_SERVER['__test.invokeTickCallable']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Laravel\Octane\Swoole\InvokeTickCallable;
use Mockery;

class InvokeTickCallableTest extends TestCase
class SwooleInvokeTickCallableTest extends TestCase
{
public function test_callable_is_invoked_when_due()
{
Expand Down
Loading