Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,30 @@ forme mécanique.
Ces exceptions sont déterministes : rejouées à l'identique à chaque redélivrance, elles brûlent les
tentatives de Messenger jusqu'au transport d'échec. Configurez-en un.

### Onze services internes du bundle passent en privé

**Qui est concerné** : une application qui tire l'un de ces onze identifiants du conteneur par
`$container->get()`. Pas celle qui les reçoit par autowiring, ni celle qui passe par leur interface.

Les implémentations concrètes derrière un alias et les décorateurs de projection n'ont pas à être
des points d'entrée du conteneur : un service public échappe à l'*inlining* et à la suppression des
définitions inutilisées, et devient une promesse de compatibilité que personne n'a voulu prendre.

| Devenu privé | À demander à la place |
| --- | --- |
| `durable.event_store.dbal`, `durable.event_store.temporal`, `durable.event_store.inner`, `durable.event_store.*.projecting` | `Gplanchat\Durable\Store\EventStoreInterface` |
| `durable.workflow_metadata_store.inner`, `durable.workflow_metadata_store.*.projecting` | `Gplanchat\Durable\Store\WorkflowMetadataStore` |
| `durable.run_catalog.dbal`, `durable.run_catalog.in_memory`, `durable.run_catalog.temporal` | `Gplanchat\Durable\Port\WorkflowRunCatalogInterface` |

Les trois interfaces restent **publiques** et autowirables, et elles pointent la même instance : ce
qui change est le chemin pour y arriver, pas ce qu'on obtient. Le reste de la surface publique du
bundle est inchangé — les workers Temporal, le magasin de liens parent/enfant, le collecteur de
profil et les classes du moteur restent joignables par leur identifiant.

Rector ne peut rien : réécrire un `$container->get('durable.event_store.dbal')` en une injection
demande de savoir où l'objet est utilisé, ce qu'aucune règle ne devine. Le tableau ci-dessus est la
procédure.

## 0.1.0-alpha8

### La garde de divergence compare aussi la charge
Expand Down
20 changes: 20 additions & 0 deletions src/Durable/RegistryActivityExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,24 @@

namespace Gplanchat\Durable;

use Psr\Container\ContainerInterface;

final class RegistryActivityExecutor implements ActivityExecutor
{
/** @var array<string, callable(array<string, mixed>): mixed> */
private array $handlers = [];

/**
* @param ContainerInterface|null $lazyHandlers handlers indexed by activity name, resolved at
* call time rather than at construction
*/
public function __construct(
private readonly ?ContainerInterface $lazyHandlers = null,
) {}

/**
* Direct registration, for hosts with no service container to offer.
*/
public function register(string $activityName, callable $handler): void
{
$this->handlers[$activityName] = $handler;
Expand All @@ -17,6 +30,13 @@ public function register(string $activityName, callable $handler): void
public function execute(string $activityName, array $payload): mixed
{
$handler = $this->handlers[$activityName] ?? null;

// Le localisateur en dernier : un enregistrement direct l'emporte, ce qui laisse un test
// remplacer un gestionnaire sans reconstruire le conteneur.
if (null === $handler && $this->lazyHandlers?->has($activityName)) {
$handler = $this->lazyHandlers->get($activityName);
}

if (null === $handler) {
throw new \RuntimeException(\sprintf('No handler registered for activity "%s"', $activityName));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@
use Gplanchat\Durable\Activity\PayloadToContractMethodInvoker;
use Gplanchat\Durable\ActivityExecutor;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
* Registers on {@see ActivityExecutor} the activities exposed by the services tagged durable.activity_handler.
*
* Through a **service locator**, not through an array of callables. Passing
* `[new Reference($invoker), '__invoke']` would force the container to resolve every reference to
* build the argument: it would then instantiate every handler in the application, and their
* connections, HTTP clients and other dependencies, in order to call one. On a worker that handles
* one activity per message, that is paid on every message.
*/
final class ActivityHandlerPass implements CompilerPassInterface
{
Expand All @@ -39,6 +46,9 @@ public function process(ContainerBuilder $container): void
$executor = $container->findDefinition($executorId);
$resolver = new ActivityContractResolver(null);

/** @var array<string, Reference> $handlerRefs */
$handlerRefs = [];

foreach ($tagged as $serviceId => $tags) {
foreach ($tags as $tag) {
$contract = $tag['contract'] ?? null;
Expand Down Expand Up @@ -72,13 +82,23 @@ public function process(ContainerBuilder $container): void
->setPublic(false)
;

$executor->addMethodCall('register', [
$activityName,
[new Reference($invokerId), '__invoke'],
]);
// A reference in the locator, not a callable built at
// compilation: building `[new Reference(...), '__invoke']` would force the
// container to instantiate **every** invoker, so every handler and its
// dependencies, in order to call one.
$handlerRefs[$activityName] = new Reference($invokerId);
}
}
}

if ([] === $handlerRefs) {
return;
}

// The locator builds only what it is asked for, and `ServiceLocatorTagPass` deduplicates it
// across passes: it is the upstream mechanism for "many candidates, one called", the one
// `MessengerPass` uses for message handlers.
$executor->setArgument('$lazyHandlers', ServiceLocatorTagPass::register($container, $handlerRefs));
}

/**
Expand Down
22 changes: 11 additions & 11 deletions src/DurableBundle/DependencyInjection/DurableExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ private function registerDbalStores(ContainerBuilder $container, array $config):
if ($eventStoreDbal) {
$container->register('durable.event_store.dbal', DbalEventStore::class)
->setArguments([$connection, $schema, $config['event_store']['table_name']])
->setPublic(true)
->setPublic(false)
;
$container->setAlias(EventStoreInterface::class, 'durable.event_store.dbal')->setPublic(true);

Expand All @@ -169,7 +169,7 @@ private function registerDbalStores(ContainerBuilder $container, array $config):
if ($metadataDbal) {
$container->register('durable.workflow_metadata_store.inner', DbalWorkflowMetadataStore::class)
->setArguments([$connection, $schema, $config['workflow_metadata']['table_name']])
->setPublic(true)
->setPublic(false)
;
$container->setAlias(WorkflowMetadataStore::class, 'durable.workflow_metadata_store.inner')->setPublic(true);
}
Expand Down Expand Up @@ -207,7 +207,7 @@ private function registerDbalRunCatalog(ContainerBuilder $container, Reference $

$container->register('durable.event_store.dbal.projecting', ProjectingEventStore::class)
->setArguments([new Reference('durable.event_store.dbal'), $projection])
->setPublic(true)
->setPublic(false)
;
$container->setAlias(EventStoreInterface::class, 'durable.event_store.dbal.projecting')->setPublic(true);

Expand All @@ -224,13 +224,13 @@ private function registerDbalRunCatalog(ContainerBuilder $container, Reference $

$container->register('durable.workflow_metadata_store.projecting', ProjectingWorkflowMetadataStore::class)
->setArguments([new Reference('durable.workflow_metadata_store.inner'), $projection])
->setPublic(true)
->setPublic(false)
;
$container->setAlias(WorkflowMetadataStore::class, 'durable.workflow_metadata_store.projecting')->setPublic(true);

$container->register('durable.run_catalog.dbal', DbalWorkflowRunCatalog::class)
->setArguments([$connection, $schema])
->setPublic(true)
->setPublic(false)
;
$container->setAlias(WorkflowRunCatalogInterface::class, 'durable.run_catalog.dbal')->setPublic(true);
}
Expand Down Expand Up @@ -260,14 +260,14 @@ private function registerInMemoryRunCatalog(ContainerBuilder $container): void

$container->register('durable.run_catalog.in_memory', InMemoryWorkflowRunCatalog::class)
->setArguments([new Reference('durable.event_store.inner')])
->setPublic(true)
->setPublic(false)
;
$catalog = new Reference('durable.run_catalog.in_memory');
$container->setAlias(WorkflowRunCatalogInterface::class, 'durable.run_catalog.in_memory')->setPublic(true);

$container->register('durable.event_store.in_memory.projecting', ProjectingEventStore::class)
->setArguments([new Reference('durable.event_store.inner'), $catalog])
->setPublic(true)
->setPublic(false)
;
$container->setAlias(EventStoreInterface::class, 'durable.event_store.in_memory.projecting')->setPublic(true);

Expand All @@ -281,7 +281,7 @@ private function registerInMemoryRunCatalog(ContainerBuilder $container): void

$container->register('durable.workflow_metadata_store.in_memory.projecting', ProjectingWorkflowMetadataStore::class)
->setArguments([new Reference('durable.workflow_metadata_store.inner'), $catalog])
->setPublic(true)
->setPublic(false)
;
$container->setAlias(WorkflowMetadataStore::class, 'durable.workflow_metadata_store.in_memory.projecting')->setPublic(true);
}
Expand Down Expand Up @@ -330,7 +330,7 @@ private static function isTemporalNative(array $config): bool
*/
private function registerEventStore(ContainerBuilder $container, array $config): void
{
$container->register('durable.event_store.inner', InMemoryEventStore::class)->setPublic(true);
$container->register('durable.event_store.inner', InMemoryEventStore::class)->setPublic(false);

$temporalConfig = $config['temporal'] ?? [];
$dsn = $temporalConfig['dsn'] ?? null;
Expand Down Expand Up @@ -378,7 +378,7 @@ private function registerEventStore(ContainerBuilder $container, array $config):
new Reference('durable.temporal.connection'),
new Reference(TemporalHistoryCursor::class),
])
->setPublic(true)
->setPublic(false)
;
if ($journal) {
$container->setAlias(WorkflowRunCatalogInterface::class, 'durable.run_catalog.temporal')->setPublic(true);
Expand Down Expand Up @@ -417,7 +417,7 @@ private function registerEventStore(ContainerBuilder $container, array $config):
new Reference(TemporalHistoryCursor::class),
new Reference(WorkflowClientInterface::class),
])
->setPublic(true)
->setPublic(false)
;

if ($journal) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

namespace unit\Gplanchat\DurableBundle\DependencyInjection\Compiler;

use Gplanchat\Durable\ActivityExecutor;
use Gplanchat\Durable\Bundle\DependencyInjection\DurableExtension;
use Gplanchat\Durable\Bundle\DurableBundle;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use unit\DurableBundle\Fixtures\CompteurDInstances;
use unit\DurableBundle\Fixtures\PremierHandler;
use unit\DurableBundle\Fixtures\SecondHandler;

/**
* Exécuter une activité ne doit construire que son gestionnaire.
*
* L'exécuteur recevait ses gestionnaires sous forme de callables `[Reference, '__invoke']`. Pour
* bâtir ce tableau, le conteneur doit résoudre chaque référence : il instancie donc **tous** les
* gestionnaires de l'application — et leurs dépendances, connexions et clients HTTP compris — pour
* en appeler un seul.
*
* Ce que ça coûte ne se voit pas en développement, où les gestionnaires sont légers. Ça se voit sur
* un worker qui traite une activité par message, avec vingt contrats déclarés.
*/
final class ActivityHandlersAreLazyTest extends TestCase
{
protected function setUp(): void
{
CompteurDInstances::reset();
}

public function testUneSeuleActiviteExecuteeNeConstruitQueSonGestionnaire(): void
{
$container = $this->compile();

/** @var ActivityExecutor $executor */
$executor = $container->get(ActivityExecutor::class);

self::assertSame(
0,
CompteurDInstances::total(),
'obtenir l\'exécuteur ne doit construire aucun gestionnaire',
);

$executor->execute('premier.faire', ['quoi' => 'ceci']);

self::assertSame(['premier'], CompteurDInstances::construits());
}

public function testLesDeuxGestionnairesRestentJoignables(): void
{
$container = $this->compile();

/** @var ActivityExecutor $executor */
$executor = $container->get(ActivityExecutor::class);

self::assertSame('premier:ceci', $executor->execute('premier.faire', ['quoi' => 'ceci']));
self::assertSame('second:cela', $executor->execute('second.faire', ['quoi' => 'cela']));
self::assertSame(['premier', 'second'], CompteurDInstances::construits());
}

public function testUneActiviteInconnueEchoueToujoursClairement(): void
{
$container = $this->compile();

/** @var ActivityExecutor $executor */
$executor = $container->get(ActivityExecutor::class);

$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches('/inconnue/');

$executor->execute('inconnue', []);
}

private function compile(): ContainerBuilder
{
$container = new ContainerBuilder();
$container->setParameter('kernel.debug', false);
(new DurableExtension())->load([[]], $container);
$container->register('messenger.default_bus', \stdClass::class)->setPublic(true);

foreach ([PremierHandler::class, SecondHandler::class] as $class) {
$container->register($class, $class)->setAutoconfigured(true)->setPublic(false);
}


(new DurableBundle())->build($container);
$container->compile();

return $container;
}
}
Loading
Loading