Prevent transition events from being dispatched multiple times

Created on 20 April 2023, over 1 year ago
Updated 21 April 2023, over 1 year ago

Problem/Motivation

If the entity is saved during a post transition event, this may create a saving loop.

As a concrete example, we have an event subscriber that listens to the commerce_order.reserve.post_transition event to automatically send an email and save the timestamp on the order. Since our workflow can go back to previous steps, we need to send this (updated) email to the client every time the transition is triggered. Since the state item originalValue is updated after the event has been dispatched, if the entity is saved during the dispatched event, the state item during postSave is always considered as an updated value and a newpost_transition event is dispatched.

Steps to reproduce

- Create an event subscriber that a listens to a `post_transition` event and that saves the entity with updated data (excepted the state field).
- Trigger the transition
- A loop of post_transition events should be happening

Proposed resolution

We could update the originalValue with the value before dispatching the post_transition event. If the entity is saved during the event, the state item will evaluate the originalValue to be same as the value and no events will be dispatched.

🐛 Bug report
Status

Closed: works as designed

Version

1.0

Component

Code

Created by

🇨🇭Switzerland aerzas

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @aerzas
  • Open in Jenkins → Open on Drupal.org →
    Core: 9.5.x + Environment: PHP 7.3 & MySQL 5.7
    last update over 1 year ago
    26 pass
  • @aerzas opened merge request.
  • Status changed to Needs review over 1 year ago
  • Open in Jenkins → Open on Drupal.org →
    Core: 9.5.x + Environment: PHP 7.3 & MySQL 5.7
    last update over 1 year ago
    26 pass
  • 🇨🇭Switzerland aerzas

    Here is the associated patch.

  • 🇨🇭Switzerland aerzas

    Although the tests have passed, this patch would prevent the post transition subscribers to known the initial state of the transition which could break some existing listeners like OrderEventSubscriber::onOrderPostTransition in the commerce_order module.

  • 🇮🇱Israel jsacksick

    @Aerzas: I think the main issue here is that you're saving the order from a postTransition, and that is bad practice... Because the post transition event is dispatched on postSave(). Saving an entity right after it was saved is a bad idea...
    Also, this would conflict with the patch from Allow same From/To state Fixed .

  • Status changed to Needs work over 1 year ago
  • 🇨🇭Switzerland aerzas

    @jsacksick I get your point but it seems realistic to programmatically update an order with further information once a transition has passed.
    - Using the preTransition to avoid saving the entity twice could be too optimistic as the transition could still fail and some processing like sending emails cannot be reverted.
    - We could listen to the entity update hook or event (like OrderEvents::ORDER_UPDATE), as the entity update is dispatched after the postTransition is resolved, and compare the original entity state, this would produce the same result but it seems less optimized to listen to all update events to catch a single transition. We would still save the entity twice though.
    - We could save some data in a separated storage (Drupal state, dedicated table, ...), we could still get it from the entity via computed field if needed. This is probably the intended way but it may feel overkill for very simple updates like storing an email timestamp or third-party service reference.

    We prepared a patch to prevent the same transition to be applied twice (provided as a reference) but I guess we'll have to investigate the third option.

  • Open in Jenkins → Open on Drupal.org →
    Core: 9.5.x + Environment: PHP 7.3 & MySQL 5.7
    last update over 1 year ago
    26 pass
  • Open in Jenkins → Open on Drupal.org →
    Core: 9.5.x + Environment: PHP 7.3 & MySQL 5.7
    last update over 1 year ago
    26 pass
  • 🇮🇱Israel jsacksick

    @Aerzas: One option you haven't mentioned is adopting the approach from the payment order updater which would consist in "queuing" the update for after the response is sent, via a destruct() method. The idea would basically be to store in a static property the IDS of orders to update on destruct, and then on destruct() reload the order, perform the necessary actions and save the order.

  • Status changed to Closed: works as designed over 1 year ago
  • 🇨🇭Switzerland aerzas

    @jsacksick That sounds like a good solution indeed, thanks for the tip.
    Let's close this issue because the solution should be developed in our custom code base.

Production build 0.71.5 2024