Resubscribe to newHeads immediately after reconnection (#189)

* Resubscribe to newHeads immediately after reconnection
This commit is contained in:
KirillPamPam
2023-03-30 13:32:02 +04:00
committed by GitHub
parent dcf360a7d7
commit 97fa616e19
14 changed files with 216 additions and 21 deletions

View File

@@ -0,0 +1,37 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.upstream.ethereum.WsConnection
import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions
import reactor.core.publisher.Flux
class WebsocketConnectionStatesHandler(
private val wsSubscriptions: WsSubscriptions,
private val onConnected: () -> Unit
) {
private var connectionId: String? = null
private var tryResubscribe = false
init {
wsSubscriptions.connectionInfoFlux()
.subscribe {
if (it.connectionId == connectionId && it.connectionState == WsConnection.ConnectionState.DISCONNECTED) {
connectionId = null
if (tryResubscribe) {
tryResubscribe = false
}
} else if (connectionId == null && it.connectionState == WsConnection.ConnectionState.CONNECTED) {
if (!tryResubscribe) {
tryResubscribe = true
onConnected()
}
}
}
}
fun subscribe(method: String): Flux<ByteArray> =
wsSubscriptions.subscribe(method)
.also {
connectionId = it.connectionId
tryResubscribe = false
}.data
}

View File

@@ -23,6 +23,7 @@ import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.WebsocketConnectionStatesHandler
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
@@ -40,11 +41,12 @@ class EthereumWsHead(
forkChoice: ForkChoice,
blockValidator: BlockValidator,
private val api: JsonRpcReader,
private val wsSubscriptions: WsSubscriptions,
wsSubscriptions: WsSubscriptions,
private val skipEnhance: Boolean
) : DefaultEthereumHead(upstreamId, forkChoice, blockValidator), Lifecycle {
private var subscription: Disposable? = null
private val wsConnectionStatesHandler = WebsocketConnectionStatesHandler(wsSubscriptions, this::onNoHeadUpdates)
override fun isRunning(): Boolean {
return subscription != null
@@ -67,7 +69,7 @@ class EthereumWsHead(
}
fun listenNewHeads(): Flux<BlockContainer> {
return wsSubscriptions.subscribe("newHeads")
return wsConnectionStatesHandler.subscribe("newHeads")
.map {
Global.objectMapper.readValue(it, BlockJson::class.java) as BlockJson<TransactionRefJson>
}

View File

@@ -25,7 +25,18 @@ interface WsConnection : AutoCloseable {
val isConnected: Boolean
fun connectionId(): String
fun getSubscribeResponses(): Flux<JsonRpcWsMessage>
fun callRpc(originalRequest: JsonRpcRequest): Mono<JsonRpcResponse>
fun connect()
fun connectionInfoFlux(): Flux<ConnectionInfo>
data class ConnectionInfo(
val connectionId: String,
val connectionState: ConnectionState
)
enum class ConnectionState {
CONNECTED, DISCONNECTED
}
}

View File

@@ -18,6 +18,8 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.upstream.ethereum.WsConnection.ConnectionState.CONNECTED
import io.emeraldpay.dshackle.upstream.ethereum.WsConnection.ConnectionState.DISCONNECTED
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
@@ -52,6 +54,7 @@ import java.net.URI
import java.time.Duration
import java.time.Instant
import java.util.Base64
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
@@ -114,17 +117,26 @@ open class WsConnectionImpl(
.multicast()
.directBestEffort<Instant>()
private val connectionInfo = Sinks
.many()
.multicast()
.directBestEffort<WsConnection.ConnectionInfo>()
private val currentRequests = ConcurrentHashMap<Int, Sinks.One<JsonRpcResponse>>()
private val connId = UUID.randomUUID().toString()
private val sendIdSeq = AtomicInteger(IDS_START)
private val sendExecutor = Executors.newSingleThreadExecutor()
private var keepConnection = true
private var firstConnect = true
private var connection: Disposable? = null
private val reconnecting = AtomicBoolean(false)
override val isConnected: Boolean
get() = connection != null && !reconnecting.get()
override fun connectionId(): String = connId
fun setReconnectIntervalSeconds(value: Long) {
reconnectBackoff = FixedBackOff(value * 1000, FixedBackOff.UNLIMITED_ATTEMPTS)
currentBackOff = reconnectBackoff.start()
@@ -135,6 +147,10 @@ open class WsConnectionImpl(
connectInternal()
}
override fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo> =
connectionInfo.asFlux()
.distinctUntilChanged { it.connectionState }
private fun tryReconnectLater() {
if (!keepConnection) {
return
@@ -143,6 +159,10 @@ open class WsConnectionImpl(
if (alreadyReconnecting) {
return
}
connectionInfo.tryEmitNext(WsConnection.ConnectionInfo(connId, DISCONNECTED))
if (firstConnect) {
firstConnect = false
}
// rpcSend is already CANCELLED, since the subscription owned by the previous connection is gone
// so we need to create a new Sink. Emit Complete is probably useless, and just in case
@@ -184,6 +204,13 @@ open class WsConnectionImpl(
tryReconnectLater()
}
}
.doOnConnected {
if (!firstConnect) {
connectionInfo.tryEmitNext(WsConnection.ConnectionInfo(connId, CONNECTED))
} else {
firstConnect = false
}
}
.doOnError(
{ _, t ->
log.warn("Failed to connect to $uri. Error: ${t.message}")
@@ -379,6 +406,7 @@ open class WsConnectionImpl(
override fun close() {
log.info("Closing connection to WebSocket $uri")
connectionInfo.tryEmitComplete()
keepConnection = false
connection?.dispose()
connection = null

View File

@@ -20,6 +20,9 @@ import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import org.springframework.util.backoff.BackOffExecution
import org.springframework.util.backoff.ExponentialBackOff
import reactor.core.Disposable
import reactor.core.publisher.Flux
import reactor.core.publisher.Sinks
import java.time.Duration
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
@@ -50,6 +53,8 @@ class WsConnectionMultiPool(
private var adjustLock = ReentrantReadWriteLock()
private val index = AtomicInteger(0)
private var connIndex = 0
private val connectionInfo = Sinks.many().multicast().directBestEffort<WsConnection.ConnectionInfo>()
private val connectionSubscriptionMap = mutableMapOf<String, Disposable>()
var scheduler: ScheduledExecutorService = Global.control
@@ -73,8 +78,13 @@ class WsConnectionMultiPool(
return next
}
override fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo> =
connectionInfo.asFlux()
override fun close() {
adjustLock.write {
connectionSubscriptionMap.values.forEach { it.dispose() }
connectionSubscriptionMap.clear()
current.forEach { it.close() }
current.clear()
}
@@ -106,6 +116,9 @@ class WsConnectionMultiPool(
}
}.also {
it.connect()
connectionSubscriptionMap[it.connectionId()] = it.connectionInfoFlux().subscribe { info ->
connectionInfo.emitNext(info) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
}
}
)
SCHEDULE_GROW
@@ -116,6 +129,7 @@ class WsConnectionMultiPool(
current.removeIf {
if (!it.isConnected) {
// DO NOT FORGET to close the connection, otherwise it would keep reconnecting but unused
connectionSubscriptionMap.remove(it.connectionId())?.dispose()
it.close()
true
} else {

View File

@@ -15,7 +15,10 @@
*/
package io.emeraldpay.dshackle.upstream.ethereum
import reactor.core.publisher.Flux
interface WsConnectionPool : AutoCloseable {
fun connect()
fun getConnection(): WsConnection
fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo>
}

View File

@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import reactor.core.publisher.Flux
class WsConnectionSinglePool(
ethereumWsConnectionFactory: EthereumWsConnectionFactory,
@@ -36,6 +37,9 @@ class WsConnectionSinglePool(
return connection
}
override fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo> =
connection.connectionInfoFlux()
override fun close() {
connection.close()
}

View File

@@ -37,5 +37,12 @@ interface WsSubscriptions {
/**
* Subscribe on remote
*/
fun subscribe(method: String): Flux<ByteArray>
fun subscribe(method: String): SubscribeData
fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo>
data class SubscribeData(
val data: Flux<ByteArray>,
val connectionId: String
)
}

View File

@@ -33,7 +33,7 @@ class WsSubscriptionsImpl(
private val ids = AtomicLong(1)
override fun subscribe(method: String): Flux<ByteArray> {
override fun subscribe(method: String): WsSubscriptions.SubscribeData {
val subscriptionId = AtomicReference("")
val conn = wsPool.getConnection()
val messages = conn.getSubscribeResponses()
@@ -41,7 +41,7 @@ class WsSubscriptionsImpl(
.filter { it.result != null } // should never happen
.map { it.result!! }
return conn.callRpc(JsonRpcRequest("eth_subscribe", listOf(method), ids.incrementAndGet()))
val messageFlux = conn.callRpc(JsonRpcRequest("eth_subscribe", listOf(method), ids.incrementAndGet()))
.flatMapMany {
if (it.hasError()) {
log.warn("Failed to establish ETH Subscription: ${it.error?.message}")
@@ -51,5 +51,10 @@ class WsSubscriptionsImpl(
messages
}
}
return WsSubscriptions.SubscribeData(messageFlux, conn.connectionId())
}
override fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo> =
wsPool.connectionInfoFlux()
}

View File

@@ -34,6 +34,7 @@ class WebsocketPendingTxes(
override fun createConnection(): Flux<TransactionId> {
return wsSubscriptions.subscribe(EthereumEgressSubscription.METHOD_PENDING_TXES)
.data
.timeout(Duration.ofSeconds(60), Mono.empty())
.map {
// comes as a JS string, i.e., within quotes