Skip to content

Commit a5ec819

Browse files
committed
Merge branch '1.3.x'
2 parents cf1c99f + a6c69eb commit a5ec819

14 files changed

+202
-200
lines changed

spring-graphql-docs/modules/ROOT/pages/request-execution.adoc

-8
Original file line numberDiff line numberDiff line change
@@ -417,14 +417,6 @@ include-code::HttpTimeoutConfiguration[]
417417
For more transport-specific timeouts, there are dedicated properties on the handler implementations like
418418
`GraphQlWebSocketHandler` and `GraphQlSseHandler`.
419419

420-
NOTE: While reactive data fetchers are cancelled automatically, this cannot be done for others
421-
as there is no consistent way to cancel processing. In this case, controller methods can get
422-
the cancellation signal from a `Mono` in the GraphQL context and manually cancel work.
423-
424-
Here is an example of using the cancellation signal to abort processing inside a controller method:
425-
426-
include-code::TimeoutController[tag=cancel,indent=0]
427-
428420

429421
[[execution.reactivedatafetcher]]
430422
== Reactive `DataFetcher`

spring-graphql-docs/src/main/java/org/springframework/graphql/docs/execution/timeout/TimeoutController.java

-56
This file was deleted.

spring-graphql/src/main/java/org/springframework/graphql/ExecutionGraphQlRequest.java

-6
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,6 @@
3636
*/
3737
public interface ExecutionGraphQlRequest extends GraphQlRequest {
3838

39-
/**
40-
* Key of the GraphQL context entry that holds a {@code Mono<Void>} that completes
41-
* when the inbound GraphQL request is cancelled at the transport level.
42-
*/
43-
String CANCEL_PUBLISHER_CONTEXT_KEY = ExecutionGraphQlRequest.class.getName() + ".cancelled";
44-
4539
/**
4640
* Return the transport assigned id for the request that in turn sets
4741
* {@link ExecutionInput.Builder#executionId(ExecutionId) executionId}.

spring-graphql/src/main/java/org/springframework/graphql/data/method/InvocableHandlerMethodSupport.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import org.springframework.core.CoroutinesUtils;
3333
import org.springframework.core.KotlinDetector;
3434
import org.springframework.data.util.KotlinReflectionUtils;
35-
import org.springframework.graphql.execution.ContextSnapshotFactoryHelper;
35+
import org.springframework.graphql.execution.ContextPropagationHelper;
3636
import org.springframework.lang.Nullable;
3737
import org.springframework.util.Assert;
3838

@@ -153,7 +153,7 @@ private CompletableFuture<?> adaptCallable(
153153
CompletableFuture<Object> future = new CompletableFuture<>();
154154
this.executor.execute(() -> {
155155
try {
156-
ContextSnapshot snapshot = ContextSnapshotFactoryHelper.captureFrom(graphQLContext);
156+
ContextSnapshot snapshot = ContextPropagationHelper.captureFrom(graphQLContext);
157157
Object value = snapshot.wrap((Callable<?>) result).call();
158158
future.complete(value);
159159
}

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

+7-15
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import reactor.core.publisher.Flux;
4040
import reactor.core.publisher.Mono;
4141

42-
import org.springframework.graphql.ExecutionGraphQlRequest;
4342
import org.springframework.lang.Nullable;
4443
import org.springframework.util.Assert;
4544

@@ -80,32 +79,30 @@ private ContextDataFetcherDecorator(
8079
public Object get(DataFetchingEnvironment env) throws Exception {
8180

8281
GraphQLContext graphQlContext = env.getGraphQlContext();
83-
ContextSnapshotFactory snapshotFactory = ContextSnapshotFactoryHelper.getInstance(graphQlContext);
82+
ContextSnapshotFactory snapshotFactory = ContextPropagationHelper.getInstance(graphQlContext);
8483
ContextSnapshot snapshot = (env.getLocalContext() instanceof GraphQLContext localContext) ?
8584
snapshotFactory.captureFrom(graphQlContext, localContext) :
8685
snapshotFactory.captureFrom(graphQlContext);
8786

88-
Mono<Void> cancelledRequest = graphQlContext.get(ExecutionGraphQlRequest.CANCEL_PUBLISHER_CONTEXT_KEY);
89-
9087
Object value = snapshot.wrap(() -> this.delegate.get(env)).call();
9188

9289
if (value instanceof DataFetcherResult<?> dataFetcherResult) {
93-
Object adapted = updateValue(dataFetcherResult.getData(), snapshot, cancelledRequest);
90+
Object adapted = updateValue(dataFetcherResult.getData(), snapshot, graphQlContext);
9491
value = DataFetcherResult.newResult()
9592
.data(adapted)
9693
.errors(dataFetcherResult.getErrors())
9794
.localContext(dataFetcherResult.getLocalContext()).build();
9895
}
9996
else {
100-
value = updateValue(value, snapshot, cancelledRequest);
97+
value = updateValue(value, snapshot, graphQlContext);
10198
}
10299

103100
return value;
104101
}
105102

106103
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
107104
private @Nullable Object updateValue(
108-
@Nullable Object value, ContextSnapshot snapshot, @Nullable Mono<Void> cancelledRequest) {
105+
@Nullable Object value, ContextSnapshot snapshot, GraphQLContext graphQlContext) {
109106

110107
if (value == null) {
111108
return null;
@@ -121,19 +118,14 @@ public Object get(DataFetchingEnvironment env) throws Exception {
121118
return this.subscriptionExceptionResolver.resolveException(exception)
122119
.flatMap((errors) -> Mono.error(new SubscriptionPublisherException(errors, exception)));
123120
});
124-
if (cancelledRequest != null) {
125-
subscriptionResult = subscriptionResult.takeUntilOther(cancelledRequest);
126-
}
127-
return subscriptionResult.contextWrite(snapshot::updateContext);
121+
return ContextPropagationHelper.bindCancelFrom(subscriptionResult, graphQlContext)
122+
.contextWrite(snapshot::updateContext);
128123
}
129124

130125
value = ReactiveAdapterRegistryHelper.toMonoIfReactive(value);
131126

132127
if (value instanceof Mono<?> mono) {
133-
if (cancelledRequest != null) {
134-
mono = mono.takeUntilOther(cancelledRequest);
135-
}
136-
value = mono.contextWrite(snapshot::updateContext).toFuture();
128+
value = ContextPropagationHelper.bindCancelFrom(mono, graphQlContext).contextWrite(snapshot::updateContext).toFuture();
137129
}
138130

139131
return value;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.execution;
18+
19+
import graphql.GraphQLContext;
20+
import io.micrometer.context.ContextSnapshot;
21+
import io.micrometer.context.ContextSnapshotFactory;
22+
import reactor.core.publisher.Flux;
23+
import reactor.core.publisher.Mono;
24+
import reactor.core.publisher.Sinks;
25+
import reactor.util.context.Context;
26+
import reactor.util.context.ContextView;
27+
28+
import org.springframework.lang.Nullable;
29+
30+
/**
31+
* Helper for propagating context values from and to Reactor and GraphQL contexts.
32+
*
33+
* @author Rossen Stoyanchev
34+
* @author Brian Clozel
35+
* @since 1.3.5
36+
*/
37+
public abstract class ContextPropagationHelper {
38+
39+
private static final ContextSnapshotFactory sharedInstance = ContextSnapshotFactory.builder().build();
40+
41+
private static final String CONTEXT_SNAPSHOT_FACTORY_KEY = ContextPropagationHelper.class.getName() + ".KEY";
42+
43+
private static final String CANCEL_PUBLISHER_KEY = ContextPropagationHelper.class.getName() + ".cancelled";
44+
45+
46+
/**
47+
* Select a {@code ContextSnapshotFactory} instance to use, either the one
48+
* passed in if it is not {@code null}, or a shared, static instance.
49+
* @param factory the candidate factory instance to use if not {@code null}
50+
* @return the instance to use
51+
*/
52+
public static ContextSnapshotFactory selectInstance(@Nullable ContextSnapshotFactory factory) {
53+
if (factory != null) {
54+
return factory;
55+
}
56+
return sharedInstance;
57+
}
58+
59+
/**
60+
* Save the {@code ContextSnapshotFactory} in the given {@link Context}.
61+
* @param factory the instance to save
62+
* @param context the context to save the instance to
63+
* @return a new context with the saved instance
64+
*/
65+
public static Context saveInstance(ContextSnapshotFactory factory, Context context) {
66+
return context.put(CONTEXT_SNAPSHOT_FACTORY_KEY, factory);
67+
}
68+
69+
/**
70+
* Save the {@code ContextSnapshotFactory} in the given {@link Context}.
71+
* @param factory the instance to save
72+
* @param context the context to save the instance to
73+
*/
74+
public static void saveInstance(ContextSnapshotFactory factory, GraphQLContext context) {
75+
context.put(CONTEXT_SNAPSHOT_FACTORY_KEY, factory);
76+
}
77+
78+
/**
79+
* Access the {@code ContextSnapshotFactory} from the given {@link ContextView}
80+
* or return a shared, static instance.
81+
* @param contextView the context where the instance is saved
82+
* @return the instance to use
83+
*/
84+
public static ContextSnapshotFactory getInstance(ContextView contextView) {
85+
ContextSnapshotFactory factory = contextView.getOrDefault(CONTEXT_SNAPSHOT_FACTORY_KEY, null);
86+
return selectInstance(factory);
87+
}
88+
89+
/**
90+
* Access the {@code ContextSnapshotFactory} from the given {@link GraphQLContext}
91+
* or return a shared, static instance.
92+
* @param context the context where the instance is saved
93+
* @return the instance to use
94+
*/
95+
public static ContextSnapshotFactory getInstance(GraphQLContext context) {
96+
ContextSnapshotFactory factory = context.get(CONTEXT_SNAPSHOT_FACTORY_KEY);
97+
return selectInstance(factory);
98+
}
99+
100+
/**
101+
* Shortcut to obtain the {@code ContextSnapshotFactory} instance, and to
102+
* capture from the given {@link ContextView}.
103+
* @param contextView the context to capture from
104+
* @return a snapshot from the capture
105+
*/
106+
public static ContextSnapshot captureFrom(ContextView contextView) {
107+
ContextSnapshotFactory factory = getInstance(contextView);
108+
return selectInstance(factory).captureFrom(contextView);
109+
}
110+
111+
/**
112+
* Shortcut to obtain the {@code ContextSnapshotFactory} instance, and to
113+
* capture from the given {@link GraphQLContext}.
114+
* @param context the context to capture from
115+
* @return a snapshot from the capture
116+
*/
117+
public static ContextSnapshot captureFrom(GraphQLContext context) {
118+
ContextSnapshotFactory factory = getInstance(context);
119+
return selectInstance(factory).captureFrom(context);
120+
}
121+
122+
/**
123+
* Create a publisher and store it into the given {@link GraphQLContext}.
124+
* This publisher can then be used to propagate cancel signals to upstream publishers.
125+
* @param context the current GraphQL context
126+
* @since 1.3.5
127+
*/
128+
public static Sinks.Empty<Void> createCancelPublisher(GraphQLContext context) {
129+
Sinks.Empty<Void> requestCancelled = Sinks.empty();
130+
context.put(CANCEL_PUBLISHER_KEY, requestCancelled.asMono());
131+
return requestCancelled;
132+
}
133+
134+
/**
135+
* Bind the source {@link Mono} to the publisher from the given {@link GraphQLContext}.
136+
* The returned {@code Mono} will be cancelled when this publisher completes.
137+
* Subscribers must use the returned {@code Mono} instance.
138+
* @param source the source {@code Mono}
139+
* @param context the current GraphQL context
140+
* @param <T> the type of published elements
141+
* @return the new {@code Mono} that will be cancelled when notified
142+
* @since 1.3.5
143+
*/
144+
public static <T> Mono<T> bindCancelFrom(Mono<T> source, GraphQLContext context) {
145+
Mono<Void> cancelSignal = context.get(CANCEL_PUBLISHER_KEY);
146+
if (cancelSignal != null) {
147+
return source.takeUntilOther(cancelSignal);
148+
}
149+
return source;
150+
}
151+
152+
/**
153+
* Bind the source {@link Flux} to the publisher from the given {@link GraphQLContext}.
154+
* The returned {@code Flux} will be cancelled when this publisher completes.
155+
* Subscribers must use the returned {@code Mono} instance.
156+
* @param source the source {@code Mono}
157+
* @param context the current GraphQL context
158+
* @param <T> the type of published elements
159+
* @return the new {@code Mono} that will be cancelled when notified
160+
* @since 1.3.5
161+
*/
162+
public static <T> Flux<T> bindCancelFrom(Flux<T> source, GraphQLContext context) {
163+
Mono<Void> cancelSignal = context.get(CANCEL_PUBLISHER_KEY);
164+
if (cancelSignal != null) {
165+
return source.takeUntilOther(cancelSignal);
166+
}
167+
return source;
168+
}
169+
170+
}

0 commit comments

Comments
 (0)