Support '<' and '>' operations for client routing on key (#755)

This commit is contained in:
Vadim Filin
2025-12-18 20:01:37 +08:00
committed by GitHub
parent 757febc0b5
commit 3e845bcdf0
6 changed files with 124 additions and 3 deletions

View File

@@ -2,6 +2,9 @@ all: build-foundation build-main
build-foundation: build-foundation:
cd foundation && ../gradlew build publishToMavenLocal cd foundation && ../gradlew build publishToMavenLocal
run-main:
./gradlew run
build-main: build-main:
./gradlew build ./gradlew build

View File

@@ -97,6 +97,7 @@ dependencies {
implementation libs.javax.annotations implementation libs.javax.annotations
implementation libs.auth0.jwt implementation libs.auth0.jwt
implementation libs.semver4j
testImplementation libs.cglib.nodep testImplementation libs.cglib.nodep
testImplementation libs.spockframework.core testImplementation libs.spockframework.core

View File

@@ -122,6 +122,8 @@ auth0-jwt = "com.auth0:java-jwt:4.4.0"
mockito-inline = "org.mockito:mockito-inline:4.0.0" mockito-inline = "org.mockito:mockito-inline:4.0.0"
semver4j = "com.vdurmont:semver4j:3.1.0"
[bundles] [bundles]
apache-commons = ["commons-io", "apache-commons-lang3", "apache-commons-collections4", "apache-commons-math3"] apache-commons = ["commons-io", "apache-commons-lang3", "apache-commons-collections4", "apache-commons-math3"]
grpc = ["grpc-protobuf", "grpc-stub", "grpc-netty", "grpc-proto-util", "grpc-services"] grpc = ["grpc-protobuf", "grpc-stub", "grpc-netty", "grpc-proto-util", "grpc-services"]

View File

@@ -25,6 +25,7 @@ sealed class MatchesResponse {
this.allResponses this.allResponses
.filter { it !is Success } .filter { it !is Success }
.joinToString("; ") { it.getCause()!! } .joinToString("; ") { it.getCause()!! }
is NotMatchedResponse -> "Not matched - ${response.getCause()}" is NotMatchedResponse -> "Not matched - ${response.getCause()}"
is SameNodeResponse -> "Upstream does not have hash ${this.upstreamHash}" is SameNodeResponse -> "Upstream does not have hash ${this.upstreamHash}"
is LowerHeightResponse -> { is LowerHeightResponse -> {
@@ -34,6 +35,8 @@ sealed class MatchesResponse {
"Upstream lower height ${this.predictedHeight} of type ${this.boundType} is greater than ${this.lowerHeight}" "Upstream lower height ${this.predictedHeight} of type ${this.boundType} is greater than ${this.lowerHeight}"
} }
} }
is RangeVersionResponse -> "Upstream version is not within the range ${this.minVersion}-${this.maxVersion}"
else -> null else -> null
} }
@@ -100,5 +103,10 @@ sealed class MatchesResponse {
val upstreamHash: Short, val upstreamHash: Short,
) : MatchesResponse() ) : MatchesResponse()
data class RangeVersionResponse(
val minVersion: String,
val maxVersion: String,
) : MatchesResponse()
object AvailabilityResponse : MatchesResponse() object AvailabilityResponse : MatchesResponse()
} }

View File

@@ -16,6 +16,7 @@
*/ */
package io.emeraldpay.dshackle.upstream package io.emeraldpay.dshackle.upstream
import com.vdurmont.semver4j.Semver
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.AvailabilityResponse
@@ -25,6 +26,7 @@ import io.emeraldpay.dshackle.upstream.MatchesResponse.GrpcResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.HeightResponse import io.emeraldpay.dshackle.upstream.MatchesResponse.HeightResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.LowerHeightResponse import io.emeraldpay.dshackle.upstream.MatchesResponse.LowerHeightResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.NotMatchedResponse import io.emeraldpay.dshackle.upstream.MatchesResponse.NotMatchedResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.RangeVersionResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.SameNodeResponse import io.emeraldpay.dshackle.upstream.MatchesResponse.SameNodeResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.SlotHeightResponse import io.emeraldpay.dshackle.upstream.MatchesResponse.SlotHeightResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.Success import io.emeraldpay.dshackle.upstream.MatchesResponse.Success
@@ -35,7 +37,6 @@ import org.apache.commons.lang3.StringUtils
import java.util.Collections import java.util.Collections
class Selector { class Selector {
companion object { companion object {
@JvmStatic @JvmStatic
@@ -55,6 +56,7 @@ class Selector {
} else { } else {
Number(selector.height) Number(selector.height)
} }
BlockchainOuterClass.HeightSelector.HeightOrNumberCase.NUMBER -> Number(selector.number) BlockchainOuterClass.HeightSelector.HeightOrNumberCase.NUMBER -> Number(selector.number)
BlockchainOuterClass.HeightSelector.HeightOrNumberCase.TAG -> when (selector.tag) { BlockchainOuterClass.HeightSelector.HeightOrNumberCase.TAG -> when (selector.tag) {
BlockchainOuterClass.BlockTag.SAFE -> Safe BlockchainOuterClass.BlockTag.SAFE -> Safe
@@ -63,10 +65,12 @@ class Selector {
BlockchainOuterClass.BlockTag.FINALIZED -> Finalized BlockchainOuterClass.BlockTag.FINALIZED -> Finalized
else -> null else -> null
} }
else -> null else -> null
} }
} }
} }
class Number(val num: Long) : HeightNumberOrTag() class Number(val num: Long) : HeightNumberOrTag()
object Pending : HeightNumberOrTag() object Pending : HeightNumberOrTag()
object Latest : HeightNumberOrTag() object Latest : HeightNumberOrTag()
@@ -97,12 +101,14 @@ class Selector {
it.hasSlotHeightSelector() -> { it.hasSlotHeightSelector() -> {
SlotMatcher(it.slotHeightSelector.slotHeight) SlotMatcher(it.slotHeightSelector.slotHeight)
} }
it.hasHeightSelector() -> { it.hasHeightSelector() -> {
when (val selector = HeightNumberOrTag.fromHeightSelector(it.heightSelector)) { when (val selector = HeightNumberOrTag.fromHeightSelector(it.heightSelector)) {
is HeightNumberOrTag.Number -> HeightMatcher(selector.num) is HeightNumberOrTag.Number -> HeightMatcher(selector.num)
else -> empty else -> empty
} }
} }
it.hasLowerHeightSelector() -> { it.hasLowerHeightSelector() -> {
if (it.lowerHeightSelector.height > 0) { if (it.lowerHeightSelector.height > 0) {
LowerHeightMatcher( LowerHeightMatcher(
@@ -115,6 +121,7 @@ class Selector {
empty empty
} }
} }
else -> empty else -> empty
} }
}.run { }.run {
@@ -126,7 +133,8 @@ class Selector {
private fun getSort(selectors: List<BlockchainOuterClass.Selector>): Sort { private fun getSort(selectors: List<BlockchainOuterClass.Selector>): Sort {
selectors.forEach { selector -> selectors.forEach { selector ->
if (selector.hasHeightSelector()) { if (selector.hasHeightSelector()) {
val heightSort = HeightNumberOrTag.fromHeightSelector(selector.heightSelector)?.getSort() ?: Sort.default val heightSort =
HeightNumberOrTag.fromHeightSelector(selector.heightSelector)?.getSort() ?: Sort.default
if (heightSort != Sort.default) { if (heightSort != Sort.default) {
return heightSort return heightSort
} }
@@ -159,6 +167,7 @@ class Selector {
anyLabel anyLabel
} }
} }
req.hasAndSelector() -> AndMatcher( req.hasAndSelector() -> AndMatcher(
Collections.unmodifiableCollection( Collections.unmodifiableCollection(
req.andSelector.selectorsList.map { req.andSelector.selectorsList.map {
@@ -168,6 +177,7 @@ class Selector {
}, },
), ),
) )
req.hasOrSelector() -> OrMatcher( req.hasOrSelector() -> OrMatcher(
Collections.unmodifiableCollection( Collections.unmodifiableCollection(
req.orSelector.selectorsList.map { req.orSelector.selectorsList.map {
@@ -177,8 +187,29 @@ class Selector {
}, },
), ),
) )
req.hasNotSelector() -> NotMatcher(convertToMatcher(req.notSelector.selector)) req.hasNotSelector() -> NotMatcher(convertToMatcher(req.notSelector.selector))
req.hasExistsSelector() -> ExistsMatcher(req.existsSelector.name) req.hasExistsSelector() -> ExistsMatcher(req.existsSelector.name)
req.hasMinVersionSelector() || req.hasMaxVersionSelector() -> {
val min = if (req.hasMinVersionSelector()) {
req.minVersionSelector.version
} else {
""
}
val max = if (req.hasMaxVersionSelector()) {
req.maxVersionSelector.version
} else {
""
}
if (min.isEmpty() && max.isEmpty()) {
anyLabel
} else {
RangeVersionMatcher(min, max)
}
}
else -> anyLabel else -> anyLabel
} }
} }
@@ -693,4 +724,80 @@ class Selector {
override fun toString(): String = "Matcher: ${describeInternal()}" override fun toString(): String = "Matcher: ${describeInternal()}"
} }
class RangeVersionMatcher(
private val minRawVersion: String,
private val maxRawVersion: String,
) : LabelSelectorMatcher() {
override fun matchesWithCause(labels: UpstreamsConfig.Labels): MatchesResponse {
val actualRawVersion = labels["client_version"]
?: return RangeVersionResponse(minRawVersion, maxRawVersion)
val actualSemver = runCatching {
Semver(actualRawVersion.removePrefix("v"), Semver.SemverType.STRICT)
}.getOrElse {
return RangeVersionResponse(minRawVersion, maxRawVersion)
}
val greaterOk =
if (minRawVersion.isEmpty()) {
true
} else {
runCatching {
val min = Semver(minRawVersion.removePrefix("v"), Semver.SemverType.STRICT)
actualSemver.isGreaterThan(min)
}.getOrElse {
false
}
}
val lessOk =
if (maxRawVersion.isEmpty()) {
true
} else {
runCatching {
val max = Semver(maxRawVersion.removePrefix("v"), Semver.SemverType.STRICT)
actualSemver.isLowerThan(max)
}.getOrElse {
false
}
}
return if (greaterOk && lessOk) {
Success
} else {
RangeVersionResponse(minRawVersion, maxRawVersion)
}
}
override fun asProto(): BlockchainOuterClass.Selector =
BlockchainOuterClass.Selector.newBuilder().setAndSelector(
BlockchainOuterClass.AndSelector.newBuilder().apply {
if (minRawVersion.isNotEmpty()) {
addSelectors(
BlockchainOuterClass.Selector.newBuilder().setMinVersionSelector(
BlockchainOuterClass.MinVersionSelector.newBuilder()
.setVersion(minRawVersion)
.build(),
),
)
}
if (maxRawVersion.isNotEmpty()) {
addSelectors(
BlockchainOuterClass.Selector.newBuilder().setMaxVersionSelector(
BlockchainOuterClass.MaxVersionSelector.newBuilder()
.setVersion(maxRawVersion)
.build(),
),
)
}
},
).build()
override fun describeInternal(): String =
"version range from '$minRawVersion' to '$maxRawVersion'"
override fun toString(): String =
"Matcher: ${describeInternal()}"
}
} }