solution: websocket unsubscribe

This commit is contained in:
Igor Artamonov
2021-10-29 16:12:54 -04:00
parent e001913acb
commit f1720ad354
3 changed files with 90 additions and 9 deletions

View File

@@ -112,6 +112,7 @@ class ProxyServer(
fun setupRoutes(routes: HttpServerRoutes) { fun setupRoutes(routes: HttpServerRoutes) {
config.routes.forEach { routeConfig -> config.routes.forEach { routeConfig ->
// TODO implement a manual handling of the routes and WS upgrade to have a better control over the connection and improve the access logging
routes.post("/" + routeConfig.id, httpHandler.proxy(routeConfig)) routes.post("/" + routeConfig.id, httpHandler.proxy(routeConfig))
if (config.websocketEnabled && wsHandler != null) { if (config.websocketEnabled && wsHandler != null) {
routes.ws("/" + routeConfig.id, wsHandler.proxy(routeConfig)) routes.ws("/" + routeConfig.id, wsHandler.proxy(routeConfig))

View File

@@ -15,6 +15,8 @@
*/ */
package io.emeraldpay.dshackle.proxy package io.emeraldpay.dshackle.proxy
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.ProxyConfig import io.emeraldpay.dshackle.config.ProxyConfig
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
@@ -25,11 +27,11 @@ import io.emeraldpay.etherjar.rpc.json.ResponseJson
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import io.netty.buffer.ByteBufInputStream import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import org.apache.commons.lang3.StringUtils
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.publisher.Sinks
import reactor.netty.http.websocket.WebsocketInbound import reactor.netty.http.websocket.WebsocketInbound
import reactor.netty.http.websocket.WebsocketOutbound import reactor.netty.http.websocket.WebsocketOutbound
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
@@ -55,11 +57,14 @@ class WebsocketHandler(
fun nextSubscriptionId(): String { fun nextSubscriptionId(): String {
val n = subscriptionId.incrementAndGet() val n = subscriptionId.incrementAndGet()
return StringUtils.leftPad(n.toString(16), 16, "0") return n.toString(16)
} }
fun proxy(routeConfig: ProxyConfig.Route): BiFunction<WebsocketInbound, WebsocketOutbound, Publisher<Void>> { fun proxy(routeConfig: ProxyConfig.Route): BiFunction<WebsocketInbound, WebsocketOutbound, Publisher<Void>> {
return BiFunction { req, resp -> return BiFunction { req, resp ->
// each connection keeps a list of subscription controllers
val control = HashMap<String, Sinks.One<Boolean>>()
val requests: Flux<RequestJson<Any>> = req.aggregateFrames() val requests: Flux<RequestJson<Any>> = req.aggregateFrames()
.receiveFrames() .receiveFrames()
.map { ByteBufInputStream(it.content()).readAllBytes() } .map { ByteBufInputStream(it.content()).readAllBytes() }
@@ -67,7 +72,7 @@ class WebsocketHandler(
val eventHandler = accessHandler.start(req, routeConfig.blockchain) val eventHandler = accessHandler.start(req, routeConfig.blockchain)
val responses = respond(routeConfig.blockchain, requests, eventHandler) val responses = respond(routeConfig.blockchain, control, requests, eventHandler)
.map { Unpooled.wrappedBuffer(it.toByteArray()) } .map { Unpooled.wrappedBuffer(it.toByteArray()) }
resp.send(responses) resp.send(responses)
@@ -97,7 +102,12 @@ class WebsocketHandler(
} }
} }
fun respond(blockchain: Chain, requests: Flux<RequestJson<Any>>, eventHandlerFactory: AccessHandlerHttp.WsHandlerFactory): Flux<String> { fun respond(
blockchain: Chain,
control: MutableMap<String, Sinks.One<Boolean>>,
requests: Flux<RequestJson<Any>>,
eventHandlerFactory: AccessHandlerHttp.WsHandlerFactory
): Flux<String> {
return requests.flatMap { call -> return requests.flatMap { call ->
val method = call.method val method = call.method
@@ -113,6 +123,8 @@ class WebsocketHandler(
Pair(mp.first, mp.second?.let { Global.objectMapper.writeValueAsBytes(it) }) Pair(mp.first, mp.second?.let { Global.objectMapper.writeValueAsBytes(it) })
} }
) )
val currentControl = Sinks.one<Boolean>()
control[subscriptionId] = currentControl
// first need to respond with ID of the subscription, and the following responses would have it in "subscription" param // first need to respond with ID of the subscription, and the following responses would have it in "subscription" param
val start = ResponseJson<String, Any>().also { val start = ResponseJson<String, Any>().also {
it.id = call.id it.id = call.id
@@ -124,6 +136,7 @@ class WebsocketHandler(
.map { event -> .map { event ->
WsSubscriptionResponse(params = WsSubscriptionData(event, subscriptionId)) WsSubscriptionResponse(params = WsSubscriptionData(event, subscriptionId))
} }
.takeUntilOther(currentControl.asMono())
Flux.concat(Mono.just(start), responses) Flux.concat(Mono.just(start), responses)
.map { Global.objectMapper.writeValueAsString(it) } .map { Global.objectMapper.writeValueAsString(it) }
.doOnNext { .doOnNext {
@@ -133,6 +146,34 @@ class WebsocketHandler(
// TODO should it produce a 404 to the AccessLog? // TODO should it produce a 404 to the AccessLog?
Mono.empty() Mono.empty()
} }
} else if (method == "eth_unsubscribe") {
val id = call.params?.getOrNull(0) ?: ""
// put it to the Access Log with fake id=0 (it doesn't matter, except the later reference)
val eventHandler: AccessHandlerHttp.RequestHandler = eventHandlerFactory.call()
eventHandler.onRequest(
BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChainValue(blockchain.id)
.addItems(
BlockchainOuterClass.NativeCallItem.newBuilder()
.setId(0)
.setMethod("eth_unsubscribe")
.setPayload(ByteString.copyFromUtf8("[\"$id\"]"))
.build()
)
.build()
)
val p = control.remove(id.toString())
val success = p?.tryEmitValue(true)?.isSuccess ?: false
val response = ResponseJson<Boolean, Any>().also {
it.id = call.id
it.result = success
}
Mono.just(response)
.map { Global.objectMapper.writeValueAsString(it) }
.doOnNext { eventHandler.onResponse(NativeCall.CallResult.ok(0, it.toByteArray())) }
.doFinally { eventHandler.close() }
} else { } else {
val eventHandler: AccessHandlerHttp.RequestHandler = eventHandlerFactory.call() val eventHandler: AccessHandlerHttp.RequestHandler = eventHandlerFactory.call()
val proxyCall = readRpcJson.convertToNativeCall(ProxyCall.RpcType.SINGLE, listOf(call)) val proxyCall = readRpcJson.convertToNativeCall(ProxyCall.RpcType.SINGLE, listOf(call))

View File

@@ -21,6 +21,7 @@ import io.emeraldpay.dshackle.rpc.NativeSubscribe
import io.emeraldpay.etherjar.rpc.json.RequestJson import io.emeraldpay.etherjar.rpc.json.RequestJson
import io.emeraldpay.grpc.Chain import io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Sinks
import spock.lang.Specification import spock.lang.Specification
import java.time.Duration import java.time.Duration
@@ -85,7 +86,7 @@ class WebsocketHandlerSpec extends Specification {
def request = new RequestJson("foo_test", [], 2) def request = new RequestJson("foo_test", [], 2)
when: when:
def act = handler.respond(Chain.ETHEREUM, Flux.just(request), requestHandler) def act = handler.respond(Chain.ETHEREUM, new HashMap<String, Sinks.One<Boolean>>(), Flux.just(request), requestHandler)
.single() .single()
.block(Duration.ofSeconds(1)) .block(Duration.ofSeconds(1))
then: then:
@@ -106,13 +107,51 @@ class WebsocketHandlerSpec extends Specification {
def request = new RequestJson("eth_subscribe", ["foo_test"], 2) def request = new RequestJson("eth_subscribe", ["foo_test"], 2)
when: when:
def act = handler.respond(Chain.ETHEREUM, Flux.just(request), requestHandler) def act = handler.respond(Chain.ETHEREUM, new HashMap<String, Sinks.One<Boolean>>(), Flux.just(request), requestHandler)
.collectList() .collectList()
.block(Duration.ofSeconds(1)) .block(Duration.ofSeconds(1))
then: then:
act[0] == '{"jsonrpc":"2.0","id":2,"result":"0000000000000001"}' act[0] == '{"jsonrpc":"2.0","id":2,"result":"1"}'
act[1] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":1},"subscription":"0000000000000001"}}' act[1] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":1},"subscription":"1"}}'
act[2] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":2},"subscription":"0000000000000001"}}' act[2] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":2},"subscription":"1"}}'
act.size() == 3 act.size() == 3
} }
def "Unsubscribe"() {
setup:
def handler = new WebsocketHandler(
new ReadRpcJson(), new WriteRpcJson(), Stub(NativeCall), Stub(NativeSubscribe), requestHandlerFactory, Stub(ProxyServer.RequestMetricsFactory)
)
def control = new HashMap<String, Sinks.One<Boolean>>()
Sinks.One<Boolean> sink = Sinks.one();
control["5"] = sink
def request = new RequestJson("eth_unsubscribe", ["5"], 0)
when:
def act = handler.respond(Chain.ETHEREUM, control, Flux.just(request), requestHandler)
.single()
.block(Duration.ofSeconds(1))
def sinkResponse = sink.asMono().block()
then:
act == '{"jsonrpc":"2.0","id":0,"result":true}'
sinkResponse != null
}
def "Unsubscribe when no subscription"() {
setup:
def handler = new WebsocketHandler(
new ReadRpcJson(), new WriteRpcJson(), Stub(NativeCall), Stub(NativeSubscribe), requestHandlerFactory, Stub(ProxyServer.RequestMetricsFactory)
)
def control = new HashMap<String, Sinks.One<Boolean>>()
def request = new RequestJson("eth_unsubscribe", ["5"], 0)
when:
def act = handler.respond(Chain.ETHEREUM, control, Flux.just(request), requestHandler)
.single()
.block(Duration.ofSeconds(1))
then:
act == '{"jsonrpc":"2.0","id":0,"result":false}'
}
} }