Skip to content

Commit cf5c82d

Browse files
authored
Merge branch '8.0.x' into fix/databinding-raw-collection-8.0.x
2 parents a4c7442 + e40cb27 commit cf5c82d

94 files changed

Lines changed: 4918 additions & 264 deletions

File tree

Some content is hidden

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

.github/workflows/gradle.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jobs:
4444
path: |
4545
~/.gradle/caches/modules-2
4646
~/.gradle/wrapper
47+
~/.embedmongo
4748
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
4849
restore-keys: |
4950
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -93,6 +94,7 @@ jobs:
9394
path: |
9495
~/.gradle/caches/modules-2
9596
~/.gradle/wrapper
97+
~/.embedmongo
9698
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
9799
restore-keys: |
98100
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -181,6 +183,7 @@ jobs:
181183
path: |
182184
~/.gradle/caches/modules-2
183185
~/.gradle/wrapper
186+
~/.embedmongo
184187
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
185188
restore-keys: |
186189
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -211,6 +214,7 @@ jobs:
211214
path: |
212215
~/.gradle/caches/modules-2
213216
~/.gradle/wrapper
217+
~/.embedmongo
214218
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
215219
buildRerunTasks:
216220
if: ${{ !contains(github.event.head_commit.message, '[skip tests]') }}
@@ -263,6 +267,7 @@ jobs:
263267
path: |
264268
~/.gradle/caches/modules-2
265269
~/.gradle/wrapper
270+
~/.embedmongo
266271
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
267272
restore-keys: |
268273
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -291,6 +296,7 @@ jobs:
291296
path: |
292297
~/.gradle/caches/modules-2
293298
~/.gradle/wrapper
299+
~/.embedmongo
294300
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
295301
buildForge:
296302
name: "Build Grails Forge (Java ${{ matrix.java }}, indy=${{ matrix.indy }})"
@@ -321,6 +327,7 @@ jobs:
321327
path: |
322328
~/.gradle/caches/modules-2
323329
~/.gradle/wrapper
330+
~/.embedmongo
324331
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
325332
restore-keys: |
326333
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -441,6 +448,7 @@ jobs:
441448
path: |
442449
~/.gradle/caches/modules-2
443450
~/.gradle/wrapper
451+
~/.embedmongo
444452
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
445453
restore-keys: |
446454
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -476,6 +484,7 @@ jobs:
476484
path: |
477485
~/.gradle/caches/modules-2
478486
~/.gradle/wrapper
487+
~/.embedmongo
479488
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
480489
- name: "📤 Upload Geb Reports"
481490
if: failure()
@@ -676,6 +685,7 @@ jobs:
676685
path: |
677686
~/.gradle/caches/modules-2
678687
~/.gradle/wrapper
688+
~/.embedmongo
679689
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
680690
restore-keys: |
681691
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -728,6 +738,7 @@ jobs:
728738
path: |
729739
~/.gradle/caches/modules-2
730740
~/.gradle/wrapper
741+
~/.embedmongo
731742
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
732743
restore-keys: |
733744
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -879,6 +890,7 @@ jobs:
879890
path: |
880891
~/.gradle/caches/modules-2
881892
~/.gradle/wrapper
893+
~/.embedmongo
882894
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
883895
restore-keys: |
884896
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -946,6 +958,7 @@ jobs:
946958
path: |
947959
~/.gradle/caches/modules-2
948960
~/.gradle/wrapper
961+
~/.embedmongo
949962
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
950963
restore-keys: |
951964
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -1064,6 +1077,7 @@ jobs:
10641077
path: |
10651078
~/.gradle/caches/modules-2
10661079
~/.gradle/wrapper
1080+
~/.embedmongo
10671081
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
10681082
restore-keys: |
10691083
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -1181,6 +1195,7 @@ jobs:
11811195
path: |
11821196
~/.gradle/caches/modules-2
11831197
~/.gradle/wrapper
1198+
~/.embedmongo
11841199
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
11851200
restore-keys: |
11861201
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-

.github/workflows/groovy-joint-workflow.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ jobs:
154154
path: |
155155
~/.gradle/caches/modules-2
156156
~/.gradle/wrapper
157+
~/.embedmongo
157158
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}
158159
restore-keys: |
159160
gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-
@@ -197,4 +198,5 @@ jobs:
197198
path: |
198199
~/.gradle/caches/modules-2
199200
~/.gradle/wrapper
201+
~/.embedmongo
200202
key: gradle-deps-${{ runner.os }}-${{ github.base_ref || github.ref_name }}-${{ hashFiles('**/dependencies.gradle', '**/gradle-wrapper.properties') }}

gradle/functional-test-config.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ tasks.withType(Test).configureEach { Test task ->
199199
systemProperty('geb.build.reportsDir', reporting.file("$projectDir/build/geb-reports"))
200200
systemProperty('grails.geb.reporting.directory', reporting.file("$projectDir/build/geb-reports").canonicalFile.absolutePath)
201201
systemProperty('TESTCONFIG', System.getProperty('TESTCONFIG'))
202+
203+
if (isMongo && rootProject.hasProperty('mongodbContainerVersion')) {
204+
// The version the build asks for reaches the Gradle JVM as a project property and stops
205+
// there, while the application under test reads a system property: without this a matrix
206+
// asking for one version and an application defaulting to another agree only by accident.
207+
systemProperty('mongodbContainerVersion', rootProject.property('mongodbContainerVersion'))
208+
}
202209
}
203210

204211
tasks.named('groovydoc').configure {

gradle/mongodb-forked-test-config.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,11 @@ tasks.withType(Test).configureEach {
6767
}
6868
// Used in the TCK test to selectively enable/disable tests
6969
systemProperty('mongodb.gorm.suite', 'true')
70+
71+
// The version the build asks for reaches the Gradle JVM as a project property and stops there,
72+
// while the test reads a system property: without this a matrix asking for one version and a
73+
// test defaulting to another agree only by accident. CI passes -PmongodbContainerVersion.
74+
if (rootProject.hasProperty('mongodbContainerVersion')) {
75+
systemProperty('mongodbContainerVersion', rootProject.property('mongodbContainerVersion'))
76+
}
7077
}

gradle/mongodb-test-config.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,12 @@ tasks.withType(Test).configureEach {
6767
}
6868
// Used in the TCK test to selectively enable/disable tests
6969
systemProperty('mongodb.gorm.suite', 'true')
70+
71+
// The version the build asks for reaches the Gradle JVM as a project property and stops there,
72+
// while the test reads a system property: without this a matrix asking for one version and a
73+
// test defaulting to another agree only by accident. CI passes -PmongodbContainerVersion.
74+
if (rootProject.hasProperty('mongodbContainerVersion')) {
75+
systemProperty('mongodbContainerVersion', rootProject.property('mongodbContainerVersion'))
76+
}
7077
}
7178

grails-async/plugin/src/main/groovy/org/grails/plugins/web/async/AsyncWebRequestPromiseDecoratorLookupStrategy.groovy

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import groovy.transform.CompileStatic
2222

2323
import grails.async.decorator.PromiseDecorator
2424
import grails.async.decorator.PromiseDecoratorLookupStrategy
25+
import groovy.util.logging.Slf4j
26+
import jakarta.servlet.http.HttpServletRequest
27+
28+
import grails.async.web.AsyncGrailsWebRequest
29+
2530
import org.grails.web.servlet.mvc.GrailsWebRequest
2631

2732
/**
@@ -30,6 +35,7 @@ import org.grails.web.servlet.mvc.GrailsWebRequest
3035
* @author Graeme Rocher
3136
* @since 2.3
3237
*/
38+
@Slf4j
3339
@CompileStatic
3440
class AsyncWebRequestPromiseDecoratorLookupStrategy implements PromiseDecoratorLookupStrategy {
3541

@@ -38,9 +44,76 @@ class AsyncWebRequestPromiseDecoratorLookupStrategy implements PromiseDecoratorL
3844
final webRequest = GrailsWebRequest.lookup()
3945
if (webRequest) {
4046
List<PromiseDecorator> decorators = []
41-
decorators.add(new AsyncWebRequestPromiseDecorator(webRequest))
47+
// Held so that a decorator which does not survive its own construction leaves the
48+
// request as it found it: the web request it builds stores itself on the request
49+
// before asking the container to start, and a later lookup that found that
50+
// half-built object would use one whose asynchronous context was never assigned.
51+
Object boundBefore = attachedWebRequest(webRequest)
52+
try {
53+
decorators.add(new AsyncWebRequestPromiseDecorator(webRequest))
54+
}
55+
catch (IllegalStateException asynchronousProcessingUnavailable) {
56+
reattach(webRequest, boundBefore)
57+
if (!supportsAsync(webRequest)) {
58+
// A request that cannot process asynchronously at all is a mistake worth
59+
// reporting: the caller asked for something this request will never do.
60+
throw asynchronousProcessingUnavailable
61+
}
62+
// Otherwise the request does support it and is simply past the point of taking
63+
// another task: the container is delivering the result of the one that ran, and
64+
// refuses to start a second cycle on the same request. A callback attached to a
65+
// promise that completed while it was being attached lands exactly here, and the
66+
// refusal used to escape and fail the very response being delivered.
67+
//
68+
// The refusal is caught rather than anticipated on purpose. Asking first - the
69+
// async manager reports a concurrent result, say - is a check the container can
70+
// invalidate between the answer and the call: measured over 4800 requests, asking
71+
// still let one through, where catching let none through over twice as many.
72+
log.debug('Not binding this request to the promise: {}',
73+
asynchronousProcessingUnavailable.message)
74+
return Collections.emptyList()
75+
}
4276
return decorators
4377
}
4478
return Collections.emptyList()
4579
}
80+
81+
/**
82+
* What the request already carries, which is either nothing or the web request of an
83+
* asynchronous cycle that is genuinely under way.
84+
*/
85+
private static Object attachedWebRequest(GrailsWebRequest webRequest) {
86+
try {
87+
return webRequest.currentRequest.getAttribute(AsyncGrailsWebRequest.WEB_REQUEST)
88+
}
89+
catch (IllegalStateException requestIsGone) {
90+
return null
91+
}
92+
}
93+
94+
private static void reattach(GrailsWebRequest webRequest, Object boundBefore) {
95+
try {
96+
HttpServletRequest request = webRequest.currentRequest
97+
if (boundBefore == null) {
98+
request.removeAttribute(AsyncGrailsWebRequest.WEB_REQUEST)
99+
}
100+
else {
101+
request.setAttribute(AsyncGrailsWebRequest.WEB_REQUEST, boundBefore)
102+
}
103+
}
104+
catch (IllegalStateException requestIsGone) {
105+
log.debug('The request was recycled before its binding could be put back: {}',
106+
requestIsGone.message)
107+
}
108+
}
109+
110+
private static boolean supportsAsync(GrailsWebRequest webRequest) {
111+
try {
112+
return webRequest.currentRequest.asyncSupported
113+
}
114+
catch (IllegalStateException requestIsGone) {
115+
// Recycled, so it is in no state to take a task either way.
116+
return true
117+
}
118+
}
46119
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.grails.plugins.web.async
20+
21+
import jakarta.servlet.AsyncContext
22+
import jakarta.servlet.ServletRequest
23+
import jakarta.servlet.ServletResponse
24+
import jakarta.servlet.http.HttpServletRequest
25+
26+
import grails.async.web.AsyncGrailsWebRequest
27+
28+
import org.springframework.mock.web.MockHttpServletRequest
29+
import org.springframework.mock.web.MockHttpServletResponse
30+
import org.springframework.mock.web.MockServletContext
31+
import org.springframework.web.context.request.RequestContextHolder
32+
33+
import spock.lang.Specification
34+
35+
import org.grails.web.servlet.mvc.GrailsWebRequest
36+
37+
/**
38+
* What happens when a promise is handed a request the container will not start a new asynchronous
39+
* cycle on, which is where a callback attached to a promise that completed while it was being
40+
* attached arrives.
41+
*/
42+
class AsyncWebRequestPromiseDecoratorLookupStrategySpec extends Specification {
43+
44+
AsyncWebRequestPromiseDecoratorLookupStrategy strategy = new AsyncWebRequestPromiseDecoratorLookupStrategy()
45+
46+
void cleanup() {
47+
RequestContextHolder.resetRequestAttributes()
48+
}
49+
50+
void 'a request the container will not start again is not decorated'() {
51+
given: 'a request that supports asynchronous processing and refuses to start another cycle'
52+
bind(requestRefusingToStart())
53+
54+
when:
55+
List decorators = strategy.findDecorators()
56+
57+
then: 'the promise runs with nothing bound to it, rather than failing the response'
58+
decorators.isEmpty()
59+
}
60+
61+
void 'a request that will not start again is left as it was found'() {
62+
given:
63+
MockHttpServletRequest request = requestRefusingToStart()
64+
bind(request)
65+
66+
when: 'the decorator stores its web request on the request and is then refused'
67+
strategy.findDecorators()
68+
69+
then: 'nothing half-built is left behind for the next lookup to find and use'
70+
request.getAttribute(AsyncGrailsWebRequest.WEB_REQUEST) == null
71+
}
72+
73+
void 'a request that cannot process asynchronously at all still says so'() {
74+
given: 'what a caller gets for asking of a request that will never do this'
75+
MockHttpServletRequest request = requestRefusingToStart()
76+
request.asyncSupported = false
77+
bind(request)
78+
79+
when:
80+
strategy.findDecorators()
81+
82+
then: 'a mistake rather than a race, and reported as one'
83+
thrown(IllegalStateException)
84+
}
85+
86+
void 'a thread with no request bound to it is not decorated'() {
87+
expect:
88+
strategy.findDecorators().isEmpty()
89+
}
90+
91+
private static MockHttpServletRequest requestRefusingToStart() {
92+
MockHttpServletRequest request = new MockHttpServletRequest() {
93+
94+
@Override
95+
AsyncContext startAsync() {
96+
throw new IllegalStateException(
97+
'Calling [asyncStart()] is not valid for a request with Async state [DISPATCHING]')
98+
}
99+
100+
@Override
101+
AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) {
102+
startAsync()
103+
}
104+
}
105+
request.asyncSupported = true
106+
request
107+
}
108+
109+
private static void bind(HttpServletRequest request) {
110+
RequestContextHolder.setRequestAttributes(
111+
new GrailsWebRequest(request, new MockHttpServletResponse(), new MockServletContext()))
112+
}
113+
114+
}

grails-data-mongodb/core/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ dependencies {
133133
testImplementation 'org.apache.grails.testing:grails-testing-support-core'
134134
testImplementation 'org.spockframework:spock-core'
135135
testImplementation project(':grails-testing-support-mongodb')
136+
137+
// The transaction specifications need a replica set, which the embedded server provides without
138+
// a container; flapdoodle runs the real mongod behind it.
139+
testImplementation project(':grails-data-mongodb-embedded')
140+
testImplementation "de.flapdoodle.embed:de.flapdoodle.embed.mongo:$flapdoodleVersion"
136141
testImplementation project(':grails-data-simple'), {
137142
// test: SimpleMapDatastore, used as a cheap non-Mongo Datastore to exercise
138143
// MongoStaticApi's "not a MongoDatastore" constructor branch without needing a real Mongo

0 commit comments

Comments
 (0)