Resubscribe to newHeads immediately after reconnection (#189)
* Resubscribe to newHeads immediately after reconnection
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user