Support '<' and '>' operations for client routing on key (#755)
This commit is contained in:
3
Makefile
3
Makefile
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Submodule emerald-grpc updated: f5c4aac819...f18804408f
@@ -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"]
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user