Skip to content

Commit

Permalink
Additional session support for worker mode (#7)
Browse files Browse the repository at this point in the history
Additional session support for worker mode
  • Loading branch information
FluffyDiscord authored Sep 16, 2024
1 parent d2004ca commit 5625a9a
Show file tree
Hide file tree
Showing 9 changed files with 1,107 additions and 23 deletions.
894 changes: 894 additions & 0 deletions .editorconfig

Large diffs are not rendered by default.

12 changes: 0 additions & 12 deletions .idea/codeception.xml

This file was deleted.

10 changes: 0 additions & 10 deletions .idea/phpunit.xml

This file was deleted.

14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ class MyController
}
```

## Sessions

Currently, Symfony might struggle with sessions in worker mode, like loosing logged user or the opposite,
leaking logged user session to another request due to missing globals (explained at the end).

Bundle introduces `FluffyDiscord\RoadRunnerBundle\Session\WorkerSessionStorageFactory`, that handles native session correctly.
This factory gets registered automatically by default if you are using `symfony/flex` (pending flex recipe PR). You can also register it manually, for example in `framework.yaml`:

```yaml
framework:
session:
storage_factory_id: FluffyDiscord\RoadRunnerBundle\Session\WorkerSessionStorageFactory
```

## Sentry

Built in support for [Sentry](https://packagist.org/packages/sentry/sentry-symfony). Just install & configure it as you normally do.
Expand Down
19 changes: 19 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use FluffyDiscord\RoadRunnerBundle\Factory\RPCFactory;
use FluffyDiscord\RoadRunnerBundle\Session\WorkerSessionStorageFactory;
use FluffyDiscord\RoadRunnerBundle\Worker\CentrifugoWorker;
use FluffyDiscord\RoadRunnerBundle\Worker\HttpWorker as BundleHttpWorker;
use FluffyDiscord\RoadRunnerBundle\Worker\WorkerRegistry;
Expand All @@ -20,6 +21,8 @@
use Spiral\RoadRunner\WorkerInterface as RoadRunnerWorkerInterface;
use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\HttpKernel\KernelInterface;

return static function (ContainerConfigurator $container) {
Expand Down Expand Up @@ -80,6 +83,22 @@
])
;

// Worker sessions fix
$services
->set(WorkerSessionStorageFactory::class)
->args([
param('session.storage.options'),
service('session.handler'),
inline_service(MetadataBag::class)
->args([
param('session.metadata.storage_key'),
param('session.metadata.update_threshold'),
]),
service(RequestStack::class),
false,
])
;

// Centrifugo
if (class_exists(RoadRunnerCentrifugoWorker::class)) {
$services
Expand Down
36 changes: 36 additions & 0 deletions install/.rr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
######################################################################################
# THIS IS SAMPLE OF THE CONFIGURATION #
# IT'S NOT A DEFAULT CONFIGURATION, IT'S JUST A REFERENCE TO ALL OPTIONS AND PLUGINS #
# MORE DOCS CAN BE FOUND HERE: <https://docs.roadrunner.dev/docs/general/config> #
######################################################################################

# More info: https://docs.roadrunner.dev/docs/general/config

version: "3"

server:
command: "php public/index.php"
env:
- APP_RUNTIME: FluffyDiscord\RoadRunnerBundle\Runtime\Runtime
http:
address: 0.0.0.0:8080
pool:
debug: true # development only! https://docs.roadrunner.dev/docs/php-worker/pool#workers-pool-configuration
middleware: [ "gzip", "static" ]
static:
dir: "public"
forbid: [ ".php", ".htaccess" ]

logs:
mode: development
channels:
http:
level: debug
server:
level: info
mode: raw
metrics:
level: debug

rpc:
listen: tcp://127.0.0.1:6001
91 changes: 91 additions & 0 deletions src/Session/WorkerSessionStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace FluffyDiscord\RoadRunnerBundle\Session;

use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;

class WorkerSessionStorage extends NativeSessionStorage
{
public function __construct(
array $options,
\SessionHandlerInterface|AbstractProxy|null $handler,
?MetadataBag $metaBag,
private readonly RequestStack $requestStack,
)
{
parent::__construct($options, $handler, $metaBag);
}

public function start(): bool
{
if ($this->started) {
return true;
}

if (\PHP_SESSION_ACTIVE === session_status()) {
throw new \RuntimeException('Failed to start the session: already started by PHP.');
}

if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL) && headers_sent($file, $line)) {
throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line));
}

/*
* Explanation of the session ID regular expression: `/^[a-zA-Z0-9,-]{22,250}$/`.
*
* ---------- Part 1
*
* The part `[a-zA-Z0-9,-]` is related to the PHP ini directive `session.sid_bits_per_character` defined as 6.
* See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character.
* Allowed values are integers such as:
* - 4 for range `a-f0-9`
* - 5 for range `a-v0-9`
* - 6 for range `a-zA-Z0-9,-`
*
* ---------- Part 2
*
* The part `{22,250}` is related to the PHP ini directive `session.sid_length`.
* See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length.
* Allowed values are integers between 22 and 256, but we use 250 for the max.
*
* Where does the 250 come from?
* - The length of Windows and Linux filenames is limited to 255 bytes. Then the max must not exceed 255.
* - The session filename prefix is `sess_`, a 5 bytes string. Then the max must not exceed 255 - 5 = 250.
*
* ---------- Conclusion
*
* The parts 1 and 2 prevent the warning below:
* `PHP Warning: SessionHandler::read(): Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed.`
*
* The part 2 prevents the warning below:
* `PHP Warning: SessionHandler::read(): open(filepath, O_RDWR) failed: No such file or directory (2).`
*/
$sessionId = $this->requestStack->getCurrentRequest()?->cookies->get(session_name());
if ($sessionId) {
if (!preg_match('/^[a-zA-Z0-9,-]{22,250}$/', $sessionId)) {
$sessionId = session_create_id();
}
}

session_id($sessionId);

// ok to try and start the session
if (!session_start()) {
throw new \RuntimeException('Failed to start the session.');
}

$this->loadSession();

return true;

}

public function reset(): void
{
$this->started = false;
$this->saveHandler->close();
}
}
51 changes: 51 additions & 0 deletions src/Session/WorkerSessionStorageFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace FluffyDiscord\RoadRunnerBundle\Session;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag;
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface;
use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface;
use Symfony\Contracts\Service\ResetInterface;

class WorkerSessionStorageFactory implements SessionStorageFactoryInterface, ResetInterface
{
private ?WorkerSessionStorage $workerSessionStorage = null;

public function __construct(
#[Autowire(param: "session.storage.options")]
private readonly array $options,

#[Autowire(service: "session.handler")]
private readonly AbstractProxy|\SessionHandlerInterface|null $handler,

#[Autowire(service: "worker_session_factory_metadata_bag")]
private readonly ?MetadataBag $metaBag,

private readonly RequestStack $requestStack,
private readonly bool $secure = false,
)
{
}

public function createStorage(?Request $request): SessionStorageInterface
{
if ($this->workerSessionStorage === null) {
$this->workerSessionStorage = new WorkerSessionStorage($this->options, $this->handler, $this->metaBag, $this->requestStack);
}

if ($this->secure && $request?->isSecure()) {
$this->workerSessionStorage->setOptions(['cookie_secure' => true]);
}

return $this->workerSessionStorage;
}

public function reset(): void
{
$this->workerSessionStorage?->reset();
}
}
3 changes: 2 additions & 1 deletion src/Worker/HttpWorker.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ public function start(): void
$this->sentryHubInterface?->popScope();
}
}
} catch (\Throwable) {
} catch (\Throwable $throwable) {
$worker->getWorker()->stop();
throw $throwable;
}
}
}

0 comments on commit 5625a9a

Please sign in to comment.