Reload config (#315)
This commit is contained in:
10
build.gradle
10
build.gradle
@@ -1,3 +1,5 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
@@ -124,14 +126,10 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileKotlin {
|
compileKotlin {
|
||||||
kotlinOptions {
|
compilerOptions.jvmTarget.set(JvmTarget.JVM_20)
|
||||||
jvmTarget = "20"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
compileTestKotlin {
|
compileTestKotlin {
|
||||||
kotlinOptions {
|
compilerOptions.jvmTarget.set(JvmTarget.JVM_20)
|
||||||
jvmTarget = "20"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
BIN
foundation/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
foundation/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
foundation/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
foundation/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -16,12 +16,23 @@ class ChainOptions {
|
|||||||
val validateChain: Boolean,
|
val validateChain: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
open class DefaultOptions : PartialOptions() {
|
data class DefaultOptions(
|
||||||
var chains: List<String>? = null
|
var chains: List<String>? = null,
|
||||||
var options: PartialOptions? = null
|
var options: PartialOptions? = null
|
||||||
}
|
)
|
||||||
|
|
||||||
open class PartialOptions {
|
data class PartialOptions(
|
||||||
|
var disableValidation: Boolean? = null,
|
||||||
|
var disableUpstreamValidation: Boolean? = null,
|
||||||
|
var validationInterval: Int? = null,
|
||||||
|
var timeout: Duration? = null,
|
||||||
|
var providesBalance: Boolean? = null,
|
||||||
|
var validatePeers: Boolean? = null,
|
||||||
|
var validateCalllimit: Boolean? = null,
|
||||||
|
var minPeers: Int? = null,
|
||||||
|
var validateSyncing: Boolean? = null,
|
||||||
|
var validateChain: Boolean? = null
|
||||||
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getDefaults(): PartialOptions {
|
fun getDefaults(): PartialOptions {
|
||||||
@@ -31,29 +42,6 @@ class ChainOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var disableValidation: Boolean? = null
|
|
||||||
var disableUpstreamValidation: Boolean? = null
|
|
||||||
var validationInterval: Int? = null
|
|
||||||
set(value) {
|
|
||||||
require(value == null || value > 0) {
|
|
||||||
"validation-interval must be a positive number: $value"
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
var timeout: Duration? = null
|
|
||||||
var providesBalance: Boolean? = null
|
|
||||||
var validatePeers: Boolean? = null
|
|
||||||
var validateCalllimit: Boolean? = null
|
|
||||||
var minPeers: Int? = null
|
|
||||||
set(value) {
|
|
||||||
require(value == null || value >= 0) {
|
|
||||||
"min-peers must be a positive number: $value"
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
var validateSyncing: Boolean? = null
|
|
||||||
var validateChain: Boolean? = null
|
|
||||||
|
|
||||||
fun merge(overwrites: PartialOptions?): PartialOptions {
|
fun merge(overwrites: PartialOptions?): PartialOptions {
|
||||||
if (overwrites == null) {
|
if (overwrites == null) {
|
||||||
return this
|
return this
|
||||||
|
|||||||
@@ -35,4 +35,6 @@ open class FileResolver(
|
|||||||
}
|
}
|
||||||
return File(baseDir, path)
|
return File(baseDir, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun file() = baseDir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class DynamicMergeFlux<K : Any, T>(private val scheduler: Scheduler) {
|
|||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
sources.forEach { (_, d) -> d.dispose() }
|
sources.forEach { (_, d) -> d.dispose() }
|
||||||
|
sources.clear()
|
||||||
merge.emitComplete { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
merge.emitComplete { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,21 +23,23 @@ import java.util.Arrays
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
open class UpstreamsConfig {
|
data class UpstreamsConfig(
|
||||||
var defaultOptions: MutableList<ChainOptions.DefaultOptions> = ArrayList()
|
var defaultOptions: MutableList<ChainOptions.DefaultOptions> = ArrayList(),
|
||||||
var upstreams: MutableList<Upstream<*>> = ArrayList<Upstream<*>>()
|
var upstreams: MutableList<Upstream<*>> = ArrayList(),
|
||||||
|
) {
|
||||||
|
|
||||||
class Upstream<T : UpstreamConnection> {
|
data class Upstream<T : UpstreamConnection>(
|
||||||
var id: String? = null
|
var id: String? = null,
|
||||||
var nodeId: Int? = null
|
var nodeId: Int? = null,
|
||||||
var chain: String? = null
|
var chain: String? = null,
|
||||||
var options: ChainOptions.PartialOptions? = null
|
var options: ChainOptions.PartialOptions? = null,
|
||||||
var isEnabled = true
|
var isEnabled: Boolean = true,
|
||||||
var connection: T? = null
|
var connection: T? = null,
|
||||||
val labels = Labels()
|
val labels: Labels = Labels(),
|
||||||
var methods: Methods? = null
|
var methods: Methods? = null,
|
||||||
var methodGroups: MethodGroups? = null
|
var methodGroups: MethodGroups? = null,
|
||||||
var role: UpstreamRole = UpstreamRole.PRIMARY
|
var role: UpstreamRole = UpstreamRole.PRIMARY,
|
||||||
|
) {
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <Z : UpstreamConnection> cast(type: Class<Z>): Upstream<Z> {
|
fun <Z : UpstreamConnection> cast(type: Class<Z>): Upstream<Z> {
|
||||||
@@ -56,9 +58,9 @@ open class UpstreamsConfig {
|
|||||||
|
|
||||||
open class UpstreamConnection
|
open class UpstreamConnection
|
||||||
|
|
||||||
open class RpcConnection : UpstreamConnection() {
|
open class RpcConnection(
|
||||||
var rpc: HttpEndpoint? = null
|
open var rpc: HttpEndpoint? = null,
|
||||||
}
|
) : UpstreamConnection()
|
||||||
|
|
||||||
class GrpcConnection : UpstreamConnection() {
|
class GrpcConnection : UpstreamConnection() {
|
||||||
var host: String? = null
|
var host: String? = null
|
||||||
@@ -68,9 +70,11 @@ open class UpstreamsConfig {
|
|||||||
var upstreamRating: Int = 0
|
var upstreamRating: Int = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
class EthereumConnection : RpcConnection() {
|
data class EthereumConnection(
|
||||||
var ws: WsEndpoint? = null
|
override var rpc: HttpEndpoint? = null,
|
||||||
var connectorMode: String? = null
|
var ws: WsEndpoint? = null,
|
||||||
|
var connectorMode: String? = null,
|
||||||
|
) : RpcConnection(rpc) {
|
||||||
|
|
||||||
fun resolveMode(): ConnectorMode {
|
fun resolveMode(): ConnectorMode {
|
||||||
return if (connectorMode == null) {
|
return if (connectorMode == null) {
|
||||||
@@ -87,27 +91,28 @@ open class UpstreamsConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BitcoinConnection : RpcConnection() {
|
data class BitcoinConnection(
|
||||||
var esplora: HttpEndpoint? = null
|
override var rpc: HttpEndpoint? = null,
|
||||||
var zeroMq: BitcoinZeroMq? = null
|
var esplora: HttpEndpoint? = null,
|
||||||
}
|
var zeroMq: BitcoinZeroMq? = null,
|
||||||
|
) : RpcConnection()
|
||||||
|
|
||||||
class EthereumPosConnection : UpstreamConnection() {
|
data class EthereumPosConnection(
|
||||||
var execution: EthereumConnection? = null
|
var execution: EthereumConnection? = null,
|
||||||
var upstreamRating: Int = 0
|
var upstreamRating: Int = 0,
|
||||||
}
|
) : UpstreamConnection()
|
||||||
|
|
||||||
data class BitcoinZeroMq(
|
data class BitcoinZeroMq(
|
||||||
val host: String = "127.0.0.1",
|
val host: String = "127.0.0.1",
|
||||||
val port: Int,
|
val port: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
class HttpEndpoint(val url: URI) {
|
data class HttpEndpoint(val url: URI) {
|
||||||
var basicAuth: AuthConfig.ClientBasicAuth? = null
|
var basicAuth: AuthConfig.ClientBasicAuth? = null
|
||||||
var tls: AuthConfig.ClientTlsAuth? = null
|
var tls: AuthConfig.ClientTlsAuth? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
class WsEndpoint(val url: URI) {
|
data class WsEndpoint(val url: URI) {
|
||||||
var origin: URI? = null
|
var origin: URI? = null
|
||||||
var basicAuth: AuthConfig.ClientBasicAuth? = null
|
var basicAuth: AuthConfig.ClientBasicAuth? = null
|
||||||
var frameSize: Int? = null
|
var frameSize: Int? = null
|
||||||
@@ -159,17 +164,17 @@ open class UpstreamsConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Methods(
|
data class Methods(
|
||||||
val enabled: Set<Method>,
|
val enabled: Set<Method>,
|
||||||
val disabled: Set<Method>,
|
val disabled: Set<Method>,
|
||||||
)
|
)
|
||||||
|
|
||||||
class MethodGroups(
|
data class MethodGroups(
|
||||||
val enabled: Set<String>,
|
val enabled: Set<String>,
|
||||||
val disabled: Set<String>,
|
val disabled: Set<String>,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Method(
|
data class Method(
|
||||||
val name: String,
|
val name: String,
|
||||||
val quorum: String? = null,
|
val quorum: String? = null,
|
||||||
val static: String? = null,
|
val static: String? = null,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package io.emeraldpay.dshackle.config.reload
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Config
|
||||||
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
|
import io.emeraldpay.dshackle.config.MainConfig
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfigReader
|
||||||
|
import io.emeraldpay.dshackle.foundation.ChainOptionsReader
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ReloadConfigService(
|
||||||
|
private val config: Config,
|
||||||
|
fileResolver: FileResolver,
|
||||||
|
private val mainConfig: MainConfig,
|
||||||
|
|
||||||
|
) {
|
||||||
|
private val optionsReader = ChainOptionsReader()
|
||||||
|
private val upstreamsConfigReader = UpstreamsConfigReader(fileResolver, optionsReader)
|
||||||
|
|
||||||
|
fun readUpstreamsConfig() = upstreamsConfigReader.read(config.getConfigPath().inputStream())!!
|
||||||
|
|
||||||
|
fun currentUpstreamsConfig() = mainConfig.upstreams!!
|
||||||
|
|
||||||
|
fun updateUpstreamsConfig(newConfig: UpstreamsConfig) {
|
||||||
|
mainConfig.upstreams = newConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package io.emeraldpay.dshackle.config.reload
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Chain
|
||||||
|
import io.emeraldpay.dshackle.Global.Companion.chainById
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.foundation.ChainOptions
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import sun.misc.Signal
|
||||||
|
import sun.misc.SignalHandler
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ReloadConfigSetup(
|
||||||
|
private val reloadConfigService: ReloadConfigService,
|
||||||
|
private val reloadConfigUpstreamService: ReloadConfigUpstreamService,
|
||||||
|
) : SignalHandler {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(this::class.java)
|
||||||
|
private val signalHup = Signal("HUP")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val reloadLock = ReentrantLock()
|
||||||
|
|
||||||
|
init {
|
||||||
|
Signal.handle(signalHup, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(sig: Signal) {
|
||||||
|
if (sig == signalHup) {
|
||||||
|
try {
|
||||||
|
handle()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn("Config is not reloaded, cause - ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handle() {
|
||||||
|
if (reloadLock.tryLock()) {
|
||||||
|
try {
|
||||||
|
log.info("Reloading config...")
|
||||||
|
|
||||||
|
reloadConfig()
|
||||||
|
|
||||||
|
log.info("Config is reloaded")
|
||||||
|
} finally {
|
||||||
|
reloadLock.unlock()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Reloading is in progress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun reloadConfig() {
|
||||||
|
val newUpstreamsConfig = reloadConfigService.readUpstreamsConfig()
|
||||||
|
val currentUpstreamsConfig = reloadConfigService.currentUpstreamsConfig()
|
||||||
|
|
||||||
|
val chainsToReload = analyzeDefaultOptions(
|
||||||
|
currentUpstreamsConfig.defaultOptions,
|
||||||
|
newUpstreamsConfig.defaultOptions,
|
||||||
|
)
|
||||||
|
val upstreamsAnalyzeData = analyzeUpstreams(
|
||||||
|
currentUpstreamsConfig.upstreams,
|
||||||
|
newUpstreamsConfig.upstreams,
|
||||||
|
)
|
||||||
|
|
||||||
|
val upstreamsToRemove = upstreamsAnalyzeData.removed
|
||||||
|
.plus(upstreamsAnalyzeData.reloaded)
|
||||||
|
.filterNot { chainsToReload.contains(it.second) }
|
||||||
|
val upstreamsToAdd = upstreamsAnalyzeData.added
|
||||||
|
.plus(upstreamsAnalyzeData.reloaded.map { it.first })
|
||||||
|
|
||||||
|
reloadConfigUpstreamService.reloadUpstreams(chainsToReload, upstreamsToRemove, upstreamsToAdd, newUpstreamsConfig)
|
||||||
|
|
||||||
|
reloadConfigService.updateUpstreamsConfig(newUpstreamsConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun analyzeUpstreams(
|
||||||
|
currentUpstreams: List<UpstreamsConfig.Upstream<*>>,
|
||||||
|
newUpstreams: List<UpstreamsConfig.Upstream<*>>,
|
||||||
|
): UpstreamAnalyzeData {
|
||||||
|
if (currentUpstreams == newUpstreams) {
|
||||||
|
return UpstreamAnalyzeData()
|
||||||
|
}
|
||||||
|
val reloaded = mutableSetOf<Pair<String, Chain>>()
|
||||||
|
val removed = mutableSetOf<Pair<String, Chain>>()
|
||||||
|
val currentUpstreamsMap = currentUpstreams.associateBy { it.id!! to chainById(it.chain) }
|
||||||
|
val newUpstreamsMap = newUpstreams.associateBy { it.id!! to chainById(it.chain) }
|
||||||
|
|
||||||
|
currentUpstreamsMap.forEach {
|
||||||
|
val newUpstream = newUpstreamsMap[it.key]
|
||||||
|
if (newUpstream == null) {
|
||||||
|
removed.add(it.key)
|
||||||
|
} else if (newUpstream != it.value) {
|
||||||
|
reloaded.add(it.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val added = newUpstreamsMap.minus(currentUpstreamsMap.keys).mapTo(mutableSetOf()) { it.key.first }
|
||||||
|
|
||||||
|
return UpstreamAnalyzeData(added, removed, reloaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun analyzeDefaultOptions(
|
||||||
|
currentDefaultOptions: List<ChainOptions.DefaultOptions>,
|
||||||
|
newDefaultOptions: List<ChainOptions.DefaultOptions>,
|
||||||
|
): Set<Chain> {
|
||||||
|
val chainsToReload = mutableSetOf<Chain>()
|
||||||
|
|
||||||
|
val currentOptions = getChainOptions(currentDefaultOptions)
|
||||||
|
val newOptions = getChainOptions(newDefaultOptions)
|
||||||
|
|
||||||
|
if (currentOptions == newOptions) {
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
val removed = mutableSetOf<Chain>()
|
||||||
|
|
||||||
|
currentOptions.forEach {
|
||||||
|
val newChainOption = newOptions[it.key]
|
||||||
|
if (newChainOption == null) {
|
||||||
|
removed.add(chainById(it.key))
|
||||||
|
} else if (newChainOption != it.value) {
|
||||||
|
chainsToReload.add(chainById(it.key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val added = newOptions.minus(currentOptions.keys).map { chainById(it.key) }
|
||||||
|
|
||||||
|
return chainsToReload.plus(added).plus(removed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getChainOptions(
|
||||||
|
defaultOptions: List<ChainOptions.DefaultOptions>,
|
||||||
|
): Map<String, List<ChainOptions.PartialOptions>> {
|
||||||
|
return defaultOptions.stream()
|
||||||
|
.flatMap { options -> options.chains?.stream()?.map { it to options.options } }
|
||||||
|
.collect(
|
||||||
|
Collectors.groupingBy(
|
||||||
|
{ it.first },
|
||||||
|
Collectors.mapping(
|
||||||
|
{ it.second },
|
||||||
|
Collectors.toUnmodifiableList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class UpstreamAnalyzeData(
|
||||||
|
val added: Set<String> = emptySet(),
|
||||||
|
val removed: Set<Pair<String, Chain>> = emptySet(),
|
||||||
|
val reloaded: Set<Pair<String, Chain>> = emptySet(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package io.emeraldpay.dshackle.config.reload
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Chain
|
||||||
|
import io.emeraldpay.dshackle.Global
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.startup.ConfiguredUpstreams
|
||||||
|
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
|
||||||
|
import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ReloadConfigUpstreamService(
|
||||||
|
private val eventPublisher: ApplicationEventPublisher,
|
||||||
|
private val multistreamHolder: CurrentMultistreamHolder,
|
||||||
|
private val configuredUpstreams: ConfiguredUpstreams,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun reloadUpstreams(
|
||||||
|
chainsToReload: Set<Chain>,
|
||||||
|
upstreamsToRemove: List<Pair<String, Chain>>,
|
||||||
|
upstreamsToAdd: Set<String>,
|
||||||
|
newUpstreamsConfig: UpstreamsConfig,
|
||||||
|
) {
|
||||||
|
val usedChains = removeUpstreams(chainsToReload, upstreamsToRemove)
|
||||||
|
|
||||||
|
addUpstreams(newUpstreamsConfig, chainsToReload, upstreamsToAdd)
|
||||||
|
|
||||||
|
usedChains.forEach {
|
||||||
|
multistreamHolder.getUpstream(it)
|
||||||
|
.run {
|
||||||
|
if (!this.haveUpstreams() && this.isRunning()) {
|
||||||
|
this.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeUpstreams(
|
||||||
|
chainsToReload: Set<Chain>,
|
||||||
|
upstreamsToRemove: List<Pair<String, Chain>>,
|
||||||
|
): Set<Chain> {
|
||||||
|
val usedChains = mutableSetOf<Chain>()
|
||||||
|
|
||||||
|
chainsToReload.forEach {
|
||||||
|
usedChains.add(it)
|
||||||
|
multistreamHolder.getUpstream(it)
|
||||||
|
.getAll()
|
||||||
|
.forEach { up ->
|
||||||
|
eventPublisher.publishEvent(UpstreamChangeEvent(it, up, UpstreamChangeEvent.ChangeType.REMOVED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
upstreamsToRemove.forEach { pair ->
|
||||||
|
usedChains.add(pair.second)
|
||||||
|
multistreamHolder.getUpstream(pair.second)
|
||||||
|
.getAll()
|
||||||
|
.find { pair.first == it.getId() }
|
||||||
|
?.let {
|
||||||
|
eventPublisher.publishEvent(UpstreamChangeEvent(pair.second, it, UpstreamChangeEvent.ChangeType.REMOVED))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return usedChains
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addUpstreams(
|
||||||
|
newUpstreamsConfig: UpstreamsConfig,
|
||||||
|
chainsToReload: Set<Chain>,
|
||||||
|
upstreamsToAdd: Set<String>,
|
||||||
|
) {
|
||||||
|
val configToReload = UpstreamsConfig(
|
||||||
|
newUpstreamsConfig.defaultOptions,
|
||||||
|
newUpstreamsConfig.upstreams.filter {
|
||||||
|
chainsToReload.contains(Global.chainById(it.chain)) || upstreamsToAdd.contains(it.id)
|
||||||
|
}.toMutableList(),
|
||||||
|
)
|
||||||
|
configuredUpstreams.processUpstreams(configToReload)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,6 +102,10 @@ open class ConfiguredUpstreams(
|
|||||||
|
|
||||||
override fun run(args: ApplicationArguments) {
|
override fun run(args: ApplicationArguments) {
|
||||||
log.debug("Starting upstreams")
|
log.debug("Starting upstreams")
|
||||||
|
processUpstreams(this.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun processUpstreams(config: UpstreamsConfig) {
|
||||||
val defaultOptions = buildDefaultOptions(config)
|
val defaultOptions = buildDefaultOptions(config)
|
||||||
config.upstreams.parallelStream().forEach { up ->
|
config.upstreams.parallelStream().forEach { up ->
|
||||||
if (!up.isEnabled) {
|
if (!up.isEnabled) {
|
||||||
@@ -239,7 +243,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)),
|
||||||
connectorFactory,
|
connectorFactory,
|
||||||
chainConf,
|
chainConf,
|
||||||
true,
|
true,
|
||||||
@@ -289,7 +293,7 @@ open class ConfiguredUpstreams(
|
|||||||
?: "bitcoin-${seq.getAndIncrement()}",
|
?: "bitcoin-${seq.getAndIncrement()}",
|
||||||
chain, directApi, head,
|
chain, directApi, head,
|
||||||
options, config.role,
|
options, config.role,
|
||||||
QuorumForLabels.QuorumItem(1, config.labels),
|
QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(config.labels)),
|
||||||
methods, esplora, chainConf,
|
methods, esplora, chainConf,
|
||||||
)
|
)
|
||||||
upstream.start()
|
upstream.start()
|
||||||
@@ -328,7 +332,7 @@ open class ConfiguredUpstreams(
|
|||||||
chain,
|
chain,
|
||||||
options, config.role,
|
options, config.role,
|
||||||
methods,
|
methods,
|
||||||
QuorumForLabels.QuorumItem(1, config.labels),
|
QuorumForLabels.QuorumItem(1, UpstreamsConfig.Labels.fromMap(config.labels)),
|
||||||
connectorFactory,
|
connectorFactory,
|
||||||
chainConf,
|
chainConf,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import io.emeraldpay.dshackle.upstream.Upstream
|
|||||||
/**
|
/**
|
||||||
* An update event to the list of currently available upstreams.
|
* An update event to the list of currently available upstreams.
|
||||||
*/
|
*/
|
||||||
class UpstreamChangeEvent(
|
data class UpstreamChangeEvent(
|
||||||
/**
|
/**
|
||||||
* Target blockchain
|
* Target blockchain
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import reactor.core.scheduler.Scheduler
|
|||||||
open class DynamicMergedHead(
|
open class DynamicMergedHead(
|
||||||
forkChoice: ForkChoice,
|
forkChoice: ForkChoice,
|
||||||
private val label: String = "",
|
private val label: String = "",
|
||||||
headScheduler: Scheduler,
|
private val headScheduler: Scheduler,
|
||||||
) : AbstractHead(forkChoice, headScheduler, upstreamId = label), Lifecycle {
|
) : AbstractHead(forkChoice, headScheduler, upstreamId = label), Lifecycle {
|
||||||
|
|
||||||
private var subscription: Disposable? = null
|
private var subscription: Disposable? = null
|
||||||
private val dynamicFlux: DynamicMergeFlux<String, BlockContainer> = DynamicMergeFlux(headScheduler)
|
|
||||||
|
@Volatile
|
||||||
|
private var dynamicFlux: DynamicMergeFlux<String, BlockContainer> = DynamicMergeFlux(headScheduler)
|
||||||
|
|
||||||
override fun isRunning(): Boolean {
|
override fun isRunning(): Boolean {
|
||||||
return subscription != null
|
return subscription != null
|
||||||
@@ -30,6 +32,7 @@ open class DynamicMergedHead(
|
|||||||
override fun stop() {
|
override fun stop() {
|
||||||
super.stop()
|
super.stop()
|
||||||
dynamicFlux.stop()
|
dynamicFlux.stop()
|
||||||
|
dynamicFlux = DynamicMergeFlux(headScheduler)
|
||||||
subscription?.dispose()
|
subscription?.dispose()
|
||||||
subscription = null
|
subscription = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ abstract class Multistream(
|
|||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var quorumLabels: List<QuorumForLabels.QuorumItem>? = null
|
private var quorumLabels: List<QuorumForLabels.QuorumItem>? = null
|
||||||
private val removed: MutableMap<String, Upstream> = HashMap()
|
|
||||||
private val meters: MutableMap<String, List<Meter.Id>> = HashMap()
|
private val meters: MutableMap<String, List<Meter.Id>> = HashMap()
|
||||||
private val addedUpstreams = Sinks.many()
|
private val addedUpstreams = Sinks.many()
|
||||||
.multicast()
|
.multicast()
|
||||||
@@ -169,7 +168,6 @@ abstract class Multistream(
|
|||||||
}.also {
|
}.also {
|
||||||
if (it) {
|
if (it) {
|
||||||
upstreams.add(upstream)
|
upstreams.add(upstream)
|
||||||
removed.remove(upstream.getId())
|
|
||||||
addHead(upstream)
|
addHead(upstream)
|
||||||
monitorUpstream(upstream)
|
monitorUpstream(upstream)
|
||||||
}
|
}
|
||||||
@@ -179,7 +177,7 @@ abstract class Multistream(
|
|||||||
upstreams.removeIf { up ->
|
upstreams.removeIf { up ->
|
||||||
(up.getId() == id).also {
|
(up.getId() == id).also {
|
||||||
if (it) {
|
if (it) {
|
||||||
removed[id] = up
|
up.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.also {
|
}.also {
|
||||||
@@ -392,17 +390,17 @@ abstract class Multistream(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.warn("Head processing error: ${e.javaClass} ${e.message}")
|
log.warn("Head processing error: ${e.javaClass} ${e.message}")
|
||||||
}
|
}
|
||||||
val statuses = upstreams.asSequence().plus(removed.values).map { it.getStatus() }
|
val statuses = upstreams.asSequence().map { it.getStatus() }
|
||||||
.groupBy { it }
|
.groupBy { it }
|
||||||
.map { "${it.key.name}/${it.value.size}" }
|
.map { "${it.key.name}/${it.value.size}" }
|
||||||
.joinToString(",")
|
.joinToString(",")
|
||||||
val lag = upstreams.plus(removed.values).joinToString(", ") {
|
val lag = upstreams.joinToString(", ") {
|
||||||
// by default, when no lag is available it uses Long.MAX_VALUE, and it doesn't make sense to print
|
// by default, when no lag is available it uses Long.MAX_VALUE, and it doesn't make sense to print
|
||||||
// status with such value. use NA (as Not Available) instead
|
// status with such value. use NA (as Not Available) instead
|
||||||
val value = it.getLag()
|
val value = it.getLag()
|
||||||
value?.toString() ?: "NA"
|
value?.toString() ?: "NA"
|
||||||
}
|
}
|
||||||
val weak = upstreams.plus(removed.values)
|
val weak = upstreams
|
||||||
.filter { it.getStatus() != UpstreamAvailability.OK }
|
.filter { it.getStatus() != UpstreamAvailability.OK }
|
||||||
.joinToString(", ") { it.getId() }
|
.joinToString(", ") { it.getId() }
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import io.emeraldpay.dshackle.reader.JsonRpcReader
|
|||||||
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
import io.emeraldpay.dshackle.upstream.calls.CallMethods
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
interface Upstream {
|
interface Upstream : Lifecycle {
|
||||||
fun isAvailable(): Boolean
|
fun isAvailable(): Boolean
|
||||||
fun getStatus(): UpstreamAvailability
|
fun getStatus(): UpstreamAvailability
|
||||||
fun observeStatus(): Flux<UpstreamAvailability>
|
fun observeStatus(): Flux<UpstreamAvailability>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
def callTargetsHolder = new CallTargetsHolder()
|
def callTargetsHolder = new CallTargetsHolder()
|
||||||
def configurer = new ConfiguredUpstreams(
|
def configurer = new ConfiguredUpstreams(
|
||||||
Stub(FileResolver),
|
Stub(FileResolver),
|
||||||
Stub(UpstreamsConfig),
|
new UpstreamsConfig(),
|
||||||
Stub(CompressionConfig),
|
Stub(CompressionConfig),
|
||||||
callTargetsHolder,
|
callTargetsHolder,
|
||||||
Mock(ApplicationEventPublisher),
|
Mock(ApplicationEventPublisher),
|
||||||
@@ -59,7 +59,7 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
def callTargetsHolder = new CallTargetsHolder()
|
def callTargetsHolder = new CallTargetsHolder()
|
||||||
def configurer = new ConfiguredUpstreams(
|
def configurer = new ConfiguredUpstreams(
|
||||||
Stub(FileResolver),
|
Stub(FileResolver),
|
||||||
Stub(UpstreamsConfig),
|
new UpstreamsConfig(),
|
||||||
Stub(CompressionConfig),
|
Stub(CompressionConfig),
|
||||||
callTargetsHolder,
|
callTargetsHolder,
|
||||||
Mock(ApplicationEventPublisher),
|
Mock(ApplicationEventPublisher),
|
||||||
@@ -92,7 +92,7 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
def callTargetsHolder = new CallTargetsHolder()
|
def callTargetsHolder = new CallTargetsHolder()
|
||||||
def configurer = new ConfiguredUpstreams(
|
def configurer = new ConfiguredUpstreams(
|
||||||
Stub(FileResolver),
|
Stub(FileResolver),
|
||||||
Stub(UpstreamsConfig),
|
new UpstreamsConfig(),
|
||||||
Stub(CompressionConfig),
|
Stub(CompressionConfig),
|
||||||
callTargetsHolder,
|
callTargetsHolder,
|
||||||
Mock(ApplicationEventPublisher),
|
Mock(ApplicationEventPublisher),
|
||||||
@@ -120,7 +120,7 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
def callTargetsHolder = new CallTargetsHolder()
|
def callTargetsHolder = new CallTargetsHolder()
|
||||||
def configurer = new ConfiguredUpstreams(
|
def configurer = new ConfiguredUpstreams(
|
||||||
Stub(FileResolver),
|
Stub(FileResolver),
|
||||||
Stub(UpstreamsConfig),
|
new UpstreamsConfig(),
|
||||||
Stub(CompressionConfig),
|
Stub(CompressionConfig),
|
||||||
callTargetsHolder,
|
callTargetsHolder,
|
||||||
Mock(ApplicationEventPublisher),
|
Mock(ApplicationEventPublisher),
|
||||||
@@ -153,7 +153,7 @@ class ConfiguredUpstreamsSpec extends Specification {
|
|||||||
def callTargetsHolder = new CallTargetsHolder()
|
def callTargetsHolder = new CallTargetsHolder()
|
||||||
def configurer = new ConfiguredUpstreams(
|
def configurer = new ConfiguredUpstreams(
|
||||||
Stub(FileResolver),
|
Stub(FileResolver),
|
||||||
Stub(UpstreamsConfig),
|
new UpstreamsConfig(),
|
||||||
Stub(CompressionConfig),
|
Stub(CompressionConfig),
|
||||||
callTargetsHolder,
|
callTargetsHolder,
|
||||||
Mock(ApplicationEventPublisher),
|
Mock(ApplicationEventPublisher),
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package io.emeraldpay.dshackle.config.reload
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Chain.ETHEREUM__MAINNET
|
||||||
|
import io.emeraldpay.dshackle.Chain.POLYGON__MAINNET
|
||||||
|
import io.emeraldpay.dshackle.Config
|
||||||
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
|
import io.emeraldpay.dshackle.config.MainConfig
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfigReader
|
||||||
|
import io.emeraldpay.dshackle.foundation.ChainOptionsReader
|
||||||
|
import io.emeraldpay.dshackle.startup.ConfiguredUpstreams
|
||||||
|
import io.emeraldpay.dshackle.startup.UpstreamChangeEvent
|
||||||
|
import io.emeraldpay.dshackle.upstream.CurrentMultistreamHolder
|
||||||
|
import io.emeraldpay.dshackle.upstream.Multistream
|
||||||
|
import io.emeraldpay.dshackle.upstream.Upstream
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
import org.mockito.kotlin.times
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
|
import org.springframework.util.ResourceUtils
|
||||||
|
import sun.misc.Signal
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class ReloadConfigTest {
|
||||||
|
private val fileResolver = FileResolver(File(""))
|
||||||
|
private val mainConfig = MainConfig()
|
||||||
|
|
||||||
|
private val optionsReader = ChainOptionsReader()
|
||||||
|
private val upstreamsConfigReader = UpstreamsConfigReader(fileResolver, optionsReader)
|
||||||
|
|
||||||
|
private val config = mock<Config>()
|
||||||
|
private val reloadConfigService = ReloadConfigService(config, fileResolver, mainConfig)
|
||||||
|
private val applicationEventPublisher = mock<ApplicationEventPublisher>()
|
||||||
|
private val configuredUpstreams = mock<ConfiguredUpstreams>()
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setupTests() {
|
||||||
|
mainConfig.upstreams = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reload upstreams changes`() {
|
||||||
|
val up1 = upstream("local1")
|
||||||
|
val up2 = upstream("local2")
|
||||||
|
val up3 = upstream("local3")
|
||||||
|
|
||||||
|
val msEth = mock<Multistream> {
|
||||||
|
on { getAll() } doReturn listOf(up1, up2)
|
||||||
|
}
|
||||||
|
val msPoly = mock<Multistream> {
|
||||||
|
on { getAll() } doReturn listOf(up3)
|
||||||
|
}
|
||||||
|
val newConfigFile = ResourceUtils.getFile("classpath:configs/upstreams-changed.yaml")
|
||||||
|
whenever(config.getConfigPath()).thenReturn(newConfigFile)
|
||||||
|
|
||||||
|
val currentMultistreamHolder = mock<CurrentMultistreamHolder> {
|
||||||
|
on { getUpstream(ETHEREUM__MAINNET) } doReturn msEth
|
||||||
|
on { getUpstream(POLYGON__MAINNET) } doReturn msPoly
|
||||||
|
}
|
||||||
|
val reloadConfigUpstreamService = ReloadConfigUpstreamService(
|
||||||
|
applicationEventPublisher,
|
||||||
|
currentMultistreamHolder,
|
||||||
|
configuredUpstreams,
|
||||||
|
)
|
||||||
|
val reloadConfig = ReloadConfigSetup(reloadConfigService, reloadConfigUpstreamService)
|
||||||
|
|
||||||
|
val initialConfigIs = ResourceUtils.getFile("classpath:configs/upstreams-initial.yaml").inputStream()
|
||||||
|
val initialConfig = upstreamsConfigReader.read(initialConfigIs)!!
|
||||||
|
val newConfig = upstreamsConfigReader.read(newConfigFile.inputStream())!!
|
||||||
|
mainConfig.upstreams = initialConfig
|
||||||
|
|
||||||
|
reloadConfig.handle(Signal("HUP"))
|
||||||
|
|
||||||
|
val captor = ArgumentCaptor.forClass(UpstreamChangeEvent::class.java)
|
||||||
|
verify(applicationEventPublisher, times(2)).publishEvent(captor.capture())
|
||||||
|
verify(configuredUpstreams).processUpstreams(
|
||||||
|
UpstreamsConfig(
|
||||||
|
newConfig.defaultOptions,
|
||||||
|
mutableListOf(newConfig.upstreams[0], newConfig.upstreams[2]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(3, mainConfig.upstreams!!.upstreams.size)
|
||||||
|
assertEquals(newConfig, mainConfig.upstreams)
|
||||||
|
assertEquals(
|
||||||
|
UpstreamChangeEvent(ETHEREUM__MAINNET, up1, UpstreamChangeEvent.ChangeType.REMOVED),
|
||||||
|
captor.allValues[0],
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
UpstreamChangeEvent(POLYGON__MAINNET, up3, UpstreamChangeEvent.ChangeType.REMOVED),
|
||||||
|
captor.allValues[1],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `stop multistream of there are no upstreams left`() {
|
||||||
|
val up1 = upstream("local1")
|
||||||
|
val up2 = upstream("local2")
|
||||||
|
val up3 = upstream("local3")
|
||||||
|
|
||||||
|
val msEth = mock<Multistream> {
|
||||||
|
on { getAll() } doReturn listOf(up1, up2)
|
||||||
|
on { haveUpstreams() } doReturn false
|
||||||
|
on { isRunning() } doReturn true
|
||||||
|
}
|
||||||
|
val msPoly = mock<Multistream> {
|
||||||
|
on { getAll() } doReturn listOf(up3)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newConfigFile = ResourceUtils.getFile("classpath:configs/upstreams-changed-upstreams-removed.yaml")
|
||||||
|
whenever(config.getConfigPath()).thenReturn(newConfigFile)
|
||||||
|
|
||||||
|
val currentMultistreamHolder = mock<CurrentMultistreamHolder> {
|
||||||
|
on { getUpstream(ETHEREUM__MAINNET) } doReturn msEth
|
||||||
|
on { getUpstream(POLYGON__MAINNET) } doReturn msPoly
|
||||||
|
}
|
||||||
|
val reloadConfigUpstreamService = ReloadConfigUpstreamService(
|
||||||
|
applicationEventPublisher,
|
||||||
|
currentMultistreamHolder,
|
||||||
|
configuredUpstreams,
|
||||||
|
)
|
||||||
|
val reloadConfig = ReloadConfigSetup(reloadConfigService, reloadConfigUpstreamService)
|
||||||
|
val initialConfigIs = ResourceUtils.getFile("classpath:configs/upstreams-initial.yaml").inputStream()
|
||||||
|
val initialConfig = upstreamsConfigReader.read(initialConfigIs)!!
|
||||||
|
val newConfig = upstreamsConfigReader.read(newConfigFile.inputStream())!!
|
||||||
|
mainConfig.upstreams = initialConfig
|
||||||
|
|
||||||
|
reloadConfig.handle(Signal("HUP"))
|
||||||
|
|
||||||
|
val captor = ArgumentCaptor.forClass(UpstreamChangeEvent::class.java)
|
||||||
|
verify(applicationEventPublisher, times(2)).publishEvent(captor.capture())
|
||||||
|
verify(configuredUpstreams).processUpstreams(
|
||||||
|
UpstreamsConfig(
|
||||||
|
newConfig.defaultOptions,
|
||||||
|
mutableListOf(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
verify(msEth).stop()
|
||||||
|
assertEquals(1, mainConfig.upstreams!!.upstreams.size)
|
||||||
|
assertEquals(newConfig, mainConfig.upstreams)
|
||||||
|
assertEquals(
|
||||||
|
UpstreamChangeEvent(ETHEREUM__MAINNET, up1, UpstreamChangeEvent.ChangeType.REMOVED),
|
||||||
|
captor.allValues[0],
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
UpstreamChangeEvent(ETHEREUM__MAINNET, up2, UpstreamChangeEvent.ChangeType.REMOVED),
|
||||||
|
captor.allValues[1],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun upstream(id: String): Upstream =
|
||||||
|
mock {
|
||||||
|
on { getId() } doReturn id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
cluster:
|
||||||
|
upstreams:
|
||||||
|
- id: local3
|
||||||
|
chain: polygon
|
||||||
|
labels:
|
||||||
|
provider: provider1
|
||||||
|
connection:
|
||||||
|
ethereum-pos:
|
||||||
|
execution:
|
||||||
|
rpc:
|
||||||
|
url: "http://localhost"
|
||||||
|
ws:
|
||||||
|
url: "ws://localhost"
|
||||||
34
src/test/resources/configs/upstreams-changed.yaml
Normal file
34
src/test/resources/configs/upstreams-changed.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
cluster:
|
||||||
|
upstreams:
|
||||||
|
- id: local1
|
||||||
|
chain: ethereum
|
||||||
|
labels:
|
||||||
|
provider: provider1
|
||||||
|
connection:
|
||||||
|
ethereum-pos:
|
||||||
|
execution:
|
||||||
|
rpc:
|
||||||
|
url: "http://localhost"
|
||||||
|
- id: local2
|
||||||
|
chain: ethereum
|
||||||
|
labels:
|
||||||
|
provider: provider1
|
||||||
|
connection:
|
||||||
|
ethereum-pos:
|
||||||
|
execution:
|
||||||
|
rpc:
|
||||||
|
url: "http://localhost"
|
||||||
|
ws:
|
||||||
|
url: "ws://localhost"
|
||||||
|
- id: local3
|
||||||
|
chain: polygon
|
||||||
|
labels:
|
||||||
|
new: label
|
||||||
|
provider: provider1
|
||||||
|
connection:
|
||||||
|
ethereum-pos:
|
||||||
|
execution:
|
||||||
|
rpc:
|
||||||
|
url: "http://localhost"
|
||||||
|
ws:
|
||||||
|
url: "ws://localhost"
|
||||||
35
src/test/resources/configs/upstreams-initial.yaml
Normal file
35
src/test/resources/configs/upstreams-initial.yaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
cluster:
|
||||||
|
upstreams:
|
||||||
|
- id: local1
|
||||||
|
chain: ethereum
|
||||||
|
labels:
|
||||||
|
provider: provider1
|
||||||
|
connection:
|
||||||
|
ethereum-pos:
|
||||||
|
execution:
|
||||||
|
rpc:
|
||||||
|
url: "http://localhost"
|
||||||
|
ws:
|
||||||
|
url: "ws://localhost"
|
||||||
|
- id: local2
|
||||||
|
chain: ethereum
|
||||||
|
labels:
|
||||||
|
provider: provider1
|
||||||
|
connection:
|
||||||
|
ethereum-pos:
|
||||||
|
execution:
|
||||||
|
rpc:
|
||||||
|
url: "http://localhost"
|
||||||
|
ws:
|
||||||
|
url: "ws://localhost"
|
||||||
|
- id: local3
|
||||||
|
chain: polygon
|
||||||
|
labels:
|
||||||
|
provider: provider1
|
||||||
|
connection:
|
||||||
|
ethereum-pos:
|
||||||
|
execution:
|
||||||
|
rpc:
|
||||||
|
url: "http://localhost"
|
||||||
|
ws:
|
||||||
|
url: "ws://localhost"
|
||||||
Reference in New Issue
Block a user