Track web socket subscription_ids passed from grpc, retry eth_getLogs on errors/empty results (#465)

* retry getLogs when result is empty or error 

* print error logs with grpc subscription ids

* add logs on NativeSubscription's flux
This commit is contained in:
msizov
2024-05-06 15:15:28 +07:00
committed by GitHub
parent 77c7eee6f6
commit 258507de4d
4 changed files with 78 additions and 31 deletions

View File

@@ -108,15 +108,27 @@ class BlockchainRpc(
override fun nativeSubscribe(request: Mono<BlockchainOuterClass.NativeSubscribeRequest>): Flux<BlockchainOuterClass.NativeSubscribeReplyItem> { override fun nativeSubscribe(request: Mono<BlockchainOuterClass.NativeSubscribeRequest>): Flux<BlockchainOuterClass.NativeSubscribeReplyItem> {
var metrics: RequestMetrics? = null var metrics: RequestMetrics? = null
return nativeSubscribe.nativeSubscribe( return request
request .doOnNext {
.doOnNext { log.info("Starting subscription " + it.subscriptionId)
metrics = chainMetrics.get(it.chain) metrics = chainMetrics.get(it.chain)
metrics!!.nativeSubscribeMetric.increment() metrics!!.nativeSubscribeMetric.increment()
}, }.flatMapMany {
).doOnNext { req ->
metrics?.nativeSubscribeRespMetric?.increment() nativeSubscribe.nativeSubscribe(
}.doOnError { failMetric.increment() } Mono.just(req),
).doOnNext {
metrics?.nativeSubscribeRespMetric?.increment()
}.doOnComplete {
log.info("Subscription ${req.subscriptionId} completed")
}.doOnCancel {
log.info("Subscription ${req.subscriptionId} canceled")
}.doOnError {
t ->
log.info("Error ${t.message} in subscription ${req.subscriptionId}")
failMetric.increment()
}
}
} }
override fun subscribeHead(request: Mono<Common.Chain>): Flux<BlockchainOuterClass.ChainHead> { override fun subscribeHead(request: Mono<Common.Chain>): Flux<BlockchainOuterClass.ChainHead> {

View File

@@ -46,20 +46,33 @@ open class NativeSubscribe(
} }
private val objectMapper = Global.objectMapper private val objectMapper = Global.objectMapper
fun nativeSubscribe(request: Mono<BlockchainOuterClass.NativeSubscribeRequest>): Flux<NativeSubscribeReplyItem> { fun nativeSubscribe(request: Mono<BlockchainOuterClass.NativeSubscribeRequest>): Flux<NativeSubscribeReplyItem> {
return request return request
.flatMapMany(this@NativeSubscribe::start) .flatMapMany {
.map(this@NativeSubscribe::convertToProto) it ->
.onErrorMap(this@NativeSubscribe::convertToStatus) val subscriptionId = it.subscriptionId
Mono.just(it)
.flatMapMany {
start(it, subscriptionId)
}
.map(this@NativeSubscribe::convertToProto)
.doOnCancel {
log.warn("Subscription $subscriptionId cancelled")
}.onErrorMap {
convertToStatus(it, subscriptionId)
}
}
} }
fun start(request: BlockchainOuterClass.NativeSubscribeRequest): Publisher<ResponseHolder> { fun start(request: BlockchainOuterClass.NativeSubscribeRequest): Publisher<ResponseHolder> = start(request, "")
fun start(request: BlockchainOuterClass.NativeSubscribeRequest, subscriptionId: String): Publisher<ResponseHolder> {
val chain = Chain.byId(request.chainValue) val chain = Chain.byId(request.chainValue)
val multistream = getUpstream(chain) val multistream = getUpstream(chain)
if (!multistream.getSubscriptionTopics().contains(request.method)) { if (!multistream.getSubscriptionTopics().contains(request.method)) {
log.error("sub_id:" + subscriptionId + "subscribe ${request.method} is not supported for ${chain.chainCode}")
return Mono.error(UnsupportedOperationException("subscribe ${request.method} is not supported for ${chain.chainCode}")) return Mono.error(UnsupportedOperationException("subscribe ${request.method} is not supported for ${chain.chainCode}"))
} }
@@ -81,33 +94,38 @@ open class NativeSubscribe(
objectMapper.readValue(it.newInput(), List::class.java) objectMapper.readValue(it.newInput(), List::class.java)
} }
} }
subscribe(chain, method, params, matcher) subscribe(chain, method, params, matcher, subscriptionId)
} }
return publisher.map { ResponseHolder(it, nonce) } return publisher.map { ResponseHolder(it, nonce) }
} }
fun convertToStatus(t: Throwable) = when (t) { fun convertToStatus(t: Throwable) = convertToStatus(t, "")
is SilentException.UnsupportedBlockchain -> StatusException( fun convertToStatus(t: Throwable, subscriptionId: String = "") = when (t) {
Status.UNAVAILABLE.withDescription("BLOCKCHAIN UNAVAILABLE: ${t.blockchainId}"), is SilentException.UnsupportedBlockchain -> {
) log.error("sub_id:$subscriptionId BLOCKCHAIN UNAVAILABLE: ${t.blockchainId}")
StatusException(Status.UNAVAILABLE.withDescription("BLOCKCHAIN UNAVAILABLE: ${t.blockchainId}"))
}
is UnsupportedOperationException -> StatusException( is UnsupportedOperationException -> {
Status.UNIMPLEMENTED.withDescription(t.message), log.error("sub_id:$subscriptionId unimplemented error ${t.message}")
) StatusException(Status.UNIMPLEMENTED.withDescription(t.message))
}
else -> { else -> {
log.warn("Unhandled error", t) log.warn("sub_id:$subscriptionId Unhandled error", t)
StatusException( StatusException(
Status.INTERNAL.withDescription(t.message), Status.INTERNAL.withDescription(t.message),
) )
} }
} }
open fun subscribe(chain: Chain, method: String, params: Any?, matcher: Selector.Matcher): Flux<out Any> = open fun subscribe(chain: Chain, method: String, params: Any?, matcher: Selector.Matcher): Flux<out Any> =
subscribe(chain, method, params, matcher)
open fun subscribe(chain: Chain, method: String, params: Any?, matcher: Selector.Matcher, subscriptionId: String): Flux<out Any> =
getUpstream(chain).getEgressSubscription() getUpstream(chain).getEgressSubscription()
.subscribe(method, params, matcher) .subscribe(method, params, matcher)
.doOnError { .doOnError {
log.error("Error during subscription to $method, chain $chain, params $params", it) log.error("sub_id:$subscriptionId Error during subscription to $method, chain $chain, params $params", it)
} }
private fun getUpstream(chain: Chain): Multistream = private fun getUpstream(chain: Chain): Multistream =

View File

@@ -21,6 +21,7 @@ import io.emeraldpay.dshackle.data.BlockId
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Multistream import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.ethereum.EthereumCachingReader import io.emeraldpay.dshackle.upstream.ethereum.EthereumCachingReader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumDirectReader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumDirectReader.Result import io.emeraldpay.dshackle.upstream.ethereum.EthereumDirectReader.Result
import io.emeraldpay.dshackle.upstream.ethereum.hex.HexData import io.emeraldpay.dshackle.upstream.ethereum.hex.HexData
import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionLogJson import io.emeraldpay.dshackle.upstream.ethereum.json.TransactionLogJson
@@ -28,6 +29,7 @@ import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.LogMessage
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.kotlin.core.publisher.onErrorResume
import reactor.kotlin.core.publisher.switchIfEmpty import reactor.kotlin.core.publisher.switchIfEmpty
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -35,7 +37,7 @@ class ProduceLogs(
private val logs: Reader<BlockId, Result<List<TransactionLogJson>>>, private val logs: Reader<BlockId, Result<List<TransactionLogJson>>>,
private val chain: Chain, private val chain: Chain,
) { ) {
private val MAX_RETRIES = 3
companion object { companion object {
private val log = LoggerFactory.getLogger(ProduceLogs::class.java) private val log = LoggerFactory.getLogger(ProduceLogs::class.java)
} }
@@ -73,11 +75,26 @@ class ProduceLogs(
.map { it.copy(removed = true) } .map { it.copy(removed = true) }
} }
fun produceAdded(update: ConnectBlockUpdates.Update): Flux<LogMessage> { private fun produceAddedFallback(update: ConnectBlockUpdates.Update, retries: Int): Mono<EthereumDirectReader.Result<List<TransactionLogJson>>> {
return logs.read(update.blockHash).switchIfEmpty { return logs.read(update.blockHash).switchIfEmpty {
log.warn("Cannot find receipt for block ${update.blockHash} for chain ${chain.chainName}") if (retries > MAX_RETRIES) {
Mono.empty() log.warn("Cannot find receipt for block ${update.blockHash} for chain ${chain.chainName} retries so far: $retries")
}.map { Mono.empty()
} else {
produceAddedFallback(update, retries + 1)
}
}.onErrorResume { t ->
if (retries > MAX_RETRIES) {
log.error("Error ${t.message} produced ${update.blockHash} for chain ${chain.chainName} retries so far: $retries")
Mono.empty()
} else {
produceAddedFallback(update, retries + 1)
}
}
}
fun produceAdded(update: ConnectBlockUpdates.Update): Flux<LogMessage> {
return produceAddedFallback(update, 0).map {
it.data it.data
}.flatMapMany { }.flatMapMany {
val messages = it.map { log -> val messages = it.map { log ->