-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Hibernate Validator with Kotlin and RestEasy - Custom Validations executed twice #31595
Comments
/cc @evanchooly (kotlin), @geoand (kotlin), @gsmet (hibernate-validator), @yrodiere (hibernate-validator) |
Could you please assemble a simple project reproducing the issue? Also would you happen to know if the same issue happens in plain Java? |
Hello @gsmet, thanks a lot for the quick response. I added a repo here => https://github.com/cfinkelstein/kotlin-reasy-validator-bug |
Did not test with Java so far. |
I added some tests "https://github.com/cfinkelstein/kotlin-reasy-validator-bug". The strange thing is that the bug occurs not for every run. It seems that it works if I execute multiple calls but not if I just trigger a single request. Tested it also with a http client. Where it sometimes worked and and sometimes not. Pls checkout the attached video. screen_recording_kotlin_resteasy.mp4 |
Hey @gsmet, I changed now all my integrations to Uni instead of the default suspend handling:
It seems to be no difference in terms of execution time/performance etc. |
I tried your sample with the latest Quarkus version and the validation was only executed once as expected |
Please ignore my previous comment as I was able to reproduce the problem |
The reason this happens is that CDI interceptors get invoked every time the continuation is invoked. @Ladicek IIRC, you had to deal with a similar problem in Fault Tolerance, no? Do we have a good way of disabling interceptors with some API call (I am pretty sure I can distinguish between the first and subsequent calls, so if I had an API to call to disable the interceptors, the issue would be solved)? |
I vaguely recall stumbling upon interceptors on suspending functions, but not in the FT context. If I remember correctly, the Kotlin compiler emits a synthetic method that is called repeatedly, and calls it from the original. I think (but not sure) that the Kotlin compiler also copies annotations from the original method to the synthetic method that implements the state machine, in which case the synthetic method is intercepted on each call. We have some code in place to not intercept certain synthetic methods, but I don't recall from the top of my head. I'd first look at the interceptor binding annotations. |
Thanks for the input. Let's see what @mkouba has to say as well :) |
From what I recall, that's true. All the annotations are copied over which is almost certainly confusing things. We could likely filter out any synthetic methods or, and my memory is foggy here, any methods with coroutine types in the signatures. I know the kotlin compiler does some fancy footwork with |
We do not intercept non-bridge synthetic methods since quarkus 3.0.0.Alpha4; the change got introduced in this pull request. Maybe we should backport the specific commit in 2.16? |
I think we got a little sidetracked here, so let me demonstrate with some code exactly what the issue is. Here is a JAX-RS Resource: @Path("/users")
class UserController {
@GET
@Path("/nb/{userId}")
suspend fun userById(
@UUIDPathElement
@PathParam("userId") userId: String): Response {
println("test")
delay(100)
return Response.status(200).build()
}
} Note the use of Now, in RESTEasy Reactive we initiate the Coroutine call like this: public class UserController$quarkuscoroutineinvoker$userById_0506e3a2a56eb32a9c3d1b74fd72a6e695e67551 implements CoroutineEndpointInvoker {
public Object invokeCoroutine(Object var1, Object[] var2, Continuation var3) {
Object var4 = var2[0];
return ((UserController)var1).userById((String)var4, var3);
}
} Now every time the coroutine is resumed (so in the example above after So my question is how can I invoke |
You can't. |
You cannot "invoke a method without interceptors", that's not how it works. BTW, I recalled that you're right, I did have the same problem in Fault Tolerance. This is how I solved it: https://github.com/smallrye/smallrye-fault-tolerance/blob/main/implementation/autoconfig/core/src/main/java/io/smallrye/faulttolerance/autoconfig/KotlinSupport.java
|
Well, then we need to add something, because the current behavior is problematic for Kotlin |
+1 |
Well, for each intercepted bean we generate an intercepted subclass that represents the contextual instance of the bean. And for each intercepted method we add a synthetic method that forwards invocation to the superclass - the original bean class. For example, if you bean class declares a method
And this method is called when the last interceptor in the chain calls Now this is of course an implementation detail but if we standardize the name of the generated synthetic method... |
@mkouba sorry, I am missing how that helps in this case. |
|
Ah okay, I now see that is public in the generated I propose we do standardize the name (or even have some kind of utility that will allow code that generates other code to know the name of the method.) I could certainly just call it myself assuming what the name will be, but that sounds pretty brittle :) WDYT? |
I don't think calling the |
So what do you propose instead @Ladicek ? |
A simple annotation transformation should work. Additionally, if the generated method is synthetic (which I don't remember and didn't check now), the issue should fix itself in Quarkus 3 :-) |
You mean the |
No, I mean transform (remove) annotations on the We could also backport the change for not intercepting synthetic method to Quarkus 2.16, I guess. |
Interesting, I don't see |
Oh this is an interesting one! This class @ApplicationScoped
@Path("/users")
class UserController {
@GET
@Path("/nb/{userId}")
suspend fun userById(@UUIDPathElement @PathParam("userId") userId: String): Response {
delay(100)
return Response.status(200).build()
}
} is Quarkus then removes the If you make the class Making the class and method |
Wow, that's neat :). Too bad we can't do much here then except propose the use of |
I think we should probably bail out on |
Well, it works in some cases, so maybe we should just log a warning? |
Yeah, it works when the I mean, I don't want to bail out on all |
Only method-level bindings are considered and private static methods are ignored; see the docs. |
Well, the This https://github.com/quarkusio/quarkus/blob/main/extensions/hibernate-validator/deployment/src/main/java/io/quarkus/hibernate/validator/deployment/MethodValidatedAnnotationsTransformer.java seems to be the annotation transformation that adds the interceptor binding to methods that have validator annotations, and it seems to skip |
We have tests that prove(?) otherwise. From what I can tell for the RR case, the problem only occurs when there are interceptors |
Well exactly, that's what I'm talking about :-) Sorry for not being clear -- I'm proposing to unconditionally disallow interception (and decoration) of |
+1 for that |
One point I missed here. I could not see this issue while using the validations which are provided from the library like @NotNull. Is there any difference in the way how those are handled? |
There's no difference. I'm pretty confident that the classes using the other validations were different in some other regard -- were |
I have an initial implementation of noninterceptable |
Yeah, the only tests I can think of are in RR, HR and RM |
Describe the bug
Hello there,
I recognized that custom validators will be executed twice in Kotlin with reactive context.
Endpoint:
Expected behavior
Following GET Request:
Should deliver HTTP 200
Actual behavior
Following GET Request:
Delivers HTTP 400 as the Validator will be called a second time with 'null' for uuid.
How to Reproduce?
See above.
Output of
uname -a
orver
Linux cfn7655 5.19.0-35-generic #36~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Feb 17 15:17:25 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
Output of
java -version
openjdk version "19.0.1" 2022-10-18
OpenJDK Runtime Environment GraalVM CE 22.3.0 (build 19.0.1+10-jvmci-22.3-b08)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.0 (build 19.0.1+10-jvmci-22.3-b08, mixed mode, sharing)
GraalVM version (if different from Java)
No response
Quarkus version or git rev
No response
Build tool (ie. output of
mvnw --version
orgradlew --version
)Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Additional information
No response
The text was updated successfully, but these errors were encountered: