From 1bdb6ed2a2caaaf42ed182c10175339b8e856178 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Sat, 21 Oct 2023 08:11:41 +0200 Subject: [PATCH] Issue #3394062 by ReINFaTe: Fiber loops in Renderer and BigPipe are never suspended --- core/lib/Drupal/Core/Render/Renderer.php | 2 +- core/modules/big_pipe/src/Render/BigPipe.php | 2 +- .../src/Unit/Render/FiberPlaceholderTest.php | 127 ++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 core/modules/big_pipe/tests/src/Unit/Render/FiberPlaceholderTest.php diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php index fec1cf215c5a..afd993bee701 100644 --- a/core/lib/Drupal/Core/Render/Renderer.php +++ b/core/lib/Drupal/Core/Render/Renderer.php @@ -708,8 +708,8 @@ protected function replacePlaceholders(array &$elements) { }); } } + $iterations = 0; while (count($fibers) > 0) { - $iterations = 0; foreach ($fibers as $placeholder => $fiber) { if (!$fiber->isStarted()) { $fiber->start(); diff --git a/core/modules/big_pipe/src/Render/BigPipe.php b/core/modules/big_pipe/src/Render/BigPipe.php index eb4b73109678..4f479d663adb 100644 --- a/core/modules/big_pipe/src/Render/BigPipe.php +++ b/core/modules/big_pipe/src/Render/BigPipe.php @@ -553,8 +553,8 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde } $fibers[$placeholder_id] = new \Fiber(fn() => $this->renderPlaceholder($placeholder_id, $placeholder_render_array)); } + $iterations = 0; while (count($fibers) > 0) { - $iterations = 0; foreach ($fibers as $placeholder_id => $fiber) { // Keep skipping the messages placeholder until it's the only Fiber // remaining. @todo https://www.drupal.org/project/drupal/issues/3379885 diff --git a/core/modules/big_pipe/tests/src/Unit/Render/FiberPlaceholderTest.php b/core/modules/big_pipe/tests/src/Unit/Render/FiberPlaceholderTest.php new file mode 100644 index 000000000000..dd1f0c768832 --- /dev/null +++ b/core/modules/big_pipe/tests/src/Unit/Render/FiberPlaceholderTest.php @@ -0,0 +1,127 @@ +<?php + +namespace Drupal\Tests\big_pipe\Unit\Render; + +use Drupal\big_pipe\Render\BigPipe; +use Drupal\big_pipe\Render\BigPipeResponse; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Controller\ControllerResolverInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; +use Drupal\Core\Render\HtmlResponse; +use Drupal\Core\Render\PlaceholderGeneratorInterface; +use Drupal\Core\Render\RenderCacheInterface; +use Drupal\Core\Render\Renderer; +use Drupal\Core\Security\TrustedCallbackInterface; +use Drupal\Core\Theme\ThemeManagerInterface; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +/** + * @coversDefaultClass \Drupal\big_pipe\Render\BigPipe + * @group big_pipe + */ +class FiberPlaceholderTest extends UnitTestCase { + + /** + * @covers \Drupal\big_pipe\Render\BigPipe::sendPlaceholders + */ + public function testLongPlaceholderFiberSuspendingLoop() { + $request_stack = $this->prophesize(RequestStack::class); + $request_stack->getMainRequest() + ->willReturn(new Request()); + $request_stack->getCurrentRequest() + ->willReturn(new Request()); + + $renderer = new Renderer( + $this->prophesize(ControllerResolverInterface::class)->reveal(), + $this->prophesize(ThemeManagerInterface::class)->reveal(), + $this->prophesize(ElementInfoManagerInterface::class)->reveal(), + $this->prophesize(PlaceholderGeneratorInterface::class)->reveal(), + $this->prophesize(RenderCacheInterface::class)->reveal(), + $request_stack->reveal(), + [ + 'required_cache_contexts' => [ + 'languages:language_interface', + 'theme', + ], + ] + ); + + $session = $this->prophesize(SessionInterface::class); + $session->start()->willReturn(TRUE); + + $bigpipe = new BigPipe( + $renderer, + $session->reveal(), + $request_stack->reveal(), + $this->prophesize(HttpKernelInterface::class)->reveal(), + $this->createMock(EventDispatcherInterface::class), + $this->prophesize(ConfigFactoryInterface::class)->reveal(), + ); + $response = new BigPipeResponse(new HtmlResponse()); + + $attachments = [ + 'library' => [], + 'drupalSettings' => [ + 'ajaxPageState' => [], + ], + 'big_pipe_placeholders' => [ + 'callback=%5CDrupal%5CTests%5Cbig_pipe%5CUnit%5CRender%5CTurtleLazyBuilder%3A%3Aturtle&&token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU' => [ + '#lazy_builder' => [ + '\Drupal\Tests\big_pipe\Unit\Render\TurtleLazyBuilder::turtle', + [], + ], + ], + ], + ]; + $response->setAttachments($attachments); + + // Construct minimal HTML response. + $content = '<html><body><span data-big-pipe-placeholder-id="callback=%5CDrupal%5CTests%5Cbig_pipe%5CUnit%5CRender%5CTurtleLazyBuilder%3A%3Aturtle&&token=uhKFNfT4eF449_W-kDQX8E5z4yHyt0-nSHUlwaGAQeU"></body></html>'; + $response->setContent($content); + + // Capture the result to avoid PHPUnit complaining. + ob_start(); + $fiber = new \Fiber(function () use ($bigpipe, $response) { + $bigpipe->sendContent($response); + }); + $fiber->start(); + $this->assertFalse($fiber->isTerminated(), 'Placeholder fibers with long execution time supposed to return control before terminating'); + ob_get_clean(); + } + +} + +class TurtleLazyBuilder implements TrustedCallbackInterface { + + /** + * #lazy_builder callback. + * + * Suspends its own execution twice to simulate long operation. + * + * @return array + */ + public static function turtle(): array { + if (\Fiber::getCurrent() !== NULL) { + \Fiber::suspend(); + } + if (\Fiber::getCurrent() !== NULL) { + \Fiber::suspend(); + } + return [ + '#markup' => '<span>Turtle is finally here. But how?</span>', + ]; + } + + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['turtle']; + } + +} -- GitLab