Add coroutines support to RSocket @MessageMapping

Closes gh-22780
This commit is contained in:
Sebastien Deleuze
2019-04-30 15:42:55 +02:00
parent 842e7e5ef7
commit 2e6059f6b0
7 changed files with 311 additions and 3 deletions

View File

@@ -0,0 +1,225 @@
/*
* Copyright 2002-2019 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.messaging.rsocket
import java.time.Duration
import io.netty.buffer.PooledByteBufAllocator
import io.rsocket.RSocketFactory
import io.rsocket.frame.decoder.PayloadDecoder
import io.rsocket.transport.netty.server.CloseableChannel
import io.rsocket.transport.netty.server.TcpServerTransport
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.Test
import reactor.core.publisher.Flux
import reactor.core.publisher.ReplayProcessor
import reactor.test.StepVerifier
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.codec.CharSequenceEncoder
import org.springframework.core.codec.StringDecoder
import org.springframework.core.io.buffer.NettyDataBufferFactory
import org.springframework.messaging.handler.annotation.MessageExceptionHandler
import org.springframework.messaging.handler.annotation.MessageMapping
import org.springframework.stereotype.Controller
/**
* Coroutines server-side handling of RSocket requests.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
*/
class RSocketClientToServerCoroutinesIntegrationTests {
@Test
fun echoAsync() {
val result = Flux.range(1, 3).concatMap { i -> requester.route("echo-async").data("Hello " + i!!).retrieveMono(String::class.java) }
StepVerifier.create(result)
.expectNext("Hello 1 async").expectNext("Hello 2 async").expectNext("Hello 3 async")
.expectComplete()
.verify(Duration.ofSeconds(5))
}
@Test
fun echoStream() {
val result = requester.route("echo-stream").data("Hello").retrieveFlux(String::class.java)
StepVerifier.create(result)
.expectNext("Hello 0").expectNextCount(6).expectNext("Hello 7")
.thenCancel()
.verify(Duration.ofSeconds(5))
}
@Test
fun echoChannel() {
val result = requester.route("echo-channel")
.data(Flux.range(1, 10).map { i -> "Hello " + i!! }, String::class.java)
.retrieveFlux(String::class.java)
StepVerifier.create(result)
.expectNext("Hello 1 async").expectNextCount(8).expectNext("Hello 10 async")
.thenCancel() // https://github.com/rsocket/rsocket-java/issues/613
.verify(Duration.ofSeconds(5))
}
@Test
fun unitReturnValue() {
val result = requester.route("unit-return-value").data("Hello").retrieveFlux(String::class.java)
StepVerifier.create(result).expectComplete().verify(Duration.ofSeconds(5))
}
@Test
fun unitReturnValueFromExceptionHandler() {
val result = requester.route("unit-return-value").data("bad").retrieveFlux(String::class.java)
StepVerifier.create(result).expectComplete().verify(Duration.ofSeconds(5))
}
@Test
fun handleWithThrownException() {
val result = requester.route("thrown-exception").data("a").retrieveMono(String::class.java)
StepVerifier.create(result)
.expectNext("Invalid input error handled")
.expectComplete()
.verify(Duration.ofSeconds(5))
}
@FlowPreview
@Controller
class ServerController {
val fireForgetPayloads = ReplayProcessor.create<String>()
@MessageMapping("echo-async")
suspend fun echoAsync(payload: String): String {
delay(10)
return "$payload async"
}
@MessageMapping("echo-stream")
fun echoStream(payload: String): Flow<String> {
var i = 0
return flow {
while(true) {
delay(10)
emit("$payload ${i++}")
}
}
}
@MessageMapping("echo-channel")
fun echoChannel(payloads: Flow<String>) = payloads.map {
delay(10)
"$it async"
}
@MessageMapping("thrown-exception")
suspend fun handleAndThrow(payload: String): String {
delay(10)
throw IllegalArgumentException("Invalid input error")
}
@MessageMapping("unit-return-value")
suspend fun unitReturnValue(payload: String) =
if (payload != "bad") delay(10) else throw IllegalStateException("bad")
@MessageExceptionHandler
suspend fun handleException(ex: IllegalArgumentException): String {
delay(10)
return "${ex.message} handled"
}
@MessageExceptionHandler
suspend fun handleExceptionWithVoidReturnValue(ex: IllegalStateException) {
delay(10)
}
}
@Configuration
open class ServerConfig {
@Bean
open fun controller(): ServerController {
return ServerController()
}
@Bean
open fun messageHandlerAcceptor(): MessageHandlerAcceptor {
val acceptor = MessageHandlerAcceptor()
acceptor.rSocketStrategies = rsocketStrategies()
return acceptor
}
@Bean
open fun rsocketStrategies(): RSocketStrategies {
return RSocketStrategies.builder()
.decoder(StringDecoder.allMimeTypes())
.encoder(CharSequenceEncoder.allMimeTypes())
.dataBufferFactory(NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT))
.build()
}
}
companion object {
private lateinit var context: AnnotationConfigApplicationContext
private lateinit var server: CloseableChannel
private val interceptor = FireAndForgetCountingInterceptor()
private lateinit var requester: RSocketRequester
@BeforeClass
@JvmStatic
fun setupOnce() {
context = AnnotationConfigApplicationContext(ServerConfig::class.java)
server = RSocketFactory.receive()
.addServerPlugin(interceptor)
.frameDecoder(PayloadDecoder.ZERO_COPY)
.acceptor(context.getBean(MessageHandlerAcceptor::class.java))
.transport(TcpServerTransport.create("localhost", 7000))
.start()
.block()!!
requester = RSocketRequester.builder()
.rsocketFactory { factory -> factory.frameDecoder(PayloadDecoder.ZERO_COPY) }
.rsocketStrategies(context.getBean(RSocketStrategies::class.java))
.connectTcp("localhost", 7000)
.block()!!
}
@AfterClass
@JvmStatic
fun tearDownOnce() {
requester.rsocket().dispose()
server.dispose()
}
}
}