Merge pull request #124 from p2p-org/ws_connection_pool
Add ws connection pool
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user