Fix unsubscribe requests (#721)
This commit is contained in:
@@ -35,6 +35,7 @@ import io.emeraldpay.dshackle.upstream.ethereum.domain.TransactionId
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.TransactionIdSerializer
|
||||
import io.emeraldpay.dshackle.upstream.ton.TonMasterchainInfo
|
||||
import io.emeraldpay.dshackle.upstream.ton.TonMasterchainInfoDeserializer
|
||||
import java.math.BigInteger
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
@@ -64,6 +65,20 @@ class Global {
|
||||
} ?: Chain.UNSPECIFIED
|
||||
}
|
||||
|
||||
fun getSubId(subId: String, chain: Chain): Any {
|
||||
return if (isSolana(chain)) {
|
||||
runCatching {
|
||||
BigInteger(subId) as Any
|
||||
}.getOrElse { subId }
|
||||
} else {
|
||||
subId
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSolana(chain: Chain): Boolean {
|
||||
return chain == Chain.SOLANA__MAINNET || chain == Chain.SOLANA__DEVNET || chain == Chain.SOLANA__TESTNET
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val objectMapper: ObjectMapper = createObjectMapper()
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ object BeaconChainSpecific : AbstractPollChainSpecific() {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ object CosmosChainSpecific : AbstractPollChainSpecific() {
|
||||
override fun listenNewHeadsRequest() = throw NotImplementedError()
|
||||
// ChainRequest("subscribe", ListParams("tm.event = 'NewBlockHeader'"))
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String) = throw NotImplementedError()
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any) = throw NotImplementedError()
|
||||
// ChainRequest("unsubscribe", ListParams("tm.event = 'NewBlockHeader'"))
|
||||
|
||||
override fun upstreamValidators(
|
||||
|
||||
@@ -57,7 +57,7 @@ object EthereumChainSpecific : AbstractPollChainSpecific() {
|
||||
ChainRequest("eth_getBlockByNumber", ListParams("latest", false))
|
||||
override fun listenNewHeadsRequest(): ChainRequest =
|
||||
ChainRequest("eth_subscribe", ListParams("newHeads"))
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest =
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest =
|
||||
ChainRequest("eth_unsubscribe", ListParams(subId))
|
||||
|
||||
override fun localReaderBuilder(
|
||||
@@ -208,7 +208,7 @@ object EthereumChainSpecific : AbstractPollChainSpecific() {
|
||||
return EthereumUpstreamSettingsDetector(upstream, chain)
|
||||
}
|
||||
|
||||
override fun makeIngressSubscription(ws: WsSubscriptions): IngressSubscription {
|
||||
override fun makeIngressSubscription(chain: Chain, ws: WsSubscriptions): IngressSubscription {
|
||||
return EthereumWsIngressSubscription(ws)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package io.emeraldpay.dshackle.upstream.ethereum
|
||||
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.data.BlockContainer
|
||||
import io.emeraldpay.dshackle.reader.ChainReader
|
||||
import io.emeraldpay.dshackle.upstream.BlockValidator
|
||||
@@ -48,7 +49,7 @@ class GenericWsHead(
|
||||
private val wsSubscriptions: WsSubscriptions,
|
||||
private val wsConnectionResubscribeScheduler: Scheduler,
|
||||
headScheduler: Scheduler,
|
||||
upstream: DefaultUpstream,
|
||||
private val upstream: DefaultUpstream,
|
||||
private val chainSpecific: ChainSpecific,
|
||||
jsonRpcWsClient: JsonRpcWsClient,
|
||||
timeout: Duration,
|
||||
@@ -122,19 +123,30 @@ class GenericWsHead(
|
||||
.flatMap { data ->
|
||||
chainSpecific.getFromHeader(data, "unknown", api)
|
||||
}
|
||||
.timeout(wsHeadTimeout, Mono.error(RuntimeException("No response from subscribe to newHeads")))
|
||||
.timeout(
|
||||
wsHeadTimeout,
|
||||
Mono.error(RuntimeException("No response from subscribe to newHeads")),
|
||||
)
|
||||
.onErrorResume { err ->
|
||||
log.error("Error getting heads for {}, message {}", upstreamId, err.message)
|
||||
unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
UPSTREAM_SETTINGS_ERROR -> {
|
||||
log.warn("Couldn't check chain settings via ws connection for {}, ws sub will be recreated", upstreamId)
|
||||
log.warn(
|
||||
"Couldn't check chain settings via ws connection for {}, ws sub will be recreated",
|
||||
upstreamId,
|
||||
)
|
||||
subscribed.set(false)
|
||||
Mono.empty()
|
||||
}
|
||||
|
||||
UPSTREAM_FATAL_SETTINGS_ERROR -> {
|
||||
log.error("Chain settings check hasn't been passed via ws connection, upstream {} will be removed", upstreamId)
|
||||
log.error(
|
||||
"Chain settings check hasn't been passed via ws connection, upstream {} will be removed",
|
||||
upstreamId,
|
||||
)
|
||||
headLivenessSink.emitNext(HeadLivenessState.FATAL_ERROR) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
||||
Mono.empty()
|
||||
}
|
||||
@@ -154,7 +166,13 @@ class GenericWsHead(
|
||||
|
||||
private fun unsubscribe(): Mono<BlockContainer> {
|
||||
subscribed.set(false)
|
||||
return wsSubscriptions.unsubscribe(chainSpecific.unsubscribeNewHeadsRequest(subscriptionId.get()).copy(id = ids.getAndIncrement()))
|
||||
return wsSubscriptions.unsubscribe(
|
||||
chainSpecific.unsubscribeNewHeadsRequest(
|
||||
Global.getSubId(subscriptionId.get(), upstream.getChain()),
|
||||
).copy(
|
||||
id = ids.getAndIncrement(),
|
||||
),
|
||||
)
|
||||
.flatMap { it.requireResult() }
|
||||
.doOnNext { log.warn("{} has just unsubscribed from newHeads", upstreamId) }
|
||||
.onErrorResume {
|
||||
@@ -171,11 +189,13 @@ class GenericWsHead(
|
||||
wsSubscriptions.subscribe(chainSpecific.listenNewHeadsRequest().copy(id = ids.getAndIncrement()))
|
||||
.also {
|
||||
connectionId.set(it.connectionId)
|
||||
subscriptionId.set(it.subId.get())
|
||||
if (!connected.get()) {
|
||||
connected.set(true)
|
||||
}
|
||||
}.data
|
||||
}.data.flatMapMany {
|
||||
subscriptionId.set(it.t1)
|
||||
it.t2
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Flux.error(e)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import io.emeraldpay.dshackle.upstream.ChainRequest
|
||||
import io.emeraldpay.dshackle.upstream.ChainResponse
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.util.function.Tuple2
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
/**
|
||||
@@ -48,7 +49,7 @@ interface WsSubscriptions {
|
||||
fun unsubscribe(request: ChainRequest): Mono<ChainResponse>
|
||||
|
||||
data class SubscribeData(
|
||||
val data: Flux<ByteArray>,
|
||||
val data: Mono<Tuple2<String, Flux<ByteArray>>>,
|
||||
val connectionId: String,
|
||||
val subId: AtomicReference<String>,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.emeraldpay.dshackle.upstream.rpcclient.ListParams
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.util.function.Tuples
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
class WsSubscriptionsImpl(
|
||||
@@ -40,8 +41,8 @@ class WsSubscriptionsImpl(
|
||||
.filter { it.result != null } // should never happen
|
||||
.map { it.result!! }
|
||||
|
||||
val messageFlux = conn.callRpc(request)
|
||||
.flatMapMany {
|
||||
val message = conn.callRpc(request)
|
||||
.flatMap {
|
||||
if (it.hasError()) {
|
||||
log.warn("Failed to establish subscription: ${it.error?.message}")
|
||||
Mono.error(ChainException(it.id, it.error!!))
|
||||
@@ -52,11 +53,11 @@ class WsSubscriptionsImpl(
|
||||
it.getResultAsProcessedString()
|
||||
}
|
||||
subscriptionId.set(id)
|
||||
messages
|
||||
Mono.just(Tuples.of(subscriptionId.get(), messages))
|
||||
}
|
||||
}
|
||||
|
||||
return WsSubscriptions.SubscribeData(messageFlux, conn.connectionId(), subscriptionId)
|
||||
return WsSubscriptions.SubscribeData(message, conn.connectionId(), subscriptionId)
|
||||
}
|
||||
|
||||
override fun unsubscribe(request: ChainRequest): Mono<ChainResponse> {
|
||||
|
||||
@@ -37,6 +37,7 @@ class WebsocketPendingTxes(
|
||||
override fun createConnection(): Flux<TransactionId> {
|
||||
return wsSubscriptions.subscribe(ChainRequest("eth_subscribe", ListParams(EthereumEgressSubscription.METHOD_PENDING_TXES)))
|
||||
.data
|
||||
.flatMapMany { it.t2 }
|
||||
.timeout(Duration.ofSeconds(85), Mono.empty())
|
||||
.map {
|
||||
// comes as a JS string, i.e., within quotes
|
||||
|
||||
@@ -72,7 +72,7 @@ abstract class AbstractChainSpecific : ChainSpecific {
|
||||
config: UpstreamsConfig.Upstream<*>?,
|
||||
): UpstreamRpcMethodsDetector? = null
|
||||
|
||||
override fun makeIngressSubscription(ws: WsSubscriptions): IngressSubscription {
|
||||
override fun makeIngressSubscription(chain: Chain, ws: WsSubscriptions): IngressSubscription {
|
||||
return NoIngressSubscription()
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ interface ChainSpecific {
|
||||
|
||||
fun listenNewHeadsRequest(): ChainRequest
|
||||
|
||||
fun unsubscribeNewHeadsRequest(subId: String): ChainRequest
|
||||
fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest
|
||||
|
||||
fun finalizationDetectorBuilder(): FinalizationDetector
|
||||
|
||||
@@ -96,7 +96,7 @@ interface ChainSpecific {
|
||||
config: UpstreamsConfig.Upstream<*>?,
|
||||
): UpstreamRpcMethodsDetector?
|
||||
|
||||
fun makeIngressSubscription(ws: WsSubscriptions): IngressSubscription
|
||||
fun makeIngressSubscription(chain: Chain, ws: WsSubscriptions): IngressSubscription
|
||||
|
||||
fun callSelector(caches: Caches): CallSelector?
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.emeraldpay.dshackle.upstream.generic
|
||||
|
||||
import io.emeraldpay.dshackle.Chain
|
||||
import io.emeraldpay.dshackle.Global
|
||||
import io.emeraldpay.dshackle.upstream.ChainRequest
|
||||
import io.emeraldpay.dshackle.upstream.IngressSubscription
|
||||
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
|
||||
@@ -11,9 +13,12 @@ import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.math.log
|
||||
|
||||
class GenericIngressSubscription(val conn: WsSubscriptions, val methods: List<String>) : IngressSubscription {
|
||||
class GenericIngressSubscription(
|
||||
val chain: Chain,
|
||||
val conn: WsSubscriptions,
|
||||
val methods: List<String>,
|
||||
) : IngressSubscription {
|
||||
override fun getAvailableTopics(): List<String> {
|
||||
return methods
|
||||
}
|
||||
@@ -24,6 +29,7 @@ class GenericIngressSubscription(val conn: WsSubscriptions, val methods: List<St
|
||||
override fun <T> get(topic: String, params: Any?, unsubscribeMethod: String): SubscriptionConnect<T> {
|
||||
return holders.computeIfAbsent(topic to params) { key ->
|
||||
GenericSubscriptionConnect(
|
||||
chain,
|
||||
conn,
|
||||
key.first,
|
||||
key.second,
|
||||
@@ -34,6 +40,7 @@ class GenericIngressSubscription(val conn: WsSubscriptions, val methods: List<St
|
||||
}
|
||||
|
||||
class GenericSubscriptionConnect(
|
||||
val chain: Chain,
|
||||
val conn: WsSubscriptions,
|
||||
val topic: String,
|
||||
val params: Any?,
|
||||
@@ -48,6 +55,7 @@ class GenericSubscriptionConnect(
|
||||
override fun createConnection(): Flux<Any> {
|
||||
val sub = conn.subscribe(ChainRequest(topic, ListParams(getParams(params) as List<Any>)))
|
||||
return sub.data
|
||||
.flatMapMany { it.t2 }
|
||||
.timeout(
|
||||
Duration.ofSeconds(85),
|
||||
Mono.empty<ByteArray?>().doOnEach {
|
||||
@@ -57,11 +65,18 @@ class GenericSubscriptionConnect(
|
||||
.onErrorResume {
|
||||
log.error("Error during subscription to $topic", it)
|
||||
Mono.empty()
|
||||
}.doFinally {
|
||||
}
|
||||
.doFinally {
|
||||
if (unsubscribeMethod != "") {
|
||||
conn.unsubscribe(ChainRequest(unsubscribeMethod, ListParams(sub.subId.get()))).subscribe {
|
||||
log.info("unsubscribed from ${sub.subId.get()}")
|
||||
}
|
||||
conn.unsubscribe(
|
||||
ChainRequest(
|
||||
unsubscribeMethod,
|
||||
ListParams(Global.getSubId(sub.subId.get(), chain)),
|
||||
),
|
||||
)
|
||||
.subscribe {
|
||||
log.info("unsubscribed from ${sub.subId.get()}")
|
||||
}
|
||||
}
|
||||
} as Flux<Any>
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ open class GenericConnectorFactory(
|
||||
val specific = ChainSpecificRegistry.resolve(chain)
|
||||
if (wsFactory != null && connectorType == WS_ONLY) {
|
||||
return GenericWsConnector(
|
||||
chain,
|
||||
wsFactory,
|
||||
upstream,
|
||||
forkChoice,
|
||||
|
||||
@@ -71,7 +71,7 @@ class GenericRpcConnector(
|
||||
pool = wsFactory?.create(upstream)
|
||||
wsSubs = pool?.let { WsSubscriptionsImpl(it) }
|
||||
jsonRpcWsClient = pool?.let { JsonRpcWsClient(pool) }
|
||||
ingressSubscription = wsSubs?.let { chainSpecific.makeIngressSubscription(it) }
|
||||
ingressSubscription = wsSubs?.let { chainSpecific.makeIngressSubscription(chain, it) }
|
||||
|
||||
head = when (connectorType) {
|
||||
RPC_ONLY -> {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.emeraldpay.dshackle.upstream.generic.connectors
|
||||
|
||||
import io.emeraldpay.dshackle.Chain
|
||||
import io.emeraldpay.dshackle.reader.ChainReader
|
||||
import io.emeraldpay.dshackle.upstream.BlockValidator
|
||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||
@@ -19,6 +20,7 @@ import reactor.core.scheduler.Scheduler
|
||||
import java.time.Duration
|
||||
|
||||
class GenericWsConnector(
|
||||
chain: Chain,
|
||||
wsFactory: WsConnectionPoolFactory,
|
||||
upstream: DefaultUpstream,
|
||||
forkChoice: ForkChoice,
|
||||
@@ -51,7 +53,7 @@ class GenericWsConnector(
|
||||
expectedBlockTime,
|
||||
)
|
||||
liveness = HeadLivenessValidatorImpl(head, expectedBlockTime, headLivenessScheduler, upstream.getId())
|
||||
subscriptions = chainSpecific.makeIngressSubscription(wsSubscriptions)
|
||||
subscriptions = chainSpecific.makeIngressSubscription(chain, wsSubscriptions)
|
||||
}
|
||||
|
||||
override fun headLivenessEvents(): Flux<HeadLivenessState> {
|
||||
|
||||
@@ -56,7 +56,7 @@ object KadenaChainSpecific : AbstractPollChainSpecific() {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ object NearChainSpecific : AbstractPollChainSpecific() {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ object PolkadotChainSpecific : AbstractPollChainSpecific() {
|
||||
override fun listenNewHeadsRequest(): ChainRequest =
|
||||
ChainRequest("chain_subscribeNewHeads", ListParams())
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest =
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest =
|
||||
ChainRequest("chain_unsubscribeNewHeads", ListParams(subId))
|
||||
|
||||
override fun localReaderBuilder(
|
||||
@@ -146,8 +146,8 @@ object PolkadotChainSpecific : AbstractPollChainSpecific() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun makeIngressSubscription(ws: WsSubscriptions): IngressSubscription {
|
||||
return GenericIngressSubscription(ws, DefaultPolkadotMethods.subs.map { it.first })
|
||||
override fun makeIngressSubscription(chain: Chain, ws: WsSubscriptions): IngressSubscription {
|
||||
return GenericIngressSubscription(chain, ws, DefaultPolkadotMethods.subs.map { it.first })
|
||||
}
|
||||
|
||||
override fun upstreamRpcMethodsDetector(
|
||||
|
||||
@@ -61,7 +61,7 @@ object RippleChainSpecific : AbstractPollChainSpecific() {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ object SolanaChainSpecific : AbstractChainSpecific() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
|
||||
return ChainRequest("blockUnsubscribe", ListParams(subId))
|
||||
}
|
||||
|
||||
@@ -154,8 +154,8 @@ object SolanaChainSpecific : AbstractChainSpecific() {
|
||||
return SolanaUpstreamSettingsDetector(upstream)
|
||||
}
|
||||
|
||||
override fun makeIngressSubscription(ws: WsSubscriptions): IngressSubscription {
|
||||
return GenericIngressSubscription(ws, DefaultSolanaMethods.subs.map { it.first })
|
||||
override fun makeIngressSubscription(chain: Chain, ws: WsSubscriptions): IngressSubscription {
|
||||
return GenericIngressSubscription(chain, ws, DefaultSolanaMethods.subs.map { it.first })
|
||||
}
|
||||
|
||||
override fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription {
|
||||
|
||||
@@ -55,7 +55,7 @@ object StarknetChainSpecific : AbstractPollChainSpecific() {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ object TonHttpSpecific : AbstractPollChainSpecific() {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
|
||||
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user