Switch no response error to null or separate errors (#210)

This commit is contained in:
KirillPamPam
2023-05-22 17:57:32 +04:00
committed by GitHub
parent 72c4f1192b
commit 4f21cb995d
22 changed files with 779 additions and 151 deletions

View File

@@ -9,6 +9,7 @@ const val SPAN_REQUEST_API_TYPE = "request.api.type"
const val SPAN_REQUEST_UPSTREAM_ID = "request.upstreamId" const val SPAN_REQUEST_UPSTREAM_ID = "request.upstreamId"
const val SPAN_RESPONSE_UPSTREAM_ID = "response.upstreamId" const val SPAN_RESPONSE_UPSTREAM_ID = "response.upstreamId"
const val SPAN_REQUEST_ID = "request.id" const val SPAN_REQUEST_ID = "request.id"
const val SPAN_NO_RESPONSE_MESSAGE = "no-response.message"
const val LOCAL_READER = "localReader" const val LOCAL_READER = "localReader"
const val REMOTE_QUORUM_RPC_READER = "remoteQuorumRpcReader" const val REMOTE_QUORUM_RPC_READER = "remoteQuorumRpcReader"

View File

@@ -5,14 +5,14 @@ import brave.handler.SpanHandler
import brave.propagation.TraceContext import brave.propagation.TraceContext
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import io.emeraldpay.dshackle.commons.SPAN_ERROR
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.cloud.sleuth.Span import org.springframework.cloud.sleuth.Span
import java.time.Duration import java.time.Duration
class ErrorSpanHandler( class ProviderSpanHandler(
@Qualifier("spanMapper") @Qualifier("spanMapper")
private val spanMapper: ObjectMapper, private val spanMapper: ObjectMapper,
private val spanExportableList: List<SpanExportable>
) : SpanHandler() { ) : SpanHandler() {
private val spans = Caffeine private val spans = Caffeine
.newBuilder() .newBuilder()
@@ -48,7 +48,7 @@ class ErrorSpanHandler(
} }
} }
return if (spansInfo.hasError) { return if (spansInfo.exportable) {
spanMapper.writeValueAsString(spansInfo.spans) spanMapper.writeValueAsString(spansInfo.spans)
} else { } else {
"" ""
@@ -68,13 +68,13 @@ class ErrorSpanHandler(
private fun processSpanInfo(span: MutableSpan, spansInfo: SpansInfo) { private fun processSpanInfo(span: MutableSpan, spansInfo: SpansInfo) {
spansInfo.spans.add(span) spansInfo.spans.add(span)
if (span.tags().containsKey(SPAN_ERROR)) { if (spanExportableList.any { it.isExportable(span) }) {
spansInfo.hasError = true spansInfo.exportable = true
} }
} }
private data class SpansInfo( private data class SpansInfo(
var hasError: Boolean = false, var exportable: Boolean = false,
val spans: MutableList<MutableSpan> = mutableListOf() val spans: MutableList<MutableSpan> = mutableListOf()
) )
} }

View File

@@ -9,7 +9,7 @@ import org.springframework.cloud.sleuth.Tracer
class ServerSpansInterceptor( class ServerSpansInterceptor(
private val tracer: Tracer, private val tracer: Tracer,
private val errorSpanHandler: ErrorSpanHandler private val providerSpanHandler: ProviderSpanHandler
) : ServerInterceptor { ) : ServerInterceptor {
override fun <ReqT : Any?, RespT : Any?> interceptCall( override fun <ReqT : Any?, RespT : Any?> interceptCall(
call: ServerCall<ReqT, RespT>, call: ServerCall<ReqT, RespT>,
@@ -32,7 +32,7 @@ class ServerSpansInterceptor(
tracer.currentSpan()?.let { tracer.currentSpan()?.let {
val parentId = it.context().parentId() val parentId = it.context().parentId()
if (parentId != null) { if (parentId != null) {
val spans = errorSpanHandler.getErrorSpans(it.context().spanId(), it) val spans = providerSpanHandler.getErrorSpans(it.context().spanId(), it)
if (spans.isNotBlank()) { if (spans.isNotBlank()) {
headers.put(Metadata.Key.of(SPAN_HEADER, Metadata.ASCII_STRING_MARSHALLER), spans) headers.put(Metadata.Key.of(SPAN_HEADER, Metadata.ASCII_STRING_MARSHALLER), spans)
} }

View File

@@ -56,13 +56,14 @@ open class SpanConfig {
@Bean @Bean
open fun errorSpanHandler( open fun errorSpanHandler(
@Qualifier("spanMapper") @Qualifier("spanMapper")
spanMapper: ObjectMapper spanMapper: ObjectMapper,
): ErrorSpanHandler = ErrorSpanHandler(spanMapper) spanExportableList: List<SpanExportable>
): ProviderSpanHandler = ProviderSpanHandler(spanMapper, spanExportableList)
@Bean @Bean
open fun serverSpansInterceptor( open fun serverSpansInterceptor(
tracer: org.springframework.cloud.sleuth.Tracer, tracer: org.springframework.cloud.sleuth.Tracer,
errorSpanHandler: ErrorSpanHandler providerSpanHandler: ProviderSpanHandler
): ServerInterceptor = ServerSpansInterceptor(tracer, errorSpanHandler) ): ServerInterceptor = ServerSpansInterceptor(tracer, providerSpanHandler)
} }
} }

View File

@@ -0,0 +1,20 @@
package io.emeraldpay.dshackle.config.spans
import brave.handler.MutableSpan
import io.emeraldpay.dshackle.commons.SPAN_ERROR
import io.emeraldpay.dshackle.commons.SPAN_NO_RESPONSE_MESSAGE
import org.springframework.stereotype.Component
interface SpanExportable {
fun isExportable(span: MutableSpan): Boolean
}
@Component
class ErrorSpanExportable : SpanExportable {
override fun isExportable(span: MutableSpan): Boolean = span.tags().containsKey(SPAN_ERROR)
}
@Component
class NoResponseSpanExportable : SpanExportable {
override fun isExportable(span: MutableSpan): Boolean = span.tags().containsKey(SPAN_NO_RESPONSE_MESSAGE)
}

View File

@@ -20,6 +20,11 @@ import io.emeraldpay.dshackle.upstream.ApiSource
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.signature.ResponseSigner import io.emeraldpay.dshackle.upstream.signature.ResponseSigner
import org.springframework.cloud.sleuth.Tracer import org.springframework.cloud.sleuth.Tracer
import java.util.concurrent.atomic.AtomicInteger
interface QuorumReader : Reader<JsonRpcRequest, QuorumRpcReader.Result> {
fun attempts(): AtomicInteger
}
// creates instance of a Quorum based reader // creates instance of a Quorum based reader
interface QuorumReaderFactory { interface QuorumReaderFactory {
@@ -30,10 +35,10 @@ interface QuorumReaderFactory {
} }
} }
fun create(apis: ApiSource, quorum: CallQuorum, signer: ResponseSigner?, tracer: Tracer): Reader<JsonRpcRequest, QuorumRpcReader.Result> fun create(apis: ApiSource, quorum: CallQuorum, signer: ResponseSigner?, tracer: Tracer): QuorumReader
class Default : QuorumReaderFactory { class Default : QuorumReaderFactory {
override fun create(apis: ApiSource, quorum: CallQuorum, signer: ResponseSigner?, tracer: Tracer): Reader<JsonRpcRequest, QuorumRpcReader.Result> { override fun create(apis: ApiSource, quorum: CallQuorum, signer: ResponseSigner?, tracer: Tracer): QuorumReader {
return QuorumRpcReader(apis, quorum, signer, tracer) return QuorumRpcReader(apis, quorum, signer, tracer)
} }
} }

View File

@@ -15,10 +15,11 @@
*/ */
package io.emeraldpay.dshackle.quorum package io.emeraldpay.dshackle.quorum
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.commons.API_READER import io.emeraldpay.dshackle.commons.API_READER
import io.emeraldpay.dshackle.commons.SPAN_NO_RESPONSE_MESSAGE
import io.emeraldpay.dshackle.commons.SPAN_REQUEST_API_TYPE import io.emeraldpay.dshackle.commons.SPAN_REQUEST_API_TYPE
import io.emeraldpay.dshackle.commons.SPAN_REQUEST_UPSTREAM_ID import io.emeraldpay.dshackle.commons.SPAN_REQUEST_UPSTREAM_ID
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.reader.SpannedReader import io.emeraldpay.dshackle.reader.SpannedReader
import io.emeraldpay.dshackle.upstream.ApiSource import io.emeraldpay.dshackle.upstream.ApiSource
import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.Upstream
@@ -48,7 +49,7 @@ class QuorumRpcReader(
private val quorum: CallQuorum, private val quorum: CallQuorum,
private val signer: ResponseSigner?, private val signer: ResponseSigner?,
private val tracer: Tracer private val tracer: Tracer
) : Reader<JsonRpcRequest, QuorumRpcReader.Result> { ) : QuorumReader {
companion object { companion object {
private val log = LoggerFactory.getLogger(QuorumRpcReader::class.java) private val log = LoggerFactory.getLogger(QuorumRpcReader::class.java)
@@ -56,6 +57,8 @@ class QuorumRpcReader(
constructor(apiControl: ApiSource, quorum: CallQuorum, tracer: Tracer) : this(apiControl, quorum, null, tracer) constructor(apiControl: ApiSource, quorum: CallQuorum, tracer: Tracer) : this(apiControl, quorum, null, tracer)
override fun attempts(): AtomicInteger = apiControl.attempts()
override fun read(key: JsonRpcRequest): Mono<Result> { override fun read(key: JsonRpcRequest): Mono<Result> {
// needs at least one response, so start a request // needs at least one response, so start a request
apiControl.request(1) apiControl.request(1)
@@ -95,7 +98,7 @@ class QuorumRpcReader(
.transform(processResult(defaultResult)) .transform(processResult(defaultResult))
} }
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, Tuple4<ByteArray, Optional<ResponseSigner.Signature>, Upstream, Optional<String>>, CallQuorum> { res, a -> val quorumReduce = BiFunction<CallQuorum, Tuple4<ByteArray, Optional<ResponseSigner.Signature>, Upstream, Optional<String>>, CallQuorum> { res, a ->
if (res.record(a.t1, a.t2.orElse(null), a.t3, a.t4.orElse(null))) { if (res.record(a.t1, a.t2.orElse(null), a.t3, a.t4.orElse(null))) {
log.trace("Quorum is resolved for method ${key.method}") log.trace("Quorum is resolved for method ${key.method}")
@@ -122,7 +125,7 @@ class QuorumRpcReader(
} }
} }
fun processResult(defaultResult: Mono<Result>): Function<Mono<CallQuorum>, Mono<Result>> { private fun processResult(defaultResult: Mono<Result>): Function<Mono<CallQuorum>, Mono<Result>> {
return Function { quorumResult -> return Function { quorumResult ->
quorumResult quorumResult
.filter { it.isResolved() } // return nothing if not resolved .filter { it.isResolved() } // return nothing if not resolved
@@ -134,7 +137,7 @@ class QuorumRpcReader(
} }
} }
fun callApi(api: Upstream, key: JsonRpcRequest): Mono<Tuple4<ByteArray, Optional<ResponseSigner.Signature>, Upstream, Optional<String>>> { private fun callApi(api: Upstream, key: JsonRpcRequest): Mono<Tuple4<ByteArray, Optional<ResponseSigner.Signature>, Upstream, Optional<String>>> {
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,
@@ -152,7 +155,7 @@ class QuorumRpcReader(
.map { Tuples.of(it.t1, it.t2, api, it.t3) } .map { Tuples.of(it.t1, it.t2, api, it.t3) }
} }
fun withSignatureAndUpstream(api: Upstream, key: JsonRpcRequest, response: JsonRpcResponse): Function<Mono<ByteArray>, Mono<Tuple3<ByteArray, Optional<ResponseSigner.Signature>, Optional<String>>>> { private fun withSignatureAndUpstream(api: Upstream, key: JsonRpcRequest, response: JsonRpcResponse): Function<Mono<ByteArray>, Mono<Tuple3<ByteArray, Optional<ResponseSigner.Signature>, Optional<String>>>> {
return Function { src -> return Function { src ->
src.map { src.map {
val signature = response.providedSignature val signature = response.providedSignature
@@ -166,7 +169,7 @@ class QuorumRpcReader(
} }
} }
fun <T> withErrorResume(api: Upstream, key: JsonRpcRequest): Function<Mono<T>, Mono<T>> { private fun <T> withErrorResume(api: Upstream, key: JsonRpcRequest): Function<Mono<T>, Mono<T>> {
return Function { src -> return Function { src ->
src.onErrorResume { err -> src.onErrorResume { err ->
log.error("Error during call upstream ${api.getId()} with method ${key.method}", err) log.error("Error during call upstream ${api.getId()} with method ${key.method}", err)
@@ -195,22 +198,33 @@ class QuorumRpcReader(
} }
} }
fun setupDefaultResult(key: JsonRpcRequest): Mono<Result> { private fun setupDefaultResult(key: JsonRpcRequest): Mono<Result> {
return Mono.just(quorum).flatMap { q -> return Mono.just(quorum).flatMap { q ->
if (q.isFailed()) { if (q.isFailed()) {
val err = q.getError()?.asException(JsonRpcResponse.NumberId(key.id)) val err = q.getError()?.asException(JsonRpcResponse.NumberId(key.id))
?: JsonRpcException(JsonRpcResponse.NumberId(key.id), JsonRpcError(-32603, "Unhandled Upstream error")) ?: JsonRpcException(JsonRpcResponse.NumberId(key.id), JsonRpcError(-32603, "Unhandled Upstream error"))
log.warn("Quorum is failed. Method ${key.method}, message ${err.message}") log.warn("Quorum is failed. Method ${key.method}, message ${err.message}")
Mono.error<Result>(err) Mono.error(err)
} else { } else {
log.warn("Did not get any result from upstream. Method [${key.method}] using [$q]") log.warn("Did not get any result from upstream. Method [${key.method}] using [$q]")
Mono.empty<Result>() noResponse(key.method, q)
} }
} }
} }
fun getValidAttemptsCount(): AtomicInteger = private fun noResponse(method: String, q: CallQuorum): Mono<Result> {
apiControl.attempts() return apiControl.upstreamsMatchesResponse()?.run {
tracer.currentSpan()?.tag(SPAN_NO_RESPONSE_MESSAGE, getFullCause())
val cause = getCause(method) ?: return Mono.empty()
if (cause.shouldReturnNull) {
Mono.just(
Result(Global.nullValue, null, 1, emptyList(), null)
)
} else {
Mono.error(RpcException(1, "No response for method $method. Cause - ${cause.cause}"))
}
} ?: Mono.error(RpcException(1, "Quorum [$q] is not resolved [isResolved - ${q.isResolved()}]"))
}
class Result( class Result(
val value: ByteArray, val value: ByteArray,

View File

@@ -66,7 +66,6 @@ import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono import reactor.kotlin.core.publisher.toMono
import reactor.util.context.Context import reactor.util.context.Context
import java.util.EnumMap import java.util.EnumMap
import java.util.concurrent.atomic.AtomicInteger
@Service @Service
open class NativeCall( open class NativeCall(
@@ -392,18 +391,16 @@ open class NativeCall(
return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method")) return Mono.error(RpcException(RpcResponseError.CODE_METHOD_NOT_EXIST, "Unsupported method"))
} }
val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum, signer, tracer) val reader = quorumReaderFactory.create(ctx.getApis(), ctx.callQuorum, signer, tracer)
val counter = if (reader is QuorumRpcReader) { val counter = reader.attempts()
reader.getValidAttemptsCount()
} else {
AtomicInteger(-1)
}
return SpannedReader(reader, tracer, REMOTE_QUORUM_RPC_READER) return SpannedReader(reader, tracer, REMOTE_QUORUM_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))
.map { .map {
val bytes = ctx.resultDecorator.processResult(it) val bytes = ctx.resultDecorator.processResult(it)
validateResult(bytes, "remote", ctx) validateResult(bytes, "remote", ctx)
CallResult.ok(ctx.id, ctx.nonce, bytes, it.signature, it.providedUpstreamId ?: it.resolvers.first().getId(), ctx) val upId = it.providedUpstreamId
?: if (it.resolvers.isEmpty()) ctx.upstream.getId() else it.resolvers.first().getId()
CallResult.ok(ctx.id, ctx.nonce, bytes, it.signature, upId, ctx)
} }
.onErrorResume { t -> .onErrorResume { t ->
Mono.just(CallResult.fail(ctx.id, ctx.nonce, t, ctx)) Mono.just(CallResult.fail(ctx.id, ctx.nonce, t, ctx))

View File

@@ -63,9 +63,11 @@ class TrackBitcoinAddress(
/** /**
* Criteria for a remote grpc upstream that can provide a balance * Criteria for a remote grpc upstream that can provide a balance
*/ */
private val balanceUpstreamMatcher = Selector.LocalAndMatcher( private val balanceUpstreamMatcher = Selector.MultiMatcher(
Selector.GrpcMatcher(), listOf(
Selector.CapabilityMatcher(Capability.BALANCE) Selector.GrpcMatcher(),
Selector.CapabilityMatcher(Capability.BALANCE)
)
) )
@EventListener @EventListener

View File

@@ -29,4 +29,6 @@ interface ApiSource : Publisher<Upstream> {
fun request(tries: Int) fun request(tries: Int)
fun attempts(): AtomicInteger fun attempts(): AtomicInteger
fun upstreamsMatchesResponse(): UpstreamsMatchesResponse?
} }

View File

@@ -49,6 +49,7 @@ class FilteredApis(
private val retryLimit: Long, private val retryLimit: Long,
jitter: Int jitter: Int
) : ApiSource { ) : ApiSource {
private val internalMatcher: Selector.Matcher
companion object { companion object {
private val log = LoggerFactory.getLogger(FilteredApis::class.java) private val log = LoggerFactory.getLogger(FilteredApis::class.java)
@@ -94,6 +95,7 @@ class FilteredApis(
private var started = false private var started = false
private val control = Sinks.many().unicast().onBackpressureBuffer<Boolean>() private val control = Sinks.many().unicast().onBackpressureBuffer<Boolean>()
private var upstreamsMatchesResponse: UpstreamsMatchesResponse? = UpstreamsMatchesResponse()
init { init {
delay = if (jitter > 0) { delay = if (jitter > 0) {
@@ -129,6 +131,9 @@ class FilteredApis(
monitoring.countFallback.record(fallbackUpstreams.size.toDouble()) monitoring.countFallback.record(fallbackUpstreams.size.toDouble())
} }
} }
internalMatcher = Selector.MultiMatcher(
listOf(Selector.AvailabilityMatcher(), matcher)
)
} }
private fun getMetrics(chain: Chain): Monitoring { private fun getMetrics(chain: Chain): Monitoring {
@@ -181,14 +186,16 @@ class FilteredApis(
} }
result.filter { up -> result.filter { up ->
(up.isAvailable() && matcher.matches(up)).also { val matchesResponse = internalMatcher.matchesWithCause(up)
if (it) { processMatchesResponse(up.getId(), matchesResponse)
counter.incrementAndGet() matchesResponse.matched()
}
}
} }
.zipWith(control.asFlux()) .zipWith(control.asFlux())
.map { it.t1 } .map {
upstreamsMatchesResponse = null
counter.incrementAndGet()
it.t1
}
.doOnSubscribe { .doOnSubscribe {
if (!started) { if (!started) {
// in addition to subscription the FilteredAPI should use request() method to prepare the control flow // in addition to subscription the FilteredAPI should use request() method to prepare the control flow
@@ -198,6 +205,14 @@ class FilteredApis(
.subscribe(subscriber) .subscribe(subscriber)
} }
private fun processMatchesResponse(upstreamId: String, matchesResponse: MatchesResponse) {
upstreamsMatchesResponse?.run {
if (!matchesResponse.matched()) {
addUpstreamMatchesResponse(upstreamId, matchesResponse)
}
}
}
override fun resolve() { override fun resolve() {
control.tryEmitComplete() control.tryEmitComplete()
} }
@@ -213,8 +228,10 @@ class FilteredApis(
override fun attempts(): AtomicInteger = override fun attempts(): AtomicInteger =
counter counter
override fun upstreamsMatchesResponse(): UpstreamsMatchesResponse? = upstreamsMatchesResponse
override fun toString(): String { override fun toString(): String {
return "Filter API: ${allUpstreams.size} upstreams with $matcher" return "Filter API: ${allUpstreams.size} upstreams with $internalMatcher"
} }
class Monitoring(chain: Chain) { class Monitoring(chain: Chain) {

View File

@@ -0,0 +1,83 @@
package io.emeraldpay.dshackle.upstream
sealed class MatchesResponse {
fun matched(): Boolean {
return when (this) {
is Success -> true
else -> false
}
}
fun getCause(): String? =
when (this) {
is LabelResponse -> "No label `${this.name}` with values ${this.values}"
AvailabilityResponse -> "Upstream is not available"
is CapabilityResponse -> "Upstream does not have capability ${this.capability}"
is ExistsResponse -> "Label ${this.name} does not exist"
GrpcResponse -> "Upstream is not grpc"
is HeightResponse -> "Upstream height ${this.currentHeight} is less than ${this.height}"
is MethodResponse -> "Method ${this.method} is not supported"
is MultiResponse ->
this.allResponses
.filter { it !is Success }
.joinToString("; ") { it.getCause()!! }
is NotMatchedResponse -> "Not matched - ${response.getCause()}"
is SameNodeResponse -> "Upstream does not have hash ${this.upstreamHash}"
else -> null
}
object Success : MatchesResponse()
data class LabelResponse(
val name: String,
val values: Collection<String>
) : MatchesResponse()
data class NotMatchedResponse(
val response: MatchesResponse
) : MatchesResponse()
data class MultiResponse(
private val responses: Set<MatchesResponse>
) : MatchesResponse() {
val allResponses = mutableSetOf<MatchesResponse>()
init {
responses.forEach {
if (it is MultiResponse) {
it.allResponses.forEach { resp ->
allResponses.add(resp)
}
} else {
allResponses.add(it)
}
}
}
}
data class MethodResponse(
val method: String
) : MatchesResponse()
data class ExistsResponse(
val name: String
) : MatchesResponse()
data class CapabilityResponse(
val capability: Capability
) : MatchesResponse()
object GrpcResponse : MatchesResponse()
data class HeightResponse(
val height: Long,
val currentHeight: Long
) : MatchesResponse()
data class SameNodeResponse(
val upstreamHash: Byte
) : MatchesResponse()
object AvailabilityResponse : MatchesResponse()
}

View File

@@ -18,6 +18,14 @@ package io.emeraldpay.dshackle.upstream
import io.emeraldpay.api.proto.BlockchainOuterClass import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.config.UpstreamsConfig import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.MatchesResponse.AvailabilityResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.CapabilityResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.ExistsResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.GrpcResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.HeightResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.NotMatchedResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.SameNodeResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.Success
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import java.util.Collections import java.util.Collections
@@ -121,17 +129,27 @@ class Selector {
} }
} }
interface Matcher { abstract class Matcher {
fun matches(up: Upstream): Boolean fun matches(up: Upstream): Boolean = matchesWithCause(up).matched()
fun describeInternal(): String abstract fun matchesWithCause(up: Upstream): MatchesResponse
abstract fun describeInternal(): String
} }
data class MultiMatcher( data class MultiMatcher(
private val matchers: Collection<Matcher> private val matchers: Collection<Matcher>
) : Matcher { ) : Matcher() {
override fun matches(up: Upstream): Boolean {
return matchers.all { it.matches(up) } override fun matchesWithCause(up: Upstream): MatchesResponse {
val responses = matchers.map { it.matchesWithCause(up) }
return if (responses.all { it is Success }) {
Success
} else {
MatchesResponse.MultiResponse(
responses.filter { it !is Success }.toSet()
)
}
} }
fun getMatchers(): Collection<Matcher> { fun getMatchers(): Collection<Matcher> {
@@ -158,10 +176,14 @@ class Selector {
data class MethodMatcher( data class MethodMatcher(
val method: String val method: String
) : Matcher { ) : Matcher() {
override fun matches(up: Upstream): Boolean {
return up.getMethods().isCallable(method) override fun matchesWithCause(up: Upstream): MatchesResponse =
} if (up.getMethods().isCallable(method)) {
Success
} else {
MatchesResponse.MethodResponse(method)
}
override fun describeInternal(): String { override fun describeInternal(): String {
return "allow method $method" return "allow method $method"
@@ -172,19 +194,26 @@ class Selector {
} }
} }
abstract class LabelSelectorMatcher : Matcher { abstract class LabelSelectorMatcher : Matcher() {
override fun matches(up: Upstream): Boolean {
return up.getLabels().any(this::matches) override fun matchesWithCause(up: Upstream): MatchesResponse {
val labelsResponses = up.getLabels().map { matchesWithCause(it) }
return if (labelsResponses.any { it is Success }) {
Success
} else {
MatchesResponse.MultiResponse(
labelsResponses.filter { it !is Success }.toSet()
)
}
} }
abstract fun matches(labels: UpstreamsConfig.Labels): Boolean abstract fun matchesWithCause(labels: UpstreamsConfig.Labels): MatchesResponse
abstract fun asProto(): BlockchainOuterClass.Selector? abstract fun asProto(): BlockchainOuterClass.Selector?
} }
class EmptyMatcher : Matcher { class EmptyMatcher : Matcher() {
override fun matches(up: Upstream): Boolean {
return true override fun matchesWithCause(up: Upstream): MatchesResponse = Success
}
override fun describeInternal(): String { override fun describeInternal(): String {
return "empty" return "empty"
@@ -197,18 +226,12 @@ class Selector {
class AnyLabelMatcher : LabelSelectorMatcher() { class AnyLabelMatcher : LabelSelectorMatcher() {
override fun matches(labels: UpstreamsConfig.Labels): Boolean { override fun matchesWithCause(labels: UpstreamsConfig.Labels): MatchesResponse = Success
return true
}
override fun asProto(): BlockchainOuterClass.Selector? { override fun asProto(): BlockchainOuterClass.Selector? {
return null return null
} }
override fun matches(up: Upstream): Boolean {
return true
}
override fun describeInternal(): String { override fun describeInternal(): String {
return "any label" return "any label"
} }
@@ -218,26 +241,22 @@ class Selector {
} }
} }
class LocalAndMatcher(vararg val matchers: Matcher) : Matcher { class LabelMatcher(
val name: String,
val values: Collection<String>
) : LabelSelectorMatcher() {
override fun matches(up: Upstream): Boolean { override fun matchesWithCause(labels: UpstreamsConfig.Labels): MatchesResponse {
return matchers.all { it.matches(up) } val response = labels[name]?.let { labelValue ->
}
override fun describeInternal(): String {
return "local upstream"
}
override fun toString(): String {
return "Matcher: ${describeInternal()}"
}
}
class LabelMatcher(val name: String, val values: Collection<String>) : LabelSelectorMatcher() {
override fun matches(labels: UpstreamsConfig.Labels): Boolean {
return labels.get(name)?.let { labelValue ->
values.any { it == labelValue } values.any { it == labelValue }
} ?: false } ?: false
return if (response) {
Success
} else {
MatchesResponse.LabelResponse(
name, values
)
}
} }
override fun asProto(): BlockchainOuterClass.Selector { override fun asProto(): BlockchainOuterClass.Selector {
@@ -257,9 +276,19 @@ class Selector {
} }
} }
class OrMatcher(val matchers: Collection<LabelSelectorMatcher>) : LabelSelectorMatcher() { class OrMatcher(
override fun matches(labels: UpstreamsConfig.Labels): Boolean { val matchers: Collection<LabelSelectorMatcher>,
return matchers.any { matcher -> matcher.matches(labels) } ) : LabelSelectorMatcher() {
override fun matchesWithCause(labels: UpstreamsConfig.Labels): MatchesResponse {
val responses = matchers.map { it.matchesWithCause(labels) }
return if (responses.any { it is Success }) {
Success
} else {
MatchesResponse.MultiResponse(
responses.filter { it !is Success }.toSet()
)
}
} }
override fun asProto(): BlockchainOuterClass.Selector { override fun asProto(): BlockchainOuterClass.Selector {
@@ -279,9 +308,19 @@ class Selector {
} }
} }
class AndMatcher(val matchers: Collection<LabelSelectorMatcher>) : LabelSelectorMatcher() { class AndMatcher(
override fun matches(labels: UpstreamsConfig.Labels): Boolean { val matchers: Collection<LabelSelectorMatcher>
return matchers.all { matcher -> matcher.matches(labels) } ) : LabelSelectorMatcher() {
override fun matchesWithCause(labels: UpstreamsConfig.Labels): MatchesResponse {
val responses = matchers.map { it.matchesWithCause(labels) }
return if (responses.all { it is Success }) {
Success
} else {
MatchesResponse.MultiResponse(
responses.filter { it !is Success }.toSet()
)
}
} }
override fun asProto(): BlockchainOuterClass.Selector { override fun asProto(): BlockchainOuterClass.Selector {
@@ -301,9 +340,17 @@ class Selector {
} }
} }
class NotMatcher(val matcher: LabelSelectorMatcher) : LabelSelectorMatcher() { class NotMatcher(
override fun matches(labels: UpstreamsConfig.Labels): Boolean { val matcher: LabelSelectorMatcher
return !matcher.matches(labels) ) : LabelSelectorMatcher() {
override fun matchesWithCause(labels: UpstreamsConfig.Labels): MatchesResponse {
val response = matcher.matchesWithCause(labels)
return if (response !is Success) {
Success
} else {
NotMatchedResponse(response)
}
} }
override fun asProto(): BlockchainOuterClass.Selector { override fun asProto(): BlockchainOuterClass.Selector {
@@ -323,10 +370,16 @@ class Selector {
} }
} }
class ExistsMatcher(val name: String) : LabelSelectorMatcher() { class ExistsMatcher(
override fun matches(labels: UpstreamsConfig.Labels): Boolean { val name: String
return labels.containsKey(name) ) : LabelSelectorMatcher() {
}
override fun matchesWithCause(labels: UpstreamsConfig.Labels): MatchesResponse =
if (labels.containsKey(name)) {
Success
} else {
ExistsResponse(name)
}
override fun asProto(): BlockchainOuterClass.Selector { override fun asProto(): BlockchainOuterClass.Selector {
return BlockchainOuterClass.Selector.newBuilder().setExistsSelector( return BlockchainOuterClass.Selector.newBuilder().setExistsSelector(
@@ -345,10 +398,14 @@ class Selector {
} }
} }
class CapabilityMatcher(val capability: Capability) : Matcher { class CapabilityMatcher(val capability: Capability) : Matcher() {
override fun matches(up: Upstream): Boolean {
return up.getCapabilities().contains(capability) override fun matchesWithCause(up: Upstream): MatchesResponse =
} if (up.getCapabilities().contains(capability)) {
Success
} else {
CapabilityResponse(capability)
}
override fun describeInternal(): String { override fun describeInternal(): String {
return "provides $capability API" return "provides $capability API"
@@ -359,10 +416,14 @@ class Selector {
} }
} }
class GrpcMatcher : Matcher { class GrpcMatcher : Matcher() {
override fun matches(up: Upstream): Boolean {
return up.isGrpc() override fun matchesWithCause(up: Upstream): MatchesResponse =
} if (up.isGrpc()) {
Success
} else {
GrpcResponse
}
override fun describeInternal(): String { override fun describeInternal(): String {
return "is gRPC" return "is gRPC"
@@ -373,9 +434,15 @@ class Selector {
} }
} }
class HeightMatcher(val height: Long) : Matcher { class HeightMatcher(val height: Long) : Matcher() {
override fun matches(up: Upstream): Boolean {
return (up.getHead().getCurrentHeight() ?: 0) >= height override fun matchesWithCause(up: Upstream): MatchesResponse {
val currentHeight = up.getHead().getCurrentHeight() ?: 0
return if (currentHeight >= height) {
Success
} else {
HeightResponse(height, currentHeight)
}
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -400,9 +467,14 @@ class Selector {
} }
} }
class SameNodeMatcher(private val upstreamHash: Byte) : Matcher { class SameNodeMatcher(private val upstreamHash: Byte) : Matcher() {
override fun matches(up: Upstream): Boolean =
up.nodeId() == upstreamHash override fun matchesWithCause(up: Upstream): MatchesResponse =
if (up.nodeId() == upstreamHash) {
Success
} else {
SameNodeResponse(upstreamHash)
}
override fun describeInternal(): String = override fun describeInternal(): String =
"upstream node-id=${upstreamHash.toUByte()}" "upstream node-id=${upstreamHash.toUByte()}"
@@ -417,4 +489,17 @@ class Selector {
return other.upstreamHash == upstreamHash return other.upstreamHash == upstreamHash
} }
} }
class AvailabilityMatcher : Matcher() {
override fun matchesWithCause(up: Upstream): MatchesResponse =
if (up.isAvailable()) {
Success
} else {
AvailabilityResponse
}
override fun describeInternal(): String = "availability"
override fun toString(): String = "Matcher: ${describeInternal()}"
}
} }

View File

@@ -0,0 +1,99 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.dshackle.upstream.MatchesResponse.HeightResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.MultiResponse
import kotlin.math.min
class UpstreamsMatchesResponse {
companion object {
private val possibleNullReturnMethods = listOf(
"eth_getTransactionByHash",
"eth_getTransactionReceipt",
"eth_getBlockByHash",
"eth_getBlockByNumber",
"eth_getTransactionByBlockHashAndIndex",
"eth_getTransactionByBlockNumberAndIndex",
"eth_getUncleByBlockHashAndIndex",
"eth_getUncleByBlockNumberAndIndex"
)
}
private val responses = LinkedHashSet<UpstreamNotMatchedResponse>()
fun addUpstreamMatchesResponse(upstreamId: String, response: MatchesResponse) {
if (!response.matched()) {
responses.add(UpstreamNotMatchedResponse(upstreamId, response))
}
}
fun getFullCause(): String? =
if (responses.isEmpty()) {
null
} else {
responses
.joinToString("; ") { "${it.upstreamId} - ${it.matchesResponse.getCause()}" }
.run {
substring(0, min(200, this.length))
}
}
fun getCause(method: String): NotMatchesCause? {
if (responses.isEmpty()) {
return null
}
val commonMatchesResponse = hasCommonMatchesResponse()
return if (commonMatchesResponse is HeightResponse && possibleNullReturnMethods.contains(method)) {
NotMatchesCause(true)
} else if (commonMatchesResponse != null && commonMatchesResponse !is HeightResponse) {
NotMatchesCause(false, commonMatchesResponse.getCause())
} else {
null
}
}
private fun hasCommonMatchesResponse(): MatchesResponse? {
val matchesResponses = responses.map { it.matchesResponses }
val commonResponses = mutableListOf<MatchesResponse>()
for (matchesResponse in matchesResponses[0]) {
if (matchesResponses.all { it.any { resp -> matchesResponse.javaClass == resp.javaClass } }) {
commonResponses.add(matchesResponse)
}
}
if (commonResponses.isEmpty()) {
return null
}
val heightResponse = commonResponses.find { it is HeightResponse }
return heightResponse ?: commonResponses[0]
}
data class NotMatchesCause(
val shouldReturnNull: Boolean,
val cause: String? = null
)
private class UpstreamNotMatchedResponse(
val upstreamId: String,
val matchesResponse: MatchesResponse
) {
val matchesResponses: Set<MatchesResponse> = when (matchesResponse) {
is MultiResponse -> matchesResponse.allResponses
else -> setOf(matchesResponse)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as UpstreamNotMatchedResponse
if (upstreamId != other.upstreamId) return false
return true
}
override fun hashCode(): Int {
return upstreamId.hashCode()
}
}
}

View File

@@ -17,9 +17,11 @@ class RemoteUnspentReader(
private val log = LoggerFactory.getLogger(RemoteUnspentReader::class.java) private val log = LoggerFactory.getLogger(RemoteUnspentReader::class.java)
} }
private val selector = Selector.LocalAndMatcher( private val selector = Selector.MultiMatcher(
Selector.GrpcMatcher(), listOf(
Selector.CapabilityMatcher(Capability.BALANCE) Selector.GrpcMatcher(),
Selector.CapabilityMatcher(Capability.BALANCE)
)
) )
override fun read(key: Address): Mono<List<SimpleUnspent>> { override fun read(key: Address): Mono<List<SimpleUnspent>> {

View File

@@ -289,4 +289,33 @@ class QuorumRpcReaderSpec extends Specification {
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
} }
def "Error if no upstreams"() {
setup:
def api = Stub(Reader)
def up = Mock(Upstream) {
_ * getId() >> "id1"
_ * isAvailable() >> false
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * getIngressReader() >> api
}
def apis = new FilteredApis(
Chain.ETHEREUM,
[up], Selector.empty
)
def reader = new QuorumRpcReader(apis, new AlwaysQuorum(), Stub(Tracer))
when:
def act = reader.read(new JsonRpcRequest("eth_test", []))
.map {
new String(it.value)
}
then:
StepVerifier.create(act)
.expectErrorMatches { t ->
t instanceof RpcException && t.rpcMessage == "No response for method eth_test. Cause - Upstream is not available" && t.error.code == 1
}
.verify(Duration.ofSeconds(4))
}
} }

View File

@@ -25,6 +25,7 @@ import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.config.CacheConfig import io.emeraldpay.dshackle.config.CacheConfig
import io.emeraldpay.dshackle.config.MainConfig import io.emeraldpay.dshackle.config.MainConfig
import io.emeraldpay.dshackle.quorum.AlwaysQuorum import io.emeraldpay.dshackle.quorum.AlwaysQuorum
import io.emeraldpay.dshackle.quorum.QuorumReader
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
import io.emeraldpay.dshackle.quorum.QuorumRpcReader import io.emeraldpay.dshackle.quorum.QuorumRpcReader
import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.reader.Reader
@@ -52,6 +53,7 @@ import spock.lang.Ignore
import spock.lang.Specification import spock.lang.Specification
import java.time.Duration import java.time.Duration
import java.util.concurrent.atomic.AtomicInteger
class NativeCallSpec extends Specification { class NativeCallSpec extends Specification {
@@ -131,7 +133,7 @@ class NativeCallSpec extends Specification {
def nativeCall = nativeCall() def nativeCall = nativeCall()
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) { nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, null, 1, Collections.singletonList(ups), null)) 1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"foo\"".bytes, null, 1, Collections.singletonList(ups), null))
} }
} }
@@ -152,7 +154,8 @@ class NativeCallSpec extends Specification {
def nativeCall = nativeCall() def nativeCall = nativeCall()
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) { nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * attempts() >> new AtomicInteger(1)
1 * read(new JsonRpcRequest("eth_test", [], 10)) >> Mono.empty() 1 * read(new JsonRpcRequest("eth_test", [], 10)) >> Mono.empty()
} }
} }
@@ -176,7 +179,7 @@ class NativeCallSpec extends Specification {
def nativeCall = nativeCall() def nativeCall = nativeCall()
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) { nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_test", [], 10)) >> Mono.error( 1 * read(new JsonRpcRequest("eth_test", [], 10)) >> Mono.error(
new JsonRpcException(JsonRpcResponse.Id.from(12), new JsonRpcError(-32123, "Foo Bar", "Foo Bar Baz"), true) new JsonRpcException(JsonRpcResponse.Id.from(12), new JsonRpcError(-32123, "Foo Bar", "Foo Bar Baz"), true)
) )
@@ -613,7 +616,7 @@ class NativeCallSpec extends Specification {
} }
def nativeCall = nativeCall(multistreamHolder) def nativeCall = nativeCall(multistreamHolder)
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) { nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"0xab\"".bytes, null, 1, Collections.singletonList(ups), null)) 1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"0xab\"".bytes, null, 1, Collections.singletonList(ups), null))
} }
} }
@@ -648,7 +651,7 @@ class NativeCallSpec extends Specification {
} }
def nativeCall = nativeCall(multistreamHolder) def nativeCall = nativeCall(multistreamHolder)
nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) { nativeCall.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"0xab\"".bytes, null, 1, Collections.singletonList(ups), null)) 1 * read(_) >> Mono.just(new QuorumRpcReader.Result("\"0xab\"".bytes, null, 1, Collections.singletonList(ups), null))
} }
} }

View File

@@ -33,6 +33,8 @@ import spock.lang.Specification
import java.time.Duration import java.time.Duration
import static java.util.List.of
class FilteredApisSpec extends Specification { class FilteredApisSpec extends Specification {
def ethereumTargets = new DefaultEthereumMethods(Chain.ETHEREUM) def ethereumTargets = new DefaultEthereumMethods(Chain.ETHEREUM)
@@ -194,6 +196,29 @@ class FilteredApisSpec extends Specification {
.expectNext(ups[2], ups[3], ups[4], ups[5], ups[0], ups[1]) .expectNext(ups[2], ups[3], ups[4], ups[5], ups[0], ups[1])
.expectComplete() .expectComplete()
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
act.attempts().get() == 6
}
def "FilteredApis is requested 3 times"() {
setup:
def apis = (0..5).collect {
new EthereumApiStub(it)
}
def ups = apis.collect {
TestingCommons.upstream(it)
}
when:
def act = new FilteredApis(Chain.ETHEREUM, ups, Selector.empty, 2, 1, 0)
act.request(3)
then:
StepVerifier.create(act)
.expectNext(ups[2], ups[3], ups[4])
.then {
act.resolve()
}
.expectComplete()
.verify(Duration.ofSeconds(1))
act.attempts().get() == 3
} }
def "Start with offset - 5 items"() { def "Start with offset - 5 items"() {
@@ -330,4 +355,207 @@ class FilteredApisSpec extends Specification {
.expectComplete() .expectComplete()
.verify(Duration.ofSeconds(1)) .verify(Duration.ofSeconds(1))
} }
def "No upstreams if they all are unavailable"() {
setup:
List<Upstream> ups = [
Mock(Upstream) {
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> false
_ * getId() >> "id1"
_ * getStatus() >> UpstreamAvailability.SYNCING
},
Mock(Upstream) {
_ * getId() >> "id2"
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> false
_ * getStatus() >> UpstreamAvailability.SYNCING
}
]
when:
def act = new FilteredApis(Chain.ETHEREUM, ups, Selector.empty)
act.request(1)
then:
StepVerifier.create(act)
.expectNextCount(0)
.expectComplete()
.verify(Duration.ofSeconds(5))
act.upstreamsMatchesResponse() != null
act.upstreamsMatchesResponse().getFullCause() == "id1 - Upstream is not available; id2 - Upstream is not available"
act.upstreamsMatchesResponse().getCause("").cause == "Upstream is not available"
}
def "No upstreams if they all are not matched"() {
setup:
List<Upstream> ups = [
Mock(Upstream) {
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> true
_ * getId() >> "id1"
_ * getStatus() >> UpstreamAvailability.OK
_ * getLabels() >> of(UpstreamsConfig.Labels.fromMap(Map.of("node", "archive")))
},
Mock(Upstream) {
_ * getId() >> "id2"
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> true
_ * getStatus() >> UpstreamAvailability.OK
_ * getLabels() >> of(UpstreamsConfig.Labels.fromMap(Map.of("node", "archive")))
}
]
when:
def act = new FilteredApis(Chain.ETHEREUM, ups, new Selector.LabelMatcher("node", of("test")))
act.request(1)
then:
StepVerifier.create(act)
.expectNextCount(0)
.expectComplete()
.verify(Duration.ofSeconds(5))
act.upstreamsMatchesResponse() != null
act.upstreamsMatchesResponse().getFullCause() == "id1 - No label `node` with values [test]; id2 - No label `node` with values [test]"
act.upstreamsMatchesResponse().getCause("").cause == "No label `node` with values [test]"
}
def "No upstreams if they all are not matched by first matcher"() {
setup:
List<Upstream> ups = [
Mock(Upstream) {
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> false
_ * getId() >> "id1"
_ * getStatus() >> UpstreamAvailability.OK
_ * getHead() >> Mock(Head) {
_ * getCurrentHeight() >> 100000
}
_ * getLabels() >> of(
UpstreamsConfig.Labels.fromMap(
Map.of("node", "archive", "type", "super")
)
)
},
Mock(Upstream) {
_ * getId() >> "id2"
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> false
_ * getHead() >> Mock(Head) {
_ * getCurrentHeight() >> 100000
}
_ * getStatus() >> UpstreamAvailability.OK
_ * getLabels() >> of(UpstreamsConfig.Labels.fromMap(Map.of("node", "archive")))
}
]
when:
def act = new FilteredApis(
Chain.ETHEREUM, ups,
new Selector.MultiMatcher(
of(
new Selector.HeightMatcher(100000000),
)
)
)
act.request(1)
then:
StepVerifier.create(act)
.expectNextCount(0)
.expectComplete()
.verify(Duration.ofSeconds(5))
act.upstreamsMatchesResponse() != null
act.upstreamsMatchesResponse().getFullCause() == "id1 - Upstream is not available; Upstream height 100000 is less than 100000000; id2 - Upstream is not available; Upstream height 100000 is less than 100000000"
act.upstreamsMatchesResponse().getCause("eth_getTransactionByHash").cause == null
act.upstreamsMatchesResponse().getCause("eth_getTransactionByHash").shouldReturnNull
}
def "No upstreams if they all are not matched and return null cause"() {
setup:
List<Upstream> ups = [
Mock(Upstream) {
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> false
_ * getId() >> "id1"
_ * getStatus() >> UpstreamAvailability.OK
_ * getHead() >> Mock(Head) {
_ * getCurrentHeight() >> 100000
}
_ * getLabels() >> of(
UpstreamsConfig.Labels.fromMap(
Map.of("node", "archive", "type", "super")
)
)
},
Mock(Upstream) {
_ * getId() >> "id2"
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> false
_ * getHead() >> Mock(Head) {
_ * getCurrentHeight() >> 100000
}
_ * getStatus() >> UpstreamAvailability.OK
_ * getLabels() >> of(UpstreamsConfig.Labels.fromMap(Map.of("node", "archive")))
}
]
when:
def act = new FilteredApis(
Chain.ETHEREUM, ups,
new Selector.MultiMatcher(
of(
new Selector.HeightMatcher(100000000),
)
)
)
act.request(1)
then:
StepVerifier.create(act)
.expectNextCount(0)
.expectComplete()
.verify(Duration.ofSeconds(5))
act.upstreamsMatchesResponse() != null
act.upstreamsMatchesResponse().getFullCause() == "id1 - Upstream is not available; Upstream height 100000 is less than 100000000; id2 - Upstream is not available; Upstream height 100000 is less than 100000000"
act.upstreamsMatchesResponse().getCause("other") == null
}
def "Second upstream if first is not matched"() {
setup:
def up = Mock(Upstream) {
_ * getId() >> "id2"
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> true
_ * getHead() >> Mock(Head) {
_ * getCurrentHeight() >> 100000001
}
_ * getStatus() >> UpstreamAvailability.OK
_ * getLabels() >> of(UpstreamsConfig.Labels.fromMap(Map.of("node", "test")))
}
List<Upstream> ups = [
Mock(Upstream) {
_ * getRole() >> UpstreamsConfig.UpstreamRole.PRIMARY
_ * isAvailable() >> true
_ * getId() >> "id1"
_ * getStatus() >> UpstreamAvailability.OK
_ * getHead() >> Mock(Head) {
_ * getCurrentHeight() >> 100000
}
_ * getLabels() >> of(UpstreamsConfig.Labels.fromMap(Map.of("node", "archive")))
}, up
]
when:
def act = new FilteredApis(
Chain.ETHEREUM, ups,
new Selector.MultiMatcher(
of(
new Selector.HeightMatcher(100000000),
new Selector.LabelMatcher("node", of("test"))
)
)
)
act.request(1)
then:
StepVerifier.create(act)
.expectNext(up)
.then {
act.resolve()
}
.expectComplete()
.verify(Duration.ofSeconds(5))
act.upstreamsMatchesResponse() == null
}
} }

View File

@@ -178,9 +178,12 @@ class SelectorSpec extends Specification {
def "LABEL matches single label"() { def "LABEL matches single label"() {
setup: setup:
def matcher = new Selector.LabelMatcher("test", ["foo"]) def matcher = new Selector.LabelMatcher("test", ["foo"])
def up = Mock(Upstream) {
1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(maps)]
}
expect: expect:
matcher.matches(UpstreamsConfig.Labels.fromMap(maps)) matcher.matches(up)
where: where:
maps << [ maps << [
@@ -193,9 +196,12 @@ class SelectorSpec extends Specification {
def "LABEL matches one label two values"() { def "LABEL matches one label two values"() {
setup: setup:
def matcher = new Selector.LabelMatcher("test", ["foo", "bar"]) def matcher = new Selector.LabelMatcher("test", ["foo", "bar"])
def up = Mock(Upstream) {
1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(maps)]
}
expect: expect:
matcher.matches(UpstreamsConfig.Labels.fromMap(maps)) matcher.matches(up)
where: where:
maps << [ maps << [
@@ -212,9 +218,12 @@ class SelectorSpec extends Specification {
new Selector.LabelMatcher("test", ["foo", "bar"]) new Selector.LabelMatcher("test", ["foo", "bar"])
] ]
) )
def up = Mock(Upstream) {
1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(maps)]
}
expect: expect:
matcher.matches(UpstreamsConfig.Labels.fromMap(maps)) matcher.matches(up)
where: where:
maps << [ maps << [
@@ -232,9 +241,12 @@ class SelectorSpec extends Specification {
new Selector.LabelMatcher("test2", ["baz"]) new Selector.LabelMatcher("test2", ["baz"])
] ]
) )
def up = Mock(Upstream) {
1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(maps)]
}
expect: expect:
matcher.matches(UpstreamsConfig.Labels.fromMap(maps)) matcher.matches(up)
where: where:
maps << [ maps << [
@@ -252,9 +264,12 @@ class SelectorSpec extends Specification {
new Selector.LabelMatcher("test2", ["baz"]) new Selector.LabelMatcher("test2", ["baz"])
] ]
) )
def up = Mock(Upstream) {
1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(maps)]
}
expect: expect:
matcher.matches(UpstreamsConfig.Labels.fromMap(maps)) matcher.matches(up)
where: where:
maps << [ maps << [
@@ -277,9 +292,12 @@ class SelectorSpec extends Specification {
) )
] ]
) )
def up = Mock(Upstream) {
1 * getLabels() >> [UpstreamsConfig.Labels.fromMap(maps)]
}
expect: expect:
matcher.matches(UpstreamsConfig.Labels.fromMap(maps)) matcher.matches(up)
where: where:
maps << [ maps << [

View File

@@ -5,9 +5,9 @@ import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CurrentBlockCache import io.emeraldpay.dshackle.cache.CurrentBlockCache
import io.emeraldpay.dshackle.data.DefaultContainer import io.emeraldpay.dshackle.data.DefaultContainer
import io.emeraldpay.dshackle.quorum.QuorumReader
import io.emeraldpay.dshackle.quorum.QuorumReaderFactory import io.emeraldpay.dshackle.quorum.QuorumReaderFactory
import io.emeraldpay.dshackle.quorum.QuorumRpcReader import io.emeraldpay.dshackle.quorum.QuorumRpcReader
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.test.TestingCommons import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.ApiSource import io.emeraldpay.dshackle.upstream.ApiSource
import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.Head
@@ -56,7 +56,7 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _,) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
new QuorumRpcReader.Result( new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolvers, null) Global.objectMapper.writeValueAsBytes(json), null, 1, resolvers, null)
@@ -86,7 +86,7 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> Mono.just(
new QuorumRpcReader.Result( new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(null), null, 1, resolvers, null Global.objectMapper.writeValueAsBytes(null), null, 1, resolvers, null
@@ -124,7 +124,7 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> Mono.just(
new QuorumRpcReader.Result( new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolvers, null Global.objectMapper.writeValueAsBytes(json), null, 1, resolvers, null
@@ -160,7 +160,7 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
new QuorumRpcReader.Result( new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, resolvers, null Global.objectMapper.writeValueAsBytes(json), null, 1, resolvers, null
@@ -196,7 +196,7 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just(
new QuorumRpcReader.Result( new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, new ArrayList<Byte>(), null Global.objectMapper.writeValueAsBytes(json), null, 1, new ArrayList<Byte>(), null
@@ -233,7 +233,7 @@ class EthereumDirectReaderSpec extends Specification {
up, caches, new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, caches, new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getTransactionReceipt", [hash1])) >> Mono.just(
new QuorumRpcReader.Result( new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(json), null, 1, new ArrayList<Byte>(), null Global.objectMapper.writeValueAsBytes(json), null, 1, new ArrayList<Byte>(), null
@@ -261,7 +261,7 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getTransactionByHash", [hash1])) >> Mono.just(
new QuorumRpcReader.Result( new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes(null), null, 1, resolvers, null Global.objectMapper.writeValueAsBytes(null), null, 1, resolvers, null
@@ -292,7 +292,7 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >> Mono.just(
new QuorumRpcReader.Result( new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes("0x100"), null, 1, resolvers, null Global.objectMapper.writeValueAsBytes("0x100"), null, 1, resolvers, null
@@ -324,7 +324,7 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getBalance", [address1, "0xa8c9bb"])) >> Mono.just( 1 * read(new JsonRpcRequest("eth_getBalance", [address1, "0xa8c9bb"])) >> Mono.just(
new QuorumRpcReader.Result( new QuorumRpcReader.Result(
Global.objectMapper.writeValueAsBytes("0x100"), null, 1, resolvers, null Global.objectMapper.writeValueAsBytes("0x100"), null, 1, resolvers, null
@@ -365,11 +365,11 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
ethereumDirectReader.quorumReaderFactory = Mock(QuorumReaderFactory) { ethereumDirectReader.quorumReaderFactory = Mock(QuorumReaderFactory) {
2 * create(_, _, _, _) >> Mock(Reader) { 2 * create(_, _, _, _) >> Mock(QuorumReader) {
2 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >>> 2 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >>>
[Mono.error(new RuntimeException()), Mono.error(new RuntimeException())] [Mono.error(new RuntimeException()), Mono.error(new RuntimeException())]
} }
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> result 1 * read(new JsonRpcRequest("eth_getBlockByHash", [hash1, false])) >> result
} }
} }
@@ -408,11 +408,11 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
ethereumDirectReader.quorumReaderFactory = Mock(QuorumReaderFactory) { ethereumDirectReader.quorumReaderFactory = Mock(QuorumReaderFactory) {
2 * create(_, _, _, _) >> Mock(Reader) { 2 * create(_, _, _, _) >> Mock(QuorumReader) {
2 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >>> 2 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >>>
[Mono.error(new RuntimeException()), Mono.error(new RuntimeException())] [Mono.error(new RuntimeException()), Mono.error(new RuntimeException())]
} }
1 * create(_, _, _, _) >> Mock(Reader) { 1 * create(_, _, _, _) >> Mock(QuorumReader) {
1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> result 1 * read(new JsonRpcRequest("eth_getBlockByNumber", ["0x64", false])) >> result
} }
} }
@@ -442,7 +442,7 @@ class EthereumDirectReaderSpec extends Specification {
up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock() up, Caches.default(), new CurrentBlockCache(), calls, TestingCommons.tracerMock()
) )
reader.quorumReaderFactory = Mock(QuorumReaderFactory) { reader.quorumReaderFactory = Mock(QuorumReaderFactory) {
4 * create(_, _, _, _) >> Mock(Reader) { 4 * create(_, _, _, _) >> Mock(QuorumReader) {
4 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >>> 4 * read(new JsonRpcRequest("eth_getBalance", [address1, "latest"])) >>>
[Mono.error(new RuntimeException()), Mono.error(new RuntimeException()), [Mono.error(new RuntimeException()), Mono.error(new RuntimeException()),
Mono.error(new RuntimeException()), Mono.error(new RuntimeException())] Mono.error(new RuntimeException()), Mono.error(new RuntimeException())]

View File

@@ -37,7 +37,7 @@ class CollectSpanConfigTest {
appCtx.getBean(SpanConfig::class.java) appCtx.getBean(SpanConfig::class.java)
} }
assertThrows(NoSuchBeanDefinitionException::class.java) { assertThrows(NoSuchBeanDefinitionException::class.java) {
appCtx.getBean(ErrorSpanHandler::class.java) appCtx.getBean(ProviderSpanHandler::class.java)
} }
assertThrows(NoSuchBeanDefinitionException::class.java) { assertThrows(NoSuchBeanDefinitionException::class.java) {
appCtx.getBean(ServerSpansInterceptor::class.java) appCtx.getBean(ServerSpansInterceptor::class.java)
@@ -72,7 +72,7 @@ class CollectSpanConfigTest {
appCtx.getBean(SpanConfig::class.java) appCtx.getBean(SpanConfig::class.java)
} }
assertDoesNotThrow { assertDoesNotThrow {
appCtx.getBean(ErrorSpanHandler::class.java) appCtx.getBean(ProviderSpanHandler::class.java)
} }
assertDoesNotThrow { assertDoesNotThrow {
appCtx.getBean(ServerSpansInterceptor::class.java) appCtx.getBean(ServerSpansInterceptor::class.java)
@@ -113,7 +113,7 @@ class CollectSpanConfigTest {
appCtx.getBean(SpanConfig::class.java) appCtx.getBean(SpanConfig::class.java)
} }
assertThrows(NoSuchBeanDefinitionException::class.java) { assertThrows(NoSuchBeanDefinitionException::class.java) {
appCtx.getBean(ErrorSpanHandler::class.java) appCtx.getBean(ProviderSpanHandler::class.java)
} }
assertThrows(NoSuchBeanDefinitionException::class.java) { assertThrows(NoSuchBeanDefinitionException::class.java) {
appCtx.getBean(ServerSpansInterceptor::class.java) appCtx.getBean(ServerSpansInterceptor::class.java)

View File

@@ -5,16 +5,20 @@ import brave.handler.SpanHandler
import brave.propagation.TraceContext import brave.propagation.TraceContext
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import io.emeraldpay.dshackle.commons.SPAN_ERROR import io.emeraldpay.dshackle.commons.SPAN_ERROR
import io.emeraldpay.dshackle.commons.SPAN_NO_RESPONSE_MESSAGE
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.springframework.cloud.sleuth.Span import org.springframework.cloud.sleuth.Span
import org.springframework.cloud.sleuth.brave.bridge.BraveTraceContext import org.springframework.cloud.sleuth.brave.bridge.BraveTraceContext
class ErrorSpanHandlerTest { class ProviderSpanHandlerTest {
private val mapper = SpanConfig().spanMapper() private val mapper = SpanConfig().spanMapper()
private val spanExportableList = listOf(ErrorSpanExportable(), NoResponseSpanExportable())
private val ctx = TraceContext.newBuilder() private val ctx = TraceContext.newBuilder()
.traceId(1223324) .traceId(1223324)
.spanId(234235) .spanId(234235)
@@ -48,13 +52,11 @@ class ErrorSpanHandlerTest {
assertEquals("", result) assertEquals("", result)
} }
@Test @ParameterizedTest
fun `span with length of traceId greater than 20 and with parentId is collected`() { @MethodSource("spans")
val spanId = "f7e83f2b69ec684d" fun `span with length of traceId greater than 20 and with parentId is collected`(span: MutableSpan) {
val currentSpan = Mockito.mock(Span::class.java) val currentSpan = Mockito.mock(Span::class.java)
val handler = spanHandler() val handler = spanHandler()
val span = span("6666632728347823749827349723985", spanId)
.apply { parentId("f7e83f2b69ec682d") }
`when`(currentSpan.context()).thenReturn(BraveTraceContext(ctx)) `when`(currentSpan.context()).thenReturn(BraveTraceContext(ctx))
@@ -92,5 +94,25 @@ class ErrorSpanHandlerTest {
tag(SPAN_ERROR, "true") tag(SPAN_ERROR, "true")
} }
private fun spanHandler() = ErrorSpanHandler(mapper) companion object {
@JvmStatic
fun spans() = listOf(
MutableSpan()
.apply {
traceId("6666632728347823749827349723985")
id("f7e83f2b69ec682d")
tag(SPAN_ERROR, "true")
parentId("f7e83f2b69ec682d")
},
MutableSpan()
.apply {
traceId("6666632728347823749827349723985")
id("f7e83f2b69ec111d")
parentId("f7e83f2b69ec682d")
tag(SPAN_NO_RESPONSE_MESSAGE, "noResp")
}
)
}
private fun spanHandler() = ProviderSpanHandler(mapper, spanExportableList)
} }