Skip to content

Commit

Permalink
Implement request lifecycle handler
Browse files Browse the repository at this point in the history
  • Loading branch information
timacdonald committed Sep 14, 2022
1 parent 4d46bda commit 2c17da4
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 0 deletions.
58 changes: 58 additions & 0 deletions src/Illuminate/Foundation/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@

namespace Illuminate\Foundation\Http;

use Carbon\CarbonInterval;
use DateTimeInterface;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Http\Kernel as KernelContract;
use Illuminate\Foundation\Http\Events\RequestHandled;
use Illuminate\Routing\Pipeline;
use Illuminate\Routing\Router;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\InteractsWithTime;
use InvalidArgumentException;
use Throwable;

class Kernel implements KernelContract
{
use InteractsWithTime;

/**
* The application implementation.
*
Expand Down Expand Up @@ -63,6 +69,20 @@ class Kernel implements KernelContract
*/
protected $routeMiddleware = [];

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

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

/**
* The priority-sorted list of middleware.
*
Expand Down Expand Up @@ -105,6 +125,8 @@ public function __construct(Application $app, Router $router)
*/
public function handle($request)
{
$this->requestStartedAt = Carbon::now();

try {
$request->enableHttpMethodParameterOverride();

Expand Down Expand Up @@ -180,6 +202,16 @@ public function terminate($request, $response)
$this->terminateMiddleware($request, $response);

$this->app->terminate();

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

if ($this->requestStartedAt->diffInMilliseconds($end) > $threshold) {
$handler($this->requestStartedAt, $request, $response);
}
}

$this->requestStartedAt = null;
}

/**
Expand Down Expand Up @@ -211,6 +243,32 @@ protected function terminateMiddleware($request, $response)
}
}

public function whenRequestLifecycleLongerThan($threshold, $handler)
{
$threshold = $threshold instanceof DateTimeInterface
? $this->secondsUntil($threshold) * 1000
: $threshold;

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

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

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

/**
* Gather the route middleware for the given request.
*
Expand Down
169 changes: 169 additions & 0 deletions tests/Integration/Http/RequestDurationThresholdTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

namespace Illuminate\Tests\Integration\Http;

use Carbon\CarbonInterval;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Route;
use Orchestra\Testbench\TestCase;

class RequestDurationThresholdTest extends TestCase
{
public function testItCanHandleExceedingRequestDuration()
{
Route::get('test-route', fn () => 'ok');
$request = Request::create('http://localhost/test-route');
$response = new Response();
$called = false;
$kernel = $this->app[Kernel::class];
$kernel->whenRequestLifecycleLongerThan(CarbonInterval::seconds(1), function () use (&$called) {
$called = true;
});

Carbon::setTestNow(now());
$kernel->handle($request);

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

$this->assertTrue($called);
}

public function testItDoesntCallWhenExactlyThresholdDuration()
{
Route::get('test-route', fn () => 'ok');
$request = Request::create('http://localhost/test-route');
$response = new Response();
$called = false;
$kernel = $this->app[Kernel::class];
$kernel->whenRequestLifecycleLongerThan(CarbonInterval::seconds(1), function () use (&$called) {
$called = true;
});

Carbon::setTestNow(now());
$kernel->handle($request);

Carbon::setTestNow(Carbon::now()->addSeconds(1));
$kernel->terminate($request, $response);

$this->assertFalse($called);
}

public function testItProvidesRequestToHandler()
{
Route::get('test-route', fn () => 'ok');
$request = Request::create('http://localhost/test-route');
$response = new Response();
$url = null;
$kernel = $this->app[Kernel::class];
$kernel->whenRequestLifecycleLongerThan(CarbonInterval::seconds(1), function ($startedAt, $request) use (&$url) {
$url = $request->url();
});

Carbon::setTestNow(now());
$kernel->handle($request);

Carbon::setTestNow(Carbon::now()->addSeconds(2));
$kernel->terminate($request, $response);

$this->assertSame('http://localhost/test-route', $url);
}

public function testItCanExceedThresholdWhenSpecifyingDurationAsMilliseconds()
{
Route::get('test-route', fn () => 'ok');
$request = Request::create('http://localhost/test-route');
$response = new Response();
$called = false;
$kernel = $this->app[Kernel::class];
$kernel->whenRequestLifecycleLongerThan(1000, function () use (&$called) {
$called = true;
});

Carbon::setTestNow(now());
$kernel->handle($request);

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

$this->assertTrue($called);
}

public function testItCanStayUnderThresholdWhenSpecifyingDurationAsMilliseconds()
{
Route::get('test-route', fn () => 'ok');
$request = Request::create('http://localhost/test-route');
$response = new Response();
$called = false;
$kernel = $this->app[Kernel::class];
$kernel->whenRequestLifecycleLongerThan(1000, function () use (&$called) {
$called = true;
});

Carbon::setTestNow(now());
$kernel->handle($request);

Carbon::setTestNow(Carbon::now()->addSeconds(1));
$kernel->terminate($request, $response);

$this->assertFalse($called);
}

public function testItCanExceedThresholdWhenSpecifyingDurationAsDateTime()
{
Route::get('test-route', fn () => 'ok');
$request = Request::create('http://localhost/test-route');
$response = new Response();
$called = false;
$kernel = $this->app[Kernel::class];
$kernel->whenRequestLifecycleLongerThan(now()->addSeconds(1), function () use (&$called) {
$called = true;
});

Carbon::setTestNow(now());
$kernel->handle($request);

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

$this->assertTrue($called);
}

public function testItCanStayUnderThresholdWhenSpecifyingDurationAsDateTime()
{
Route::get('test-route', fn () => 'ok');
$request = Request::create('http://localhost/test-route');
$response = new Response();
$called = false;
$kernel = $this->app[Kernel::class];
$kernel->whenRequestLifecycleLongerThan(now()->addSeconds(1), function () use (&$called) {
$called = true;
});

Carbon::setTestNow(now());
$kernel->handle($request);

Carbon::setTestNow(Carbon::now()->addSeconds(1));
$kernel->terminate($request, $response);

$this->assertFalse($called);
}

public function testItClearsStartTimeAfterHandlingRequest()
{
$kernel = $this->app[Kernel::class];
Route::get('test-route', fn () => 'ok');
$request = Request::create('http://localhost/test-route');
$response = new Response();

Carbon::setTestNow(now());
$kernel->handle($request);
$this->assertTrue(Carbon::now()->eq($kernel->requestStartedAt()));

$kernel->terminate($request, $response);
$this->assertNull($kernel->requestStartedAt());
}
}

0 comments on commit 2c17da4

Please sign in to comment.