solution: use reactor based stubs for grpc server

This commit is contained in:
Igor Artamonov
2019-07-29 22:27:15 -04:00
parent 4c9343c146
commit 36e089f65f
18 changed files with 448 additions and 304 deletions

View File

@@ -1,6 +1,7 @@
package io.emeraldpay.dshackle
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import org.springframework.context.annotation.Bean
@@ -22,6 +23,7 @@ open class Config {
val objectMapper = ObjectMapper()
objectMapper.registerModule(module)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
objectMapper
.setDateFormat(SimpleDateFormat("yyyy-MM-dd\'T\'HH:mm:ss.SSS"))
.setTimeZone(TimeZone.getTimeZone("UTC"))

View File

@@ -1,15 +1,14 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainGrpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.grpc.Chain
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.grpc.stub.StreamObserver
import io.infinitape.etherjar.domain.TransactionId
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.time.Instant
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Service
class BlockchainRpc(
@@ -19,51 +18,35 @@ class BlockchainRpc(
@Autowired private val trackAddress: TrackAddress,
@Autowired private val describe: Describe,
@Autowired private val subscribeStatus: SubscribeStatus
): BlockchainGrpc.BlockchainImplBase() {
): ReactorBlockchainGrpc.BlockchainImplBase() {
private val log = LoggerFactory.getLogger(BlockchainRpc::class.java)
override fun nativeCall(request: BlockchainOuterClass.NativeCallRequest, responseObserver: StreamObserver<BlockchainOuterClass.NativeCallReplyItem>) {
nativeCall.nativeCall(request, responseObserver)
override fun nativeCall(request: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> {
return nativeCall.nativeCall(request)
}
override fun subscribeHead(request: Common.Chain, responseObserver: StreamObserver<BlockchainOuterClass.ChainHead>) {
streamHead.add(Chain.byId(request.type.number), responseObserver)
override fun subscribeHead(request: Mono<Common.Chain>): Flux<BlockchainOuterClass.ChainHead> {
return streamHead.add(request)
}
override fun subscribeTxStatus(request: BlockchainOuterClass.TxStatusRequest, responseObserver: StreamObserver<BlockchainOuterClass.TxStatus>) {
val tx = TrackTx.TrackedTx(
Chain.byId(request.chainValue),
StreamSender(responseObserver),
Instant.now(),
TransactionId.from(request.txId),
Math.min(Math.max(1, request.confirmationLimit), 100)
)
trackTx.add(tx)
override fun subscribeTxStatus(request: Mono<BlockchainOuterClass.TxStatusRequest>): Flux<BlockchainOuterClass.TxStatus> {
return trackTx.add(request)
}
override fun subscribeBalance(request: BlockchainOuterClass.BalanceRequest, responseObserver: StreamObserver<BlockchainOuterClass.AddressBalance>) {
trackAddress.add(request, responseObserver)
override fun subscribeBalance(request: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return trackAddress.subscribe(request)
}
override fun getBalance(request: BlockchainOuterClass.BalanceRequest, responseObserver: StreamObserver<BlockchainOuterClass.AddressBalance>) {
val addresses = trackAddress.initializeFor(request, responseObserver)
trackAddress.send(request, addresses)
.doOnError { t ->
log.error("Failed to process balance", t)
responseObserver.onError(Exception("Internal error"))
}
.subscribe {
responseObserver.onCompleted()
}
override fun getBalance(request: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return trackAddress.getBalance(request)
}
override fun describe(request: BlockchainOuterClass.DescribeRequest, responseObserver: StreamObserver<BlockchainOuterClass.DescribeResponse>) {
describe.describe(request, responseObserver)
override fun describe(request: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
return describe.describe(request)
}
override fun subscribeStatus(request: BlockchainOuterClass.StatusRequest, responseObserver: StreamObserver<BlockchainOuterClass.ChainStatus>) {
subscribeStatus.subscribeStatus(request, responseObserver)
override fun subscribeStatus(request: Mono<BlockchainOuterClass.StatusRequest>): Flux<BlockchainOuterClass.ChainStatus> {
return subscribeStatus.subscribeStatus(request)
}
}

View File

@@ -10,6 +10,7 @@ import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
@Service
class Describe(
@@ -17,25 +18,26 @@ class Describe(
@Autowired private val subscribeStatus: SubscribeStatus
) {
fun describe(request: BlockchainOuterClass.DescribeRequest, responseObserver: StreamObserver<BlockchainOuterClass.DescribeResponse>) {
val resp = BlockchainOuterClass.DescribeResponse.newBuilder()
upstreams.getAvailable().forEach { chain ->
upstreams.getUpstream(chain)?.let { chainUpstreams ->
chainUpstreams.getAll().let { ups ->
if (ups.isNotEmpty()) {
val status = subscribeStatus.chainStatus(chain, ups)
resp.addChains(
BlockchainOuterClass.DescribeChain.newBuilder()
.setChain(Common.ChainRef.forNumber(chain.id))
.setStatus(status)
.build()
)
fun describe(requestMono: Mono<BlockchainOuterClass.DescribeRequest>): Mono<BlockchainOuterClass.DescribeResponse> {
return requestMono.map { _ ->
val resp = BlockchainOuterClass.DescribeResponse.newBuilder()
upstreams.getAvailable().forEach { chain ->
upstreams.getUpstream(chain)?.let { chainUpstreams ->
chainUpstreams.getAll().let { ups ->
if (ups.isNotEmpty()) {
val status = subscribeStatus.chainStatus(chain, ups)
resp.addChains(
BlockchainOuterClass.DescribeChain.newBuilder()
.setChain(Common.ChainRef.forNumber(chain.id))
.setStatus(status)
.build()
)
}
}
}
}
resp.build()
}
responseObserver.onNext(resp.build())
responseObserver.onCompleted()
}
}

View File

@@ -4,12 +4,15 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.dshackle.upstream.EthereumApi
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.toFlux
import reactor.core.publisher.toMono
import reactor.util.function.Tuples
@@ -23,55 +26,50 @@ class NativeCall(
private val log = LoggerFactory.getLogger(NativeCall::class.java)
open fun nativeCall(request: BlockchainOuterClass.NativeCallRequest, responseObserver: StreamObserver<BlockchainOuterClass.NativeCallReplyItem>) {
val chain= Chain.byId(request.chain.number)
if (chain == Chain.UNSPECIFIED) {
throw Exception("Invalid chain id: ${request.chain.number}")
open fun nativeCall(requestMono: Mono<BlockchainOuterClass.NativeCallRequest>): Flux<BlockchainOuterClass.NativeCallReplyItem> {
return requestMono.flatMapMany { request ->
val chain= Chain.byId(request.chain.number)
if (chain == Chain.UNSPECIFIED) {
throw Exception("Invalid chain id: ${request.chain.number}")
}
val upstream = upstreams.getUpstream(chain)?.getApi() ?: throw Exception("Chain ${chain.id} is unavailable")
request.itemsList.toFlux().map {
val method = it.target
val params = it.payload.toStringUtf8()
CallContext(it.id, upstream, Tuples.of(method, params))
}
}
.map {
val params = extractParams(it.payload.t2)
it.withPayload(Tuples.of(it.payload.t1, params))
}
.flatMap { ctx ->
ctx.upstream.execute(ctx.id, ctx.payload.t1, ctx.payload.t2).map { resp ->
ctx.withPayload(resp)
}.onErrorMap {
CallFailure(ctx.id, it)
}
}
.map {
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setSucceed(true)
.setId(it.id)
.setPayload(ByteString.copyFrom(it.payload))
.build()
}
.onErrorResume() {
val id: Int = if (it != null && CallFailure::class.isInstance(it)) {
(it as CallFailure).id
} else {
log.error("Lost context for a native call", it)
0
}
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setSucceed(false)
.setId(id)
.build()
.toMono()
}
val upstream = upstreams.getUpstream(chain)?.getApi() ?: throw Exception("Chain ${chain.id} is unavailable")
request.itemsList.toFlux()
.map {
val method = it.target
val params = it.payload.toStringUtf8()
return@map CallContext(it.id, Tuples.of(method, params))
}
.map {
val params = extractParams(it.payload.t2)
return@map it.withPayload(Tuples.of(it.payload.t1, params))
}
.flatMap { ctx ->
upstream.execute(ctx.id, ctx.payload.t1, ctx.payload.t2).map { resp ->
ctx.withPayload(resp)
}.onErrorMap {
CallFailure(ctx.id, it)
}
}
.map {
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setSucceed(true)
.setId(it.id)
.setPayload(ByteString.copyFrom(it.payload))
.build()
}
.onErrorResume() {
val id: Int = if (it != null && CallFailure::class.isInstance(it)) {
(it as CallFailure).id
} else {
log.error("Lost context for a native call", it)
0
}
return@onErrorResume BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setSucceed(false)
.setId(id)
.build()
.toMono()
}
.doOnComplete {
responseObserver.onCompleted()
}
.subscribe {
responseObserver.onNext(it)
}
}
private fun extractParams(jsonParams: String): List<Any> {
@@ -79,9 +77,9 @@ class NativeCall(
return req as List<Any>
}
private class CallContext<T>(val id: Int, val payload: T) {
private class CallContext<T>(val id: Int, val upstream: EthereumApi, val payload: T) {
fun <X> withPayload(payload: X): CallContext<X> {
return CallContext(id, payload)
return CallContext(id, upstream, payload)
}
}

View File

@@ -2,15 +2,18 @@ package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.upstream.AvailableChains
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.json.BlockJson
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toFlux
import java.lang.Exception
import java.util.concurrent.ConcurrentLinkedQueue
@@ -19,33 +22,36 @@ import kotlin.collections.HashMap
@Service
class StreamHead(
@Autowired private val upstreams: Upstreams
@Autowired private val upstreams: Upstreams,
@Autowired private val availableChains: AvailableChains
) {
private val log = LoggerFactory.getLogger(StreamHead::class.java)
private val clients = HashMap<Chain, ConcurrentLinkedQueue<StreamSender<BlockchainOuterClass.ChainHead>>>()
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TopicProcessor<BlockchainOuterClass.ChainHead>>>()
@PostConstruct
fun init() {
listOf(Chain.ETHEREUM, Chain.ETHEREUM_CLASSIC, Chain.TESTNET_MORDEN, Chain.TESTNET_KOVAN).forEach { chain ->
if (upstreams.getUpstream(chain)?.getHead() != null) {
clients[chain] = ConcurrentLinkedQueue()
subscribe(chain)
}
availableChains.observe().subscribe { chain ->
clients[chain] = ConcurrentLinkedQueue()
subscribe(chain)
}
}
private fun subscribe(chain: Chain) {
upstreams.getUpstream(chain)!!.getHead().getFlux()
upstreams.getUpstream(chain)?.let { up ->
up.getHead()
.getFlux()
.doOnComplete {
log.info("Closing streams for ${chain.chainCode}")
clients.replace(chain, ConcurrentLinkedQueue())!!.forEach { client ->
try {
client.stream.onCompleted()
} catch (e: Throwable) {}
client.dispose()
} catch (e: Throwable) {
}
}
}
.subscribe { block -> onBlock(chain, block) }
}
}
private fun onBlock(chain: Chain, block: BlockJson<TransactionId>) {
@@ -56,25 +62,28 @@ class StreamHead(
}
}
fun add(chain: Chain, client: StreamObserver<BlockchainOuterClass.ChainHead>) {
val sender = StreamSender(client)
if (!clients.containsKey(chain)) {
client.onError(Exception("Chain ${chain.chainCode} is not available for streaming"))
return
fun add(requestMono: Mono<Common.Chain>): Flux<BlockchainOuterClass.ChainHead> {
return requestMono.map { request ->
Chain.byId(request.type.number)
}.filter {
it != Chain.UNSPECIFIED && clients.containsKey(it)
}.flatMapMany { chain ->
val sender = TopicProcessor.create<BlockchainOuterClass.ChainHead>()
clients[chain]!!.add(sender)
notify(chain, sender)
sender
}
clients[chain]!!.add(sender)
process(chain, sender)
}
fun process(chain: Chain, client: StreamSender<BlockchainOuterClass.ChainHead>): Boolean {
val upstream = upstreams.getUpstream(chain) ?: return false
fun notify(chain: Chain, client: TopicProcessor<BlockchainOuterClass.ChainHead>) {
val upstream = upstreams.getUpstream(chain) ?: return
val head = upstream.getHead().getHead()
return head.map {
head.subscribe {
notify(chain, it, client)
}.defaultIfEmpty(false).block()!!
}
}
fun notify(chain: Chain, block: BlockJson<TransactionId>, client: StreamSender<BlockchainOuterClass.ChainHead>): Boolean {
fun notify(chain: Chain, block: BlockJson<TransactionId>, client: TopicProcessor<BlockchainOuterClass.ChainHead>) {
val data = BlockchainOuterClass.ChainHead.newBuilder()
.setChainValue(chain.id)
.setHeight(block.number)
@@ -82,16 +91,8 @@ class StreamHead(
.setWeight(ByteString.copyFrom(block.totalDifficulty.toByteArray()))
.setBlockId(block.hash.toHex().substring(2))
.build()
var sent: Boolean = false
try {
sent = client.send(data)
if (!sent) {
clients[chain]!!.remove(client)
}
} catch (e: Exception) {
log.error("Send error ${e.javaClass}: ${e.message}")
}
return sent
client.onNext(data)
}
}

View File

@@ -1,26 +0,0 @@
package io.emeraldpay.dshackle.rpc
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.grpc.stub.StreamObserver
import org.slf4j.LoggerFactory
class StreamSender<T>(val stream: StreamObserver<T>) {
private val log = LoggerFactory.getLogger(StreamSender::class.java)
fun send(value: T): Boolean {
try {
stream.onNext(value)
return true
} catch (e: StatusRuntimeException) {
if (e.status.code != Status.CANCELLED.code) {
log.warn("Channel errored with ${e.status}: ${e.message}")
}
} catch (e: Exception) {
log.warn("Channel errored with ${e.javaClass.name}: ${e.message}")
stream.onError(e)
}
return false
}
}

View File

@@ -2,34 +2,32 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.upstream.Upstream
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.dshackle.upstream.*
import io.emeraldpay.grpc.Chain
import io.grpc.StatusRuntimeException
import io.grpc.stub.StreamObserver
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.Disposable
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Service
class SubscribeStatus(
@Autowired private val upstreams: Upstreams
@Autowired private val upstreams: Upstreams,
@Autowired private val availableChains: AvailableChains
) {
fun subscribeStatus(request: BlockchainOuterClass.StatusRequest, responseObserver: StreamObserver<BlockchainOuterClass.ChainStatus>) {
upstreams.getAvailable().forEach { chain ->
var d: Disposable? = null
val chainUpstream = upstreams.getUpstream(chain)
d = chainUpstream?.observeStatus()?.subscribe { availability ->
val status = chainStatus(chain, chainUpstream.getAll())
try {
responseObserver.onNext(status)
} catch (e: StatusRuntimeException) {
// gRPC channel was closed
d?.dispose()
fun subscribeStatus(requestMono: Mono<BlockchainOuterClass.StatusRequest>): Flux<BlockchainOuterClass.ChainStatus> {
return requestMono.flatMapMany {
val ups = availableChains.getAll().mapNotNull { chain ->
val chainUpstream = upstreams.getUpstream(chain)
chainUpstream?.observeStatus()?.map { avail ->
ChainSubscription(chain, chainUpstream, avail)
}
}
Flux.merge(ups)
.map {
chainStatus(it.chain, it.up.getAll())
}
}
}
@@ -40,12 +38,13 @@ class SubscribeStatus(
val quorum = ups.filter {
it.getStatus() > UpstreamAvailability.UNAVAILABLE
}.count()
val status = BlockchainOuterClass.ChainStatus.newBuilder()
return BlockchainOuterClass.ChainStatus.newBuilder()
.setAvailability(BlockchainOuterClass.AvailabilityEnum.forNumber(available.grpcId))
.setChain(Common.ChainRef.forNumber(chain.id))
.setQuorum(quorum)
.build()
return status
}
class ChainSubscription(val chain: Chain, val up: AggregatedUpstreams, val avail: UpstreamAvailability)
}

View File

@@ -2,19 +2,20 @@ package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.dshackle.upstream.AvailableChains
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain
import io.grpc.stub.StreamObserver
import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.domain.Wei
import io.infinitape.etherjar.rpc.Commands
import io.infinitape.etherjar.rpc.json.BlockTag
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toFlux
import reactor.math.sum
import java.lang.Exception
@@ -26,19 +27,21 @@ import javax.annotation.PostConstruct
@Service
class TrackAddress(
@Autowired private val upstreams: Upstreams
@Autowired private val upstreams: Upstreams,
@Autowired private val availableChains: AvailableChains
) {
private val log = LoggerFactory.getLogger(TrackAddress::class.java)
private val clients = HashMap<Chain, ConcurrentLinkedQueue<TrackedAddress>>()
private val allChains = listOf(Chain.TESTNET_MORDEN, Chain.ETHEREUM_CLASSIC, Chain.ETHEREUM, Chain.TESTNET_KOVAN)
@PostConstruct
fun init() {
allChains.forEach { chain ->
clients[chain] = ConcurrentLinkedQueue()
upstreams.getUpstream(chain)?.getHead()?.let { head ->
head.getFlux().subscribe { verifyAll(chain) }
availableChains.observe().subscribe { chain ->
if (!clients.containsKey(chain)) {
clients[chain] = ConcurrentLinkedQueue()
upstreams.getUpstream(chain)?.getHead()?.let { head ->
head.getFlux().subscribe { verifyAll(chain) }
}
}
}
}
@@ -46,7 +49,7 @@ class TrackAddress(
@Scheduled(fixedDelay = 120_000)
fun pingOld() {
val period = Duration.ofMinutes(15)
allChains.forEach { chain ->
availableChains.getAll().forEach { chain ->
clients[chain]?.let { clients ->
clients.toFlux().filter {
it.lastPing < Instant.now().minus(period)
@@ -57,26 +60,32 @@ class TrackAddress(
}
}
fun initializeFor(request: BlockchainOuterClass.BalanceRequest, responseObserver: StreamObserver<BlockchainOuterClass.AddressBalance>): List<TrackedAddress> {
fun initializeSimple(request: BlockchainOuterClass.BalanceRequest): Flux<SimpleAddress> {
val chain = Chain.byId(request.asset.chainValue)
if (!allChains.contains(chain)) {
responseObserver.onError(Exception("Unsupported chain ${request.asset.chainValue}"))
return Collections.emptyList()
if (!availableChains.supports(chain)) {
return Flux.error(Exception("Unsupported chain ${request.asset.chainValue}"))
}
if (request.asset.code?.toLowerCase() != "ether") {
responseObserver.onError(Exception("Unsupported asset ${request.asset.code}"))
return Collections.emptyList()
return Flux.error(Exception("Unsupported asset ${request.asset.code}"))
}
val new = java.util.ArrayList<TrackedAddress>()
val observer = StreamSender<BlockchainOuterClass.AddressBalance>(responseObserver)
if (request.address.addrTypeCase == Common.AnyAddress.AddrTypeCase.ADDRESS_SINGLE) {
new.add(forAddress(request.address.addressSingle, chain, observer))
} else if (request.address.addrTypeCase == Common.AnyAddress.AddrTypeCase.ADDRESS_MULTI) {
request.address.addressMulti.addressesList.forEach { address ->
new.add(forAddress(address, chain, observer))
return when {
request.address.addrTypeCase == Common.AnyAddress.AddrTypeCase.ADDRESS_SINGLE ->
Flux.just(simpleAddress(request.address.addressSingle, chain))
request.address.addrTypeCase == Common.AnyAddress.AddrTypeCase.ADDRESS_MULTI ->
Flux.fromIterable(request.address.addressMulti.addressesList)
.map { simpleAddress(it, chain) }
else -> {
log.error("Unsupported address type: ${request.address.addrTypeCase}")
Flux.empty()
}
}
return new
}
fun initializeSubscription(request: BlockchainOuterClass.BalanceRequest, observer: TopicProcessor<BlockchainOuterClass.AddressBalance>): Flux<TrackedAddress> {
return initializeSimple(request)
.map {
it.asTracked(observer)
}
}
fun send(request: BlockchainOuterClass.BalanceRequest, addresses: List<TrackedAddress>): Mono<Long> {
@@ -86,21 +95,28 @@ class TrackAddress(
.sum()
}
fun add(request: BlockchainOuterClass.BalanceRequest, responseObserver: StreamObserver<BlockchainOuterClass.AddressBalance>) {
val chain = Chain.byId(request.asset.chainValue)
val new = initializeFor(request, responseObserver)
send(request, new)
.doFinally {
clients[chain]?.addAll(new)
}
.subscribe()
fun subscribe(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request ->
val chain = Chain.byId(request.asset.chainValue)
val sender = TopicProcessor.create<BlockchainOuterClass.AddressBalance>()
initializeSubscription(request, sender)
.doOnNext { tracked -> clients[chain]?.add(tracked) }
.thenMany(sender)
}
}
private fun forAddress(address: Common.SingleAddress, chain: Chain, observer: StreamSender<BlockchainOuterClass.AddressBalance>): TrackedAddress {
fun getBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request ->
initializeSimple(request)
.flatMap { getBalance(it) }
.map { process(it) }
}
}
private fun simpleAddress(address: Common.SingleAddress, chain: Chain): SimpleAddress {
val addressParsed = Address.from(address.address)
return TrackedAddress(
return SimpleAddress(
chain,
observer,
addressParsed
)
}
@@ -116,13 +132,21 @@ class TrackAddress(
}
}
fun getBalance(addr: SimpleAddress): Mono<SimpleAddress> {
val up = upstreams.getUpstream(addr.chain) ?: return Mono.error(Exception("Unsupported chain: ${addr.chain}"))
return up.getApi()
.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST))
.timeout(Duration.ofSeconds(15))
.map { value ->
addr.withBalance(value)
}
}
private fun verify(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> {
val up = upstreams.getUpstream(chain) ?: return Flux.empty<TrackedAddress>()
return group.toFlux()
.flatMap { a ->
up.getApi()
.executeAndConvert(Commands.eth().getBalance(a.address, BlockTag.LATEST))
.map { Update(a, it) }
getBalance(a).map { Update(a, it.balance!!) }
}
.filter {
it.addr.balance == null || it.addr.balance != it.value
@@ -135,31 +159,38 @@ class TrackAddress(
}
}
private fun notify(address: TrackedAddress): Boolean {
val sent = address.stream.send(
BlockchainOuterClass.AddressBalance.newBuilder()
.setBalance(address.balance!!.amount!!.toString(10))
.setAsset(Common.Asset.newBuilder()
.setChainValue(address.chain.id)
.setCode("ETHER")
)
.setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.toHex()))
.build()
)
if (!sent) {
clients[address.chain]?.remove(address)
}
private fun process(address: SimpleAddress): BlockchainOuterClass.AddressBalance {
return BlockchainOuterClass.AddressBalance.newBuilder()
.setBalance(address.balance!!.amount!!.toString(10))
.setAsset(Common.Asset.newBuilder()
.setChainValue(address.chain.id)
.setCode("ETHER")
)
.setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.toHex()))
.build()
}
private fun notify(address: TrackedAddress) {
address.stream.onNext(process(address))
address.lastPing = Instant.now()
return sent
}
class Update(val addr: TrackedAddress, val value: Wei)
class TrackedAddress(val chain: Chain,
val stream: StreamSender<BlockchainOuterClass.AddressBalance>,
val address: Address,
val since: Instant = Instant.now(),
open class SimpleAddress(val chain: Chain, val address: Address, var balance: Wei? = null) {
fun asTracked(stream: TopicProcessor<BlockchainOuterClass.AddressBalance>): TrackedAddress {
return TrackedAddress(chain, stream, address, balance = this.balance)
}
open fun withBalance(balance: Wei) = SimpleAddress(chain, address, balance)
}
class TrackedAddress(chain: Chain,
val stream: TopicProcessor<BlockchainOuterClass.AddressBalance>,
address: Address,
var lastPing: Instant = Instant.now(),
var balance: Wei? = null
)
balance: Wei? = null
): SimpleAddress(chain, address, balance) {
override fun withBalance(balance: Wei) = TrackedAddress(chain, stream, address, lastPing, balance);
}
}

View File

@@ -3,6 +3,7 @@ package io.emeraldpay.dshackle.rpc
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.upstream.AvailableChains
import io.emeraldpay.dshackle.upstream.ConfiguredUpstreams
import io.emeraldpay.dshackle.upstream.Upstreams
import io.emeraldpay.grpc.Chain
@@ -12,7 +13,9 @@ import io.infinitape.etherjar.rpc.Commands
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toFlux
import java.lang.Exception
import java.math.BigInteger
@@ -20,10 +23,13 @@ import java.time.Duration
import java.time.Instant
import java.util.concurrent.ConcurrentLinkedQueue
import javax.annotation.PostConstruct
import kotlin.math.max
import kotlin.math.min
@Service
class TrackTx(
@Autowired private val upstreams: Upstreams
@Autowired private val upstreams: Upstreams,
@Autowired private val availableChains: AvailableChains
) {
private val ZERO_BLOCK = BlockHash.from("0x0000000000000000000000000000000000000000000000000000000000000000")
@@ -33,7 +39,7 @@ class TrackTx(
@PostConstruct
fun init() {
listOf(Chain.TESTNET_MORDEN, Chain.ETHEREUM_CLASSIC, Chain.ETHEREUM, Chain.TESTNET_KOVAN).forEach { chain ->
availableChains.observe().subscribe { chain ->
clients[chain] = ConcurrentLinkedQueue()
upstreams.getUpstream(chain)?.getHead()?.let { head ->
head.getFlux().subscribe { verifyAll(chain) }
@@ -41,18 +47,42 @@ class TrackTx(
}
}
private fun currentList(chain: Chain): ConcurrentLinkedQueue<TrackedTx> {
return clients[chain]!!
private fun currentList(chain: Chain): ConcurrentLinkedQueue<TrackedTx>? {
return clients[chain]
}
fun add(tx: TrackedTx) {
currentList(tx.chain).add(tx)
verify(tx)
notify(tx)
fun add(requestMono: Mono<BlockchainOuterClass.TxStatusRequest>): Flux<BlockchainOuterClass.TxStatus> {
return requestMono.map { request ->
val sender = TopicProcessor.create<BlockchainOuterClass.TxStatus>()
TrackTx.TrackedTx(
Chain.byId(request.chainValue),
sender,
Instant.now(),
TransactionId.from(request.txId),
min(max(1, request.confirmationLimit), 100)
)
}.filter {
clients.containsKey(it.chain)
}.map { tx ->
currentList(tx.chain)!!.let { list ->
list.add(tx)
tx.stream.doOnError {
list.remove(tx)
tx.stream.dispose()
}
}
tx
}.map { tx ->
verify(tx)
notify(tx)
tx
}.flatMapMany { tx ->
tx.stream
}
}
private fun verifyAll(chain: Chain) {
currentList(chain)
currentList(chain)!!
.toFlux()
.filter(this::verify)
.subscribe {
@@ -61,7 +91,8 @@ class TrackTx(
}
private fun loadWeight(tx: TrackedTx): Mono<TrackedTx> {
val upstream = upstreams.getUpstream(tx.chain)!!
val upstream = upstreams.getUpstream(tx.chain)
?: return Mono.error(Exception("Unsupported blockchain: ${tx.chain}"))
return upstream.getApi()
.executeAndConvert(Commands.eth().getBlock(tx.status.blockHash))
.map { block ->
@@ -81,7 +112,7 @@ class TrackTx(
private fun verify(tx: TrackedTx): Boolean {
val found = tx.status.found
val mined = tx.status.mined
val upstream = upstreams.getUpstream(tx.chain)!!
val upstream = upstreams.getUpstream(tx.chain) ?: return false
val execution = upstream.getApi()
.executeAndConvert(Commands.eth().getTransaction(tx.txid))
val update = execution.flatMap {
@@ -122,7 +153,7 @@ class TrackTx(
return true
}
private fun notify(tx: TrackedTx): Boolean {
private fun notify(tx: TrackedTx) {
val client = tx.stream
val data = BlockchainOuterClass.TxStatus.newBuilder()
.setTxId(tx.txid.toHex())
@@ -140,23 +171,11 @@ class TrackTx(
.setTimestamp(tx.status.blockTime!!.toEpochMilli())
)
}
var sent: Boolean = false
try {
sent = client.send(data.build())
if (!sent || tx.shouldClose()) {
if (sent) {
client.stream.onCompleted()
}
currentList(tx.chain).remove(tx)
}
} catch (e: Exception) {
log.error("Send error ${e.javaClass}: ${e.message}")
}
return sent
client.onNext(data.build())
}
class TrackedTx(val chain: Chain,
val stream: StreamSender<BlockchainOuterClass.TxStatus>,
val stream: TopicProcessor<BlockchainOuterClass.TxStatus>,
val since: Instant,
val txid: TransactionId,
val maxConfirmations: Int,

View File

@@ -0,0 +1,32 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.grpc.Chain
import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
import reactor.core.publisher.TopicProcessor
import java.util.*
import kotlin.collections.LinkedHashSet
@Repository
class AvailableChains {
private val all = LinkedHashSet<Chain>()
private val bus = TopicProcessor.create<Chain>()
fun add(chain: Chain) {
all.add(chain)
bus.onNext(chain)
}
fun observe(): Flux<Chain> {
return Flux.from(bus)
}
fun supports(chain: Chain): Boolean {
return all.contains(chain)
}
fun getAll(): Set<Chain> {
return Collections.unmodifiableSet(all)
}
}

View File

@@ -12,6 +12,8 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.core.env.Environment
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toFlux
import java.io.File
import java.net.URI
@@ -21,7 +23,8 @@ import javax.annotation.PostConstruct
@Repository
open class ConfiguredUpstreams(
@Autowired val env: Environment,
@Autowired private val objectMapper: ObjectMapper
@Autowired private val objectMapper: ObjectMapper,
@Autowired private val availableChains: AvailableChains
) : Upstreams {
private val log = LoggerFactory.getLogger(ConfiguredUpstreams::class.java)
@@ -123,7 +126,8 @@ open class ConfiguredUpstreams(
endpoint.port ?: 443,
objectMapper,
options,
up.auth
up.auth,
availableChains
)
log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}")
ds.start()
@@ -145,6 +149,7 @@ open class ConfiguredUpstreams(
if (current == null) {
val created = ChainUpstreams(chain, ArrayList<Upstream>())
chainMapping[chain] = created
availableChains.add(chain)
return created
}
return current

View File

@@ -11,11 +11,13 @@ import io.infinitape.etherjar.domain.BlockHash
import io.infinitape.etherjar.domain.TransactionId
import io.infinitape.etherjar.rpc.*
import io.infinitape.etherjar.rpc.json.BlockJson
import io.infinitape.etherjar.rpc.json.BlockTag
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.publisher.TopicProcessor
import reactor.core.publisher.toMono
import java.lang.Exception
import java.math.BigInteger
import java.time.Duration
import java.util.concurrent.atomic.AtomicReference
@@ -74,6 +76,11 @@ open class GrpcUpstream(
val curr = headBlock.get()
curr == null || curr.totalDifficulty < block.totalDifficulty
}
.flatMap {
getApi()
.executeAndConvert(Commands.eth().getBlock(it.hash))
.timeout(Duration.ofSeconds(15))
}
.doOnError { err ->
log.error("Head subscription error", err)
}

View File

@@ -21,7 +21,8 @@ class GrpcUpstreams(
private val port: Int,
private val objectMapper: ObjectMapper,
private val options: UpstreamsConfig.Options,
private val auth: UpstreamsConfig.TlsAuth? = null
private val auth: UpstreamsConfig.TlsAuth? = null,
private val availableChains: AvailableChains
) {
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
@@ -90,6 +91,7 @@ class GrpcUpstreams(
return if (current == null) {
val created = GrpcUpstream(chain, client!!, objectMapper, options)
known[chain] = created
availableChains.add(chain)
created.connect()
created
} else {

View File

@@ -1,6 +1,7 @@
package io.emeraldpay.dshackle.upstream
import io.emeraldpay.grpc.Chain
import reactor.core.publisher.Flux
interface Upstreams {
fun getOrCreateUpstream(chain: Chain): AggregatedUpstreams