Problem/Motivation

After #2807501: ResourceResponse::$responseData is serialized, but is never used: unserializing it on every Page Cache hit is a huge performance penalty was committed - which hugely improved performance for anonymous responses (thanks to page_cache.module), I realized that I probably should look at the REST module's performance for authenticated responses too (by ensuring it's accelerated properly by dynamic_page_cache.module).

Turns out it is indeed not effectively accelerating things. In fact, it suffers from the same problem that #2807501, but for different reasons.

Proposed resolution

ResourceResponseSubscriber reacts to the KernelEvents::RESPONSE event, with priority 5 (to run before FinishResponseSubscriber). This means it runs after DynamicPageCacheSubscriber::onResponse() (with priority 100) … which means Dynamic Page Cache is serializing the ResourceResponse, where $responseData still contains an entity to unserialize (the response has not yet been "flattened" by \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::flattenResponse()). And hence it's fairly slow: because even in case of a Dynamic Page Cache hit, the entity must be PHP-unserialized and then serialized into the requested format: JSON, HAL+JSON, or something else.

Fortunately the solution is therefore quite simple: further increase the priority of ResourceResponseSubscriber: from 5 to >100 (because DynamicPageCacheSubscriber has priority 100).

We could choose to go a bit further, and not only make ResourceResponseSubscriber run before DynamicPageCacheSubscriber, but also make it lighter: don't inject any dependencies, and instead request them from the container at runtime (so that in case of a Dynamic Page Cache hit, we did not incur the cost of instantiating those services). But #10 has shown that the incremental performance gain of that is minimal (lower than the standard deviation even), and therefore not worth the additional complexity. Besides, that could still be done in a follow-up. This issue is about fixing the glaring performance problem first.

The end result is a ~15% performance boost: from 76 ms/req to 65 ms/req for requesting a serialized JSON article node. The difference will be bigger with more complex content (e.g. many fields, lots of data in the fields). This is measured on a PHP 7.0.12 setup.

Remaining tasks

None.

User interface changes

None.

API changes

None.

Data model changes

None.

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

Title: ResourceResponse(Subscriber) + Dynamic Page Cache » ResourceResponse(Subscriber) + Dynamic Page Cache: making authenticated ResourceResponses significantly faster
wim leers’s picture

Category: Task » Plan
wim leers’s picture

StatusFileSize
new2.57 KB

Attached: the patch that includes the steps towards a solution that I attempted in the IS. For future reference.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wim leers’s picture

Status: Active » Needs review
StatusFileSize
new2.77 KB

This needed a reroll: #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method changed the priority of ResourceResponseSubscriber from 0 to 5, because it must run before FinishResponseSubscriber.

Status: Needs review » Needs work

The last submitted patch, 6: 2827797-6.patch, failed testing.

wim leers’s picture

Assigned: Unassigned » wim leers

Let's get this issue going again.

wim leers’s picture

Issue tags: +needs profiling

So we have 3 states:

  1. Drupal 8.4.x HEAD, in which REST responses are never cached by Dynamic Page Cache.
  2. Solution "step 1" as described in the IS: just change the priority of DynamicPageCacheSubscriber.
  3. Solution "step 1 + step 2" as described in the IS: change the priority of DynamicPageCacheSubscriber and stop injecting its dependencies,
    instead retrieve them at runtime, when we know it's going to be called.

We know we want to move from state 1 to either state 2 or state 3. State 1 is incorrect. States 2 and 3 are both correct.

The sole remaining question is: do we want to go to state 2 or state 3? State 3 has potentially better performance, but comes with ugliness.

We can answer this question by doing the necessary profiling. So, working on that.

wim leers’s picture

Performance of the 3 possible states

  1. Requests per second:    13.12 [#/sec] (mean)
    Time per request:       76.195 [ms] (mean)
    Time per request:       76.195 [ms] (mean, across all concurrent requests)
    Transfer rate:          61.30 [Kbytes/sec] received
    
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0    0   0.0      0       0
    Processing:    73   76   3.1     75      94
    Waiting:       73   76   3.1     75      93
    Total:         73   76   3.1     75      94
    
    Percentage of the requests served within a certain time (ms)
      50%     75
      66%     76
      75%     77
      80%     78
      90%     80
      95%     83
      98%     85
      99%     94
     100%     94 (longest request)
    
  2. Requests per second:    15.43 [#/sec] (mean)
    Time per request:       64.803 [ms] (mean)
    Time per request:       64.803 [ms] (mean, across all concurrent requests)
    Transfer rate:          72.08 [Kbytes/sec] received
    
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0    0   0.0      0       0
    Processing:    61   64   3.1     63      84
    Waiting:       61   64   3.1     63      84
    Total:         62   65   3.1     63      84
    
    Percentage of the requests served within a certain time (ms)
      50%     63
      66%     65
      75%     67
      80%     67
      90%     68
      95%     71
      98%     71
      99%     84
     100%     84 (longest request)
    
  3. Requests per second:    15.58 [#/sec] (mean)
    Time per request:       64.183 [ms] (mean)
    Time per request:       64.183 [ms] (mean, across all concurrent requests)
    Transfer rate:          72.77 [Kbytes/sec] received
    
    Connection Times (ms)
                  min  mean[+/-sd] median   max
    Connect:        0    0   0.0      0       0
    Processing:    61   64   2.6     63      74
    Waiting:       61   64   2.6     63      74
    Total:         62   64   2.6     63      74
    
    Percentage of the requests served within a certain time (ms)
      50%     63
      66%     64
      75%     66
      80%     67
      90%     68
      95%     70
      98%     72
      99%     74
     100%     74 (longest request)
    

Analysis

  1. 13.12 req/s, or 76 ms/req, std dev of 3.1 ms
  2. 15.43 req/s, or 65 ms/req, std dev of 3.1 ms
  3. 15.58 req/s, or 64 ms/req, std dev of 2.6 ms

Going from state 1 to state 2 yields a significant performance boost: 15% faster responses (11 ms less per request). Standard deviation unchanged. A difference of 3.5 standard deviations.

Going from state 2 to state 3 yields a negligible performance boost: 1.5% faster responses (1 ms less per request). Standard deviation slightly lower (probably because less code is executed on cache hit requests). But the performance gain is less than half of the standard deviation of state 2/3.

Conclusion

State 3 is not worth the additional complexity. State 2 yields most of the gains. The reason we don't see bigger gains by going from state 2 to state 3, is that the bootstrapping + routing + access checking represent the vast majority of those 65 ms to generate a response, hence we only observe a 1.5% (1 ms) additional performance gain.

wim leers’s picture

Issue tags: +Needs tests

We need to update the IS to reflect the findings of #10. But first, we need test coverage to prove that in HEAD, Dynamic Page Cache is not accelerating REST responses at all.

IOW: we want a failing test-only patch, and then a passing tests+fix patch. Working on that.

dawehner’s picture

ResourceResponseSubscriber this let me thinking ... isnt' that what \Symfony\Component\HttpKernel\KernelEvents::VIEW is designed to do?

* The VIEW event occurs when the return value of a controller
* is not a Response instance
.

Could this convert the response over?

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.92 KB

Before posting a failing test, here's a test showing that in fact Dynamic Page Cache was already working. Like the issue summary says:

You see, ResourceResponseSubscriber reacts to the KernelEvents::RESPONSE event, with the default priority (zero). This means it runs after DynamicPageCacheSubscriber::onResponse() … which means it is serializing the ResourceResponse, where $responseData still contains an entity to deserialize. And hence it's fairly slow.

i.e. Dynamic Page Cache is caching a ResourceResponse that still contains a \Drupal\node\Entity\Node object. So, we need to read that from cache, turn it into a PHP object (un-PHP-serialize it), and then serialize it down to JSON (or HAL+JSON or whatever the requested format is). What we should be doing, is serialize that Node object to JSON (or HAL+JSON or …) and then let Dynamic Page Cache cache it.

That is what this issue is fixing.

Next: a failing test.

Status: Needs review » Needs work

The last submitted patch, 13: 2827797-12.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new3.46 KB
new3.2 KB

The reason #13 failed for MenuLinkContent and Shortcut is that their canonical URLs are admin routes because their URL path starts with /admin/…. Dynamic Page Cache explicitly avoids caching admin routes.

(We could change that in Dynamic Page Cache for REST routes that happen to start with, but that's out of scope here.)

wim leers’s picture

StatusFileSize
new5.43 KB
new3.88 KB

Or we could let the REST module undo what \Drupal\system\EventSubscriber\AdminRouteSubscriber does, but only for REST routes.

That'd be much more elegant than what #15 does. All of the complexity in #15's interdiff would go away!

And it'd be within scope because we're only touching rest.module.

(Of course, ideally, AdminRouteSubscriber would only be touching _format: html routes! This again shows that Drupal 8 is not really API-first yet: many things are still HTML-centric.)

wim leers’s picture

Issue tags: -Needs tests
StatusFileSize
new2.38 KB
new5.36 KB

Now, let's finally add the failing test coverage, to prove that this functionality has poor performance in HEAD.

wim leers’s picture

StatusFileSize
new1.01 KB
new6.32 KB

And then finally, let's bring the fix ("state 2", as discussed in #10).

#17 should be red, this should be green :)

wim leers’s picture

And now, finally, the IS has been updated.

This is definitely ready for review, and I think/hope an RTBC is within reach :)

The last submitted patch, 15: 2827797-15.patch, failed testing.

The last submitted patch, 17: 2827797-17.patch, failed testing.

tedbow’s picture

+++ b/core/modules/rest/src/EventSubscriber/RestRouteSubscriber.php
@@ -0,0 +1,40 @@
+      if (strpos($name, 'rest.') === 0 && $route->hasOption('_admin_route')) {

Is there any there anything we can check for besides strpos($name, 'rest.') === 0? I guess modules are suppose to use the machine name for module as the start to all their routes?

What if we ever wanted to add an non-resource route to the REST module. Like some sort of config form. What if a custom module did this and didn't follow the rule about machine names?

What about $route->getDefault('_controller') == 'Drupal\rest\RequestHandler::handle'? I wish there was a better way we could tell if a route was REST resource.

Think this is close to RTBC. I TEST_ONLY patch would be good prove the ResourceResponse object is stored currently in the Dynamic Page Cache.

dawehner’s picture

(Of course, ideally, AdminRouteSubscriber would only be touching _format: html routes! This again shows that Drupal 8 is not really API-first yet: many things are still HTML-centric.)

Right, but what is the reason we cannot do that? Can't we check whether format is set, and in case it doesn't support HTML we would not set the option ...

wim leers’s picture

StatusFileSize
new3.81 KB
new6.14 KB

#22: it's best practice that routes prefix their route names by the module name. I've yet to encounter routes that use another module's route name as the prefix, which is the only way this could cause problems for something else. The test-only patch is the patch in #17.

#23: Yes, we could. I'd prefer that, actually. So let's see if we can do that: done, let's see if all tests still pass.

wim leers’s picture

Glad to see it's passing :)

tedbow’s picture

My point in #22 is no longer a concern because #24 removed RestRouteSubscriber.

The patch looks good to me but will let @dawehner RTBC if thinks it is good.

dawehner’s picture

StatusFileSize
new9.53 KB
new3.64 KB
  1. +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
    @@ -196,8 +196,9 @@ protected function flattenResponse(ResourceResponseInterface $response) {
        * {@inheritdoc}
        */
       public static function getSubscribedEvents() {
    -    // Run shortly before \Drupal\Core\EventSubscriber\FinishResponseSubscriber.
    -    $events[KernelEvents::RESPONSE][] = ['onResponse', 5];
    +    // Run before \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber
    +    // (priority 100), so that Dynamic Page Cache can cache flattened responses.
    +    $events[KernelEvents::RESPONSE][] = ['onResponse', 128];
         return $events;
    

    I agree for now just changing the priority is the right thing to do. Moving to the view event would be certainly way more of an effort.

  2. +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
    @@ -395,13 +399,34 @@ public function testGet() {
    +      $this->assertSame(['HIT'], $response->getHeader('X-Drupal-Dynamic-Cache'));
    +      // Assert that Dynamic Page Cache did not store a ResourceResponse object,
    +      // which needs serialization after every cache hit. Instead, it should
    +      // contain a flattened response. Otherwise performance suffers.
    +      // @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::flattenResponse()
    +      $cache_items = $this->container->get('database')
    +        ->query("SELECT cid, data FROM {cache_dynamic_page_cache} WHERE cid LIKE :pattern", [
    +          ':pattern' => '%[route]=rest.%',
    +        ])
    +        ->fetchAllAssoc('cid');
    +      foreach ($cache_items as $cid => $cache_item) {
    +        $cached_data = unserialize($cache_item->data);
    +        if (!isset($cached_data['#cache_redirect'])) {
    +          $cached_response = $cached_data['#response'];
    +          $this->assertNotInstanceOf(ResourceResponseInterface::class, $cached_response);
    +          $this->assertInstanceOf(CacheableResponseInterface::class, $cached_response);
    +        }
    

    Nice test coverage

  3. +++ b/core/modules/system/src/EventSubscriber/AdminRouteSubscriber.php
    @@ -15,14 +16,27 @@ class AdminRouteSubscriber extends RouteSubscriberBase {
        */
       protected function alterRoutes(RouteCollection $collection) {
    -    foreach ($collection->all() as $route) {
    -      if (strpos($route->getPath(), '/admin') === 0 && !$route->hasOption('_admin_route')) {
    +    foreach ($collection->all() as $name => $route) {
    +      if (strpos($route->getPath(), '/admin') === 0 && !$route->hasOption('_admin_route') && static::isHtmlRoute($route)) {
             $route->setOption('_admin_route', TRUE);
           }
         }
       }
     
       /**
    +   * Whether the given route is a HTML route.
    +   *
    +   * @param \Symfony\Component\Routing\Route $route
    +   *   The route to inspect.
    +   *
    +   * @return bool
    +   *   Whether the given route is a HTML route.
    +   */
    +  protected static function isHtmlRoute(Route $route) {
    +    return !$route->hasRequirement('_format') || in_array('html', explode('|', $route->getRequirement('_format')), TRUE);
    +  }
    +
    

    I was about to say: it'd be nice to have test coverage, but you know, here is an updated patch with it.

Status: Needs review » Needs work

The last submitted patch, 27: 2827797-27.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new9.63 KB
new1.39 KB

It works on my system :P

wim leers’s picture

#27:

  1. Good :)
  2. Thanks!
  3. Woot, thank you so much! <3 That looks great!

Sadly, PHPLint is failing… because PHP 5. Your code works fine in PHP 7. Easy to fix. EDIT: dammit, you already did that in #29 :)

I think @tedbow can now RTBC this one :)

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

New test coverage for AdminRouteSubscriber looks good.

Woot! Thanks @Wim Leers and @dawehner for your work on this! RTBC

wim leers’s picture

Category: Plan » Task
effulgentsia’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/system/src/EventSubscriber/AdminRouteSubscriber.php
@@ -15,14 +16,27 @@ class AdminRouteSubscriber extends RouteSubscriberBase {
-      if (strpos($route->getPath(), '/admin') === 0 && !$route->hasOption('_admin_route')) {
+      if (strpos($route->getPath(), '/admin') === 0 && !$route->hasOption('_admin_route') && static::isHtmlRoute($route)) {

Can this be moved to a separate issue (even if it means postponing this one on that). I'd like it to have a dedicated commit in the commit history and a dedicated change record. Because this will introduce some (potentially desirable) side effects. For example, it will restrict AdminPathConfigEntityConverter and LanguageNegotiationUserAdmin to only operate on HTML routes. As well as similar code in contrib. It's not entirely obvious to me whether for those 2 examples if that's a desirable change or not: is the concept of "administration" and changing config overrides behavior and/or language negotiation based on whether you're "administering" a strictly HTML concept?

effulgentsia’s picture

is the concept of "administration" and changing config overrides behavior and/or language negotiation based on whether you're "administering" a strictly HTML concept?

If the concept of non-HTML "administration" is still something that's meaningful to preserve, then the CR for the hunk in #34 could just indicate that the behavior of automatically registering routes with /admin/* paths as administrative routes is limited to only HTML routes. And that if someone wants a non-HTML route to be administrative, then they need to explicitly add the _admin_route option to the route themselves, regardless of that route's path. However, that kind of begs the question of why we want to make /admin/* paths have different defaults based on the format.

The reason #13 failed for MenuLinkContent and Shortcut is that their canonical URLs are admin routes because their URL path starts with /admin/…

Maybe that's a bug with those 2 entity classes? Does it make any sense that their HTML routes are administrative? Furthermore, the MenuLinkContent one ends in /edit, which is maybe also a bug?

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new6.32 KB

For example, it will restrict AdminPathConfigEntityConverter and LanguageNegotiationUserAdmin to only operate on HTML routes.

That's … a great point!

So let's go back to the patch in #18 then. I don't think we should block this on deciding how to change the semantics of the _admin_route route option, because clearly it has unintended consequences that need further investigation.

dawehner’s picture

+++ b/core/modules/rest/src/EventSubscriber/RestRouteSubscriber.php
@@ -0,0 +1,40 @@
+    foreach ($collection->all() as $name => $route) {
+      // Removes the '_admin_route' route option from each REST route that had
+      // it added by \Drupal\system\EventSubscriber\AdminRouteSubscriber.
+      if (strpos($name, 'rest.') === 0 && $route->hasOption('_admin_route')) {
+        $route->setOption('_admin_route', FALSE);
+      }
+    }

This documentation just tells what is happening, but it doesn't tell why this is done.

wim leers’s picture

Issue tags: +Baltimore2017, +Needs tests
StatusFileSize
new4.05 KB
new6.38 KB

Discussed this with Daniel at DrupalCon Baltimore — we both independently realized that #36 (i.e. the approach in #18) does not actually address the concerns raised by @effulgentsia in #34 + #35.

Because the end result is still that AdminPathConfigEntityConverter and LanguageNegotiationUserAdmin will only operate on HTML routes.

So, we realized there's a simpler solution: update \Drupal\dynamic_page_cache\PageCache\ResponsePolicy\DenyAdminRoutes to only ignore HTML admin routes. Then we avoid all of these negative consequences.

(We still should make the concept of "admin routes" sane: it's currently too broadly defined, and hence causes consequences like this one. But solving that is a massively complicated "unravel lots of things" task which this issue should not be blocked on.)

Therefore I present this simpler approach :) (Which still needs tests.)

wim leers’s picture

Issue tags: -Needs tests
StatusFileSize
new5.13 KB
new11.46 KB

And tests. (The test-only/FAIL patch is also the interdiff.)

wim leers’s picture

Priority: Normal » Major

And I think this issue definitely qualifies as major to ensure we don't regress in this (i.e. don't break BC).

The last submitted patch, 39: 2827797-39-tests-only-FAIL.patch, failed testing.

effulgentsia’s picture

+++ b/core/modules/dynamic_page_cache/src/PageCache/ResponsePolicy/DenyAdminRoutes.php
@@ -6,9 +6,10 @@
- * Cache policy for routes with the '_admin_route' option set.
+ * Cache policy for HTML routes with the '_admin_route' option set.
  *
  * This policy rule denies caching of responses generated for admin routes,
  * because admin routes have very low cache hit ratios due to low traffic and

If the reason this policy rule exists is for the reason stated in the above PHPDoc, then I don't see why it makes sense to couple it to the HTML format. If we don't want to cache low hit ratio HTML responses, then why do we want to cache low hit ratio non-HTML responses? And if a particular resource has a low hit ratio in HTML, then why would it have a high hit ratio in non-HTML? Maybe there are logical answers to that, but if so, they need to be documented.

However, I don't think the problem is with this policy, but rather with the entity types that define incorrect canonical link templates. Therefore, I opened #2873529: BlockContent, MenuLinkContent, and Shortcut define incorrect canonical links. If we think that analysis is correct, but don't want to hold up this issue on resolving that one, then perhaps it makes sense to meanwhile write this patch's tests to not expect caching for the entity types with canonical resources at admin routes?

wim leers’s picture

then why do we want to cache low hit ratio non-HTML responses

Who says these are low cache hit ratio? For example Shortcut (/admin/config/user-interface/shortcut/link/{shortcut}) and MenuLinkContent (/admin/structure/menu/item/{menu_link_content}/edit) are content entities with an /admin path prefix that clearly are frequently used.

The PHPDoc you quoted is specifically for the admin area of a Drupal site, i.e. where site builders go to configure things. Clearly, retrieving entities via REST is something different entirely. We just have to adjust DenyAdminRoutes to do what it actually intended to do, and the reason that it wasn't already doing that, is exactly that _admin_route's semantics are poorly defined.

However, I don't think the problem is with this policy, but rather with the entity types that define incorrect canonical link templates. Therefore, I opened #2873529: BlockContent, MenuLinkContent, and Shortcut define incorrect canonical links. If we think that analysis is correct, but don't want to hold up this issue on resolving that one, then perhaps it makes sense to meanwhile write this patch's tests to not expect caching for the entity types with canonical resources at admin routes?

I see what you are saying, and that would be nice, but … then DenyAdminRoutes would still be insufficiently precise/strict (again, due to _admin_route's vagueness), and therefore any contrib or custom entity types whose routes use an /admin path prefix would suffer from the same problems.

Therefore I think we should indeed have the discussion that you started in #2873529: BlockContent, MenuLinkContent, and Shortcut define incorrect canonical links, but I also think we should do what's in #39.

effulgentsia’s picture

The PHPDoc you quoted is specifically for the admin area of a Drupal site, i.e. where site builders go to configure things. Clearly, retrieving entities via REST is something different entirely.

I agree that REST GET requests for entities don't make sense to treat as administrative routes. A way to express that would be to change EntityResource::getBaseRoute() to add the '_admin_route' => FALSE route option. I don't know whether that should be just for GET methods or if it should be added to all methods. I also don't know if it should be broadened to all REST resources by default, not just entities. If so, then that could be moved to ResourceBase::getBaseRoute(). I suggest opening a separate issue to figure out the scope we want for that.

I also think that if we want to change DenyAdminRoutes to be scoped to just HTML routes, we should do it in a separate issue. But I'm not convinced of the logic for doing so, other than to work around the bug that some routes are incorrectly marked as administrative.

I still think that the scope of this issue should be to change our tests to not expect caching for the problematic entity types. And then change those tests again when one of the other issues is correspondingly fixed.

dawehner’s picture

I still think that the scope of this issue should be to change our tests to not expect caching for the problematic entity types. And then change those tests again when one of the other issues is correspondingly fixed.

I think I agree, mostly because caching of those is less important than most of content entities, which are on not admin routes. So even if we don't support caching for those, we still have a major benefit.

wim leers’s picture

Status: Needs review » Reviewed & tested by the community
Related issues: +#2874938: AdminRouteSubscriber must only mark HTML routes as administrative
StatusFileSize
new12.41 KB
new7.18 KB
new5.33 KB
new5.48 KB

Okay then…

I agree that REST GET requests for entities don't make sense to treat as administrative routes. A way to express that would be to change EntityResource::getBaseRoute() to add the '_admin_route' => FALSE route option. I don't know whether that should be just for GET methods or if it should be added to all methods. I also don't know if it should be broadened to all REST resources by default, not just entities. If so, then that could be moved to ResourceBase::getBaseRoute(). I suggest opening a separate issue to figure out the scope we want for that.

Done: #2874938: AdminRouteSubscriber must only mark HTML routes as administrative. I posted my concerns about this in that issue.

But I'm not convinced of the logic for doing so, other than to work around the bug that some routes are incorrectly marked as administrative.

Well… that's exactly the sort of thing that request & response policies exist for. But anyway, clearly that's not going to be accepted.


So, this brings back much of what #15's interdiff was doing. Which is to say: very complex work-arounds for what is otherwise trivially simple. At least this means that once we fix the underlying problems, the test coverage becomes simple once again.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 46: 2827797-46.patch, failed testing.

wim leers’s picture

Status: Needs work » Reviewed & tested by the community

Passes fine locally.

And sure enough:

00:21:46.509 Segmentation fault (core dumped)
00:21:46.541 FATAL Drupal\node\Tests\Views\FrontPageTest: test runner returned a non-zero error code (139).
00:21:46.543 Drupal\node\Tests\Views\FrontPageTest                          0 passes   1 fails                            

PHP segfault; most likely random.

effulgentsia’s picture

I opened #2877528: Change Dynamic Page Cache's response policy from "deny admin routes" to "deny html admin routes", and changed the @todos in this patch to reference that issue in addition to #2874938: AdminRouteSubscriber must only mark HTML routes as administrative.

As a side-note, #2877136: dynamic_page_cache.module has an incorrect and misleading @file doc tripped me up while digging into all that.

As this is only a comment change, leaving at RTBC. I'll commit this tomorrow unless objections are raised before then.

effulgentsia’s picture

Ticking the credit box for @tedbow for his reviews.

effulgentsia’s picture

+++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -196,8 +196,9 @@ protected function flattenResponse(ResourceResponseInterface $response) {
-    // Run shortly before \Drupal\Core\EventSubscriber\FinishResponseSubscriber.
-    $events[KernelEvents::RESPONSE][] = ['onResponse', 5];
+    // Run before \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber
+    // (priority 100), so that Dynamic Page Cache can cache flattened responses.
+    $events[KernelEvents::RESPONSE][] = ['onResponse', 128];

Do we need a CR for this? Normally, I don't think changing the priority of a subscriber automatically requires a CR, but I'm curious if any of you know of a reason to provide one in this case.

  • effulgentsia committed 39803eb on 8.4.x
    Issue #2827797 by Wim Leers, dawehner, effulgentsia, tedbow:...
effulgentsia’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: +8.4.0 release notes
effulgentsia’s picture

+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -395,13 +409,44 @@ public function testGet() {
+        // Assert that Dynamic Page Cache did not store a ResourceResponse object,
+        // which needs serialization after every cache hit. Instead, it should
+        // contain a flattened response. Otherwise performance suffers.
+        // @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber::flattenResponse()
+        $cache_items = $this->container->get('database')
+          ->query("SELECT cid, data FROM {cache_dynamic_page_cache} WHERE cid LIKE :pattern", [
+            ':pattern' => '%[route]=rest.%',
+          ])
+          ->fetchAllAssoc('cid');
+        foreach ($cache_items as $cid => $cache_item) {

This test seems a little brittle. If the cid pattern changes in the future, this test will return an empty array and the foreach will get silently skipped, so the test will still pass. Perhaps there should be a followup to improve on this? Do we really have a situation where we don't know the exact $cid to check for?

wim leers’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

wim leers’s picture

Issue tags: +API-First Initiative