From 3e845bcdf0b676349d5b5f4ab08851f81d24227b Mon Sep 17 00:00:00 2001 From: Vadim Filin Date: Thu, 18 Dec 2025 20:01:37 +0800 Subject: [PATCH] Support '<' and '>' operations for client routing on key (#755) --- Makefile | 3 + build.gradle | 1 + emerald-grpc | 2 +- gradle/libs.versions.toml | 2 + .../dshackle/upstream/MatchesResponse.kt | 8 ++ .../emeraldpay/dshackle/upstream/Selector.kt | 111 +++++++++++++++++- 6 files changed, 124 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index c0d84241..bc1bac9b 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,9 @@ all: build-foundation build-main build-foundation: cd foundation && ../gradlew build publishToMavenLocal +run-main: + ./gradlew run + build-main: ./gradlew build diff --git a/build.gradle b/build.gradle index 85c02587..91b5926f 100644 --- a/build.gradle +++ b/build.gradle @@ -97,6 +97,7 @@ dependencies { implementation libs.javax.annotations implementation libs.auth0.jwt + implementation libs.semver4j testImplementation libs.cglib.nodep testImplementation libs.spockframework.core diff --git a/emerald-grpc b/emerald-grpc index f5c4aac8..f1880440 160000 --- a/emerald-grpc +++ b/emerald-grpc @@ -1 +1 @@ -Subproject commit f5c4aac8196db84b708149ed3c5f92b103682a0a +Subproject commit f18804408ff4876db82b4106d05ac4e76b8170ef diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 928227fc..85b152c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -122,6 +122,8 @@ auth0-jwt = "com.auth0:java-jwt:4.4.0" mockito-inline = "org.mockito:mockito-inline:4.0.0" +semver4j = "com.vdurmont:semver4j:3.1.0" + [bundles] 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"] diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/MatchesResponse.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MatchesResponse.kt index f8026ce0..471fb556 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/MatchesResponse.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/MatchesResponse.kt @@ -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() } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt index f79bda82..0fb9fbad 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/Selector.kt @@ -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): 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()}" + } }