solution: base implementation for NativeSubscribe
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user