Skip to content

Throw exception to prevent infinite loop caused by third party code. #18678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

convenient
Copy link
Contributor

@convenient convenient commented Oct 17, 2018

Description

This issue is a little esoteric, so bear with me.

It is possible that Magento can get caught in an infinite loop when third party observers/plugins hook into a quote totalling event (or something triggered by a quote totalling event) and attempt to get the quote from the singleton. This is because the trigger_recollect flag can trigger quote totalling before the quote has been fully assigned to the checkout session object property.

This "problem" existed in Magento 1 (https://magento.stackexchange.com/a/133312/17176) and I've debugged it a few times on different websites over the years. It's always manifested as an OOM error, or the CPU going crazy, or php timeout limits being hit. I recently saw this on a M2 site and want to do something about it in the core as once a stack trace has been identified it becomes a lot easier to identify the start of the loop and to correct it in the third party code.

Exception that represents error in the program logic. This kind of exception should lead directly to a fix in your code - http://php.net/manual/en/class.logicexception.php

I propose that a logic exception be thrown in this scenario as it would have the following benefits

  • Save system resources from being consumed unnecessarily (prevent redundant looping mysql operations, long running web requests etc)
  • Give a clear error message to the customer, the report page will be thrown with an error code rather than a white page loading forever.
  • Give developers a clear stack trace to the point where the loop began, to properly isolate and correct the third party code.

In the attached tests I verify how the bug only occurs when trigger_recollect=1 is set while the custom code is active.

Prior to the code change my test displayed the issue like this, stopping at 200 levels deep due to xdebugs maximum nesting level. This would have continued forever.

1) Magento\Quote\Model\QuoteInfiniteLoopTest::testLoadQuoteWithTriggerRecollectInfiniteLoop
Error: Maximum function nesting level of '200' reached, aborting!

/magento-2-3-develop/lib/internal/Magento/Framework/Config/Data/Scoped.php:105
/magento-2-3-develop/lib/internal/Magento/Framework/Config/Data/Scoped.php:96
/magento-2-3-develop/lib/internal/Magento/Framework/App/ResourceConnection/Config.php:68
/magento-2-3-develop/lib/internal/Magento/Framework/App/ResourceConnection.php:93
/magento-2-3-develop/generated/code/Magento/Framework/App/ResourceConnection/Interceptor.php:24
/magento-2-3-develop/lib/internal/Magento/Framework/App/ResourceConnection.php:194
/magento-2-3-develop/generated/code/Magento/Framework/App/ResourceConnection/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/Eav/Model/Entity/AbstractEntity.php:752
/magento-2-3-develop/generated/code/Magento/Catalog/Model/ResourceModel/Product/Interceptor.php:544
/magento-2-3-develop/app/code/Magento/Eav/Model/Entity/AbstractEntity.php:767
/magento-2-3-develop/generated/code/Magento/Catalog/Model/ResourceModel/Product/Interceptor.php:557
/magento-2-3-develop/app/code/Magento/Eav/Model/Entity/AbstractEntity.php:1886
/magento-2-3-develop/generated/code/Magento/Catalog/Model/ResourceModel/Product/Interceptor.php:674
/magento-2-3-develop/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php:199
/magento-2-3-develop/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php:543
/magento-2-3-develop/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php:167
/magento-2-3-develop/app/code/Magento/Catalog/Model/ResourceModel/Collection/AbstractCollection.php:72
/magento-2-3-develop/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php:375
/magento-2-3-develop/generated/code/Magento/Catalog/Model/ResourceModel/Product/Collection/Interceptor.php:14
/magento-2-3-develop/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php:116
/magento-2-3-develop/lib/internal/Magento/Framework/ObjectManager/Factory/Dynamic/Developer.php:66
/magento-2-3-develop/lib/internal/Magento/Framework/ObjectManager/ObjectManager.php:56
/magento-2-3-develop/generated/code/Magento/Catalog/Model/ResourceModel/Product/CollectionFactory.php:43
/magento-2-3-develop/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php:223
/magento-2-3-develop/app/code/Magento/Quote/Model/ResourceModel/Quote/Item/Collection.php:190
/magento-2-3-develop/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php:590
/magento-2-3-develop/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php:561
/magento-2-3-develop/lib/internal/Magento/Framework/Data/Collection.php:834
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1418
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:830
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:210
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:131
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteInfiniteLoopTest.php:113

With the code change the trace still shows a loop, but much shorter and without exhausting resources and much easier to debug.

1) Magento\Quote\Model\QuoteInfiniteLoopTest::testLoadQuoteWithTriggerRecollectInfiniteLoop
LogicException: Infinite loop detected, review the trace for the looping path

/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:223
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/app/code/Magento/TestModuleQuoteTotalsObserver/Observer/AfterCollectTotals.php:43
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:72
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Invoker/InvokerDefault.php:60
/magento-2-3-develop/lib/internal/Magento/Framework/Event/Manager.php:66
/magento-2-3-develop/generated/code/Magento/Framework/Event/Manager/Proxy.php:95
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote/TotalsCollector.php:169
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:1984
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:1064
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:2420
/magento-2-3-develop/app/code/Magento/Quote/Model/Quote.php:901
/magento-2-3-develop/generated/code/Magento/Quote/Model/Quote/Interceptor.php:479
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:215
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:121
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:24
/magento-2-3-develop/app/code/Magento/Quote/Model/QuoteRepository.php:147
/magento-2-3-develop/generated/code/Magento/Quote/Model/QuoteRepository/Interceptor.php:50
/magento-2-3-develop/app/code/Magento/Checkout/Model/Session.php:232
/magento-2-3-develop/generated/code/Magento/Checkout/Model/Session/Interceptor.php:63
/magento-2-3-develop/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteInfiniteLoopTest.php:113

Manual testing scenarios

I do not believe manual testing scenarios are relevant to this change as it requires custom third party code to trigger the loop.

Edit: Added some testing notes in this comment, but will require third party code to be added. #18678 (comment)

Contribution checklist

  • Pull request has a meaningful description of its purpose
  • All commits are accompanied by meaningful commit messages
  • All new or changed code is covered with unit/integration tests (if applicable)
  • All automated tests passed successfully (all builds on Travis CI are green)

Third party code combined with `trigger_recollect` can cause infinite loops in some scenarios. The fix needs to be applied to the third party code, but the Magento core could be more explicit with these kinds of errors.
@magento-cicd2
Copy link
Contributor

magento-cicd2 commented Oct 17, 2018

CLA assistant check
All committers have signed the CLA.

@magento-engcom-team
Copy link
Contributor

Hi @convenient. Thank you for your contribution
Here is some useful tips how you can test your changes using Magento test environment.
Add the comment under your pull request to deploy test or vanilla Magento instance:

  • @magento-engcom-team give me test instance - deploy test instance based on PR changes
  • @magento-engcom-team give me $VERSION instance - deploy vanilla Magento instance

For more details, please, review the Magento Contributor Assistant documentation

@orlangur orlangur self-assigned this Oct 17, 2018
@orlangur
Copy link
Contributor

@convenient please provide clear reproducible steps following best practices.

Injecting session object like described is not a way to go, there is even a dedicated MEQP2 sniff to catch this.

Give a clear error message to the customer, the report page will be thrown with an error code rather than a white page loading forever

Such problems should be found during testing prior to production deployment.

@convenient
Copy link
Contributor Author

Hey @orlangur

I'll rebase any code style issues after our discussion is completed just to save potential wasted effort on my part.

Injecting session object like described is not a way to go, there is even a dedicated MEQP2 sniff to catch this.

Agree on this, it is not the way to go. And that is what the proposed exception is trying to help reinforce. Not all third party vendors run the sniffers and full array of suites available to ensure the code is up to scratch and following best practices. As it goes injecting the session object is a common carry over from M1 and I've seen it in a few M2 sites.

I think it's important to consider that in a pure vanilla M2 instance the proposed changes to app/code/Magento/Checkout/Model/Session.php will have no effect at all, they only become active when inexpert third party code is applied.

This PR is kind of like another layer of developer tooling for a very specific kind of bug.

Such problems should be found during testing prior to production deployment.

The testing for these issues is often missed as it requires the trigger_recollect=1 flag to be set on the quote. The observers and third party code can work perfectly well until an administrator applies a catalog rule, or updates the product while its in the customers quote. This is not a very usual flow to be tested.

please provide clear reproducible steps following best practices.

It is not possible to provide reproducible steps using best practices as this PR is here to help enforce best practices right at the last possible step, on the server itself. To demonstrate this issue I have to provide an example of some naive non-best practice code.

All the CI tools help prevent these kinds of errors but if they're not run or missed we need something on the production box that will help in time of crisis. I believe the system should be robust all the way from the developers machine, through QA, to staging and then production.

Steps to replicate using non-best-practice third party code

  1. Create an observer which hooks into sales_quote_collect_totals_after
class AfterCollectTotals implements ObserverInterface
{
    /**
     * @var \Magento\Checkout\Model\Session
     */
    private $session;

    /**
     * AfterCollectTotals constructor.
     * @param \Magento\Checkout\Model\Session $messageManager
     */
    public function __construct(\Magento\Checkout\Model\Session $messageManager)
    {
        $this->session = $messageManager;
    }

    /**
     * @param Observer $observer
     * @return void
     */
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        /**
         * Best practices say we should use the quote directly from the observer object, but sometimes we're a few levels
         * deep into the collect totals loop, this could be part of a shipping module or something to do with addresses
         * in which case you'd have to go $address->getQuote() etc, however this is not always done properly and
         * the session object can be used by the developer.
         */
        //$quote = $observer->getData('quote');

        /**
         * Get the session object from the quote
         */
        $this->session->getQuote();
    }
}
  1. On the frontend, add a simple product to your cart and go to the basket page. See that it loads.
  2. In the admin panel, navigate to that product and update its price. This will trigger markQuotesRecollect which will set trigger_recollect=1 against the quote.
  3. On the frontend again, refresh your basket page. See the infinite looping error occur
  4. At this point the guest customer will have to clear their cookies and start a new purchase, as the quote will never complete loading for them.
  5. A registered customer is permanently blocked as their quote is associated to their customer account, and the quote will never complete its collect totals loop until the code is fixed. Alternatively they could check out as guest.

Below are two recordings, one with the proposed exception and one without.

Demonstration without code change

This hits the xdebug nesting limit, but would carry on "forever" in production

demonstration showing uncaught infinite loop

Demonstration with code change

A clear stack trace is logged and the customer will see an error report. I think we should have the application handle this situation rather than rely on PHP fatal errors.

demonstration showing detected infinite loop

@orlangur
Copy link
Contributor

@convenient,

Demonstration with code change
A clear stack trace is logged and the customer will see an error report. I think we should have the application handle this situation rather than rely on PHP fatal errors.

This is already handled as intended: fatal error is saved to logs and it must be later investigated.

What is an issue with code debugging in its current state? Event dispatched should be pretty obvious from stack trace and then it's not so hard to find a poorly written observer.

The problem I see with proposed code changed is that there are to many places in Magento where similar fuse would be required if we go this way.

@convenient
Copy link
Contributor Author

Hey there, sorry for the delay in response it's been a busy few weeks.

This is already handled as intended: fatal error is saved to logs and it must be later investigated.

What is an issue with code debugging in its current state? Event dispatched should be pretty obvious from stack trace and then it's not so hard to find a poorly written observer.

A fatal error would be enough to alert people that something is happening, but not enough for them to begin debugging immediately IMO. Every time I've seen this it's been a pain to debug as the problem can present itself in different hard to understand ways depending on the server configuration.

  • I've seen servers with insanely high memory limits and max runtime, which actually caused PHP to segfault when this infinite loop occurred meaning no useful log data was written.
  • I've seen PHP-FPM processes hit their max memory limit, and give garbled output to New Relic like Maximum execution time of 0x3cd seconds exceeded.
  • When a fatal error does occur they don't normally give the full stack trace. I just put my instance into production mode, disabled xdebug and blackfire, and triggered this error. Here is the full log data that was captured, no trace is provided in my pretty standard php and fpm configuration.
[07-Nov-2018 14:03:44 UTC] PHP Fatal error:  Allowed memory size of 792723456 bytes exhausted (tried to allocate 16384 bytes) in /Users/lukerodgers/src/magento-2-3-develop/vendor/magento/zendframework1/library/Zend/Db/Statement/Pdo.php on line 228

Perhaps you know as configuration to enforce the full stack trace? Traces are usually provided for fatal errors as I tested with my same instance and configuration and got the following results. It seems that OOM errors have different behaviour.

$foo = new stdClass();
$foo->bar();
[07-Nov-2018 14:01:08 UTC] PHP Fatal error:  Uncaught Error: Call to undefined method stdClass::bar() in /Users/lukerodgers/src/magento-2-3-develop/pub/index.php:42
Stack trace:
#0 {main}
  thrown in /Users/lukerodgers/src/magento-2-3-develop/pub/index.php on line 42

There's evidence of people experiencing these collect totals and trigger_recollect loops many times over the years in a variety of Magento installations, the dev98 team described it as causing "server outage" which is consistent of things I've seen in the past and which would be mitigated by this PR.

I believe that should this PR be merged that it will help anyone debugging these kinds of issues greatly by stopping the loop on its first iteration (thus freeing the rest of the system resources and helping keep the rest of the site stable) and by providing a proper Magento stack trace and all associated error logging.

The problem I see with proposed code changed is that there are to many places in Magento where similar fuse would be required if we go this way.

What other classes do you believe this would cause a precedent for? As i've highlighted above I've seen and heard a lot about infinite loops in the quotes model, but not much anywhere else.

Let me know your thoughts, thanks.

@convenient convenient force-pushed the throw-infinite-loop-logic-exception branch from 53810d5 to 8be25bf Compare November 16, 2018 15:34
@convenient
Copy link
Contributor Author

Hey I'm not sure what to do to resolve these codacy issues.

  • $this->objectManager = Bootstrap::getObjectManager(); is used by other tests, I cannot see any annotations etc to ignore this alert. What do I do?
  • Please update your custom ruleset. I don't believe these are relevant to my changes?

@orlangur
Copy link
Contributor

@convenient those can be ignored.

And thanks for the great insight by the way, I'm pretty convinced the problem is real now, just trying to understand whether this is a correct place in code for fix.

@convenient
Copy link
Contributor Author

@orlangur brilliant thank you. Let me know when you have any feedback 👍

@kanevbg
Copy link

kanevbg commented Nov 21, 2018

Just to confirm that the problem is very real and very old coming from Magento 1

@convenient
Copy link
Contributor Author

Hey @orlangur how are you?

I was wondering if you'd had any thoughts on this issue?

@convenient
Copy link
Contributor Author

Hey there. Anything needed from me? Rebased or anything?

*
* @var bool
*/
protected $_isLoading = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, avoid underscore in var name and make variable private since it used in the current class only.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sidolov actioned in 746add6.

To me this is inconsistent with the rest of the variables in the class but okay.

@ghost ghost assigned sidolov Aug 15, 2019
@magento-engcom-team
Copy link
Contributor

Hi @sidolov, thank you for the review.
ENGCOM-5688 has been created to process this Pull Request
✳️ @sidolov, could you please add one of the following labels to the Pull Request?

Label Description
Auto-Tests: Covered All changes in Pull Request is covered by auto-tests
Auto-Tests: Not Covered Changes in Pull Request requires coverage by auto-tests
Auto-Tests: Not Required Changes in Pull Request does not require coverage by auto-tests

@magento-engcom-team
Copy link
Contributor

@convenient thank you for contributing. Please accept Community Contributors team invitation here to gain extended permissions for this repository.

@convenient
Copy link
Contributor Author

@sidolov I'm not sure what #18678 (comment) is trying to invite me to, I thought I already had a lot of privs and its just redirecting me to https://github.com/magento

You've approved this but should I address the static analysis fails?

@magento-engcom-team magento-engcom-team merged commit 578c043 into magento:2.3-develop Aug 28, 2019
@m2-assistant
Copy link

m2-assistant bot commented Aug 28, 2019

Hi @convenient, thank you for your contribution!
Please, complete Contribution Survey, it will take less than a minute.
Your feedback will help us to improve contribution process.

@magento-engcom-team magento-engcom-team added this to the Release: 2.3.4 milestone Aug 28, 2019
@convenient convenient deleted the throw-infinite-loop-logic-exception branch August 29, 2019 07:00
@sidolov sidolov added the Auto-Tests: Covered All changes in Pull Request is covered by auto-tests label Sep 14, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Auto-Tests: Covered All changes in Pull Request is covered by auto-tests Component: Checkout Partner: Ampersand partners-contribution Pull Request is created by Magento Partner Progress: accept Release Line: 2.3
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants