Merge pull request #47 from p2p-org/forward-selectors

Added forwarding of selectors to grpc upstreams
This commit is contained in:
a10zn8
2022-11-30 14:08:10 +04:00
committed by GitHub
11 changed files with 193 additions and 31 deletions

View File

@@ -242,6 +242,8 @@ open class NativeCall(
val requestDecorator = getRequestDecorator(requestItem.method) val requestDecorator = getRequestDecorator(requestItem.method)
val resultDecorator = getResultDecorator(requestItem.method) val resultDecorator = getResultDecorator(requestItem.method)
val selector = request.takeIf { it.hasSelector() }?.let { Selectors.keepForwarded(it.selector) }
ValidCallContext( ValidCallContext(
requestItem.id, requestItem.id,
nonce, nonce,
@@ -250,7 +252,8 @@ open class NativeCall(
callQuorum, callQuorum,
RawCallDetails(method, params), RawCallDetails(method, params),
requestDecorator, requestDecorator,
resultDecorator resultDecorator,
selector
) )
} }
} }
@@ -267,7 +270,7 @@ open class NativeCall(
fun fetch(ctx: ValidCallContext<ParsedCallDetails>): Mono<CallResult> { fun fetch(ctx: ValidCallContext<ParsedCallDetails>): Mono<CallResult> {
return ctx.upstream.getRoutedApi(ctx.matcher) return ctx.upstream.getRoutedApi(ctx.matcher)
.flatMap { api -> .flatMap { api ->
api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce)) api.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce, ctx.forwardedSelector))
.flatMap(JsonRpcResponse::requireResult) .flatMap(JsonRpcResponse::requireResult)
.map { .map {
if (ctx.nonce != null) { if (ctx.nonce != null) {
@@ -300,7 +303,7 @@ open class NativeCall(
AtomicInteger(-1) AtomicInteger(-1)
} }
return reader return reader
.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce)) .read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce, ctx.forwardedSelector))
.map { .map {
val bytes = ctx.resultDecorator.processResult(it) val bytes = ctx.resultDecorator.processResult(it)
CallResult(ctx.id, ctx.nonce, bytes, null, it.signature) CallResult(ctx.id, ctx.nonce, bytes, null, it.signature)
@@ -418,7 +421,8 @@ open class NativeCall(
val callQuorum: CallQuorum, val callQuorum: CallQuorum,
val payload: T, val payload: T,
val requestDecorator: RequestDecorator, val requestDecorator: RequestDecorator,
val resultDecorator: ResultDecorator val resultDecorator: ResultDecorator,
val forwardedSelector: BlockchainOuterClass.Selector?
) : CallContext { ) : CallContext {
constructor( constructor(
@@ -428,7 +432,7 @@ open class NativeCall(
matcher: Selector.Matcher, matcher: Selector.Matcher,
callQuorum: CallQuorum, callQuorum: CallQuorum,
payload: T payload: T
) : this(id, nonce, upstream, matcher, callQuorum, payload, NoneRequestDecorator(), NoneResultDecorator()) ) : this(id, nonce, upstream, matcher, callQuorum, payload, NoneRequestDecorator(), NoneResultDecorator(), null)
override fun isValid(): Boolean { override fun isValid(): Boolean {
return true return true
@@ -443,7 +447,7 @@ open class NativeCall(
} }
fun <X> withPayload(payload: X): ValidCallContext<X> { fun <X> withPayload(payload: X): ValidCallContext<X> {
return ValidCallContext(id, nonce, upstream, matcher, callQuorum, payload, requestDecorator, resultDecorator) return ValidCallContext(id, nonce, upstream, matcher, callQuorum, payload, requestDecorator, resultDecorator, forwardedSelector)
} }
fun getApis(): ApiSource { fun getApis(): ApiSource {

View File

@@ -0,0 +1,43 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass.*
object Selectors {
private fun processMultiple(original: List<Selector>, creator: (List<Selector>) -> Selector.Builder): Selector? {
val newSelectors = original.mapNotNull { keepForwarded(it) }
return if (newSelectors.size > 1) {
return creator(newSelectors).setShouldBeForwarded(true).build()
} else if (newSelectors.size == 1) {
newSelectors.first()
} else {
null
}
}
fun keepForwarded(selector: Selector): Selector? {
if (!selector.shouldBeForwarded) return null
if (selector.hasOrSelector()) {
return processMultiple(selector.orSelector.selectorsList) {
Selector.newBuilder()
.setOrSelector(OrSelector.newBuilder().addAllSelectors(it))
}
} else if (selector.hasAndSelector()) {
return processMultiple(selector.andSelector.selectorsList) {
Selector.newBuilder()
.setAndSelector(AndSelector.newBuilder().addAllSelectors(it))
}
} else if (selector.hasNotSelector()) {
val inner = keepForwarded(selector.notSelector.selector)
return inner?.let {
Selector.newBuilder()
.setNotSelector(NotSelector.newBuilder().setSelector(it).build())
.setShouldBeForwarded(true)
.build()
}
} else {
return selector
}
}
}

View File

@@ -26,7 +26,6 @@ import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Capability import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream import io.emeraldpay.dshackle.upstream.bitcoin.BitcoinUpstream
@@ -67,7 +66,7 @@ class BitcoinGrpcUpstream(
} }
private val extractBlock = ExtractBlock() private val extractBlock = ExtractBlock()
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty) private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.getReader()
private val blockConverter: Function<BlockchainOuterClass.ChainHead, BlockContainer> = Function { value -> private val blockConverter: Function<BlockchainOuterClass.ChainHead, BlockContainer> = Function { value ->
val block = BlockContainer( val block = BlockContainer(
value.height, value.height,

View File

@@ -28,7 +28,6 @@ import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.Capability import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
@@ -106,7 +105,7 @@ open class EthereumGrpcUpstream(
private val grpcHead = GrpcHead(chain, this, remote, blockConverter, reloadBlock, MostWorkForkChoice()) private val grpcHead = GrpcHead(chain, this, remote, blockConverter, reloadBlock, MostWorkForkChoice())
private var capabilities: Set<Capability> = emptySet() private var capabilities: Set<Capability> = emptySet()
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty) private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.getReader()
var timeout = Defaults.timeout var timeout = Defaults.timeout
override fun getBlockchainApi(): ReactorBlockchainGrpc.ReactorBlockchainStub { override fun getBlockchainApi(): ReactorBlockchainGrpc.ReactorBlockchainStub {

View File

@@ -28,7 +28,6 @@ import io.emeraldpay.dshackle.startup.QuorumForLabels
import io.emeraldpay.dshackle.upstream.Capability import io.emeraldpay.dshackle.upstream.Capability
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.Lifecycle
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.CallMethods
@@ -106,7 +105,7 @@ open class EthereumPosGrpcUpstream(
private val grpcHead = GrpcHead(chain, this, remote, blockConverter, reloadBlock, NoChoiceWithPriorityForkChoice(nodeRating)) private val grpcHead = GrpcHead(chain, this, remote, blockConverter, reloadBlock, NoChoiceWithPriorityForkChoice(nodeRating))
private var capabilities: Set<Capability> = emptySet() private var capabilities: Set<Capability> = emptySet()
private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.forSelector(Selector.empty) private val defaultReader: Reader<JsonRpcRequest, JsonRpcResponse> = client.getReader()
var timeout = Defaults.timeout var timeout = Defaults.timeout
override fun start() { override fun start() {

View File

@@ -22,7 +22,6 @@ import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import io.emeraldpay.etherjar.rpc.RpcException import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.etherjar.rpc.RpcResponseError import io.emeraldpay.etherjar.rpc.RpcResponseError
@@ -40,14 +39,13 @@ class JsonRpcGrpcClient(
private val log = LoggerFactory.getLogger(JsonRpcGrpcClient::class.java) private val log = LoggerFactory.getLogger(JsonRpcGrpcClient::class.java)
} }
fun forSelector(matcher: Selector.Matcher): Reader<JsonRpcRequest, JsonRpcResponse> { fun getReader(): Reader<JsonRpcRequest, JsonRpcResponse> {
return Executor(stub, chain, matcher, metrics) return Executor(stub, chain, metrics)
} }
class Executor( class Executor(
private val stub: ReactorBlockchainGrpc.ReactorBlockchainStub, private val stub: ReactorBlockchainGrpc.ReactorBlockchainStub,
private val chain: Chain, private val chain: Chain,
private val matcher: Selector.Matcher,
private val metrics: RpcMetrics private val metrics: RpcMetrics
) : Reader<JsonRpcRequest, JsonRpcResponse> { ) : Reader<JsonRpcRequest, JsonRpcResponse> {
@@ -56,11 +54,7 @@ class JsonRpcGrpcClient(
val req = BlockchainOuterClass.NativeCallRequest.newBuilder() val req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChainValue(chain.id) .setChainValue(chain.id)
if (matcher != Selector.empty) { key.selector?.let { req.selector = it }
Selector.extractLabels(matcher)?.asProto().let {
req.setSelector(it)
}
}
val reqItem = BlockchainOuterClass.NativeCallItem.newBuilder() val reqItem = BlockchainOuterClass.NativeCallItem.newBuilder()
.setId(1) .setId(1)

View File

@@ -19,16 +19,23 @@ import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
data class JsonRpcRequest( data class JsonRpcRequest(
val method: String, val method: String,
val params: List<Any?>, val params: List<Any?>,
val id: Int, val id: Int,
val nonce: Long? val nonce: Long?,
val selector: BlockchainOuterClass.Selector?
) { ) {
@JvmOverloads constructor(method: String, params: List<Any?>, nonce: Long? = null) : this(method, params, 1, nonce) @JvmOverloads constructor(
method: String,
params: List<Any?>,
nonce: Long? = null,
selectors: BlockchainOuterClass.Selector? = null
) : this(method, params, 1, nonce, selectors)
fun toJson(): ByteArray { fun toJson(): ByteArray {
val json = mapOf( val json = mapOf(
@@ -63,7 +70,7 @@ data class JsonRpcRequest(
throw IllegalStateException("Unsupported param type: ${it.asToken()}") throw IllegalStateException("Unsupported param type: ${it.asToken()}")
} }
} }
return JsonRpcRequest(method, params, id, null) return JsonRpcRequest(method, params, id, null, null)
} }
} }
} }

View File

@@ -543,7 +543,7 @@ class NativeCallSpec extends Specification {
def nativeCall = nativeCall() def nativeCall = nativeCall()
def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(), def ctx = new NativeCall.ValidCallContext(1, null, Stub(Multistream), Selector.empty, new AlwaysQuorum(),
new NativeCall.RawCallDetails("eth_getFilterUpdates", '["0xabcd"]'), new NativeCall.RawCallDetails("eth_getFilterUpdates", '["0xabcd"]'),
new NativeCall.WithFilterIdDecorator(), new NativeCall.NoneResultDecorator()) new NativeCall.WithFilterIdDecorator(), new NativeCall.NoneResultDecorator(), null)
when: when:
def act = nativeCall.parseParams(ctx) def act = nativeCall.parseParams(ctx)
then: then:
@@ -564,7 +564,7 @@ class NativeCallSpec extends Specification {
} }
def call = new NativeCall.ValidCallContext(1, 10, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum, def call = new NativeCall.ValidCallContext(1, 10, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_getFilterChanges", []), new NativeCall.ParsedCallDetails("eth_getFilterChanges", []),
new NativeCall.WithFilterIdDecorator(), new NativeCall.CreateFilterDecorator()) new NativeCall.WithFilterIdDecorator(), new NativeCall.CreateFilterDecorator(), null)
when: when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1)) def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1))
@@ -586,7 +586,7 @@ class NativeCallSpec extends Specification {
} }
def call = new NativeCall.ValidCallContext(1, 10, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum, def call = new NativeCall.ValidCallContext(1, 10, TestingCommons.multistream(TestingCommons.api()), Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_getFilterChanges", []), new NativeCall.ParsedCallDetails("eth_getFilterChanges", []),
new NativeCall.WithFilterIdDecorator(), new NativeCall.CreateFilterDecorator()) new NativeCall.WithFilterIdDecorator(), new NativeCall.CreateFilterDecorator(), null)
when: when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1)) def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1))

View File

@@ -84,7 +84,7 @@ class WsConnectionSpec extends Specification {
when: when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe() Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null)) def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null))
then: then:
StepVerifier.create(act) StepVerifier.create(act)
@@ -106,7 +106,7 @@ class WsConnectionSpec extends Specification {
when: when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe() Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null)) def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null))
then: then:
StepVerifier.create(act) StepVerifier.create(act)
@@ -130,7 +130,7 @@ class WsConnectionSpec extends Specification {
when: when:
Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe() Flux.from(ws.handle(wsApiMock.inbound, wsApiMock.outbound)).subscribe()
def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null)) def act = ws.call(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null))
then: then:
StepVerifier.create(act) StepVerifier.create(act)

View File

@@ -0,0 +1,117 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass.*
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import java.util.stream.Stream
internal class SelectorsTest {
companion object {
@JvmStatic
fun data(): Stream<Arguments> {
val leafLabelSelector = LabelSelector.newBuilder().build()
return Stream.of(
Arguments.of(
Selector.newBuilder().setLabelSelector(leafLabelSelector).build(),
null
),
Arguments.of(
Selector.newBuilder().setShouldBeForwarded(true)
.setLabelSelector(leafLabelSelector).build(),
Selector.newBuilder().setShouldBeForwarded(true)
.setLabelSelector(leafLabelSelector).build()
),
Arguments.of(
Selector.newBuilder().setOrSelector(OrSelector.newBuilder()).build(),
null
),
Arguments.of(
Selector.newBuilder().setShouldBeForwarded(true)
.setOrSelector(OrSelector.newBuilder()).build(),
null
),
Arguments.of(
Selector.newBuilder().setShouldBeForwarded(true)
.setOrSelector(
OrSelector.newBuilder().addSelectors(
Selector.newBuilder().setLabelSelector(leafLabelSelector)
)
).build(),
null
),
Arguments.of(
Selector.newBuilder().setShouldBeForwarded(true)
.setOrSelector(
OrSelector.newBuilder().addSelectors(
Selector.newBuilder().setShouldBeForwarded(true)
.setLabelSelector(leafLabelSelector)
)
).build(),
Selector.newBuilder().setShouldBeForwarded(true)
.setLabelSelector(leafLabelSelector).build()
),
Arguments.of(
Selector.newBuilder().setShouldBeForwarded(true)
.setOrSelector(
OrSelector.newBuilder()
.addSelectors(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
.addSelectors(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
.addSelectors(Selector.newBuilder().setLabelSelector(leafLabelSelector))
).build(),
Selector.newBuilder().setShouldBeForwarded(true)
.setOrSelector(
OrSelector.newBuilder()
.addSelectors(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
.addSelectors(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
).build()
),
Arguments.of(
Selector.newBuilder().setShouldBeForwarded(true)
.setAndSelector(
AndSelector.newBuilder()
.addSelectors(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
.addSelectors(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
.addSelectors(Selector.newBuilder().setLabelSelector(leafLabelSelector))
).build(),
Selector.newBuilder().setShouldBeForwarded(true)
.setAndSelector(
AndSelector.newBuilder()
.addSelectors(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
.addSelectors(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
).build()
),
Arguments.of(
Selector.newBuilder().setShouldBeForwarded(true)
.setNotSelector(
NotSelector.newBuilder()
.setSelector(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
).build(),
Selector.newBuilder().setShouldBeForwarded(true)
.setNotSelector(
NotSelector.newBuilder()
.setSelector(Selector.newBuilder().setShouldBeForwarded(true).setLabelSelector(leafLabelSelector))
).build()
),
Arguments.of(
Selector.newBuilder().setShouldBeForwarded(true)
.setNotSelector(
NotSelector.newBuilder()
.setSelector(Selector.newBuilder().setLabelSelector(leafLabelSelector))
).build(),
null
)
)
}
}
@ParameterizedTest
@MethodSource("data")
fun testKeepForwarded(input: Selector, expected: Selector?) {
assertEquals(expected, Selectors.keepForwarded(input))
}
}