Fix deadlock, remove setting UNAVAIL status (#333)
This commit is contained in:
@@ -35,6 +35,16 @@ open class SchedulersConfig {
|
|||||||
return makeScheduler("ws-connection-resubscribe-scheduler", 2, monitoringConfig)
|
return makeScheduler("ws-connection-resubscribe-scheduler", 2, monitoringConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun wsScheduler(monitoringConfig: MonitoringConfig): Scheduler {
|
||||||
|
return makeScheduler("ws-scheduler", 4, monitoringConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
open fun headLivenessScheduler(monitoringConfig: MonitoringConfig): Scheduler {
|
||||||
|
return makeScheduler("head-liveness-scheduler", 4, monitoringConfig)
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
open fun grpcChannelExecutor(monitoringConfig: MonitoringConfig): Executor {
|
open fun grpcChannelExecutor(monitoringConfig: MonitoringConfig): Executor {
|
||||||
return makePool("grpc-client-channel", 10, monitoringConfig)
|
return makePool("grpc-client-channel", 10, monitoringConfig)
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ import org.springframework.context.ApplicationEventPublisher
|
|||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
import reactor.core.scheduler.Schedulers
|
import reactor.core.scheduler.Schedulers
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -95,6 +94,8 @@ open class ConfiguredUpstreams(
|
|||||||
private val clientSpansInterceptor: ClientInterceptor?,
|
private val clientSpansInterceptor: ClientInterceptor?,
|
||||||
@Qualifier("headScheduler")
|
@Qualifier("headScheduler")
|
||||||
private val headScheduler: Scheduler,
|
private val headScheduler: Scheduler,
|
||||||
|
private val wsScheduler: Scheduler,
|
||||||
|
private val headLivenessScheduler: Scheduler,
|
||||||
private val authorizationConfig: AuthorizationConfig,
|
private val authorizationConfig: AuthorizationConfig,
|
||||||
private val grpcAuthContext: GrpcAuthContext,
|
private val grpcAuthContext: GrpcAuthContext,
|
||||||
) : ApplicationRunner {
|
) : ApplicationRunner {
|
||||||
@@ -258,7 +259,7 @@ open class ConfiguredUpstreams(
|
|||||||
options,
|
options,
|
||||||
config.role,
|
config.role,
|
||||||
methods,
|
methods,
|
||||||
QuorumForLabels.QuorumItem(1, config.labels),
|
QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(config.labels)),
|
||||||
chainConfig,
|
chainConfig,
|
||||||
connectorFactory,
|
connectorFactory,
|
||||||
eventPublisher,
|
eventPublisher,
|
||||||
@@ -388,7 +389,7 @@ open class ConfiguredUpstreams(
|
|||||||
chain,
|
chain,
|
||||||
endpoint.url,
|
endpoint.url,
|
||||||
endpoint.origin ?: URI("http://localhost"),
|
endpoint.origin ?: URI("http://localhost"),
|
||||||
headScheduler,
|
wsScheduler,
|
||||||
).apply {
|
).apply {
|
||||||
config = endpoint
|
config = endpoint
|
||||||
basicAuth = endpoint.basicAuth
|
basicAuth = endpoint.basicAuth
|
||||||
@@ -424,6 +425,7 @@ open class ConfiguredUpstreams(
|
|||||||
blockValidator,
|
blockValidator,
|
||||||
wsConnectionResubscribeScheduler,
|
wsConnectionResubscribeScheduler,
|
||||||
headScheduler,
|
headScheduler,
|
||||||
|
headLivenessScheduler,
|
||||||
chainsConf.expectedBlockTime,
|
chainsConf.expectedBlockTime,
|
||||||
)
|
)
|
||||||
if (!connectorFactory.isValid()) {
|
if (!connectorFactory.isValid()) {
|
||||||
|
|||||||
@@ -168,6 +168,10 @@ abstract class AbstractHead @JvmOverloads constructor(
|
|||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun headLiveness(): Flux<Boolean> {
|
||||||
|
return Flux.empty()
|
||||||
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
stopping = false
|
stopping = false
|
||||||
log.debug("Start ${this.javaClass.simpleName} $upstreamId")
|
log.debug("Start ${this.javaClass.simpleName} $upstreamId")
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ abstract class DefaultUpstream(
|
|||||||
private val statusStream = Sinks.many()
|
private val statusStream = Sinks.many()
|
||||||
.multicast()
|
.multicast()
|
||||||
.directBestEffort<UpstreamAvailability>()
|
.directBestEffort<UpstreamAvailability>()
|
||||||
|
protected val stateStream: Sinks.Many<Boolean> = Sinks.many()
|
||||||
|
.multicast()
|
||||||
|
.directBestEffort()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (id.length < 3 || !id.matches(Regex("[a-zA-Z][a-zA-Z0-9_-]+[a-zA-Z0-9]"))) {
|
if (id.length < 3 || !id.matches(Regex("[a-zA-Z][a-zA-Z0-9_-]+[a-zA-Z0-9]"))) {
|
||||||
@@ -106,6 +109,10 @@ abstract class DefaultUpstream(
|
|||||||
return statusStream.asFlux().distinctUntilChanged()
|
return statusStream.asFlux().distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun observeState(): Flux<Boolean> {
|
||||||
|
return stateStream.asFlux()
|
||||||
|
}
|
||||||
|
|
||||||
override fun setLag(lag: Long) {
|
override fun setLag(lag: Long) {
|
||||||
lag.coerceAtLeast(0).let { nLag ->
|
lag.coerceAtLeast(0).let { nLag ->
|
||||||
status.updateAndGet { curr ->
|
status.updateAndGet { curr ->
|
||||||
|
|||||||
@@ -39,4 +39,6 @@ class EmptyHead : Head {
|
|||||||
|
|
||||||
override fun onSyncingNode(isSyncing: Boolean) {
|
override fun onSyncingNode(isSyncing: Boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun headLiveness(): Flux<Boolean> = Flux.empty()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,4 +43,6 @@ interface Head {
|
|||||||
fun stop()
|
fun stop()
|
||||||
|
|
||||||
fun onSyncingNode(isSyncing: Boolean)
|
fun onSyncingNode(isSyncing: Boolean)
|
||||||
|
|
||||||
|
fun headLiveness(): Flux<Boolean>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,6 +265,10 @@ abstract class Multistream(
|
|||||||
).distinct()
|
).distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun observeState(): Flux<Boolean> {
|
||||||
|
return Flux.empty()
|
||||||
|
}
|
||||||
|
|
||||||
override fun isAvailable(): Boolean {
|
override fun isAvailable(): Boolean {
|
||||||
return getAll().any { it.isAvailable() }
|
return getAll().any { it.isAvailable() }
|
||||||
}
|
}
|
||||||
@@ -315,10 +319,14 @@ abstract class Multistream(
|
|||||||
.distinctUntilChanged {
|
.distinctUntilChanged {
|
||||||
it.getId()
|
it.getId()
|
||||||
}.flatMap { upstream ->
|
}.flatMap { upstream ->
|
||||||
upstream.observeStatus().map { upstream }
|
val statusStream = upstream.observeStatus().map { upstream }
|
||||||
|
val stateStream = upstream.observeState().map { upstream }
|
||||||
|
Flux.merge(stateStream, statusStream)
|
||||||
.takeUntilOther(
|
.takeUntilOther(
|
||||||
subscribeRemovedUpstreams()
|
subscribeRemovedUpstreams()
|
||||||
.filter { it.getId() == upstream.getId() },
|
.filter {
|
||||||
|
it.getId() == upstream.getId()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.subscribe {
|
.subscribe {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface Upstream : Lifecycle {
|
|||||||
fun isAvailable(): Boolean
|
fun isAvailable(): Boolean
|
||||||
fun getStatus(): UpstreamAvailability
|
fun getStatus(): UpstreamAvailability
|
||||||
fun observeStatus(): Flux<UpstreamAvailability>
|
fun observeStatus(): Flux<UpstreamAvailability>
|
||||||
|
fun observeState(): Flux<Boolean>
|
||||||
fun getHead(): Head
|
fun getHead(): Head
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -91,4 +91,6 @@ class EnrichedMergedHead constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSyncingNode(isSyncing: Boolean) {}
|
override fun onSyncingNode(isSyncing: Boolean) {}
|
||||||
|
|
||||||
|
override fun headLiveness(): Flux<Boolean> = Flux.empty()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ object EthereumChainSpecific : ChainSpecific {
|
|||||||
|
|
||||||
override fun latestBlockRequest() = JsonRpcRequest("eth_getBlockByNumber", listOf("latest", false))
|
override fun latestBlockRequest() = JsonRpcRequest("eth_getBlockByNumber", listOf("latest", false))
|
||||||
override fun listenNewHeadsRequest(): JsonRpcRequest = JsonRpcRequest("eth_subscribe", listOf("newHeads"))
|
override fun listenNewHeadsRequest(): JsonRpcRequest = JsonRpcRequest("eth_subscribe", listOf("newHeads"))
|
||||||
|
override fun unsubscribeNewHeadsRequest(subId: String): JsonRpcRequest =
|
||||||
|
JsonRpcRequest("eth_unsubscribe", listOf(subId))
|
||||||
|
|
||||||
override fun localReaderBuilder(
|
override fun localReaderBuilder(
|
||||||
cachingReader: CachingReader,
|
cachingReader: CachingReader,
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import io.emeraldpay.dshackle.reader.JsonRpcReader
|
|||||||
import io.emeraldpay.dshackle.upstream.BlockValidator
|
import io.emeraldpay.dshackle.upstream.BlockValidator
|
||||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.Lifecycle
|
import io.emeraldpay.dshackle.upstream.Lifecycle
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
||||||
import io.emeraldpay.dshackle.upstream.generic.ChainSpecific
|
import io.emeraldpay.dshackle.upstream.generic.ChainSpecific
|
||||||
import io.emeraldpay.dshackle.upstream.generic.GenericHead
|
import io.emeraldpay.dshackle.upstream.generic.GenericHead
|
||||||
@@ -32,6 +31,7 @@ import reactor.core.publisher.Sinks
|
|||||||
import reactor.core.scheduler.Scheduler
|
import reactor.core.scheduler.Scheduler
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
class GenericWsHead(
|
class GenericWsHead(
|
||||||
forkChoice: ForkChoice,
|
forkChoice: ForkChoice,
|
||||||
@@ -40,7 +40,7 @@ class GenericWsHead(
|
|||||||
private val wsSubscriptions: WsSubscriptions,
|
private val wsSubscriptions: WsSubscriptions,
|
||||||
private val wsConnectionResubscribeScheduler: Scheduler,
|
private val wsConnectionResubscribeScheduler: Scheduler,
|
||||||
headScheduler: Scheduler,
|
headScheduler: Scheduler,
|
||||||
private val upstream: DefaultUpstream,
|
upstream: DefaultUpstream,
|
||||||
private val chainSpecific: ChainSpecific,
|
private val chainSpecific: ChainSpecific,
|
||||||
) : GenericHead(upstream.getId(), forkChoice, blockValidator, headScheduler, chainSpecific), Lifecycle {
|
) : GenericHead(upstream.getId(), forkChoice, blockValidator, headScheduler, chainSpecific), Lifecycle {
|
||||||
|
|
||||||
@@ -51,6 +51,9 @@ class GenericWsHead(
|
|||||||
|
|
||||||
private var subscription: Disposable? = null
|
private var subscription: Disposable? = null
|
||||||
private val noHeadUpdatesSink = Sinks.many().multicast().directBestEffort<Boolean>()
|
private val noHeadUpdatesSink = Sinks.many().multicast().directBestEffort<Boolean>()
|
||||||
|
private val headLivenessSink = Sinks.many().multicast().directBestEffort<Boolean>()
|
||||||
|
|
||||||
|
private var subscriptionId = AtomicReference("")
|
||||||
|
|
||||||
init {
|
init {
|
||||||
registerHeadResubscribeFlux()
|
registerHeadResubscribeFlux()
|
||||||
@@ -85,18 +88,14 @@ class GenericWsHead(
|
|||||||
|
|
||||||
fun listenNewHeads(): Flux<BlockContainer> {
|
fun listenNewHeads(): Flux<BlockContainer> {
|
||||||
return subscribe()
|
return subscribe()
|
||||||
.transform {
|
|
||||||
Flux.concat(it.next().doOnNext { upstream.setStatus(UpstreamAvailability.OK) }, it)
|
|
||||||
}
|
|
||||||
.map {
|
.map {
|
||||||
chainSpecific.parseHeader(it, "unknown")
|
chainSpecific.parseHeader(it, "unknown")
|
||||||
}
|
}
|
||||||
.timeout(Duration.ofSeconds(60), Mono.error(RuntimeException("No response from subscribe to newHeads")))
|
.timeout(Duration.ofSeconds(60), Mono.error(RuntimeException("No response from subscribe to newHeads")))
|
||||||
.onErrorResume {
|
.onErrorResume {
|
||||||
log.error("Error getting heads for $upstreamId - ${it.message}")
|
log.error("Error getting heads for $upstreamId - ${it.message}")
|
||||||
upstream.setStatus(UpstreamAvailability.UNAVAILABLE)
|
|
||||||
subscribed = false
|
subscribed = false
|
||||||
Mono.empty()
|
unsubscribe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +105,19 @@ class GenericWsHead(
|
|||||||
noHeadUpdatesSink.tryEmitComplete()
|
noHeadUpdatesSink.tryEmitComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun headLiveness(): Flux<Boolean> = headLivenessSink.asFlux()
|
||||||
|
|
||||||
|
private fun unsubscribe(): Mono<BlockContainer> {
|
||||||
|
return wsSubscriptions.unsubscribe(chainSpecific.unsubscribeNewHeadsRequest(subscriptionId.get()).copy(id = ids.getAndIncrement()))
|
||||||
|
.flatMap { it.requireResult() }
|
||||||
|
.doOnNext { log.warn("{} has just unsubscribed from newHeads", upstreamId) }
|
||||||
|
.onErrorResume {
|
||||||
|
log.error("{} couldn't unsubscribe from newHeads", upstreamId, it)
|
||||||
|
Mono.empty()
|
||||||
|
}
|
||||||
|
.then(Mono.empty())
|
||||||
|
}
|
||||||
|
|
||||||
private val ids = AtomicInteger(1)
|
private val ids = AtomicInteger(1)
|
||||||
|
|
||||||
private fun subscribe(): Flux<ByteArray> {
|
private fun subscribe(): Flux<ByteArray> {
|
||||||
@@ -113,6 +125,7 @@ class GenericWsHead(
|
|||||||
wsSubscriptions.subscribe(chainSpecific.listenNewHeadsRequest().copy(id = ids.getAndIncrement()))
|
wsSubscriptions.subscribe(chainSpecific.listenNewHeadsRequest().copy(id = ids.getAndIncrement()))
|
||||||
.also {
|
.also {
|
||||||
connectionId = it.connectionId
|
connectionId = it.connectionId
|
||||||
|
subscriptionId = it.subId
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
connected = true
|
connected = true
|
||||||
}
|
}
|
||||||
@@ -126,6 +139,7 @@ class GenericWsHead(
|
|||||||
val connectionStates = wsSubscriptions.connectionInfoFlux()
|
val connectionStates = wsSubscriptions.connectionInfoFlux()
|
||||||
.map {
|
.map {
|
||||||
if (it.connectionId == connectionId && it.connectionState == WsConnection.ConnectionState.DISCONNECTED) {
|
if (it.connectionId == connectionId && it.connectionState == WsConnection.ConnectionState.DISCONNECTED) {
|
||||||
|
headLivenessSink.emitNext(false) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
||||||
subscribed = false
|
subscribed = false
|
||||||
connected = false
|
connected = false
|
||||||
connectionId = null
|
connectionId = null
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ class HeadLivenessValidator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getFlux(): Flux<Boolean> {
|
fun getFlux(): Flux<Boolean> {
|
||||||
|
val headLiveness = head.headLiveness()
|
||||||
// first we have moving window of 2 blocks and check that they are consecutive ones
|
// first we have moving window of 2 blocks and check that they are consecutive ones
|
||||||
return head.getFlux().map { it.height }.buffer(2, 1).map {
|
val headFlux = head.getFlux().map { it.height }.buffer(2, 1).map {
|
||||||
it.last() - it.first() == 1L
|
it.last() - it.first() == 1L
|
||||||
}.scan(Pair(0, true)) { acc, value ->
|
}.scan(Pair(0, true)) { acc, value ->
|
||||||
// then we accumulate consecutive true events, false resets counter
|
// then we accumulate consecutive true events, false resets counter
|
||||||
@@ -52,5 +53,7 @@ class HeadLivenessValidator(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
).repeat().subscribeOn(scheduler)
|
).repeat().subscribeOn(scheduler)
|
||||||
|
|
||||||
|
return Flux.merge(headFlux, headLiveness)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ open class WsConnectionFactory(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun createWsConnection(connIndex: Int = 0, onDisconnect: () -> Unit): WsConnection =
|
open fun createWsConnection(connIndex: Int = 0): WsConnection =
|
||||||
WsConnectionImpl(uri, origin, basicAuth, metrics(connIndex), onDisconnect, scheduler).also { ws ->
|
WsConnectionImpl(uri, origin, basicAuth, metrics(connIndex), scheduler).also { ws ->
|
||||||
config?.frameSize?.let {
|
config?.frameSize?.let {
|
||||||
ws.frameSize = it
|
ws.frameSize = it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ open class WsConnectionImpl(
|
|||||||
private val origin: URI,
|
private val origin: URI,
|
||||||
private val basicAuth: AuthConfig.ClientBasicAuth?,
|
private val basicAuth: AuthConfig.ClientBasicAuth?,
|
||||||
private val rpcMetrics: RpcMetrics?,
|
private val rpcMetrics: RpcMetrics?,
|
||||||
private val onDisconnect: () -> Unit,
|
|
||||||
private val scheduler: Scheduler,
|
private val scheduler: Scheduler,
|
||||||
) : AutoCloseable, WsConnection, Cloneable {
|
) : AutoCloseable, WsConnection, Cloneable {
|
||||||
|
|
||||||
@@ -198,7 +197,6 @@ open class WsConnectionImpl(
|
|||||||
connection = HttpClient.create()
|
connection = HttpClient.create()
|
||||||
.resolver(DefaultAddressResolverGroup.INSTANCE)
|
.resolver(DefaultAddressResolverGroup.INSTANCE)
|
||||||
.doOnDisconnected {
|
.doOnDisconnected {
|
||||||
onDisconnect()
|
|
||||||
disconnects.tryEmitNext(Instant.now())
|
disconnects.tryEmitNext(Instant.now())
|
||||||
log.info("Disconnected from $uri")
|
log.info("Disconnected from $uri")
|
||||||
if (keepConnection) {
|
if (keepConnection) {
|
||||||
|
|||||||
@@ -16,8 +16,6 @@
|
|||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.Global
|
import io.emeraldpay.dshackle.Global
|
||||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
|
||||||
import org.springframework.util.backoff.BackOffExecution
|
import org.springframework.util.backoff.BackOffExecution
|
||||||
import org.springframework.util.backoff.ExponentialBackOff
|
import org.springframework.util.backoff.ExponentialBackOff
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
@@ -39,7 +37,6 @@ import kotlin.concurrent.write
|
|||||||
*/
|
*/
|
||||||
class WsConnectionMultiPool(
|
class WsConnectionMultiPool(
|
||||||
private val wsConnectionFactory: WsConnectionFactory,
|
private val wsConnectionFactory: WsConnectionFactory,
|
||||||
private val upstream: DefaultUpstream,
|
|
||||||
private val connections: Int,
|
private val connections: Int,
|
||||||
) : WsConnectionPool {
|
) : WsConnectionPool {
|
||||||
|
|
||||||
@@ -110,16 +107,14 @@ class WsConnectionMultiPool(
|
|||||||
SCHEDULE_FULL
|
SCHEDULE_FULL
|
||||||
} else {
|
} else {
|
||||||
current.add(
|
current.add(
|
||||||
wsConnectionFactory.createWsConnection(connIndex++) {
|
wsConnectionFactory.createWsConnection(connIndex++)
|
||||||
if (isUnavailable()) {
|
.also {
|
||||||
upstream.setStatus(UpstreamAvailability.UNAVAILABLE)
|
it.connect()
|
||||||
}
|
connectionSubscriptionMap[it.connectionId()] = it.connectionInfoFlux()
|
||||||
}.also {
|
.subscribe { info ->
|
||||||
it.connect()
|
connectionInfo.emitNext(info) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
||||||
connectionSubscriptionMap[it.connectionId()] = it.connectionInfoFlux().subscribe { info ->
|
}
|
||||||
connectionInfo.emitNext(info) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
SCHEDULE_GROW
|
SCHEDULE_GROW
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ class WsConnectionPoolFactory(
|
|||||||
"Creating instance for different upstream. ${upstream.getId()} != id"
|
"Creating instance for different upstream. ${upstream.getId()} != id"
|
||||||
}
|
}
|
||||||
return if (connections > 1) {
|
return if (connections > 1) {
|
||||||
WsConnectionMultiPool(wsConnectionFactory, upstream, connections)
|
WsConnectionMultiPool(wsConnectionFactory, connections)
|
||||||
} else {
|
} else {
|
||||||
WsConnectionSinglePool(wsConnectionFactory, upstream)
|
WsConnectionSinglePool(wsConnectionFactory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
class WsConnectionSinglePool(
|
class WsConnectionSinglePool(
|
||||||
wsConnectionFactory: WsConnectionFactory,
|
wsConnectionFactory: WsConnectionFactory,
|
||||||
private val upstream: DefaultUpstream,
|
|
||||||
) : WsConnectionPool {
|
) : WsConnectionPool {
|
||||||
private val connection = wsConnectionFactory.createWsConnection {
|
private val connection = wsConnectionFactory.createWsConnection()
|
||||||
upstream.setStatus(UpstreamAvailability.UNAVAILABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
if (!connection.isConnected) {
|
if (!connection.isConnected) {
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A JSON-RPC Subscription client.
|
* A JSON-RPC Subscription client.
|
||||||
@@ -42,8 +45,11 @@ interface WsSubscriptions {
|
|||||||
|
|
||||||
fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo>
|
fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo>
|
||||||
|
|
||||||
|
fun unsubscribe(request: JsonRpcRequest): Mono<JsonRpcResponse>
|
||||||
|
|
||||||
data class SubscribeData(
|
data class SubscribeData(
|
||||||
val data: Flux<ByteArray>,
|
val data: Flux<ByteArray>,
|
||||||
val connectionId: String,
|
val connectionId: String,
|
||||||
|
val subId: AtomicReference<String>,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
|||||||
|
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
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
|
||||||
@@ -49,7 +50,15 @@ class WsSubscriptionsImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return WsSubscriptions.SubscribeData(messageFlux, conn.connectionId())
|
return WsSubscriptions.SubscribeData(messageFlux, conn.connectionId(), subscriptionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unsubscribe(request: JsonRpcRequest): Mono<JsonRpcResponse> {
|
||||||
|
if (request.params.isEmpty() || request.params.contains("")) {
|
||||||
|
return Mono.empty()
|
||||||
|
}
|
||||||
|
return wsPool.getConnection()
|
||||||
|
.callRpc(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo> =
|
override fun connectionInfoFlux(): Flux<WsConnection.ConnectionInfo> =
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ interface ChainSpecific {
|
|||||||
|
|
||||||
fun listenNewHeadsRequest(): JsonRpcRequest
|
fun listenNewHeadsRequest(): JsonRpcRequest
|
||||||
|
|
||||||
|
fun unsubscribeNewHeadsRequest(subId: String): JsonRpcRequest
|
||||||
|
|
||||||
fun localReaderBuilder(cachingReader: CachingReader, methods: CallMethods, head: Head): Mono<JsonRpcReader>
|
fun localReaderBuilder(cachingReader: CachingReader, methods: CallMethods, head: Head): Mono<JsonRpcReader>
|
||||||
|
|
||||||
fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription
|
fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import org.springframework.context.ApplicationEventPublisher
|
|||||||
import org.springframework.context.Lifecycle
|
import org.springframework.context.Lifecycle
|
||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Sinks
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ open class GenericUpstream(
|
|||||||
}
|
}
|
||||||
livenessSubscription = connector.hasLiveSubscriptionHead().subscribe({
|
livenessSubscription = connector.hasLiveSubscriptionHead().subscribe({
|
||||||
hasLiveSubscriptionHead.set(it)
|
hasLiveSubscriptionHead.set(it)
|
||||||
eventPublisher?.publishEvent(UpstreamChangeEvent(chain, this, UpstreamChangeEvent.ChangeType.UPDATED))
|
stateStream.emitNext(true) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
||||||
}, {
|
}, {
|
||||||
log.debug("Error while checking live subscription for ${getId()}", it)
|
log.debug("Error while checking live subscription for ${getId()}", it)
|
||||||
},)
|
},)
|
||||||
@@ -197,5 +198,5 @@ open class GenericUpstream(
|
|||||||
return connector.getIngressSubscription()
|
return connector.getIngressSubscription()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isRunning() = connector.isRunning()
|
override fun isRunning() = connector.isRunning() && validationSettingsSubscription == null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ open class GenericConnectorFactory(
|
|||||||
private val blockValidator: BlockValidator,
|
private val blockValidator: BlockValidator,
|
||||||
private val wsConnectionResubscribeScheduler: Scheduler,
|
private val wsConnectionResubscribeScheduler: Scheduler,
|
||||||
private val headScheduler: Scheduler,
|
private val headScheduler: Scheduler,
|
||||||
|
private val headLivenessScheduler: Scheduler,
|
||||||
private val expectedBlockTime: Duration,
|
private val expectedBlockTime: Duration,
|
||||||
) : ConnectorFactory {
|
) : ConnectorFactory {
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ open class GenericConnectorFactory(
|
|||||||
blockValidator,
|
blockValidator,
|
||||||
wsConnectionResubscribeScheduler,
|
wsConnectionResubscribeScheduler,
|
||||||
headScheduler,
|
headScheduler,
|
||||||
|
headLivenessScheduler,
|
||||||
expectedBlockTime,
|
expectedBlockTime,
|
||||||
specific,
|
specific,
|
||||||
)
|
)
|
||||||
@@ -74,6 +76,7 @@ open class GenericConnectorFactory(
|
|||||||
blockValidator,
|
blockValidator,
|
||||||
wsConnectionResubscribeScheduler,
|
wsConnectionResubscribeScheduler,
|
||||||
headScheduler,
|
headScheduler,
|
||||||
|
headLivenessScheduler,
|
||||||
expectedBlockTime,
|
expectedBlockTime,
|
||||||
specific,
|
specific,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class GenericRpcConnector(
|
|||||||
blockValidator: BlockValidator,
|
blockValidator: BlockValidator,
|
||||||
wsConnectionResubscribeScheduler: Scheduler,
|
wsConnectionResubscribeScheduler: Scheduler,
|
||||||
headScheduler: Scheduler,
|
headScheduler: Scheduler,
|
||||||
|
headLivenessScheduler: Scheduler,
|
||||||
expectedBlockTime: Duration,
|
expectedBlockTime: Duration,
|
||||||
chainSpecific: ChainSpecific,
|
chainSpecific: ChainSpecific,
|
||||||
) : GenericConnector, CachesEnabled {
|
) : GenericConnector, CachesEnabled {
|
||||||
@@ -52,7 +53,7 @@ class GenericRpcConnector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun hasLiveSubscriptionHead(): Flux<Boolean> {
|
override fun hasLiveSubscriptionHead(): Flux<Boolean> {
|
||||||
return liveness.getFlux()
|
return liveness.getFlux().distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -107,7 +108,7 @@ class GenericRpcConnector(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
liveness = HeadLivenessValidator(head, expectedBlockTime, headScheduler, id)
|
liveness = HeadLivenessValidator(head, expectedBlockTime, headLivenessScheduler, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setCaches(caches: Caches) {
|
override fun setCaches(caches: Caches) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class GenericWsConnector(
|
|||||||
blockValidator: BlockValidator,
|
blockValidator: BlockValidator,
|
||||||
wsConnectionResubscribeScheduler: Scheduler,
|
wsConnectionResubscribeScheduler: Scheduler,
|
||||||
headScheduler: Scheduler,
|
headScheduler: Scheduler,
|
||||||
|
headLivenessScheduler: Scheduler,
|
||||||
expectedBlockTime: Duration,
|
expectedBlockTime: Duration,
|
||||||
chainSpecific: ChainSpecific,
|
chainSpecific: ChainSpecific,
|
||||||
) : GenericConnector {
|
) : GenericConnector {
|
||||||
@@ -46,12 +47,12 @@ class GenericWsConnector(
|
|||||||
upstream,
|
upstream,
|
||||||
chainSpecific,
|
chainSpecific,
|
||||||
)
|
)
|
||||||
liveness = HeadLivenessValidator(head, expectedBlockTime, headScheduler, upstream.getId())
|
liveness = HeadLivenessValidator(head, expectedBlockTime, headLivenessScheduler, upstream.getId())
|
||||||
subscriptions = chainSpecific.makeIngressSubscription(wsSubscriptions)
|
subscriptions = chainSpecific.makeIngressSubscription(wsSubscriptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasLiveSubscriptionHead(): Flux<Boolean> {
|
override fun hasLiveSubscriptionHead(): Flux<Boolean> {
|
||||||
return liveness.getFlux()
|
return liveness.getFlux().distinctUntilChanged()
|
||||||
}
|
}
|
||||||
override fun start() {
|
override fun start() {
|
||||||
pool.connect()
|
pool.connect()
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ object PolkadotChainSpecific : ChainSpecific {
|
|||||||
override fun listenNewHeadsRequest(): JsonRpcRequest =
|
override fun listenNewHeadsRequest(): JsonRpcRequest =
|
||||||
JsonRpcRequest("chain_subscribeNewHeads", listOf())
|
JsonRpcRequest("chain_subscribeNewHeads", listOf())
|
||||||
|
|
||||||
|
override fun unsubscribeNewHeadsRequest(subId: String): JsonRpcRequest =
|
||||||
|
JsonRpcRequest("chain_unsubscribeNewHeads", listOf(subId))
|
||||||
|
|
||||||
override fun localReaderBuilder(
|
override fun localReaderBuilder(
|
||||||
cachingReader: CachingReader,
|
cachingReader: CachingReader,
|
||||||
methods: CallMethods,
|
methods: CallMethods,
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ object StarknetChainSpecific : ChainSpecific {
|
|||||||
throw NotImplementedError()
|
throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun unsubscribeNewHeadsRequest(subId: String): JsonRpcRequest {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
|
|
||||||
override fun localReaderBuilder(
|
override fun localReaderBuilder(
|
||||||
cachingReader: CachingReader,
|
cachingReader: CachingReader,
|
||||||
methods: CallMethods,
|
methods: CallMethods,
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
AuthorizationConfig.default(),
|
AuthorizationConfig.default(),
|
||||||
new GrpcAuthContext()
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
@@ -69,6 +71,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
AuthorizationConfig.default(),
|
AuthorizationConfig.default(),
|
||||||
new GrpcAuthContext()
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
@@ -102,6 +106,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
AuthorizationConfig.default(),
|
AuthorizationConfig.default(),
|
||||||
new GrpcAuthContext()
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
@@ -130,6 +136,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
AuthorizationConfig.default(),
|
AuthorizationConfig.default(),
|
||||||
new GrpcAuthContext()
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
@@ -163,6 +171,8 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
null,
|
null,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
AuthorizationConfig.default(),
|
AuthorizationConfig.default(),
|
||||||
new GrpcAuthContext()
|
new GrpcAuthContext()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -82,4 +82,9 @@ class EthereumHeadMock implements Head {
|
|||||||
void onSyncingNode(boolean isSyncing) {
|
void onSyncingNode(boolean isSyncing) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Flux<Boolean> headLiveness() {
|
||||||
|
return Flux.empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class FilteredApisSpec extends Specification {
|
|||||||
BlockValidator.ALWAYS_VALID,
|
BlockValidator.ALWAYS_VALID,
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
Schedulers.boundedElastic(),
|
Schedulers.boundedElastic(),
|
||||||
|
Schedulers.boundedElastic(),
|
||||||
Duration.ofSeconds(12)
|
Duration.ofSeconds(12)
|
||||||
)
|
)
|
||||||
new GenericUpstream(
|
new GenericUpstream(
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ import io.emeraldpay.dshackle.upstream.BlockValidator
|
|||||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.AlwaysForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.AlwaysForkChoice
|
||||||
import io.emeraldpay.etherjar.domain.BlockHash
|
|
||||||
import io.emeraldpay.etherjar.domain.TransactionId
|
|
||||||
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
|
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||||
|
import io.emeraldpay.etherjar.domain.BlockHash
|
||||||
|
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
@@ -38,6 +38,7 @@ import spock.lang.Specification
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
class GenericWsHeadSpec extends Specification {
|
class GenericWsHeadSpec extends Specification {
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ class GenericWsHeadSpec extends Specification {
|
|||||||
act == res
|
act == res
|
||||||
|
|
||||||
1 * ws.subscribe(_) >> new WsSubscriptions.SubscribeData(
|
1 * ws.subscribe(_) >> new WsSubscriptions.SubscribeData(
|
||||||
Flux.fromIterable([headBlock]), "id"
|
Flux.fromIterable([headBlock]), "id", new AtomicReference<String>("")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +97,8 @@ class GenericWsHeadSpec extends Specification {
|
|||||||
def ws = Mock(WsSubscriptions) {
|
def ws = Mock(WsSubscriptions) {
|
||||||
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
||||||
2 * subscribe(_) >>> [
|
2 * subscribe(_) >>> [
|
||||||
new WsSubscriptions.SubscribeData(Flux.error(new RuntimeException()), "id"),
|
new WsSubscriptions.SubscribeData(Flux.error(new RuntimeException()), "id", new AtomicReference<String>("")),
|
||||||
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id")
|
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id", new AtomicReference<String>(""))
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,8 +151,8 @@ class GenericWsHeadSpec extends Specification {
|
|||||||
def ws = Mock(WsSubscriptions) {
|
def ws = Mock(WsSubscriptions) {
|
||||||
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
||||||
2 * subscribe(_) >>> [
|
2 * subscribe(_) >>> [
|
||||||
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
|
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id", new AtomicReference<String>("")),
|
||||||
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id")
|
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id", new AtomicReference<String>(""))
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +192,7 @@ class GenericWsHeadSpec extends Specification {
|
|||||||
def ws = Mock(WsSubscriptions) {
|
def ws = Mock(WsSubscriptions) {
|
||||||
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
||||||
1 * subscribe(_) >>> [
|
1 * subscribe(_) >>> [
|
||||||
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
|
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id", new AtomicReference<String>("")),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +231,7 @@ class GenericWsHeadSpec extends Specification {
|
|||||||
def ws = Mock(WsSubscriptions) {
|
def ws = Mock(WsSubscriptions) {
|
||||||
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
||||||
1 * subscribe(_) >>> [
|
1 * subscribe(_) >>> [
|
||||||
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
|
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id", new AtomicReference<String>("")),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,8 +283,8 @@ class GenericWsHeadSpec extends Specification {
|
|||||||
def ws = Mock(WsSubscriptions) {
|
def ws = Mock(WsSubscriptions) {
|
||||||
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
||||||
2 * subscribe(_) >>> [
|
2 * subscribe(_) >>> [
|
||||||
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"),
|
new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id", new AtomicReference<String>("")),
|
||||||
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id"),
|
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id", new AtomicReference<String>("")),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,4 +317,74 @@ class GenericWsHeadSpec extends Specification {
|
|||||||
.thenCancel()
|
.thenCancel()
|
||||||
.verify(Duration.ofSeconds(1))
|
.verify(Duration.ofSeconds(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def "Unsubscribe if there is an error during subscription"() {
|
||||||
|
setup:
|
||||||
|
def block = new BlockJson<TransactionRefJson>()
|
||||||
|
block.number = 100
|
||||||
|
block.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
|
||||||
|
block.parentHash = parent
|
||||||
|
block.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS)
|
||||||
|
block.uncles = []
|
||||||
|
block.totalDifficulty = BigInteger.ONE
|
||||||
|
|
||||||
|
def apiMock = TestingCommons.api()
|
||||||
|
def subId = "subId"
|
||||||
|
def ws = Mock(WsSubscriptions) {
|
||||||
|
1 * it.connectionInfoFlux() >> Flux.empty()
|
||||||
|
1 * it.subscribe(_) >> new WsSubscriptions.SubscribeData(
|
||||||
|
Flux.error(new RuntimeException()), "id", new AtomicReference<String>(subId)
|
||||||
|
)
|
||||||
|
1 * it.unsubscribe(new JsonRpcRequest("eth_unsubscribe", List.of(subId), 2, null, null)) >>
|
||||||
|
Mono.just(new JsonRpcResponse("".bytes, null))
|
||||||
|
}
|
||||||
|
|
||||||
|
def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE)
|
||||||
|
|
||||||
|
when:
|
||||||
|
def act = head.listenNewHeads()
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(act)
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
def "If there is ws disconnect then head must emit false its liveness state"() {
|
||||||
|
setup:
|
||||||
|
def secondBlock = new BlockJson<TransactionRefJson>()
|
||||||
|
secondBlock.parentHash = parent
|
||||||
|
secondBlock.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS)
|
||||||
|
secondBlock.number = 105
|
||||||
|
secondBlock.hash = BlockHash.from("0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8")
|
||||||
|
|
||||||
|
def secondHeadBlock = secondBlock.with {
|
||||||
|
Global.objectMapper.writeValueAsBytes(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
def apiMock = TestingCommons.api()
|
||||||
|
|
||||||
|
def connectionInfoSink = Sinks.many().multicast().directBestEffort()
|
||||||
|
def ws = Mock(WsSubscriptions) {
|
||||||
|
1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux()
|
||||||
|
1 * subscribe(_) >>> [
|
||||||
|
new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id", new AtomicReference<String>(""))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE)
|
||||||
|
|
||||||
|
when:
|
||||||
|
head.start()
|
||||||
|
def liveness = head.headLiveness()
|
||||||
|
|
||||||
|
then:
|
||||||
|
StepVerifier.create(liveness)
|
||||||
|
.then {
|
||||||
|
connectionInfoSink.tryEmitNext(new WsConnection.ConnectionInfo("id", WsConnection.ConnectionState.DISCONNECTED))
|
||||||
|
}
|
||||||
|
.expectNext(false)
|
||||||
|
.thenCancel()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package io.emeraldpay.dshackle.upstream.ethereum
|
|||||||
|
|
||||||
import io.emeraldpay.dshackle.test.EthereumHeadMock
|
import io.emeraldpay.dshackle.test.EthereumHeadMock
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
|
import io.emeraldpay.dshackle.upstream.Head
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.scheduler.Schedulers
|
import reactor.core.scheduler.Schedulers
|
||||||
import reactor.test.StepVerifier
|
import reactor.test.StepVerifier
|
||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
|
||||||
|
|
||||||
class HeadLivenessValidatorSpec extends Specification{
|
class HeadLivenessValidatorSpec extends Specification{
|
||||||
def "emits true"() {
|
def "emits true"() {
|
||||||
@@ -24,6 +24,20 @@ class HeadLivenessValidatorSpec extends Specification{
|
|||||||
}.expectNext(true).thenCancel().verify(Duration.ofSeconds(1))
|
}.expectNext(true).thenCancel().verify(Duration.ofSeconds(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def "emits false if head liveness emits false"() {
|
||||||
|
when:
|
||||||
|
def head = Mock(Head) {
|
||||||
|
1 * it.headLiveness() >> Flux.just(false)
|
||||||
|
1 * it.getFlux() >> Flux.just(TestingCommons.blockForEthereum(1))
|
||||||
|
}
|
||||||
|
def checker = new HeadLivenessValidator(head, Duration.ofSeconds(10), Schedulers.boundedElastic(), "test")
|
||||||
|
then:
|
||||||
|
StepVerifier.create(checker.flux)
|
||||||
|
.expectNext(false)
|
||||||
|
.thenCancel()
|
||||||
|
.verify(Duration.ofSeconds(1))
|
||||||
|
}
|
||||||
|
|
||||||
def "starts accumulating trues but immediately emits after false"() {
|
def "starts accumulating trues but immediately emits after false"() {
|
||||||
when:
|
when:
|
||||||
def head = new EthereumHeadMock()
|
def head = new EthereumHeadMock()
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import io.emeraldpay.dshackle.test.GenericUpstreamMock
|
|||||||
import io.emeraldpay.dshackle.test.MockWSServer
|
import io.emeraldpay.dshackle.test.MockWSServer
|
||||||
import io.emeraldpay.dshackle.test.TestingCommons
|
import io.emeraldpay.dshackle.test.TestingCommons
|
||||||
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
import io.emeraldpay.dshackle.upstream.DefaultUpstream
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
|
||||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
|
||||||
import reactor.core.scheduler.Schedulers
|
import reactor.core.scheduler.Schedulers
|
||||||
import reactor.test.StepVerifier
|
import reactor.test.StepVerifier
|
||||||
@@ -110,33 +109,6 @@ class WsConnectionImplRealSpec extends Specification {
|
|||||||
.verify(Duration.ofSeconds(1))
|
.verify(Duration.ofSeconds(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
def "Gets UNAVAIL status right after disconnect"() {
|
|
||||||
setup:
|
|
||||||
def up = Mock(DefaultUpstream) {
|
|
||||||
_ * getId() >> "test"
|
|
||||||
}
|
|
||||||
conn = new WsConnectionPoolFactory(
|
|
||||||
"test",
|
|
||||||
1,
|
|
||||||
new WsConnectionFactory(
|
|
||||||
"test",
|
|
||||||
Chain.ETHEREUM__MAINNET,
|
|
||||||
"ws://localhost:${port}".toURI(),
|
|
||||||
"http://localhost:${port}".toURI(),
|
|
||||||
Schedulers.boundedElastic()
|
|
||||||
)
|
|
||||||
).create(up).getConnection()
|
|
||||||
when:
|
|
||||||
conn.connect()
|
|
||||||
conn.reconnectIntervalSeconds = 10
|
|
||||||
Thread.sleep(SLEEP)
|
|
||||||
server.stop()
|
|
||||||
Thread.sleep(100)
|
|
||||||
|
|
||||||
then:
|
|
||||||
1 * up.setStatus(UpstreamAvailability.UNAVAILABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
def "Try to connects to server until it's available"() {
|
def "Try to connects to server until it's available"() {
|
||||||
when:
|
when:
|
||||||
server.stop()
|
server.stop()
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
}
|
}
|
||||||
def up = Mock(DefaultUpstream)
|
def up = Mock(DefaultUpstream)
|
||||||
def factory = Mock(WsConnectionFactory)
|
def factory = Mock(WsConnectionFactory)
|
||||||
def pool = new WsConnectionMultiPool(factory, up, 3)
|
def pool = new WsConnectionMultiPool(factory, 3)
|
||||||
pool.scheduler = Stub(ScheduledExecutorService)
|
pool.scheduler = Stub(ScheduledExecutorService)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
pool.connect()
|
pool.connect()
|
||||||
|
|
||||||
then:
|
then:
|
||||||
1 * factory.createWsConnection(0, _) >> conn
|
1 * factory.createWsConnection(0) >> conn
|
||||||
1 * conn.connect()
|
1 * conn.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,14 +54,14 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
}
|
}
|
||||||
def up = Mock(DefaultUpstream)
|
def up = Mock(DefaultUpstream)
|
||||||
def factory = Mock(WsConnectionFactory)
|
def factory = Mock(WsConnectionFactory)
|
||||||
def pool = new WsConnectionMultiPool(factory, up, 3)
|
def pool = new WsConnectionMultiPool(factory, 3)
|
||||||
pool.scheduler = Stub(ScheduledExecutorService)
|
pool.scheduler = Stub(ScheduledExecutorService)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
pool.connect()
|
pool.connect()
|
||||||
|
|
||||||
then:
|
then:
|
||||||
1 * factory.createWsConnection(0, _) >> conn1
|
1 * factory.createWsConnection(0) >> conn1
|
||||||
1 * conn1.connect()
|
1 * conn1.connect()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
@@ -69,7 +69,7 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
|
|
||||||
then:
|
then:
|
||||||
1 * conn1.isConnected() >> true
|
1 * conn1.isConnected() >> true
|
||||||
1 * factory.createWsConnection(1, _) >> conn2
|
1 * factory.createWsConnection(1) >> conn2
|
||||||
1 * conn2.connect()
|
1 * conn2.connect()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
@@ -78,7 +78,7 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
1 * conn1.isConnected() >> true
|
1 * conn1.isConnected() >> true
|
||||||
1 * conn2.isConnected() >> true
|
1 * conn2.isConnected() >> true
|
||||||
1 * factory.createWsConnection(2, _) >> conn3
|
1 * factory.createWsConnection(2) >> conn3
|
||||||
1 * conn3.connect()
|
1 * conn3.connect()
|
||||||
|
|
||||||
when:
|
when:
|
||||||
@@ -88,7 +88,7 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
1 * conn1.isConnected() >> true
|
1 * conn1.isConnected() >> true
|
||||||
1 * conn2.isConnected() >> true
|
1 * conn2.isConnected() >> true
|
||||||
1 * conn3.isConnected() >> true
|
1 * conn3.isConnected() >> true
|
||||||
0 * factory.createWsConnection(_, _)
|
0 * factory.createWsConnection(_)
|
||||||
}
|
}
|
||||||
|
|
||||||
def "recreate connection after failure"() {
|
def "recreate connection after failure"() {
|
||||||
@@ -107,7 +107,7 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
}
|
}
|
||||||
def up = Mock(DefaultUpstream)
|
def up = Mock(DefaultUpstream)
|
||||||
def factory = Mock(WsConnectionFactory)
|
def factory = Mock(WsConnectionFactory)
|
||||||
def pool = new WsConnectionMultiPool(factory, up, 3)
|
def pool = new WsConnectionMultiPool(factory, 3)
|
||||||
pool.scheduler = Stub(ScheduledExecutorService)
|
pool.scheduler = Stub(ScheduledExecutorService)
|
||||||
|
|
||||||
when: "initial fill"
|
when: "initial fill"
|
||||||
@@ -119,9 +119,9 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
_ * conn1.isConnected() >> true
|
_ * conn1.isConnected() >> true
|
||||||
_ * conn2.isConnected() >> true
|
_ * conn2.isConnected() >> true
|
||||||
_ * conn3.isConnected() >> true
|
_ * conn3.isConnected() >> true
|
||||||
1 * factory.createWsConnection(0, _) >> conn1
|
1 * factory.createWsConnection(0) >> conn1
|
||||||
1 * factory.createWsConnection(1, _) >> conn2
|
1 * factory.createWsConnection(1) >> conn2
|
||||||
1 * factory.createWsConnection(2, _) >> conn3
|
1 * factory.createWsConnection(2) >> conn3
|
||||||
1 * conn1.connect()
|
1 * conn1.connect()
|
||||||
1 * conn2.connect()
|
1 * conn2.connect()
|
||||||
1 * conn3.connect()
|
1 * conn3.connect()
|
||||||
@@ -133,7 +133,7 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
1 * conn1.isConnected() >> true
|
1 * conn1.isConnected() >> true
|
||||||
1 * conn2.isConnected() >> true
|
1 * conn2.isConnected() >> true
|
||||||
1 * conn3.isConnected() >> true
|
1 * conn3.isConnected() >> true
|
||||||
0 * factory.createWsConnection(_, _)
|
0 * factory.createWsConnection(_)
|
||||||
|
|
||||||
when: "one failed"
|
when: "one failed"
|
||||||
pool.connect()
|
pool.connect()
|
||||||
@@ -142,7 +142,7 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
(1.._) * conn1.isConnected() >> true
|
(1.._) * conn1.isConnected() >> true
|
||||||
(1.._) * conn2.isConnected() >> false
|
(1.._) * conn2.isConnected() >> false
|
||||||
(1.._) * conn3.isConnected() >> true
|
(1.._) * conn3.isConnected() >> true
|
||||||
0 * factory.createWsConnection(_, _) // doesn't create immediately, but schedules it for the next adjust
|
0 * factory.createWsConnection(_) // doesn't create immediately, but schedules it for the next adjust
|
||||||
1 * conn2.close()
|
1 * conn2.close()
|
||||||
|
|
||||||
when: "needs one more"
|
when: "needs one more"
|
||||||
@@ -151,7 +151,7 @@ class WsConnectionMultiPoolSpec extends Specification {
|
|||||||
then:
|
then:
|
||||||
1 * conn1.isConnected() >> true
|
1 * conn1.isConnected() >> true
|
||||||
1 * conn3.isConnected() >> true
|
1 * conn3.isConnected() >> true
|
||||||
1 * factory.createWsConnection(3, _) >> conn4
|
1 * factory.createWsConnection(3) >> conn4
|
||||||
1 * conn4.connect()
|
1 * conn4.connect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import reactor.core.publisher.Flux
|
|||||||
import spock.lang.Specification
|
import spock.lang.Specification
|
||||||
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
class WebsocketPendingTxesSpec extends Specification {
|
class WebsocketPendingTxesSpec extends Specification {
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ class WebsocketPendingTxesSpec extends Specification {
|
|||||||
|
|
||||||
then:
|
then:
|
||||||
1 * ws.subscribe(new JsonRpcRequest("eth_subscribe", ["newPendingTransactions"])) >> new WsSubscriptions.SubscribeData(
|
1 * ws.subscribe(new JsonRpcRequest("eth_subscribe", ["newPendingTransactions"])) >> new WsSubscriptions.SubscribeData(
|
||||||
Flux.fromIterable(responses), "id"
|
Flux.fromIterable(responses), "id", new AtomicReference<String>("")
|
||||||
)
|
)
|
||||||
txes.collect {it.toHex() } == [
|
txes.collect {it.toHex() } == [
|
||||||
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
|
"0xa61bab14fc9720ea8725622688c2f964666d7c2afdae38af7dad53f12f242d5c",
|
||||||
|
|||||||
Reference in New Issue
Block a user