-
-
Notifications
You must be signed in to change notification settings - Fork 162
feat(core): support frankenphp worker mode #1996
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
Changes from 1 commit
1b0c6da
c6ea676
cf6b270
9fe220f
2565c4f
db99c50
ae7a69d
c7b17f8
093b5da
c87866e
2124266
c3ff106
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Tempest\Http; | ||
|
|
||
| use Tempest\Container\Singleton; | ||
| use Tempest\Http\Cookie\Cookie; | ||
|
|
||
| #[Singleton] | ||
| final class OpaqueRequest implements Request | ||
| { | ||
| public function __construct( | ||
| private RequestHolder $requestHolder, | ||
| ) {} | ||
|
|
||
| public array $cookies { | ||
| get { | ||
| return $this->requestHolder->request->cookies; | ||
| } | ||
| } | ||
|
|
||
| public array $files { | ||
| get { | ||
| return $this->requestHolder->request->files; | ||
| } | ||
| } | ||
|
|
||
| public array $query { | ||
| get { | ||
| return $this->requestHolder->request->query; | ||
| } | ||
| } | ||
|
|
||
| public string $path { | ||
| get { | ||
| return $this->requestHolder->request->path; | ||
| } | ||
| } | ||
| public RequestHeaders $headers { | ||
| get { | ||
| return $this->requestHolder->request->headers; | ||
| } | ||
| } | ||
|
|
||
| public array $body { | ||
| get { | ||
| return $this->requestHolder->request->body; | ||
| } | ||
| } | ||
|
|
||
| public ?string $raw { | ||
| get { | ||
| return $this->requestHolder->request->raw; | ||
| } | ||
| } | ||
|
|
||
| public string $uri { | ||
| get { | ||
| return $this->requestHolder->request->uri; | ||
| } | ||
| } | ||
|
|
||
| public Method $method { | ||
| get { | ||
| return $this->requestHolder->request->method; | ||
| } | ||
| } | ||
|
|
||
| public function has(string $key): bool | ||
| { | ||
| return $this->requestHolder->request->has($key); | ||
| } | ||
|
|
||
| public function hasBody(?string $key = null): bool | ||
| { | ||
| return $this->requestHolder->request->hasBody($key); | ||
| } | ||
|
|
||
| public function hasQuery(string $key): bool | ||
| { | ||
| return $this->requestHolder->request->hasQuery($key); | ||
| } | ||
|
|
||
| public function get(string $key, mixed $default = null): mixed | ||
| { | ||
| return $this->requestHolder->request->get($key, $default); | ||
| } | ||
|
|
||
| public function getSessionValue(string $name): mixed | ||
| { | ||
| return $this->requestHolder->request->getSessionValue($name); | ||
| } | ||
|
|
||
| public function getCookie(string $name): ?Cookie | ||
| { | ||
| return $this->requestHolder->request->getCookie($name); | ||
| } | ||
|
|
||
| public function accepts(ContentType ...$contentType): bool | ||
| { | ||
| return $this->requestHolder->request->accepts(...$contentType); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Tempest\Http; | ||
|
|
||
| use Tempest\Container\Singleton; | ||
|
|
||
| #[Singleton] | ||
| final class RequestHolder | ||
| { | ||
| private(set) Request $request; | ||
|
|
||
| public function setRequest(Request $request): void | ||
| { | ||
| $this->request = $request; | ||
| } | ||
|
|
||
| public function clear(): void | ||
| { | ||
| unset($this->request); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Tempest\Router; | ||
|
|
||
| use Tempest\Container\Container; | ||
| use Tempest\Container\Singleton; | ||
| use Tempest\Core\Application; | ||
| use Tempest\Core\Kernel; | ||
| use Tempest\Core\Tempest; | ||
| use Tempest\Http\RequestFactory; | ||
| use Tempest\Http\RequestHolder; | ||
|
|
||
| #[Singleton] | ||
| final readonly class WorkerApplication implements Application | ||
| { | ||
| public function __construct( | ||
| private Container $container, | ||
| private int $maxLoops = -1, | ||
| ) {} | ||
|
|
||
| /** @param \Tempest\Discovery\DiscoveryLocation[] $discoveryLocations */ | ||
| public static function boot(string $root, array $discoveryLocations = [], int $maxLoops = -1): self | ||
| { | ||
| $container = Tempest::boot($root, $discoveryLocations); | ||
|
|
||
| return new self($container, $maxLoops); | ||
| } | ||
|
|
||
| public function run(): never | ||
| { | ||
| // Inspired from https://github.com/php-runtime/frankenphp-symfony/blob/main/src/Runner.php | ||
| // Prevent worker script termination when a client connection is interrupted | ||
| ignore_user_abort(true); | ||
|
|
||
| $requestFactory = $this->container->get(RequestFactory::class); | ||
| $responseSender = $this->container->get(ResponseSender::class); | ||
| $router = $this->container->get(WorkerRouter::class); | ||
| $requestHolder = $this->container->get(RequestHolder::class); | ||
|
|
||
| $server = array_filter($_SERVER, static fn (string $key) => ! str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY); | ||
|
|
||
| $handler = function () use ($server, $requestFactory, $responseSender, $router, $requestHolder): void { | ||
| // Merge the environment variables coming from DotEnv with the ones tied to the current request | ||
| $_SERVER += $server; | ||
|
|
||
| $psrRequest = $requestFactory->make(); | ||
| $response = $router->dispatch($psrRequest); | ||
| $responseSender->send($response); | ||
| // TODO: this probably should be handled by RESET event I'm talking about below | ||
| $requestHolder->clear(); | ||
| }; | ||
|
|
||
| $loops = 0; | ||
|
|
||
| // it still allows to run application without frankenphp, but it will be a single request and then exit | ||
| // this is only useful for testing and development | ||
| if (! function_exists('frankenphp_handle_request')) { | ||
| $handler(); | ||
|
|
||
| exit(); | ||
| } | ||
|
|
||
| do { | ||
| $ret = \frankenphp_handle_request($handler); | ||
|
|
||
| // TODO: there should be some event to RESET state | ||
| // for example: CookieManager should reset its internal cookies state after each request | ||
|
|
||
| gc_collect_cycles(); | ||
| } while ($ret && (-1 === $this->maxLoops || ++$loops < $this->maxLoops)); | ||
|
|
||
| $this->container->get(Kernel::class)->shutdown(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Tempest\Router; | ||
|
|
||
| use Psr\Http\Message\ServerRequestInterface as PsrRequest; | ||
| use Tempest\Container\Container; | ||
| use Tempest\Http\Mappers\PsrRequestToGenericRequestMapper; | ||
| use Tempest\Http\OpaqueRequest; | ||
| use Tempest\Http\Request; | ||
| use Tempest\Http\RequestHolder; | ||
| use Tempest\Http\Response; | ||
|
|
||
| use function Tempest\Mapper\map; | ||
|
|
||
| final readonly class WorkerRouter implements Router | ||
| { | ||
| public function __construct( | ||
| private Container $container, | ||
| private Router $router, | ||
| private RequestHolder $requestHolder, | ||
| ) {} | ||
|
|
||
| public function dispatch(Request|PsrRequest $request): Response | ||
| { | ||
| if (! $request instanceof Request) { | ||
| $request = map($request)->with(PsrRequestToGenericRequestMapper::class)->do(); | ||
| } | ||
|
|
||
| $this->requestHolder->setRequest($request); | ||
| $this->container->singleton(Request::class, $this->container->get(OpaqueRequest::class)); | ||
|
|
||
| return $this->router->dispatch($request); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,17 @@ | |
| declare(strict_types=1); | ||
|
|
||
| use Tempest\Router\HttpApplication; | ||
| use Tempest\Router\WorkerApplication; | ||
|
|
||
| require_once __DIR__ . '/../vendor/autoload.php'; | ||
|
|
||
| if (function_exists('frankenphp_handle_request')) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not a good idea, see #1792 (comment)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, I fully agree. I guess in the final form of this PR I will just remove it, because as far as I understand from reading discord discussion, preparing tempest dockerfiles is another beast to deal with, and probably shouldn't even be handled in this PR. I made that draft-PR to mark/communicate that I'm willing to try work on worker mode support.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds great, we're looking forward to your contribution! In this case, this comment might also be helpful, this is one of the known limitations. |
||
| WorkerApplication::boot( | ||
| root: __DIR__ . '/../', | ||
| maxLoops: 500, // TODO: make it configurable | ||
| )->run(); | ||
|
|
||
| exit(); | ||
| } | ||
|
|
||
| HttpApplication::boot(__DIR__ . '/../')->run(); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a good idea, see #1792 (comment)