Skip to content

Commit

Permalink
feat(octane): add support for ticks and intervals with FrankenPHP
Browse files Browse the repository at this point in the history
  • Loading branch information
vinceAmstoutz committed Dec 19, 2024
1 parent ee88fe3 commit affb97d
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 14 deletions.
58 changes: 46 additions & 12 deletions src/Concerns/RegistersTickHandlers.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,41 @@
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
*/
public function tick(string $key, callable $callback, int $seconds = 1, bool $immediate = true)
public function tick(string $key, callable $callback, int $seconds = 1, bool $immediate = true): SwooleInvokeTickCallable|FrankenPhpInvokeTickCallable
{
$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 +49,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

0 comments on commit affb97d

Please sign in to comment.