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

Feature: cache validation rules #2603

Merged
merged 25 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cfa9b82
split validation rules into two parts: cacheable and non-cacheable
k0ka Aug 22, 2024
09ecb9e
save schema hash in cache and expose it via SchemaBuilder
k0ka Aug 22, 2024
fe1a856
query hashing and tests
k0ka Aug 22, 2024
b7c6347
add disabled test
k0ka Aug 22, 2024
ec9f70f
Huge Request benchmark
k0ka Aug 22, 2024
0d8ea16
Apply php-cs-fixer changes
k0ka Aug 22, 2024
e5c3fcb
fix phpstan errors
k0ka Aug 22, 2024
fe03ff0
documentation
k0ka Aug 22, 2024
b8451f4
changelog, fix benchmark a little
k0ka Aug 22, 2024
b3b42b0
add consistent docs to master and 6th version
k0ka Aug 22, 2024
c9a0453
fix tests for enabled validation cache
k0ka Aug 22, 2024
dca7a2e
Merge branch 'master' into feat-cache-validation-rules
k0ka Aug 26, 2024
ee4f3e2
Update CHANGELOG.md
k0ka Aug 26, 2024
6f69bde
Apply suggestions from code review
k0ka Sep 19, 2024
2f5ddcd
Apply php-cs-fixer changes
k0ka Sep 19, 2024
9ba4441
use Event::fake object assertions instead of static functions
k0ka Sep 19, 2024
b7ee225
rename `executeAndCacheValidationRules` to `validateCacheableRules`
k0ka Sep 19, 2024
c8eb73d
Merge branch 'master' into feat-cache-validation-rules
k0ka Sep 19, 2024
289f80a
make `DocumentAST::$hash` not nullable
k0ka Sep 20, 2024
9b50d11
assert actual hash value in `DocumentASTTest::testParsesSimpleSchema`
k0ka Sep 20, 2024
bd8185a
Update src/Support/Contracts/ProvidesCacheableValidationRules.php
k0ka Sep 20, 2024
7647571
Apply php-cs-fixer changes
k0ka Sep 20, 2024
c7857a7
fix phpstan
k0ka Sep 20, 2024
7df6e21
clean up
spawnia Sep 20, 2024
baaa1a2
explain why errors are not cached
spawnia Sep 20, 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
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
Loading