Skip to content

Commit 9ec288f

Browse files
authored
Reuse reentrantly-registered type in mapAnnotatedObject (fixes first-request crash in long-lived workers) (#812)
* fix: reuse a reentrantly-registered type in TypeGenerator::mapAnnotatedObject In long-lived workers (Swoole/RoadRunner/FrankenPHP) the first request after a fresh worker could fail with "Cached type in registry is not the type returned by type mapper."; subsequent requests succeeded. Root cause: mapAnnotatedObject() checks the registry BEFORE instantiating the annotated object via the container, but never after. Instantiating it can reentrantly resolve and register this same type (a dependency in the container graph references it). The outer call then builds a duplicate instance and returns it, which trips RecursiveTypeMapper's registry identity check. (mapFactoryMethod() already guards its own cache after its container->get(); mapAnnotatedObject() did not.) Re-check the registry after container->get() and reuse the registered instance. RecursiveTypeMapper's identity check is intentionally left intact — it now verifies the fix rather than crashing on the benign duplicate. Refs #531 * docs: add long-lived-worker concurrency notes + cut 8.3.0 docs version - troubleshooting: document the cold-build type-registry race ("Cached type in registry..." / duplicate named types) and the warm-or-lock fix for Swoole/RoadRunner/FrankenPHP workers - internals: cross-reference the RecursiveTypeMapper identity invariant - query-plan: fix unbalanced parens in the getFieldSelection() example - generate the previously-missing 8.3.0 versioned docs snapshot
1 parent 377a3bc commit 9ec288f

48 files changed

Lines changed: 5956 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/TypeGenerator.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ public function mapAnnotatedObject(string $annotatedObjectClassName): MutableInt
125125
$type->description = $resolvedDescription;
126126
}
127127

128+
// Instantiating the annotated object via the container (above) can reentrantly resolve and
129+
// register this very type — e.g. a dependency in the container graph references it. If so,
130+
// reuse that instance rather than return the duplicate we just built, which would later trip
131+
// the registry identity check in RecursiveTypeMapper (the first-request crash in long-lived
132+
// workers). mapFactoryMethod() guards its own cache the same way after its container->get.
133+
// See https://github.com/thecodingmachine/graphqlite/issues/531
134+
if ($this->typeRegistry->hasType($type->name)) {
135+
return $this->typeRegistry->getMutableInterface($type->name);
136+
}
137+
128138
return $type;
129139
}
130140

tests/TypeGeneratorTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,36 @@ public function testextendAnnotatedObjectException(): void
5151
$this->expectException(MissingAnnotationException::class);
5252
$typeGenerator->extendAnnotatedObject(new stdClass(), $type);
5353
}
54+
55+
public function testMapAnnotatedObjectReusesTypeRegisteredReentrantlyDuringContainerGet(): void
56+
{
57+
// Reproduces the cold-registry race behind #531: instantiating the annotated object via the
58+
// container reentrantly resolves (and registers) this same type. mapAnnotatedObject must
59+
// reuse that instance rather than build a duplicate — otherwise RecursiveTypeMapper's
60+
// identity check throws "Cached type in registry is not the type returned by type mapper."
61+
// on the first request in long-lived workers (Swoole/RoadRunner/FrankenPHP).
62+
$typeRegistry = $this->getTypeRegistry();
63+
$reentrantlyRegistered = new MutableObjectType(['name' => 'TestObject', 'fields' => []], TypeFoo::class);
64+
65+
$container = new LazyContainer([
66+
TypeFoo::class => static function () use ($typeRegistry, $reentrantlyRegistered) {
67+
if (! $typeRegistry->hasType('TestObject')) {
68+
$typeRegistry->registerType($reentrantlyRegistered);
69+
}
70+
71+
return new TypeFoo();
72+
},
73+
]);
74+
75+
$typeGenerator = new TypeGenerator(
76+
$this->getAnnotationReader(),
77+
new NamingStrategy(),
78+
$typeRegistry,
79+
$container,
80+
$this->getTypeMapper(),
81+
$this->getFieldsBuilder(),
82+
);
83+
84+
$this->assertSame($reentrantlyRegistered, $typeGenerator->mapAnnotatedObject(TypeFoo::class));
85+
}
5486
}

website/docs/internals.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ Imagine that class "B" extends class "A" and class "A" maps to GraphQL type "ATy
113113

114114
Since "B" *is a* "A", the "recursive type mapper" role is to make sure that "B" will also map to GraphQL type "AType".
115115

116+
The `RecursiveTypeMapper` also enforces a per-name identity invariant on the type registry: a given GraphQL type name must
117+
resolve to a single type instance. In long-lived or coroutine runtimes (Swoole, RoadRunner, FrankenPHP) a concurrent
118+
first-request build can violate it and surface the `Cached type in registry is not the type returned by type mapper.`
119+
error — see [Troubleshooting](troubleshooting.md) for the cause and fix.
120+
116121
## Parameter mapper middlewares
117122

118123
"Parameter middlewares" are used to decide what argument should be injected into a parameter.

website/docs/query-plan.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class ProductsController
5151
#[Query]
5252
public function products(ResolveInfo $info): array
5353
{
54-
if (isset($info->getFieldSelection()['manufacturer']) {
54+
if (isset($info->getFieldSelection()['manufacturer'])) {
5555
// Let's perform a request with a JOIN on manufacturer
5656
} else {
5757
// Let's perform a request without a JOIN on manufacturer

website/docs/troubleshooting.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,41 @@ GraphQLite controllers in the Symfony controller namespace (`App\Controller` by
2222
object type-hinted in a method of a controller is a service ([because all controllers are tagged with the "controller.service_arguments" tag](https://symfony.com/doc/current/service_container/3.3-di-changes.html#controllers-are-registered-as-services))
2323

2424
To fix this issue, do not put your GraphQLite controller in the same namespace as the Symfony controllers and
25-
reconfigure your `config/graphqlite.yml` file to point to your new namespace.
25+
reconfigure your `config/graphqlite.yml` file to point to your new namespace.
26+
27+
28+
**Error: Cached type in registry is not the type returned by type mapper.**
29+
30+
**Schema must contain unique named types but contains multiple types named "X".**
31+
32+
You are most likely running GraphQLite inside a long-lived / coroutine PHP runtime (Swoole, RoadRunner, FrankenPHP) where
33+
a single worker serves many requests. These errors appear only on the **first request(s) after a cold worker boot**
34+
every subsequent request on that worker is fine.
35+
36+
In a long-lived runtime the in-memory `TypeRegistry` and type caches persist across requests, which is exactly what makes
37+
them fast. But the *first* schema build is expensive and does real filesystem I/O (class discovery, PSR-16 file-cache
38+
reads, docblock/annotation reading). Under coroutine concurrency — for example a client firing two introspection queries
39+
at once, or simply two concurrent first requests — the runtime can yield between a type cache **miss** check and the
40+
subsequent cache **write**. Two builders then construct duplicate type instances for the same GraphQL type name: the
41+
first error is GraphQLite's `RecursiveTypeMapper` tripping its identity invariant, and the second is webonyx/graphql-php
42+
rejecting the duplicate named types when assembling the schema.
43+
44+
The fix is to **single-flight the first schema build** so that the registry is fully warm before any concurrent request
45+
can race it. The simplest approach is to warm the schema once at worker bootstrap, before serving traffic — build it and
46+
force the full type map to resolve while the worker is still single-threaded (for instance in an `OnWorkerStart` /
47+
bootstrap hook):
48+
49+
```php
50+
$schema = $factory->createSchema();
51+
$schema->getTypeMap(); // forces every lazy type to resolve while single-threaded
52+
```
53+
54+
Calling `$schema->assertValid()` instead also fully populates the registry. Once the registry is warm, every later
55+
request reuses it and the race window never opens again.
56+
57+
If you cannot warm at bootstrap, guard the first build with a lock so that only one coroutine/worker builds the schema
58+
while the others wait, then all reuse the warm registry.
59+
60+
Note that a per-request cache check is **not** sufficient on its own: the build yields between the check and the write,
61+
so the type cache is only authoritative once the first build has completed end to end without interruption. That is why
62+
the fix lives at the application/runtime boundary (warm or lock) rather than in a per-call cache re-check.
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
---
2+
id: changelog
3+
title: Changelog
4+
sidebar_label: Changelog
5+
---
6+
7+
## >8.0.0
8+
9+
**For all future changelog details, refer to the [Releases](https://github.com/thecodingmachine/graphqlite/releases).
10+
This CHANGELOG.md will no longer be independently maintained.**
11+
12+
## 8.0.0
13+
14+
### Breaking Changes
15+
16+
- [#677 Drops support for Doctrine annotations](https://github.com/thecodingmachine/graphqlite/pull/677) @fogrye
17+
18+
### Improvements
19+
20+
- [#668 Adds working examples to docs](https://github.com/thecodingmachine/graphqlite/pull/668) @shish
21+
- [#698 Performance optimizations and caching in development environments (`devMode()`)](https://github.com/thecodingmachine/graphqlite/pull/698) @oprypkhantc]
22+
23+
### Bug Fixes
24+
25+
- [#702 Fix prefetching for nested fields](https://github.com/thecodingmachine/graphqlite/pull/702) @sudevva
26+
27+
### Minor Changes
28+
29+
- [#695 Removes dependecy to unmaintained thecodingmachine/cache-utils dependency](https://github.com/thecodingmachine/graphqlite/pull/695) @xyng
30+
- [#712 Caching improvements with use of multiple ClassFinders](https://github.com/thecodingmachine/graphqlite/pull/712) @andrew-demb
31+
32+
## 7.1.0
33+
34+
### Breaking Changes
35+
36+
- #698 Removes some methods and classes, namely:
37+
- Deprecated `SchemaFactory::addControllerNamespace()` and `SchemaFactory::addTypeNamespace()` in favor of `SchemaFactory::addNamespace()`
38+
- Deprecated `SchemaFactory::setGlobTTL()` in favor of `SchemaFactory::devMode()` and `SchemaFactory::prodMode()`
39+
- Removed `FactoryContext::get*TTL()` and `RootTypeMapperFactoryContext::get*TTL()` as GraphQLite no longer uses TTLs to invalidate caches
40+
- Removed `StaticClassListTypeMapper` in favor of `ClassFinderTypeMapper` used with `StaticClassFinder`
41+
- Renamed `GlobTypeMapper` to `ClassFinderTypeMapper`
42+
- Renamed `SchemaFactory::setClassBoundCacheContractFactory()` to `SchemaFactory::setClassBoundCache()`,
43+
`FactoryContext::getClassBoundCacheContractFactory()` to `FactoryContext::getClassBoundCache()` and changed their signatures
44+
- Removed `RootTypeMapperFactoryContext::getTypeNamespaces()` in favor of `RootTypeMapperFactoryContext::getClassFinder()`
45+
46+
### Improvements
47+
48+
- #698 Performance optimizations and caching in development environments (`devMode()`). @oprypkhantc
49+
50+
## 7.0.0
51+
52+
### Breaking Changes
53+
54+
- #664 Replaces [thecodingmachine/class-explorer](https://github.com/thecodingmachine/class-explorer) with [kcs/class-finder](https://github.com/alekitto/class-finder) resulting in the `SchemaFactory::setClassNameMapper` being renamed to `SchemaFactory::setFinder`. This now expects an instance of `Kcs\ClassFinder\Finder` instead of `Kcs\ClassFinder\Finder\FinderInterface`. @fogrye
55+
56+
### New Features
57+
58+
- #649 Adds support for `subscription` operations. @oojacoboo
59+
- #612 Automatic query complexity analysis. @oprypkhantc
60+
- #611 Automatic persisted queries. @oprypkhantc
61+
62+
### Improvements
63+
64+
- #658 Improves on prefetching for nested fields. @grynchuk
65+
- #646 Improves exception handling during schema parsing. @fogrye
66+
- #636 Allows the use of middleware on construtor params/fields. @oprypkhantc
67+
- #623 Improves support for description arguments on types/fields. @downace
68+
- #628 Properly handles `@param` annotations for generics support on field annotated constructor arguments. @oojacoboo
69+
- #584 Immutability improvements across the codebase. @oprypkhantc
70+
- #588 Prefetch improvements. @oprpkhantc
71+
- #606 Adds support for phpdoc descriptions and deprecation annotations on native enums. @mdoelker
72+
- Thanks to @shish, @cvergne and @mshapovalov for updating the docs!
73+
74+
### Minor Changes
75+
76+
- #639 Added support for Symfony 7. @janatjak
77+
78+
79+
## 6.2.3
80+
81+
Adds support for `Psr\Container` 1.1 with #601
82+
83+
## 6.2.2
84+
85+
This is a very simple release. We support Doctrine annotation 1.x and we've deprecated `SchemaFactory::setDoctrineAnnotationReader` in favor of native PHP attributes.
86+
87+
## 6.2.1
88+
89+
- Added support for new `Void` return types, allowing use of `void` from operation resolvers. #574
90+
- Improvements with authorization middleware #571
91+
- Updated vendor dependencies: #580 #558
92+
93+
## 6.2.0
94+
95+
Lots of little nuggets in this release! We're now targeting PHP ^8.1 and have testing on 8.2.
96+
97+
- Better support for union types and enums: #530, #535, #561, #570
98+
- Various bug and interface fixes: #532, #575, #564
99+
- GraphQL v15 required: #542
100+
- Lots of codebase improvements, more strict typing: #548
101+
102+
A special thanks to @rusted-love and @oprypkhantc for their contributions.
103+
104+
## 6.1.0
105+
106+
A shoutout to @bladl for his work on this release, improving the code for better typing and PHP 8.0 syntax updates!
107+
108+
### Breaking Changes
109+
110+
- #518 PSR-11 support now requires version 2
111+
- #508 Due to some of the code improvements, additional typing has been added to some interfaces/classes. For instance, `RootTypeMapperInterface::toGraphQLOutputType` and `RootTypeMapperInterface::toGraphQLInputType` now have the following signatures:
112+
113+
```php
114+
/**
115+
* @param (OutputType&GraphQLType)|null $subType
116+
*
117+
* @return OutputType&GraphQLType
118+
*/
119+
public function toGraphQLOutputType(
120+
Type $type,
121+
OutputType|null $subType,
122+
ReflectionMethod|ReflectionProperty $reflector,
123+
DocBlock $docBlockObj
124+
): OutputType;
125+
126+
/**
127+
* @param (InputType&GraphQLType)|null $subType
128+
*
129+
* @return InputType&GraphQLType
130+
*/
131+
public function toGraphQLInputType(
132+
Type $type,
133+
InputType|null $subType,
134+
string $argumentName,
135+
ReflectionMethod|ReflectionProperty $reflector,
136+
DocBlock $docBlockObj
137+
): InputType;
138+
```
139+
140+
### Improvements
141+
142+
- #510
143+
- #508
144+
145+
## 5.0.0
146+
147+
### Dependencies
148+
149+
- Upgraded to using version 14.9 of [webonyx/graphql-php](https://github.com/webonyx/graphql-php)
150+
151+
## 4.3.0
152+
153+
### Breaking change
154+
155+
- The method `setAnnotationCacheDir($directory)` has been removed from the `SchemaFactory`. The annotation
156+
cache will use your `Psr\SimpleCache\CacheInterface` compliant cache handler set through the `SchemaFactory`
157+
constructor.
158+
159+
### Minor changes
160+
161+
- Removed dependency for doctrine/cache and unified some of the cache layers following a PSR interface.
162+
- Cleaned up some of the documentation in an attempt to get things accurate with versioned releases.
163+
164+
## 4.2.0
165+
166+
### Breaking change
167+
168+
The method signature for `toGraphQLOutputType` and `toGraphQLInputType` have been changed to the following:
169+
170+
```php
171+
/**
172+
* @param \ReflectionMethod|\ReflectionProperty $reflector
173+
*/
174+
public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType;
175+
176+
/**
177+
* @param \ReflectionMethod|\ReflectionProperty $reflector
178+
*/
179+
public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType;
180+
```
181+
182+
### New features
183+
184+
- [@Input](attributes-reference.md#input) annotation is introduced as an alternative to `#[Factory]`. Now GraphQL input type can be created in the same manner as `#[Type]` in combination with `#[Field]` - [example](input-types.mdx#input-attribute).
185+
- New attributes has been added to [@Field](attributes-reference.md#field) annotation: `for`, `inputType` and `description`.
186+
- The following annotations now can be applied to class properties directly: `#[Field]`, `#[Logged]`, `#[Right]`, `@FailWith`, `@HideIfUnauthorized` and `#[Security]`.
187+
188+
## 4.1.0
189+
190+
### Breaking change
191+
192+
There is one breaking change introduced in the minor version (this was important to allow PHP 8 compatibility).
193+
194+
- The **ecodev/graphql-upload** package (used to get support for file uploads in GraphQL input types) is now a "recommended" dependency only.
195+
If you are using GraphQL file uploads, you need to add `ecodev/graphql-upload` to your `composer.json`.
196+
197+
### New features
198+
199+
- All annotations can now be accessed as PHP 8 attributes
200+
- The `@deprecated` annotation in your PHP code translates into deprecated fields in your GraphQL schema
201+
- You can now specify the GraphQL name of the Enum types you define
202+
- Added the possibility to inject pure Webonyx objects in GraphQLite schema
203+
204+
### Minor changes
205+
206+
- Migrated from `zend/diactoros` to `laminas/diactoros`
207+
- Making the annotation cache directory configurable
208+
209+
### Miscellaneous
210+
211+
- Migrated from Travis to Github actions
212+
213+
## 4.0.0
214+
215+
This is a complete refactoring from 3.x. While existing annotations are kept compatible, the internals have completely
216+
changed.
217+
218+
### New features
219+
220+
- You can directly [annotate a PHP interface with `#[Type]` to make it a GraphQL interface](inheritance-interfaces.mdx#mapping-interfaces)
221+
- You can autowire services in resolvers, thanks to the new `@Autowire` annotation
222+
- Added [user input validation](validation.mdx) (using the Symfony Validator or the Laravel validator or a custom `#[Assertion]` annotation
223+
- Improved security handling:
224+
- Unauthorized access to fields can now generate GraphQL errors (rather that schema errors in GraphQLite v3)
225+
- Added fine-grained security using the `#[Security]` annotation. A field can now be [marked accessible or not depending on the context](fine-grained-security.mdx).
226+
For instance, you can restrict access to the field "viewsCount" of the type `BlogPost` only for post that the current user wrote.
227+
- You can now inject the current logged user in any query / mutation / field using the `#[InjectUser]` annotation
228+
- Performance:
229+
- You can inject the [Webonyx query plan in a parameter from a resolver](query-plan.mdx)
230+
- You can use the [dataloader pattern to improve performance drastically via the "prefetchMethod" attribute](prefetch-method.mdx)
231+
- Customizable error handling has been added:
232+
- You can throw [many errors in one exception](error-handling.mdx#many-errors-for-one-exception) with `TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException`
233+
- You can force input types using `@UseInputType(for="$id", inputType="ID!")`
234+
- You can extend an input types (just like you could extend an output type in v3) using [the new `#[Decorate]` annotation](extend-input-type.mdx)
235+
- In a factory, you can [exclude some optional parameters from the GraphQL schema](input-types#ignoring-some-parameters)
236+
237+
Many extension points have been added
238+
239+
- Added a "root type mapper" (useful to map scalar types to PHP types or to add custom annotations related to resolvers)
240+
- Added ["field middlewares"](field-middlewares.md) (useful to add middleware that modify the way GraphQL fields are handled)
241+
- Added a ["parameter type mapper"](argument-resolving.md) (useful to add customize parameter resolution or add custom annotations related to parameters)
242+
243+
New framework specific features:
244+
245+
### Symfony
246+
247+
- The Symfony bundle now provides a "login" and a "logout" mutation (and also a "me" query)
248+
249+
### Laravel
250+
251+
- [Native integration with the Laravel paginator](laravel-package-advanced.mdx#support-for-pagination) has been added
252+
253+
### Internals
254+
255+
- The `FieldsBuilder` class has been split in many different services (`FieldsBuilder`, `TypeHandler`, and a
256+
chain of *root type mappers*)
257+
- The `FieldsBuilderFactory` class has been completely removed.
258+
- Overall, there is not much in common internally between 4.x and 3.x. 4.x is much more flexible with many more hook points
259+
than 3.x. Try it out!

0 commit comments

Comments
 (0)