Make settings validation periodically (#374)

This commit is contained in:
KirillPamPam
2024-01-09 11:36:48 +04:00
committed by GitHub
parent 7a496563ea
commit 4d2364d353
9 changed files with 136 additions and 65 deletions

View File

@@ -20,6 +20,7 @@ import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.foundation.ChainOptions import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.startup.configure.UpstreamCreationData
import io.emeraldpay.dshackle.startup.configure.UpstreamFactory import io.emeraldpay.dshackle.startup.configure.UpstreamFactory
import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -54,11 +55,20 @@ open class ConfiguredUpstreams(
log.error("Chain is unknown: ${up.chain}") log.error("Chain is unknown: ${up.chain}")
return@forEach return@forEach
} }
val upstream = upstreamFactory.createUpstream(chain.type, up, defaultOptions) val upstreamCreationData = upstreamFactory.createUpstream(chain.type, up, defaultOptions)
if (upstream != null) { if (upstreamCreationData != UpstreamCreationData.default()) {
val eventType = if (upstreamCreationData.isValid) {
UpstreamChangeEvent.ChangeType.ADDED
} else {
UpstreamChangeEvent.ChangeType.OBSERVED
}
multistreamHolder.getUpstream(chain) multistreamHolder.getUpstream(chain)
.processUpstreamsEvents( .processUpstreamsEvents(
UpstreamChangeEvent(chain, upstream, UpstreamChangeEvent.ChangeType.ADDED), UpstreamChangeEvent(
chain,
upstreamCreationData.upstream!!,
eventType,
),
) )
} }
} }

View File

@@ -59,6 +59,16 @@ data class UpstreamChangeEvent(
* Upstream is removed (it still doesn't mean it wouldn't return again after some reconfiguration) * Upstream is removed (it still doesn't mean it wouldn't return again after some reconfiguration)
*/ */
REMOVED, REMOVED,
/**
* Upstream is removed but not stopped due to upstream fatal settings error (it could return some reconfiguration)
*/
FATAL_SETTINGS_ERROR_REMOVED,
/**
* Upstream is observed for state updates (it could be added later)
*/
OBSERVED,
} }
override fun setCaches(caches: Caches) { override fun setCaches(caches: Caches) {

View File

@@ -10,7 +10,6 @@ import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.CallTargetsHolder import io.emeraldpay.dshackle.upstream.CallTargetsHolder
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.MergedHead import io.emeraldpay.dshackle.upstream.MergedHead
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcHead import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcHead
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcUpstream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinRpcUpstream
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinZMQHead import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinZMQHead
@@ -38,13 +37,13 @@ class BitcoinUpstreamCreator(
chain: Chain, chain: Chain,
options: ChainOptions.Options, options: ChainOptions.Options,
chainConf: ChainsConfig.ChainConfig, chainConf: ChainsConfig.ChainConfig,
): Upstream? { ): UpstreamCreationData {
val config = upstreamsConfig.cast(UpstreamsConfig.BitcoinConnection::class.java) val config = upstreamsConfig.cast(UpstreamsConfig.BitcoinConnection::class.java)
val conn = config.connection!! val conn = config.connection!!
val httpFactory = genericConnectorFactoryCreator.buildHttpFactory(conn.rpc) val httpFactory = genericConnectorFactoryCreator.buildHttpFactory(conn.rpc)
if (httpFactory == null) { if (httpFactory == null) {
log.warn("Upstream doesn't have API configuration") log.warn("Upstream doesn't have API configuration")
return null return UpstreamCreationData.default()
} }
val directApi = httpFactory.create(config.id, chain) val directApi = httpFactory.create(config.id, chain)
val esplora = conn.esplora?.let { endpoint -> val esplora = conn.esplora?.let { endpoint ->
@@ -74,6 +73,6 @@ class BitcoinUpstreamCreator(
methods, esplora, chainConf, methods, esplora, chainConf,
) )
upstream.start() upstream.start()
return upstream return UpstreamCreationData(upstream, true)
} }
} }

View File

@@ -6,7 +6,6 @@ import io.emeraldpay.dshackle.config.IndexConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.foundation.ChainOptions import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.upstream.CallTargetsHolder import io.emeraldpay.dshackle.upstream.CallTargetsHolder
import io.emeraldpay.dshackle.upstream.Upstream
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@Component @Component
@@ -22,7 +21,7 @@ class EthereumUpstreamCreator(
chain: Chain, chain: Chain,
options: ChainOptions.Options, options: ChainOptions.Options,
chainConf: ChainsConfig.ChainConfig, chainConf: ChainsConfig.ChainConfig,
): Upstream? { ): UpstreamCreationData {
var rating = 0 var rating = 0
val connection = if (upstreamsConfig.connection is UpstreamsConfig.EthereumPosConnection) { val connection = if (upstreamsConfig.connection is UpstreamsConfig.EthereumPosConnection) {
val posConn = upstreamsConfig.cast(UpstreamsConfig.EthereumPosConnection::class.java) val posConn = upstreamsConfig.cast(UpstreamsConfig.EthereumPosConnection::class.java)

View File

@@ -8,7 +8,6 @@ import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.BlockValidator import io.emeraldpay.dshackle.upstream.BlockValidator
import io.emeraldpay.dshackle.upstream.CallTargetsHolder import io.emeraldpay.dshackle.upstream.CallTargetsHolder
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice import io.emeraldpay.dshackle.upstream.forkchoice.NoChoiceWithPriorityForkChoice
import io.emeraldpay.dshackle.upstream.generic.ChainSpecificRegistry import io.emeraldpay.dshackle.upstream.generic.ChainSpecificRegistry
import io.emeraldpay.dshackle.upstream.generic.GenericUpstream import io.emeraldpay.dshackle.upstream.generic.GenericUpstream
@@ -31,7 +30,7 @@ open class GenericUpstreamCreator(
chain: Chain, chain: Chain,
options: ChainOptions.Options, options: ChainOptions.Options,
chainConf: ChainsConfig.ChainConfig, chainConf: ChainsConfig.ChainConfig,
): Upstream? { ): UpstreamCreationData {
return buildGenericUpstream( return buildGenericUpstream(
upstreamsConfig.nodeId, upstreamsConfig.nodeId,
upstreamsConfig, upstreamsConfig,
@@ -51,10 +50,10 @@ open class GenericUpstreamCreator(
options: ChainOptions.Options, options: ChainOptions.Options,
chainConfig: ChainsConfig.ChainConfig, chainConfig: ChainsConfig.ChainConfig,
nodeRating: Int, nodeRating: Int,
): Upstream? { ): UpstreamCreationData {
if (config.connection == null) { if (config.connection == null) {
log.warn("Upstream doesn't have connection configuration") log.warn("Upstream doesn't have connection configuration")
return null return UpstreamCreationData.default()
} }
val cs = ChainSpecificRegistry.resolve(chain) val cs = ChainSpecificRegistry.resolve(chain)
@@ -66,7 +65,7 @@ open class GenericUpstreamCreator(
NoChoiceWithPriorityForkChoice(nodeRating, config.id!!), NoChoiceWithPriorityForkChoice(nodeRating, config.id!!),
BlockValidator.ALWAYS_VALID, BlockValidator.ALWAYS_VALID,
chainConfig, chainConfig,
) ?: return null ) ?: return UpstreamCreationData.default()
val methods = buildMethods(config, chain) val methods = buildMethods(config, chain)
@@ -93,9 +92,9 @@ open class GenericUpstreamCreator(
upstream.start() upstream.start()
if (!upstream.isRunning) { if (!upstream.isRunning) {
log.debug("Upstream ${upstream.getId()} is not running, it can't be added") log.debug("Upstream ${upstream.getId()} is not running, it can't be added")
return null return UpstreamCreationData.default()
} }
return upstream return UpstreamCreationData(upstream, upstream.isValid())
} }
private fun getHash(nodeId: Int?, obj: Any): Byte = private fun getHash(nodeId: Int?, obj: Any): Byte =

View File

@@ -7,7 +7,6 @@ import io.emeraldpay.dshackle.config.IndexConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.foundation.ChainOptions import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.upstream.CallTargetsHolder import io.emeraldpay.dshackle.upstream.CallTargetsHolder
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods import io.emeraldpay.dshackle.upstream.calls.ManagedCallMethods
import org.slf4j.Logger import org.slf4j.Logger
@@ -23,7 +22,7 @@ abstract class UpstreamCreator(
fun createUpstream( fun createUpstream(
upstreamsConfig: UpstreamsConfig.Upstream<*>, upstreamsConfig: UpstreamsConfig.Upstream<*>,
defaultOptions: Map<Chain, ChainOptions.PartialOptions>, defaultOptions: Map<Chain, ChainOptions.PartialOptions>,
): Upstream? { ): UpstreamCreationData {
val chain = Global.chainById(upstreamsConfig.chain) val chain = Global.chainById(upstreamsConfig.chain)
if (chain == Chain.UNSPECIFIED) { if (chain == Chain.UNSPECIFIED) {
throw IllegalArgumentException("Chain is unknown: ${upstreamsConfig.chain}") throw IllegalArgumentException("Chain is unknown: ${upstreamsConfig.chain}")
@@ -42,7 +41,7 @@ abstract class UpstreamCreator(
chain: Chain, chain: Chain,
options: ChainOptions.Options, options: ChainOptions.Options,
chainConf: ChainsConfig.ChainConfig, chainConf: ChainsConfig.ChainConfig,
): Upstream? ): UpstreamCreationData
protected fun buildMethods(config: UpstreamsConfig.Upstream<*>, chain: Chain): CallMethods { protected fun buildMethods(config: UpstreamsConfig.Upstream<*>, chain: Chain): CallMethods {
return if (config.methods != null || config.methodGroups != null) { return if (config.methods != null || config.methodGroups != null) {

View File

@@ -7,6 +7,15 @@ import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
data class UpstreamCreationData(
val upstream: Upstream?,
val isValid: Boolean,
) {
companion object {
fun default() = UpstreamCreationData(null, false)
}
}
@Component @Component
class UpstreamFactory( class UpstreamFactory(
private val genericUpstreamCreator: GenericUpstreamCreator, private val genericUpstreamCreator: GenericUpstreamCreator,
@@ -18,7 +27,7 @@ class UpstreamFactory(
type: BlockchainType, type: BlockchainType,
upstreamsConfig: UpstreamsConfig.Upstream<*>, upstreamsConfig: UpstreamsConfig.Upstream<*>,
defaultOptions: Map<Chain, ChainOptions.PartialOptions>, defaultOptions: Map<Chain, ChainOptions.PartialOptions>,
): Upstream? { ): UpstreamCreationData {
return when (type) { return when (type) {
BlockchainType.ETHEREUM -> ethereumUpstreamCreator.createUpstream(upstreamsConfig, defaultOptions) BlockchainType.ETHEREUM -> ethereumUpstreamCreator.createUpstream(upstreamsConfig, defaultOptions)
BlockchainType.BITCOIN -> bitcoinUpstreamCreator.createUpstream(upstreamsConfig, defaultOptions) BlockchainType.BITCOIN -> bitcoinUpstreamCreator.createUpstream(upstreamsConfig, defaultOptions)

View File

@@ -84,6 +84,9 @@ abstract class Multistream(
@Volatile @Volatile
private var quorumLabels: List<QuorumForLabels.QuorumItem>? = null private var quorumLabels: List<QuorumForLabels.QuorumItem>? = null
private val meters: MutableMap<String, List<Meter.Id>> = HashMap() private val meters: MutableMap<String, List<Meter.Id>> = HashMap()
private val observedUpstreams = Sinks.many()
.multicast()
.directBestEffort<Upstream>()
private val addedUpstreams = Sinks.many() private val addedUpstreams = Sinks.many()
.multicast() .multicast()
.directBestEffort<Upstream>() .directBestEffort<Upstream>()
@@ -153,6 +156,15 @@ abstract class Multistream(
.subscribe { .subscribe {
onUpstreamChange(it) onUpstreamChange(it)
} }
observedUpstreams.asFlux()
.flatMap {
it.observeState()
.takeUntil { event -> event.type == UpstreamChangeEvent.ChangeType.ADDED }
}
.subscribe {
this.processUpstreamsEvents(it)
}
} }
/** /**
@@ -176,10 +188,10 @@ abstract class Multistream(
} }
} }
fun removeUpstream(id: String): Boolean = fun removeUpstream(id: String, stopUpstream: Boolean): Boolean =
getUpstreams().removeIf { up -> getUpstreams().removeIf { up ->
(up.getId() == id).also { (up.getId() == id).also {
if (it) { if (it && stopUpstream) {
up.stop() up.stop()
} }
} }
@@ -332,7 +344,7 @@ abstract class Multistream(
.takeUntilOther( .takeUntilOther(
subscribeRemovedUpstreams() subscribeRemovedUpstreams()
.filter { .filter {
it.getId() == upstream.getId() it.getId() == upstream.getId() && !it.isRunning()
}, },
) )
} }
@@ -460,16 +472,29 @@ abstract class Multistream(
} }
UpstreamChangeEvent.ChangeType.REMOVED -> { UpstreamChangeEvent.ChangeType.REMOVED -> {
removeUpstream(event.upstream.getId()).takeIf { it }?.let { removeUpstream(event, true)
try {
removedUpstreams.emitNext(event.upstream) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
onUpstreamsUpdated()
log.info("Upstream ${event.upstream.getId()} with chain $chain has been removed")
} catch (e: Sinks.EmissionException) {
log.error("error during event processing $event", e)
}
}
} }
UpstreamChangeEvent.ChangeType.FATAL_SETTINGS_ERROR_REMOVED -> {
removeUpstream(event, false)
}
UpstreamChangeEvent.ChangeType.OBSERVED -> {
observedUpstreams.emitNext(event.upstream) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
log.info("Upstream ${event.upstream.getId()} with chain $chain has been added to the observation")
}
}
}
}
private fun removeUpstream(event: UpstreamChangeEvent, stopUpstream: Boolean) {
removeUpstream(event.upstream.getId(), stopUpstream).takeIf { it }?.let {
try {
removedUpstreams.emitNext(event.upstream) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
onUpstreamsUpdated()
log.info("Upstream ${event.upstream.getId()} with chain $chain has been removed")
} catch (e: Sinks.EmissionException) {
log.error("error during event processing $event", e)
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import io.emeraldpay.dshackle.foundation.ChainOptions
import io.emeraldpay.dshackle.reader.JsonRpcReader import io.emeraldpay.dshackle.reader.JsonRpcReader
import io.emeraldpay.dshackle.startup.QuorumForLabels import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent.ChangeType.UPDATED
import io.emeraldpay.dshackle.upstream.Capability import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.DefaultUpstream
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
@@ -57,6 +58,9 @@ open class GenericUpstream(
private val lowerBoundBlockDetector = lowerBoundBlockDetectorBuilder(chain, this) private val lowerBoundBlockDetector = lowerBoundBlockDetectorBuilder(chain, this)
private val started = AtomicBoolean(false)
private val isUpstreamValid = AtomicBoolean(false)
override fun getHead(): Head { override fun getHead(): Head {
return connector.getHead() return connector.getHead()
} }
@@ -108,44 +112,55 @@ open class GenericUpstream(
return return
} }
ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR -> { ValidateUpstreamSettingsResult.UPSTREAM_SETTINGS_ERROR -> {
validateUpstreamSettings() log.warn("Non fatal upstream settings error, continue validation...")
} }
else -> { ValidateUpstreamSettingsResult.UPSTREAM_VALID -> {
isUpstreamValid.set(true)
upstreamStart() upstreamStart()
} }
} }
validateUpstreamSettings()
} else { } else {
isUpstreamValid.set(true)
upstreamStart() upstreamStart()
} }
started.set(true)
} }
private fun validateUpstreamSettings() { private fun validateUpstreamSettings() {
if (validator != null) { if (validator != null) {
validationSettingsSubscription = Flux.interval( validationSettingsSubscription = Flux.interval(
Duration.ofSeconds(10),
Duration.ofSeconds(20), Duration.ofSeconds(20),
).flatMap { ).flatMap {
validator.validateUpstreamSettings() validator.validateUpstreamSettings()
}.subscribe { }
when (it) { .distinctUntilChanged()
ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR -> { .subscribe {
connector.stop() when (it) {
disposeValidationSettingsSubscription() ValidateUpstreamSettingsResult.UPSTREAM_FATAL_SETTINGS_ERROR -> {
} if (isUpstreamValid.get()) {
log.warn("There is a fatal error after upstream settings validation, removing ${getId()}...")
partialStop()
sendUpstreamStateEvent(UpstreamChangeEvent.ChangeType.FATAL_SETTINGS_ERROR_REMOVED)
}
isUpstreamValid.set(false)
}
ValidateUpstreamSettingsResult.UPSTREAM_VALID -> { ValidateUpstreamSettingsResult.UPSTREAM_VALID -> {
upstreamStart() if (!isUpstreamValid.get()) {
stateEventStream.emitNext( log.warn("Upstream ${getId()} is now valid, adding to the multistream...")
UpstreamChangeEvent(chain, this, UpstreamChangeEvent.ChangeType.ADDED), upstreamStart()
) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED } sendUpstreamStateEvent(UpstreamChangeEvent.ChangeType.ADDED)
disposeValidationSettingsSubscription() }
} isUpstreamValid.set(true)
}
else -> { else -> {
log.warn("Continue validation of upstream ${getId()}") log.warn("Continue validation of upstream ${getId()}")
}
} }
} }
}
} }
} }
@@ -165,9 +180,7 @@ open class GenericUpstream(
} }
livenessSubscription = connector.hasLiveSubscriptionHead().subscribe({ livenessSubscription = connector.hasLiveSubscriptionHead().subscribe({
hasLiveSubscriptionHead.set(it) hasLiveSubscriptionHead.set(it)
stateEventStream.emitNext( sendUpstreamStateEvent(UPDATED)
UpstreamChangeEvent(chain, this, UpstreamChangeEvent.ChangeType.UPDATED),
) { _, 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)
},) },)
@@ -177,19 +190,21 @@ open class GenericUpstream(
} }
override fun stop() { override fun stop() {
partialStop()
validationSettingsSubscription?.dispose()
validationSettingsSubscription = null
connector.stop()
started.set(false)
}
private fun partialStop() {
validatorSubscription?.dispose() validatorSubscription?.dispose()
validatorSubscription = null validatorSubscription = null
livenessSubscription?.dispose() livenessSubscription?.dispose()
livenessSubscription = null livenessSubscription = null
lowerBlockDetectorSubscription?.dispose() lowerBlockDetectorSubscription?.dispose()
lowerBlockDetectorSubscription = null lowerBlockDetectorSubscription = null
disposeValidationSettingsSubscription() connector.getHead().stop()
connector.stop()
}
private fun disposeValidationSettingsSubscription() {
validationSettingsSubscription?.dispose()
validationSettingsSubscription = null
} }
private fun updateLabels(label: Pair<String, String>) { private fun updateLabels(label: Pair<String, String>) {
@@ -202,9 +217,7 @@ open class GenericUpstream(
private fun detectLowerBlock() { private fun detectLowerBlock() {
lowerBlockDetectorSubscription = lowerBoundBlockDetector.lowerBlock() lowerBlockDetectorSubscription = lowerBoundBlockDetector.lowerBlock()
.subscribe { .subscribe {
stateEventStream.emitNext( sendUpstreamStateEvent(UPDATED)
UpstreamChangeEvent(chain, this, UpstreamChangeEvent.ChangeType.UPDATED),
) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
} }
} }
@@ -212,5 +225,13 @@ open class GenericUpstream(
return connector.getIngressSubscription() return connector.getIngressSubscription()
} }
override fun isRunning() = connector.isRunning() && validationSettingsSubscription == null override fun isRunning() = connector.isRunning() || started.get()
fun isValid(): Boolean = isUpstreamValid.get()
private fun sendUpstreamStateEvent(eventType: UpstreamChangeEvent.ChangeType) {
stateEventStream.emitNext(
UpstreamChangeEvent(chain, this, eventType),
) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
}
} }