2

Spring Boot 3, WebFlux, Kotlin 1.8, coroutines 1.7.

I've got a simple @RestController with a function like:

@PutMapping("/{id}", consumes = [APPLICATION_JSON_VALUE])
suspend fun update(/* ... */): ResponseEntity<Lead> = service.update(/* ... */).okOrNotFound()

The service simply does a lookup, modifies the current entity and saves it, throwing OptimisticLockingFailureException if the version doesn't match.

This all works properly, except that exception stack traces are lost.

The stack trace should look like this, with the exception originating in the service at the point of calling save on the repo:

org.springframework.dao.OptimisticLockingFailureException: Failed to update table [lead]; Version does not match for row with Id [01884a1a-70b4-7e94-80a3-e8f8e0d1bbda]
    (Coroutine boundary)
    at com.example.LeadService.update(LeadService.kt:70)
    at kotlin.reflect.full.KCallables.callSuspend(KCallables.kt:56)
Caused by: org.springframework.dao.OptimisticLockingFailureException: Failed to update table [lead]; Version does not match for row with Id [01884a1a-70b4-7e94-80a3-e8f8e0d1bbda]
    at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$13(R2dbcEntityTemplate.java:636)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.MonoHandleFuseable] :
    reactor.core.publisher.Mono.handle
    org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:628)
Error has been observed at the following site(s):
    *________Mono.handle ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:628)
    *__________Mono.then ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:641)
    |_                   ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$11(R2dbcEntityTemplate.java:609)
    *_______Mono.flatMap ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$12(R2dbcEntityTemplate.java:592)
    *_______Mono.flatMap ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:574)
    |_                   ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.update(R2dbcEntityTemplate.java:567)
    |_                   ⇢ at org.springframework.data.r2dbc.repository.support.SimpleR2dbcRepository.save(SimpleR2dbcRepository.java:122)
    |_                   ⇢ at org.springframework.data.repository.core.support.RepositoryMethodInvoker$ReactiveInvocationListenerDecorator.lambda$decorate$1(RepositoryMethodInvoker.java:228)
    *_____Mono.usingWhen ⇢ at org.springframework.data.repository.core.support.RepositoryMethodInvoker$ReactiveInvocationListenerDecorator.decorate(RepositoryMethodInvoker.java:225)
    |_ Mono.contextWrite ⇢ at kotlinx.coroutines.reactor.ReactorContextInjector.injectCoroutineContext(ReactorContextInjector.kt:21)
Original Stack Trace:
        at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$13(R2dbcEntityTemplate.java:636)
        <removed for brevity>

However, the stack trace looks like this, with the exception appearing to happen in the controller function instead (look at checkpoint line):

org.springframework.dao.OptimisticLockingFailureException: Failed to update table [lead]; Version does not match for row with Id [01884a1a-70b4-7e94-80a3-e8f8e0d1bbda]
    (Coroutine boundary)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.MonoCreate] :
    reactor.core.publisher.Mono.create
    kotlinx.coroutines.reactor.MonoKt.monoInternal(Mono.kt:85)
Error has been observed at the following site(s):
    *______Mono.create ⇢ at kotlinx.coroutines.reactor.MonoKt.monoInternal(Mono.kt:85)
    |_                 ⇢ at kotlinx.coroutines.reactor.MonoKt.mono(Mono.kt:34)
    |_     Mono.filter ⇢ at org.springframework.core.CoroutinesUtils.invokeSuspendingFunction(CoroutinesUtils.java:109)
    |_ Mono.onErrorMap ⇢ at org.springframework.core.CoroutinesUtils.invokeSuspendingFunction(CoroutinesUtils.java:110)
    |_       Mono.from ⇢ at org.springframework.core.ReactiveAdapterRegistry$ReactorAdapter.toPublisher(ReactiveAdapterRegistry.java:214)
    |_       Mono.from ⇢ at org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler.handleResult(ResponseEntityResultHandler.java:128)
    |_    Mono.flatMap ⇢ at org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler.handleResult(ResponseEntityResultHandler.java:138)
    |_      checkpoint ⇢ Handler com.example.LeadController#update(String, UUIDv7, LeadInfo, boolean, Continuation) [DispatcherHandler]
Original Stack Trace:
        (Coroutine boundary)
        at org.springframework.transaction.reactive.TransactionalOperatorExtensionsKt.executeAndAwait(TransactionalOperatorExtensions.kt:51)
        at kotlin.reflect.full.KCallables.callSuspend(KCallables.kt:56)
Caused by: org.springframework.dao.OptimisticLockingFailureException: Failed to update table [lead]; Version does not match for row with Id [01884a1a-70b4-7e94-80a3-e8f8e0d1bbda]
    at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$13(R2dbcEntityTemplate.java:636)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Assembly trace from producer [reactor.core.publisher.MonoHandleFuseable] :
    reactor.core.publisher.Mono.handle
    org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:628)
Error has been observed at the following site(s):
    *________Mono.handle ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:628)
    *__________Mono.then ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:641)
    |_                   ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$11(R2dbcEntityTemplate.java:609)
    *_______Mono.flatMap ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$12(R2dbcEntityTemplate.java:592)
    *_______Mono.flatMap ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.doUpdate(R2dbcEntityTemplate.java:574)
    |_                   ⇢ at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.update(R2dbcEntityTemplate.java:567)
    |_                   ⇢ at org.springframework.data.r2dbc.repository.support.SimpleR2dbcRepository.save(SimpleR2dbcRepository.java:122)
    |_                   ⇢ at org.springframework.data.repository.core.support.RepositoryMethodInvoker$ReactiveInvocationListenerDecorator.lambda$decorate$1(RepositoryMethodInvoker.java:228)
    *_____Mono.usingWhen ⇢ at org.springframework.data.repository.core.support.RepositoryMethodInvoker$ReactiveInvocationListenerDecorator.decorate(RepositoryMethodInvoker.java:225)
    |_ Mono.contextWrite ⇢ at kotlinx.coroutines.reactor.ReactorContextInjector.injectCoroutineContext(ReactorContextInjector.kt:21)
Original Stack Trace:
        at org.springframework.data.r2dbc.core.R2dbcEntityTemplate.lambda$doUpdate$13(R2dbcEntityTemplate.java:636)
        <removed for brevity>

It happens using either a R2dbcRepository (Mono/Flux return types) or CoroutineCrudRepository (suspend functions).

Exceptions thrown in the service itself or by code that doesn't cross a Reactive/coroutine boundary have a correct stack trace. So this hack works, but it is ugly:

val lead = try {
    repo.save(updated)
} catch (@Suppress("SwallowedException") e: OptimisticLockingFailureException) {
    throw OptimisticLockingFailureException(e.message.orEmpty(), e.cause)
}

Is there anything I can do to avoid this hack? It feels like the r2dbc implementation of OptimisticLockingFailureException might need to implement CopyThrowable?

Eric
  • 283
  • 5
  • 12
  • sometimes "devil hides in simplicity"...why is your controller returning "non-reactive-type"?? – xerx593 May 27 '23 at 11:20
  • You can either use reactive types or suspend functions, both are supported by Spring WebFlux. https://docs.spring.io/spring-framework/reference/web/webflux/controller/ann-methods/return-types.html – Eric May 28 '23 at 15:03

0 Answers0