Skip to content

Commit

Permalink
Cache query validation results
Browse files Browse the repository at this point in the history
  • Loading branch information
k0ka authored Sep 20, 2024
1 parent 82d0272 commit bebb209
Show file tree
Hide file tree
Showing 20 changed files with 650 additions and 46 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ You can find and compare releases at the [GitHub release page](https://github.co

## Unreleased

### Added

- Cache query validation results https://github.com/nuwave/lighthouse/pull/2603

## v6.44.2

### Fixed
Expand Down
94 changes: 94 additions & 0 deletions benchmarks/HugeRequestBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php declare(strict_types=1);

namespace Benchmarks;

final class HugeRequestBench extends QueryBench
{
protected string $schema = /** @lang GraphQL */ <<<'GRAPHQL'
type Query {
foo: String!
@field(resolver: "Benchmarks\\HugeRequestBench@resolve")
}
GRAPHQL;

protected ?string $query = null;

/**
* Resolves foo.
*
* @skip
*/
public function resolve(): string
{
return 'foo';
}

/** Generates query with $count fragments. */
private function generateQuery(int $count): string
{
$query = '{';
for ($i = 0; $i < $count; ++$i) {
$query .= '...foo' . $i . PHP_EOL;
}

$query .= '}' . PHP_EOL;
for ($i = 0; $i < $count; ++$i) {
$query .= 'fragment foo' . $i . ' on Query {' . PHP_EOL;
$query .= 'foo' . PHP_EOL;
$query .= '}' . PHP_EOL;
}

return $query;
}

/**
* @Warmup(1)
*
* @Revs(10)
*
* @Iterations(10)
*
* @ParamProviders({"providePerformanceTuning"})
*
* @BeforeMethods("setPerformanceTuning")
*/
public function benchmark1(): void
{
$this->query ??= $this->generateQuery(1);
$this->graphQL($this->query);
}

/**
* @Warmup(1)
*
* @Revs(10)
*
* @Iterations(10)
*
* @ParamProviders({"providePerformanceTuning"})
*
* @BeforeMethods("setPerformanceTuning")
*/
public function benchmark10(): void
{
$this->query ??= $this->generateQuery(10);
$this->graphQL($this->query);
}

/**
* @Warmup(1)
*
* @Revs(10)
*
* @Iterations(10)
*
* @ParamProviders({"providePerformanceTuning"})
*
* @BeforeMethods("setPerformanceTuning")
*/
public function benchmark100(): void
{
$this->query ??= $this->generateQuery(100);
$this->graphQL($this->query);
}
}
24 changes: 21 additions & 3 deletions benchmarks/HugeResponseBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ public function resolve(): array
}

/**
* @Warmup(1)
*
* @Revs(10)
*
* @Iterations(10)
*
* @OutputTimeUnit("seconds", precision=3)
* @ParamProviders({"providePerformanceTuning"})
*
* @BeforeMethods("setPerformanceTuning")
*/
public function benchmark1(): void
{
Expand All @@ -66,9 +72,15 @@ public function benchmark1(): void
}

/**
* @Warmup(1)
*
* @Revs(10)
*
* @Iterations(10)
*
* @OutputTimeUnit("seconds", precision=3)
* @ParamProviders({"providePerformanceTuning"})
*
* @BeforeMethods("setPerformanceTuning")
*/
public function benchmark100(): void
{
Expand All @@ -84,9 +96,15 @@ public function benchmark100(): void
}

/**
* @Warmup(1)
*
* @Revs(10)
*
* @Iterations(10)
*
* @OutputTimeUnit("seconds", precision=3)
* @ParamProviders({"providePerformanceTuning"})
*
* @BeforeMethods("setPerformanceTuning")
*/
public function benchmark10k(): void
{
Expand Down
42 changes: 35 additions & 7 deletions benchmarks/QueryBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ public function setUp(): void
{
parent::setUp();

$routeName = config('lighthouse.route.name');
$configRepository = $this->app->make(ConfigRepository::class);
assert($configRepository instanceof ConfigRepository);

$routeName = $configRepository->get('lighthouse.route.name');
$this->graphQLEndpoint = route($routeName);
}

Expand All @@ -35,15 +38,40 @@ protected function graphQLEndpointUrl(array $routeParams = []): string
}

/**
* Define environment setup.
* Set up function with the performance tuning.
*
* @param \Illuminate\Foundation\Application $app
* @param array{0: bool, 1: bool, 2: bool} $params Performance tuning parameters
*/
protected function getEnvironmentSetUp($app): void
public function setPerformanceTuning(array $params): void
{
parent::getEnvironmentSetUp($app);
$this->setUp();

$configRepository = $this->app->make(ConfigRepository::class);
assert($configRepository instanceof ConfigRepository);

if ($params[0]) {
$configRepository->set('lighthouse.field_middleware', []);
}

$configRepository->set('lighthouse.query_cache.enable', $params[1]);
$configRepository->set('lighthouse.validation_cache.enable', $params[2]);
}

$config = $app->make(ConfigRepository::class);
$config->set('lighthouse.field_middleware', []);
/**
* Indexes:
* 0: Remove all middlewares
* 1: Enable query cache
* 2: Enable validation cache
*
* @return array<string, array{0: bool, 1: bool, 2: bool}>
*/
public function providePerformanceTuning(): array
{
return [
'nothing' => [false, false, false],
'query cache' => [false, true, false],
'query + validation cache' => [false, true, true],
'everything' => [true, true, true],
];
}
}
8 changes: 8 additions & 0 deletions docs/6/performance/query-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ Lighthouse supports Automatic Persisted Queries (APQ), compatible with the

APQ is enabled by default, but depends on query caching being enabled.

## Query validation caching

Lighthouse can cache the result of the query validation process as well. It only caches queries without errors.
`QueryComplexity` validation can not be cached as it is dependent on the query, so it is always executed.

Query validation caching is disabled by default. You can enable it by setting `validation_cache.enable` to `true` in the
configuration in `config/lighthouse.php`.

## Testing caveats

If you are mocking Laravel cache classes like `\Illuminate\Support\Facades\Cache` or `\Illuminate\Cache\Repository` and asserting expectations in your unit tests, it might be best to disable the query cache in your `phpunit.xml`:
Expand Down
6 changes: 3 additions & 3 deletions docs/6/security/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,12 @@ By default, Lighthouse enables all default query validation rules from `webonyx/
This covers fundamental checks, e.g. queried fields match the schema, variables have values of the correct type.

If you want to add custom rules or change which ones are used, you can bind a custom implementation
of the interface `\Nuwave\Lighthouse\Support\Contracts\ProvidesValidationRules` through a service provider.
of the interface `\Nuwave\Lighthouse\Support\Contracts\ProvidesCacheableValidationRules` through a service provider.

```php
use Nuwave\Lighthouse\Support\Contracts\ProvidesValidationRules;

final class MyCustomRulesProvider implements ProvidesValidationRules {}
final class MyCustomRulesProvider implements ProvidesCacheableValidationRules {}

$this->app->bind(ProvidesValidationRules::class, MyCustomRulesProvider::class);
$this->app->bind(ProvidesCacheableValidationRules::class, MyCustomRulesProvider::class);
```
9 changes: 9 additions & 0 deletions docs/master/performance/query-caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ Lighthouse supports Automatic Persisted Queries (APQ), compatible with the

APQ is enabled by default, but depends on query caching being enabled.

## Query validation caching

Lighthouse can cache the result of the query validation process as well.
It only caches queries without errors.
`QueryComplexity` validation can not be cached as it is dependent on the query, so it is always executed.

Query validation caching is disabled by default.
You can enable it by setting `validation_cache.enable` to `true` in `config/lighthouse.php`.

## Testing caveats

If you are mocking Laravel cache classes like `\Illuminate\Support\Facades\Cache` or `\Illuminate\Cache\Repository` and asserting expectations in your unit tests, it might be best to disable the query cache in your `phpunit.xml`:
Expand Down
6 changes: 3 additions & 3 deletions docs/master/security/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,12 @@ By default, Lighthouse enables all default query validation rules from `webonyx/
This covers fundamental checks, e.g. queried fields match the schema, variables have values of the correct type.

If you want to add custom rules or change which ones are used, you can bind a custom implementation
of the interface `\Nuwave\Lighthouse\Support\Contracts\ProvidesValidationRules` through a service provider.
of the interface `\Nuwave\Lighthouse\Support\Contracts\ProvidesCacheableValidationRules` through a service provider.

```php
use Nuwave\Lighthouse\Support\Contracts\ProvidesValidationRules;

final class MyCustomRulesProvider implements ProvidesValidationRules {}
final class MyCustomRulesProvider implements ProvidesCacheableValidationRules {}

$this->app->bind(ProvidesValidationRules::class, MyCustomRulesProvider::class);
$this->app->bind(ProvidesCacheableValidationRules::class, MyCustomRulesProvider::class);
```
40 changes: 40 additions & 0 deletions src/Execution/CacheableValidationRulesProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php declare(strict_types=1);

namespace Nuwave\Lighthouse\Execution;

use GraphQL\Validator\DocumentValidator;
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Nuwave\Lighthouse\Support\Contracts\ProvidesCacheableValidationRules;

class CacheableValidationRulesProvider implements ProvidesCacheableValidationRules
{
public function __construct(
protected ConfigRepository $configRepository,
) {}

public function cacheableValidationRules(): array
{
$result = [
QueryDepth::class => new QueryDepth($this->configRepository->get('lighthouse.security.max_query_depth', 0)),
DisableIntrospection::class => new DisableIntrospection($this->configRepository->get('lighthouse.security.disable_introspection', 0)),
] + DocumentValidator::allRules();

unset($result[QueryComplexity::class]);

return $result;
}

public function validationRules(): ?array
{
$maxQueryComplexity = $this->configRepository->get('lighthouse.security.max_query_complexity', 0);

return $maxQueryComplexity === 0
? []
: [
QueryComplexity::class => new QueryComplexity($maxQueryComplexity),
];
}
}
Loading

0 comments on commit bebb209

Please sign in to comment.