solution: configurable timeout
This commit is contained in:
@@ -45,7 +45,7 @@ upstreams:
|
|||||||
|
|
||||||
Which sets the following:
|
Which sets the following:
|
||||||
|
|
||||||
- application listen on 0.0.0.0:2448
|
- application listen on 0.0.0.0:2449
|
||||||
- TLS security is disabled (_don't use in production!_)
|
- TLS security is disabled (_don't use in production!_)
|
||||||
- read upstreams configuration from file `upstreams.yaml` in the current directory
|
- read upstreams configuration from file `upstreams.yaml` in the current directory
|
||||||
|
|
||||||
|
|||||||
@@ -86,9 +86,15 @@ In the example above we have:
|
|||||||
|
|
||||||
Options (default or as part of upstream config):
|
Options (default or as part of upstream config):
|
||||||
|
|
||||||
- `disable-validation` - if `true` then Dshackle would not try to verify status of the upstream (useful for a trusted cloud
|
[cols="2,1,5a"]
|
||||||
provider such as Infura, but is not recommended for an own node)
|
|===
|
||||||
- `min-peers` - do not use upstream with less than specified connected peers
|
| Option | Default | Description
|
||||||
|
|
||||||
|
| `disable-validation` | false | if `true` then Dshackle will not try to verify status of the upstream (could be useful for a trusted cloud
|
||||||
|
provider such as Infura, but disabling it is not recommended for a normal node)
|
||||||
|
| `min-peers` | 3 | specify minimum amount of connected peers, Dshackle will not use upstream with less than specified number
|
||||||
|
| `timeout` | 60 | timeout in seconds after which request to the upstream will be discarded (and may be retried on an another upstream)
|
||||||
|
|===
|
||||||
|
|
||||||
=== Connection type
|
=== Connection type
|
||||||
|
|
||||||
@@ -153,12 +159,13 @@ upstreams:
|
|||||||
- name: eth_getBlockByNumber
|
- name: eth_getBlockByNumber
|
||||||
----
|
----
|
||||||
|
|
||||||
Such configuration option allows to execute methods `trace_transaction` and also disables `eth_getBlockByNumber` on that
|
Such configuration option allows to execute method `trace_transaction` and also disables `eth_getBlockByNumber` on that
|
||||||
particular upstream. If a client tries to execute method `trace_transaction` it will be executed on that upstream, or
|
particular upstream. If a client requests to execute method `trace_transaction` then it will be scheduled to that upstream (or
|
||||||
another upstream will have such method enabled.
|
any upstream with such method enabled).
|
||||||
|
|
||||||
Together with label `archive: true` it's possible to specify during execution that a client wants to execute method only
|
NOTE: It's especially useful when used together with upstream labels. If an archive upstream has label `archive: true` it's
|
||||||
on an archive node.
|
possible to specify that the client wants to execute method `trace_transaction` only on an archive node(s), which has
|
||||||
|
complete historical data for tracing.
|
||||||
|
|
||||||
=== Authentication
|
=== Authentication
|
||||||
|
|
||||||
@@ -167,7 +174,9 @@ on an archive node.
|
|||||||
All connection types can use TLS secured connection, with optional client certificate authentication:
|
All connection types can use TLS secured connection, with optional client certificate authentication:
|
||||||
|
|
||||||
- `ca` path to certificate required from remote server
|
- `ca` path to certificate required from remote server
|
||||||
- optional `certificate` and `key` for client authentication. Please note that `key` is encoded with _PKCS 8_
|
- optional `certificate` and `key` for client authentication.
|
||||||
|
|
||||||
|
NOTE: Please note that `key` must be encoded with _PKCS 8_
|
||||||
|
|
||||||
==== Basic Authentication
|
==== Basic Authentication
|
||||||
|
|
||||||
|
|||||||
@@ -13,21 +13,13 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle
|
||||||
|
|
||||||
import reactor.core.publisher.Flux
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
|
|
||||||
class UpstreamServices {
|
class Defaults {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun onceOk(up: Upstream, waitTime: Duration = Duration.ofSeconds(15)): Mono<Boolean> {
|
val timeout = Duration.ofSeconds(60)
|
||||||
return Flux.concat(Flux.just(up.getStatus()), up.observeStatus())
|
|
||||||
.timeout(waitTime, Mono.just(UpstreamAvailability.UNAVAILABLE))
|
|
||||||
.filter { it == UpstreamAvailability.OK }
|
|
||||||
.next()
|
|
||||||
.hasElement()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.config
|
package io.emeraldpay.dshackle.config
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
@@ -27,6 +29,7 @@ class UpstreamsConfig {
|
|||||||
|
|
||||||
open class Options {
|
open class Options {
|
||||||
var disableValidation: Boolean? = null
|
var disableValidation: Boolean? = null
|
||||||
|
var timeout = Defaults.timeout
|
||||||
|
|
||||||
var minPeers: Int? = 1
|
var minPeers: Int? = 1
|
||||||
set(minPeers) {
|
set(minPeers) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import java.io.InputStream
|
|||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
class UpstreamsConfigReader {
|
class UpstreamsConfigReader {
|
||||||
|
|
||||||
@@ -185,6 +186,9 @@ class UpstreamsConfigReader {
|
|||||||
getValueAsInt(values, "min-peers")?.let {
|
getValueAsInt(values, "min-peers")?.let {
|
||||||
options.minPeers = it
|
options.minPeers = it
|
||||||
}
|
}
|
||||||
|
getValueAsInt(values, "timeout")?.let {
|
||||||
|
options.timeout = Duration.ofSeconds(it.toLong())
|
||||||
|
}
|
||||||
getValueAsBool(values, "disable-validation")?.let {
|
getValueAsBool(values, "disable-validation")?.let {
|
||||||
options.disableValidation = it
|
options.disableValidation = it
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package io.emeraldpay.dshackle.rpc
|
|||||||
|
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.upstream.Selector
|
import io.emeraldpay.dshackle.upstream.Selector
|
||||||
import io.emeraldpay.dshackle.upstream.Upstreams
|
import io.emeraldpay.dshackle.upstream.Upstreams
|
||||||
import io.emeraldpay.grpc.Chain
|
import io.emeraldpay.grpc.Chain
|
||||||
@@ -169,7 +170,7 @@ class TrackAddress(
|
|||||||
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(Selector.empty)
|
return up.getApi(Selector.empty)
|
||||||
.flatMap { api -> api.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST)) }
|
.flatMap { api -> api.executeAndConvert(Commands.eth().getBalance(addr.address, BlockTag.LATEST)) }
|
||||||
.timeout(Duration.ofSeconds(15))
|
.timeout(Defaults.timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateBalances(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> {
|
private fun updateBalances(chain: Chain, group: List<TrackedAddress>): Flux<TrackedAddress> {
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ open class ConfiguredUpstreams(
|
|||||||
config.upstreams.forEach { up ->
|
config.upstreams.forEach { up ->
|
||||||
|
|
||||||
if (up.connection is UpstreamsConfig.GrpcConnection) {
|
if (up.connection is UpstreamsConfig.GrpcConnection) {
|
||||||
buildGrpcUpstream(up as UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>)
|
val options = up.options ?: UpstreamsConfig.Options()
|
||||||
|
buildGrpcUpstream(up as UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, options)
|
||||||
} else {
|
} else {
|
||||||
val chain = chainNames[up.chain]
|
val chain = chainNames[up.chain]
|
||||||
if (chain == null) {
|
if (chain == null) {
|
||||||
@@ -145,7 +146,10 @@ open class ConfiguredUpstreams(
|
|||||||
rpcClient,
|
rpcClient,
|
||||||
objectMapper,
|
objectMapper,
|
||||||
methods
|
methods
|
||||||
)
|
).apply {
|
||||||
|
timeout = options.timeout
|
||||||
|
}
|
||||||
|
|
||||||
urls.add(endpoint.url)
|
urls.add(endpoint.url)
|
||||||
}
|
}
|
||||||
if (rpcApi != null) {
|
if (rpcApi != null) {
|
||||||
@@ -174,7 +178,7 @@ open class ConfiguredUpstreams(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildGrpcUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>) {
|
private fun buildGrpcUpstream(config: UpstreamsConfig.Upstream<UpstreamsConfig.GrpcConnection>, options: UpstreamsConfig.Options) {
|
||||||
val endpoint = config.connection!!
|
val endpoint = config.connection!!
|
||||||
val ds = GrpcUpstreams(
|
val ds = GrpcUpstreams(
|
||||||
config.id!!,
|
config.id!!,
|
||||||
@@ -183,7 +187,9 @@ open class ConfiguredUpstreams(
|
|||||||
objectMapper,
|
objectMapper,
|
||||||
endpoint.auth,
|
endpoint.auth,
|
||||||
fileResolver
|
fileResolver
|
||||||
)
|
).apply {
|
||||||
|
timeout = options.timeout
|
||||||
|
}
|
||||||
log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}")
|
log.info("Using ALL CHAINS (gRPC) upstream, at ${endpoint.host}:${endpoint.port}")
|
||||||
ds.start()
|
ds.start()
|
||||||
.doOnNext {
|
.doOnNext {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream
|
package io.emeraldpay.dshackle.upstream
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
|
||||||
import io.infinitape.etherjar.rpc.Batch
|
import io.infinitape.etherjar.rpc.Batch
|
||||||
@@ -36,7 +37,7 @@ class UpstreamValidator(
|
|||||||
return ethereumUpstream.getApi(Selector.empty)
|
return ethereumUpstream.getApi(Selector.empty)
|
||||||
.map { api -> api.rpcClient.execute(batch) }
|
.map { api -> api.rpcClient.execute(batch) }
|
||||||
.flatMap { Mono.fromCompletionStage(it) }
|
.flatMap { Mono.fromCompletionStage(it) }
|
||||||
.timeout(Duration.ofSeconds(10))
|
.timeout(Defaults.timeout)
|
||||||
.map {
|
.map {
|
||||||
if (syncing.get().isSyncing) {
|
if (syncing.get().isSyncing) {
|
||||||
UpstreamAvailability.SYNCING
|
UpstreamAvailability.SYNCING
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.upstream.CallMethods
|
import io.emeraldpay.dshackle.upstream.CallMethods
|
||||||
import io.infinitape.etherjar.rpc.RpcCall
|
import io.infinitape.etherjar.rpc.RpcCall
|
||||||
import io.infinitape.etherjar.rpc.RpcClient
|
import io.infinitape.etherjar.rpc.RpcClient
|
||||||
@@ -31,7 +32,7 @@ open class DirectEthereumApi(
|
|||||||
val targets: CallMethods
|
val targets: CallMethods
|
||||||
): EthereumApi(objectMapper) {
|
): EthereumApi(objectMapper) {
|
||||||
|
|
||||||
private val timeout = Duration.ofSeconds(5)
|
var timeout = Defaults.timeout
|
||||||
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
private val log = LoggerFactory.getLogger(EthereumApi::class.java)
|
||||||
|
|
||||||
override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
override fun execute(id: Int, method: String, params: List<Any>): Mono<ByteArray> {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.infinitape.etherjar.rpc.Batch
|
import io.infinitape.etherjar.rpc.Batch
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
import io.infinitape.etherjar.rpc.Commands
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -39,13 +40,13 @@ class EthereumRpcHead(
|
|||||||
val batch = Batch()
|
val batch = Batch()
|
||||||
val f = batch.add(Commands.eth().blockNumber)
|
val f = batch.add(Commands.eth().blockNumber)
|
||||||
api.rpcClient.execute(batch)
|
api.rpcClient.execute(batch)
|
||||||
Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5), Mono.empty())
|
Mono.fromCompletionStage(f).timeout(Defaults.timeout, Mono.empty())
|
||||||
}
|
}
|
||||||
.flatMap {
|
.flatMap {
|
||||||
val batch = Batch()
|
val batch = Batch()
|
||||||
val f = batch.add(Commands.eth().getBlock(it))
|
val f = batch.add(Commands.eth().getBlock(it))
|
||||||
api.rpcClient.execute(batch)
|
api.rpcClient.execute(batch)
|
||||||
Mono.fromCompletionStage(f).timeout(Duration.ofSeconds(5), Mono.empty())
|
Mono.fromCompletionStage(f).timeout(Defaults.timeout, Mono.empty())
|
||||||
}
|
}
|
||||||
.onErrorContinue { err, _ ->
|
.onErrorContinue { err, _ ->
|
||||||
log.debug("RPC error ${err.message}")
|
log.debug("RPC error ${err.message}")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package io.emeraldpay.dshackle.upstream.ethereum
|
package io.emeraldpay.dshackle.upstream.ethereum
|
||||||
|
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.infinitape.etherjar.domain.TransactionId
|
import io.infinitape.etherjar.domain.TransactionId
|
||||||
import io.infinitape.etherjar.rpc.Commands
|
import io.infinitape.etherjar.rpc.Commands
|
||||||
@@ -62,7 +63,7 @@ class EthereumWs(
|
|||||||
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(250))
|
.exponentialBackoff(Duration.ofMillis(50), Duration.ofMillis(250))
|
||||||
.apply(n)
|
.apply(n)
|
||||||
}
|
}
|
||||||
.timeout(Duration.ofSeconds(5), Mono.empty())
|
.timeout(Defaults.timeout, Mono.empty())
|
||||||
.subscribe(topic::onNext)
|
.subscribe(topic::onNext)
|
||||||
} else {
|
} else {
|
||||||
topic.onNext(it)
|
topic.onNext(it)
|
||||||
@@ -73,6 +74,5 @@ class EthereumWs(
|
|||||||
fun getFlux(): Flux<BlockJson<TransactionId>> {
|
fun getFlux(): Flux<BlockJson<TransactionId>> {
|
||||||
return Flux.from(this.topic)
|
return Flux.from(this.topic)
|
||||||
.onBackpressureLatest()
|
.onBackpressureLatest()
|
||||||
.sample(Duration.ofMillis(100))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@ import com.salesforce.reactorgrpc.GrpcRetry
|
|||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.Common
|
import io.emeraldpay.api.proto.Common
|
||||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.upstream.*
|
import io.emeraldpay.dshackle.upstream.*
|
||||||
import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead
|
import io.emeraldpay.dshackle.upstream.ethereum.DefaultEthereumHead
|
||||||
@@ -61,9 +62,10 @@ open class GrpcUpstream(
|
|||||||
private val nodes = AtomicReference<NodeDetailsList>(NodeDetailsList())
|
private val nodes = AtomicReference<NodeDetailsList>(NodeDetailsList())
|
||||||
private val head = DefaultEthereumHead()
|
private val head = DefaultEthereumHead()
|
||||||
private var targets: CallMethods? = null
|
private var targets: CallMethods? = null
|
||||||
|
|
||||||
private var headSubscription: Disposable? = null
|
private var headSubscription: Disposable? = null
|
||||||
|
|
||||||
|
var timeout = Defaults.timeout
|
||||||
|
|
||||||
open fun createApi(matcher: Selector.Matcher): DirectEthereumApi {
|
open fun createApi(matcher: Selector.Matcher): DirectEthereumApi {
|
||||||
val targets = this.getMethods()
|
val targets = this.getMethods()
|
||||||
val transport = Selector.extractLabels(matcher)?.let { selector ->
|
val transport = Selector.extractLabels(matcher)?.let { selector ->
|
||||||
@@ -123,7 +125,7 @@ open class GrpcUpstream(
|
|||||||
}.flatMap {
|
}.flatMap {
|
||||||
getApi(Selector.EmptyMatcher())
|
getApi(Selector.EmptyMatcher())
|
||||||
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(it.hash)) }
|
.flatMap { api -> api.executeAndConvert(Commands.eth().getBlock(it.hash)) }
|
||||||
.timeout(Duration.ofSeconds(5), Mono.error(TimeoutException("Timeout from upstream")))
|
.timeout(timeout, Mono.error(TimeoutException("Timeout from upstream")))
|
||||||
.doOnError { t ->
|
.doOnError { t ->
|
||||||
setStatus(UpstreamAvailability.UNAVAILABLE)
|
setStatus(UpstreamAvailability.UNAVAILABLE)
|
||||||
val msg = "Failed to download block data for chain $chain on $parentId"
|
val msg = "Failed to download block data for chain $chain on $parentId"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.grpc
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||||
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
|
||||||
|
import io.emeraldpay.dshackle.Defaults
|
||||||
import io.emeraldpay.dshackle.FileResolver
|
import io.emeraldpay.dshackle.FileResolver
|
||||||
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
import io.emeraldpay.dshackle.config.UpstreamsConfig
|
||||||
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
|
||||||
@@ -50,10 +51,11 @@ class GrpcUpstreams(
|
|||||||
) {
|
) {
|
||||||
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
|
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
|
||||||
|
|
||||||
|
var timeout = Defaults.timeout
|
||||||
|
|
||||||
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
private var client: ReactorBlockchainGrpc.ReactorBlockchainStub? = null
|
||||||
private val known = HashMap<Chain, GrpcUpstream>()
|
private val known = HashMap<Chain, GrpcUpstream>()
|
||||||
private val lock = ReentrantLock()
|
private val lock = ReentrantLock()
|
||||||
|
|
||||||
private var grpcTransport: EmeraldGrpcTransport? = null
|
private var grpcTransport: EmeraldGrpcTransport? = null
|
||||||
|
|
||||||
fun start(): Flux<UpstreamChange> {
|
fun start(): Flux<UpstreamChange> {
|
||||||
@@ -162,6 +164,7 @@ class GrpcUpstreams(
|
|||||||
val current = known[chain]
|
val current = known[chain]
|
||||||
return if (current == null) {
|
return if (current == null) {
|
||||||
val created = GrpcUpstream(id, chain, client!!, objectMapper, grpcTransport!!.copyForChain(chain))
|
val created = GrpcUpstream(id, chain, client!!, objectMapper, grpcTransport!!.copyForChain(chain))
|
||||||
|
created.timeout = this.timeout
|
||||||
known[chain] = created
|
known[chain] = created
|
||||||
created.start()
|
created.start()
|
||||||
UpstreamChange(chain, created, UpstreamChange.ChangeType.ADDED)
|
UpstreamChange(chain, created, UpstreamChange.ChangeType.ADDED)
|
||||||
|
|||||||
Reference in New Issue
Block a user