solution: correctly account for upstream lag

This commit is contained in:
Igor Artamonov
2019-08-09 00:29:26 -04:00
parent 7f9478d538
commit 7205a33ff3
12 changed files with 340 additions and 59 deletions

View File

@@ -15,6 +15,7 @@ class ChainUpstreams (
private val log = LoggerFactory.getLogger(ChainUpstreams::class.java) private val log = LoggerFactory.getLogger(ChainUpstreams::class.java)
private var seq = 0 private var seq = 0
private var head: EthereumHead? private var head: EthereumHead?
private var lagObserver: HeadLagObserver? = null
init { init {
head = updateHead() head = updateHead()
@@ -28,10 +29,18 @@ class ChainUpstreams (
if (current != null && Closeable::class.java.isAssignableFrom(current.javaClass)) { if (current != null && Closeable::class.java.isAssignableFrom(current.javaClass)) {
(current as Closeable).close() (current as Closeable).close()
} }
lagObserver?.close()
lagObserver = null
return if (upstreams.size == 1) { return if (upstreams.size == 1) {
upstreams.first().getHead() val upstream = upstreams.first()
upstream.setLag(0)
upstream.getHead()
} else { } 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!! return head!!
} }
override fun setLag(lag: Long) {
}
override fun getLag(): Long {
return 0
}
fun printStatus() { fun printStatus() {
var height: Long? = null var height: Long? = null
try { try {
@@ -73,8 +89,10 @@ class ChainUpstreams (
.groupBy { it } .groupBy { it }
.map { "${it.key.name}/${it.value.size}" } .map { "${it.key.name}/${it.value.size}" }
.joinToString(",") .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]")
} }

View File

@@ -16,6 +16,7 @@ import reactor.core.publisher.toFlux
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
import kotlin.collections.HashMap import kotlin.collections.HashMap
@@ -27,7 +28,7 @@ open class ConfiguredUpstreams(
) : Upstreams { ) : Upstreams {
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java) private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
private val chainMapping = HashMap<Chain, ChainUpstreams>() private val chainMapping = ConcurrentHashMap<Chain, ChainUpstreams>()
private val chainNames = mapOf( private val chainNames = mapOf(
"ethereum" to Chain.ETHEREUM, "ethereum" to Chain.ETHEREUM,

View File

@@ -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)
}

View File

@@ -7,6 +7,7 @@ import io.infinitape.etherjar.rpc.json.BlockJson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.TopicProcessor import reactor.core.publisher.TopicProcessor
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
open class EthereumUpstream( open class EthereumUpstream(
@@ -16,7 +17,7 @@ open class EthereumUpstream(
private val options: UpstreamsConfig.Options, private val options: UpstreamsConfig.Options,
val node: NodeDetailsList.NodeDetails, val node: NodeDetailsList.NodeDetails,
private val targets: EthereumTargets private val targets: EthereumTargets
): Upstream { ): DefaultUpstream() {
override fun getSupportedTargets(): Set<String> { override fun getSupportedTargets(): Set<String> {
return targets.getSupportedMethods() return targets.getSupportedMethods()
@@ -33,34 +34,17 @@ open class EthereumUpstream(
} }
private val validator = UpstreamValidator(this, options) private val validator = UpstreamValidator(this, options)
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}")
api.upstream = this api.upstream = this
validator.start() validator.start()
.subscribe { .subscribe(this::setStatus)
status.set(it)
statusStream.onNext(it)
}
} }
override fun isAvailable(matcher: Selector.Matcher): Boolean { override fun isAvailable(matcher: Selector.Matcher): Boolean {
return status.get() == UpstreamAvailability.OK && matcher.matches(node.labels) return getStatus() == 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)
} }
override fun getHead(): EthereumHead { override fun getHead(): EthereumHead {
@@ -78,4 +62,5 @@ open class EthereumUpstream(
override fun getOptions(): UpstreamsConfig.Options { override fun getOptions(): UpstreamsConfig.Options {
return options return options
} }
} }

View File

@@ -30,7 +30,7 @@ open class GrpcUpstream(
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val options: UpstreamsConfig.Options, private val options: UpstreamsConfig.Options,
private val targets: EthereumTargets private val targets: EthereumTargets
): Upstream { ): DefaultUpstream() {
constructor(chain: Chain, client: ReactorBlockchainGrpc.ReactorBlockchainStub, objectMapper: ObjectMapper, targets: EthereumTargets) constructor(chain: Chain, client: ReactorBlockchainGrpc.ReactorBlockchainStub, objectMapper: ObjectMapper, targets: EthereumTargets)
: this(chain, client, objectMapper, UpstreamsConfig.Options.getDefaults(), targets) : 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 { fun getNodes(): NodeDetailsList {
return nodes.get() 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 { override fun getHead(): EthereumHead {
return head return head
} }

View File

@@ -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
}
}

View File

@@ -2,44 +2,32 @@ package io.emeraldpay.dshackle.upstream
import io.infinitape.etherjar.domain.TransactionId import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.json.BlockJson import io.infinitape.etherjar.rpc.json.BlockJson
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum { class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
private var head: Flux<BlockJson<TransactionId>> = Flux.empty<BlockJson<TransactionId>>() private val result: AtomicReference<ByteArray> = AtomicReference()
private val lock = ReentrantLock()
private var resolved = false
private var result: ByteArray? = null
override fun init(head: Head<BlockJson<TransactionId>>) { override fun init(head: Head<BlockJson<TransactionId>>) {
this.head = head.getFlux()
} }
override fun isResolved(): Boolean { override fun isResolved(): Boolean {
return resolved && result != null return result.get() != null
} }
override fun record(response: ByteArray, upstream: Upstream) { override fun record(response: ByteArray, upstream: Upstream) {
Mono.from(head) val lagging = upstream.getLag() > maxLag
.zipWith(upstream.getHead().getHead()) if (!lagging) {
.map { result.set(response)
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
}
}
}
} }
override fun getResult(): ByteArray { override fun getResult(): ByteArray {
return result!! return result.get()
} }
} }

View File

@@ -11,4 +11,6 @@ interface Upstream {
fun getApi(matcher: Selector.Matcher): EthereumApi fun getApi(matcher: Selector.Matcher): EthereumApi
fun getOptions(): UpstreamsConfig.Options fun getOptions(): UpstreamsConfig.Options
fun getSupportedTargets(): Set<String> fun getSupportedTargets(): Set<String>
fun setLag(lag: Long)
fun getLag(): Long
} }

View File

@@ -32,6 +32,7 @@ class FilteringApiIteratorSpec extends Specification {
} }
def matcher = new Selector.LabelMatcher("test", ["foo"]) def matcher = new Selector.LabelMatcher("test", ["foo"])
upstreams.forEach { upstreams.forEach {
it.setLag(0)
it.setStatus(UpstreamAvailability.OK) it.setStatus(UpstreamAvailability.OK)
} }
when: when:

View File

@@ -56,6 +56,7 @@ class GrpcUpstreamSpec extends Specification {
} }
}) })
def upstream = new GrpcUpstream(chain, client, objectMapper, ethereumTargets) def upstream = new GrpcUpstream(chain, client, objectMapper, ethereumTargets)
upstream.setLag(0)
when: when:
upstream.connect() upstream.connect()
def h = upstream.head.head.block(Duration.ofSeconds(1)) def h = upstream.head.head.block(Duration.ofSeconds(1))
@@ -111,6 +112,7 @@ class GrpcUpstreamSpec extends Specification {
} }
}) })
def upstream = new GrpcUpstream(chain, client, objectMapper, ethereumTargets) def upstream = new GrpcUpstream(chain, client, objectMapper, ethereumTargets)
upstream.setLag(0)
when: when:
upstream.connect() upstream.connect()
finished.get() finished.get()
@@ -167,6 +169,7 @@ class GrpcUpstreamSpec extends Specification {
} }
}) })
def upstream = new GrpcUpstream(chain, client, objectMapper, ethereumTargets) def upstream = new GrpcUpstream(chain, client, objectMapper, ethereumTargets)
upstream.setLag(0)
when: when:
upstream.connect() upstream.connect()
finished.get() finished.get()

View File

@@ -0,0 +1,116 @@
package io.emeraldpay.dshackle.upstream
import io.infinitape.etherjar.rpc.json.BlockJson
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.test.StepVerifier
import reactor.util.function.Tuples
import spock.lang.Specification
import java.time.Duration
class HeadLagObserverSpec extends Specification {
def "Updates lag distance"() {
setup:
EthereumHead master = Mock()
EthereumHead head1 = Mock()
EthereumHead head2 = Mock()
Upstream up1 = Mock {
_ * getHead() >> head1
}
Upstream up2 = Mock {
_ * getHead() >> head2
}
def blocks = [100, 101, 102].collect { i ->
return new BlockJson().with {
it.number = i
it.totalDifficulty = 2000 + i
return it
}
}
def masterBus = TopicProcessor.create()
1 * master.getFlux() >> Flux.from(masterBus)
1 * head1.getHead() >> Mono.just(blocks[1])
1 * head1.getFlux() >> Flux.just(blocks[2])
.delaySubscription(Duration.ofSeconds(1))
1 * head2.getHead() >> Mono.just(blocks[0])
1 * head2.getFlux() >> Flux.just(blocks[1])
.delaySubscription(Duration.ofMillis(100))
1 * up1.setLag(0)
1 * up2.setLag(1)
1 * up2.setLag(0)
HeadLagObserver observer = new HeadLagObserver(master, [up1, up2])
when:
def act = observer.subscription().take(Duration.ofMillis(1200))
then:
StepVerifier.create(act)
.then { masterBus.onNext(blocks[1]) }
.expectNextCount(3)
.verifyComplete()
}
def "Probes until there is no difference"() {
setup:
EthereumHead master = Mock()
HeadLagObserver observer = new HeadLagObserver(master, [])
Upstream up = Mock()
def blocks = [100, 101, 102].collect { i ->
return new BlockJson().with {
it.number = i
it.totalDifficulty = 2000 + i
return it
}
}
def upblocks = Flux.fromIterable(blocks)
when:
def act = observer.mapLagging(blocks[2], up, upblocks)
then:
StepVerifier.create(act)
.expectNext(Tuples.of(2L, up))
.expectNext(Tuples.of(1L, up))
.expectNext(Tuples.of(0L, up))
.verifyComplete()
}
def "Correct distance"() {
setup:
EthereumHead master = Mock()
HeadLagObserver observer = new HeadLagObserver(master, [])
expect:
def top = new BlockJson().with {
it.number = topHeight
it.totalDifficulty = topDiff
return it
}
def curr = new BlockJson().with {
it.number = currHeight
it.totalDifficulty = currDiff
return it
}
delta as Long == observer.extractDistance(top, curr)
where:
topHeight | topDiff | currHeight | currDiff | delta
100 | 1000 | 100 | 1000 | 0
101 | 1010 | 100 | 1000 | 1
102 | 1020 | 100 | 1000 | 2
103 | 1030 | 100 | 1000 | 3
150 | 1500 | 100 | 1000 | 50
100 | 1000 | 101 | 1010 | 0
100 | 1000 | 102 | 1020 | 0
100 | 1000 | 100 | 1010 | 6
100 | 1100 | 100 | 1000 | 6
}
}

View File

@@ -0,0 +1,47 @@
package io.emeraldpay.dshackle.upstream
import spock.lang.Specification
class NotLaggingQuorumSpec extends Specification {
def "Resolves if no lag"() {
setup:
def up = Mock(Upstream)
def value = "foo".getBytes()
def quorum = new NotLaggingQuorum(1)
when:
quorum.record(value, up)
then:
1 * up.getLag() >> 0
quorum.isResolved()
quorum.result == value
}
def "Resolves if ok lag"() {
setup:
def up = Mock(Upstream)
def value = "foo".getBytes()
def quorum = new NotLaggingQuorum(1)
when:
quorum.record(value, up)
then:
1 * up.getLag() >> 1
quorum.isResolved()
quorum.result == value
}
def "Ignores if lags"() {
setup:
def up = Mock(Upstream)
def value = "foo".getBytes()
def quorum = new NotLaggingQuorum(1)
when:
quorum.record(value, up)
then:
1 * up.getLag() >> 2
!quorum.isResolved()
}
}