Stream responses from node (#361)

This commit is contained in:
KirillPamPam
2023-12-14 12:09:25 +04:00
committed by GitHub
parent a9a3e64c32
commit 6e8f600cdc
39 changed files with 768 additions and 450 deletions

View File

@@ -19,12 +19,13 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
open class AlwaysQuorum : CallQuorum { open class AlwaysQuorum : CallQuorum {
private var resolved = false private var resolved = false
private var result: ByteArray? = null private var result: JsonRpcResponse? = null
private var rpcError: JsonRpcError? = null private var rpcError: JsonRpcError? = null
private var sig: ResponseSigner.Signature? = null private var sig: ResponseSigner.Signature? = null
private val resolvers = ArrayList<Upstream>() private val resolvers = ArrayList<Upstream>()
@@ -42,7 +43,7 @@ open class AlwaysQuorum : CallQuorum {
} }
override fun record( override fun record(
response: ByteArray, response: JsonRpcResponse,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,
): Boolean { ): Boolean {
@@ -63,7 +64,7 @@ open class AlwaysQuorum : CallQuorum {
resolvers.add(upstream) resolvers.add(upstream)
} }
override fun getResult(): ByteArray? { override fun getResponse(): JsonRpcResponse? {
return result return result
} }

View File

@@ -17,11 +17,12 @@
package io.emeraldpay.dshackle.quorum package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
open class BroadcastQuorum() : CallQuorum, ValueAwareQuorum<String>(String::class.java) { open class BroadcastQuorum() : CallQuorum, ValueAwareQuorum<String>(String::class.java) {
private var result: ByteArray? = null private var result: JsonRpcResponse? = null
private var txid: String? = null private var txid: String? = null
private var sig: ResponseSigner.Signature? = null private var sig: ResponseSigner.Signature? = null
@@ -33,7 +34,7 @@ open class BroadcastQuorum() : CallQuorum, ValueAwareQuorum<String>(String::clas
return result == null return result == null
} }
override fun getResult(): ByteArray? { override fun getResponse(): JsonRpcResponse? {
return result return result
} }
@@ -42,7 +43,7 @@ open class BroadcastQuorum() : CallQuorum, ValueAwareQuorum<String>(String::clas
} }
override fun recordValue( override fun recordValue(
response: ByteArray, response: JsonRpcResponse,
responseValue: String?, responseValue: String?,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,
@@ -55,7 +56,6 @@ open class BroadcastQuorum() : CallQuorum, ValueAwareQuorum<String>(String::clas
} }
override fun recordError( override fun recordError(
response: ByteArray?,
errorMessage: String?, errorMessage: String?,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,

View File

@@ -19,6 +19,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
interface CallQuorum { interface CallQuorum {
@@ -26,7 +27,7 @@ interface CallQuorum {
fun isFailed(): Boolean fun isFailed(): Boolean
fun record( fun record(
response: ByteArray, response: JsonRpcResponse,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,
): Boolean ): Boolean
@@ -38,7 +39,7 @@ interface CallQuorum {
) )
fun getSignature(): ResponseSigner.Signature? fun getSignature(): ResponseSigner.Signature?
fun getResult(): ByteArray? fun getResponse(): JsonRpcResponse?
fun getError(): JsonRpcError? fun getError(): JsonRpcError?
fun getResolvedBy(): Collection<Upstream> fun getResolvedBy(): Collection<Upstream>
} }

View File

@@ -1,12 +1,13 @@
package io.emeraldpay.dshackle.quorum package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import io.emeraldpay.etherjar.hex.HexQuantity import io.emeraldpay.etherjar.hex.HexQuantity
class MaximumValueQuorum : CallQuorum, ValueAwareQuorum<String>(String::class.java) { class MaximumValueQuorum : CallQuorum, ValueAwareQuorum<String>(String::class.java) {
private var max: Long? = null private var max: Long? = null
private var result: ByteArray? = null private var result: JsonRpcResponse? = null
private var sig: ResponseSigner.Signature? = null private var sig: ResponseSigner.Signature? = null
override fun isResolved(): Boolean { override fun isResolved(): Boolean {
@@ -17,7 +18,7 @@ class MaximumValueQuorum : CallQuorum, ValueAwareQuorum<String>(String::class.ja
return result == null return result == null
} }
override fun getResult(): ByteArray? { override fun getResponse(): JsonRpcResponse? {
return result return result
} }
@@ -25,7 +26,7 @@ class MaximumValueQuorum : CallQuorum, ValueAwareQuorum<String>(String::class.ja
return sig return sig
} }
override fun recordValue( override fun recordValue(
response: ByteArray, response: JsonRpcResponse,
responseValue: String?, responseValue: String?,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,
@@ -48,7 +49,6 @@ class MaximumValueQuorum : CallQuorum, ValueAwareQuorum<String>(String::class.ja
} }
override fun recordError( override fun recordError(
response: ByteArray?,
errorMessage: String?, errorMessage: String?,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,

View File

@@ -19,6 +19,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@@ -29,7 +30,7 @@ import java.util.concurrent.atomic.AtomicReference
*/ */
class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum { class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
private val result: AtomicReference<ByteArray> = AtomicReference() private val result: AtomicReference<JsonRpcResponse> = AtomicReference()
private val failed = AtomicReference(false) private val failed = AtomicReference(false)
private var rpcError: JsonRpcError? = null private var rpcError: JsonRpcError? = null
private var sig: ResponseSigner.Signature? = null private var sig: ResponseSigner.Signature? = null
@@ -44,7 +45,7 @@ class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
} }
override fun record( override fun record(
response: ByteArray, response: JsonRpcResponse,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,
): Boolean { ): Boolean {
@@ -75,7 +76,7 @@ class NotLaggingQuorum(val maxLag: Long = 0) : CallQuorum {
return sig return sig
} }
override fun getResult(): ByteArray { override fun getResponse(): JsonRpcResponse {
return result.get() return result.get()
} }

View File

@@ -4,11 +4,12 @@ import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
class NotNullQuorum : CallQuorum { class NotNullQuorum : CallQuorum {
private var sig: ResponseSigner.Signature? = null private var sig: ResponseSigner.Signature? = null
private var result: ByteArray? = null private var result: JsonRpcResponse? = null
private var rpcError: JsonRpcError? = null private var rpcError: JsonRpcError? = null
private val resolvers = ArrayList<Upstream>() private val resolvers = ArrayList<Upstream>()
private var allFailed = true private var allFailed = true
@@ -19,14 +20,14 @@ class NotNullQuorum : CallQuorum {
override fun isFailed(): Boolean = rpcError != null override fun isFailed(): Boolean = rpcError != null
override fun record( override fun record(
response: ByteArray, response: JsonRpcResponse,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,
): Boolean { ): Boolean {
allFailed = false allFailed = false
val receivedNull = response.isEmpty() || Global.nullValue.contentEquals(response) val receivedNull = response.getResult().isEmpty() || Global.nullValue.contentEquals(response.getResult())
val upId = upstream.getId() val upId = upstream.getId()
if (seenUpstreams.contains(upId) || !receivedNull) { if (seenUpstreams.contains(upId) || !receivedNull || response.hasStream()) {
sig = signature sig = signature
result = response result = response
resolvers.add(upstream) resolvers.add(upstream)
@@ -42,7 +43,7 @@ class NotNullQuorum : CallQuorum {
if (allFailed) { if (allFailed) {
rpcError = error.error rpcError = error.error
} else { } else {
result = Global.nullValue result = JsonRpcResponse(Global.nullValue, null)
} }
sig = signature sig = signature
} }
@@ -52,7 +53,7 @@ class NotNullQuorum : CallQuorum {
override fun getSignature(): ResponseSigner.Signature? = sig override fun getSignature(): ResponseSigner.Signature? = sig
override fun getResult(): ByteArray? = result override fun getResponse(): JsonRpcResponse? = result
override fun getError(): JsonRpcError? = rpcError override fun getError(): JsonRpcError? = rpcError

View File

@@ -100,7 +100,7 @@ class QuorumRpcReader(
} }
private fun execute(key: JsonRpcRequest, retrySpec: reactor.util.retry.Retry): Function<Flux<Upstream>, Mono<CallQuorum>> { private fun execute(key: JsonRpcRequest, retrySpec: reactor.util.retry.Retry): Function<Flux<Upstream>, Mono<CallQuorum>> {
val quorumReduce = BiFunction<CallQuorum, Tuple3<ByteArray, Optional<ResponseSigner.Signature>, Upstream>, CallQuorum> { res, a -> val quorumReduce = BiFunction<CallQuorum, Tuple3<JsonRpcResponse, Optional<ResponseSigner.Signature>, Upstream>, CallQuorum> { res, a ->
if (res.record(a.t1, a.t2.orElse(null), a.t3)) { if (res.record(a.t1, a.t2.orElse(null), a.t3)) {
log.trace("Quorum is resolved for method ${key.method}") log.trace("Quorum is resolved for method ${key.method}")
apiControl.resolve() apiControl.resolve()
@@ -131,14 +131,15 @@ class QuorumRpcReader(
quorumResult quorumResult
.filter { it.isResolved() } // return nothing if not resolved .filter { it.isResolved() } // return nothing if not resolved
.map { quorum -> .map { quorum ->
val response = quorum.getResponse()!!
// TODO find actual quorum number // TODO find actual quorum number
Result(quorum.getResult()!!, quorum.getSignature(), 1, resolvedBy()) Result(response.getResult(), quorum.getSignature(), 1, resolvedBy(), response.stream)
} }
.switchIfEmpty(defaultResult) .switchIfEmpty(defaultResult)
} }
} }
private fun callApi(api: Upstream, key: JsonRpcRequest): Mono<Tuple3<ByteArray, Optional<ResponseSigner.Signature>, Upstream>> { private fun callApi(api: Upstream, key: JsonRpcRequest): Mono<Tuple3<JsonRpcResponse, Optional<ResponseSigner.Signature>, Upstream>> {
val apiReader = api.getIngressReader() val apiReader = api.getIngressReader()
val spanParams = mapOf( val spanParams = mapOf(
SPAN_REQUEST_API_TYPE to apiReader.javaClass.name, SPAN_REQUEST_API_TYPE to apiReader.javaClass.name,
@@ -156,11 +157,16 @@ class QuorumRpcReader(
.map { Tuples.of(it.t1, it.t2, api) } .map { Tuples.of(it.t1, it.t2, api) }
} }
private fun withSignatureAndUpstream(api: Upstream, key: JsonRpcRequest, response: JsonRpcResponse): Function<Mono<ByteArray>, Mono<Tuple2<ByteArray, Optional<ResponseSigner.Signature>>>> { private fun withSignatureAndUpstream(api: Upstream, key: JsonRpcRequest, response: JsonRpcResponse): Function<Mono<ByteArray>, Mono<Tuple2<JsonRpcResponse, Optional<ResponseSigner.Signature>>>> {
return Function { src -> return Function { src ->
src.map { src.map {
val signature = getSignature(key, response, api.getId()) // TODO: do streaming signature
Tuples.of(it, Optional.ofNullable(signature)) val signature = if (response.hasStream()) {
null
} else {
getSignature(key, response, api.getId())
}
Tuples.of(response, Optional.ofNullable(signature))
} }
} }
} }
@@ -222,7 +228,7 @@ class QuorumRpcReader(
val cause = getCause(method) ?: return Mono.empty() val cause = getCause(method) ?: return Mono.empty()
if (cause.shouldReturnNull) { if (cause.shouldReturnNull) {
Mono.just( Mono.just(
Result(Global.nullValue, null, 1, null), Result(Global.nullValue, null, 1, null, null),
) )
} else { } else {
Mono.error(RpcException(1, "No response for method $method. Cause - ${cause.cause}")) Mono.error(RpcException(1, "No response for method $method. Cause - ${cause.cause}"))

View File

@@ -20,6 +20,7 @@ import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
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 org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -37,18 +38,21 @@ abstract class ValueAwareQuorum<T>(
} }
override fun record( override fun record(
response: ByteArray, response: JsonRpcResponse,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,
): Boolean { ): Boolean {
if (response.hasStream()) {
throw IllegalStateException("ValueAwareQuorum works with value, response must not have stream")
}
try { try {
val value = extractValue(response, clazz) val value = extractValue(response.getResult(), clazz)
recordValue(response, value, signature, upstream) recordValue(response, value, signature, upstream)
resolvers.add(upstream) resolvers.add(upstream)
} catch (e: RpcException) { } catch (e: RpcException) {
recordError(response, e.rpcMessage, signature, upstream) recordError(e.rpcMessage, signature, upstream)
} catch (e: Exception) { } catch (e: Exception) {
recordError(response, e.message, signature, upstream) recordError(e.message, signature, upstream)
} }
return isResolved() return isResolved()
} }
@@ -59,18 +63,17 @@ abstract class ValueAwareQuorum<T>(
upstream: Upstream, upstream: Upstream,
) { ) {
this.rpcError = error.error this.rpcError = error.error
recordError(null, error.error.message, signature, upstream) recordError(error.error.message, signature, upstream)
} }
abstract fun recordValue( abstract fun recordValue(
response: ByteArray, response: JsonRpcResponse,
responseValue: T?, responseValue: T?,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,
) )
abstract fun recordError( abstract fun recordError(
response: ByteArray?,
errorMessage: String?, errorMessage: String?,
signature: ResponseSigner.Signature?, signature: ResponseSigner.Signature?,
upstream: Upstream, upstream: Upstream,

View File

@@ -42,7 +42,7 @@ class BroadcastReader(
}.map { }.map {
if (it.jsonRpcResponse.hasResult()) { if (it.jsonRpcResponse.hasResult()) {
val sig = getSignature(key, it.jsonRpcResponse, it.upstream.getId()) val sig = getSignature(key, it.jsonRpcResponse, it.upstream.getId())
quorum.record(it.jsonRpcResponse.getResult(), sig, it.upstream) quorum.record(it.jsonRpcResponse, sig, it.upstream)
} else { } else {
val err = JsonRpcException(JsonRpcResponse.NumberId(key.id), it.jsonRpcResponse.error!!, it.upstream.getId()) val err = JsonRpcException(JsonRpcResponse.NumberId(key.id), it.jsonRpcResponse.error!!, it.upstream.getId())
quorum.record(err, null, it.upstream) quorum.record(err, null, it.upstream)
@@ -55,10 +55,11 @@ class BroadcastReader(
.flatMap { .flatMap {
if (quorum.isResolved()) { if (quorum.isResolved()) {
val res = Result( val res = Result(
quorum.getResult()!!, quorum.getResponse()!!.getResult(),
quorum.getSignature(), quorum.getSignature(),
upstreams.size, upstreams.size,
quorum.getResolvedBy().first(), quorum.getResolvedBy().first(),
null,
) )
Mono.just(res) Mono.just(res)
} else { } else {

View File

@@ -10,9 +10,11 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.rpcclient.stream.Chunk
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 org.springframework.cloud.sleuth.Tracer import org.springframework.cloud.sleuth.Tracer
import reactor.core.publisher.Flux
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
abstract class RpcReader( abstract class RpcReader(
@@ -47,6 +49,7 @@ abstract class RpcReader(
val signature: ResponseSigner.Signature?, val signature: ResponseSigner.Signature?,
val quorum: Int, val quorum: Int,
val resolvedBy: Upstream?, val resolvedBy: Upstream?,
val stream: Flux<Chunk>?,
) )
} }

View File

@@ -41,7 +41,7 @@ import java.util.concurrent.TimeUnit
@Service @Service
@DependsOn("monitoringSetup") @DependsOn("monitoringSetup")
class BlockchainRpc( class BlockchainRpc(
private val nativeCallStream: NativeCallStream, private val nativeCall: NativeCall,
private val nativeSubscribe: NativeSubscribe, private val nativeSubscribe: NativeSubscribe,
private val streamHead: StreamHead, private val streamHead: StreamHead,
private val describe: Describe, private val describe: Describe,
@@ -73,7 +73,7 @@ class BlockchainRpc(
var startTime = 0L var startTime = 0L
var metrics: RequestMetrics? = null var metrics: RequestMetrics? = null
val idsMap = mutableMapOf<Int, String>() val idsMap = mutableMapOf<Int, String>()
return nativeCallStream.nativeCall( return nativeCall.nativeCall(
request request
.subscribeOn(scheduler) .subscribeOn(scheduler)
.doOnNext { req -> .doOnNext { req ->

View File

@@ -44,11 +44,13 @@ import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.stream.Chunk
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
import io.micrometer.core.instrument.Metrics import io.micrometer.core.instrument.Metrics
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.reactivestreams.Publisher
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.cloud.sleuth.Span import org.springframework.cloud.sleuth.Span
import org.springframework.cloud.sleuth.Tracer import org.springframework.cloud.sleuth.Tracer
@@ -76,7 +78,8 @@ open class NativeCall(
open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> { open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> {
return nativeCallResult(requestMono) return nativeCallResult(requestMono)
.map(this::buildResponse) .sort { o1, o2 -> o1.id - o2.id }
.flatMapSequential(this::processCallResult)
.onErrorResume(this::processException) .onErrorResume(this::processException)
} }
@@ -103,6 +106,24 @@ open class NativeCall(
} }
} }
private fun processCallResult(callResult: CallResult): Publisher<BlockchainOuterClass.NativeCallReplyItem> {
return if (callResult.stream == null) {
Mono.just(buildResponse(callResult))
} else {
val stream = callResult.stream.map { stream ->
val result = BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setSucceed(true)
.setFinalChunk(stream.finalChunk)
.setChunked(true)
.setId(callResult.id)
result.payload = ByteString.copyFrom(stream.chunkData)
result.build()
}
stream
}
}
private fun completeSpan(callResult: CallResult, requestCount: Int) { private fun completeSpan(callResult: CallResult, requestCount: Int) {
val span = tracer.currentSpan() val span = tracer.currentSpan()
if (callResult.isError()) { if (callResult.isError()) {
@@ -311,6 +332,8 @@ open class NativeCall(
val selector = request.takeIf { it.hasSelector() }?.let { Selectors.keepForwarded(it.selector) } val selector = request.takeIf { it.hasSelector() }?.let { Selectors.keepForwarded(it.selector) }
val isStreamRequest = request.chunkSize != 0
ValidCallContext( ValidCallContext(
requestItem.id, requestItem.id,
nonce, nonce,
@@ -321,6 +344,7 @@ open class NativeCall(
requestDecorator, requestDecorator,
resultDecorator, resultDecorator,
selector, selector,
isStreamRequest,
requestId, requestId,
requestCount, requestCount,
) )
@@ -371,12 +395,16 @@ open class NativeCall(
val counter = reader.attempts() val counter = reader.attempts()
return SpannedReader(reader, tracer, RPC_READER) return SpannedReader(reader, tracer, RPC_READER)
.read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce, ctx.forwardedSelector)) .read(JsonRpcRequest(ctx.payload.method, ctx.payload.params, ctx.nonce, ctx.forwardedSelector, ctx.streamRequest))
.map { .map {
val bytes = ctx.resultDecorator.processResult(it)
validateResult(bytes, "remote", ctx)
val upId = it.resolvedBy?.getId() ?: ctx.upstream.getId() val upId = it.resolvedBy?.getId() ?: ctx.upstream.getId()
CallResult.ok(ctx.id, ctx.nonce, bytes, it.signature, upId, ctx) if (it.stream == null) {
val bytes = ctx.resultDecorator.processResult(it)
validateResult(bytes, "remote", ctx)
CallResult.ok(ctx.id, ctx.nonce, bytes, it.signature, upId, ctx)
} else {
CallResult.ok(ctx.id, ctx.nonce, ByteArray(0), it.signature, upId, ctx, it.stream)
}
} }
.onErrorResume { t -> .onErrorResume { t ->
Mono.just(CallResult.fail(ctx.id, ctx.nonce, t, ctx)) Mono.just(CallResult.fail(ctx.id, ctx.nonce, t, ctx))
@@ -500,6 +528,7 @@ open class NativeCall(
val requestDecorator: RequestDecorator, val requestDecorator: RequestDecorator,
val resultDecorator: ResultDecorator, val resultDecorator: ResultDecorator,
val forwardedSelector: BlockchainOuterClass.Selector?, val forwardedSelector: BlockchainOuterClass.Selector?,
val streamRequest: Boolean,
requestId: String, requestId: String,
requestCount: Int, requestCount: Int,
) : CallContext(requestId, requestCount) { ) : CallContext(requestId, requestCount) {
@@ -515,7 +544,7 @@ open class NativeCall(
requestCount: Int, requestCount: Int,
) : this( ) : this(
id, nonce, upstream, matcher, callQuorum, payload, id, nonce, upstream, matcher, callQuorum, payload,
NoneRequestDecorator(), NoneResultDecorator(), null, requestId, requestCount, NoneRequestDecorator(), NoneResultDecorator(), null, false, requestId, requestCount,
) )
override fun isValid(): Boolean { override fun isValid(): Boolean {
@@ -535,7 +564,7 @@ open class NativeCall(
fun <X> withPayload(payload: X): ValidCallContext<X> { fun <X> withPayload(payload: X): ValidCallContext<X> {
return ValidCallContext( return ValidCallContext(
id, nonce, upstream, matcher, callQuorum, payload, id, nonce, upstream, matcher, callQuorum, payload,
requestDecorator, resultDecorator, forwardedSelector, requestId, requestCount, requestDecorator, resultDecorator, forwardedSelector, streamRequest, requestId, requestCount,
) )
} }
@@ -617,6 +646,7 @@ open class NativeCall(
val signature: ResponseSigner.Signature?, val signature: ResponseSigner.Signature?,
val upstreamId: String?, val upstreamId: String?,
val ctx: ValidCallContext<ParsedCallDetails>?, val ctx: ValidCallContext<ParsedCallDetails>?,
val stream: Flux<Chunk>? = null,
) { ) {
constructor( constructor(
@@ -633,6 +663,10 @@ open class NativeCall(
return CallResult(id, nonce, result, null, signature, upstreamId, ctx) return CallResult(id, nonce, result, null, signature, upstreamId, ctx)
} }
fun ok(id: Int, nonce: Long?, result: ByteArray, signature: ResponseSigner.Signature?, upstreamId: String?, ctx: ValidCallContext<ParsedCallDetails>?, stream: Flux<Chunk>?): CallResult {
return CallResult(id, nonce, result, null, signature, upstreamId, ctx, stream)
}
fun fail(id: Int, nonce: Long?, error: CallError, ctx: ValidCallContext<ParsedCallDetails>?): CallResult { fun fail(id: Int, nonce: Long?, error: CallError, ctx: ValidCallContext<ParsedCallDetails>?): CallResult {
return CallResult(id, nonce, null, error, null, null, ctx) return CallResult(id, nonce, null, error, null, null, ctx)
} }

View File

@@ -1,69 +0,0 @@
package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass.NativeCallReplyItem
import io.emeraldpay.api.proto.BlockchainOuterClass.NativeCallRequest
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import kotlin.math.min
@Service
class NativeCallStream(
private val nativeCall: NativeCall,
) {
fun nativeCall(
requestMono: Mono<NativeCallRequest>,
): Flux<NativeCallReplyItem> {
return requestMono.flatMapMany { req ->
nativeCall.nativeCall(Mono.just(req))
.map { StreamNativeResult(it, req.chunkSize) }
.transform {
if (!req.sorted || req.itemsList.size == 1) {
it
} else {
it.sort { o1, o2 -> o1.response.id - o2.response.id }
}
}
}.concatMap {
val chunkSize = it.chunkSize
val response = it.response
if (chunkSize == 0 || response.payload.size() <= chunkSize || !response.succeed) {
Mono.just(response)
} else {
Flux.fromIterable(chunks(response, chunkSize))
}
}
}
private fun chunks(response: NativeCallReplyItem, chunkSize: Int): List<NativeCallReplyItem> {
val chunks = mutableListOf<ByteString>()
val responseBytes = response.payload
for (i in 0 until responseBytes.size() step+chunkSize) {
chunks.add(responseBytes.substring(i, min(i + chunkSize, responseBytes.size())))
}
return chunks
.mapIndexed { index, bytes ->
NativeCallReplyItem.newBuilder()
.apply {
id = response.id
payload = bytes
succeed = true
upstreamId = response.upstreamId
chunked = true
finalChunk = index == chunks.size - 1
if (this.finalChunk && response.hasSignature()) {
signature = response.signature
}
}.build()
}
}
private data class StreamNativeResult(
val response: NativeCallReplyItem,
val chunkSize: Int,
)
}

View File

@@ -90,7 +90,7 @@ class JsonRpcGrpcClient(
} else { } else {
null null
} }
Mono.just(JsonRpcResponse(bytes, null, JsonRpcResponse.NumberId(0), signature, resp.upstreamId)) Mono.just(JsonRpcResponse(bytes, null, JsonRpcResponse.NumberId(0), null, signature, resp.upstreamId))
} else { } else {
metrics?.fails?.increment() metrics?.fails?.increment()
Mono.error( Mono.error(

View File

@@ -17,6 +17,11 @@ package io.emeraldpay.dshackle.upstream.rpcclient
import io.emeraldpay.dshackle.config.AuthConfig import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.reader.JsonRpcHttpReader import io.emeraldpay.dshackle.reader.JsonRpcHttpReader
import io.emeraldpay.dshackle.upstream.rpcclient.stream.AggregateResponse
import io.emeraldpay.dshackle.upstream.rpcclient.stream.JsonRpcStreamParser
import io.emeraldpay.dshackle.upstream.rpcclient.stream.Response
import io.emeraldpay.dshackle.upstream.rpcclient.stream.SingleResponse
import io.emeraldpay.dshackle.upstream.rpcclient.stream.StreamResponse
import io.emeraldpay.etherjar.rpc.RpcException import io.emeraldpay.etherjar.rpc.RpcException
import io.emeraldpay.etherjar.rpc.RpcResponseError import io.emeraldpay.etherjar.rpc.RpcResponseError
import io.micrometer.core.instrument.Metrics import io.micrometer.core.instrument.Metrics
@@ -29,8 +34,6 @@ import org.apache.commons.lang3.time.StopWatch
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.netty.http.client.HttpClient import reactor.netty.http.client.HttpClient
import reactor.netty.resources.ConnectionProvider import reactor.netty.resources.ConnectionProvider
import reactor.util.function.Tuple2
import reactor.util.function.Tuples
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.security.KeyStore import java.security.KeyStore
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
@@ -51,6 +54,7 @@ class JsonRpcHttpClient(
) : JsonRpcHttpReader { ) : JsonRpcHttpReader {
private val parser = ResponseRpcParser() private val parser = ResponseRpcParser()
private val streamParser = JsonRpcStreamParser()
private val httpClient: HttpClient private val httpClient: HttpClient
init { init {
@@ -91,18 +95,34 @@ class JsonRpcHttpClient(
this.httpClient = build this.httpClient = build
} }
fun execute(request: ByteArray): Mono<Tuple2<Int, ByteArray>> { private fun execute(request: JsonRpcRequest): Mono<out Response> {
val bytesRequest = request.toJson()
val response = httpClient val response = httpClient
.post() .post()
.uri(target) .uri(target)
.send(Mono.just(request).map { Unpooled.wrappedBuffer(it) }) .send(Mono.just(Unpooled.wrappedBuffer(bytesRequest)))
return response.response { header, bytes -> return if (!request.isStreamed) {
val statusCode = header.status().code() response.response { header, bytes ->
bytes.aggregate().asByteArray().map { val statusCode = header.status().code()
Tuples.of(statusCode, it)
} bytes.aggregate().asByteArray().map {
}.single() AggregateResponse(it, statusCode)
}
}.single()
} else {
response.responseConnection { t, u ->
streamParser.streamParse(
t.status().code(),
u.inbound().receive()
.asByteArray()
.doFinally {
u.dispose()
},
)
}.single()
}
} }
override fun onStop() { override fun onStop() {
@@ -113,7 +133,6 @@ class JsonRpcHttpClient(
override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> { override fun read(key: JsonRpcRequest): Mono<JsonRpcResponse> {
val startTime = StopWatch() val startTime = StopWatch()
return Mono.just(key) return Mono.just(key)
.map(JsonRpcRequest::toJson)
.doOnNext { .doOnNext {
if (!startTime.isStarted) { if (!startTime.isStarted) {
startTime.start() startTime.start()
@@ -167,26 +186,40 @@ class JsonRpcHttpClient(
* Process response from the upstream and convert it to JsonRpcResponse. * Process response from the upstream and convert it to JsonRpcResponse.
* The input is a pair of (Http Status Code, Http Response Body) * The input is a pair of (Http Status Code, Http Response Body)
*/ */
private fun asJsonRpcResponse(key: JsonRpcRequest): Function<Mono<Tuple2<Int, ByteArray>>, Mono<JsonRpcResponse>> { private fun asJsonRpcResponse(key: JsonRpcRequest): Function<Mono<out Response>, Mono<JsonRpcResponse>> {
return Function { resp -> return Function { resp ->
resp.map { resp.map {
val parsed = parser.parse(it.t2) when (it) {
val statusCode = it.t1 is AggregateResponse -> {
if (statusCode != 200) { val parsed = parser.parse(it.response)
if (parsed.hasError() && parsed.error!!.code != RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE) { val statusCode = it.code
// extracted the error details from the HTTP Body if (statusCode != 200) {
parsed if (parsed.hasError() && parsed.error!!.code != RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE) {
} else { // extracted the error details from the HTTP Body
// here we got a valid response with ERROR as HTTP Status Code. We assume that HTTP Status has parsed
// a higher priority so return an error here anyway } else {
JsonRpcResponse.error( // here we got a valid response with ERROR as HTTP Status Code. We assume that HTTP Status has
RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, // a higher priority so return an error here anyway
"HTTP Code: $statusCode", JsonRpcResponse.error(
JsonRpcResponse.NumberId(key.id), RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE,
) "HTTP Code: $statusCode",
JsonRpcResponse.NumberId(key.id),
)
}
} else {
parsed
}
}
is StreamResponse -> {
JsonRpcResponse(it.stream, key.id)
}
is SingleResponse -> {
if (it.hasError()) {
JsonRpcResponse(null, it.error)
} else {
JsonRpcResponse(it.result, null)
}
} }
} else {
parsed
} }
} }
} }

View File

@@ -28,6 +28,7 @@ data class JsonRpcRequest(
val id: Int, val id: Int,
val nonce: Long?, val nonce: Long?,
val selector: BlockchainOuterClass.Selector?, val selector: BlockchainOuterClass.Selector?,
val isStreamed: Boolean = false,
) { ) {
@JvmOverloads constructor( @JvmOverloads constructor(
@@ -35,7 +36,8 @@ data class JsonRpcRequest(
params: List<Any?>, params: List<Any?>,
nonce: Long? = null, nonce: Long? = null,
selectors: BlockchainOuterClass.Selector? = null, selectors: BlockchainOuterClass.Selector? = null,
) : this(method, params, 1, nonce, selectors) isStreamed: Boolean = false,
) : this(method, params, 1, nonce, selectors, isStreamed)
fun toJson(): ByteArray { fun toJson(): ByteArray {
val json = mapOf( val json = mapOf(

View File

@@ -18,14 +18,16 @@ package io.emeraldpay.dshackle.upstream.rpcclient
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.SerializerProvider
import io.emeraldpay.dshackle.upstream.rpcclient.stream.Chunk
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
class JsonRpcResponse( class JsonRpcResponse(
private val result: ByteArray?, private val result: ByteArray?,
val error: JsonRpcError?, val error: JsonRpcError?,
val id: Id, val id: Id,
val stream: Flux<Chunk>?,
/** /**
* When making a request through Dshackle protocol a remote may provide its signature with the response, which we keep here * When making a request through Dshackle protocol a remote may provide its signature with the response, which we keep here
*/ */
@@ -33,10 +35,13 @@ class JsonRpcResponse(
val providedUpstreamId: String? = null, val providedUpstreamId: String? = null,
) { ) {
constructor(result: ByteArray?, error: JsonRpcError?) : this(result, error, NumberId(0)) constructor(stream: Flux<Chunk>, id: Int) :
this(null, null, NumberId(id.toLong()), stream, null, null)
constructor(result: ByteArray?, error: JsonRpcError?) : this(result, error, NumberId(0), null)
constructor(result: ByteArray?, error: JsonRpcError?, resolvedBy: String?) : constructor(result: ByteArray?, error: JsonRpcError?, resolvedBy: String?) :
this(result, error, NumberId(0), null, resolvedBy) this(result, error, NumberId(0), null, null, resolvedBy)
companion object { companion object {
private val NULL_VALUE = "null".toByteArray() private val NULL_VALUE = "null".toByteArray()
@@ -48,7 +53,7 @@ class JsonRpcResponse(
@JvmStatic @JvmStatic
fun ok(value: ByteArray, id: Id): JsonRpcResponse { fun ok(value: ByteArray, id: Id): JsonRpcResponse {
return JsonRpcResponse(value, null, id) return JsonRpcResponse(value, null, id, null)
} }
@JvmStatic @JvmStatic
@@ -63,23 +68,25 @@ class JsonRpcResponse(
@JvmStatic @JvmStatic
fun error(error: JsonRpcError, id: Id): JsonRpcResponse { fun error(error: JsonRpcError, id: Id): JsonRpcResponse {
return JsonRpcResponse(null, error, id) return JsonRpcResponse(null, error, id, null)
} }
@JvmStatic @JvmStatic
fun error(code: Int, msg: String, id: Id): JsonRpcResponse { fun error(code: Int, msg: String, id: Id): JsonRpcResponse {
return JsonRpcResponse(null, JsonRpcError(code, msg), id) return JsonRpcResponse(null, JsonRpcError(code, msg), id, null)
} }
} }
fun hasResult(): Boolean { fun hasResult(): Boolean {
return result != null return result != null || stream != null
} }
fun hasError(): Boolean { fun hasError(): Boolean {
return error != null return error != null
} }
fun hasStream(): Boolean = stream != null
fun isNull(): Boolean { fun isNull(): Boolean {
return result != null && NULL_VALUE.contentEquals(result) return result != null && NULL_VALUE.contentEquals(result)
} }
@@ -120,7 +127,7 @@ class JsonRpcResponse(
} }
fun copyWithId(id: Id): JsonRpcResponse { fun copyWithId(id: Id): JsonRpcResponse {
return JsonRpcResponse(result, error, id, providedSignature, providedUpstreamId) return JsonRpcResponse(result, error, id, stream, providedSignature, providedUpstreamId)
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@@ -0,0 +1,283 @@
package io.emeraldpay.dshackle.upstream.rpcclient.stream
import com.fasterxml.jackson.core.JsonFactory
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import io.emeraldpay.dshackle.upstream.rpcclient.ResponseRpcParser
import io.emeraldpay.etherjar.rpc.RpcResponseError
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.netty.ByteBufFlux
import java.util.Arrays
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
class JsonRpcStreamParser {
companion object {
private val jsonFactory = JsonFactory()
private val responseRpcParser = ResponseRpcParser()
private const val ARRAY_OPEN_BRACKET: Byte = '['.code.toByte()
private const val ARRAY_CLOSE_BRACKET: Byte = ']'.code.toByte()
private const val OBJECT_OPEN_BRACKET: Byte = '{'.code.toByte()
private const val OBJECT_CLOSE_BRACKET: Byte = '}'.code.toByte()
private const val BACKSLASH: Byte = '\\'.code.toByte()
private const val QUOTE: Byte = '"'.code.toByte()
}
fun streamParse(statusCode: Int, response: Flux<ByteArray>): Mono<out Response> {
return response.switchOnFirst({ first, responseStream ->
if (first.get() == null) {
aggregateResponse(responseStream, statusCode)
} else {
val whatCount = AtomicReference<Count>()
val endStream = AtomicBoolean(false)
val firstBytes = first.get()!!
val firstPart: SingleResponse? = parseFirstPart(firstBytes, endStream, whatCount)
if (firstPart == null) {
aggregateResponse(responseStream, statusCode)
} else {
processSingleResponse(firstPart, responseStream, endStream, whatCount)
}
}
}, false,)
.single()
.onErrorResume {
Mono.just(
SingleResponse(
null,
JsonRpcError(RpcResponseError.CODE_UPSTREAM_INVALID_RESPONSE, it.message ?: "Internal error"),
),
)
}
}
private fun processSingleResponse(
response: SingleResponse,
responseStream: Flux<ByteArray>,
endStream: AtomicBoolean,
whatCount: AtomicReference<Count>,
): Mono<out Response> {
if (response.noResponse()) {
throw IllegalStateException("Invalid JSON structure")
} else {
if (response.hasError()) {
return Mono.just(response)
} else {
return if (endStream.get()) {
Mono.just(SingleResponse(response.result, null))
} else {
Mono.just(
StreamResponse(
streamParts(
response.result!!,
responseStream,
endStream,
whatCount,
),
),
)
}
}
}
}
private fun aggregateResponse(response: Flux<ByteArray>, statusCode: Int): Mono<AggregateResponse> {
return ByteBufFlux.fromInbound(response).aggregate().asByteArray()
.map { AggregateResponse(it, statusCode) }
}
private fun streamParts(
firstBytes: ByteArray,
responseStream: Flux<ByteArray>,
endStream: AtomicBoolean,
whatCount: AtomicReference<Count>,
): Flux<Chunk> {
return Flux.concat(
Mono.just(Chunk(firstBytes, false)),
responseStream.skip(1)
.filter { !endStream.get() }
.map { bytes ->
val whatCountValue = whatCount.get()
for (i in bytes.indices) {
when (whatCountValue) {
is CountObjectBrackets -> {
countBrackets(bytes[i], whatCountValue.count, OBJECT_OPEN_BRACKET, OBJECT_CLOSE_BRACKET)
}
is CountArrayBrackets -> {
countBrackets(bytes[i], whatCountValue.count, ARRAY_OPEN_BRACKET, ARRAY_CLOSE_BRACKET)
}
is CountSlashes -> {
countQuotesAndSlashes(bytes[i], whatCountValue)
}
}
if (whatCountValue.isFinished()) {
endStream.set(true)
return@map Chunk(Arrays.copyOfRange(bytes, 0, i + 1), true)
}
}
Chunk(bytes, false)
},
Mono.just(endStream)
.flatMap {
if (!it.get()) {
Mono.just(Chunk(ByteArray(0), true))
} else {
Mono.empty()
}
},
)
}
private fun parseFirstPart(
firstBytes: ByteArray,
endStream: AtomicBoolean,
whatCount: AtomicReference<Count>,
): SingleResponse? {
jsonFactory.createParser(firstBytes).use { parser ->
while (true) {
parser.nextToken()
if (firstBytes.size == parser.currentLocation.byteOffset.toInt()) {
break
}
if (parser.currentName != null) {
if (parser.currentName == "result") {
val token = parser.nextToken()
val tokenStart = parser.tokenLocation.byteOffset.toInt()
return if (token.isScalarValue) {
val count = CountSlashes(AtomicInteger(1))
whatCount.set(count)
SingleResponse(processScalarValue(parser, tokenStart, firstBytes, count, endStream), null)
} else {
when (token) {
JsonToken.START_OBJECT -> {
val count = CountObjectBrackets(AtomicInteger(1))
whatCount.set(count)
SingleResponse(
processAndCountBrackets(tokenStart, firstBytes, count.count, endStream, OBJECT_OPEN_BRACKET, OBJECT_CLOSE_BRACKET),
null,
)
}
JsonToken.START_ARRAY -> {
val count = CountArrayBrackets(AtomicInteger(1))
whatCount.set(count)
SingleResponse(
processAndCountBrackets(tokenStart, firstBytes, count.count, endStream, ARRAY_OPEN_BRACKET, ARRAY_CLOSE_BRACKET),
null,
)
}
else -> {
throw IllegalStateException("'result' not an object nor array'")
}
}
}
} else if (parser.currentName == "error") {
return SingleResponse(null, responseRpcParser.readError(parser))
}
}
}
return null
}
}
private fun processAndCountBrackets(
tokenStart: Int,
bytes: ByteArray,
brackets: AtomicInteger,
endStream: AtomicBoolean,
openBracket: Byte,
closeBracket: Byte,
): ByteArray {
for (i in tokenStart + 1 until bytes.size) {
countBrackets(bytes[i], brackets, openBracket, closeBracket)
if (brackets.get() == 0) {
endStream.set(true)
return Arrays.copyOfRange(bytes, tokenStart, i + 1)
}
}
return Arrays.copyOfRange(bytes, tokenStart, bytes.size)
}
private fun countBrackets(
byte: Byte,
brackets: AtomicInteger,
openBracket: Byte,
closeBracket: Byte,
) {
if (byte == openBracket) {
brackets.incrementAndGet()
} else if (byte == closeBracket) {
brackets.decrementAndGet()
}
}
private fun countQuotesAndSlashes(
byte: Byte,
countSlashes: CountSlashes,
) {
if (byte == BACKSLASH && !countSlashes.hasSlash()) {
countSlashes.count.incrementAndGet()
} else if (countSlashes.hasSlash()) {
countSlashes.count.decrementAndGet()
} else if (!countSlashes.hasSlash() && byte == QUOTE) {
countSlashes.count.set(0)
}
}
private fun processScalarValue(
parser: JsonParser,
tokenStart: Int,
bytes: ByteArray,
countSlashes: CountSlashes,
endStream: AtomicBoolean,
): ByteArray {
when (parser.currentToken) {
JsonToken.VALUE_NULL -> {
endStream.set(true)
return "null".toByteArray()
}
JsonToken.VALUE_STRING -> {
for (i in tokenStart + 1 until bytes.size) {
countQuotesAndSlashes(bytes[i], countSlashes)
if (countSlashes.isFinished()) {
endStream.set(true)
return Arrays.copyOfRange(bytes, tokenStart, i + 1)
}
}
return Arrays.copyOfRange(bytes, tokenStart, bytes.size)
}
else -> {
endStream.set(true)
return parser.text.toByteArray()
}
}
}
private abstract class Count(
val count: AtomicInteger,
) {
open fun isFinished(): Boolean = count.get() == 0
}
private class CountArrayBrackets(
countBrackets: AtomicInteger,
) : Count(countBrackets)
private class CountObjectBrackets(
countBrackets: AtomicInteger,
) : Count(countBrackets)
private class CountSlashes(
countSlashes: AtomicInteger,
) : Count(countSlashes) {
fun hasSlash() = count.get() == 2
}
}

View File

@@ -0,0 +1,80 @@
package io.emeraldpay.dshackle.upstream.rpcclient.stream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcError
import reactor.core.publisher.Flux
sealed class Response
data class SingleResponse(
val result: ByteArray?,
val error: JsonRpcError?,
) : Response() {
fun hasError() = error != null
fun noResponse() = result == null && error == null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SingleResponse) return false
if (result != null) {
if (other.result == null) return false
if (!result.contentEquals(other.result)) return false
} else if (other.result != null) return false
if (error != other.error) return false
return true
}
override fun hashCode(): Int {
var result1 = result?.contentHashCode() ?: 0
result1 = 31 * result1 + (error?.hashCode() ?: 0)
return result1
}
}
data class StreamResponse(
val stream: Flux<Chunk>,
) : Response()
data class AggregateResponse(
val response: ByteArray,
val code: Int,
) : Response() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AggregateResponse) return false
if (!response.contentEquals(other.response)) return false
if (code != other.code) return false
return true
}
override fun hashCode(): Int {
var result = response.contentHashCode()
result = 31 * result + code
return result
}
}
data class Chunk(
val chunkData: ByteArray,
val finalChunk: Boolean,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Chunk) return false
if (!chunkData.contentEquals(other.chunkData)) return false
if (finalChunk != other.finalChunk) return false
return true
}
override fun hashCode(): Int {
var result = chunkData.contentHashCode()
result = 31 * result + finalChunk.hashCode()
return result
}
}

View File

@@ -62,7 +62,7 @@ class BaseHandlerSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE) def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.items.add(request) call.items.add(request)
call.ids[0] = 5 call.ids[0] = 5
def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, null, null) def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, null, null, null)
when: when:
def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, false)) def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, false))
.collectList() .collectList()
@@ -85,7 +85,7 @@ class BaseHandlerSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.BATCH) def call = new ProxyCall(ProxyCall.RpcType.BATCH)
call.items.add(request) call.items.add(request)
call.ids[0] = 5 call.ids[0] = 5
def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, null, null) def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, null, null, null)
when: when:
def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, false)) def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, false))
.collectList() .collectList()
@@ -116,8 +116,8 @@ class BaseHandlerSpec extends Specification {
call.items.add(request2) call.items.add(request2)
call.ids[1] = 6 call.ids[1] = 6
def response = [ def response = [
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null, null, null), new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null, null, null, null),
new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, null, null) new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, null, null, null)
] ]
when: when:
def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, true)) def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, true))
@@ -149,8 +149,8 @@ class BaseHandlerSpec extends Specification {
call.items.add(request2) call.items.add(request2)
call.ids[1] = 6 call.ids[1] = 6
def response = [ def response = [
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null, null, null), new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null, null, null, null),
new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, null, null) new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, null, null, null)
] ]
when: when:
def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, true)) def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, true))
@@ -189,8 +189,8 @@ class BaseHandlerSpec extends Specification {
// note there is only 2 responses // note there is only 2 responses
def response = [ def response = [
new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null, null, null), new NativeCall.CallResult(1, null, '{"foo": 2}'.bytes, null, null, null, null, null),
new NativeCall.CallResult(2, null, '{"foo": 3}'.bytes, null, null, null, null) new NativeCall.CallResult(2, null, '{"foo": 3}'.bytes, null, null, null, null, null)
] ]
when: when:
def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, true)) def act = Flux.from(handler.execute(Chain.ETHEREUM__MAINNET, call, requestHandler, true))

View File

@@ -42,7 +42,7 @@ class HttpHandlerSpec extends Specification {
.setMethod("test_test") .setMethod("test_test")
.setPayload(ByteString.copyFromUtf8("[]")) .setPayload(ByteString.copyFromUtf8("[]"))
.build() .build()
def respItem = new NativeCall.CallResult(1, null, "100".bytes, null, null, null, null) def respItem = new NativeCall.CallResult(1, null, "100".bytes, null, null, null, null, null)
def req = BlockchainOuterClass.NativeCallRequest.newBuilder() def req = BlockchainOuterClass.NativeCallRequest.newBuilder()
.setChain(Common.ChainRef.CHAIN_ETHEREUM__MAINNET) .setChain(Common.ChainRef.CHAIN_ETHEREUM__MAINNET)
.addItems(reqItem) .addItems(reqItem)
@@ -128,7 +128,7 @@ class HttpHandlerSpec extends Specification {
def act = handler.execute(Chain.ETHEREUM__MAINNET, call, new AccessHandlerHttp.NoOpHandler(), false) def act = handler.execute(Chain.ETHEREUM__MAINNET, call, new AccessHandlerHttp.NoOpHandler(), false)
then: then:
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, null, "".bytes, null, null, null, null)) 1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, null, "".bytes, null, null, null, null, null))
StepVerifier.create(act) StepVerifier.create(act)
.expectNext("hello") .expectNext("hello")
.expectComplete() .expectComplete()

View File

@@ -85,7 +85,7 @@ class WebsocketHandlerSpec extends Specification {
def "Respond to a single call"() { def "Respond to a single call"() {
setup: setup:
def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, "test", null) def response = new NativeCall.CallResult(0, null, '{"foo": 1}'.bytes, null, null, "test", null, null)
def nativeCall = Mock(NativeCall) { def nativeCall = Mock(NativeCall) {
1 * it.nativeCallResult(_) >> Flux.fromIterable([response]) 1 * it.nativeCallResult(_) >> Flux.fromIterable([response])

View File

@@ -85,7 +85,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE) def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 105 call.ids[1] = 105
def data = [ def data = [
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null, null, null) new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null, null, null, null)
] ]
when: when:
def act = writer.toJson(call, data[0]) def act = writer.toJson(call, data[0])
@@ -98,7 +98,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE) def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1 call.ids[1] = 1
def data = [ def data = [
new NativeCall.CallResult(1, null, null, new NativeCall.CallError(1, "Internal Error", null, null, null), null, null, null) new NativeCall.CallResult(1, null, null, new NativeCall.CallError(1, "Internal Error", null, null, null), null, null, null, null)
] ]
when: when:
def act = writer.toJson(call, data[0]) def act = writer.toJson(call, data[0])
@@ -111,7 +111,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE) def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = "aaa" call.ids[1] = "aaa"
def data = [ def data = [
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null, null, null) new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null, null, null, null)
] ]
when: when:
def act = writer.toJson(call, data[0]) def act = writer.toJson(call, data[0])
@@ -126,9 +126,9 @@ class WriteRpcJsonSpec extends Specification {
call.ids[2] = 11 call.ids[2] = 11
call.ids[3] = 15 call.ids[3] = 15
def data = [ def data = [
new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null, null, null), new NativeCall.CallResult(1, null, '"0x98dbb1"'.bytes, null, null, null, null, null),
new NativeCall.CallResult(2, null, null, new NativeCall.CallError(2, "oops", null, null, null), null, null, null), new NativeCall.CallResult(2, null, null, new NativeCall.CallError(2, "oops", null, null, null), null, null, null, null),
new NativeCall.CallResult(3, null, '{"hash": "0x2484f459dc"}'.bytes, null, null, null, null), new NativeCall.CallResult(3, null, '{"hash": "0x2484f459dc"}'.bytes, null, null, null, null, null),
] ]
when: when:
def act = Flux.fromIterable(data) def act = Flux.fromIterable(data)
@@ -154,7 +154,7 @@ class WriteRpcJsonSpec extends Specification {
def call = new ProxyCall(ProxyCall.RpcType.SINGLE) def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 10 call.ids[1] = 10
def data = [ def data = [
new NativeCall.CallResult(1, null, '"0x1"'.bytes, null, null, null, null), new NativeCall.CallResult(1, null, '"0x1"'.bytes, null, null, null, null, null),
] ]
when: when:
def act = Flux.fromIterable(data) def act = Flux.fromIterable(data)

View File

@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import spock.lang.Specification import spock.lang.Specification
@@ -42,10 +43,10 @@ class AlwaysQuorumSpec extends Specification {
def quorum = new AlwaysQuorum() def quorum = new AlwaysQuorum()
def up = Stub(Upstream) def up = Stub(Upstream)
when: when:
quorum.record("123".bytes, new ResponseSigner.Signature("sig1".bytes, "test", 100), up) quorum.record(new JsonRpcResponse("123".bytes, null), new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
then: then:
quorum.isResolved() quorum.isResolved()
quorum.getResult() == "123".bytes quorum.getResponse().getResult() == "123".bytes
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100) quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
!quorum.isFailed() !quorum.isFailed()
} }

View File

@@ -18,9 +18,9 @@ package io.emeraldpay.dshackle.quorum
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import io.emeraldpay.dshackle.Global import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import spock.lang.Specification import spock.lang.Specification
class BroadcastQuorumSpec extends Specification { class BroadcastQuorumSpec extends Specification {
@@ -35,20 +35,20 @@ class BroadcastQuorumSpec extends Specification {
def upstream3 = Stub(Upstream) def upstream3 = Stub(Upstream)
when: when:
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream1) q.record(new JsonRpcResponse('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null), null, upstream1)
then: then:
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _) 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
when: when:
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream2) q.record(new JsonRpcResponse('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null), null, upstream2)
then: then:
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _) 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
when: when:
q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3) q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3)
then: then:
1 * q.recordError(_, _, _, _) 1 * q.recordError(_, _, _)
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c" objectMapper.readValue(q.response.getResult(), Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
} }
def "Remembers first response"() { def "Remembers first response"() {
@@ -61,19 +61,19 @@ class BroadcastQuorumSpec extends Specification {
when: when:
q.record(new JsonRpcException(1, "Internal error"), null, upstream1) q.record(new JsonRpcException(1, "Internal error"), null, upstream1)
then: then:
1 * q.recordError(_, _, _, _) 1 * q.recordError(_, _, _)
when: when:
q.record('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null, upstream2) q.record(new JsonRpcResponse('"0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"'.bytes, null), null, upstream2)
then: then:
1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _) 1 * q.recordValue(_, "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c", _, _)
when: when:
q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3) q.record(new JsonRpcException(1, "Nonce too low"), null, upstream3)
then: then:
1 * q.recordError(_, _, _, _) 1 * q.recordError(_, _, _)
q.isResolved() q.isResolved()
objectMapper.readValue(q.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c" objectMapper.readValue(q.response.result, Object) == "0xeaa972c0d8d1ecd3e34fbbef6d34e06670e745c788bdba31c4234a1762f0378c"
} }
def "Failed if error received 3+ times"() { def "Failed if error received 3+ times"() {

View File

@@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import spock.lang.Specification import spock.lang.Specification
class MaximumValueQuorumSpec extends Specification { class MaximumValueQuorumSpec extends Specification {
@@ -18,11 +19,11 @@ class MaximumValueQuorumSpec extends Specification {
} }
when: when:
def quorum = new MaximumValueQuorum() def quorum = new MaximumValueQuorum()
quorum.record('"0x137"'.bytes, null, up) quorum.record(new JsonRpcResponse('"0x137"'.bytes, null), null, up)
quorum.record('"0x138"'.bytes, null, up1) quorum.record(new JsonRpcResponse('"0x138"'.bytes, null), null, up1)
quorum.record('"0x139"'.bytes, null, up2) quorum.record(new JsonRpcResponse('"0x139"'.bytes, null), null, up2)
then: then:
quorum.result == '"0x139"'.bytes quorum.response.result == '"0x139"'.bytes
quorum.resolvedBy.size() == 1 quorum.resolvedBy.size() == 1
quorum.isResolved() quorum.isResolved()
quorum.resolvedBy.contains(up2) quorum.resolvedBy.contains(up2)
@@ -41,11 +42,11 @@ class MaximumValueQuorumSpec extends Specification {
} }
when: when:
def quorum = new MaximumValueQuorum() def quorum = new MaximumValueQuorum()
quorum.record('"0x137"'.bytes, null, up) quorum.record(new JsonRpcResponse('"0x137"'.bytes, null), null, up)
quorum.record('"0x138"'.bytes, null, up1) quorum.record(new JsonRpcResponse('"0x138"'.bytes, null), null, up1)
quorum.record(new JsonRpcException(10, "error"), null, up2) quorum.record(new JsonRpcException(10, "error"), null, up2)
then: then:
quorum.result == '"0x138"'.bytes quorum.response.result == '"0x138"'.bytes
quorum.isResolved() quorum.isResolved()
quorum.resolvedBy.size() == 1 quorum.resolvedBy.size() == 1
quorum.resolvedBy.contains(up1) quorum.resolvedBy.contains(up1)

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import spock.lang.Specification import spock.lang.Specification
@@ -30,12 +31,12 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1) def quorum = new NotLaggingQuorum(1)
when: when:
quorum.record(value, null, up) quorum.record(new JsonRpcResponse(value, null), null, up)
then: then:
1 * up.getLag() >> 0 1 * up.getLag() >> 0
quorum.isResolved() quorum.isResolved()
!quorum.isFailed() !quorum.isFailed()
quorum.result == value quorum.response.result == value
} }
def "Keeps signature and upstream"() { def "Keeps signature and upstream"() {
@@ -45,12 +46,12 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1) def quorum = new NotLaggingQuorum(1)
when: when:
quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up) quorum.record(new JsonRpcResponse(value, null), new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
then: then:
1 * up.getLag() >> 0 1 * up.getLag() >> 0
quorum.isResolved() quorum.isResolved()
!quorum.isFailed() !quorum.isFailed()
quorum.result == value quorum.response.result == value
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100) quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
} }
@@ -61,12 +62,12 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1) def quorum = new NotLaggingQuorum(1)
when: when:
quorum.record(value, null, up) quorum.record(new JsonRpcResponse(value, null), null, up)
then: then:
1 * up.getLag() >> 1 1 * up.getLag() >> 1
quorum.isResolved() quorum.isResolved()
!quorum.isFailed() !quorum.isFailed()
quorum.result == value quorum.response.result == value
} }
def "Ignores if lags"() { def "Ignores if lags"() {
@@ -76,7 +77,7 @@ class NotLaggingQuorumSpec extends Specification {
def quorum = new NotLaggingQuorum(1) def quorum = new NotLaggingQuorum(1)
when: when:
quorum.record(value, null, up) quorum.record(new JsonRpcResponse(value, null), null, up)
then: then:
1 * up.getLag() >> 2 1 * up.getLag() >> 2
!quorum.isResolved() !quorum.isResolved()

View File

@@ -2,6 +2,7 @@ package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcException
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import spock.lang.Specification import spock.lang.Specification
@@ -19,19 +20,20 @@ class NotNullQuorumSpec extends Specification {
1 * getId() >> "id2" 1 * getId() >> "id2"
} }
def value = "null".getBytes() def value = "null".getBytes()
def response = new JsonRpcResponse(value, null)
def quorum = new NotNullQuorum() def quorum = new NotNullQuorum()
when: when:
def res = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up) def res = quorum.record(response, new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
def res1 = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up1) def res1 = quorum.record(response, new ResponseSigner.Signature("sig1".bytes, "test", 100), up1)
def res2 = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up2) def res2 = quorum.record(response, new ResponseSigner.Signature("sig1".bytes, "test", 100), up2)
def res3 = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up) def res3 = quorum.record(response, new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
then: then:
!res !res
!res1 !res1
!res2 !res2
res3 res3
quorum.result == value quorum.response.result == value
!quorum.isFailed() !quorum.isFailed()
quorum.isResolved() quorum.isResolved()
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100) quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
@@ -77,7 +79,7 @@ class NotNullQuorumSpec extends Specification {
def quorum = new NotNullQuorum() def quorum = new NotNullQuorum()
when: when:
def res = quorum.record(value, new ResponseSigner.Signature("sig1".bytes, "test", 100), up) def res = quorum.record(new JsonRpcResponse(value, null), new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
quorum.record(new JsonRpcException(10, "error"), new ResponseSigner.Signature("sig1".bytes, "test", 100), up1) quorum.record(new JsonRpcException(10, "error"), new ResponseSigner.Signature("sig1".bytes, "test", 100), up1)
quorum.record(new JsonRpcException(10, "error"), new ResponseSigner.Signature("sig1".bytes, "test", 100), up2) quorum.record(new JsonRpcException(10, "error"), new ResponseSigner.Signature("sig1".bytes, "test", 100), up2)
quorum.record(new JsonRpcException(10, "error"), new ResponseSigner.Signature("sig1".bytes, "test", 100), up) quorum.record(new JsonRpcException(10, "error"), new ResponseSigner.Signature("sig1".bytes, "test", 100), up)
@@ -86,7 +88,7 @@ class NotNullQuorumSpec extends Specification {
!res !res
quorum.isResolved() quorum.isResolved()
!quorum.isFailed() !quorum.isFailed()
quorum.result == value quorum.response.result == value
quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100) quorum.signature == new ResponseSigner.Signature("sig1".bytes, "test", 100)
} }
} }

View File

@@ -15,8 +15,9 @@
*/ */
package io.emeraldpay.dshackle.quorum package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.upstream.Head
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable import org.jetbrains.annotations.Nullable
@@ -66,14 +67,14 @@ class ValueAwareQuorumSpec extends Specification {
} }
@Override @Override
void recordValue(@NotNull byte[] response, @Nullable Object responseValue, @Nullable ResponseSigner.Signature signature, @NotNull Upstream upstream) { void recordValue(@NotNull JsonRpcResponse response, @Nullable Object responseValue, @Nullable ResponseSigner.Signature signature, @NotNull Upstream upstream) {
} }
@Override @Override
void recordError(@Nullable byte[] response, @Nullable String errorMessage, @Nullable ResponseSigner.Signature signature, @NotNull Upstream upstream) { void recordError(@Nullable String errorMessage, @Nullable ResponseSigner.Signature signature, @NotNull Upstream upstream) {
} }
@@ -88,8 +89,8 @@ class ValueAwareQuorumSpec extends Specification {
} }
@Override @Override
byte[] getResult() { JsonRpcResponse getResponse() {
return new byte[0] return new JsonRpcResponse(null, null)
} }
@Override @Override

View File

@@ -181,7 +181,7 @@ class BroadcastReaderSpec extends Specification {
when: when:
def act = reader def act = reader
.read(new JsonRpcRequest("eth_sendRawTransaction", ["0x1"])) .read(new JsonRpcRequest("eth_sendRawTransaction", ["0x1"]))
.switchIfEmpty(Mono.just(new RpcReader.Result(new byte[0], null, 0, null))) .switchIfEmpty(Mono.just(new RpcReader.Result(new byte[0], null, 0, null, null)))
then: then:
StepVerifier.create(act) StepVerifier.create(act)
.expectErrorMessage("Unhandled Upstream error") .expectErrorMessage("Unhandled Upstream error")

View File

@@ -130,7 +130,7 @@ class NativeCallSpec extends Specification {
def nativeCall = nativeCall() def nativeCall = nativeCall()
nativeCall.rpcReaderFactory = Mock(RpcReaderFactory) { nativeCall.rpcReaderFactory = Mock(RpcReaderFactory) {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(_) >> Mono.just(new RpcReader.Result("\"foo\"".bytes, null, 1, ups)) 1 * read(_) >> Mono.just(new RpcReader.Result("\"foo\"".bytes, null, 1, ups, null))
} }
} }
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,
@@ -238,7 +238,7 @@ class NativeCallSpec extends Specification {
when: when:
def resp = nativeCall.buildResponse( def resp = nativeCall.buildResponse(
new NativeCall.CallResult(1561, 10, objectMapper.writeValueAsBytes(json), null, null, null, null) new NativeCall.CallResult(1561, 10, objectMapper.writeValueAsBytes(json), null, null, null, null, null)
) )
then: then:
resp.id == 1561 resp.id == 1561
@@ -253,7 +253,7 @@ class NativeCallSpec extends Specification {
when: when:
def resp = nativeCall.buildResponse( def resp = nativeCall.buildResponse(
new NativeCall.CallResult(1561, 10, objectMapper.writeValueAsBytes(json), null, new ResponseSigner.Signature("sig1".bytes, "test", 100), "test", null) new NativeCall.CallResult(1561, 10, objectMapper.writeValueAsBytes(json), null, new ResponseSigner.Signature("sig1".bytes, "test", 100), "test", null, null)
) )
then: then:
resp.id == 1561 resp.id == 1561
@@ -584,7 +584,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(), null, "reqId", 1) new NativeCall.WithFilterIdDecorator(), new NativeCall.NoneResultDecorator(), null, false, "reqId", 1)
when: when:
def act = nativeCall.parseParams(ctx) def act = nativeCall.parseParams(ctx)
then: then:
@@ -614,12 +614,12 @@ class NativeCallSpec extends Specification {
def nativeCall = nativeCall(multistreamHolder) def nativeCall = nativeCall(multistreamHolder)
nativeCall.rpcReaderFactory = Mock(RpcReaderFactory) { nativeCall.rpcReaderFactory = Mock(RpcReaderFactory) {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(_) >> Mono.just(new RpcReader.Result("\"0xab\"".bytes, null, 1, ups)) 1 * read(_) >> Mono.just(new RpcReader.Result("\"0xab\"".bytes, null, 1, ups, null))
} }
} }
def call = new NativeCall.ValidCallContext(1, 10, multistream, Selector.empty, quorum, def call = new NativeCall.ValidCallContext(1, 10, multistream, Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_getFilterChanges", []), new NativeCall.ParsedCallDetails("eth_getFilterChanges", []),
new NativeCall.WithFilterIdDecorator(), new NativeCall.CreateFilterDecorator(), null, "reqId", 1) new NativeCall.WithFilterIdDecorator(), new NativeCall.CreateFilterDecorator(), null, false, "reqId", 1)
when: when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1)) def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1))
@@ -650,12 +650,12 @@ class NativeCallSpec extends Specification {
def nativeCall = nativeCall(multistreamHolder) def nativeCall = nativeCall(multistreamHolder)
nativeCall.rpcReaderFactory = Mock(RpcReaderFactory) { nativeCall.rpcReaderFactory = Mock(RpcReaderFactory) {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(_) >> Mono.just(new RpcReader.Result("\"0xab\"".bytes, null, 1, ups)) 1 * read(_) >> Mono.just(new RpcReader.Result("\"0xab\"".bytes, null, 1, ups, null))
} }
} }
def call = new NativeCall.ValidCallContext(1, 10, multistream, Selector.empty, quorum, def call = new NativeCall.ValidCallContext(1, 10, multistream, Selector.empty, quorum,
new NativeCall.ParsedCallDetails("eth_getFilterChanges", []), new NativeCall.ParsedCallDetails("eth_getFilterChanges", []),
new NativeCall.WithFilterIdDecorator(), new NativeCall.CreateFilterDecorator(), null, "reqId", 1) new NativeCall.WithFilterIdDecorator(), new NativeCall.CreateFilterDecorator(), null, false, "reqId", 1)
when: when:
def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1)) def resp = nativeCall.executeOnRemote(call).block(Duration.ofSeconds(1))

View File

@@ -108,7 +108,7 @@ class ApiReaderMock implements Reader<JsonRpcRequest, JsonRpcResponse> {
} }
error = new JsonRpcError(-32601, "Method ${request.method} with ${request.params} is not mocked") error = new JsonRpcError(-32601, "Method ${request.method} with ${request.params} is not mocked")
} }
return new JsonRpcResponse(result, error, JsonRpcResponse.Id.from(request.id), null, null) return new JsonRpcResponse(result, error, JsonRpcResponse.Id.from(request.id), null, null, null)
} as Callable<JsonRpcResponse> } as Callable<JsonRpcResponse>
return Mono.fromCallable(call) return Mono.fromCallable(call)
} }

View File

@@ -54,7 +54,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolver) Global.objectMapper.writeValueAsBytes(json), null, 1, resolver, null)
) )
} }
} }
@@ -81,7 +81,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes(null), null, 1, resolver Global.objectMapper.writeValueAsBytes(null), null, 1, resolver, null
) )
) )
} }
@@ -114,7 +114,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolver Global.objectMapper.writeValueAsBytes(json), null, 1, resolver, null
) )
) )
} }
@@ -146,7 +146,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getLogs", [Map.of("blockHash", hash1)])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getLogs", [Map.of("blockHash", hash1)])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes([json]), null, 1, resolver Global.objectMapper.writeValueAsBytes([json]), null, 1, resolver, null
) )
) )
} }
@@ -179,7 +179,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolver Global.objectMapper.writeValueAsBytes(json), null, 1, resolver, null
) )
) )
} }
@@ -212,7 +212,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolver Global.objectMapper.writeValueAsBytes(json), null, 1, resolver, null
) )
) )
} }
@@ -246,7 +246,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolver Global.objectMapper.writeValueAsBytes(json), null, 1, resolver, null
) )
) )
} }
@@ -271,7 +271,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes(null), null, 1, resolver Global.objectMapper.writeValueAsBytes(null), null, 1, resolver, null
) )
) )
} }
@@ -301,7 +301,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes("0x100"), null, 1, resolver Global.objectMapper.writeValueAsBytes("0x100"), null, 1, resolver, null
) )
) )
} }
@@ -332,7 +332,7 @@ class EthereumDirectReaderSpec extends Specification {
1 * create(_) >> Mock(RpcReader) { 1 * create(_) >> Mock(RpcReader) {
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "0xa8c9bb"])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBalance", [address1, "0xa8c9bb"])) >> Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes("0x100"), null, 1, resolver Global.objectMapper.writeValueAsBytes("0x100"), null, 1, resolver, null
) )
) )
} }
@@ -361,7 +361,7 @@ class EthereumDirectReaderSpec extends Specification {
} }
def result = Mono.just( def result = Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolver) Global.objectMapper.writeValueAsBytes(json), null, 1, resolver, null)
) )
EthereumDirectReader ethereumDirectReader = new EthereumDirectReader( EthereumDirectReader ethereumDirectReader = new EthereumDirectReader(
Stub(Multistream), Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() Stub(Multistream), Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
@@ -401,7 +401,7 @@ class EthereumDirectReaderSpec extends Specification {
} }
def result = Mono.just( def result = Mono.just(
new RpcReader.Result( new RpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolver) Global.objectMapper.writeValueAsBytes(json), null, 1, resolver, null)
) )
EthereumDirectReader ethereumDirectReader = new EthereumDirectReader( EthereumDirectReader ethereumDirectReader = new EthereumDirectReader(
Stub(Multistream), Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() Stub(Multistream), Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()

View File

@@ -335,7 +335,7 @@ class GenericWsHeadSpec extends Specification {
1 * it.subscribe(_) >> new WsSubscriptions.SubscribeData( 1 * it.subscribe(_) >> new WsSubscriptions.SubscribeData(
Flux.error(new RuntimeException()), "id", new AtomicReference<String>(subId) Flux.error(new RuntimeException()), "id", new AtomicReference<String>(subId)
) )
1 * it.unsubscribe(new JsonRpcRequest("eth_unsubscribe", List.of(subId), 2, null, null)) >> 1 * it.unsubscribe(new JsonRpcRequest("eth_unsubscribe", List.of(subId), 2, null, null, false)) >>
Mono.just(new JsonRpcResponse("".bytes, null)) Mono.just(new JsonRpcResponse("".bytes, null))
} }

View File

@@ -58,7 +58,7 @@ class WsConnectionImplSpec 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.callRpc(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null)) def act = ws.callRpc(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null, false))
then: then:
StepVerifier.create(act) StepVerifier.create(act)
@@ -90,7 +90,7 @@ class WsConnectionImplSpec 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.callRpc(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null)) def act = ws.callRpc(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null, false))
then: then:
StepVerifier.create(act) StepVerifier.create(act)
@@ -124,7 +124,7 @@ class WsConnectionImplSpec 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.callRpc(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null)) def act = ws.callRpc(new JsonRpcRequest("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], 15, null, null, false))
then: then:
StepVerifier.create(act) StepVerifier.create(act)

View File

@@ -15,7 +15,7 @@
*/ */
package io.emeraldpay.dshackle.upstream.rpcclient package io.emeraldpay.dshackle.upstream.rpcclient
import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.etherjar.rpc.RpcResponseError import io.emeraldpay.etherjar.rpc.RpcResponseError
import io.micrometer.core.instrument.Counter import io.micrometer.core.instrument.Counter
@@ -23,9 +23,7 @@ import io.micrometer.core.instrument.Timer
import org.mockserver.integration.ClientAndServer import org.mockserver.integration.ClientAndServer
import org.mockserver.model.HttpRequest import org.mockserver.model.HttpRequest
import org.mockserver.model.HttpResponse import org.mockserver.model.HttpResponse
import org.mockserver.model.MediaType
import org.springframework.util.SocketUtils import org.springframework.util.SocketUtils
import reactor.test.StepVerifier
import spock.lang.Specification import spock.lang.Specification
import java.time.Duration import java.time.Duration
@@ -69,35 +67,6 @@ class JsonRpcHttpClientSpec extends Specification {
new String(act.result) == '"0x98de45"' new String(act.result) == '"0x98de45"'
} }
def "Make request with basic auth"() {
setup:
def auth = new AuthConfig.ClientBasicAuth("user", "passwd")
def client = new JsonRpcHttpClient("localhost:${port}", metrics, auth, null)
mockServer.when(
HttpRequest.request()
.withMethod("POST")
.withBody("ping")
).respond(
HttpResponse.response()
.withBody("pong")
)
when:
def act = client.execute("ping".bytes).map { new String(it.t2) }
then:
StepVerifier.create(act)
.expectNext("pong")
.expectComplete()
.verify(Duration.ofSeconds(1))
mockServer.verify(
HttpRequest.request()
.withMethod("POST")
.withBody("ping")
.withContentType(MediaType.APPLICATION_JSON)
.withHeader("authorization", "Basic dXNlcjpwYXNzd2Q=")
)
}
def "Produces RPC Exception on error status code"() { def "Produces RPC Exception on error status code"() {
setup: setup:
def client = new JsonRpcHttpClient("localhost:${port}", metrics, null, null) def client = new JsonRpcHttpClient("localhost:${port}", metrics, null, null)

View File

@@ -63,7 +63,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and null result"() { def "Serialize int id and null result"() {
setup: setup:
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.NumberId(1), null, null) def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.NumberId(1), null, null, null)
when: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)
then: then:
@@ -72,7 +72,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and string result"() { def "Serialize int id and string result"() {
setup: setup:
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.NumberId(10), null, null) def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.NumberId(10), null, null, null)
when: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)
then: then:
@@ -81,7 +81,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and object result"() { def "Serialize int id and object result"() {
setup: setup:
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.NumberId(101), null, null) def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.NumberId(101), null, null, null)
when: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)
then: then:
@@ -90,7 +90,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize int id and error"() { def "Serialize int id and error"() {
setup: setup:
def json = new JsonRpcResponse(null, new JsonRpcError(-32041, "Oooops"), new JsonRpcResponse.NumberId(101), null, null) def json = new JsonRpcResponse(null, new JsonRpcError(-32041, "Oooops"), new JsonRpcResponse.NumberId(101), null, null, null)
when: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)
then: then:
@@ -99,7 +99,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize string id and null result"() { def "Serialize string id and null result"() {
setup: setup:
def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.StringId("asf01t1gg"), null, null) def json = new JsonRpcResponse("null".bytes, null, new JsonRpcResponse.StringId("asf01t1gg"), null, null, null)
when: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)
then: then:
@@ -108,7 +108,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize string id and string result"() { def "Serialize string id and string result"() {
setup: setup:
def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.StringId("10"), null, null) def json = new JsonRpcResponse('"Hello World"'.bytes, null, new JsonRpcResponse.StringId("10"), null, null, null)
when: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)
then: then:
@@ -117,7 +117,7 @@ class JsonRpcResponseSpec extends Specification {
def "Serialize string id and object result"() { def "Serialize string id and object result"() {
setup: setup:
def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.StringId("g8gk19g"), null, null) def json = new JsonRpcResponse('{"foo": "Hello World", "bar": 1}'.bytes, null, new JsonRpcResponse.StringId("g8gk19g"), null, null, null)
when: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)
then: then:
@@ -128,7 +128,7 @@ class JsonRpcResponseSpec extends Specification {
setup: setup:
def json = new JsonRpcResponse(null, def json = new JsonRpcResponse(null,
new JsonRpcError(-32041, "Oooops"), new JsonRpcError(-32041, "Oooops"),
new JsonRpcResponse.StringId("9kbo29gkaasf"), null, null) new JsonRpcResponse.StringId("9kbo29gkaasf"), null, null, null )
when: when:
def act = objectMapper.writeValueAsString(json) def act = objectMapper.writeValueAsString(json)
then: then:

View File

@@ -1,175 +0,0 @@
package io.emeraldpay.dshackle.rpc
import com.fasterxml.jackson.databind.JsonNode
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.BlockchainOuterClass.NativeCallRequest
import io.emeraldpay.dshackle.Global
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.springframework.util.ResourceUtils
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.test.StepVerifier
import java.time.Duration
class NativeCallStreamTest {
private val upstreamId = "upstreamId"
private val mapper = Global.objectMapper
@Test
fun `streaming response is equal to the original response`() {
val responseFile = ResourceUtils.getFile("classpath:responses/get-by-number-response.json")
val response = mapper.writeValueAsBytes(mapper.readValue(responseFile, JsonNode::class.java))
val nativeCallResponse = BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(true)
.setUpstreamId(upstreamId)
.setPayload(ByteString.copyFrom(response))
.build()
val nativeCallMock = mock<NativeCall> {
on { nativeCall(any()) } doReturn Flux.just(nativeCallResponse)
}
val nativeCallStream = NativeCallStream(nativeCallMock)
val req = Mono.just(
NativeCallRequest.newBuilder()
.setChunkSize(1000)
.build(),
)
val result = nativeCallStream.nativeCall(req)
.collectList()
.block()!!
.map { it.payload.toByteArray() }
.reduce { acc, bytes -> acc.plus(bytes) }
assertTrue(response.contentEquals(result))
}
@Test
fun `streaming responses is correct`() {
val response = "\"0x1126938\"".toByteArray()
val nativeCallResponse = BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(15)
.setSucceed(true)
.setUpstreamId(upstreamId)
.setPayload(ByteString.copyFrom(response))
.build()
val nativeCallMock = mock<NativeCall> {
on { nativeCall(any()) } doReturn Flux.just(nativeCallResponse)
}
val nativeCallStream = NativeCallStream(nativeCallMock)
val req = Mono.just(
NativeCallRequest.newBuilder()
.setChunkSize(5)
.build(),
)
val chunkResponse: (Int) -> BlockchainOuterClass.NativeCallReplyItem.Builder = { id ->
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(id)
.setChunked(true)
.setSucceed(true)
.setUpstreamId(upstreamId)
}
val result = nativeCallStream.nativeCall(req)
StepVerifier.create(result)
.expectNext(
chunkResponse(15)
.setPayload(ByteString.copyFrom("\"0x11".toByteArray()))
.build(),
)
.expectNext(
chunkResponse(15)
.setPayload(ByteString.copyFrom("26938".toByteArray()))
.build(),
)
.expectNext(
chunkResponse(15)
.setFinalChunk(true)
.setPayload(ByteString.copyFrom("\"".toByteArray()))
.build(),
)
.expectComplete()
.verify(Duration.ofSeconds(3))
}
@Test
fun `no streaming if response is too small`() {
val response = "\"0x1\"".toByteArray()
val nativeCallResponse = BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(15)
.setSucceed(true)
.setUpstreamId(upstreamId)
.setPayload(ByteString.copyFrom(response))
.build()
val nativeCallMock = mock<NativeCall> {
on { nativeCall(any()) } doReturn Flux.just(nativeCallResponse)
}
val nativeCallStream = NativeCallStream(nativeCallMock)
val req = Mono.just(
NativeCallRequest.newBuilder()
.setChunkSize(1000)
.build(),
)
val result = nativeCallStream.nativeCall(req)
StepVerifier.create(result)
.expectNext(
nativeCallResponse,
)
.expectComplete()
.verify(Duration.ofSeconds(3))
}
@Test
fun `sort responses by request id is correct`() {
val response = "\"0x1\"".toByteArray()
val response2 = "\"0x2\"".toByteArray()
val response3 = "\"0x3\"".toByteArray()
val nativeCallResponse: (Int, ByteArray) -> BlockchainOuterClass.NativeCallReplyItem = { id, resp ->
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(id)
.setChunked(true)
.setSucceed(true)
.setUpstreamId(upstreamId)
.setPayload(ByteString.copyFrom(resp))
.build()
}
val nativeCallMock = mock<NativeCall> {
on { nativeCall(any()) } doReturn Flux.just(
nativeCallResponse(1, response),
nativeCallResponse(2, response2),
nativeCallResponse(3, response3),
).flatMap {
when (it.id) {
1 -> Mono.just(it).delayElement(Duration.ofMillis(200))
2 -> Mono.just(it).delayElement(Duration.ofMillis(100))
else -> Mono.just(it)
}
}
}
val nativeCallStream = NativeCallStream(nativeCallMock)
val req = Mono.just(
NativeCallRequest.newBuilder()
.setSorted(true)
.build(),
)
val result = nativeCallStream.nativeCall(req)
StepVerifier.create(result)
.expectNextMatches { it.payload.toByteArray().contentEquals(response) }
.expectNextMatches { it.payload.toByteArray().contentEquals(response2) }
.expectNextMatches { it.payload.toByteArray().contentEquals(response3) }
.expectComplete()
.verify(Duration.ofSeconds(3))
}
}

View File

@@ -0,0 +1,131 @@
package io.emeraldpay.dshackle.upstream.rpcclient.stream
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
import java.time.Duration
class JsonRpcStreamParserTest {
private val streamParser = JsonRpcStreamParser()
@Test
fun `if first part couldn't be parsed then aggregate response`() {
val statusCode = 200
val bytes = "{\"strangeResponse\": 2}".toByteArray()
val stream: Flux<ByteArray> = Flux.just(bytes)
StepVerifier.create(streamParser.streamParse(statusCode, stream))
.expectNext(AggregateResponse(bytes, statusCode))
.expectComplete()
.verify(Duration.ofSeconds(1))
}
@ParameterizedTest
@MethodSource("data")
fun `if first part has result field then single response`(
response: ByteArray,
result: ByteArray,
) {
val statusCode = 200
val stream: Flux<ByteArray> = Flux.just(response)
StepVerifier.create(streamParser.streamParse(statusCode, stream))
.expectNext(SingleResponse(result, null))
.expectComplete()
.verify(Duration.ofSeconds(1))
}
@ParameterizedTest
@MethodSource("dataStream")
fun `if big result then stream response`(
response: List<ByteArray>,
chunks: List<Chunk>,
) {
val statusCode = 200
val stream: Flux<ByteArray> = Flux.fromIterable(response)
val result = streamParser.streamParse(statusCode, stream).block()
assertTrue(result is StreamResponse)
assertNotNull(result)
StepVerifier.create((result as StreamResponse).stream)
.expectNextSequence(chunks)
.expectComplete()
.verify(Duration.ofSeconds(5))
}
companion object {
@JvmStatic
fun data(): List<Arguments> = listOf(
Arguments.of("{\"id\": 2,\"result\": \"0x12\"}".toByteArray(), "\"0x12\"".toByteArray()),
Arguments.of("{\"id\": 2,\"result\": 11}".toByteArray(), "11".toByteArray()),
Arguments.of("{\"id\": 2,\"result\": false}".toByteArray(), "false".toByteArray()),
Arguments.of("{\"id\": 2,\"result\": null}".toByteArray(), "null".toByteArray()),
Arguments.of("{\"id\": 2,\"result\": {\"name\": \"value\"}".toByteArray(), "{\"name\": \"value\"}".toByteArray()),
Arguments.of("{\"id\": 2,\"result\": [{\"name\": \"value\"}]".toByteArray(), "[{\"name\": \"value\"}]".toByteArray()),
)
@JvmStatic
fun dataStream(): List<Arguments> = listOf(
Arguments.of(
listOf("{\"id\": 2,\"result\": \"0x12".toByteArray(), "222\"}".toByteArray()),
listOf(
Chunk("\"0x12".toByteArray(), false),
Chunk("222\"".toByteArray(), true),
),
),
Arguments.of(
listOf(
"{\"id\": 2,\"result\": \"0x12".toByteArray(),
"123\\\"".toByteArray(),
"222\"}".toByteArray(),
),
listOf(
Chunk("\"0x12".toByteArray(), false),
Chunk("123\\\"".toByteArray(), false),
Chunk("222\"".toByteArray(), true),
),
),
Arguments.of(
listOf(
"{\"id\": 2,\"result\": \"0x12".toByteArray(),
"1\\n23\\\"".toByteArray(),
"456\\".toByteArray(),
"\\222\\\\\\\\\"}".toByteArray(),
),
listOf(
Chunk("\"0x12".toByteArray(), false),
Chunk("1\\n23\\\"".toByteArray(), false),
Chunk("456\\".toByteArray(), false),
Chunk("\\222\\\\\\\\\"".toByteArray(), true),
),
),
Arguments.of(
listOf("{\"id\": 2,\"result\": {\"name\": ".toByteArray(), "\"bigName\"".toByteArray(), "}".toByteArray()),
listOf(
Chunk("{\"name\": ".toByteArray(), false),
Chunk("\"bigName\"".toByteArray(), false),
Chunk("}".toByteArray(), true),
),
),
Arguments.of(
listOf(
"{\"id\": 2,\"result\": [{\"name\": ".toByteArray(),
"\"bigName\"".toByteArray(),
"}],".toByteArray(),
"\"field\": \"value\"}".toByteArray(),
),
listOf(
Chunk("[{\"name\": ".toByteArray(), false),
Chunk("\"bigName\"".toByteArray(), false),
Chunk("}]".toByteArray(), true),
),
),
)
}
}