Fix unsubscribe requests (#721)

This commit is contained in:
KirillPamPam
2025-09-10 15:18:54 +04:00
committed by GitHub
parent 650fa5cf0e
commit 0ed77f1cfd
26 changed files with 125 additions and 62 deletions

View File

@@ -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()

View File

@@ -37,7 +37,7 @@ object BeaconChainSpecific : AbstractPollChainSpecific() {
throw NotImplementedError()
}
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
throw NotImplementedError()
}

View File

@@ -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(

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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>,
)

View File

@@ -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> {

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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?

View File

@@ -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>
}

View File

@@ -53,6 +53,7 @@ open class GenericConnectorFactory(
val specific = ChainSpecificRegistry.resolve(chain)
if (wsFactory != null && connectorType == WS_ONLY) {
return GenericWsConnector(
chain,
wsFactory,
upstream,
forkChoice,

View File

@@ -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 -> {

View File

@@ -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> {

View File

@@ -56,7 +56,7 @@ object KadenaChainSpecific : AbstractPollChainSpecific() {
throw NotImplementedError()
}
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
throw NotImplementedError()
}

View File

@@ -53,7 +53,7 @@ object NearChainSpecific : AbstractPollChainSpecific() {
throw NotImplementedError()
}
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
throw NotImplementedError()
}

View File

@@ -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(

View File

@@ -61,7 +61,7 @@ object RippleChainSpecific : AbstractPollChainSpecific() {
throw NotImplementedError()
}
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
throw NotImplementedError()
}

View File

@@ -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 {

View File

@@ -55,7 +55,7 @@ object StarknetChainSpecific : AbstractPollChainSpecific() {
throw NotImplementedError()
}
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
throw NotImplementedError()
}

View File

@@ -40,7 +40,7 @@ object TonHttpSpecific : AbstractPollChainSpecific() {
throw NotImplementedError()
}
override fun unsubscribeNewHeadsRequest(subId: String): ChainRequest {
override fun unsubscribeNewHeadsRequest(subId: Any): ChainRequest {
throw NotImplementedError()
}