Merge pull request #124 from p2p-org/ws_connection_pool

Add ws connection pool
This commit is contained in:
KirillPamPam
2023-01-26 19:17:05 +04:00
committed by GitHub
24 changed files with 576 additions and 257 deletions

View File

@@ -19,7 +19,7 @@ class WsConnectionImplRealSpec extends Specification {
@Shared
MockWSServer server
@Shared
WsConnectionImpl conn
WsConnection conn
def setup() {
if (System.getenv("CI") == "true") {
@@ -31,7 +31,16 @@ class WsConnectionImplRealSpec extends Specification {
server = new MockWSServer(port)
server.start()
Thread.sleep(SLEEP)
conn = new EthereumWsFactory("test", Chain.ETHEREUM, "ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(null)
conn = new EthereumWsConnectionPoolFactory(
"test",
1,
new EthereumWsConnectionFactory(
"test",
Chain.ETHEREUM,
"ws://localhost:${port}".toURI(),
"http://localhost:${port}".toURI()
)
).create(null).getConnection()
}
def cleanup() {
@@ -101,7 +110,16 @@ class WsConnectionImplRealSpec extends Specification {
def up = Mock(DefaultUpstream) {
_ * getId() >> "test"
}
conn = new EthereumWsFactory("test", Chain.ETHEREUM, "ws://localhost:${port}".toURI(), "http://localhost:${port}".toURI()).create(up)
conn = new EthereumWsConnectionPoolFactory(
"test",
1,
new EthereumWsConnectionFactory(
"test",
Chain.ETHEREUM,
"ws://localhost:${port}".toURI(),
"http://localhost:${port}".toURI()
)
).create(up).getConnection()
when:
conn.connect()
conn.reconnectIntervalSeconds = 10

View File

@@ -15,33 +15,36 @@
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Chain
import io.emeraldpay.dshackle.Global
import io.emeraldpay.dshackle.data.BlockContainer
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.etherjar.domain.BlockHash
import io.emeraldpay.etherjar.domain.TransactionId
import io.emeraldpay.etherjar.rpc.RpcResponseError
import io.emeraldpay.etherjar.rpc.json.BlockJson
import io.emeraldpay.etherjar.rpc.json.TransactionJson
import io.emeraldpay.etherjar.rpc.json.TransactionRefJson
import io.emeraldpay.dshackle.Chain
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
import java.time.Instant
import java.time.temporal.ChronoUnit
class WsConnectionImplSpec extends Specification {
def "Makes a RPC call"() {
setup:
def wsf = new EthereumWsFactory("test", Chain.ETHEREUM, new URI("http://localhost"), new URI("http://localhost"))
def wsf = new EthereumWsConnectionPoolFactory(
"test",
1,
new EthereumWsConnectionFactory(
"test",
Chain.ETHEREUM,
new URI("http://localhost"),
new URI("http://localhost")
)
)
def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create(null)
def ws = wsf.create(null).getConnection() as WsConnectionImpl
def tx = new TransactionJson().tap {
hash = TransactionId.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200")
@@ -63,10 +66,19 @@ class WsConnectionImplSpec extends Specification {
def "Makes a RPC call - return null"() {
setup:
def wsf = new EthereumWsFactory("test", Chain.ETHEREUM, new URI("http://localhost"), new URI("http://localhost"))
def wsf = new EthereumWsConnectionPoolFactory(
"test",
1,
new EthereumWsConnectionFactory(
"test",
Chain.ETHEREUM,
new URI("http://localhost"),
new URI("http://localhost")
)
)
def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create(null)
def ws = wsf.create(null).getConnection()
apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"], null)
@@ -86,10 +98,19 @@ class WsConnectionImplSpec extends Specification {
def "Makes a RPC call - return error"() {
setup:
def wsf = new EthereumWsFactory("test", Chain.ETHEREUM, new URI("http://localhost"), new URI("http://localhost"))
def wsf = new EthereumWsConnectionPoolFactory(
"test",
1,
new EthereumWsConnectionFactory(
"test",
Chain.ETHEREUM,
new URI("http://localhost"),
new URI("http://localhost")
)
)
def apiMock = TestingCommons.api()
def wsApiMock = apiMock.asWebsocket()
def ws = wsf.create(null)
def ws = wsf.create(null).getConnection()
apiMock.answerOnce("eth_getTransactionByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200"],
new RpcResponseError(RpcResponseError.CODE_METHOD_NOT_EXIST, "test"))

View File

@@ -0,0 +1,140 @@
/**
* Copyright (c) 2022 EmeraldPay, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.upstream.DefaultUpstream
import spock.lang.Specification
import java.util.concurrent.ScheduledExecutorService
class WsConnectionMultiPoolSpec extends Specification {
def "create connection when less than required"() {
setup:
def conn = Mock(WsConnection)
def up = Mock(DefaultUpstream)
def factory = Mock(EthereumWsConnectionFactory)
def pool = new WsConnectionMultiPool(factory, up, 3)
pool.scheduler = Stub(ScheduledExecutorService)
when:
pool.connect()
then:
1 * factory.createWsConnection(0, _) >> conn
1 * conn.connect()
}
def "create connection until target"() {
setup:
def conn1 = Mock(WsConnection)
def conn2 = Mock(WsConnection)
def conn3 = Mock(WsConnection)
def up = Mock(DefaultUpstream)
def factory = Mock(EthereumWsConnectionFactory)
def pool = new WsConnectionMultiPool(factory, up, 3)
pool.scheduler = Stub(ScheduledExecutorService)
when:
pool.connect()
then:
1 * factory.createWsConnection(0, _) >> conn1
1 * conn1.connect()
when:
pool.connect()
then:
1 * conn1.isConnected() >> true
1 * factory.createWsConnection(1, _) >> conn2
1 * conn2.connect()
when:
pool.connect()
then:
1 * conn1.isConnected() >> true
1 * conn2.isConnected() >> true
1 * factory.createWsConnection(2, _) >> conn3
1 * conn3.connect()
when:
pool.connect()
then:
1 * conn1.isConnected() >> true
1 * conn2.isConnected() >> true
1 * conn3.isConnected() >> true
0 * factory.createWsConnection(_, _)
}
def "recreate connection after failure"() {
setup:
def conn1 = Mock(WsConnection)
def conn2 = Mock(WsConnection)
def conn3 = Mock(WsConnection)
def conn4 = Mock(WsConnection)
def up = Mock(DefaultUpstream)
def factory = Mock(EthereumWsConnectionFactory)
def pool = new WsConnectionMultiPool(factory, up, 3)
pool.scheduler = Stub(ScheduledExecutorService)
when: "initial fill"
pool.connect()
pool.connect()
pool.connect()
then:
_ * conn1.isConnected() >> true
_ * conn2.isConnected() >> true
_ * conn3.isConnected() >> true
1 * factory.createWsConnection(0, _) >> conn1
1 * factory.createWsConnection(1, _) >> conn2
1 * factory.createWsConnection(2, _) >> conn3
1 * conn1.connect()
1 * conn2.connect()
1 * conn3.connect()
when: "all ok"
pool.connect()
then:
1 * conn1.isConnected() >> true
1 * conn2.isConnected() >> true
1 * conn3.isConnected() >> true
0 * factory.createWsConnection(_, _)
when: "one failed"
pool.connect()
then:
(1.._) * conn1.isConnected() >> true
(1.._) * conn2.isConnected() >> false
(1.._) * conn3.isConnected() >> true
0 * factory.createWsConnection(_, _) // doesn't create immediately, but schedules it for the next adjust
1 * conn2.close()
when: "needs one more"
pool.connect()
then:
1 * conn1.isConnected() >> true
1 * conn3.isConnected() >> true
1 * factory.createWsConnection(3, _) >> conn4
1 * conn4.connect()
}
}

View File

@@ -20,8 +20,6 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcWsMessage
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.Sinks
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
@@ -38,8 +36,11 @@ class WsSubscriptionsImplSpec extends Specification {
]
)
def conn = Mock(WsConnectionImpl)
def ws = new WsSubscriptionsImpl(conn)
def conn = Mock(WsConnection)
def pool = Mock(WsConnectionPool) {
getConnection() >> conn
}
def ws = new WsSubscriptionsImpl(pool)
when:
def act = ws.subscribe("foo_bar")
@@ -70,8 +71,11 @@ class WsSubscriptionsImplSpec extends Specification {
]
)
def conn = Mock(WsConnectionImpl)
def ws = new WsSubscriptionsImpl(conn)
def conn = Mock(WsConnection)
def pool = Mock(WsConnectionPool) {
getConnection() >> conn
}
def ws = new WsSubscriptionsImpl(pool)
when:
def act = ws.subscribe("foo_bar")

View File

@@ -1,70 +0,0 @@
package io.emeraldpay.dshackle.upstream.rpcclient
import io.emeraldpay.dshackle.reader.Reader
import reactor.core.publisher.Mono
import spock.lang.Specification
import java.time.Duration
class JsonRpcSwitchClientSpec extends Specification {
def "Uses primary response if it works"() {
setup:
def primaryCalled = false
def secondaryCalled = false
def request = new JsonRpcRequest("eth_test", [])
def response = JsonRpcResponse.ok("test".bytes, new JsonRpcResponse.NumberId(100))
def primary = Mock(Reader<JsonRpcRequest, JsonRpcResponse>) {
1 * read(request) >> Mono.fromCallable {
primaryCalled = true
response
}
}
def secondary = Mock(Reader<JsonRpcRequest, JsonRpcResponse>) {
_ * read(request) >> Mono.fromCallable {
secondaryCalled = true
response
}
}
def client = new JsonRpcSwitchClient(primary, secondary)
when:
def act = client.read(request).block(Duration.ofSeconds(1))
then:
act == response
primaryCalled
!secondaryCalled
}
def "Uses secondary response if primary fails"() {
setup:
def primaryCalled = false
def secondaryCalled = false
def request = new JsonRpcRequest("eth_test", [])
def response = JsonRpcResponse.ok("test".bytes, new JsonRpcResponse.NumberId(100))
def primary = Mock(Reader<JsonRpcRequest, JsonRpcResponse>) {
1 * read(request) >> Mono.fromCallable {
primaryCalled = true
throw new IllegalStateException("Primary Fail")
}
}
def secondary = Mock(Reader<JsonRpcRequest, JsonRpcResponse>) {
1 * read(request) >> Mono.fromCallable {
secondaryCalled = true
response
}
}
def client = new JsonRpcSwitchClient(primary, secondary)
when:
def act = client.read(request).block(Duration.ofSeconds(1))
then:
act == response
primaryCalled
secondaryCalled
}
}

View File

@@ -1,6 +1,7 @@
package io.emeraldpay.dshackle.upstream.rpcclient
import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionImpl
import io.emeraldpay.dshackle.upstream.ethereum.WsConnection
import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPool
import reactor.core.Exceptions
import spock.lang.Specification
@@ -10,8 +11,11 @@ class JsonRpcWsClientSpec extends Specification {
def "Produce error if WS is not connected"() {
setup:
def ws = Mock(WsConnectionImpl)
def client = new JsonRpcWsClient(ws)
def ws = Mock(WsConnection)
def pool = Mock(WsConnectionPool) {
getConnection() >> ws
}
def client = new JsonRpcWsClient(pool)
when:
client.read(new JsonRpcRequest("foo_bar", [], 1))
.block(Duration.ofSeconds(1))