Skip to content

Commit

Permalink
Implement command lifecycle handler (laravel#44125)
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald authored Sep 15, 2022
1 parent 4cae1c9 commit ba37a05
Show file tree
Hide file tree
Showing 2 changed files with 240 additions and 0 deletions.
65 changes: 65 additions & 0 deletions src/Illuminate/Foundation/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Illuminate\Foundation\Console;

use Carbon\CarbonInterval;
use Closure;
use DateTimeInterface;
use Illuminate\Console\Application as Artisan;
use Illuminate\Console\Command;
use Illuminate\Console\Scheduling\Schedule;
Expand All @@ -11,14 +13,18 @@
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Env;
use Illuminate\Support\InteractsWithTime;
use Illuminate\Support\Str;
use ReflectionClass;
use Symfony\Component\Finder\Finder;
use Throwable;

class Kernel implements KernelContract
{
use InteractsWithTime;

/**
* The application implementation.
*
Expand Down Expand Up @@ -54,6 +60,20 @@ class Kernel implements KernelContract
*/
protected $commandsLoaded = false;

/**
* All of the registered command duration handlers.
*
* @var array
*/
protected $commandLifecycleDurationHandlers = [];

/**
* When the currently handled command started.
*
* @var \Illuminate\Support\Carbon|null
*/
protected $commandStartedAt;

/**
* The bootstrap classes for the application.
*
Expand Down Expand Up @@ -123,6 +143,8 @@ protected function scheduleCache()
*/
public function handle($input, $output = null)
{
$this->commandStartedAt = Carbon::now();

try {
$this->bootstrap();

Expand All @@ -146,6 +168,49 @@ public function handle($input, $output = null)
public function terminate($input, $status)
{
$this->app->terminate();

foreach ($this->commandLifecycleDurationHandlers as ['threshold' => $threshold, 'handler' => $handler]) {
$end ??= Carbon::now();

if ($this->commandStartedAt->diffInMilliseconds($end) > $threshold) {
$handler($this->commandStartedAt, $input, $status);
}
}

$this->commandStartedAt = null;
}

/**
* Register a callback to be invoked when the command lifecyle duration exceeds a given amount of time.
*
* @param \DateTimeInterface|\Carbon\CarbonInterval|float|int $threshold
* @param callable $handler
* @return void
*/
public function whenCommandLifecycleIsLongerThan($threshold, $handler)
{
$threshold = $threshold instanceof DateTimeInterface
? $this->secondsUntil($threshold) * 1000
: $threshold;

$threshold = $threshold instanceof CarbonInterval
? $threshold->totalMilliseconds
: $threshold;

$this->commandLifecycleDurationHandlers[] = [
'threshold' => $threshold,
'handler' => $handler,
];
}

/**
* When the command being handled started.
*
* @return \Illuminate\Support\Carbon|null
*/
public function commandStartedAt()
{
return $this->commandStartedAt;
}

/**
Expand Down
175 changes: 175 additions & 0 deletions tests/Integration/Console/CommandDurationThresholdTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php

namespace Illuminate\Tests\Integration\Console;

use Carbon\CarbonInterval;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Carbon;
use Orchestra\Testbench\TestCase;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\ConsoleOutput;

class CommandDurationThresholdTest extends TestCase
{
public function testItCanHandleExceedingCommandDuration()
{
$kernel = $this->app[Kernel::class];
$kernel->command('foo', fn () => null);
$input = new StringInput('foo');
$called = false;
$kernel->whenCommandLifecycleIsLongerThan(CarbonInterval::seconds(1), function () use (&$called) {
$called = true;
});

Carbon::setTestNow(Carbon::now());
$kernel->handle($input, new ConsoleOutput);

$this->assertFalse($called);

Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1));
$kernel->terminate($input, 21);

$this->assertTrue($called);
}

public function testItDoesntCallWhenExactlyThresholdDuration()
{
$kernel = $this->app[Kernel::class];
$kernel->command('foo', fn () => null);
$input = new StringInput('foo');
$called = false;
$kernel->whenCommandLifecycleIsLongerThan(CarbonInterval::seconds(1), function () use (&$called) {
$called = true;
});

Carbon::setTestNow(Carbon::now());
$kernel->handle($input, new ConsoleOutput);

$this->assertFalse($called);

Carbon::setTestNow(Carbon::now()->addSeconds(1));
$kernel->terminate($input, 21);

$this->assertFalse($called);
}

public function testItProvidesArgsToHandler()
{
$kernel = $this->app[Kernel::class];
$kernel->command('foo', fn () => null);
$input = new StringInput('foo');
$args = null;
$kernel->whenCommandLifecycleIsLongerThan(CarbonInterval::seconds(0), function () use (&$args) {
$args = func_get_args();
});

Carbon::setTestNow($startedAt = Carbon::now());
$kernel->handle($input, new ConsoleOutput);
Carbon::setTestNow(Carbon::now()->addSeconds(1));
$kernel->terminate($input, 21);

$this->assertCount(3, $args);
$this->assertTrue($startedAt->eq($args[0]));
$this->assertSame($input, $args[1]);
$this->assertSame(21, $args[2]);
}

public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds()
{
$kernel = $this->app[Kernel::class];
$kernel->command('foo', fn () => null);
$input = new StringInput('foo');
$called = false;
$kernel->whenCommandLifecycleIsLongerThan(1000, function () use (&$called) {
$called = true;
});

Carbon::setTestNow(Carbon::now());
$kernel->handle($input, new ConsoleOutput);

$this->assertFalse($called);

Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1));
$kernel->terminate($input, 21);

$this->assertTrue($called);
}

public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds()
{
$kernel = $this->app[Kernel::class];
$kernel->command('foo', fn () => null);
$input = new StringInput('foo');
$called = false;
$kernel->whenCommandLifecycleIsLongerThan(1000, function () use (&$called) {
$called = true;
});

Carbon::setTestNow(Carbon::now());
$kernel->handle($input, new ConsoleOutput);

$this->assertFalse($called);

Carbon::setTestNow(Carbon::now()->addSeconds(1));
$kernel->terminate($input, 21);

$this->assertFalse($called);
}

public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime()
{
Carbon::setTestNow(Carbon::now());
$kernel = $this->app[Kernel::class];
$kernel->command('foo', fn () => null);
$input = new StringInput('foo');
$called = false;
$kernel->whenCommandLifecycleIsLongerThan(Carbon::now()->addSecond()->addMillisecond(), function () use (&$called) {
$called = true;
});

$kernel->handle($input, new ConsoleOutput);

$this->assertFalse($called);

Carbon::setTestNow(Carbon::now()->addSeconds(1)->addMilliseconds(1));
$kernel->terminate($input, 21);

$this->assertTrue($called);
}

public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime()
{
Carbon::setTestNow(Carbon::now());
$kernel = $this->app[Kernel::class];
$kernel->command('foo', fn () => null);
$input = new StringInput('foo');
$called = false;
$kernel->whenCommandLifecycleIsLongerThan(Carbon::now()->addSecond()->addMillisecond(), function () use (&$called) {
$called = true;
});

$kernel->handle($input, new ConsoleOutput);

$this->assertFalse($called);

Carbon::setTestNow(Carbon::now()->addSeconds(1));
$kernel->terminate($input, 21);

$this->assertFalse($called);
}

public function testItClearsStartTimeAfterHandlingCommand()
{
$kernel = $this->app[Kernel::class];
$kernel->command('foo', fn () => null);
$input = new StringInput('foo');

$this->assertNull($kernel->commandStartedAt());

$kernel->handle($input, new ConsoleOutput);
$this->assertNotNull($kernel->commandStartedAt());

$kernel->terminate($input, 21);
$this->assertNull($kernel->commandStartedAt());
}
}

0 comments on commit ba37a05

Please sign in to comment.