better stuck head handling
This commit is contained in:
@@ -21,13 +21,17 @@ import org.slf4j.LoggerFactory
|
|||||||
import reactor.core.Disposable
|
import reactor.core.Disposable
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
|
import reactor.core.publisher.Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER
|
||||||
|
import reactor.core.publisher.Sinks.EmitResult.OK
|
||||||
import reactor.core.scheduler.Schedulers
|
import reactor.core.scheduler.Schedulers
|
||||||
import reactor.kotlin.core.publisher.toMono
|
import reactor.kotlin.core.publisher.toMono
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
abstract class AbstractHead(
|
abstract class AbstractHead(
|
||||||
private val forkChoice: ForkChoice,
|
private val forkChoice: ForkChoice,
|
||||||
private val blockValidator: BlockValidator = BlockValidator.ALWAYS_VALID
|
private val blockValidator: BlockValidator = BlockValidator.ALWAYS_VALID,
|
||||||
|
awaitHeadTimeoutMs: Long = 60_000
|
||||||
) : Head {
|
) : Head {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -37,7 +41,20 @@ abstract class AbstractHead(
|
|||||||
private var stream = Sinks.many().multicast().directBestEffort<BlockContainer>()
|
private var stream = Sinks.many().multicast().directBestEffort<BlockContainer>()
|
||||||
private var completed = false
|
private var completed = false
|
||||||
private val beforeBlockHandlers = ArrayList<Runnable>()
|
private val beforeBlockHandlers = ArrayList<Runnable>()
|
||||||
private val lastUpdateTime = AtomicLong(0L)
|
private var stopping = false
|
||||||
|
private var lastHeadUpdated = 0L
|
||||||
|
|
||||||
|
init {
|
||||||
|
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
|
||||||
|
{
|
||||||
|
val delay = System.currentTimeMillis() - lastHeadUpdated
|
||||||
|
if (delay > awaitHeadTimeoutMs) {
|
||||||
|
log.warn("No head updates for $delay ms @ ${this.javaClass} - restart")
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}, 300, 30, TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun follow(source: Flux<BlockContainer>): Disposable {
|
fun follow(source: Flux<BlockContainer>): Disposable {
|
||||||
if (completed) {
|
if (completed) {
|
||||||
@@ -54,8 +71,14 @@ abstract class AbstractHead(
|
|||||||
// close internal stream if upstream is finished, otherwise it gets stuck,
|
// close internal stream if upstream is finished, otherwise it gets stuck,
|
||||||
// but technically it should never happen during normal work, only when the Head
|
// but technically it should never happen during normal work, only when the Head
|
||||||
// is stopping
|
// is stopping
|
||||||
completed = true
|
if (stopping) {
|
||||||
stream.tryEmitComplete()
|
log.info("Received signal $it - stop emit new head!!!")
|
||||||
|
completed = true
|
||||||
|
stream.tryEmitComplete()
|
||||||
|
} else {
|
||||||
|
log.warn("Received signal $it unexpectedly - restart head")
|
||||||
|
start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.boundedElastic())
|
.subscribeOn(Schedulers.boundedElastic())
|
||||||
.subscribe { block ->
|
.subscribe { block ->
|
||||||
@@ -69,12 +92,12 @@ abstract class AbstractHead(
|
|||||||
when (val choiceResult = forkChoice.choose(block)) {
|
when (val choiceResult = forkChoice.choose(block)) {
|
||||||
is ForkChoice.ChoiceResult.Updated -> {
|
is ForkChoice.ChoiceResult.Updated -> {
|
||||||
val newHead = choiceResult.nwhead
|
val newHead = choiceResult.nwhead
|
||||||
log.debug("New block ${newHead.height} ${newHead.hash}")
|
lastHeadUpdated = System.currentTimeMillis()
|
||||||
val result = stream.tryEmitNext(newHead)
|
when (val result = stream.tryEmitNext(newHead)) {
|
||||||
if (result.isFailure && result != Sinks.EmitResult.FAIL_ZERO_SUBSCRIBER) {
|
OK -> log.debug("New block ${newHead.height} ${newHead.hash} @ ${this.javaClass}")
|
||||||
log.warn("Failed to dispatch block: $result as ${this.javaClass}")
|
FAIL_ZERO_SUBSCRIBER -> log.debug("No subscribers for ${this.javaClass}")
|
||||||
|
else -> log.warn("Failed to dispatch block: $result as ${this.javaClass}")
|
||||||
}
|
}
|
||||||
lastUpdateTime.set(System.currentTimeMillis())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is ForkChoice.ChoiceResult.Same -> {}
|
is ForkChoice.ChoiceResult.Same -> {}
|
||||||
@@ -100,7 +123,6 @@ abstract class AbstractHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getFlux(): Flux<BlockContainer> {
|
override fun getFlux(): Flux<BlockContainer> {
|
||||||
val curHead = forkChoice.getHead()
|
|
||||||
return Flux.concat(
|
return Flux.concat(
|
||||||
forkChoice.getHead().toMono(),
|
forkChoice.getHead().toMono(),
|
||||||
stream.asFlux()
|
stream.asFlux()
|
||||||
@@ -115,6 +137,11 @@ abstract class AbstractHead(
|
|||||||
return getCurrent()?.height
|
return getCurrent()?.height
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLastUpdateTime(): Long =
|
override fun stop() {
|
||||||
lastUpdateTime.get()
|
stopping = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
stopping = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ class EmptyHead : Head {
|
|||||||
override fun getCurrentHeight(): Long? {
|
override fun getCurrentHeight(): Long? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
override fun start() {
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLastUpdateTime(): Long = 0L
|
override fun stop() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,5 +37,8 @@ interface Head {
|
|||||||
fun onBeforeBlock(handler: Runnable)
|
fun onBeforeBlock(handler: Runnable)
|
||||||
|
|
||||||
fun getCurrentHeight(): Long?
|
fun getCurrentHeight(): Long?
|
||||||
fun getLastUpdateTime(): Long
|
|
||||||
|
fun start()
|
||||||
|
|
||||||
|
fun stop()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import com.google.common.annotations.VisibleForTesting
|
|||||||
import io.emeraldpay.dshackle.cache.Caches
|
import io.emeraldpay.dshackle.cache.Caches
|
||||||
import io.emeraldpay.dshackle.cache.CachesEnabled
|
import io.emeraldpay.dshackle.cache.CachesEnabled
|
||||||
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
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
|
||||||
@@ -29,6 +30,10 @@ class MergedHead(
|
|||||||
forkChoice: ForkChoice
|
forkChoice: ForkChoice
|
||||||
) : AbstractHead(forkChoice), Lifecycle, CachesEnabled {
|
) : AbstractHead(forkChoice), Lifecycle, CachesEnabled {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(MergedHead::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private var subscription: Disposable? = null
|
private var subscription: Disposable? = null
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
override fun isRunning(): Boolean {
|
||||||
@@ -36,16 +41,20 @@ class MergedHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
super.start()
|
||||||
sources.forEach { head ->
|
sources.forEach { head ->
|
||||||
if (head is Lifecycle && !head.isRunning) {
|
if (head is Lifecycle && !head.isRunning) {
|
||||||
head.start()
|
head.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subscription?.dispose()
|
subscription?.dispose()
|
||||||
subscription = super.follow(Flux.merge(sources.map { it.getFlux() }))
|
subscription = super.follow(
|
||||||
|
Flux.merge(sources.map { it.getFlux() }).doOnNext { log.debug("New MERGED head $it") }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
super.stop()
|
||||||
sources.forEach { head ->
|
sources.forEach { head ->
|
||||||
if (head is Lifecycle && head.isRunning) {
|
if (head is Lifecycle && head.isRunning) {
|
||||||
head.stop()
|
head.stop()
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class BitcoinRpcHead(
|
|||||||
private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
|
private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||||
private val extractBlock: ExtractBlock,
|
private val extractBlock: ExtractBlock,
|
||||||
private val interval: Duration = Duration.ofSeconds(15)
|
private val interval: Duration = Duration.ofSeconds(15)
|
||||||
) : Head, AbstractHead(MostWorkForkChoice()), Lifecycle {
|
) : Head, AbstractHead(MostWorkForkChoice(), awaitHeadTimeoutMs = 1200_000), Lifecycle {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(BitcoinRpcHead::class.java)
|
private val log = LoggerFactory.getLogger(BitcoinRpcHead::class.java)
|
||||||
@@ -51,6 +51,7 @@ class BitcoinRpcHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
super.start()
|
||||||
if (refreshSubscription != null) {
|
if (refreshSubscription != null) {
|
||||||
log.warn("Called to start when running")
|
log.warn("Called to start when running")
|
||||||
return
|
return
|
||||||
@@ -76,6 +77,7 @@ class BitcoinRpcHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
super.stop()
|
||||||
val copy = refreshSubscription
|
val copy = refreshSubscription
|
||||||
refreshSubscription = null
|
refreshSubscription = null
|
||||||
copy?.dispose()
|
copy?.dispose()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class BitcoinZMQHead(
|
|||||||
private val server: ZMQServer,
|
private val server: ZMQServer,
|
||||||
private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
|
private val api: Reader<JsonRpcRequest, JsonRpcResponse>,
|
||||||
private val extractBlock: ExtractBlock,
|
private val extractBlock: ExtractBlock,
|
||||||
) : Head, AbstractHead(MostWorkForkChoice()), Lifecycle {
|
) : Head, AbstractHead(MostWorkForkChoice(), awaitHeadTimeoutMs = 1200_000), Lifecycle {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(BitcoinZMQHead::class.java)
|
private val log = LoggerFactory.getLogger(BitcoinZMQHead::class.java)
|
||||||
@@ -51,11 +51,13 @@ class BitcoinZMQHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
super.start()
|
||||||
server.start()
|
server.start()
|
||||||
refreshSubscription = super.follow(connect())
|
refreshSubscription = super.follow(connect())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
super.stop()
|
||||||
server.stop()
|
server.stop()
|
||||||
val copy = refreshSubscription
|
val copy = refreshSubscription
|
||||||
refreshSubscription = null
|
refreshSubscription = null
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class EthereumRpcHead(
|
|||||||
private var refreshSubscription: Disposable? = null
|
private var refreshSubscription: Disposable? = null
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
super.start()
|
||||||
refreshSubscription?.dispose()
|
refreshSubscription?.dispose()
|
||||||
val base = Flux.interval(interval)
|
val base = Flux.interval(interval)
|
||||||
.publishOn(scheduler)
|
.publishOn(scheduler)
|
||||||
@@ -62,6 +63,7 @@ class EthereumRpcHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
super.stop()
|
||||||
refreshSubscription?.dispose()
|
refreshSubscription?.dispose()
|
||||||
refreshSubscription = null
|
refreshSubscription = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class EthereumWsHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
super.start()
|
||||||
this.subscription?.dispose()
|
this.subscription?.dispose()
|
||||||
val heads = Flux.merge(
|
val heads = Flux.merge(
|
||||||
// get the current block, not just wait for the next update
|
// get the current block, not just wait for the next update
|
||||||
@@ -50,6 +51,7 @@ class EthereumWsHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
super.stop()
|
||||||
subscription?.dispose()
|
subscription?.dispose()
|
||||||
subscription = null
|
subscription = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ import org.springframework.context.Lifecycle
|
|||||||
import org.springframework.util.ConcurrentReferenceHashMap
|
import org.springframework.util.ConcurrentReferenceHashMap
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -70,20 +68,6 @@ open class EthereumPosMultiStream(
|
|||||||
head = updateHead()
|
head = updateHead()
|
||||||
}
|
}
|
||||||
super.init()
|
super.init()
|
||||||
Executors.newScheduledThreadPool(1).scheduleAtFixedRate({
|
|
||||||
val timeout = System.currentTimeMillis() - (head?.getLastUpdateTime() ?: 0L)
|
|
||||||
log.debug("Check head is active! Lst updated $timeout ms ago")
|
|
||||||
if (timeout > 60_000 && lock.tryLock()) {
|
|
||||||
log.warn("Timeout is over 1 min - restart head")
|
|
||||||
try {
|
|
||||||
head = updateHead()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
log.error(e.message, e)
|
|
||||||
} finally {
|
|
||||||
lock.unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 60, 30, TimeUnit.SECONDS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class GrpcHead(
|
|||||||
log.warn("Disconnected $chain from ${parent.getId()}: ${err.message}")
|
log.warn("Disconnected $chain from ${parent.getId()}: ${err.message}")
|
||||||
parent.setStatus(UpstreamAvailability.UNAVAILABLE)
|
parent.setStatus(UpstreamAvailability.UNAVAILABLE)
|
||||||
Mono.empty<BlockchainOuterClass.ChainHead>()
|
Mono.empty<BlockchainOuterClass.ChainHead>()
|
||||||
}
|
}.doFinally { log.warn("Head subscription finished: $it") }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,10 +116,12 @@ class GrpcHead(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
|
super.start()
|
||||||
this.internalStart(remote)
|
this.internalStart(remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
|
super.stop()
|
||||||
headSubscription?.dispose()
|
headSubscription?.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,12 @@ class EthereumHeadMock implements Head {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
long getLastUpdateTime() {
|
void start() {
|
||||||
return 0
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void stop() {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class AbstractHeadSpec extends Specification {
|
|||||||
.expectNext(blocks[1])
|
.expectNext(blocks[1])
|
||||||
.then {
|
.then {
|
||||||
assert called
|
assert called
|
||||||
|
head.stop()
|
||||||
source.tryEmitComplete()
|
source.tryEmitComplete()
|
||||||
}
|
}
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
@@ -83,7 +84,10 @@ class AbstractHeadSpec extends Specification {
|
|||||||
.expectNext(blocks[2])
|
.expectNext(blocks[2])
|
||||||
.then { source.tryEmitNext(blocks[3]) }
|
.then { source.tryEmitNext(blocks[3]) }
|
||||||
.expectNext(blocks[3])
|
.expectNext(blocks[3])
|
||||||
.then { source.tryEmitComplete() }
|
.then {
|
||||||
|
head.stop()
|
||||||
|
source.tryEmitComplete()
|
||||||
|
}
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify(Duration.ofSeconds(1))
|
.verify(Duration.ofSeconds(1))
|
||||||
}
|
}
|
||||||
@@ -110,7 +114,10 @@ class AbstractHeadSpec extends Specification {
|
|||||||
.then { source.tryEmitNext(wrongblock) }
|
.then { source.tryEmitNext(wrongblock) }
|
||||||
.then { source.tryEmitNext(blocks[3]) }
|
.then { source.tryEmitNext(blocks[3]) }
|
||||||
.expectNext(blocks[3])
|
.expectNext(blocks[3])
|
||||||
.then { source.tryEmitComplete() }
|
.then {
|
||||||
|
head.stop()
|
||||||
|
source.tryEmitComplete()
|
||||||
|
}
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify(Duration.ofSeconds(1))
|
.verify(Duration.ofSeconds(1))
|
||||||
}
|
}
|
||||||
@@ -132,7 +139,7 @@ class AbstractHeadSpec extends Specification {
|
|||||||
BlockContainer getHead() {
|
BlockContainer getHead() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}, new BlockValidator.AlwaysValid())
|
}, new BlockValidator.AlwaysValid(), 100_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ class MergedHeadSpec extends Specification {
|
|||||||
|
|
||||||
class TestHead1 extends AbstractHead {
|
class TestHead1 extends AbstractHead {
|
||||||
TestHead1() {
|
TestHead1() {
|
||||||
super(new MostWorkForkChoice())
|
super(new MostWorkForkChoice(), new BlockValidator.AlwaysValid(), 100_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestHead2 extends AbstractHead implements Lifecycle {
|
class TestHead2 extends AbstractHead implements Lifecycle {
|
||||||
|
|
||||||
TestHead2() {
|
TestHead2() {
|
||||||
super(new MostWorkForkChoice())
|
super(new MostWorkForkChoice(), new BlockValidator.AlwaysValid(), 100_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user