solution: correctly account for upstream lag
This commit is contained in:
@@ -15,6 +15,7 @@ class ChainUpstreams (
|
||||
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
|
||||
private var seq = 0
|
||||
private var head: EthereumHead?
|
||||
private var lagObserver: HeadLagObserver? = null
|
||||
|
||||
init {
|
||||
head = updateHead()
|
||||
@@ -28,10 +29,18 @@ class ChainUpstreams (
|
||||
if (current != null && Closeable::class.java.isAssignableFrom(current.javaClass)) {
|
||||
(current as Closeable).close()
|
||||
}
|
||||
lagObserver?.close()
|
||||
lagObserver = null
|
||||
return if (upstreams.size == 1) {
|
||||
upstreams.first().getHead()
|
||||
val upstream = upstreams.first()
|
||||
upstream.setLag(0)
|
||||
upstream.getHead()
|
||||
} else {
|
||||
EthereumHeadMerge(upstreams.map { it.getHead() })
|
||||
val newHead = EthereumHeadMerge(upstreams.map { it.getHead() })
|
||||
val lagObserver = HeadLagObserver(newHead, upstreams)
|
||||
lagObserver.start()
|
||||
this.lagObserver = lagObserver
|
||||
newHead
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +69,13 @@ class ChainUpstreams (
|
||||
return head!!
|
||||
}
|
||||
|
||||
override fun setLag(lag: Long) {
|
||||
}
|
||||
|
||||
override fun getLag(): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
fun printStatus() {
|
||||
var height: Long? = null
|
||||
try {
|
||||
@@ -73,8 +89,10 @@ class ChainUpstreams (
|
||||
.groupBy { it }
|
||||
.map { "${it.key.name}/${it.value.size}" }
|
||||
.joinToString(",")
|
||||
val lag = upstreams.map { it.getLag() }
|
||||
.joinToString(", ")
|
||||
|
||||
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses")
|
||||
log.info("State of ${chain.chainCode}: height=${height ?: '?'}, status=$statuses, lag=[$lag]")
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import reactor.core.publisher.toFlux
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.annotation.PostConstruct
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
@@ -27,7 +28,7 @@ open class ConfiguredUpstreams(
|
||||
) : Upstreams {
|
||||
|
||||
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
|
||||
private val chainMapping = HashMap<Chain, ChainUpstreams>()
|
||||
private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams>()
|
||||
|
||||
private val chainNames = mapOf(
|
||||
"ethereum" to Chain.ETHEREUM,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.TopicProcessor
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
abstract class DefaultUpstream(
|
||||
lag: Long,
|
||||
avail: UpstreamAvailability
|
||||
) : Upstream {
|
||||
|
||||
constructor() : this(Long.MAX_VALUE, UpstreamAvailability.UNAVAILABLE)
|
||||
|
||||
private val status = AtomicReference(Status(lag, avail, statusByLag(lag, avail)))
|
||||
private val statusStream: TopicProcessor<UpstreamAvailability> = TopicProcessor.create()
|
||||
|
||||
override fun getStatus(): UpstreamAvailability {
|
||||
return status.get().status
|
||||
}
|
||||
|
||||
fun setStatus(avail: UpstreamAvailability) {
|
||||
status.updateAndGet { curr ->
|
||||
Status(curr.lag, avail, statusByLag(curr.lag, avail))
|
||||
}
|
||||
}
|
||||
|
||||
fun statusByLag(lag: Long, proposed: UpstreamAvailability): UpstreamAvailability {
|
||||
return if (proposed == UpstreamAvailability.OK) {
|
||||
when {
|
||||
lag > 6 -> UpstreamAvailability.SYNCING
|
||||
lag > 1 -> UpstreamAvailability.LAGGING
|
||||
else -> proposed
|
||||
}
|
||||
} else proposed
|
||||
}
|
||||
|
||||
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||
return Flux.from(statusStream)
|
||||
}
|
||||
|
||||
override fun setLag(lag: Long) {
|
||||
if (lag < 0) {
|
||||
setLag(0)
|
||||
} else {
|
||||
status.updateAndGet { curr ->
|
||||
Status(lag, curr.avail, statusByLag(lag, curr.avail))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLag(): Long {
|
||||
return this.status.get().lag
|
||||
}
|
||||
|
||||
class Status(val lag: Long, val avail: UpstreamAvailability, val status: UpstreamAvailability)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.TopicProcessor
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
open class EthereumUpstream(
|
||||
@@ -16,7 +17,7 @@ open class EthereumUpstream(
|
||||
private val options: UpstreamsConfig.Options,
|
||||
val node: NodeDetailsList.NodeDetails,
|
||||
private val targets: EthereumTargets
|
||||
): Upstream {
|
||||
): DefaultUpstream() {
|
||||
|
||||
override fun getSupportedTargets(): Set<String> {
|
||||
return targets.getSupportedMethods()
|
||||
@@ -33,34 +34,17 @@ open class EthereumUpstream(
|
||||
}
|
||||
|
||||
private val validator = UpstreamValidator(this, options)
|
||||
private val status = AtomicReference(UpstreamAvailability.UNAVAILABLE)
|
||||
private val statusStream: TopicProcessor<UpstreamAvailability> = TopicProcessor.create()
|
||||
|
||||
init {
|
||||
log.info("Configured for ${chain.chainName}")
|
||||
api.upstream = this
|
||||
|
||||
validator.start()
|
||||
.subscribe {
|
||||
status.set(it)
|
||||
statusStream.onNext(it)
|
||||
}
|
||||
.subscribe(this::setStatus)
|
||||
}
|
||||
|
||||
override fun isAvailable(matcher: Selector.Matcher): Boolean {
|
||||
return status.get() == UpstreamAvailability.OK && matcher.matches(node.labels)
|
||||
}
|
||||
|
||||
override fun getStatus(): UpstreamAvailability {
|
||||
return status.get()
|
||||
}
|
||||
|
||||
fun setStatus(avail: UpstreamAvailability) {
|
||||
status.set(avail)
|
||||
}
|
||||
|
||||
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||
return Flux.from(statusStream)
|
||||
return getStatus() == UpstreamAvailability.OK && matcher.matches(node.labels)
|
||||
}
|
||||
|
||||
override fun getHead(): EthereumHead {
|
||||
@@ -78,4 +62,5 @@ open class EthereumUpstream(
|
||||
override fun getOptions(): UpstreamsConfig.Options {
|
||||
return options
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ open class GrpcUpstream(
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val options: UpstreamsConfig.Options,
|
||||
private val targets: EthereumTargets
|
||||
): Upstream {
|
||||
): DefaultUpstream() {
|
||||
|
||||
constructor(chain: Chain, client: ReactorBlockchainGrpc.ReactorBlockchainStub, objectMapper: ObjectMapper, targets: EthereumTargets)
|
||||
: this(chain, client, objectMapper, UpstreamsConfig.Options.getDefaults(), targets)
|
||||
@@ -123,11 +123,6 @@ open class GrpcUpstream(
|
||||
)
|
||||
}
|
||||
|
||||
private fun setStatus(value: UpstreamAvailability) {
|
||||
status.set(value)
|
||||
statusStream.onNext(value)
|
||||
}
|
||||
|
||||
fun getNodes(): NodeDetailsList {
|
||||
return nodes.get()
|
||||
}
|
||||
@@ -144,14 +139,6 @@ open class GrpcUpstream(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStatus(): UpstreamAvailability {
|
||||
return status.get()
|
||||
}
|
||||
|
||||
override fun observeStatus(): Flux<UpstreamAvailability> {
|
||||
return Flux.from(statusStream)
|
||||
}
|
||||
|
||||
override fun getHead(): EthereumHead {
|
||||
return head
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.Disposable
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.toFlux
|
||||
import reactor.util.function.Tuple2
|
||||
import reactor.util.function.Tuples
|
||||
import java.io.Closeable
|
||||
import java.time.Duration
|
||||
|
||||
class HeadLagObserver (
|
||||
private val master: EthereumHead,
|
||||
private val followers: Collection<Upstream>
|
||||
): Closeable {
|
||||
|
||||
private val log = LoggerFactory.getLogger(HeadLagObserver::class.java)
|
||||
|
||||
private var current: Disposable? = null
|
||||
|
||||
fun start() {
|
||||
current = subscription().subscribe { }
|
||||
}
|
||||
|
||||
private fun subscription(): Flux<Unit> {
|
||||
return master.getFlux()
|
||||
.flatMap(this::probeFollowers)
|
||||
.map { item ->
|
||||
item.t2.setLag(item.t1)
|
||||
}
|
||||
}
|
||||
|
||||
fun probeFollowers(top: BlockJson<TransactionId>): Flux<Tuple2<Long, Upstream>> {
|
||||
return followers.toFlux()
|
||||
.parallel(followers.size)
|
||||
.flatMap { mapLagging(top, it, getCurrentBlocks(it)) }
|
||||
.sequential()
|
||||
.onErrorContinue { t, _ -> log.warn("Failed to update lagging distance", t) }
|
||||
}
|
||||
|
||||
fun getCurrentBlocks(up: Upstream): Flux<BlockJson<TransactionId>> {
|
||||
val head = up.getHead()
|
||||
return Flux.concat(head.getHead(), head.getFlux())
|
||||
.take(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
fun mapLagging(top: BlockJson<TransactionId>, up: Upstream, blocks: Flux<BlockJson<TransactionId>>): Flux<Tuple2<Long, Upstream>> {
|
||||
return blocks
|
||||
.map { extractDistance(top, it) }
|
||||
.takeUntil{ lag -> lag <= 0L }
|
||||
.map { Tuples.of(it, up) }
|
||||
.doOnError { t ->
|
||||
log.warn("Failed to find distance for $up", t)
|
||||
}
|
||||
}
|
||||
|
||||
fun extractDistance(top: BlockJson<TransactionId>, curr: BlockJson<TransactionId>): Long {
|
||||
return when {
|
||||
curr.number > top.number -> if (curr.totalDifficulty >= top.totalDifficulty) 0 else forkDistance(top, curr)
|
||||
curr.number == top.number -> if (curr.totalDifficulty == top.totalDifficulty) 0 else forkDistance(top, curr)
|
||||
else -> top.number - curr.number
|
||||
}
|
||||
}
|
||||
|
||||
fun forkDistance(top: BlockJson<TransactionId>, curr: BlockJson<TransactionId>): Long {
|
||||
//TODO look for common ancestor? though it may be a corruption
|
||||
return 6
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
current?.dispose()
|
||||
current = null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,44 +2,32 @@ package io.emeraldpay.dshackle.upstream
|
||||
|
||||
import io.infinitape.etherjar.domain.TransactionId
|
||||
import io.infinitape.etherjar.rpc.json.BlockJson
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
|
||||
|
||||
private var head: Flux<BlockJson<TransactionId>> = Flux.empty<BlockJson<TransactionId>>()
|
||||
private val lock = ReentrantLock()
|
||||
private var resolved = false
|
||||
private var result: ByteArray? = null
|
||||
private val result: AtomicReference<ByteArray> = AtomicReference()
|
||||
|
||||
override fun init(head: Head<BlockJson<TransactionId>>) {
|
||||
this.head = head.getFlux()
|
||||
}
|
||||
|
||||
override fun isResolved(): Boolean {
|
||||
return resolved && result != null
|
||||
return result.get() != null
|
||||
}
|
||||
|
||||
override fun record(response: ByteArray, upstream: Upstream) {
|
||||
Mono.from(head)
|
||||
.zipWith(upstream.getHead().getHead())
|
||||
.map {
|
||||
val top = it.t1
|
||||
val current = it.t2
|
||||
return@map (top.number - current.number) < maxLag
|
||||
}.subscribe { fresh ->
|
||||
if (fresh) {
|
||||
lock.withLock {
|
||||
result = response
|
||||
resolved = true
|
||||
}
|
||||
}
|
||||
}
|
||||
val lagging = upstream.getLag() > maxLag
|
||||
if (!lagging) {
|
||||
result.set(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getResult(): ByteArray {
|
||||
return result!!
|
||||
return result.get()
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,6 @@ interface Upstream {
|
||||
fun getApi(matcher: Selector.Matcher): EthereumApi
|
||||
fun getOptions(): UpstreamsConfig.Options
|
||||
fun getSupportedTargets(): Set<String>
|
||||
fun setLag(lag: Long)
|
||||
fun getLag(): Long
|
||||
}
|
||||
Reference in New Issue
Block a user