problem: not subscribing to an address

This commit is contained in:
Igor Artamonov
2019-07-30 22:57:58 -04:00
parent 913ad6965a
commit fd8f558d64
6 changed files with 221 additions and 39 deletions

View File

@@ -60,7 +60,7 @@ dependencies {
compile "org.springframework.security:spring-security-core:$springVersion" compile "org.springframework.security:spring-security-core:$springVersion"
compile "org.springframework.security:spring-security-web:$springVersion" compile "org.springframework.security:spring-security-web:$springVersion"
compile "org.springframework.security:spring-security-config:$springVersion" compile "org.springframework.security:spring-security-config:$springVersion"
compile 'io.projectreactor:reactor-core:3.2.9.RELEASE' compile "io.projectreactor:reactor-core:$reactorVersion"
compile 'io.projectreactor.addons:reactor-extra:3.2.3.RELEASE' compile 'io.projectreactor.addons:reactor-extra:3.2.3.RELEASE'
compile 'io.projectreactor.kotlin:reactor-kotlin-extensions:1.0.0.M1' compile 'io.projectreactor.kotlin:reactor-kotlin-extensions:1.0.0.M1'
compile 'com.salesforce.servicelibs:reactor-grpc:0.10.0' compile 'com.salesforce.servicelibs:reactor-grpc:0.10.0'
@@ -97,6 +97,7 @@ dependencies {
testCompile 'cglib:cglib-nodep:3.2.12' testCompile 'cglib:cglib-nodep:3.2.12'
testCompile "org.spockframework:spock-core:$spockVersion" testCompile "org.spockframework:spock-core:$spockVersion"
testCompile "io.grpc:grpc-testing:${grpcVersion}" testCompile "io.grpc:grpc-testing:${grpcVersion}"
testCompile "io.projectreactor:reactor-test:$reactorVersion"
} }
compileKotlin { compileKotlin {

View File

@@ -12,6 +12,7 @@ protobufVersion=3.7.1
# Core # Core
springBootVersion=2.1.4.RELEASE springBootVersion=2.1.4.RELEASE
springVersion=5.1.4.RELEASE springVersion=5.1.4.RELEASE
reactorVersion=3.2.9.RELEASE
# Our Libs # Our Libs
etherjarVersion=0.7.0-SNAPSHOT etherjarVersion=0.7.0-SNAPSHOT

View File

@@ -4,13 +4,17 @@ import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableAsync import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import reactor.core.scheduler.Scheduler
import reactor.core.scheduler.Schedulers
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.Executors
@Configuration @Configuration
@EnableScheduling @EnableScheduling
@@ -31,4 +35,9 @@ open class Config {
return objectMapper return objectMapper
} }
@Bean @Qualifier("upstreamScheduler")
open fun upstreamScheduler(): Scheduler {
return Schedulers.fromExecutorService(Executors.newFixedThreadPool(16))
}
} }

View File

@@ -17,22 +17,25 @@ import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toFlux import reactor.core.publisher.toFlux
import reactor.math.sum import reactor.core.scheduler.Scheduler
import java.lang.Exception import java.lang.Exception
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicLong
import javax.annotation.PostConstruct import javax.annotation.PostConstruct
@Service @Service
class TrackAddress( class TrackAddress(
@Autowired private val upstreams: Upstreams, @Autowired private val upstreams: Upstreams,
@Autowired private val availableChains: AvailableChains @Autowired private val availableChains: AvailableChains,
@Autowired private val upstreamScheduler: Scheduler
) { ) {
private val log = LoggerFactory.getLogger(TrackAddress::class.java) private val log = LoggerFactory.getLogger(TrackAddress::class.java)
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedAddress>>() private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedAddress>>()
private val seq = AtomicLong(0)
@PostConstruct @PostConstruct
fun init() { fun init() {
@@ -40,7 +43,7 @@ class TrackAddress(
if (!clients.containsKey(chain)) { if (!clients.containsKey(chain)) {
clients[chain] = ConcurrentLinkedQueue() clients[chain] = ConcurrentLinkedQueue()
upstreams.getUpstream(chain)?.getHead()?.let { head -> upstreams.getUpstream(chain)?.getHead()?.let { head ->
head.getFlux().subscribe { verifyAll(chain) } head.getFlux().subscribe { updateBalancesAll(chain) }
} }
} }
} }
@@ -60,7 +63,19 @@ class TrackAddress(
} }
} }
fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux<SimpleAddress> { private fun startTracking(client: TrackedAddress) {
clients[client.chain]?.add(client) ?: log.warn("Chain ${client.chain} is not available for tracking")
}
private fun stopTracking(client: TrackedAddress) {
clients[client.chain]?.remove(client) ?: log.warn("Chain ${client.chain} is not available for tracking")
}
fun isTracked(chain: Chain, address: Address): Boolean {
return clients[chain]?.any { it.address == address } ?: false
}
private fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux<SimpleAddress> {
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
if (!availableChains.supports(chain)) { if (!availableChains.supports(chain)) {
return Flux.error(Exception("Unsupported chain ${request.asset.chainValue}")) return Flux.error(Exception("Unsupported chain ${request.asset.chainValue}"))
@@ -81,35 +96,37 @@ class TrackAddress(
} }
} }
fun initializeSubscription(request: BlockchainOuterClass.BalanceRequest, observer: TopicProcessor<BlockchainOuterClass.AddressBalance>): Flux<TrackedAddress> { private fun initializeSubscription(request: BlockchainOuterClass.BalanceRequest, observer: TopicProcessor<BlockchainOuterClass.AddressBalance>): Flux<TrackedAddress> {
return initializeSimple(request) return initializeSimple(request)
.map { .map {
it.asTracked(observer) it.asTracked(observer, seq.incrementAndGet())
} }
} }
fun send(request: BlockchainOuterClass.BalanceRequest, addresses: List<TrackedAddress>): Mono<Long> {
val chain = Chain.byId(request.asset.chainValue)
return verify(chain, addresses)
.map { updated -> notify(updated); 1 }
.sum()
}
fun subscribe(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> { fun subscribe(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request -> return requestMono.flatMapMany { request ->
val chain = Chain.byId(request.asset.chainValue) val bus = TopicProcessor.create<BlockchainOuterClass.AddressBalance>()
val sender = TopicProcessor.create<BlockchainOuterClass.AddressBalance>() initializeSubscription(request, bus)
initializeSubscription(request, sender) .flatMap { tracked ->
.doOnNext { tracked -> clients[chain]?.add(tracked) } val current = getBalance(tracked).map {
.thenMany(sender) tracked.withBalance(it)
}.doOnNext {
startTracking(it)
}.map {
buildResponse(it)
}
Flux.merge(current, bus).doFinally { stopTracking(tracked) }
}.doOnError { t ->
log.warn("Failed to process subscription", t)
}
} }
} }
fun getBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> { fun getBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request -> return requestMono.flatMapMany { request ->
initializeSimple(request) initializeSimple(request)
.flatMap { getBalance(it) } .flatMap { a -> getBalance(a).map { a.withBalance(it) } }
.map { process(it) } .map { buildResponse(it) }
} }
} }
@@ -121,33 +138,32 @@ class TrackAddress(
) )
} }
private fun verifyAll(chain: Chain) { private fun updateBalancesAll(chain: Chain) {
clients[chain]?.let { all -> clients[chain]?.let { all ->
all.toFlux() all.toFlux()
.buffer(20) .buffer(20)
.map { group -> .map { group ->
verify(chain, group).subscribe { updated -> notify(updated) } updateBalances(chain, group).subscribe { updated -> notify(updated) }
} }
.subscribe() .subscribe()
} }
} }
fun getBalance(addr: SimpleAddress): Mono<SimpleAddress> { fun getBalance(addr: SimpleAddress): Mono<Wei> {
val up = upstreams.getUpstream(addr.chain) ?: return Mono.error(Exception("Unsupported chain: ${addr.chain}")) val up = upstreams.getUpstream(addr.chain) ?: return Mono.error(Exception("Unsupported chain: ${addr.chain}"))
return up.getApi() return up.getApi()
.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST)) .executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST))
.timeout(Duration.ofSeconds(15)) .timeout(Duration.ofSeconds(15))
.map { value ->
addr.withBalance(value)
}
} }
private fun verify(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> { private fun updateBalances(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> {
val up = upstreams.getUpstream(chain) ?: return Flux.empty<TrackedAddress>() val up = upstreams.getUpstream(chain) ?: return Flux.empty<TrackedAddress>()
return group.toFlux() return group.toFlux()
.parallel(8).runOn(upstreamScheduler)
.flatMap { a -> .flatMap { a ->
getBalance(a).map { Update(a, it.balance!!) } getBalance(a).map { Update(a, it) }
} }
.sequential()
.filter { .filter {
it.addr.balance == null || it.addr.balance != it.value it.addr.balance == null || it.addr.balance != it.value
} }
@@ -159,27 +175,26 @@ class TrackAddress(
} }
} }
private fun process(address: SimpleAddress): BlockchainOuterClass.AddressBalance { private fun buildResponse(address: SimpleAddress): BlockchainOuterClass.AddressBalance {
return BlockchainOuterClass.AddressBalance.newBuilder() return BlockchainOuterClass.AddressBalance.newBuilder()
.setBalance(address.balance!!.amount!!.toString(10)) .setBalance(address.balance!!.amount!!.toString(10))
.setAsset(Common.Asset.newBuilder() .setAsset(Common.Asset.newBuilder()
.setChainValue(address.chain.id) .setChainValue(address.chain.id)
.setCode("ETHER") .setCode("ETHER"))
)
.setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.toHex())) .setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.toHex()))
.build() .build()
} }
private fun notify(address: TrackedAddress) { private fun notify(address: TrackedAddress) {
address.stream.onNext(process(address))
address.lastPing = Instant.now() address.lastPing = Instant.now()
address.stream.onNext(buildResponse(address))
} }
class Update(val addr: TrackedAddress, val value: Wei) class Update(val addr: TrackedAddress, val value: Wei)
open class SimpleAddress(val chain: Chain, val address: Address, var balance: Wei? = null) { open class SimpleAddress(val chain: Chain, val address: Address, var balance: Wei? = null) {
fun asTracked(stream: TopicProcessor<BlockchainOuterClass.AddressBalance>): TrackedAddress { fun asTracked(stream: TopicProcessor<BlockchainOuterClass.AddressBalance>, id: Long): TrackedAddress {
return TrackedAddress(chain, stream, address, balance = this.balance) return TrackedAddress(chain, stream, address, balance = this.balance, id = id)
} }
open fun withBalance(balance: Wei) = SimpleAddress(chain, address, balance) open fun withBalance(balance: Wei) = SimpleAddress(chain, address, balance)
@@ -189,8 +204,13 @@ class TrackAddress(
val stream: TopicProcessor<BlockchainOuterClass.AddressBalance>, val stream: TopicProcessor<BlockchainOuterClass.AddressBalance>,
address: Address, address: Address,
var lastPing: Instant = Instant.now(), var lastPing: Instant = Instant.now(),
balance: Wei? = null balance: Wei? = null,
val id: Long
): SimpleAddress(chain, address, balance) { ): SimpleAddress(chain, address, balance) {
override fun withBalance(balance: Wei) = TrackedAddress(chain, stream, address, lastPing, balance); override fun withBalance(balance: Wei) = TrackedAddress(chain, stream, address, lastPing, balance, id)
override fun equals(other: Any?): Boolean {
return other != null && other is TrackedAddress && other.id == id
}
} }
} }

View File

@@ -0,0 +1,129 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.test.EthereumApiMock
import io.emeraldpay.dshackle.test.TestingCommons
import io.emeraldpay.dshackle.upstream.AggregatedUpstreams
import io.emeraldpay.dshackle.upstream.AvailableChains
import io.emeraldpay.dshackle.upstream.EthereumHead
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.rpc.RpcClient
import io.infinitape.etherjar.rpc.json.BlockJson
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.core.scheduler.Schedulers
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
class TrackAddressSpec extends Specification {
AvailableChains availableChains = new AvailableChains()
Upstreams upstreams
TrackAddress trackAddress
def chain = Common.ChainRef.CHAIN_ETHEREUM
def address1 = "0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f"
def address1Proto = Common.SingleAddress.newBuilder()
.setAddress(address1)
def etherAsset = Common.Asset.newBuilder()
.setChain(chain)
.setCode("ETHER")
def setup() {
upstreams = Mock(Upstreams)
trackAddress = new TrackAddress(upstreams, availableChains, Schedulers.immediate())
}
def start() {
trackAddress.init()
availableChains.add(Chain.ETHEREUM)
availableChains.add(Chain.TESTNET_KOVAN)
}
def "get balance"() {
setup:
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
.setAsset(etherAsset)
.setAddress(Common.AnyAddress.newBuilder().setAddressSingle(address1Proto).build())
.build()
def exp = BlockchainOuterClass.AddressBalance.newBuilder()
.setAddress(address1Proto)
.setAsset(etherAsset)
.setBalance("1234567890")
.build()
def upstreamMock = Mock(AggregatedUpstreams)
def apiMock = new EthereumApiMock(Mock(RpcClient), TestingCommons.objectMapper(), Chain.ETHEREUM)
apiMock.answer("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
_ * upstreamMock.getApi() >> apiMock
start()
when:
def flux = trackAddress.getBalance(Mono.just(req))
then:
StepVerifier.create(flux)
.expectNext(exp)
.expectComplete()
.verify(Duration.ofSeconds(3))
!trackAddress.isTracked(Chain.ETHEREUM, Address.from(address1))
}
def "recheck address after each block"() {
setup:
def req = BlockchainOuterClass.BalanceRequest.newBuilder()
.setAsset(etherAsset)
.setAddress(Common.AnyAddress.newBuilder().setAddressSingle(address1Proto).build())
.build()
def exp1 = BlockchainOuterClass.AddressBalance.newBuilder()
.setAddress(address1Proto)
.setAsset(etherAsset)
.setBalance("1234567890")
.build()
def exp2 = BlockchainOuterClass.AddressBalance.newBuilder()
.setAddress(address1Proto)
.setAsset(etherAsset)
.setBalance("65432")
.build()
def block2 = new BlockJson().with {
it.number = 1
it.totalDifficulty = 100
it.hash = BlockHash.from("0xa0e65cbc1b52a8ca60562112c6060552d882f16f34a9dba2ccdc05c0a6a27c22")
return it
}
def blocksBus = TopicProcessor.create()
def upstreamMock = Mock(AggregatedUpstreams)
def headMock = Mock(EthereumHead)
def apiMock = new EthereumApiMock(Mock(RpcClient), TestingCommons.objectMapper(), Chain.ETHEREUM)
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0x499602D2")
apiMock.answerOnce("eth_getBalance", ["0xe2c8fa8120d813cd0b5e6add120295bf20cfa09f", "latest"], "0xff98")
_ * upstreams.getUpstream(Chain.ETHEREUM) >> upstreamMock
_ * upstreamMock.getApi() >> apiMock
_ * upstreamMock.getHead() >> headMock
_ * headMock.getFlux() >> blocksBus
start()
when:
def flux = trackAddress.subscribe(Mono.just(req))
then:
StepVerifier.create(flux)
.expectNext(exp1)
.then {
assert trackAddress.isTracked(Chain.ETHEREUM, Address.from(address1))
}
.then {
blocksBus.onNext(block2)
}
.expectNext(exp2)
.thenCancel()
.verify(Duration.ofSeconds(3))
!trackAddress.isTracked(Chain.ETHEREUM, Address.from(address1))
}
}

View File

@@ -25,8 +25,12 @@ class EthereumApiMock extends EthereumApi {
this.objectMapper = objectMapper this.objectMapper = objectMapper
} }
EthereumApiMock answer(@NotNull String method, List<Object> params, Object result) { EthereumApiMock answerOnce(@NotNull String method, List<Object> params, Object result) {
predefined << new PredefinedResponse(method: method, params: params, result: result) return answer(method, params, result, 1)
}
EthereumApiMock answer(@NotNull String method, List<Object> params, Object result, Integer limit = null) {
predefined << new PredefinedResponse(method: method, params: params, result: result, limit: limit)
return this return this
} }
@@ -40,6 +44,8 @@ class EthereumApiMock extends EthereumApi {
log.error("Method ${method} with ${params} is not mocked") log.error("Method ${method} with ${params} is not mocked")
json.error = new RpcResponseError(-32601, "Method ${method} with ${params} is not mocked") json.error = new RpcResponseError(-32601, "Method ${method} with ${params} is not mocked")
} }
predefined.onCalled()
predefined.print()
return Mono.just(objectMapper.writeValueAsBytes(json)) return Mono.just(objectMapper.writeValueAsBytes(json))
} }
@@ -61,8 +67,14 @@ class EthereumApiMock extends EthereumApi {
String method String method
List params List params
Object result Object result
Integer limit
boolean isSame(int id, String method, List<?> params) { boolean isSame(int id, String method, List<?> params) {
if (limit != null) {
if (limit <= 0) {
return false
}
}
if (method != this.method) { if (method != this.method) {
return false return false
} }
@@ -71,5 +83,15 @@ class EthereumApiMock extends EthereumApi {
} }
return this.params == params return this.params == params
} }
void onCalled() {
if (limit != null) {
limit--
}
}
void print() {
println "Execute API: $method ${params ? params : '_'} >> $result"
}
} }
} }