Skip to content

Commit ac0538e

Browse files
committed
Downgrade to GraphQL Java 2.4.3
1 parent fdfabbb commit ac0538e

File tree

8 files changed

+135
-27
lines changed

8 files changed

+135
-27
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ description = "Spring for GraphQL"
77
ext {
88
moduleProjects = [project(":spring-graphql"), project(":spring-graphql-test")]
99
springFrameworkVersion = "7.0.0-RC1"
10-
graphQlJavaVersion = "25.0.beta-9"
10+
graphQlJavaVersion = "24.3"
1111
}
1212

1313
subprojects {

spring-graphql/src/main/java/org/springframework/graphql/execution/ConnectionTypeDefinitionConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ private static Set<String> findConnectionTypeNames(TypeDefinitionRegistry regist
102102
.filter((type) -> type instanceof TypeName)
103103
.map((type) -> ((TypeName) type).getName())
104104
.filter((name) -> name.endsWith("Connection"))
105-
.filter((name) -> registry.getTypeOrNull(name) == null)
105+
.filter((name) -> registry.getType(name).isEmpty())
106106
.map((name) -> name.substring(0, name.length() - "Connection".length()));
107107
})
108108
.collect(Collectors.toCollection(LinkedHashSet::new));

spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import graphql.ExecutionInput;
2424
import graphql.GraphQLContext;
2525
import graphql.TrivialDataFetcher;
26+
import graphql.execution.AbortExecutionException;
2627
import graphql.execution.DataFetcherResult;
2728
import graphql.schema.DataFetcher;
2829
import graphql.schema.DataFetchingEnvironment;
@@ -53,6 +54,7 @@
5354
* <li>Re-establish Reactor Context passed via {@link ExecutionInput}.
5455
* <li>Re-establish ThreadLocal context passed via {@link ExecutionInput}.
5556
* <li>Resolve exceptions from a GraphQL subscription {@link Publisher}.
57+
* <li>Propagate the cancellation signal to {@code DataFetcher} from the transport layer.
5658
* </ul>
5759
*
5860
* @author Rossen Stoyanchev
@@ -108,6 +110,11 @@ private ContextDataFetcherDecorator(
108110
if (value == null) {
109111
return null;
110112
}
113+
if (ContextPropagationHelper.isCancelled(graphQlContext)) {
114+
return DataFetcherResult.newResult()
115+
.error(new AbortExecutionException("GraphQL request has been cancelled by the client."))
116+
.build();
117+
}
111118

112119
if (this.subscription) {
113120
Flux<?> subscriptionResult = ReactiveAdapterRegistryHelper.toSubscriptionFlux(value)
@@ -119,7 +126,8 @@ private ContextDataFetcherDecorator(
119126
return this.subscriptionExceptionResolver.resolveException(exception)
120127
.flatMap((errors) -> Mono.error(new SubscriptionPublisherException(errors, exception)));
121128
});
122-
return subscriptionResult.contextWrite(snapshot::updateContext);
129+
return ContextPropagationHelper.bindCancelFrom(subscriptionResult, graphQlContext)
130+
.contextWrite(snapshot::updateContext);
123131
}
124132

125133
value = ReactiveAdapterRegistryHelper.toMonoIfReactive(value);

spring-graphql/src/main/java/org/springframework/graphql/execution/ContextPropagationHelper.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616

1717
package org.springframework.graphql.execution;
1818

19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
1921
import graphql.GraphQLContext;
2022
import io.micrometer.context.ContextSnapshot;
2123
import io.micrometer.context.ContextSnapshotFactory;
2224
import org.jspecify.annotations.Nullable;
25+
import reactor.core.publisher.Flux;
26+
import reactor.core.publisher.Mono;
27+
import reactor.core.publisher.Sinks;
2328
import reactor.util.context.Context;
2429
import reactor.util.context.ContextView;
2530

@@ -36,6 +41,10 @@ public abstract class ContextPropagationHelper {
3641

3742
private static final String CONTEXT_SNAPSHOT_FACTORY_KEY = ContextPropagationHelper.class.getName() + ".KEY";
3843

44+
private static final String CANCELED_KEY = ContextPropagationHelper.class.getName() + ".canceled";
45+
46+
private static final String CANCELED_PUBLISHER_KEY = ContextPropagationHelper.class.getName() + ".canceledPublisher";
47+
3948

4049
/**
4150
* Select a {@code ContextSnapshotFactory} instance to use, either the one
@@ -113,4 +122,54 @@ public static ContextSnapshot captureFrom(GraphQLContext context) {
113122
return selectInstance(factory).captureFrom(context);
114123
}
115124

125+
/**
126+
* Create an atomic boolean and store it into the given {@link GraphQLContext}.
127+
* This boolean value can then be checked by upstream publishers to know whether the request is canceled.
128+
* @param context the current GraphQL context
129+
* @since 1.3.6
130+
*/
131+
public static Runnable createCancelSignal(GraphQLContext context) {
132+
AtomicBoolean requestCancelled = new AtomicBoolean();
133+
Sinks.Empty<Void> cancelSignal = Sinks.empty();
134+
context.put(CANCELED_KEY, requestCancelled);
135+
context.put(CANCELED_PUBLISHER_KEY, cancelSignal.asMono());
136+
return () -> {
137+
requestCancelled.set(true);
138+
cancelSignal.tryEmitEmpty();
139+
};
140+
}
141+
142+
/**
143+
* Return {@code true} if the current request has been cancelled, {@code false} otherwise.
144+
* This checks whether a {@link #createCancelSignal(GraphQLContext) cancellation publisher is present}
145+
* in the given context and the cancel signal has fired already.
146+
* @param context the current GraphQL context
147+
* @since 1.4.0
148+
*/
149+
public static boolean isCancelled(GraphQLContext context) {
150+
AtomicBoolean requestCancelled = context.get(CANCELED_KEY);
151+
if (requestCancelled != null) {
152+
return requestCancelled.get();
153+
}
154+
return false;
155+
}
156+
157+
/**
158+
* Bind the source {@link Flux} to the publisher from the given {@link GraphQLContext}.
159+
* The returned {@code Flux} will be cancelled when this publisher completes.
160+
* Subscribers must use the returned {@code Mono} instance.
161+
* @param source the source {@code Mono}
162+
* @param context the current GraphQL context
163+
* @param <T> the type of published elements
164+
* @return the new {@code Mono} that will be cancelled when notified
165+
* @since 1.3.5
166+
*/
167+
public static <T> Flux<T> bindCancelFrom(Flux<T> source, GraphQLContext context) {
168+
Mono<Void> cancelSignal = context.get(CANCELED_PUBLISHER_KEY);
169+
if (cancelSignal != null) {
170+
return source.takeUntilOther(cancelSignal);
171+
}
172+
return source;
173+
}
174+
116175
}

spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultExecutionGraphQlService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,13 @@ public final Mono<ExecutionGraphQlResponse> execute(ExecutionGraphQlRequest requ
9191
factory.captureFrom(contextView).updateContext(graphQLContext);
9292

9393
ExecutionInput executionInputToUse = registerDataLoaders(executionInput);
94+
Runnable cancelSignal = ContextPropagationHelper.createCancelSignal(graphQLContext);
9495

9596
return Mono.fromFuture(this.graphQlSource.graphQl().executeAsync(executionInputToUse))
9697
.onErrorResume((ex) -> ex instanceof GraphQLError, (ex) ->
9798
Mono.just(ExecutionResult.newExecutionResult().addError((GraphQLError) ex).build()))
9899
.map((result) -> new DefaultExecutionGraphQlResponse(executionInputToUse, result))
99-
.doOnCancel(executionInputToUse::cancel);
100+
.doOnCancel(cancelSignal::run);
100101
});
101102
}
102103

spring-graphql/src/main/java/org/springframework/graphql/observation/DefaultExecutionRequestObservationConvention.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public KeyValues getHighCardinalityKeyValues(ExecutionRequestObservationContext
101101
}
102102

103103
protected KeyValue executionId(ExecutionRequestObservationContext context) {
104-
return KeyValue.of(ExecutionRequestHighCardinalityKeyNames.EXECUTION_ID, context.getExecutionInput().getExecutionIdNonNull().toString());
104+
return KeyValue.of(ExecutionRequestHighCardinalityKeyNames.EXECUTION_ID, context.getExecutionInput().getExecutionId().toString());
105105
}
106106

107107
protected KeyValue operationName(ExecutionRequestObservationContext context) {

spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import io.micrometer.context.ContextRegistry;
4343
import io.micrometer.context.ContextSnapshot;
4444
import io.micrometer.context.ContextSnapshotFactory;
45+
import org.junit.jupiter.api.Disabled;
4546
import org.junit.jupiter.api.Test;
4647
import reactor.core.publisher.Flux;
4748
import reactor.core.publisher.Mono;
@@ -290,19 +291,60 @@ void trivialDataFetcherIsNotDecorated() {
290291
assertThat(dataFetcher).isInstanceOf(TrivialDataFetcher.class);
291292
}
292293

294+
@Test
295+
@Disabled("until https://github.com/spring-projects/spring-graphql/issues/1171")
296+
void cancelMonoDataFetcherWhenRequestCancelled() {
297+
AtomicBoolean dataFetcherCancelled = new AtomicBoolean();
298+
GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT)
299+
.queryFetcher("greeting", (env) ->
300+
Mono.just("Hello")
301+
.delayElement(Duration.ofSeconds(1))
302+
.doOnCancel(() -> dataFetcherCancelled.set(true))
303+
)
304+
.toGraphQl();
305+
306+
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
307+
Runnable cancelSignal = ContextPropagationHelper.createCancelSignal(input.getGraphQLContext());
308+
309+
CompletableFuture<ExecutionResult> asyncResult = graphQl.executeAsync(input);
310+
cancelSignal.run();
311+
await().atMost(Duration.ofSeconds(2)).until(dataFetcherCancelled::get);
312+
}
313+
314+
@Test
315+
@Disabled("until https://github.com/spring-projects/spring-graphql/issues/1171")
316+
void cancelFluxDataFetcherWhenRequestCancelled() {
317+
AtomicBoolean dataFetcherCancelled = new AtomicBoolean();
318+
GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT)
319+
.queryFetcher("greeting", (env) ->
320+
Flux.just("Hello")
321+
.delayElements(Duration.ofSeconds(1))
322+
.doOnCancel(() -> dataFetcherCancelled.set(true))
323+
)
324+
.toGraphQl();
325+
326+
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
327+
Runnable cancelSignal = ContextPropagationHelper.createCancelSignal(input.getGraphQLContext());
328+
329+
CompletableFuture<ExecutionResult> asyncResult = graphQl.executeAsync(input);
330+
cancelSignal.run();
331+
await().atMost(Duration.ofSeconds(2)).until(dataFetcherCancelled::get);
332+
}
333+
293334
@Test
294335
void returnAbortExecutionForBlockingDataFetcherWhenRequestCancelled() throws Exception {
295336
GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT)
296337
.queryFetcher("greeting", (env) -> "Hello")
297338
.toGraphQl();
298339

299340
ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build();
300-
input.cancel();
341+
Runnable cancelSignal = ContextPropagationHelper.createCancelSignal(input.getGraphQLContext());
342+
cancelSignal.run();
301343
ExecutionResult result = graphQl.executeAsync(input).get();
302344

303345
assertThat(result.getErrors()).hasSize(1);
304346
assertThat(result.getErrors().get(0)).isInstanceOf(AbortExecutionException.class)
305-
.extracting("message").asString().isEqualTo("Execution has been asked to be cancelled");
347+
.extracting("message").asString().isEqualTo("GraphQL request has been cancelled by the client.");
306348
}
307349

308350
@Test
@@ -317,10 +359,12 @@ void cancelFluxDataFetcherSubscriptionWhenRequestCancelled() throws Exception {
317359
.toGraphQl();
318360

319361
ExecutionInput input = ExecutionInput.newExecutionInput().query("subscription { greetings }").build();
362+
Runnable cancelSignal = ContextPropagationHelper.createCancelSignal(input.getGraphQLContext());
363+
320364
ExecutionResult executionResult = graphQl.executeAsync(input).get();
321-
input.cancel();
322-
StepVerifier.create(ResponseHelper.forSubscription(executionResult))
323-
.verifyError(AbortExecutionException.class);
365+
ResponseHelper.forSubscription(executionResult).subscribe();
366+
cancelSignal.run();
367+
324368
await().atMost(Duration.ofSeconds(2)).until(dataFetcherCancelled::get);
325369
assertThat(dataFetcherCancelled).isTrue();
326370
}

spring-graphql/src/test/java/org/springframework/graphql/execution/DefaultExecutionGraphQlServiceTests.java

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222

2323
import graphql.ErrorType;
2424
import org.dataloader.DataLoaderRegistry;
25+
import org.junit.jupiter.api.Disabled;
2526
import org.junit.jupiter.api.Test;
2627
import reactor.core.publisher.Flux;
2728
import reactor.core.publisher.Mono;
2829
import reactor.test.StepVerifier;
2930

3031
import org.springframework.graphql.Author;
3132
import org.springframework.graphql.Book;
32-
import org.springframework.graphql.BookSource;
3333
import org.springframework.graphql.ExecutionGraphQlRequest;
3434
import org.springframework.graphql.ExecutionGraphQlResponse;
3535
import org.springframework.graphql.GraphQlSetup;
@@ -83,24 +83,20 @@ void shouldHandleGraphQlErrors() {
8383
}
8484

8585
@Test
86-
void cancellationSupport() throws Exception {
87-
AtomicBoolean called = new AtomicBoolean();
88-
Mono<Book> bookMono = Mono.just(BookSource.getBookWithoutAuthor(1L))
89-
.delayElement(Duration.ofSeconds(1));
90-
91-
Mono<ExecutionGraphQlResponse> execution = GraphQlSetup.schemaResource(BookSource.schema)
92-
.queryFetcher("bookById", (env) -> bookMono)
93-
.dataFetcher("Book", "author", (env) -> {
94-
called.set(true);
95-
return BookSource.getAuthor(1L);
96-
})
86+
@Disabled("until https://github.com/spring-projects/spring-graphql/issues/1171")
87+
void cancellationSupport() {
88+
AtomicBoolean cancelled = new AtomicBoolean();
89+
Mono<String> greetingMono = Mono.just("hi")
90+
.delayElement(Duration.ofSeconds(3))
91+
.doOnCancel(() -> cancelled.set(true));
92+
93+
Mono<ExecutionGraphQlResponse> execution = GraphQlSetup.schemaContent("type Query { greeting: String }")
94+
.queryFetcher("greeting", (env) -> greetingMono)
9795
.toGraphQlService()
96+
.execute(TestExecutionRequest.forDocument("{ greeting }"));
9897

99-
.execute(TestExecutionRequest.forDocument("{ bookById(id: 1) { author { firstName } } }"));
100-
101-
StepVerifier.create(execution).thenAwait(Duration.ofMillis(500)).thenCancel().verify();
102-
Thread.sleep(1000);
103-
assertThat(called).isFalse();
98+
StepVerifier.create(execution).thenCancel().verify();
99+
assertThat(cancelled).isTrue();
104100
}
105101

106102
}

0 commit comments

Comments
 (0)