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

@@ -25,6 +25,7 @@ sealed class MatchesResponse {
this.allResponses
.filter { it !is Success }
.joinToString("; ") { it.getCause()!! }
is NotMatchedResponse -> "Not matched - ${response.getCause()}"
is SameNodeResponse -> "Upstream does not have hash ${this.upstreamHash}"
is LowerHeightResponse -> {
@@ -34,6 +35,8 @@ sealed class MatchesResponse {
"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
}
@@ -100,5 +103,10 @@ sealed class MatchesResponse {
val upstreamHash: Short,
) : MatchesResponse()
data class RangeVersionResponse(
val minVersion: String,
val maxVersion: String,
) : MatchesResponse()
object AvailabilityResponse : MatchesResponse()
}

View File

@@ -16,6 +16,7 @@
*/
package io.emeraldpay.dshackle.upstream
import com.vdurmont.semver4j.Semver
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.config.UpstreamsConfig
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.LowerHeightResponse
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.SlotHeightResponse
import io.emeraldpay.dshackle.upstream.MatchesResponse.Success
@@ -35,7 +37,6 @@ import org.apache.commons.lang3.StringUtils
import java.util.Collections
class Selector {
companion object {
@JvmStatic
@@ -55,6 +56,7 @@ class Selector {
} else {
Number(selector.height)
}
BlockchainOuterClass.HeightSelector.HeightOrNumberCase.NUMBER -> Number(selector.number)
BlockchainOuterClass.HeightSelector.HeightOrNumberCase.TAG -> when (selector.tag) {
BlockchainOuterClass.BlockTag.SAFE -> Safe
@@ -63,10 +65,12 @@ class Selector {
BlockchainOuterClass.BlockTag.FINALIZED -> Finalized
else -> null
}
else -> null
}
}
}
class Number(val num: Long) : HeightNumberOrTag()
object Pending : HeightNumberOrTag()
object Latest : HeightNumberOrTag()
@@ -97,12 +101,14 @@ class Selector {
it.hasSlotHeightSelector() -> {
SlotMatcher(it.slotHeightSelector.slotHeight)
}
it.hasHeightSelector() -> {
when (val selector = HeightNumberOrTag.fromHeightSelector(it.heightSelector)) {
is HeightNumberOrTag.Number -> HeightMatcher(selector.num)
else -> empty
}
}
it.hasLowerHeightSelector() -> {
if (it.lowerHeightSelector.height > 0) {
LowerHeightMatcher(
@@ -115,6 +121,7 @@ class Selector {
empty
}
}
else -> empty
}
}.run {
@@ -126,7 +133,8 @@ class Selector {
private fun getSort(selectors: List<BlockchainOuterClass.Selector>): Sort {
selectors.forEach { selector ->
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) {
return heightSort
}
@@ -159,6 +167,7 @@ class Selector {
anyLabel
}
}
req.hasAndSelector() -> AndMatcher(
Collections.unmodifiableCollection(
req.andSelector.selectorsList.map {
@@ -168,6 +177,7 @@ class Selector {
},
),
)
req.hasOrSelector() -> OrMatcher(
Collections.unmodifiableCollection(
req.orSelector.selectorsList.map {
@@ -177,8 +187,29 @@ class Selector {
},
),
)
req.hasNotSelector() -> NotMatcher(convertToMatcher(req.notSelector.selector))
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
}
}
@@ -693,4 +724,80 @@ class Selector {
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()}"
}
}