solution: stream updates to blockchain status
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
package io.emeraldpay.dshackle.rpc
|
package io.emeraldpay.dshackle.rpc
|
||||||
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
|
import io.grpc.StatusRuntimeException
|
||||||
import io.grpc.stub.StreamObserver
|
import io.grpc.stub.StreamObserver
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import reactor.core.Disposable
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class SubscribeStatus(
|
class SubscribeStatus(
|
||||||
@@ -13,6 +17,24 @@ class SubscribeStatus(
|
|||||||
|
|
||||||
fun subscribeStatus(request: BlockchainOuterClass.StatusRequest, responseObserver: StreamObserver<BlockchainOuterClass.ChainStatus>) {
|
fun subscribeStatus(request: BlockchainOuterClass.StatusRequest, responseObserver: StreamObserver<BlockchainOuterClass.ChainStatus>) {
|
||||||
upstreams.getAvailable().forEach { chain ->
|
upstreams.getAvailable().forEach { chain ->
|
||||||
|
var d: Disposable? = null
|
||||||
|
d = upstreams.getUpstream(chain)?.observeStatus()?.subscribe { availability ->
|
||||||
|
val chainStatus = BlockchainOuterClass.ChainStatus.newBuilder()
|
||||||
|
.setChain(Common.ChainRef.forNumber(chain.id))
|
||||||
|
.setAvailable(availability == UpstreamAvailability.OK)
|
||||||
|
.setQuorum(0)
|
||||||
|
if (availability == UpstreamAvailability.OK) {
|
||||||
|
upstreams.getUpstream(chain)?.getOptions()?.let { opts ->
|
||||||
|
chainStatus.setQuorum(opts.quorum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
responseObserver.onNext(chainStatus.build())
|
||||||
|
} catch (e: StatusRuntimeException) {
|
||||||
|
// gRPC channel was closed
|
||||||
|
d?.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,44 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
abstract class AggregatedUpstreams {
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
import java.util.function.Predicate
|
||||||
|
|
||||||
|
abstract class AggregatedUpstreams: Upstream {
|
||||||
|
|
||||||
abstract fun getAll(): List<Upstream>
|
abstract fun getAll(): List<Upstream>
|
||||||
abstract fun addUpstream(upstream: Upstream)
|
abstract fun addUpstream(upstream: Upstream)
|
||||||
abstract fun getApis(quorum: Int): Iterator<EthereumApi>
|
abstract fun getApis(quorum: Int): Iterator<EthereumApi>
|
||||||
abstract fun getApi(): EthereumApi
|
|
||||||
abstract fun getHead(): EthereumHead
|
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||||
|
val upstreamsFluxes = getAll().map { up -> up.observeStatus().map { UpstreamStatus(up, it) } }
|
||||||
|
return Flux.merge(upstreamsFluxes)
|
||||||
|
.filter(FilterBestAvailability())
|
||||||
|
.map { it.status }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAvailable(): Boolean {
|
||||||
|
return getAll().any { it.isAvailable() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStatus(): UpstreamAvailability {
|
||||||
|
val upstreams = getAll()
|
||||||
|
return if (upstreams.isEmpty()) UpstreamAvailability.UNAVAILABLE
|
||||||
|
else upstreams.map { it.getStatus() }.min()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOptions(): UpstreamsConfig.Options {
|
||||||
|
val options = UpstreamsConfig.Options()
|
||||||
|
options.quorum = getAll().filter {
|
||||||
|
it.getStatus() == UpstreamAvailability.OK
|
||||||
|
}.sumBy {
|
||||||
|
it.getOptions().quorum
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
class SingleApi(
|
class SingleApi(
|
||||||
private val quorumApi: QuorumApi
|
private val quorumApi: QuorumApi
|
||||||
@@ -47,6 +79,23 @@ abstract class AggregatedUpstreams {
|
|||||||
}
|
}
|
||||||
throw IllegalStateException("No upstream API available")
|
throw IllegalStateException("No upstream API available")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpstreamStatus(val upstream: Upstream, val status: UpstreamAvailability, val ts: Instant = Instant.now())
|
||||||
|
|
||||||
|
class FilterBestAvailability(): Predicate<UpstreamStatus> {
|
||||||
|
private val lastRef = AtomicReference<UpstreamStatus>()
|
||||||
|
|
||||||
|
override fun test(t: UpstreamStatus): Boolean {
|
||||||
|
val last = lastRef.get()
|
||||||
|
val changed = last == null
|
||||||
|
|| t.status > last.status
|
||||||
|
|| (last.upstream == t.upstream && t.status != last.status)
|
||||||
|
|| last.ts.isBefore(Instant.now() - Duration.ofSeconds(60))
|
||||||
|
if (changed) {
|
||||||
|
lastRef.set(t)
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
@@ -57,9 +58,9 @@ class ChainUpstreams (
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun printStatus() {
|
fun printStatus() {
|
||||||
var height: Long = -1
|
var height: Long? = null
|
||||||
try {
|
try {
|
||||||
height = head!!.getHead().block(Duration.ofSeconds(1))?.number ?: -1
|
height = head!!.getHead().block(Duration.ofSeconds(1))?.number
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
//timout
|
//timout
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -70,7 +71,7 @@ class ChainUpstreams (
|
|||||||
.map { "${it.key.name}/${it.value.size}" }
|
.map { "${it.key.name}/${it.value.size}" }
|
||||||
.joinToString(",")
|
.joinToString(",")
|
||||||
|
|
||||||
log.info("State of ${chain.chainCode}: height=$height, status=$statuses")
|
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import reactor.core.publisher.Mono
|
|||||||
import reactor.core.publisher.toFlux
|
import reactor.core.publisher.toFlux
|
||||||
import reactor.util.function.Tuple3
|
import reactor.util.function.Tuple3
|
||||||
import reactor.util.function.Tuples
|
import reactor.util.function.Tuples
|
||||||
|
import java.time.Duration
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
@@ -97,6 +98,7 @@ class EthereumGrpcTransport(
|
|||||||
bi.onError(RpcException(-32603, "RPC response not received"))
|
bi.onError(RpcException(-32603, "RPC response not received"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.timeout(Duration.ofSeconds(15))
|
||||||
.toFuture()
|
.toFuture()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,11 @@ package io.emeraldpay.dshackle.upstream
|
|||||||
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
|
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.TopicProcessor
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
class EthereumUpstream(
|
class EthereumUpstream(
|
||||||
@@ -24,6 +28,7 @@ class EthereumUpstream(
|
|||||||
|
|
||||||
private val validator = UpstreamValidator(this, options)
|
private val validator = UpstreamValidator(this, options)
|
||||||
private val status = AtomicReference(UpstreamAvailability.UNAVAILABLE)
|
private val status = AtomicReference(UpstreamAvailability.UNAVAILABLE)
|
||||||
|
private val statusStream: TopicProcessor<UpstreamAvailability> = TopicProcessor.create()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
log.info("Configured for ${chain.chainName}")
|
log.info("Configured for ${chain.chainName}")
|
||||||
@@ -31,6 +36,7 @@ class EthereumUpstream(
|
|||||||
validator.start()
|
validator.start()
|
||||||
.subscribe {
|
.subscribe {
|
||||||
status.set(it)
|
status.set(it)
|
||||||
|
statusStream.onNext(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +48,10 @@ class EthereumUpstream(
|
|||||||
return status.get()
|
return status.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||||
|
return Flux.from(statusStream)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getHead(): EthereumHead {
|
override fun getHead(): EthereumHead {
|
||||||
return head
|
return head
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ open class GrpcUpstream(
|
|||||||
private var status = AtomicReference<UpstreamAvailability>(UpstreamAvailability.UNAVAILABLE)
|
private var status = AtomicReference<UpstreamAvailability>(UpstreamAvailability.UNAVAILABLE)
|
||||||
private val head = Head(this)
|
private val head = Head(this)
|
||||||
private val api: EthereumApi
|
private val api: EthereumApi
|
||||||
|
private val statusStream: TopicProcessor<UpstreamAvailability> = TopicProcessor.create()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val grpcTransport = EthereumGrpcTransport(chain, client, objectMapper)
|
val grpcTransport = EthereumGrpcTransport(chain, client, objectMapper)
|
||||||
@@ -52,7 +53,7 @@ open class GrpcUpstream(
|
|||||||
.toMono()
|
.toMono()
|
||||||
|
|
||||||
val retry: Function<Flux<BlockchainOuterClass.ChainHead>, Flux<BlockchainOuterClass.ChainHead>> = Function {
|
val retry: Function<Flux<BlockchainOuterClass.ChainHead>, Flux<BlockchainOuterClass.ChainHead>> = Function {
|
||||||
status.set(UpstreamAvailability.UNAVAILABLE)
|
setStatus(UpstreamAvailability.UNAVAILABLE)
|
||||||
client.subscribeHead(chainRef)
|
client.subscribeHead(chainRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,14 +81,14 @@ open class GrpcUpstream(
|
|||||||
log.debug("New block ${block.number} on ${chain}")
|
log.debug("New block ${block.number} on ${chain}")
|
||||||
headBlock.set(block)
|
headBlock.set(block)
|
||||||
streamBlocks.onNext(block)
|
streamBlocks.onNext(block)
|
||||||
status.set(UpstreamAvailability.OK)
|
setStatus(UpstreamAvailability.OK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(conf: BlockchainOuterClass.DescribeChain) {
|
fun init(conf: BlockchainOuterClass.DescribeChain) {
|
||||||
val available = conf.available
|
val available = conf.available
|
||||||
val quorum = conf.quorum
|
val quorum = conf.quorum
|
||||||
status.set(
|
setStatus(
|
||||||
if (available && quorum > 0) UpstreamAvailability.OK else UpstreamAvailability.UNAVAILABLE
|
if (available && quorum > 0) UpstreamAvailability.OK else UpstreamAvailability.UNAVAILABLE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -95,11 +96,15 @@ open class GrpcUpstream(
|
|||||||
fun onStatus(value: BlockchainOuterClass.ChainStatus) {
|
fun onStatus(value: BlockchainOuterClass.ChainStatus) {
|
||||||
val available = value.available
|
val available = value.available
|
||||||
val quorum = value.quorum
|
val quorum = value.quorum
|
||||||
status.set(
|
setStatus(
|
||||||
if (available && quorum > 0) UpstreamAvailability.OK else UpstreamAvailability.UNAVAILABLE
|
if (available && quorum > 0) UpstreamAvailability.OK else UpstreamAvailability.UNAVAILABLE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setStatus(value: UpstreamAvailability) {
|
||||||
|
status.set(value)
|
||||||
|
statusStream.onNext(value)
|
||||||
|
}
|
||||||
// ------------------------------------------------------------------------------------------
|
// ------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@@ -111,6 +116,10 @@ open class GrpcUpstream(
|
|||||||
return status.get()
|
return status.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||||
|
return Flux.from(statusStream)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getHead(): EthereumHead {
|
override fun getHead(): EthereumHead {
|
||||||
return head
|
return head
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
|
||||||
interface Upstream {
|
interface Upstream {
|
||||||
fun isAvailable(): Boolean
|
fun isAvailable(): Boolean
|
||||||
fun getStatus(): UpstreamAvailability
|
fun getStatus(): UpstreamAvailability
|
||||||
|
fun observeStatus(): Flux<UpstreamAvailability>
|
||||||
fun getHead(): EthereumHead
|
fun getHead(): EthereumHead
|
||||||
fun getApi(): EthereumApi
|
fun getApi(): EthereumApi
|
||||||
fun getOptions(): UpstreamsConfig.Options
|
fun getOptions(): UpstreamsConfig.Options
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class EthereumGrpcTransportSpec extends Specification {
|
|||||||
def status = transport.execute(batch.items).get()
|
def status = transport.execute(batch.items).get()
|
||||||
|
|
||||||
then:
|
then:
|
||||||
1 * otherSideUpstreams.ethereumUpstream(Chain.ETHEREUM) >> otherSideAggr
|
1 * otherSideUpstreams.getUpstream(Chain.ETHEREUM) >> otherSideAggr
|
||||||
1 * otherSideAggr.api >> otherSideApi
|
1 * otherSideAggr.api >> otherSideApi
|
||||||
status.failed == 0
|
status.failed == 0
|
||||||
status.succeed == 1
|
status.succeed == 1
|
||||||
@@ -87,7 +87,7 @@ class EthereumGrpcTransportSpec extends Specification {
|
|||||||
def status = transport.execute(batch.items).get()
|
def status = transport.execute(batch.items).get()
|
||||||
|
|
||||||
then:
|
then:
|
||||||
1 * otherSideUpstreams.ethereumUpstream(Chain.ETHEREUM) >> otherSideAggr
|
1 * otherSideUpstreams.getUpstream(Chain.ETHEREUM) >> otherSideAggr
|
||||||
1 * otherSideAggr.api >> otherSideApi
|
1 * otherSideAggr.api >> otherSideApi
|
||||||
status.failed == 0
|
status.failed == 0
|
||||||
status.succeed == 2
|
status.succeed == 2
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import spock.lang.Specification
|
||||||
|
|
||||||
|
class UpstreamAvailabilitySpec extends Specification {
|
||||||
|
|
||||||
|
def "Defined order"() {
|
||||||
|
expect:
|
||||||
|
UpstreamAvailability.OK.compareTo(UpstreamAvailability.IMMATURE) < 0
|
||||||
|
UpstreamAvailability.IMMATURE.compareTo(UpstreamAvailability.SYNCING) < 0
|
||||||
|
UpstreamAvailability.SYNCING.compareTo(UpstreamAvailability.LAGGING) < 0
|
||||||
|
UpstreamAvailability.LAGGING.compareTo(UpstreamAvailability.UNAVAILABLE) < 0
|
||||||
|
|
||||||
|
UpstreamAvailability.OK.compareTo(UpstreamAvailability.UNAVAILABLE) < 0
|
||||||
|
UpstreamAvailability.UNAVAILABLE.compareTo(UpstreamAvailability.OK) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
def "Sort array"() {
|
||||||
|
setup:
|
||||||
|
def items = [UpstreamAvailability.OK, UpstreamAvailability.SYNCING, UpstreamAvailability.IMMATURE, UpstreamAvailability.OK]
|
||||||
|
when:
|
||||||
|
Collections.sort(items)
|
||||||
|
then:
|
||||||
|
items == [UpstreamAvailability.OK, UpstreamAvailability.OK, UpstreamAvailability.IMMATURE, UpstreamAvailability.SYNCING]
|
||||||
|
}
|
||||||
|
|
||||||
|
def "First is most available"() {
|
||||||
|
expect:
|
||||||
|
[UpstreamAvailability.UNAVAILABLE, UpstreamAvailability.OK].toSorted().first() == UpstreamAvailability.OK
|
||||||
|
[UpstreamAvailability.OK, UpstreamAvailability.UNAVAILABLE].toSorted().first() == UpstreamAvailability.OK
|
||||||
|
[UpstreamAvailability.OK, UpstreamAvailability.LAGGING].toSorted().first() == UpstreamAvailability.OK
|
||||||
|
[UpstreamAvailability.LAGGING, UpstreamAvailability.UNAVAILABLE].toSorted().first() == UpstreamAvailability.LAGGING
|
||||||
|
[UpstreamAvailability.OK, UpstreamAvailability.SYNCING].toSorted().first() == UpstreamAvailability.OK
|
||||||
|
[UpstreamAvailability.SYNCING, UpstreamAvailability.UNAVAILABLE].toSorted().first() == UpstreamAvailability.SYNCING
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user