Repository navigation
Add gRPC request scope support (#392) #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1484c14
2c2a10c
2b79c4e
8434508
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /* | ||
| * Copyright 2024-present the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.springframework.grpc.server.annotation; | ||
|
|
||
| import java.lang.annotation.Documented; | ||
| import java.lang.annotation.ElementType; | ||
| import java.lang.annotation.Retention; | ||
| import java.lang.annotation.RetentionPolicy; | ||
| import java.lang.annotation.Target; | ||
|
|
||
| import org.springframework.context.annotation.Scope; | ||
| import org.springframework.context.annotation.ScopedProxyMode; | ||
| import org.springframework.core.annotation.AliasFor; | ||
|
|
||
| @Target({ ElementType.TYPE, ElementType.METHOD }) | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| @Documented | ||
| @Scope("grpcRequest") | ||
| public @interface GrpcRequestScope { | ||
|
|
||
| @AliasFor(annotation = Scope.class) | ||
| ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| /* | ||
| * Copyright 2024-present the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.springframework.grpc.server.scope; | ||
|
|
||
| import java.lang.annotation.Documented; | ||
| import java.lang.annotation.ElementType; | ||
| import java.lang.annotation.Retention; | ||
| import java.lang.annotation.RetentionPolicy; | ||
| import java.lang.annotation.Target; | ||
|
|
||
| import org.springframework.context.annotation.Import; | ||
|
|
||
| @Target(ElementType.TYPE) | ||
| @Retention(RetentionPolicy.RUNTIME) | ||
| @Documented | ||
| @Import(GrpcScopeRegistrar.class) | ||
| public @interface EnableGrpcScope { | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /* | ||
| * Copyright 2024-present the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.springframework.grpc.server.scope; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| import org.springframework.beans.factory.ObjectFactory; | ||
| import org.springframework.beans.factory.config.Scope; | ||
|
|
||
| /** | ||
| * A {@link Scope} implementation for gRPC request-scoped beans. | ||
| * | ||
| * @author Fernando Rodrigues Ripol | ||
| */ | ||
| public class GrpcRequestScope implements Scope { | ||
|
|
||
| private static final ThreadLocal<Map<String, Object>> threadLocalScope = ThreadLocal.withInitial(HashMap::new); | ||
|
|
||
| @Override | ||
| public Object get(String name, ObjectFactory<?> objectFactory) { | ||
| Map<String, Object> scope = threadLocalScope.get(); | ||
| Object bean = scope.get(name); | ||
| if (bean == null) { | ||
| bean = objectFactory.getObject(); | ||
| scope.put(name, bean); | ||
| } | ||
| return bean; | ||
| } | ||
|
|
||
| @Override | ||
| public Object remove(String name) { | ||
| Map<String, Object> scope = threadLocalScope.get(); | ||
| return scope.remove(name); | ||
| } | ||
|
|
||
| @Override | ||
| public void registerDestructionCallback(String name, Runnable callback) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scoped beans never get their |
||
| } | ||
|
|
||
| @Override | ||
| public Object resolveContextualObject(String key) { | ||
| return null; | ||
| } | ||
|
|
||
| @Override | ||
| public String getConversationId() { | ||
| return null; | ||
| } | ||
|
|
||
| public static void clear() { | ||
| threadLocalScope.remove(); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /* | ||
| * Copyright 2024-present the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.springframework.grpc.server.scope; | ||
|
|
||
| import io.grpc.ForwardingServerCallListener; | ||
| import io.grpc.Metadata; | ||
| import io.grpc.ServerCall; | ||
| import io.grpc.ServerCallHandler; | ||
| import io.grpc.ServerInterceptor; | ||
|
|
||
| /** | ||
| * A {@link ServerInterceptor} to manage the lifecycle of gRPC request-scoped beans. | ||
| * | ||
| * @author Fernando Rodrigues Ripol | ||
| */ | ||
| public class GrpcRequestScopeServerInterceptor implements ServerInterceptor { | ||
|
|
||
| @Override | ||
| public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, | ||
| ServerCallHandler<ReqT, RespT> next) { | ||
| ServerCall.Listener<ReqT> delegate = next.startCall(call, headers); | ||
| return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(delegate) { | ||
| @Override | ||
| public void onComplete() { | ||
| try { | ||
| super.onComplete(); | ||
| } | ||
| finally { | ||
| GrpcRequestScope.clear(); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onCancel() { | ||
| try { | ||
| super.onCancel(); | ||
| } | ||
| finally { | ||
| GrpcRequestScope.clear(); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /* | ||
| * Copyright 2024-present the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.springframework.grpc.server.scope; | ||
|
|
||
| import org.springframework.beans.factory.support.BeanDefinitionRegistry; | ||
| import org.springframework.beans.factory.support.RootBeanDefinition; | ||
| import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; | ||
| import org.springframework.core.type.AnnotationMetadata; | ||
|
|
||
| public class GrpcScopeRegistrar implements ImportBeanDefinitionRegistrar { | ||
|
|
||
| @Override | ||
| public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { | ||
| if (!registry.containsBeanDefinition("grpcRequestScope")) { | ||
| registry.registerBeanDefinition("grpcRequestScope", new RootBeanDefinition(GrpcRequestScope.class)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defining a |
||
| } | ||
| if (!registry.containsBeanDefinition("grpcRequestScopeServerInterceptor")) { | ||
| registry.registerBeanDefinition("grpcRequestScopeServerInterceptor", | ||
| new RootBeanDefinition(GrpcRequestScopeServerInterceptor.class)); | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| /* | ||
| * Copyright 2024-present the original author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.springframework.grpc.server.annotation; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import org.springframework.context.annotation.AnnotationConfigApplicationContext; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.grpc.server.scope.EnableGrpcScope; | ||
|
|
||
| class GrpcRequestScopeTests { | ||
|
|
||
| @Test | ||
| void testGrpcRequestScopeRegistration() { | ||
| try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfig.class)) { | ||
| assertThat(context.containsBean("testBean")).isTrue(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| } | ||
|
|
||
| @Configuration | ||
| @EnableGrpcScope | ||
| static class TestConfig { | ||
|
|
||
| @Bean | ||
| @GrpcRequestScope | ||
| public TestBean testBean() { | ||
| return new TestBean(); | ||
| } | ||
|
|
||
| } | ||
|
|
||
| static class TestBean { | ||
|
|
||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A
ThreadLocalmakes the scope per thread rather than per call. gRPC runs a call's listener callbacks on its executor, so successive callbacks of one call can land on different threads, and a service that hands work to another thread gets a different instance there: in my test, a lookup from a worker thread viaContext.current().wrap(...)returned a second instance, and that instance was still returned on the worker thread after the call had completed, sinceclear()only runs on the completing thread. Keying the beans on the call'sio.grpc.Contextinstead (aContext.Key, attached withContexts.interceptCall(...)in the interceptor) gives one instance per call on every thread the context is propagated to; that is what the net.devh implementation does. Also,withInitial(HashMap::new)means a lookup outside a call quietly creates a bean in a thread-wide map instead of failing withScopeNotActiveExceptionlike@RequestScopedoes; the test for that passed without throwing.