matrei commented on PR #16237:
URL: https://github.com/apache/grails-core/pull/16237#issuecomment-5726461771

   # Review round 3
   
   **Head:** `ab080d24ae173820e0b186b47d437abf74cfa4d9` 
(`codeconsole:feature/spring-media-type-negotiation-8.0.x`)
   **Base:** `8.0.x` — merge-base still `3067d0a855`, clean merge on top of it.
   **Delta since round 2:** one commit, `ab080d24ae` (13 files, +188/−34).
   
   **What I ran locally:**
   
   - `./gradlew codeStyle` — green.
   - Module tests with `--no-build-cache` and `cleanTest` (result XML confirmed 
fresh), 0 failures: `grails-converters` (115), `grails-rest-transforms` (87), 
`grails-web-common` (97), `grails-testing-support-core` (10), `grails-xml` 
(29), `grails-gsp` plugin (666), `grails-test-suite-web` (454), 
`grails-test-suite-uber` (576).
   - `:grails-test-examples-hibernate7-demo33:test --tests 
demo.PersonControllerHibernateSpec` — **fails**, same as CI (see point 1).
   - Two scratch specs, since deleted: a `HibernateSpec` + `ControllerUnitTest` 
probe in demo33 that prints singleton state before `controller` is touched, and 
a `ControllerUnitTest` in `grails-test-suite-web` covering the plugin list, 
bean count, link generation and a pretty-printed scalar root.
   - CI on this head is **red**: 6 jobs. Four are the demo33 failure below 
(Build Grails shard 1, three Hibernate7 functional jobs), one is the 
known-flaky Geb login test (`UserControllerSpec > User list`, #16030), and the 
macOS job timed out at 6h.
   
   **Verdict: round-2 findings are resolved, but the harness fix introduced a 
real regression that fails CI. One blocking item, one should-fix.** The curated 
plugin graph is back (`core` + `restResponder`, 152 bean definitions, 
`CompiledTagInvocationSpec` passes without its workaround), duplicate 
definitions are gone, and every round-2 minor was addressed. What is left is a 
bean cycle in `XmlGrailsPlugin` that the new registration order exposes, and an 
incomplete `JSONWriter` change.
   
   ---
   
   ## Blocking
   
   ### 1. `xmlRenderer` ↔ `groovyPageLocator` cycle fails `HibernateSpec` 
controller tests (CI red)
   
   `PersonControllerHibernateSpec` in `grails-test-examples/hibernate7/demo33` 
fails deterministically, locally and in four CI jobs:
   
   ```
   BeanCreationException: Error creating bean with name 'xmlRenderer': 
Instantiation of supplied bean failed
       at GrailsUnitTest.defineBeans(GrailsUnitTest.groovy:110)
       at GrailsWebUnitTest.mockController(GrailsWebUnitTest.groovy:132)
   Caused by: UnsatisfiedDependencyException: Error creating bean with name 
'groovyPageLocator': Unsatisfied dependency
     expressed through method 'setMimeTypeResolver' parameter 0: Error creating 
bean with name 'xmlRenderer':
     Requested bean is currently in creation
       at 
XmlGrailsPlugin$_beanRegistrar_closure1.closure2$_closure5(XmlGrailsPlugin.groovy:75)
   Caused by: BeanCurrentlyInCreationException: ... 'xmlRenderer'
       at DefaultListableBeanFactory.resolveMultipleBeanCollection   <- 
Collection<MimeTypeProvider>
       at ConstructorResolver.autowireConstructor                    <- 
MimeTypesConfiguration
       at ConstructorResolver.instantiateUsingFactoryMethod          <- 
mimeTypeResolver
   ```
   
   The cycle is in production code, not in the harness:
   
   `xmlRenderer` supplier (`XmlGrailsPlugin.groovy:75`, 
`beanProvider(GrailsConventionGroovyPageLocator).getIfAvailable()`) → 
`groovyPageLocator.setMimeTypeResolver` 
(`GrailsConventionGroovyPageLocator.java:59`) → `mimeTypeResolver` factory 
method (`MimeTypesConfiguration.groovy:123`) → `MimeTypesConfiguration` 
constructor, which autowires every `MimeTypeProvider` → `Renderer extends 
MimeTypeProvider` (`Renderer.groovy:29`) → `xmlRenderer`, which is currently in 
creation. It is constructor injection on one side and a supplier on the other, 
so `allowCircularReferences` cannot break it.
   
   It only stays hidden while `mimeTypesConfiguration` happens to be 
instantiated before `xmlRenderer`, or while `groovyPageLocator` does not exist 
yet. In an ordinary `ControllerUnitTest` the second condition holds: 
`xmlRenderer` is built at refresh, before `WebSetupSpecInterceptor` defines the 
locator (my probe: `xmlRenderer` singleton present, its `groovyPageLocator` is 
`null`). The Hibernate 7 `HibernateSpec` takes its "context already exists" 
branch (`HibernateSpec.groovy:154`, `configureForBeanDefinitionRegistry` on the 
live context), which registers definitions into the already refreshed context; 
I did not trace which registration does it, but the effect is that singletons 
built at refresh are gone afterwards. My probe in that spec, before 
`controller` is touched:
   
   ```
   XML_SINGLETON: false
   LOCATOR_DEF: true
   MIME_CFG: [mimeTypesConfiguration=false]
   ```
   
   The next `defineBeans` → `preInstantiateSingletons()` then recreates 
`xmlRenderer` with the locator defined and `mimeTypesConfiguration` not yet 
built, and the cycle closes. The H5 `demo33` has the same spec and `grails-xml` 
but no such branch, so it passes. Round 2's head was green because the 
plugin-manager registration order differed; the hazard itself was already 
there. A real application with `grails-xml` and GSP has the same ordering 
dependence; the functional tests pass, so the order is favourable today, but 
nothing guarantees it.
   
   Suggested fix, in the plugin rather than the harness: do not resolve the 
locator inside the `xmlRenderer` supplier. Either make the locator lazy (pass 
an `ObjectProvider`/`Supplier<GrailsConventionGroovyPageLocator>` into 
`DefaultXmlRenderer` and resolve on first view lookup), or drop it from the 
bean and let `DefaultRendererRegistry` hand its own autowired 
`groovyPageLocator` (`DefaultRendererRegistry.groovy:66`) to renderers when 
they are registered. The lazy variant also fixes the side effect noted under 
Minor. Please add a test that reproduces the ordering (define 
`groovyPageLocator`, destroy the `xmlRenderer` and `mimeTypesConfiguration` 
singletons, call `preInstantiateSingletons()`), so this does not depend on an 
example app to be caught.
   
   ## Should fix before merge
   
   ### 2. Scalar JSON roots: `PrettyPrintJSONWriter` was not updated, and the 
upgrade guide now contradicts the code
   
   `JSONWriter.append(Writable)` now accepts `INIT` and moves to `DONE` 
(`JSONWriter.java:113-126`), which is what makes `respond 'ok'` and `render 
Role.HEAD as JSON` work on the legacy path. `PrettyPrintJSONWriter` overrides 
`append` with its own copy of the old condition 
(`PrettyPrintJSONWriter.java:80-103`, still `mode == OBJECT || mode == ARRAY`), 
and `JSON.render` picks that writer whenever `prettyPrint` is on 
(`JSON.java:111`). Scratch spec on this head:
   
   ```groovy
   def json = new JSON('ok'); json.prettyPrint = true; json.render(out)
   // ConverterException: JSONException: Value out of sequence: expected mode 
to be OBJECT or ARRAY when writing 'ok' but was INIT
   ```
   
   Same result through a controller with `render json`. So with 
`grails.converters.json.pretty.print=true`, `respond 'ok'`, a root enum, number 
or boolean still 500. `JSONWriterSpec` only covers the base class; the new 
table should run against both writers.
   
   This is also a user-visible behaviour change that goes beyond `respond`: 
every `x as JSON` / `new JSON(x)` with a scalar root now renders instead of 
throwing. `upgrading80x.adoc` section 25 ("Rendering Enum values as JSON", line 
1095) still says rendering a single enum "will now instead throw a 
`ConverterException`", and `JSONConverterTests.testJSONEnumConverting` was 
flipped from asserting that exception to asserting `"HEAD"` in this commit. 
Section 25 needs rewriting to the new outcome (a JSON string of the enum name, 
via `SimpleEnumMarshaller`), and the scalar-root change deserves a sentence of 
its own.
   
   ## Minor
   
   - In web unit tests `xmlRenderer` is built before `groovyPageLocator` 
exists, so its locator is `null` for the life of the spec and `respond` with 
`Accept: application/xml` never looks for a GSP view. On `8.0.x` the registry 
created the default XML renderer with its own autowired locator. The lazy 
locator from point 1 restores that.
   - `registerPluginBeans` (`GrailsApplicationBuilder.groovy:205-211`) guards 
the XML plugin with `ClassUtils.isPresent` but loads `ConvertersGrailsPlugin` 
unconditionally. It holds today because `grails-testing-support-web` brings 
`grails-rest-transforms` → `grails-converters` via `api`, but the core module 
excludes `grails-converters` (`build.gradle:48`), so the same guard would be 
cheap insurance.
   - `NamedJsonConfigurationRegistry.resolveMapper` calls the supplier, which 
does Spring bean lookups, while holding the registry monitor. No deadlock path 
that I can construct at request time, but a plain racy read-then-write of the 
`volatile` would do the same job without a lock (`forGrails` is idempotent per 
mapper).
   - `DefaultJsonRenderer.groovy:62`: the new instance field sits above the 
`static final PROBLEM_JSON` constant.
   
   ## Round-2 findings, status
   
   | # | Finding | Status |
   |---|---------|--------|
   | 1 | Web unit tests booted the full controllers/urlMappings/i18n graph | 
Fixed as suggested. Only `restResponder` is added to the filter 
(`GrailsApplicationBuilder.groovy:232`); the converters and XML bean registrars 
are applied directly before refresh (`:188-199`), skipped when the test 
includes those plugins itself. Measured: plugins `[core, restResponder]`, 152 
bean definitions, harness URL mapping `/(*)/(*)?/(*)?` as on `8.0.x`. 
`CompiledTagInvocationSpec` lost its `controllerName` workaround and passes. 
`ControllerJsonSerializationSpec` asserts the plugin set, no `UrlMappings` 
artefact, and that an application `JsonMapperBuilderCustomizer` sees the same 
`GrailsJsonMapperCustomizer` bean. Documented in `unitTesting.adoc` and section 
65. Side effect: point 1 above. |
   | 2 | Beans defined twice in the web harness | Fixed. `rendererRegistry`, 
the first `localeResolver` and the post-refresh `ConvertersGrailsPlugin` are 
gone from `WebSetupSpecInterceptor`; `grailsUrlMappingsHolder`, 
`grailsLinkGenerator` and `localeResolver` are guarded by `containsBean`. |
   | minor | `respond 'ok'` on the legacy path | Fixed for the default writer 
(`new JSON(object)`, `DefaultJsonRenderer.groovy:173-176`, plus the 
`JSONWriter` change). Not for pretty print, see point 2. |
   | minor | Converter rebuilt per response | Fixed, cached per source 
converter (`DefaultJsonRenderer.groovy:207-209`); the spec alternates two 
source converters with different naming strategies. |
   | minor | Registry supplier scanned on every write | Fixed, first non-null 
mapper is cached and absence is retried; covered in 
`NamedJsonConfigurationRegistrySpec`. |
   | minor | `isWebTest()` overrides `getIncludePlugins()` undocumented | Fixed 
in `unitTesting.adoc` and section 65. |
   
   ## Verified as correct
   
   - Link generation in the web harness behaves as on `8.0.x`: a link with only 
`action` fails against the curated `/(*)/(*)?/(*)?` mapping, not against 
`DefaultUrlMappings`; the urlMappings plugin is not loaded.
   - `encodeAsXML()` needing `mockCodec(XMLCodec)` in unit tests is unchanged 
from `8.0.x`; registering only the XML bean registrar (not `providedArtefacts`) 
does not regress it.
   - The `grailsConverters` cache key is the source converter instance 
(`HttpMessageConverter` has identity equality), and `forGrails` is itself 
cached per mapper, so two converters sharing a mapper still share the derived 
mapper.
   - `JSONWriter` base class: a second root value after a scalar throws and 
leaves the output untouched; `object()`/`array()` after `DONE` still throw. 
`JSONWriterSpec` covers string escaping, `GString`, numbers, booleans and 
`null`.
   - The scaffolding `UserControllerSpec > User list` failure is the known Geb 
login flake (#16030), unrelated to this PR.
   - Everything in the round-1 and round-2 "Verified as correct" lists is 
unchanged by this commit.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to