Make updateSiblings and resetChildrenIndexBy fully async - #426
Conversation
The updateDataObject and updateDocument event subscribers were calling IndexElementIndexService::updateSiblings() and resetChildrenIndexBy() synchronously, which made blocking calls to the search provider during HTTP requests. This caused significant slowdowns for large folders. Dispatch an UpdateSiblingsMessage via the message bus so that sibling and children index updates are processed asynchronously through the existing queue infrastructure, consistent with how updateData() works.
Tests cover: - UpdateSiblingsHandler: verifies service method dispatch based on element type, sort-by-index flag, resetChildrenIndexBy flag, and null element handling - DataObjectIndexUpdateSubscriber: verifies UpdateSiblingsMessage is dispatched with correct parameters - DocumentIndexUpdateSubscriber: same verification for documents
|
|
Follow up PR: cancan101#2 |
There was a problem hiding this comment.
Hello @cancan101 ,
thanks for the pr, could you merge the current 2026.x again? This should fix the failing tests.
I noticed a few things:
resetChildrenIndexBy()fills the sharedBulkOperationServicebuffer but never
callscommit()(unlikeupdateSiblings(), seeIndexElementIndexService.php:77).- I guess we need to consult
SynchronousProcessingServiceInterfaceas well here, because CLI and tests enable synchronous mode and expect direct results UpdateSiblingsMessagegets dispatched on every save, ignoring theisInstalledguard inupdateData
How do you want to handle the follow up? Merge this pr first, then rebase the follow up to 2026.x in pimcore/gdi?
| if ($event->getObject()->getChildrenSortBy() === AbstractObject::OBJECT_CHILDREN_SORT_BY_INDEX) { | ||
| $this->indexElementIndexService->resetChildrenIndexBy($event->getObject()); | ||
| } | ||
| $this->messageBus->dispatch( |
There was a problem hiding this comment.
Move the isInstalled guard up here? Or at least check it here as well?
| && $element instanceof AbstractObject | ||
| && $element->getChildrenSortBy() === AbstractObject::OBJECT_CHILDREN_SORT_BY_INDEX | ||
| ) { | ||
| $this->indexElementIndexService->resetChildrenIndexBy($element); |
There was a problem hiding this comment.
commit() missing here, either add it to message, or probably even at the end of resetChildrenIndexBy method itself.
- Commit the bulk buffer at the end of resetChildrenIndexBy() so children index resets are actually flushed to the search index (previously the buffer was filled via addUpdate() but never committed). - Respect synchronous processing in the update subscribers: dispatch UpdateSiblingsMessage with a SYNC TransportNamesStamp when SynchronousProcessingService is enabled, so CLI commands and tests get direct, synchronous results (mirrors QueueMessagesDispatcher). - Guard the message dispatch behind Installer::isInstalled() in updateDataObject()/updateDocument() so no message is dispatched when the bundle is not installed, matching the existing guard in updateData(). - Extend the subscriber unit tests to cover the synchronous-dispatch and not-installed paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017t757oNTEGGJYPTdnTaMsA
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017t757oNTEGGJYPTdnTaMsA
|
Hello @cancan101 , just wanted to ask if you are still working on it or if the pr is ready for review again? Besides the failing tests. |
Installer and QueueMessagesDispatcher are final, so PHPUnit cannot double them and every new subscriber test errored out. Type-hint the interfaces instead: - both subscribers now take Pimcore's InstallerInterface; the concrete installer is wired explicitly since that interface is too generic to autowire - add QueueMessagesDispatcherInterface and alias it to the existing service - IndexQueueServiceInterface now returns itself instead of the concrete IndexQueueService, so the fluent chain can be doubled Also drop an unused import in UpdateSiblingsHandler.
There was a problem hiding this comment.
Pull request overview
Verdict: Needs changes. The PR moves sibling indexing to Messenger, but transaction-safety and message validation remain unresolved.
Changes:
- Adds asynchronous sibling-update messages and handlers.
- Routes subscriber updates through Messenger with synchronous-mode support.
- Adds interfaces, service wiring, bulk commits, and unit tests.
Review assessment:
- Root cause: Removes normal request-time provider calls (
DataObjectIndexUpdateSubscriber.php:73-84,DocumentIndexUpdateSubscriber.php:69-80). - Call sites/boundary: Both existing callers are covered, but direct dispatch bypasses the transaction protection in
IndexQueueService.php:68-82. - BC: No public API break found; changed contracts are internal.
- Tests: Handler and dispatch branches are covered, but transaction rollback/race behavior is not.
- Documentation: No changelog or upgrade note was added.
- Risk: Non-Doctrine transports can process uncommitted updates; the message also accepts unsupported element types (
UpdateSiblingsMessage.php:23).
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/Unit/MessageHandler/UpdateSiblingsHandlerTest.php |
Tests handler branches. |
tests/Unit/EventSubscriber/DocumentIndexUpdateSubscriberTest.php |
Tests document dispatch. |
tests/Unit/EventSubscriber/DataObjectIndexUpdateSubscriberTest.php |
Tests object dispatch. |
src/Service/SearchIndex/IndexQueueServiceInterface.php |
Generalizes fluent returns. |
src/Service/SearchIndex/IndexQueue/QueueMessagesDispatcherInterface.php |
Adds dispatcher contract. |
src/Service/SearchIndex/IndexQueue/QueueMessagesDispatcher.php |
Implements new contract. |
src/Service/SearchIndex/IndexElementIndexService.php |
Commits child-index updates. |
src/MessageHandler/UpdateSiblingsHandler.php |
Processes sibling messages. |
src/Message/UpdateSiblingsMessage.php |
Defines asynchronous payload. |
src/EventSubscriber/DocumentIndexUpdateSubscriber.php |
Dispatches document updates. |
src/EventSubscriber/DataObjectIndexUpdateSubscriber.php |
Dispatches object updates. |
config/services/search/index.yaml |
Wires dispatcher interface. |
config/services.yaml |
Configures subscriber dependencies. |
config/pimcore/messenger.yaml |
Routes the new message. |
| $this->messageBus->dispatch( | ||
| new UpdateSiblingsMessage( | ||
| $event->getObject()->getId(), | ||
| ElementType::DATA_OBJECT->value, | ||
| true |
There was a problem hiding this comment.
This is pre-existing behaviour rather than something introduced here. Before this PR, updateDataObject() called IndexElementIndexService::updateSiblings() synchronously and inline, with no transaction guard at all, so the same pre-commit/rollback window already existed — without a queue in between.
On a default install this change actually improves things: Pimcore's default transport prefix is doctrine://default?queue_name= (CoreBundle config/pimcore/default.yaml), so the queued message is inserted in the same DB transaction and is rolled back together with the object.
The remaining gap applies to installs that point PIMCORE_MESSENGER_TRANSPORT_DSN_PREFIX at AMQP/Redis, plus the synchronous-mode path. Closing that properly means modelling sibling updates as generic_data_index_queue entries — the guard in IndexQueueService.php:68-82 falls back to that DB table, not to Messenger — which is a larger change than this PR. Happy to look at it as a follow-up.
| $this->messageBus->dispatch( | ||
| new UpdateSiblingsMessage( | ||
| $event->getDocument()->getId(), | ||
| ElementType::DOCUMENT->value, | ||
| false |
There was a problem hiding this comment.
This is pre-existing behaviour rather than something introduced here. Before this PR, updateDocument() called IndexElementIndexService::updateSiblings() synchronously and inline, with no transaction guard at all, so the same pre-commit/rollback window already existed — without a queue in between.
On a default install this change actually improves things: Pimcore's default transport prefix is doctrine://default?queue_name= (CoreBundle config/pimcore/default.yaml), so the queued message is inserted in the same DB transaction and is rolled back together with the object.
The remaining gap applies to installs that point PIMCORE_MESSENGER_TRANSPORT_DSN_PREFIX at AMQP/Redis, plus the synchronous-mode path. Closing that properly means modelling sibling updates as generic_data_index_queue entries — the guard in IndexQueueService.php:68-82 falls back to that DB table, not to Messenger — which is a larger change than this PR. Happy to look at it as a follow-up.
| { | ||
| public function __construct( | ||
| private int $elementId, | ||
| private string $elementType, |
The message took a plain string, so an unsupported type such as 'asset' resolved to an Asset and blew up in updateSiblings(). Use the enum as EnqueueRelatedIdsMessage does, and skip anything that is not an object or a document in the handler.
|



Changes in this pull request
Resolves #389
Additional info
DataObjectIndexUpdateSubscriber::updateDataObject()andDocumentIndexUpdateSubscriber::updateDocument()were callingIndexElementIndexService::updateSiblings()andresetChildrenIndexBy()synchronously, making blocking calls to the search provider (e.g. OpenSearch/Elasticsearch) during HTTP requestsUpdateSiblingsMessageandUpdateSiblingsHandlerto dispatch sibling/children index updates via the Symfony Messenger bus, so they are processed asynchronously through the existingpimcore_generic_data_index_queuetransportIndexElementIndexServiceInterfacedependency from both event subscribers, replacing it withMessageBusInterfaceto dispatch the new messageThis eliminates the synchronous search-provider roundtrips that caused significant slowdowns during object updates, especially for large folders.
Test plan
UpdateSiblingsMessageto the queue instead of blockingUpdateSiblingsMessageto the queue instead of blockingresetChildrenIndexByis triggered for data objects withOBJECT_CHILDREN_SORT_BY_INDEXsort modeconfig/pimcore/messenger.yaml