grpc-ecosystem / grpc-spring

Spring Boot starter module for gRPC framework.
https://grpc-ecosystem.github.io/grpc-spring/
Apache License 2.0
3.53k stars 829 forks source link

@GrpcClient and @GrpcClientBean doesn't work without GrpcService #640

Open stillya opened 2 years ago

stillya commented 2 years ago

I have two microservices with common-lib and lib for proto-definition and my grpc client located at common-lib, but it only works with first microservice, where I have GrpcService, in second I haven't grpc client, no such bean definition.

I tried @GrpcClient, @GrpcClientBean and it doesn't work. I checked dependency graph, both have grpc-client dep, both have the same version of spring. It so strange, I added breakpoint on GrpcClientBeanPostProcessor and in second microservice I don't go there, but post processor in classpath. Have any ideas about it?

Both microservices and lib written on Kotlin.

ST-DDT commented 2 years ago

Does injecting any other bean work?

stillya commented 2 years ago

Does injecting any other bean work?

Yes, everything else works fine.

ST-DDT commented 2 years ago

Then please create a @GrpcClientBean on a @Configuration class and inject the required stub or channel.

That way you you have the bean in your context and since injection works, it should work there as well.

If that does not work, there must be something else going on. Is your bean a special bean like a spring infrastructure bean? Could you provide an example project that we can use to reproduce the error?

stillya commented 2 years ago

Then please create a @GrpcClientBean on a @Configuration class and inject the required stub or channel. I do exactly this thing.

I created repository which pretty close to my project, where this problem reproducing.

This is common spring project with common-proto(lib where placed some common proto definitions), lib-commons(common lib for all microservices) and two microservices, in auth-service everything is working fine, but ms-config-constructor(it has own api-lib) fail with error on required bean not found.

@GrpcClientBean placed in lib-commons dataflow.commons.configuration.CommonConfiguration and inejcts in dataflow.commons.security.AuthorizationGrpcClient. By the way, this client injects in controllers in AuthService and ConfigConstructor just for testing purposes.

ST-DDT commented 2 years ago

I try to look into this this weekend.

ST-DDT commented 2 years ago

Sorry, I didn't have time this weekend. I will try to look into it soon.

stillya commented 2 years ago

It's ok. By the way, thank you for spending your time on this.

stillya commented 2 years ago

Hello, haven't had time to look on this bug(or not) yet?

a-simeshin commented 2 years ago

Hello, haven't had time to look on this bug(or not) yet?

Hey! I tried to investigate the problem.

We have:

  1. Configuration in the module lib-commons with @GrpcClientBean
    @GrpcClientBean(
    beanName = "authStub",
    clazz = AuthorizationServiceGrpc.AuthorizationServiceBlockingStub::class,
    client = GrpcClient("AuthClient")
    )
    @Configuration
    class CommonConfiguration {
    }
  2. Component in the module lib-commons
    @Component
    class AuthorizationGrpcClient(private val authStub: AuthorizationServiceGrpc.AuthorizationServiceBlockingStub) {
    }
  3. @RestController in another module ms-config-constructor with dependency lib-commons and access to gRPC bean client with getting from the spring context

    @RestController
    @RequestMapping(path = ["/configs"])
    class ConfigRestController(val auth: AuthorizationGrpcClient) {
    
    @Autowired
    private lateinit var authStub: AuthorizationServiceGrpc.AuthorizationServiceBlockingStub
    
    @PostConstruct
    fun beanPresentLogNotification() {
        println("gRPC bean client successfully autowired to another module [$authStub]")
        println("Bean wrapper with gRPC bean client successfully autowired to another module [$auth]")
    }
    }
  4. Logs
    Parameter 0 of constructor in dataflow.commons.security.AuthorizationGrpcClient required a bean of type 'com.dataflow.commons.AuthorizationServiceGrpc$AuthorizationServiceBlockingStub' that could not be found.


I suppose something strange happen with @Component constructor and even @Autowired injection in the SAME module, but if you'll try to manually create AuthorizationGrpcClient bean like

@GrpcClientBean(
    beanName = "authStub",
    clazz = AuthorizationServiceGrpc.AuthorizationServiceBlockingStub::class,
    client = GrpcClient("AuthClient")
)
@Configuration
class CommonConfiguration {

    @Bean
    fun decoder(): Base64.Decoder = Base64.getDecoder()

    @Bean
    fun authorizationGrpcClient(authStub: AuthorizationServiceGrpc.AuthorizationServiceBlockingStub): AuthorizationGrpcClient {
        return AuthorizationGrpcClient(authStub);
    }
}

you will get in logs

gRPC bean client successfully autowired to another module [com.dataflow.commons.AuthorizationServiceGrpc$AuthorizationServiceBlockingStub@214fba74]
Bean wrapper with gRPC bean client successfully autowired to another module [dataflow.commons.security.AuthorizationGrpcClient@252c6cdb]

@stillya hope manually bean construction for kt classes would be helpful, at least it works. Another possible workaround is to develop java classes that requires @Grpclient bean instead of kt classes. Both options definitely not great at all :(



@ST-DDT sadly GrpcClientBeanPostProcessor have 0 reaction for both method and field injections in kt class. Should we use something like runtimeOnly 'org.jetbrains.kotlin:kotlin-reflect:1.2.41' to have proper access?

  1. Breakpoint somewhere in processFields and 0 reaction for
@Component
class AuthorizationGrpcClient {

    //bean not found
    @Autowired
    lateinit var authStub: AuthorizationServiceGrpc.AuthorizationServiceBlockingStub
}
  1. Breakpoint somewhere in processMethods and 0 reaction for
@Component
class AuthorizationGrpcClient(private val authStub: AuthorizationServiceGrpc.AuthorizationServiceBlockingStub) {
     //bean not found
...
}

UPD This is kt too, but here field injection works properly because @GrpcBean authStub and AuthorizationGrpcClient already exists in the spring context

@RestController
@RequestMapping(path = ["/configs"])
class ConfigRestController(val auth: AuthorizationGrpcClient) {

    @Autowired
    private lateinit var authStub: AuthorizationServiceGrpc.AuthorizationServiceBlockingStub

    @PostConstruct
    fun beanPresentLogNotification() {
        println("gRPC bean client successfully autowired to another module [$authStub]")
        println("Bean wrapper with gRPC bean client successfully autowired to another module [$auth]")
    }
}
stillya commented 2 years ago

@a-simeshin your workaround isn't working if I wanna more than one grpcClientBean to create another bean, i.e.

@Configuration
@GrpcClientBeans(
    value = [
        GrpcClientBean(
            beanName = "authStub",
            clazz = AuthorizationServiceGrpc.AuthorizationServiceBlockingStub::class,
            client = GrpcClient("AuthorizationService")
        ),
        GrpcClientBean(
            beanName = "permissionStub",
            clazz = PermissionServiceGrpc.PermissionServiceBlockingStub::class,
            client = GrpcClient("PermissionService")
        )
    ]
)
class TestConfiguration {

    @Bean
    fun authorizationGrpcClient(
        authStub: AuthorizationServiceGrpc.AuthorizationServiceBlockingStub,
        permissionStub: PermissionServiceGrpc.PermissionServiceBlockingStub
    ): AuthorizationGrpcClient {
        return AuthorizationGrpcClient(authStub, permissionStub)
    }
}

We'll haven't even first stub-bean, which previously work. I don't know how to work around this.

Sun-Phil-Kwon-Miro commented 2 years ago

hi team any update on this?

ST-DDT commented 2 years ago

This looks like a bug in our code. I have identified the cause, but I haven't had time to fix the bug yet. The relevant code needs to be moved from GrpcClientBeanPostProcessor to a BeanFactoryPostProcessor. I have some time off work in December, so the next release with a fix for this will probably released then.

ST-DDT commented 2 years ago

For now, you can only try to circumvent the problem by adding @DependsOn(YourConfigBean)

dim-at-ocp commented 1 year ago

Hey @ST-DDT and team! First of all, thanks for excellent work on on this project!

But do you guys think there is a chance you could look into this problem any time soon? Maybe, as a 2.14.1 patch release or something?


@ST-DDT mentioned that you've identified the culprit:

The relevant code needs to be moved from GrpcClientBeanPostProcessor to a BeanFactoryPostProcessor.

And I tend to think so as well.

Spring hides the actual root cause, but if you put a breakpoint at GrpcClientBeanPostProcessor.java#L191 you'd see that the real issue is with the GrpcClientBeanPostProcessor trying to fetch a couple of Spring beans (GrpcClientBeanPostProcessor.java#L238) - NameResolverRegistration and GrpcChannelFactory - yet before the context has been refreshed (i.e. finished initialization) :

java.lang.IllegalStateException: Failed to create channel: <grpc client name here>
    at net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor.processInjectionPoint(GrpcClientBeanPostProcessor.java:218)
    at net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor.processGrpcClientBeansAnnotations(GrpcClientBeanPostProcessor.java:188)
    at net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor.postProcessBeforeInitialization(GrpcClientBeanPostProcessor.java:129)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:440)
...
Caused by: java.lang.IllegalStateException: org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext@42b02722 has not been refreshed yet
    at org.springframework.context.support.AbstractApplicationContext.assertBeanFactoryActive(AbstractApplicationContext.java:1141)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1171)
    at net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor.getChannelFactory(GrpcClientBeanPostProcessor.java:238)
    at net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor.processInjectionPoint(GrpcClientBeanPostProcessor.java:213)

And I don't see an easy way around this, e.g. enforcing specific sequence / timing or deferring injection till after context refresh. Neither I see a way to apply the @DependsOn trick mentioned above. Annotation expects a bean name, not a configuration class or anything (I'll tinker with it a little more though).

But meanwhile the client side of the project doesn't seem to work for us, which is a real bummer - the library has proven to be great so far :)

Any thoughts on a fix timing or a functional workaround are highly appreciated!

panghy commented 1 year ago

This issue is manifesting in a number of ways (we just ran into this issue when a dependency needed a gRPC stub wiring (that the parent module declares via GrpcClient). Having the component in the dependency declare @DependsOn (against the parent module's @Configuration that includes @GrpcClient works but it's obviously not ideal.

ST-DDT commented 1 year ago

I plan on fixing this, but I currently don't have any free time. 😢

krmahadevan commented 1 year ago

@ST-DDT - I have also hit this issue and looking forward to getting this issue fixed.

krmahadevan commented 1 year ago

@stillya - I tried reproducing the issue with the sample repo that you had shared and I noticed that with the below small changes (version bump and adding an annotation), the project comes up (I had to fix the flyway locations so that the app comes up)

Here's the patch

diff --git a/build.gradle b/build.gradle
index ebf6f4d..f0fe34a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,6 @@
 plugins {
     id 'java'
+    id 'idea'
 }

 group 'com.campaign'
diff --git a/gradle.properties b/gradle.properties
index 4453b2e..5e17cd7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -28,7 +28,7 @@ lombokVersion=1.18.22
 postgresVersion=42.2.14
 mapStructVersion=1.5.0.Beta2
 kotlinLoggingVersion=2.1.20
-grpcStarterVersion=2.13.1.RELEASE
+grpcStarterVersion=2.14.0.RELEASE
 protocVersion=4.0.0-rc-2
 protocGenJavaVersion=1.42.1
 testContainersVersion=1.16.2
@@ -41,4 +41,4 @@ groovyVersion=3.0.9
 jookVersion=3.15.7
 springdocOpenApiVersion=1.6.5
 springdocOpenApiKotlinVersion=1.6.5
-mockitoInlineVersion=4.3.1
\ No newline at end of file
+mockitoInlineVersion=4.3.1
diff --git a/lib-commons/src/main/kotlin/dataflow/commons/security/AuthorizationGrpcClient.kt b/lib-commons/src/main/kotlin/dataflow/commons/security/AuthorizationGrpcClient.kt
index 28dc104..239f2e2 100644
--- a/lib-commons/src/main/kotlin/dataflow/commons/security/AuthorizationGrpcClient.kt
+++ b/lib-commons/src/main/kotlin/dataflow/commons/security/AuthorizationGrpcClient.kt
@@ -4,10 +4,11 @@ import com.dataflow.commons.AuthorizationServiceGetRuleByNameRequest
 import com.dataflow.commons.AuthorizationServiceGrpc
 import dataflow.commons.dtos.abac.AbacRule
 import dataflow.commons.mappers.toDto
+import net.devh.boot.grpc.client.inject.GrpcClient
 import org.springframework.stereotype.Component

 @Component
-class AuthorizationGrpcClient(val authStub: AuthorizationServiceGrpc.AuthorizationServiceBlockingStub) {
+class AuthorizationGrpcClient (@GrpcClient("AuthClient") val authStub: AuthorizationServiceGrpc.AuthorizationServiceBlockingStub) {

     fun getRuleByName(name: String): AbacRule {
         val request: AuthorizationServiceGetRuleByNameRequest =
@@ -17,4 +18,4 @@ class AuthorizationGrpcClient(val authStub: AuthorizationServiceGrpc.Authorizati
         return authStub.getRuleByName(request).rule.toDto()
     }

-}
\ No newline at end of file
+}
diff --git a/ms-config-constructor/flyway.conf b/ms-config-constructor/flyway.conf
index 4f60615..b4954d3 100644
--- a/ms-config-constructor/flyway.conf
+++ b/ms-config-constructor/flyway.conf
@@ -2,5 +2,5 @@ flyway.user=postgres
 flyway.password=postgres
 flyway.schemas=public
 flyway.url=jdbc:postgresql://localhost:5432/df_config_constructor?currentSchema=public&ApplicationName=config_constructor
-flyway.locations=filesystem:src/main/resources/db/migration
-flyway.baselineOnMigrate=true
\ No newline at end of file
+#flyway.locations=filesystem:src/main/resources/db/migration
+flyway.baselineOnMigrate=true
stillya commented 1 year ago

@krmahadevan yeah, but my point is about to define ClientBean in configuration and use stub as a regular bean.