problem: wastes 200ms even for a single call because takes next api before validation
solution: manage apis flow right after value recorded
This commit is contained in:
@@ -32,9 +32,10 @@ open class AlwaysQuorum: CallQuorum {
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(response: ByteArray, upstream: Upstream) {
|
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||||
result = response
|
result = response
|
||||||
resolved = true
|
resolved = true
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResult(): ByteArray? {
|
override fun getResult(): ByteArray? {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ interface CallQuorum {
|
|||||||
fun init(head: Head<BlockJson<TransactionId>>)
|
fun init(head: Head<BlockJson<TransactionId>>)
|
||||||
|
|
||||||
fun isResolved(): Boolean
|
fun isResolved(): Boolean
|
||||||
fun record(response: ByteArray, upstream: Upstream)
|
fun record(response: ByteArray, upstream: Upstream): Boolean
|
||||||
fun getResult(): ByteArray?
|
fun getResult(): ByteArray?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -32,11 +32,13 @@ class NotLaggingQuorum(val maxLag: Long = 0): CallQuorum {
|
|||||||
return result.get() != null
|
return result.get() != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(response: ByteArray, upstream: Upstream) {
|
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||||
val lagging = upstream.getLag() > maxLag
|
val lagging = upstream.getLag() > maxLag
|
||||||
if (!lagging) {
|
if (!lagging) {
|
||||||
result.set(response)
|
result.set(response)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResult(): ByteArray {
|
override fun getResult(): ByteArray {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ abstract class ValueAwareQuorum<T>(
|
|||||||
return jacksonRpcConverter.fromJson(response.inputStream(), clazz)
|
return jacksonRpcConverter.fromJson(response.inputStream(), clazz)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun record(response: ByteArray, upstream: Upstream) {
|
override fun record(response: ByteArray, upstream: Upstream): Boolean {
|
||||||
try {
|
try {
|
||||||
val value = extractValue(response, clazz)
|
val value = extractValue(response, clazz)
|
||||||
recordValue(response, value, upstream)
|
recordValue(response, value, upstream)
|
||||||
@@ -40,6 +40,7 @@ abstract class ValueAwareQuorum<T>(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
recordError(response, e.message, upstream)
|
recordError(response, e.message, upstream)
|
||||||
}
|
}
|
||||||
|
return isResolved();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream)
|
abstract fun recordValue(response: ByteArray, responseValue: T?, upstream: Upstream)
|
||||||
|
|||||||
@@ -27,13 +27,11 @@ import org.apache.commons.lang3.StringUtils
|
|||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
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.publisher.Flux
|
import reactor.core.publisher.*
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import reactor.core.publisher.toFlux
|
|
||||||
import reactor.core.publisher.toMono
|
|
||||||
import reactor.util.function.Tuples
|
import reactor.util.function.Tuples
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -124,17 +122,26 @@ class NativeCall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun executeOnRemote(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
fun executeOnRemote(ctx: CallContext<ParsedCallDetails>): Mono<CallContext<ByteArray>> {
|
||||||
val p: Predicate<Any> = CallQuorum.untilResolved(ctx.callQuorum)
|
|
||||||
val all = ctx.getApis().toFlux().share()
|
val all = ctx.getApis().toFlux().share()
|
||||||
//execute on the first API immediately, and then make a delay between each call to not dos upstreams
|
//execute on the first API immediately, and then make a delay between each call to not overload upstreams
|
||||||
val immediate = Flux.from(all).take(1)
|
val immediate = Flux.from(all).take(1)
|
||||||
val retries = Flux.from(all).delayElements(Duration.ofMillis(200))
|
val repeatControl = EmitterProcessor.create<Boolean>()//TopicProcessor.create<Boolean>()
|
||||||
|
val retries = Flux.from(all).skip(1)
|
||||||
|
.zipWith(repeatControl).zipWith(Flux.interval(Duration.ofMillis(200)))
|
||||||
|
.map { it.t1.t1 }
|
||||||
|
|
||||||
return Flux.concat(immediate, retries)
|
return Flux.concat(immediate, retries)
|
||||||
.takeWhile(p)
|
|
||||||
.flatMap { api ->
|
.flatMap { api ->
|
||||||
api.execute(ctx.id, ctx.payload.method, ctx.payload.params).map { Tuples.of(it, api.upstream!!) }
|
api.execute(ctx.id, ctx.payload.method, ctx.payload.params).map { Tuples.of(it, api.upstream!!) }
|
||||||
}
|
}
|
||||||
.reduce(ctx.callQuorum, CallQuorum.asReducer())
|
.reduce(ctx.callQuorum, {res, a ->
|
||||||
|
if (res.record(a.t1, a.t2)) {
|
||||||
|
repeatControl.onComplete()
|
||||||
|
} else {
|
||||||
|
repeatControl.onNext(true)
|
||||||
|
}
|
||||||
|
res
|
||||||
|
})
|
||||||
.filter { it.isResolved() }
|
.filter { it.isResolved() }
|
||||||
.map {
|
.map {
|
||||||
val result = it.getResult()
|
val result = it.getResult()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class FilteringApiIterator(
|
|||||||
private val upstreams: List<Upstream>,
|
private val upstreams: List<Upstream>,
|
||||||
private var pos: Int,
|
private var pos: Int,
|
||||||
private val matcher: Selector.Matcher,
|
private val matcher: Selector.Matcher,
|
||||||
private val repeatLimit: Int = 3
|
private val repeatLimit: Int = 5
|
||||||
): Iterator<DirectEthereumApi> {
|
): Iterator<DirectEthereumApi> {
|
||||||
|
|
||||||
private var nextUpstream: Upstream? = null
|
private var nextUpstream: Upstream? = null
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class QuorumBasedMethods(
|
|||||||
override fun getQuorumFor(method: String): CallQuorum {
|
override fun getQuorumFor(method: String): CallQuorum {
|
||||||
return when {
|
return when {
|
||||||
hardcodedMethods.contains(method) -> AlwaysQuorum()
|
hardcodedMethods.contains(method) -> AlwaysQuorum()
|
||||||
|
firstValueMethods.contains(method) -> AlwaysQuorum()
|
||||||
anyResponseMethods.contains(method) -> NotLaggingQuorum(6)
|
anyResponseMethods.contains(method) -> NotLaggingQuorum(6)
|
||||||
headVerifiedMethods.contains(method) -> NotLaggingQuorum(1)
|
headVerifiedMethods.contains(method) -> NotLaggingQuorum(1)
|
||||||
specialMethods.contains(method) -> {
|
specialMethods.contains(method) -> {
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ class NativeCallSpec extends Specification {
|
|||||||
def act = objectMapper.readValue(resp.payload, Map)
|
def act = objectMapper.readValue(resp.payload, Map)
|
||||||
then:
|
then:
|
||||||
act == [jsonrpc:"2.0", id:1, result: "foo"]
|
act == [jsonrpc:"2.0", id:1, result: "foo"]
|
||||||
(2..3) * quorum.isResolved() // 2 times during api call (before and after) + 1 time in a filter after
|
|
||||||
1 * quorum.record(_, _)
|
1 * quorum.record(_, _)
|
||||||
1 * quorum.getResult()
|
1 * quorum.getResult()
|
||||||
}
|
}
|
||||||
@@ -86,11 +85,61 @@ class NativeCallSpec extends Specification {
|
|||||||
def act = objectMapper.readValue(resp.payload, Map)
|
def act = objectMapper.readValue(resp.payload, Map)
|
||||||
then:
|
then:
|
||||||
act == [jsonrpc:"2.0", id:1, result: "bar"]
|
act == [jsonrpc:"2.0", id:1, result: "bar"]
|
||||||
(3..4) * quorum.isResolved()
|
|
||||||
2 * quorum.record(_, _)
|
2 * quorum.record(_, _)
|
||||||
1 * quorum.getResult()
|
1 * quorum.getResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def "Have pause between repeats"() {
|
||||||
|
setup:
|
||||||
|
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
||||||
|
|
||||||
|
def upstreams = Stub(Upstreams)
|
||||||
|
RpcClient rpcClient = Stub(RpcClient)
|
||||||
|
def apiMock = TestingCommons.api(rpcClient)
|
||||||
|
apiMock.upstream = Stub(Upstream)
|
||||||
|
|
||||||
|
apiMock.answerOnce("eth_test", [], null)
|
||||||
|
apiMock.answerOnce("eth_test", [], "bar")
|
||||||
|
|
||||||
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
|
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
||||||
|
Selector.empty, quorum,
|
||||||
|
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||||
|
|
||||||
|
|
||||||
|
when:
|
||||||
|
def t1 = System.currentTimeMillis()
|
||||||
|
nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
||||||
|
def delta = System.currentTimeMillis() - t1
|
||||||
|
then:
|
||||||
|
delta >= 200
|
||||||
|
}
|
||||||
|
|
||||||
|
def "One call has no pause"() {
|
||||||
|
setup:
|
||||||
|
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
||||||
|
|
||||||
|
def upstreams = Stub(Upstreams)
|
||||||
|
RpcClient rpcClient = Stub(RpcClient)
|
||||||
|
def apiMock = TestingCommons.api(rpcClient)
|
||||||
|
apiMock.upstream = Stub(Upstream)
|
||||||
|
|
||||||
|
apiMock.answerOnce("eth_test", [], "bar")
|
||||||
|
|
||||||
|
def nativeCall = new NativeCall(upstreams, TestingCommons.objectMapper())
|
||||||
|
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock),
|
||||||
|
Selector.empty, quorum,
|
||||||
|
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||||
|
|
||||||
|
|
||||||
|
when:
|
||||||
|
def t1 = System.currentTimeMillis()
|
||||||
|
nativeCall.executeOnRemote(call).block(Duration.ofSeconds(2))
|
||||||
|
def delta = System.currentTimeMillis() - t1
|
||||||
|
then:
|
||||||
|
delta < 50
|
||||||
|
}
|
||||||
|
|
||||||
def "Returns error if no quorum"() {
|
def "Returns error if no quorum"() {
|
||||||
setup:
|
setup:
|
||||||
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
def quorum = Spy(new NonEmptyQuorum(TestingCommons.rpcConverter(), 3))
|
||||||
@@ -107,7 +156,6 @@ class NativeCallSpec extends Specification {
|
|||||||
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), Selector.empty, quorum,
|
def call = new NativeCall.CallContext(1, TestingCommons.aggregatedUpstream(apiMock), Selector.empty, quorum,
|
||||||
new NativeCall.ParsedCallDetails("eth_test", []))
|
new NativeCall.ParsedCallDetails("eth_test", []))
|
||||||
|
|
||||||
(4..5) * quorum.isResolved()
|
|
||||||
3 * quorum.record(_, _)
|
3 * quorum.record(_, _)
|
||||||
1 * quorum.getResult()
|
1 * quorum.getResult()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user