Skip to content

Commit dfea6c9

Browse files
committed
Make index dispatches failure-safe
Every save of an indexable entity dispatches on the plugin's command bus, which (with no transport routing) runs synchronously inside the Doctrine flush. If Meilisearch was slow or down, saving/deleting a product failed. EntityListener now wraps every dispatch in try/catch and logs the error at error level (dedicated setono_sylius_meilisearch monolog channel), so a search-index hiccup — or a transport outage on an async setup — can never break catalog management. All plugin command messages already share the CommandInterface marker, so a single framework.messenger.routing entry can route everything to an async transport (documented separately in the operational-model docs). Closes #124
1 parent be49d96 commit dfea6c9

3 files changed

Lines changed: 42 additions & 1 deletion

File tree

src/EventListener/Doctrine/EntityListener.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Setono\SyliusMeilisearchPlugin\EventListener\Doctrine;
66

77
use Doctrine\Persistence\Event\LifecycleEventArgs;
8+
use Psr\Log\LoggerInterface;
9+
use Psr\Log\NullLogger;
810
use Setono\SyliusMeilisearchPlugin\Message\Command\IndexEntity;
911
use Setono\SyliusMeilisearchPlugin\Message\Command\RemoveEntity;
1012
use Setono\SyliusMeilisearchPlugin\Model\IndexableInterface;
@@ -19,6 +21,7 @@ final class EntityListener
1921

2022
public function __construct(
2123
private readonly MessageBusInterface $commandBus,
24+
private readonly LoggerInterface $logger = new NullLogger(),
2225
) {
2326
$this->removeIndexableStorage = new SplObjectStorage();
2427
}
@@ -73,8 +76,20 @@ private function dispatch(LifecycleEventArgs $eventArgs, callable $message): voi
7376
{
7477
$indexable = self::extractIndexableFromEvent($eventArgs);
7578

76-
if (null !== $indexable) {
79+
if (null === $indexable) {
80+
return;
81+
}
82+
83+
try {
7784
$this->commandBus->dispatch($message($indexable));
85+
} catch (\Throwable $e) {
86+
// A search-index update (or its transport being unavailable) must never break the
87+
// entity save it is reacting to. Swallow and log instead.
88+
$this->logger->error('Failed to dispatch a Meilisearch indexing command for an entity change: {message}', [
89+
'message' => $e->getMessage(),
90+
'entity' => $indexable::class,
91+
'exception' => $e,
92+
]);
7893
}
7994
}
8095

src/Resources/config/services/event_listener.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
<!-- Doctrine specific event subscribers -->
66
<service id="Setono\SyliusMeilisearchPlugin\EventListener\Doctrine\EntityListener">
77
<argument type="service" id="setono_sylius_meilisearch.command_bus"/>
8+
<argument type="service" id="logger"/>
89

910
<tag name="doctrine.event_listener" event="postPersist"/>
1011
<tag name="doctrine.event_listener" event="postUpdate"/>
1112
<tag name="doctrine.event_listener" event="preRemove"/>
1213
<tag name="doctrine.event_listener" event="postRemove"/>
14+
<tag name="monolog.logger" channel="setono_sylius_meilisearch"/>
1315
</service>
1416

1517
<service id="Setono\SyliusMeilisearchPlugin\EventListener\Doctrine\SynonymListener">

tests/Unit/EventListener/Doctrine/EntityListenerTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Setono\SyliusMeilisearchPlugin\EventListener\Doctrine\EntityListener;
1313
use Setono\SyliusMeilisearchPlugin\Message\Command\RemoveEntity;
1414
use Setono\SyliusMeilisearchPlugin\Model\IndexableInterface;
15+
use Setono\SyliusMeilisearchPlugin\Tests\Unit\Indexer\SpyLogger;
1516
use Symfony\Component\Messenger\Envelope;
1617
use Symfony\Component\Messenger\MessageBusInterface;
1718

@@ -48,4 +49,27 @@ public function it_dispatches_a_remove_command_and_does_not_leak_storage_entries
4849
self::assertInstanceOf(\Countable::class, $storage);
4950
self::assertCount(0, $storage, 'The SplObjectStorage must not retain the entity after postRemove');
5051
}
52+
53+
/**
54+
* @test
55+
*/
56+
public function it_swallows_and_logs_a_failing_dispatch_so_the_entity_save_still_succeeds(): void
57+
{
58+
$entity = $this->prophesize(IndexableInterface::class);
59+
$entity->getId()->willReturn(7);
60+
61+
$eventArgs = new LifecycleEventArgs($entity->reveal(), $this->prophesize(ObjectManager::class)->reveal());
62+
63+
$commandBus = $this->prophesize(MessageBusInterface::class);
64+
$commandBus->dispatch(Argument::any())->willThrow(new \RuntimeException('Meilisearch is down'));
65+
66+
$logger = new SpyLogger();
67+
$listener = new EntityListener($commandBus->reveal(), $logger);
68+
69+
// Must not throw, even though the dispatch fails
70+
$listener->postPersist($eventArgs);
71+
72+
$error = $logger->firstOfLevel('error');
73+
self::assertNotNull($error);
74+
}
5175
}

0 commit comments

Comments
 (0)