Skip to content
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

Fix apq with page caches #1317

Merged
merged 26 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9f52148
Test only
dbosen Dec 6, 2022
1fb1340
fix coding style issues
dbosen Dec 6, 2022
28550e9
Add functional testing
dbosen Dec 7, 2022
9331dd2
Database on filesystem
dbosen Dec 7, 2022
bbd45ef
D10 compatibility
dbosen Dec 7, 2022
2e6e282
Fix for dynammic page cache
dbosen Dec 7, 2022
d5e9485
Fix for page cache
dbosen Dec 7, 2022
c2e9882
Fix coding style
dbosen Dec 7, 2022
11f83f3
Fix more coding styles
dbosen Dec 7, 2022
2531d02
Update to guzzle 7.4 to prevent deprecation notices from guzzle 6.5
dbosen Dec 7, 2022
41fd050
Fix phpstan issues, ignoring 'Call to deprecated method prophesize' m…
dbosen Dec 7, 2022
e5e03d5
Add cache context for extension as well
dbosen Dec 8, 2022
d5a92da
Merge branch '8.x-4.x' into fix-apq-with-page-caches
dbosen Jan 11, 2023
709408c
Merge branch '8.x-4.x' into fix-apq-with-page-caches
pmelab Apr 13, 2023
8573e5d
Merge branch '8.x-4.x' into fix-apq-with-page-caches
dbosen May 30, 2023
bac6749
use example module instead of test module
dbosen May 30, 2023
d581791
remove explicit require of guzzlehttp/guzzle
dbosen May 30, 2023
8d3d8b4
force guzzle 7
dbosen May 30, 2023
20a1b75
Merge branch '8.x-4.x' into fix-apq-with-page-caches
dbosen Nov 9, 2023
c29743e
Merge remote-tracking branch 'origin/8.x-4.x' into fix-apq-with-page-…
dbosen Nov 13, 2023
2b803c1
cleanup
dbosen Nov 13, 2023
00d5f56
Get guzzle back
dbosen Nov 13, 2023
372f4cd
Merge branch '8.x-4.x' into fix-apq-with-page-caches
klausi Mar 21, 2024
a0b7805
New approach without functional tests
dbosen Apr 2, 2024
e3292e5
Fixing it with correctly set context
dbosen Apr 3, 2024
28a6430
Merge branch '8.x-4.x' into fix-apq-with-page-caches
klausi Apr 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/EventSubscriber/ApqSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,21 @@ public function onBeforeOperation(OperationEvent $event): void {
$query = $event->getContext()->getOperation()->query;
$queryHash = $event->getContext()->getOperation()->extensions['persistedQuery']['sha256Hash'] ?? '';

if (is_string($query) && is_string($queryHash) && $queryHash !== '') {
$computedQueryHash = hash('sha256', $query);
if ($queryHash !== $computedQueryHash) {
throw new Error('Provided sha does not match query');
if (is_string($queryHash) && $queryHash !== '') {
// Add cache context for dynamic page cache compatibility on all
// operations that have the hash set.
$event->getContext()->addCacheContexts(
['url.query_args:variables', 'url.query_args:extensions']
);
dbosen marked this conversation as resolved.
Show resolved Hide resolved

// If we have a query and the hash matches then can cache it.
if (is_string($query)) {
$computedQueryHash = hash('sha256', $query);
if ($queryHash !== $computedQueryHash) {
throw new Error('Provided sha does not match query');
Copy link
Contributor

Choose a reason for hiding this comment

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

I think Error is not correct here, should be UserError, right? We want to show this error to the graphql client that they sent something wrong.

Not the fault of this pull request, was already wrong before.

Copy link
Contributor

Choose a reason for hiding this comment

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

Interestingly we are asserting in the tests that the error message is coming out in the response, so it appears to be treated already as user error for some reason ... so we can leave this as is and don't care for now.

}
$this->cache->set($queryHash, $query);
}
$this->cache->set($queryHash, $query);
}
}

Expand Down
24 changes: 22 additions & 2 deletions src/Plugin/GraphQL/PersistedQuery/AutomaticPersistedQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Drupal\graphql\Plugin\GraphQL\PersistedQuery;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\PageCache\ResponsePolicy\KillSwitch;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\graphql\PersistedQuery\PersistedQueryPluginBase;
use GraphQL\Server\OperationParams;
Expand All @@ -27,20 +28,34 @@ class AutomaticPersistedQuery extends PersistedQueryPluginBase implements Contai
*/
protected $cache;

/**
* Page cache kill switch.
*
* @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch
*/
protected $pageCacheKillSwitch;

/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, CacheBackendInterface $cache) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, CacheBackendInterface $cache, KillSwitch $pageCacheKillSwitch) {
parent::__construct($configuration, $plugin_id, $plugin_definition);

$this->cache = $cache;
$this->pageCacheKillSwitch = $pageCacheKillSwitch;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container->get('cache.graphql.apq'));
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('cache.graphql.apq'),
$container->get('page_cache_kill_switch')
);
}

/**
Expand All @@ -50,6 +65,11 @@ public function getQuery($id, OperationParams $operation) {
if ($query = $this->cache->get($id)) {
return $query->data;
}
// Preventing page cache for this request. Otherwise, we would need to add
// a cache tag to the response and flush it when we add the persisted
// query. This is not necessary, because the PersistedQueryNotFound
// response is very short-lived.
$this->pageCacheKillSwitch->trigger();
throw new RequestError('PersistedQueryNotFound');
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?php

namespace Drupal\Tests\graphql\Kernel\Framework;

use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\graphql\Kernel\GraphQLTestBase;
use Symfony\Component\HttpFoundation\Request;

/**
* Tests the automatic persisted query plugin.
*
* @group graphql
*/
class AutomaticPersistedQueriesDynamicPageCacheTest extends GraphQLTestBase {

/**
* {@inheritdoc}
*/
protected static $modules = [
'dynamic_page_cache',
];

/**
* Test plugin.
*
* @var \Drupal\graphql\Plugin\PersistedQueryPluginInterface
*/
protected $pluginApq;

/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->configureCachePolicy();

$schema = <<<GQL
schema {
query: Query
}
type Query {
node(id: String): Node
}

type Node {
title: String!
id: Int!
}
GQL;
$this->setUpSchema($schema);
$this->mockResolver('Query', 'node',
$this->builder->produce('entity_load')
->map('type', $this->builder->fromValue('node'))
->map('id', $this->builder->fromArgument('id'))
);

$this->mockResolver('Node', 'title',
$this->builder->produce('entity_label')
->map('entity', $this->builder->fromParent())
);

$this->mockResolver('Node', 'id',
$this->builder->produce('entity_id')
->map('entity', $this->builder->fromParent())
);

/** @var \Drupal\graphql\Plugin\DataProducerPluginManager $manager */
$manager = $this->container->get('plugin.manager.graphql.persisted_query');

$this->pluginApq = $manager->createInstance('automatic_persisted_query');
}

/**
* Test APQ with dynamic page cache.
*
* Tests that cache context for different variables parameter is correctly
* added to the dynamic page cache entries.
*/
public function testPageCacheWithDifferentVariables(): void {
// Before adding the persisted query plugins to the server, we want to make
// sure that there are no existing plugins already there.
$this->server->removeAllPersistedQueryInstances();
$this->server->addPersistedQueryInstance($this->pluginApq);
$this->server->save();
$endpoint = $this->server->get('endpoint');

NodeType::create([
'type' => 'test',
'name' => 'Test',
])->save();

$node = Node::create([
'nid' => 1,
'title' => 'Node 1',
'type' => 'test',
]);
$node->save();

$node = Node::create([
'nid' => 2,
'title' => 'Node 2',
'type' => 'test',
]);
$node->save();

$titleQuery = 'query($id: String!) { node(id: $id) { title } }';
$idQuery = 'query($id: String!) { node(id: $id) { id } }';

// Post query to endpoint to register the query hashes.
$parameters['extensions']['persistedQuery']['sha256Hash'] = hash('sha256', $titleQuery);
$parameters['variables'] = '{"id": "2"}';
$content = json_encode(['query' => $titleQuery] + $parameters);
$request = Request::create($endpoint, 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], $content);
$result = $this->container->get('http_kernel')->handle($request);
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we have to do all this repetitive request building and cannot use $this->query() from HttpRequestTrait?

Opened #1393 as this was also not correct in the previous test case.

$this->assertSame(200, $result->getStatusCode());
$this->assertSame(['data' => ['node' => ['title' => 'Node 2']]], json_decode($result->getContent(), TRUE));

$parameters['extensions']['persistedQuery']['sha256Hash'] = hash('sha256', $idQuery);
$parameters['variables'] = '{"id": "2"}';
$content = json_encode(['query' => $idQuery] + $parameters);
$request = Request::create($endpoint, 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json'], $content);
$result = $this->container->get('http_kernel')->handle($request);
$this->assertSame(200, $result->getStatusCode());
$this->assertSame(['data' => ['node' => ['id' => 2]]], json_decode($result->getContent(), TRUE));

// Execute apq call.
$parameters['variables'] = '{"id": "1"}';
$request = Request::create($endpoint, 'GET', $parameters);
$result = $this->container->get('http_kernel')->handle($request);
$this->assertSame(200, $result->getStatusCode());
$this->assertSame(['data' => ['node' => ['id' => 1]]], json_decode($result->getContent(), TRUE));

// Execute apq call with different variables.
$parameters['variables'] = '{"id": "2"}';
$request = Request::create($endpoint, 'GET', $parameters);
$result = $this->container->get('http_kernel')->handle($request);
$this->assertSame(200, $result->getStatusCode());
$this->assertSame(['data' => ['node' => ['id' => 2]]], json_decode($result->getContent(), TRUE));

// Execute apq call with same parameters, but different query.
$parameters['extensions']['persistedQuery']['sha256Hash'] = hash('sha256', $titleQuery);
$parameters['variables'] = '{"id": "2"}';
$request = Request::create($endpoint, 'GET', $parameters);
$result = $this->container->get('http_kernel')->handle($request);
$this->assertSame(200, $result->getStatusCode());
$this->assertSame(['data' => ['node' => ['title' => 'Node 2']]], json_decode($result->getContent(), TRUE));

}

}
8 changes: 8 additions & 0 deletions tests/src/Kernel/Framework/AutomaticPersistedQueriesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
*/
class AutomaticPersistedQueriesTest extends GraphQLTestBase {

/**
* {@inheritdoc}
*/
protected static $modules = [
'page_cache',
];

/**
* Test plugin.
*
Expand All @@ -24,6 +31,7 @@ class AutomaticPersistedQueriesTest extends GraphQLTestBase {
*/
protected function setUp(): void {
parent::setUp();
$this->configureCachePolicy();
$schema = <<<GQL
schema {
query: Query
Expand Down