solution: base implementation for NativeSubscribe

This commit is contained in:
Igor Artamonov
2021-10-04 20:58:25 -04:00
parent 84ca9d7dcf
commit 49bfa163ef
10 changed files with 338 additions and 18 deletions

View File

@@ -40,6 +40,7 @@ class AccessHandlerGrpc(
"SubscribeTxStatus" -> processSubscribeTxStatus(call, headers, next)
"GetBalance" -> processSubscribeBalance(call, headers, next, false)
"NativeCall" -> processNativeCall(call, headers, next)
"NativeSubscribe" -> processNativeSubscribe(call, headers, next)
"Describe" -> processDescribe(call, headers, next)
"SubscribeStatus" -> processStatus(call, headers, next)
else -> {
@@ -110,6 +111,18 @@ class AccessHandlerGrpc(
)
}
@Suppress("UNCHECKED_CAST")
private fun <ReqT : Any, RespT : Any> processNativeSubscribe(
call: ServerCall<ReqT, RespT>,
headers: Metadata,
next: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
return process(call, headers, next,
EventsBuilder.NativeSubscribe() as EventsBuilder.RequestReply<*, ReqT, RespT>
)
}
@Suppress("UNCHECKED_CAST")
private fun <ReqT : Any, RespT : Any> processDescribe(
call: ServerCall<ReqT, RespT>,

View File

@@ -102,6 +102,16 @@ class Events {
val nativeCall: NativeCallItemDetails
) : ChainBase(blockchain, "NativeCall", id, channel)
@JsonInclude(JsonInclude.Include.NON_NULL)
class NativeSubscribe(
blockchain: Chain, id: UUID, channel: Channel,
// info about the initial request, that may include several native calls
val request: StreamRequestDetails,
val payloadSizeBytes: Long,
val nativeSubscribe: NativeSubscribeItemDetails
) : ChainBase(blockchain, "NativeSubscribe", id, channel)
@JsonInclude(JsonInclude.Include.NON_NULL)
class Describe(
id: UUID,
@@ -139,6 +149,16 @@ class Events {
val ts: Instant = Instant.now()
)
data class NativeSubscribeItemDetails(
val method: String,
val payloadSizeBytes: Long
)
data class NativeSubscribeReplyDetails(
val replySizeBytes: Long,
val ts: Instant = Instant.now()
)
data class BalanceRequest(
val asset: String,
val addressType: String

View File

@@ -293,6 +293,36 @@ class EventsBuilder {
}
}
class NativeSubscribe :
Base<NativeSubscribe>(),
RequestReply<Events.NativeSubscribe, BlockchainOuterClass.NativeSubscribeRequest, BlockchainOuterClass.NativeSubscribeReplyItem> {
var item: Events.NativeSubscribeItemDetails? = null
val replies = HashMap<Int, Events.NativeSubscribeReplyDetails>()
override fun getT(): NativeSubscribe {
return this
}
override fun onRequest(msg: BlockchainOuterClass.NativeSubscribeRequest) {
withChain(msg.chain.number)
this.item = Events.NativeSubscribeItemDetails(
msg.method,
msg.payload.size().toLong()
)
}
override fun onReply(msg: BlockchainOuterClass.NativeSubscribeReplyItem): Events.NativeSubscribe {
return Events.NativeSubscribe(
request = requestDetails,
blockchain = chain,
nativeSubscribe = item!!,
payloadSizeBytes = msg.payload?.size()?.toLong() ?: 0L,
id = UUID.randomUUID(),
channel = Events.Channel.GRPC
)
}
}
class Describe :
Base<Describe>(),
RequestReply<Events.Describe, BlockchainOuterClass.DescribeRequest, BlockchainOuterClass.DescribeResponse> {

View File

@@ -37,6 +37,7 @@ import java.util.concurrent.TimeUnit
@Service @DependsOn("monitoringSetup")
class BlockchainRpc(
@Autowired private val nativeCall: NativeCall,
@Autowired private val nativeSubscribe: NativeSubscribe,
@Autowired private val streamHead: StreamHead,
@Autowired private val trackTx: List<TrackTx>,
@Autowired private val trackAddress: List<TrackAddress>,
@@ -73,6 +74,19 @@ class BlockchainRpc(
}.doOnError { errorMetric.increment() }
}
override fun nativeSubscribe(request: Mono<BlockchainOuterClass.NativeSubscribeRequest>): Flux<BlockchainOuterClass.NativeSubscribeReplyItem> {
var metrics: RequestMetrics? = null
return nativeSubscribe.nativeSubscribe(
request
.doOnNext {
metrics = chainMetrics.get(it.chain)
metrics!!.nativeSubscribeMetric.increment()
}
).doOnNext {
metrics?.nativeSubscribeRespMetric?.increment()
}.doOnError { errorMetric.increment() }
}
override fun subscribeHead(request: Mono<Common.Chain>): Flux<BlockchainOuterClass.ChainHead> {
return streamHead.add(
request
@@ -169,6 +183,14 @@ class BlockchainRpc(
.tag("chain", chain.chainCode)
.publishPercentileHistogram()
.register(Metrics.globalRegistry)
val nativeSubscribeMetric = Counter.builder("request.grpc.request")
.tag("type", "nativeSubscribe")
.tag("chain", chain.chainCode)
.register(Metrics.globalRegistry)
val nativeSubscribeRespMetric = Counter.builder("request.grpc.response")
.tag("type", "nativeSubscribe")
.tag("chain", chain.chainCode)
.register(Metrics.globalRegistry)
val subscribeHeadMetric = Counter.builder("request.grpc.request")
.tag("type", "subscribeHead")
.tag("chain", chain.chainCode)

View File

@@ -0,0 +1,98 @@
/**
* Copyright (c) 2021 EmeraldPay, Inc
*
* 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
*
* http://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 io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.grpc.BlockchainType
import io.emeraldpay.grpc.Chain
import io.grpc.Status
import io.grpc.StatusException
import org.reactivestreams.Publisher
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Service
class NativeSubscribe(
@Autowired private val multistreamHolder: MultistreamHolder
) {
companion object {
private val log = LoggerFactory.getLogger(NativeSubscribe::class.java)
}
private val objectMapper = Global.objectMapper
fun nativeSubscribe(request: Mono<BlockchainOuterClass.NativeSubscribeRequest>): Flux<BlockchainOuterClass.NativeSubscribeReplyItem> {
return request
.flatMapMany(this@NativeSubscribe::start)
.map(this@NativeSubscribe::convertToProto)
.onErrorMap(this@NativeSubscribe::convertToStatus)
}
fun start(it: BlockchainOuterClass.NativeSubscribeRequest): Publisher<Any> {
val chain = Chain.byId(it.chainValue)
if (BlockchainType.from(chain) != BlockchainType.ETHEREUM) {
return Mono.error(UnsupportedOperationException("Native subscribe is not supported for ${chain.chainCode}"))
}
val method = it.method
val params: List<*> = it.payload?.let { payload ->
if (payload.size() > 0) {
listOf(objectMapper.readValue(payload.newInput(), Map::class.java))
} else {
emptyList<Any>()
}
} ?: emptyList<Any>()
return subscribe(chain, method, params)
}
fun convertToStatus(t: Throwable) = when (t) {
is SilentException.UnsupportedBlockchain -> StatusException(
Status.UNAVAILABLE.withDescription("BLOCKCHAIN UNAVAILABLE: ${t.blockchainId}")
)
is UnsupportedOperationException -> StatusException(
Status.UNIMPLEMENTED.withDescription(t.message)
)
else -> {
log.warn("Unhandled error", t)
StatusException(
Status.INTERNAL.withDescription(t.message)
)
}
}
fun subscribe(chain: Chain, method: String, params: List<*>): Flux<Any> {
val up = multistreamHolder.getUpstream(chain) ?: return Flux.error(SilentException.UnsupportedBlockchain(chain))
return (up as EthereumMultistream)
.getSubscribe()
.subscribe(method, params)
}
fun convertToProto(value: Any): BlockchainOuterClass.NativeSubscribeReplyItem {
val result = objectMapper.writeValueAsBytes(value)
return BlockchainOuterClass.NativeSubscribeReplyItem.newBuilder()
.setPayload(ByteString.copyFrom(result))
.build()
}
}

View File

@@ -41,6 +41,7 @@ open class EthereumMultistream(
private var head: Head? = null
private val reader: EthereumReader = EthereumReader(this, this.caches, getMethodsFactory())
private val subscribe = EthereumSubscribe()
init {
this.init()
@@ -122,4 +123,7 @@ open class EthereumMultistream(
return Mono.just(NativeCallRouter(reader, getMethods(), getHead()))
}
open fun getSubscribe(): EthereumSubscribe {
return subscribe
}
}

View File

@@ -0,0 +1,15 @@
package io.emeraldpay.dshackle.upstream.ethereum
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
open class EthereumSubscribe {
companion object {
private val log = LoggerFactory.getLogger(EthereumSubscribe::class.java)
}
open fun subscribe(method: String, params: List<*>): Flux<Any> {
return Flux.error(UnsupportedOperationException("Method $method is not supported"))
}
}

View File

@@ -24,10 +24,8 @@ import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.rpcclient.ResponseWSParser
import io.emeraldpay.dshackle.upstream.rpcclient.RpcMetrics
import io.emeraldpay.dshackle.upstream.rpcclient.*
import io.emeraldpay.etherjar.rpc.RpcResponseError
import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import io.netty.buffer.ByteBuf
@@ -365,6 +363,7 @@ class EthereumWsFactory(
Flux.from(rpcReceive.asFlux())
.doOnSubscribe { sendRpc(request) }
.filter { resp -> resp.id.asNumber() == expectedId }
.take(Defaults.timeout)
.take(1)
.singleOrEmpty()
.doOnNext {
@@ -374,6 +373,12 @@ class EthereumWsFactory(
rpcMetrics?.errors?.increment()
}
.map { it.copyWithId(JsonRpcResponse.Id.from(originalId)) }
.defaultIfEmpty(
JsonRpcResponse(null,
JsonRpcError(RpcResponseError.CODE_INTERNAL_ERROR, "Response not received from WebSocket"),
JsonRpcResponse.Id.from(originalId)
)
)
}
}