Subscription optimization (#709)
* share logs producer to reuse logs subscriptions * support unsubscription from proxied subscriptions
This commit is contained in:
@@ -94,7 +94,7 @@ open class NativeSubscribe(
|
||||
objectMapper.readValue(it.newInput(), List::class.java)
|
||||
}
|
||||
}
|
||||
subscribe(chain, method, params, matcher, subscriptionId)
|
||||
subscribe(chain, method, params, matcher, subscriptionId, request.unsubscribeMethod)
|
||||
}
|
||||
return publisher.map { ResponseHolder(it, nonce) }
|
||||
}
|
||||
@@ -119,11 +119,11 @@ open class NativeSubscribe(
|
||||
}
|
||||
}
|
||||
open fun subscribe(chain: Chain, method: String, params: Any?, matcher: Selector.Matcher): Flux<out Any> =
|
||||
subscribe(chain, method, params, matcher, "")
|
||||
subscribe(chain, method, params, matcher, "", "")
|
||||
|
||||
open fun subscribe(chain: Chain, method: String, params: Any?, matcher: Selector.Matcher, subscriptionId: String): Flux<out Any> =
|
||||
open fun subscribe(chain: Chain, method: String, params: Any?, matcher: Selector.Matcher, subscriptionId: String, unsubscribeMethod: String): Flux<out Any> =
|
||||
getUpstream(chain).getEgressSubscription()
|
||||
.subscribe(method, params, matcher)
|
||||
.subscribe(method, params, matcher, unsubscribeMethod)
|
||||
.doOnError {
|
||||
log.error("sub_id:$subscriptionId Error during subscription to $method, chain $chain, params $params", it)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import reactor.core.publisher.Flux
|
||||
|
||||
interface EgressSubscription {
|
||||
fun getAvailableTopics(): List<String>
|
||||
fun subscribe(topic: String, params: Any?, matcher: Selector.Matcher): Flux<out Any>
|
||||
fun subscribe(topic: String, params: Any?, matcher: Selector.Matcher, unsubscribeMethod: String): Flux<out Any>
|
||||
}
|
||||
|
||||
object EmptyEgressSubscription : EgressSubscription {
|
||||
@@ -27,7 +27,7 @@ object EmptyEgressSubscription : EgressSubscription {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun subscribe(topic: String, params: Any?, matcher: Selector.Matcher): Flux<out Any> {
|
||||
override fun subscribe(topic: String, params: Any?, matcher: Selector.Matcher, unsubscribeMethod: String): Flux<out Any> {
|
||||
return Flux.empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,5 +21,5 @@ package io.emeraldpay.dshackle.upstream
|
||||
interface IngressSubscription {
|
||||
|
||||
fun getAvailableTopics(): List<String>
|
||||
fun <T> get(topic: String, params: Any?): SubscriptionConnect<T>?
|
||||
fun <T> get(topic: String, params: Any?, unsubscribeMethod: String): SubscriptionConnect<T>?
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ open class NoIngressSubscription : IngressSubscription {
|
||||
return listOf()
|
||||
}
|
||||
|
||||
override fun <T> get(topic: String, params: Any?): SubscriptionConnect<T>? {
|
||||
override fun <T> get(topic: String, params: Any?, unsubscribeMethod: String): SubscriptionConnect<T>? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ open class EthereumEgressSubscription(
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun subscribe(topic: String, params: Any?, matcher: Selector.Matcher): Flux<out Any> {
|
||||
override fun subscribe(topic: String, params: Any?, matcher: Selector.Matcher, unsubscribeMethod: String): Flux<out Any> {
|
||||
if (topic == METHOD_NEW_HEADS) {
|
||||
return newHeads.connect(matcher)
|
||||
}
|
||||
|
||||
@@ -20,63 +20,20 @@ import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.SubscriptionConnect
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.domain.Address
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.hex.Hex32
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.hex.HexDataComparator
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.LogMessage
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.scheduler.Scheduler
|
||||
import java.util.function.Function
|
||||
|
||||
open class ConnectLogs(
|
||||
upstream: Multistream,
|
||||
private val connectBlockUpdates: ConnectBlockUpdates,
|
||||
scheduler: Scheduler,
|
||||
) {
|
||||
companion object {
|
||||
private val ADDR_COMPARATOR = HexDataComparator()
|
||||
private val TOPIC_COMPARATOR = HexDataComparator()
|
||||
}
|
||||
|
||||
constructor(upstream: Multistream, scheduler: Scheduler) : this(upstream, ConnectBlockUpdates(upstream, scheduler))
|
||||
|
||||
private val produceLogs = ProduceLogs(upstream)
|
||||
|
||||
fun start(matcher: Selector.Matcher): Flux<LogMessage> {
|
||||
return produceLogs.produce(connectBlockUpdates.connect(matcher))
|
||||
}
|
||||
private val sharedLogsProducer = SharedLogsProducer(upstream, scheduler)
|
||||
|
||||
open fun create(addresses: List<Address>, topics: List<List<Hex32>?>): SubscriptionConnect<LogMessage> {
|
||||
return object : SubscriptionConnect<LogMessage> {
|
||||
override fun connect(matcher: Selector.Matcher): Flux<LogMessage> {
|
||||
if (addresses.isEmpty() && topics.isEmpty()) {
|
||||
return start(matcher)
|
||||
}
|
||||
return start(matcher)
|
||||
.transform(filtered(addresses, topics))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filtered(addresses: List<Address>, selectedTopics: List<List<Hex32>?>): Function<Flux<LogMessage>, Flux<LogMessage>> {
|
||||
val sortedAddresses: List<Address> = addresses.sortedWith(ADDR_COMPARATOR)
|
||||
val topicSets: List<Set<Hex32>?> = selectedTopics.map { topicsOrNull ->
|
||||
topicsOrNull?.toSet()
|
||||
}
|
||||
|
||||
return Function { logs ->
|
||||
logs.filter { log ->
|
||||
val goodAddress = sortedAddresses.isEmpty() ||
|
||||
sortedAddresses.binarySearch(log.address, ADDR_COMPARATOR) >= 0
|
||||
|
||||
val goodTopics = if (topicSets.isEmpty()) {
|
||||
true
|
||||
} else if (log.topics.size < topicSets.size) {
|
||||
false
|
||||
} else {
|
||||
topicSets.zip(log.topics).all { (wantedTopics, logTopic) ->
|
||||
wantedTopics == null || logTopic in wantedTopics
|
||||
}
|
||||
}
|
||||
|
||||
goodAddress && goodTopics
|
||||
return sharedLogsProducer.subscribe(addresses, topics, matcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class EthereumWsIngressSubscription(
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> get(topic: String, params: Any?): SubscriptionConnect<T>? {
|
||||
override fun <T> get(topic: String, params: Any?, unsubscribeMethod: String): SubscriptionConnect<T>? {
|
||||
if (topic == EthereumEgressSubscription.METHOD_PENDING_TXES) {
|
||||
return pendingTxes as SubscriptionConnect<T>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2021 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.subscribe
|
||||
|
||||
import io.emeraldpay.dshackle.upstream.Multistream
|
||||
import io.emeraldpay.dshackle.upstream.Selector
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.domain.Address
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.hex.Hex32
|
||||
import io.emeraldpay.dshackle.upstream.ethereum.subscribe.json.LogMessage
|
||||
import org.slf4j.LoggerFactory
|
||||
import reactor.core.Disposable
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
import reactor.core.scheduler.Scheduler
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class SharedLogsProducer(
|
||||
upstream: Multistream,
|
||||
scheduler: Scheduler,
|
||||
) {
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(SharedLogsProducer::class.java)
|
||||
}
|
||||
|
||||
private val produceLogs = ProduceLogs(upstream)
|
||||
private val connectBlockUpdates = ConnectBlockUpdates(upstream, scheduler)
|
||||
|
||||
private val subscriptions = ConcurrentHashMap<String, LogsSubscription>()
|
||||
private val subscriptionCounter = AtomicInteger(0)
|
||||
|
||||
// Map of matcher hash to shared streams
|
||||
private val sharedStreams = ConcurrentHashMap<String, Disposable>()
|
||||
private val logsSinks = ConcurrentHashMap<String, Sinks.Many<LogMessage>>()
|
||||
|
||||
fun subscribe(
|
||||
addresses: List<Address>,
|
||||
topics: List<List<Hex32>?>,
|
||||
matcher: Selector.Matcher,
|
||||
): Flux<LogMessage> {
|
||||
val subscriptionId = "logs-sub-${subscriptionCounter.incrementAndGet()}"
|
||||
val subscription = LogsSubscription(subscriptionId, addresses, topics, matcher)
|
||||
val matcherKey = matcher.describeInternal()
|
||||
|
||||
subscriptions[subscriptionId] = subscription
|
||||
|
||||
// Start shared stream if this is the first subscription for this matcher
|
||||
val subscriptionsForMatcher = subscriptions.values.filter { it.matcher.describeInternal() == matcherKey }
|
||||
if (subscriptionsForMatcher.size == 1) {
|
||||
startSharedStream(matcher)
|
||||
}
|
||||
|
||||
val logsFlux = logsSinks[matcherKey]?.asFlux() ?: Flux.empty()
|
||||
|
||||
return logsFlux
|
||||
.filter { logMessage -> subscription.matches(logMessage) }
|
||||
.doFinally { // Remove subscription when stream ends
|
||||
subscriptions.remove(subscriptionId)
|
||||
// Stop shared stream if no more subscriptions for this matcher
|
||||
val remainingSubscriptionsForMatcher = subscriptions.values.filter { it.matcher.describeInternal() == matcherKey }
|
||||
if (remainingSubscriptionsForMatcher.isEmpty()) {
|
||||
stopSharedStream(matcherKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startSharedStream(matcher: Selector.Matcher) {
|
||||
val matcherKey = matcher.describeInternal()
|
||||
|
||||
val logsSink = Sinks.many().multicast().onBackpressureBuffer<LogMessage>()
|
||||
logsSinks[matcherKey] = logsSink
|
||||
|
||||
val sharedStream = produceLogs.produce(connectBlockUpdates.connect(matcher))
|
||||
.subscribe(
|
||||
{ logMessage ->
|
||||
logsSink.emitNext(logMessage) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
||||
},
|
||||
{ error ->
|
||||
log.error("Error in shared logs stream for matcher: $matcherKey", error)
|
||||
logsSink.emitError(error) { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
||||
},
|
||||
{
|
||||
logsSink.emitComplete { _, res -> res == Sinks.EmitResult.FAIL_NON_SERIALIZED }
|
||||
},
|
||||
)
|
||||
sharedStreams[matcherKey] = sharedStream
|
||||
}
|
||||
|
||||
private fun stopSharedStream(matcherKey: String) {
|
||||
sharedStreams[matcherKey]?.dispose()
|
||||
sharedStreams.remove(matcherKey)
|
||||
logsSinks[matcherKey]?.tryEmitComplete()
|
||||
logsSinks.remove(matcherKey)
|
||||
}
|
||||
|
||||
private data class LogsSubscription(
|
||||
val id: String,
|
||||
val addresses: List<Address>,
|
||||
val topics: List<List<Hex32>?>,
|
||||
val matcher: Selector.Matcher,
|
||||
) {
|
||||
fun matches(logMessage: LogMessage): Boolean {
|
||||
val addressMatch = addresses.isEmpty() || addresses.contains(logMessage.address)
|
||||
|
||||
val topicsMatch = if (topics.isEmpty()) {
|
||||
true
|
||||
} else if (logMessage.topics.size < topics.size) {
|
||||
false
|
||||
} else {
|
||||
topics.zip(logMessage.topics).all { (wantedTopics, logTopic) ->
|
||||
wantedTopics == null || logTopic in wantedTopics
|
||||
}
|
||||
}
|
||||
|
||||
return addressMatch && topicsMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,17 +22,17 @@ class GenericEgressSubscription(
|
||||
.distinct()
|
||||
}
|
||||
|
||||
override fun subscribe(topic: String, params: Any?, matcher: Matcher): Flux<ByteArray> {
|
||||
override fun subscribe(topic: String, params: Any?, matcher: Matcher, unsubscribeMethod: String): Flux<ByteArray> {
|
||||
val up = multistream.getUpstreams()
|
||||
.filter { it.isAvailable() }
|
||||
.shuffled()
|
||||
.first { matcher.matches(it) } as GenericUpstream
|
||||
|
||||
val result = up.getIngressSubscription().get<ByteArray>(topic, params)?.connect(matcher)
|
||||
val result = up.getIngressSubscription().get<ByteArray>(topic, params, unsubscribeMethod)?.connect(matcher)
|
||||
if (result == null) {
|
||||
log.warn("subscription source not found for topic {}", topic)
|
||||
return Flux.empty()
|
||||
}
|
||||
return up.getIngressSubscription().get<ByteArray>(topic, params)?.connect(matcher) ?: Flux.empty()
|
||||
return up.getIngressSubscription().get<ByteArray>(topic, params, unsubscribeMethod)?.connect(matcher) ?: Flux.empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,13 @@ class GenericIngressSubscription(val conn: WsSubscriptions, val methods: List<St
|
||||
private val holders = ConcurrentHashMap<Pair<String, Any?>, SubscriptionConnect<out Any>>()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> get(topic: String, params: Any?): SubscriptionConnect<T> {
|
||||
override fun <T> get(topic: String, params: Any?, unsubscribeMethod: String): SubscriptionConnect<T> {
|
||||
return holders.computeIfAbsent(topic to params) { key ->
|
||||
GenericSubscriptionConnect(
|
||||
conn,
|
||||
key.first,
|
||||
key.second,
|
||||
unsubscribeMethod,
|
||||
)
|
||||
} as SubscriptionConnect<T>
|
||||
}
|
||||
@@ -36,6 +37,7 @@ class GenericSubscriptionConnect(
|
||||
val conn: WsSubscriptions,
|
||||
val topic: String,
|
||||
val params: Any?,
|
||||
val unsubscribeMethod: String,
|
||||
) : GenericPersistentConnect() {
|
||||
|
||||
companion object {
|
||||
@@ -44,8 +46,8 @@ class GenericSubscriptionConnect(
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun createConnection(): Flux<Any> {
|
||||
return conn.subscribe(ChainRequest(topic, ListParams(getParams(params) as List<Any>)))
|
||||
.data
|
||||
val sub = conn.subscribe(ChainRequest(topic, ListParams(getParams(params) as List<Any>)))
|
||||
return sub.data
|
||||
.timeout(
|
||||
Duration.ofSeconds(85),
|
||||
Mono.empty<ByteArray?>().doOnEach {
|
||||
@@ -55,6 +57,12 @@ class GenericSubscriptionConnect(
|
||||
.onErrorResume {
|
||||
log.error("Error during subscription to $topic", it)
|
||||
Mono.empty()
|
||||
}.doFinally {
|
||||
if (unsubscribeMethod != "") {
|
||||
conn.unsubscribe(ChainRequest(unsubscribeMethod, ListParams(sub.subId.get()))).subscribe {
|
||||
log.info("unsubscribed from ${sub.subId.get()}")
|
||||
}
|
||||
}
|
||||
} as Flux<Any>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user