Perf improvements (#758)

This commit is contained in:
KirillPamPam
2025-12-17 16:38:12 +04:00
committed by GitHub
parent 4e8487bc89
commit 4f2959a75f
14 changed files with 71 additions and 20 deletions

View File

@@ -35,6 +35,7 @@ import io.emeraldpay.dshackle.upstream.ethereum.domain.TransactionId
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.TransactionIdSerializer import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.TransactionIdSerializer
import io.emeraldpay.dshackle.upstream.ton.TonMasterchainInfo import io.emeraldpay.dshackle.upstream.ton.TonMasterchainInfo
import io.emeraldpay.dshackle.upstream.ton.TonMasterchainInfoDeserializer import io.emeraldpay.dshackle.upstream.ton.TonMasterchainInfoDeserializer
import reactor.netty.resources.LoopResources
import java.math.BigInteger import java.math.BigInteger
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@@ -46,6 +47,8 @@ class Global {
companion object { companion object {
val wsLoops: LoopResources = LoopResources.create("reactor-ws")
val nullValue: ByteArray = "null".toByteArray() val nullValue: ByteArray = "null".toByteArray()
var metricsExtended = false var metricsExtended = false

View File

@@ -76,6 +76,16 @@ open class SchedulersConfig {
return makeScheduler("auth-scheduler", 4, monitoringConfig) return makeScheduler("auth-scheduler", 4, monitoringConfig)
} }
@Bean
open fun httpScheduler(monitoringConfig: MonitoringConfig): Scheduler {
return makeScheduler("http-scheduler", 30, monitoringConfig)
}
@Bean
open fun eventsScheduler(monitoringConfig: MonitoringConfig): Scheduler {
return makeScheduler("ws-events-scheduler", 30, monitoringConfig)
}
private fun makeScheduler(name: String, size: Int, monitoringConfig: MonitoringConfig): Scheduler { private fun makeScheduler(name: String, size: Int, monitoringConfig: MonitoringConfig): Scheduler {
return Schedulers.fromExecutorService(makePool(name, size, monitoringConfig)) return Schedulers.fromExecutorService(makePool(name, size, monitoringConfig))
} }

View File

@@ -26,6 +26,8 @@ open class GenericConnectorFactoryCreator(
private val wsScheduler: Scheduler, private val wsScheduler: Scheduler,
private val headLivenessScheduler: Scheduler, private val headLivenessScheduler: Scheduler,
private val monitoringCfg: MonitoringConfig, private val monitoringCfg: MonitoringConfig,
private val httpScheduler: Scheduler,
private val eventsScheduler: Scheduler,
) : ConnectorFactoryCreator { ) : ConnectorFactoryCreator {
protected val log = LoggerFactory.getLogger(this::class.java) protected val log = LoggerFactory.getLogger(this::class.java)
@@ -75,6 +77,7 @@ open class GenericConnectorFactoryCreator(
conn.basicAuth, conn.basicAuth,
tls, tls,
monitoringCfg.nettyMetricsConfig.enabled, monitoringCfg.nettyMetricsConfig.enabled,
httpScheduler,
) )
} }
} }
@@ -92,6 +95,7 @@ open class GenericConnectorFactoryCreator(
endpoint.url, endpoint.url,
endpoint.origin ?: URI("http://localhost"), endpoint.origin ?: URI("http://localhost"),
wsScheduler, wsScheduler,
eventsScheduler,
).apply { ).apply {
config = endpoint config = endpoint
basicAuth = endpoint.basicAuth basicAuth = endpoint.basicAuth

View File

@@ -22,6 +22,8 @@ class RestConnectorFactoryCreator(
private val headScheduler: Scheduler, private val headScheduler: Scheduler,
private val headLivenessScheduler: Scheduler, private val headLivenessScheduler: Scheduler,
monitoringCfg: MonitoringConfig, monitoringCfg: MonitoringConfig,
httpScheduler: Scheduler,
eventsScheduler: Scheduler,
) : GenericConnectorFactoryCreator( ) : GenericConnectorFactoryCreator(
fileResolver, fileResolver,
Schedulers.single(), Schedulers.single(),
@@ -29,6 +31,8 @@ class RestConnectorFactoryCreator(
Schedulers.single(), Schedulers.single(),
headLivenessScheduler, headLivenessScheduler,
monitoringCfg, monitoringCfg,
httpScheduler,
eventsScheduler,
) { ) {
override fun createConnectorFactory( override fun createConnectorFactory(
id: String, id: String,

View File

@@ -10,6 +10,7 @@ import io.micrometer.core.instrument.Metrics
import io.micrometer.core.instrument.Tag import io.micrometer.core.instrument.Tag
import io.micrometer.core.instrument.Timer import io.micrometer.core.instrument.Timer
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import reactor.core.scheduler.Scheduler
class BasicHttpFactory( class BasicHttpFactory(
private val url: String, private val url: String,
@@ -18,6 +19,7 @@ class BasicHttpFactory(
private val basicAuth: AuthConfig.ClientBasicAuth?, private val basicAuth: AuthConfig.ClientBasicAuth?,
private val tls: ByteArray?, private val tls: ByteArray?,
private val nettyMetricsEnabled: Boolean, private val nettyMetricsEnabled: Boolean,
private val httpScheduler: Scheduler,
) : HttpFactory { ) : HttpFactory {
private val log = LoggerFactory.getLogger(this::class.java) private val log = LoggerFactory.getLogger(this::class.java)
@@ -44,8 +46,8 @@ class BasicHttpFactory(
) )
if (chain.type.apiType == ApiType.REST) { if (chain.type.apiType == ApiType.REST) {
return RestHttpReader(url, maxConnections, queueSize, metrics, basicAuth, tls) return RestHttpReader(url, maxConnections, queueSize, metrics, httpScheduler, basicAuth, tls)
} }
return JsonRpcHttpReader(url, maxConnections, queueSize, metrics, basicAuth, tls) return JsonRpcHttpReader(url, maxConnections, queueSize, metrics, httpScheduler, basicAuth, tls)
} }
} }

View File

@@ -17,6 +17,7 @@ open class WsConnectionFactory(
private val uri: URI, private val uri: URI,
private val origin: URI, private val origin: URI,
private val scheduler: Scheduler, private val scheduler: Scheduler,
private val eventsScheduler: Scheduler,
) { ) {
var basicAuth: AuthConfig.ClientBasicAuth? = null var basicAuth: AuthConfig.ClientBasicAuth? = null
@@ -45,7 +46,7 @@ open class WsConnectionFactory(
} }
open fun createWsConnection(connIndex: Int = 0): WsConnection = open fun createWsConnection(connIndex: Int = 0): WsConnection =
WsConnectionImpl(uri, origin, basicAuth, metrics(connIndex), scheduler).also { ws -> WsConnectionImpl(uri, origin, basicAuth, metrics(connIndex), scheduler, eventsScheduler).also { ws ->
config?.frameSize?.let { config?.frameSize?.let {
ws.frameSize = it ws.frameSize = it
} }

View File

@@ -29,7 +29,6 @@ import io.emeraldpay.dshackle.upstream.ethereum.rpc.RpcResponseError
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcWsMessage import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcWsMessage
import io.emeraldpay.dshackle.upstream.rpcclient.ResponseWSParser import io.emeraldpay.dshackle.upstream.rpcclient.ResponseWSParser
import io.micrometer.core.instrument.Metrics import io.micrometer.core.instrument.Metrics
import io.netty.buffer.ByteBufInputStream
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.resolver.DefaultAddressResolverGroup import io.netty.resolver.DefaultAddressResolverGroup
import org.reactivestreams.Publisher import org.reactivestreams.Publisher
@@ -66,6 +65,7 @@ open class WsConnectionImpl(
private val basicAuth: AuthConfig.ClientBasicAuth?, private val basicAuth: AuthConfig.ClientBasicAuth?,
private val requestMetrics: RequestMetrics?, private val requestMetrics: RequestMetrics?,
private val scheduler: Scheduler, private val scheduler: Scheduler,
private val eventsScheduler: Scheduler,
) : AutoCloseable, WsConnection, Cloneable { ) : AutoCloseable, WsConnection, Cloneable {
companion object { companion object {
@@ -193,6 +193,7 @@ open class WsConnectionImpl(
log.info("Connecting to WebSocket: $uri") log.info("Connecting to WebSocket: $uri")
connection?.dispose() connection?.dispose()
connection = HttpClient.create() connection = HttpClient.create()
.runOn(Global.wsLoops)
.resolver(DefaultAddressResolverGroup.INSTANCE) .resolver(DefaultAddressResolverGroup.INSTANCE)
.doOnDisconnected { .doOnDisconnected {
disconnects.tryEmitNext(Instant.now()) disconnects.tryEmitNext(Instant.now())
@@ -251,8 +252,9 @@ open class WsConnectionImpl(
var read = false var read = false
val consumer = inbound val consumer = inbound
.aggregateFrames(msgSizeLimit) .aggregateFrames(msgSizeLimit)
.receiveFrames() .receive()
.map { ByteBufInputStream(it.content()).readAllBytes() } .asByteArray()
.publishOn(eventsScheduler)
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.flatMap { .flatMap {
try { try {

View File

@@ -17,6 +17,7 @@ import io.netty.handler.codec.http.HttpMethod
import org.apache.commons.lang3.time.StopWatch import org.apache.commons.lang3.time.StopWatch
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import reactor.kotlin.core.publisher.switchIfEmpty import reactor.kotlin.core.publisher.switchIfEmpty
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -25,6 +26,7 @@ class RestHttpReader(
maxConnections: Int, maxConnections: Int,
queueSize: Int, queueSize: Int,
metrics: RequestMetrics, metrics: RequestMetrics,
private val httpScheduler: Scheduler,
basicAuth: AuthConfig.ClientBasicAuth? = null, basicAuth: AuthConfig.ClientBasicAuth? = null,
tlsCAAuth: ByteArray? = null, tlsCAAuth: ByteArray? = null,
) : HttpReader(target, maxConnections, queueSize, metrics, basicAuth, tlsCAAuth) { ) : HttpReader(target, maxConnections, queueSize, metrics, basicAuth, tlsCAAuth) {
@@ -86,7 +88,7 @@ class RestHttpReader(
response.response { header, bytes -> response.response { header, bytes ->
val statusCode = header.status().code() val statusCode = header.status().code()
bytes.aggregate().asByteArray().map { bytes.aggregate().asByteArray().publishOn(httpScheduler).map {
AggregateResponse(it, statusCode) AggregateResponse(it, statusCode)
}.switchIfEmpty { }.switchIfEmpty {
Mono.just(AggregateResponse(ByteArray(0), statusCode)) Mono.just(AggregateResponse(ByteArray(0), statusCode))
@@ -95,13 +97,13 @@ class RestHttpReader(
} else { } else {
response.responseConnection { t, u -> response.responseConnection { t, u ->
if (t.status().code() != 200) { if (t.status().code() != 200) {
u.inbound().receive().aggregate().asByteArray() u.inbound().receive().aggregate().asByteArray().publishOn(httpScheduler)
.map { AggregateResponse(it, t.status().code()) } .map { AggregateResponse(it, t.status().code()) }
} else { } else {
Mono.just( Mono.just(
StreamResponse( StreamResponse(
Flux.concat( Flux.concat(
u.inbound().receive().asByteArray() u.inbound().receive().asByteArray().publishOn(httpScheduler)
.map { Chunk(it, false) }, .map { Chunk(it, false) },
Mono.just(Chunk(ByteArray(0), true)), Mono.just(Chunk(ByteArray(0), true)),
), ),

View File

@@ -29,6 +29,7 @@ import io.emeraldpay.dshackle.upstream.stream.StreamResponse
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import org.apache.commons.lang3.time.StopWatch import org.apache.commons.lang3.time.StopWatch
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Scheduler
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.function.Function import java.util.function.Function
@@ -40,6 +41,7 @@ class JsonRpcHttpReader(
maxConnections: Int, maxConnections: Int,
queueSize: Int, queueSize: Int,
metrics: RequestMetrics, metrics: RequestMetrics,
private val httpScheduler: Scheduler,
basicAuth: AuthConfig.ClientBasicAuth? = null, basicAuth: AuthConfig.ClientBasicAuth? = null,
tlsCAAuth: ByteArray? = null, tlsCAAuth: ByteArray? = null,
) : HttpReader(target, maxConnections, queueSize, metrics, basicAuth, tlsCAAuth) { ) : HttpReader(target, maxConnections, queueSize, metrics, basicAuth, tlsCAAuth) {
@@ -59,15 +61,20 @@ class JsonRpcHttpReader(
response.response { header, bytes -> response.response { header, bytes ->
val statusCode = header.status().code() val statusCode = header.status().code()
bytes.aggregate().asByteArray().map { bytes.aggregate().asByteArray()
AggregateResponse(it, statusCode) .publishOn(httpScheduler)
} .map {
AggregateResponse(it, statusCode)
}
}.single() }.single()
} else { } else {
response.responseConnection { t, u -> response.responseConnection { t, u ->
streamParser.streamParse( streamParser.streamParse(
t.status().code(), t.status().code(),
u.inbound().receive().asByteArray(), u.inbound()
.receive()
.asByteArray()
.publishOn(httpScheduler),
) )
}.single() }.single()
} }

View File

@@ -227,7 +227,15 @@ class ApiReaderMock implements Reader<ChainRequest, ChainResponse> {
@Override @Override
ByteBufFlux receive() { ByteBufFlux receive() {
throw new UnsupportedOperationException() return ByteBufFlux
.fromString(
Flux.merge(
jsonResponses,
responses.map {
Global.objectMapper.writeValueAsString(it)
}
)
)
} }
@Override @Override

View File

@@ -43,6 +43,7 @@ class WsConnectionImplRealSpec extends Specification {
Chain.ETHEREUM__MAINNET, Chain.ETHEREUM__MAINNET,
"ws://localhost:${port}".toURI(), "ws://localhost:${port}".toURI(),
"http://localhost:${port}".toURI(), "http://localhost:${port}".toURI(),
Schedulers.boundedElastic(),
Schedulers.boundedElastic() Schedulers.boundedElastic()
) )
).create(upstream).getConnection() ).create(upstream).getConnection()

View File

@@ -46,7 +46,8 @@ class WsConnectionImplSpec extends Specification {
Chain.ETHEREUM__MAINNET, Chain.ETHEREUM__MAINNET,
new URI("http://localhost"), new URI("http://localhost"),
new URI("http://localhost"), new URI("http://localhost"),
Schedulers.boundedElastic() Schedulers.boundedElastic(),
Schedulers.boundedElastic(),
) )
) )
def apiMock = TestingCommons.api() def apiMock = TestingCommons.api()
@@ -81,7 +82,8 @@ class WsConnectionImplSpec extends Specification {
Chain.ETHEREUM__MAINNET, Chain.ETHEREUM__MAINNET,
new URI("http://localhost"), new URI("http://localhost"),
new URI("http://localhost"), new URI("http://localhost"),
Schedulers.boundedElastic() Schedulers.boundedElastic(),
Schedulers.boundedElastic(),
) )
) )
def apiMock = TestingCommons.api() def apiMock = TestingCommons.api()
@@ -114,7 +116,8 @@ class WsConnectionImplSpec extends Specification {
Chain.ETHEREUM__MAINNET, Chain.ETHEREUM__MAINNET,
new URI("http://localhost"), new URI("http://localhost"),
new URI("http://localhost"), new URI("http://localhost"),
Schedulers.boundedElastic() Schedulers.boundedElastic(),
Schedulers.boundedElastic(),
) )
) )
def apiMock = TestingCommons.api() def apiMock = TestingCommons.api()

View File

@@ -27,6 +27,7 @@ import org.mockserver.integration.ClientAndServer
import org.mockserver.model.HttpRequest import org.mockserver.model.HttpRequest
import org.mockserver.model.HttpResponse import org.mockserver.model.HttpResponse
import org.springframework.util.SocketUtils import org.springframework.util.SocketUtils
import reactor.core.scheduler.Schedulers
import spock.lang.Specification import spock.lang.Specification
import java.time.Duration import java.time.Duration
@@ -52,7 +53,7 @@ class JsonRpcHttpReaderSpec extends Specification {
def "Make a request"() { def "Make a request"() {
setup: setup:
JsonRpcHttpReader client = new JsonRpcHttpReader("localhost:${port}", 50, 50, metrics,null, null) JsonRpcHttpReader client = new JsonRpcHttpReader("localhost:${port}", 50, 50, metrics, Schedulers.boundedElastic(),null, null)
def resp = '{' + def resp = '{' +
' "jsonrpc": "2.0",' + ' "jsonrpc": "2.0",' +
' "result": "0x98de45",' + ' "result": "0x98de45",' +
@@ -73,7 +74,7 @@ class JsonRpcHttpReaderSpec extends Specification {
def "Produces RPC Exception on error status code"() { def "Produces RPC Exception on error status code"() {
setup: setup:
def client = new JsonRpcHttpReader("localhost:${port}", 50, 50, metrics, null, null) def client = new JsonRpcHttpReader("localhost:${port}", 50, 50, metrics, Schedulers.boundedElastic(), null, null)
mockServer.when( mockServer.when(
HttpRequest.request() HttpRequest.request()
@@ -97,7 +98,7 @@ class JsonRpcHttpReaderSpec extends Specification {
def "Tries to extract message if HTTP error if it still contains a JSON RPC message"() { def "Tries to extract message if HTTP error if it still contains a JSON RPC message"() {
setup: setup:
def client = new JsonRpcHttpReader("localhost:${port}", 50, 50, metrics, null, null) def client = new JsonRpcHttpReader("localhost:${port}", 50, 50, metrics, Schedulers.boundedElastic(), null, null)
mockServer.when( mockServer.when(
HttpRequest.request() HttpRequest.request()

View File

@@ -19,6 +19,7 @@ import org.junit.jupiter.params.provider.MethodSource
import org.mockito.Mockito.mockConstruction import org.mockito.Mockito.mockConstruction
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import reactor.core.scheduler.Schedulers
import reactor.core.scheduler.Schedulers.immediate import reactor.core.scheduler.Schedulers.immediate
import java.io.File import java.io.File
import java.net.URI import java.net.URI
@@ -39,6 +40,8 @@ class GenericConnectorFactoryCreatorTest {
immediate(), immediate(),
immediate(), immediate(),
MonitoringConfig.default(), MonitoringConfig.default(),
Schedulers.boundedElastic(),
Schedulers.boundedElastic(),
) )
var args: List<*>? = null var args: List<*>? = null