From edffe6dd0985f51a07838cf678ae1ca6d5ea2ff8 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Wed, 18 Oct 2017 18:00:33 +0300 Subject: [PATCH 01/14] WIP --- discovery/build.sbt | 2 + discovery/devnet.conf | 25 +++++++++ discovery/src/main/resources/application.conf | 56 +++++++++++++++++++ .../DiscoveryApp.scala | 52 +++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100755 discovery/build.sbt create mode 100644 discovery/devnet.conf create mode 100644 discovery/src/main/resources/application.conf create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala diff --git a/discovery/build.sbt b/discovery/build.sbt new file mode 100755 index 00000000000..307becbfe16 --- /dev/null +++ b/discovery/build.sbt @@ -0,0 +1,2 @@ +libraryDependencies += "com.typesafe.akka" % "akka-http-experimental_2.11" % "2.4.11.2" + \ No newline at end of file diff --git a/discovery/devnet.conf b/discovery/devnet.conf new file mode 100644 index 00000000000..8cc645f16dc --- /dev/null +++ b/discovery/devnet.conf @@ -0,0 +1,25 @@ +generator { + chainId = D + accounts = [ + "EB52Qiw82RE1mtQpVu73d92t6CcpT8FtXGeWigidJCET", + "788h8WCsVet6sMceHT8vn9VqRy3Ms929BwdW24eFp1r7", + "8UhM36mbnGFS2tRWDqtEX2tReVKq6Ww6jyMCekFjqoDQ", + ] + n = 100 + every = 500 ms + send-to { + address = 34.251.200.245 + port = 6864 + } + probabilities { + payment: 0.1 + issue: 0.1 + transfer: 0.3 + reissue: 0.05 + burn: 0.075 + exchange: 0.1 + lease: 0.1 + lease-cancel: 0.075 + create-alias: 0.1 + } +} diff --git a/discovery/src/main/resources/application.conf b/discovery/src/main/resources/application.conf new file mode 100644 index 00000000000..b2208bcd862 --- /dev/null +++ b/discovery/src/main/resources/application.conf @@ -0,0 +1,56 @@ +generator { + chain-id = T + accounts = [ + "f8ypmbNfr6ocg8kJ7F1MaC4A89f672ZY6LETRiAEbrb", + "A7ucz2PXvDx3CCDYRhtRkHHzPZZnCTP4T6mN18QvsfCQ", + "uD933WUgrNzhFE2amLT6pijejUm2bTocKqU1Nh3D3rk", + "63HeWce3hCmeTSxciqG3MsHSp9A1B8jueeS6NmoGAu3F" + ] + send-to = [ + { + address = 52.30.47.67 + port = 6863 + } + ] + + worker { + iterations = 100 + delay = 5s + + auto-reconnect = false + reconnect-delay = 3s + } + + mode = "WIDE" + narrow { + transactions = 10 + probabilities { + payment: 0.1 + issue: 0.1 + transfer: 0.3 + reissue: 0.05 + burn: 0.075 + exchange: 0.1 + lease: 0.1 + lease-cancel: 0.075 + create-alias: 0.1 + } + } + + wide { + transactions = 10 + limit-dest-accounts = 10000 + min-fee = 100000 + max-fee = 150000 + } + + dyn-wide { + limit-dest-accounts = 10000 + start = 1 + grow-adder = 0.0408 + min-fee = 100000 + max-fee = 150000 + } +} + +include "local.conf" diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala new file mode 100644 index 00000000000..fb6c122bac6 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala @@ -0,0 +1,52 @@ +package com.wavesplatform.discovery + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.model.ws.{Message, TextMessage} +import akka.stream.{ActorMaterializer, OverflowStrategy} +import akka.stream.scaladsl.{Flow, Sink, Source} + +import scala.io.StdIn + +object DiscoveryApp extends App { + + implicit val actorSystem = ActorSystem("akka-system") + implicit val flowMaterializer = ActorMaterializer() + + val interface = "localhost" + val port = 8080 + + val source: Source[Message, _] = Source.single(0).map(_ => { + println("Connected") + TextMessage("!!!!!") + }) + + val sink: Sink[Message, _] = Sink.ignore + + val echoService: Flow[Message, Message, _] = Flow[Message].map { + case TextMessage.Strict(txt) => TextMessage("ECHO: " + txt) + case _ => TextMessage("Message type unsupported") + } + + import akka.http.scaladsl.server.Directives._ + + val route = get { + pathEndOrSingleSlash { + complete("Welcome to websocket server") + } + } ~ get { + path("ws-echo") { + handleWebSocketMessages(echoService) + } + } + + + val binding = Http().bindAndHandle(route, interface, port) + println(s"Server is now online at http://$interface:$port\nPress RETURN to stop...") + StdIn.readLine() + + import actorSystem.dispatcher + + binding.flatMap(_.unbind()).onComplete(_ => actorSystem.terminate()) + println("Server is down...") +} From 0edb64bba9291160d3dbf70ffcd79a06dd8b0ea5 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Thu, 19 Oct 2017 17:43:16 +0300 Subject: [PATCH 02/14] WIP --- discovery/build.sbt | 2 +- discovery/src/main/resources/application.conf | 56 +---- .../DiscoveryApp.scala | 227 +++++++++++++++--- .../Settings.scala | 27 +++ 4 files changed, 224 insertions(+), 88 deletions(-) create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala diff --git a/discovery/build.sbt b/discovery/build.sbt index 307becbfe16..7d1eabcc348 100755 --- a/discovery/build.sbt +++ b/discovery/build.sbt @@ -1,2 +1,2 @@ libraryDependencies += "com.typesafe.akka" % "akka-http-experimental_2.11" % "2.4.11.2" - \ No newline at end of file +libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.8.0" diff --git a/discovery/src/main/resources/application.conf b/discovery/src/main/resources/application.conf index b2208bcd862..9e5b70b0eec 100644 --- a/discovery/src/main/resources/application.conf +++ b/discovery/src/main/resources/application.conf @@ -1,56 +1,4 @@ -generator { +discovery { chain-id = T - accounts = [ - "f8ypmbNfr6ocg8kJ7F1MaC4A89f672ZY6LETRiAEbrb", - "A7ucz2PXvDx3CCDYRhtRkHHzPZZnCTP4T6mN18QvsfCQ", - "uD933WUgrNzhFE2amLT6pijejUm2bTocKqU1Nh3D3rk", - "63HeWce3hCmeTSxciqG3MsHSp9A1B8jueeS6NmoGAu3F" - ] - send-to = [ - { - address = 52.30.47.67 - port = 6863 - } - ] - - worker { - iterations = 100 - delay = 5s - - auto-reconnect = false - reconnect-delay = 3s - } - - mode = "WIDE" - narrow { - transactions = 10 - probabilities { - payment: 0.1 - issue: 0.1 - transfer: 0.3 - reissue: 0.05 - burn: 0.075 - exchange: 0.1 - lease: 0.1 - lease-cancel: 0.075 - create-alias: 0.1 - } - } - - wide { - transactions = 10 - limit-dest-accounts = 10000 - min-fee = 100000 - max-fee = 150000 - } - - dyn-wide { - limit-dest-accounts = 10000 - start = 1 - grow-adder = 0.0408 - min-fee = 100000 - max-fee = 150000 - } + initial-peers = ["52.30.47.67:6863", "52.28.66.217:6863", "52.77.111.219:6863", "52.51.92.182:6863"] } - -include "local.conf" diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala index fb6c122bac6..78e93703481 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala @@ -1,52 +1,213 @@ package com.wavesplatform.discovery -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.model.ws.{Message, TextMessage} -import akka.stream.{ActorMaterializer, OverflowStrategy} -import akka.stream.scaladsl.{Flow, Sink, Source} +import java.net.InetSocketAddress +import java.util +import java.util.concurrent.TimeUnit -import scala.io.StdIn +import com.wavesplatform.Version +import com.wavesplatform.network.{BasicMessagesRepo, BlockForged, BlockMessageSpec, GetBlock, GetBlockSpec, GetPeers, GetPeersSpec, GetSignatures, GetSignaturesSpec, Handshake, KnownPeers, LocalScoreChanged, Message, MicroBlockInv, MicroBlockInvMessageSpec, MicroBlockRequest, MicroBlockRequestMessageSpec, MicroBlockResponse, MicroBlockResponseMessageSpec, PeerDatabase, PeersSpec, PipelineInitializer, RawBytes, ScoreMessageSpec, Signatures, SignaturesSpec, id} +import com.wavesplatform.settings.Constants +import io.netty.bootstrap.Bootstrap +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled.wrappedBuffer +import io.netty.channel.ChannelHandler.Sharable +import io.netty.channel.{ChannelHandlerContext, ChannelInboundHandlerAdapter, EventLoopGroup} +import io.netty.channel.socket.SocketChannel +import io.netty.handler.codec._ +import scorex.crypto.hash.FastCryptographicHash +import scorex.network.message.Message.ChecksumLength +import scorex.network.message.MessageSpec +import scorex.utils.ScorexLogging -object DiscoveryApp extends App { +import scala.concurrent.{Await, ExecutionContext, Future} +import scala.concurrent.duration.FiniteDuration +import scala.util.control.NonFatal +import scala.util.{Failure, Random, Success} - implicit val actorSystem = ActorSystem("akka-system") - implicit val flowMaterializer = ActorMaterializer() +class LegacyFrameCodec() extends ByteToMessageCodec[RawBytes] with ScorexLogging { + import LegacyFrameCodec._ - val interface = "localhost" - val port = 8080 + override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]) = try { + require(in.readInt() == Magic, "invalid magic number") - val source: Source[Message, _] = Source.single(0).map(_ => { - println("Connected") - TextMessage("!!!!!") - }) + val code = in.readByte() + require(messageSpecs.contains(code), s"Unexpected message code $code") - val sink: Sink[Message, _] = Sink.ignore + val spec = messageSpecs(code) + val length = in.readInt() + require(length <= spec.maxLength, s"${spec.messageName} length $length exceeds ${spec.maxLength}") - val echoService: Flow[Message, Message, _] = Flow[Message].map { - case TextMessage.Strict(txt) => TextMessage("ECHO: " + txt) - case _ => TextMessage("Message type unsupported") - } + val dataBytes = new Array[Byte](length) + if (length > 0) { + val declaredChecksum = in.readSlice(ChecksumLength) + in.readBytes(dataBytes) + val actualChecksum = wrappedBuffer(FastCryptographicHash.hash(dataBytes), 0, ChecksumLength) - import akka.http.scaladsl.server.Directives._ + require(declaredChecksum.equals(actualChecksum), "invalid checksum") + actualChecksum.release() - val route = get { - pathEndOrSingleSlash { - complete("Welcome to websocket server") } - } ~ get { - path("ws-echo") { - handleWebSocketMessages(echoService) + + out.add(RawBytes(code, dataBytes)) + } catch { + case NonFatal(e) => + log.warn(s"${id(ctx)} Malformed network message", e) + } + + override def encode(ctx: ChannelHandlerContext, msg: RawBytes, out: ByteBuf) = { + out.writeInt(Magic) + out.writeByte(msg.code) + if (msg.data.length > 0) { + out.writeInt(msg.data.length) + out.writeBytes(FastCryptographicHash.hash(msg.data), 0, ChecksumLength) + out.writeBytes(msg.data) + } else { + out.writeInt(0) } } +} + +object LegacyFrameCodec { + val Magic = 0x12345678 + + private val messageSpecs: Map[Byte, MessageSpec[_ <: AnyRef]] = + BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap +} + +@Sharable +class MessageCodec() extends MessageToMessageCodec[RawBytes, Message] with ScorexLogging { + + private val specs: Map[Byte, MessageSpec[_ <: AnyRef]] = BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap + + override def encode(ctx: ChannelHandlerContext, msg: Message, out: util.List[AnyRef]) = msg match { + case LocalScoreChanged(score) => out.add(RawBytes(ScoreMessageSpec.messageCode, ScoreMessageSpec.serializeData(score))) + case GetPeers => out.add(RawBytes(GetPeersSpec.messageCode, Array[Byte]())) + case k: KnownPeers => out.add(RawBytes(PeersSpec.messageCode, PeersSpec.serializeData(k))) + case gs: GetSignatures => out.add(RawBytes(GetSignaturesSpec.messageCode, GetSignaturesSpec.serializeData(gs))) + case s: Signatures => out.add(RawBytes(SignaturesSpec.messageCode, SignaturesSpec.serializeData(s))) + case g: GetBlock => out.add(RawBytes(GetBlockSpec.messageCode, GetBlockSpec.serializeData(g))) + case BlockForged(b) => out.add(RawBytes(BlockMessageSpec.messageCode, b.bytes)) + case m: MicroBlockInv => out.add(RawBytes(MicroBlockInvMessageSpec.messageCode, MicroBlockInvMessageSpec.serializeData(m))) + case m: MicroBlockRequest => out.add(RawBytes(MicroBlockRequestMessageSpec.messageCode, MicroBlockRequestMessageSpec.serializeData(m))) + case m: MicroBlockResponse => out.add(RawBytes(MicroBlockResponseMessageSpec.messageCode, MicroBlockResponseMessageSpec.serializeData(m))) + case r: RawBytes => out.add(r) + } + + override def decode(ctx: ChannelHandlerContext, msg: RawBytes, out: util.List[AnyRef]): Unit = { + specs(msg.code).deserializeData(msg.data) match { + case Success(x) => out.add(x) + case Failure(e) => println(e.getMessage) + } + } +} + +class MessageHandler(handler: PartialFunction[Any, Unit]) extends ChannelInboundHandlerAdapter{ + override def channelRead(ctx: ChannelHandlerContext, msg: scala.Any): Unit = { + handler(msg) +// msg match { +// case hs: Handshake => { +// ctx.writeAndFlush(GetPeers) +// } +// case KnownPeers(peers) => { +// ctx.close() +// } +// case _ => +// } + } +} + +class HandshakeHandler() extends ReplayingDecoder[Void] with ScorexLogging { + private val handshake = + Handshake(Constants.ApplicationName + Settings.default.chainId, Version.VersionTuple, + "discovery", new Random().nextLong(), None) + + override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]): Unit = { + out.add(Handshake.decode(in)) + ctx.pipeline().remove(this) + } + + override def channelActive(ctx: ChannelHandlerContext): Unit = { + ctx.writeAndFlush(handshake.encode(ctx.alloc().buffer())) + } +} + +object DiscoveryApp extends App { + import io.netty.channel.nio.NioEventLoopGroup + + implicit val workerGroup = new NioEventLoopGroup + + + import io.netty.channel.socket.nio.NioSocketChannel + + + implicit val ec: ExecutionContext = ExecutionContext.global + + def getPeersFromNode(address: InetSocketAddress)(implicit eventLoopGroup: EventLoopGroup): scala.concurrent.Future[Seq[InetSocketAddress]] = Future { + var peers: Seq[InetSocketAddress] = Seq.empty + + new Bootstrap() + .group(workerGroup) + .channel(classOf[NioSocketChannel]) + .handler(new PipelineInitializer[SocketChannel](Seq( + new HandshakeHandler(), + new LengthFieldPrepender(4), + new LengthFieldBasedFrameDecoder(100 * 1024 * 1024, 0, 4, 0, 4), + new LegacyFrameCodec(), + new MessageCodec(), + new MessageHandler({ + case hs: Handshake => println("HS!") + case _ => + }) + ))) + .remoteAddress(address.getAddress, address.getPort) + .connect().channel().closeFuture().sync() + + peers + } + var results = Await.result(getPeersFromNode(Settings.default.initialPeers.head), FiniteDuration(1, TimeUnit.MINUTES)) + print(results) + workerGroup.shutdownGracefully() - val binding = Http().bindAndHandle(route, interface, port) - println(s"Server is now online at http://$interface:$port\nPress RETURN to stop...") - StdIn.readLine() - import actorSystem.dispatcher - binding.flatMap(_.unbind()).onComplete(_ => actorSystem.terminate()) - println("Server is down...") + // implicit val actorSystem = ActorSystem("akka-system") +// implicit val flowMaterializer = ActorMaterializer() +// +// val interface = "localhost" +// val port = 8080 +// +// val source: Source[Message, _] = Source.single(0).map(_ => { +// println("Connected") +// TextMessage("!!!!!") +// }) +// +// val sink: Sink[Message, _] = Sink.ignore +// +// val echoService: Flow[Message, Message, _] = Flow[Message].map { +// case TextMessage.Strict(txt) => TextMessage("ECHO: " + txt) +// case _ => TextMessage("Message type unsupported") +// } +// +// import akka.http.scaladsl.server.Directives._ +// +// val route = get { +// pathEndOrSingleSlash { +// complete("Welcome to websocket server") +// } +// } ~ get { +// path("ws-echo") { +// handleWebSocketMessages(echoService) +// } +// } +// +// +// val binding = Http().bindAndHandle(route, interface, port) +// println(s"Server is now online at http://$interface:$port\nPress RETURN to stop...") +// StdIn.readLine() +// +// import actorSystem.dispatcher +// +// binding.flatMap(_.unbind()).onComplete(_ => actorSystem.terminate()) +// println("Server is down...") } diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala b/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala new file mode 100644 index 00000000000..92d7abe32a2 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala @@ -0,0 +1,27 @@ +package com.wavesplatform.discovery + +import java.net.InetSocketAddress + +import net.ceedubs.ficus.Ficus._ +import com.typesafe.config.{Config, ConfigFactory} +import net.ceedubs.ficus.readers.{NameMapper, ValueReader} + +case class Settings(chainId: Char, initialPeers: Seq[InetSocketAddress]) + +object Settings{ + implicit val readConfigInHyphen: NameMapper = net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase // IDEA bug + + implicit val inetSocketAddressReader: ValueReader[InetSocketAddress] = { (config: Config, path: String) => + val value = config.as[String](s"$path").split(":") + new InetSocketAddress( + value(0), + value(1).toInt + ) + } + + implicit val charReader: ValueReader[Char] = (config: Config, path: String) => config.as[String](s"$path").head + + import net.ceedubs.ficus.readers.ArbitraryTypeReader._ + + lazy val default: Settings = ConfigFactory.load().as[Settings]("discovery") +} \ No newline at end of file From b333d12e3b06f52a1aa53931c2f7e7b9affdd65b Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Thu, 19 Oct 2017 17:43:16 +0300 Subject: [PATCH 03/14] WIP --- discovery/build.sbt | 3 +- discovery/src/main/resources/application.conf | 59 +----- .../DiscoveryApp.scala | 180 +++++++++++++++--- .../Settings.scala | 27 +++ .../actors/MainActor.scala | 106 +++++++++++ .../actors/PeerDiscoveryActor.scala | 28 +++ .../collections/Pool.scala | 27 +++ .../com.wavesplatform.discovery/package.scala | 45 +++++ 8 files changed, 396 insertions(+), 79 deletions(-) create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/collections/Pool.scala create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/package.scala diff --git a/discovery/build.sbt b/discovery/build.sbt index 307becbfe16..1c7ea2deafa 100755 --- a/discovery/build.sbt +++ b/discovery/build.sbt @@ -1,2 +1,3 @@ libraryDependencies += "com.typesafe.akka" % "akka-http-experimental_2.11" % "2.4.11.2" - \ No newline at end of file +libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.8.0" +libraryDependencies += "com.typesafe.akka" % "akka-actor_2.10" % "2.2-M1" diff --git a/discovery/src/main/resources/application.conf b/discovery/src/main/resources/application.conf index b2208bcd862..74a5c9ab30d 100644 --- a/discovery/src/main/resources/application.conf +++ b/discovery/src/main/resources/application.conf @@ -1,56 +1,5 @@ -generator { - chain-id = T - accounts = [ - "f8ypmbNfr6ocg8kJ7F1MaC4A89f672ZY6LETRiAEbrb", - "A7ucz2PXvDx3CCDYRhtRkHHzPZZnCTP4T6mN18QvsfCQ", - "uD933WUgrNzhFE2amLT6pijejUm2bTocKqU1Nh3D3rk", - "63HeWce3hCmeTSxciqG3MsHSp9A1B8jueeS6NmoGAu3F" - ] - send-to = [ - { - address = 52.30.47.67 - port = 6863 - } - ] - - worker { - iterations = 100 - delay = 5s - - auto-reconnect = false - reconnect-delay = 3s - } - - mode = "WIDE" - narrow { - transactions = 10 - probabilities { - payment: 0.1 - issue: 0.1 - transfer: 0.3 - reissue: 0.05 - burn: 0.075 - exchange: 0.1 - lease: 0.1 - lease-cancel: 0.075 - create-alias: 0.1 - } - } - - wide { - transactions = 10 - limit-dest-accounts = 10000 - min-fee = 100000 - max-fee = 150000 - } - - dyn-wide { - limit-dest-accounts = 10000 - start = 1 - grow-adder = 0.0408 - min-fee = 100000 - max-fee = 150000 - } +discovery { + chain-id = W + //initial-peers = ["52.30.47.67:6863", "52.28.66.217:6863", "52.77.111.219:6863", "52.51.92.182:6863"] + initial-peers = ["138.201.152.163:6868", "138.201.152.164:6868", "138.201.152.165:6868", "35.156.19.4:6868", "52.50.69.247:6868", "52.57.147.71:6868"] } - -include "local.conf" diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala index fb6c122bac6..e54ae27a6e4 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala @@ -1,52 +1,186 @@ package com.wavesplatform.discovery -import akka.actor.ActorSystem +import java.util +import java.util.concurrent.TimeUnit + +import akka.NotUsed +import akka.actor.{Actor, ActorRef, ActorSystem, AllForOneStrategy, OneForOneStrategy, Props, SupervisorStrategy} import akka.http.scaladsl.Http -import akka.http.scaladsl.model.ws.{Message, TextMessage} +import akka.http.scaladsl.model.ws.TextMessage import akka.stream.{ActorMaterializer, OverflowStrategy} import akka.stream.scaladsl.{Flow, Sink, Source} +import com.wavesplatform.Version +import com.wavesplatform.discovery.actors.MainActor.WebSocketConnected +import com.wavesplatform.discovery.actors.{IOActor, MainActor} +import com.wavesplatform.network.{BasicMessagesRepo, BlockForged, BlockMessageSpec, GetBlock, GetBlockSpec, GetPeers, GetPeersSpec, GetSignatures, GetSignaturesSpec, Handshake, KnownPeers, LocalScoreChanged, Message, MicroBlockInv, MicroBlockInvMessageSpec, MicroBlockRequest, MicroBlockRequestMessageSpec, MicroBlockResponse, MicroBlockResponseMessageSpec, PeerDatabase, PeersSpec, PipelineInitializer, RawBytes, ScoreMessageSpec, Signatures, SignaturesSpec, id} +import com.wavesplatform.settings.Constants +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled.wrappedBuffer +import io.netty.channel.ChannelHandler.Sharable +import io.netty.channel.{ChannelHandlerContext, ChannelInboundHandlerAdapter, EventLoopGroup} +import io.netty.handler.codec._ +import scorex.crypto.hash.FastCryptographicHash +import scorex.network.message.Message.ChecksumLength +import scorex.network.message.MessageSpec +import scorex.utils.ScorexLogging +import scala.concurrent.{Await, ExecutionContext, Future} +import scala.concurrent.duration.FiniteDuration import scala.io.StdIn +import scala.util.control.NonFatal +import scala.util.{Failure, Random, Success} + +class LegacyFrameCodec() extends ByteToMessageCodec[RawBytes] with ScorexLogging { + import LegacyFrameCodec._ + + override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]) = try { + require(in.readInt() == Magic, "invalid magic number") + + val code = in.readByte() + require(messageSpecs.contains(code), s"Unexpected message code $code") + + val spec = messageSpecs(code) + val length = in.readInt() + require(length <= spec.maxLength, s"${spec.messageName} length $length exceeds ${spec.maxLength}") + + val dataBytes = new Array[Byte](length) + if (length > 0) { + val declaredChecksum = in.readSlice(ChecksumLength) + in.readBytes(dataBytes) + val actualChecksum = wrappedBuffer(FastCryptographicHash.hash(dataBytes), 0, ChecksumLength) + + require(declaredChecksum.equals(actualChecksum), "invalid checksum") + actualChecksum.release() + + } + + out.add(RawBytes(code, dataBytes)) + } catch { + case NonFatal(e) => + log.warn(s"${id(ctx)} Malformed network message", e) + } + + override def encode(ctx: ChannelHandlerContext, msg: RawBytes, out: ByteBuf) = { + out.writeInt(Magic) + out.writeByte(msg.code) + if (msg.data.length > 0) { + out.writeInt(msg.data.length) + out.writeBytes(FastCryptographicHash.hash(msg.data), 0, ChecksumLength) + out.writeBytes(msg.data) + } else { + out.writeInt(0) + } + } +} + +object LegacyFrameCodec { + val Magic = 0x12345678 + + private val messageSpecs: Map[Byte, MessageSpec[_ <: AnyRef]] = + BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap +} + +@Sharable +class MessageCodec() extends MessageToMessageCodec[RawBytes, Message] with ScorexLogging { + + private val specs: Map[Byte, MessageSpec[_ <: AnyRef]] = BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap + + override def encode(ctx: ChannelHandlerContext, msg: Message, out: util.List[AnyRef]) = msg match { + case LocalScoreChanged(score) => out.add(RawBytes(ScoreMessageSpec.messageCode, ScoreMessageSpec.serializeData(score))) + case GetPeers => out.add(RawBytes(GetPeersSpec.messageCode, Array[Byte]())) + case k: KnownPeers => out.add(RawBytes(PeersSpec.messageCode, PeersSpec.serializeData(k))) + case gs: GetSignatures => out.add(RawBytes(GetSignaturesSpec.messageCode, GetSignaturesSpec.serializeData(gs))) + case s: Signatures => out.add(RawBytes(SignaturesSpec.messageCode, SignaturesSpec.serializeData(s))) + case g: GetBlock => out.add(RawBytes(GetBlockSpec.messageCode, GetBlockSpec.serializeData(g))) + case BlockForged(b) => out.add(RawBytes(BlockMessageSpec.messageCode, b.bytes)) + case m: MicroBlockInv => out.add(RawBytes(MicroBlockInvMessageSpec.messageCode, MicroBlockInvMessageSpec.serializeData(m))) + case m: MicroBlockRequest => out.add(RawBytes(MicroBlockRequestMessageSpec.messageCode, MicroBlockRequestMessageSpec.serializeData(m))) + case m: MicroBlockResponse => out.add(RawBytes(MicroBlockResponseMessageSpec.messageCode, MicroBlockResponseMessageSpec.serializeData(m))) + case r: RawBytes => out.add(r) + } + + override def decode(ctx: ChannelHandlerContext, msg: RawBytes, out: util.List[AnyRef]): Unit = { + specs(msg.code).deserializeData(msg.data) match { + case Success(x) => out.add(x) + case Failure(e) => println(e.getMessage) + } + } +} + +class MessageHandler(handler: PartialFunction[(Any, ChannelHandlerContext), Unit]) extends ChannelInboundHandlerAdapter{ + override def channelRead(ctx: ChannelHandlerContext, msg: scala.Any): Unit = { + handler((msg, ctx)) + } +} + +class HandshakeHandler() extends ReplayingDecoder[Void] with ScorexLogging { + private val handshake = + Handshake(Constants.ApplicationName + Settings.default.chainId, Version.VersionTuple, + "discovery", new Random().nextLong(), None) + + override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]): Unit = { + out.add(Handshake.decode(in)) + ctx.pipeline().remove(this) + } + + override def channelActive(ctx: ChannelHandlerContext): Unit = { + ctx.writeAndFlush(handshake.encode(ctx.alloc().buffer())) + } +} + object DiscoveryApp extends App { - implicit val actorSystem = ActorSystem("akka-system") + implicit val system = ActorSystem("Default") implicit val flowMaterializer = ActorMaterializer() - val interface = "localhost" - val port = 8080 + val mainActor = system.actorOf(Props[MainActor], name = "main") - val source: Source[Message, _] = Source.single(0).map(_ => { - println("Connected") - TextMessage("!!!!!") - }) + mainActor ! MainActor.Peers(Settings.default.initialPeers.toSet) - val sink: Sink[Message, _] = Sink.ignore + implicit val ec: ExecutionContext = ExecutionContext.global + system.scheduler.schedule(FiniteDuration(1, TimeUnit.SECONDS),FiniteDuration(1, TimeUnit.SECONDS), mainActor, MainActor.Discover) - val echoService: Flow[Message, Message, _] = Flow[Message].map { - case TextMessage.Strict(txt) => TextMessage("ECHO: " + txt) - case _ => TextMessage("Message type unsupported") - } + //var results = Await.result(getPeersFromNode(Settings.default.initialPeers(2)), FiniteDuration(1, TimeUnit.MINUTES)) + //print(results) - import akka.http.scaladsl.server.Directives._ + //workerGroup.shutdownGracefully() + + val interface = "localhost" + val port = 8080 +// + import akka.http.scaladsl.server.Directives._ +// val route = get { pathEndOrSingleSlash { complete("Welcome to websocket server") } } ~ get { path("ws-echo") { - handleWebSocketMessages(echoService) - } - } + val sink: Sink[akka.http.scaladsl.model.ws.Message, _] = Sink.ignore + + val source: Source[akka.http.scaladsl.model.ws.Message, NotUsed] = + Source.actorRef[String](1, OverflowStrategy.dropTail) + .mapMaterializedValue { actor => + mainActor ! WebSocketConnected(actor) + NotUsed + }.map( + // transform domain message to web socket message + (outMsg: String) => TextMessage(outMsg)) + handleWebSocketMessages(Flow.fromSinkAndSource(sink, source)) + } + } +// +// val binding = Http().bindAndHandle(route, interface, port) println(s"Server is now online at http://$interface:$port\nPress RETURN to stop...") StdIn.readLine() - - import actorSystem.dispatcher - - binding.flatMap(_.unbind()).onComplete(_ => actorSystem.terminate()) - println("Server is down...") +// +// import actorSystem.dispatcher +// +// binding.flatMap(_.unbind()).onComplete(_ => actorSystem.terminate()) +// println("Server is down...") } diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala b/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala new file mode 100644 index 00000000000..92d7abe32a2 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala @@ -0,0 +1,27 @@ +package com.wavesplatform.discovery + +import java.net.InetSocketAddress + +import net.ceedubs.ficus.Ficus._ +import com.typesafe.config.{Config, ConfigFactory} +import net.ceedubs.ficus.readers.{NameMapper, ValueReader} + +case class Settings(chainId: Char, initialPeers: Seq[InetSocketAddress]) + +object Settings{ + implicit val readConfigInHyphen: NameMapper = net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase // IDEA bug + + implicit val inetSocketAddressReader: ValueReader[InetSocketAddress] = { (config: Config, path: String) => + val value = config.as[String](s"$path").split(":") + new InetSocketAddress( + value(0), + value(1).toInt + ) + } + + implicit val charReader: ValueReader[Char] = (config: Config, path: String) => config.as[String](s"$path").head + + import net.ceedubs.ficus.readers.ArbitraryTypeReader._ + + lazy val default: Settings = ConfigFactory.load().as[Settings]("discovery") +} \ No newline at end of file diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala b/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala new file mode 100644 index 00000000000..c9a08c3c798 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala @@ -0,0 +1,106 @@ +package com.wavesplatform.discovery.actors + +import java.net.InetSocketAddress +import java.time.LocalDateTime + +import akka.actor.SupervisorStrategy.Restart +import akka.actor.{Actor, ActorRef, OneForOneStrategy, Props, SupervisorStrategy} +import akka.routing.{ActorRefRoutee, Router, SmallestMailboxRoutingLogic} +import com.wavesplatform.discovery.collections.Pool +import play.api.libs.json._ + +class ExpirationSet[T](val expirationTimeMilis: Long) extends scala.collection.mutable.Set[T]{ + private var inner = Map.empty[T, Long] + private def freshInner = { + val time = System.currentTimeMillis() + inner = inner.filter({ case (k, v) => + time - v > expirationTimeMilis }) + + inner + } + + override def +=(elem: T) = { + inner += ((elem, System.currentTimeMillis())) + this + } + + override def -=(elem: T) = { + inner = inner.-(elem) + this + } + + override def contains(elem: T) = freshInner.contains(elem) + + override def iterator = freshInner.keys.iterator +} + +class MainActor extends Actor { + + import MainActor._ + + var router = { + val routees = Vector.fill(5) { + val r = context.actorOf(Props[PeerDiscoveryActor]) + context watch r + ActorRefRoutee(r) + } + Router(SmallestMailboxRoutingLogic(), routees) + } + + private val alivePeers = new Pool[InetSocketAddress] + private val deadPeers = new ExpirationSet[InetSocketAddress](1000*60*60*5) + private val peerResponses = scala.collection.mutable.Map.empty[InetSocketAddress, Set[InetSocketAddress]] + private val connections = scala.collection.mutable.Set.empty[ActorRef] + + override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() { + case _: Exception => Restart + } + + def receive: PartialFunction[Any, Unit] = { + + case Peers(p) => p.foreach(alivePeers.add) + + case Discover => alivePeers.next().foreach(peer => router.route(PeerDiscoveryActor.GetPeersFrom(peer), self)) + + case PeerInfo(peer, peers) => { + self ! Peers(peers) + + peerResponses.put(peer, peers) match { + case Some(oldValue) if oldValue == peers => //nothing changed + case _ if peers.nonEmpty => broadcastPeerInfo(peer, peers) + case _ => + } + } + + case PeerProblem(peer) => { + alivePeers.remove(peer) + deadPeers.add(peer) + } + + case WebSocketConnected(client) => { + connections.add(client) + client ! jsonPeersData + } + } + + private def jsonPeersData = peerResponses.foldLeft(Json.obj())((json, keyValue) => json.+(keyValue._1.toString, JsArray(keyValue._2.map(v => JsString(v.toString)).toSeq))).toString() + + private def broadcastPeerInfo(peer: InetSocketAddress, peers: Set[InetSocketAddress]): Unit = { + val response = Json.obj(peer.toString -> JsArray(peers.map(p => JsString(p.toString)).toSeq)).toString() + connections.foreach(c => c ! response) + } +} + +object MainActor { + + case class PeerInfo(peer: InetSocketAddress, peers: Set[InetSocketAddress]) + + case class PeerProblem(peer: InetSocketAddress) + + case class Peers(peers: Set[InetSocketAddress]) + + case class WebSocketConnected(actor: ActorRef) + + case object Discover + +} \ No newline at end of file diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala new file mode 100644 index 00000000000..004d659b0fb --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala @@ -0,0 +1,28 @@ +package com.wavesplatform.discovery.actors + +import java.net.InetSocketAddress +import java.util.concurrent.TimeUnit + +import akka.actor.Actor + +import scala.concurrent.Await +import scala.concurrent.duration._ +import com.wavesplatform.discovery._ + +object PeerDiscoveryActor { + case class GetPeersFrom(peer: InetSocketAddress) +} + +class PeerDiscoveryActor extends Actor { + import PeerDiscoveryActor._ + def receive: PartialFunction[Any, Unit] = { + case GetPeersFrom(peer) => context.parent ! MainActor.PeerInfo(peer, Await.result(getPeersFromNode(peer), FiniteDuration(1, TimeUnit.MINUTES))) + } + + override def preRestart(reason: Throwable, message: Option[Any]): Unit = { + message match { + case Some(GetPeersFrom(peer)) => context.parent ! MainActor.PeerInfo(peer, Set.empty) + case _ => + } + } +} \ No newline at end of file diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/collections/Pool.scala b/discovery/src/main/scala/com.wavesplatform.discovery/collections/Pool.scala new file mode 100644 index 00000000000..ecfd2197289 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/collections/Pool.scala @@ -0,0 +1,27 @@ +package com.wavesplatform.discovery.collections + +class Pool[T] { + private val queue = scala.collection.mutable.Queue.empty[T] + private val items = scala.collection.mutable.Set.empty[T] + + def add(item: T): Unit = { + if (!items.contains(item)) { + items.add(item) + queue.enqueue(item) + } + } + + def next(): Option[T] = { + if (queue.nonEmpty) { + val item = queue.dequeue() + queue.enqueue(item) + Some(item) + } + else None + } + + def remove(item: T): Unit = { + items.remove(item) + queue.dequeueFirst(i => i == item) + } +} diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/package.scala b/discovery/src/main/scala/com.wavesplatform.discovery/package.scala new file mode 100644 index 00000000000..a4358e8bbe2 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/package.scala @@ -0,0 +1,45 @@ +package com.wavesplatform + +import java.net.InetSocketAddress + +import com.wavesplatform.network.{GetPeers, Handshake, KnownPeers, PipelineInitializer} +import io.netty.bootstrap.Bootstrap +import io.netty.channel.EventLoopGroup +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.{LengthFieldBasedFrameDecoder, LengthFieldPrepender} + +import scala.concurrent.{ExecutionContext, Future} + +package object discovery { + + implicit val workerGroup = new NioEventLoopGroup + implicit val ec: ExecutionContext = ExecutionContext.global + + def getPeersFromNode(address: InetSocketAddress)(implicit eventLoopGroup: EventLoopGroup): scala.concurrent.Future[Set[InetSocketAddress]] = Future { + var peers: Set[InetSocketAddress] = Set.empty + + new Bootstrap() + .group(workerGroup) + .channel(classOf[NioSocketChannel]) + .handler(new PipelineInitializer[SocketChannel](Seq( + new HandshakeHandler(), + new LengthFieldPrepender(4), + new LengthFieldBasedFrameDecoder(100 * 1024 * 1024, 0, 4, 0, 4), + new LegacyFrameCodec(), + new MessageCodec(), + new MessageHandler({ case (msg, ctx) => + msg match { + case hs: Handshake => ctx.writeAndFlush(GetPeers) + case KnownPeers(p) => peers = p.toSet; ctx.close() + case _ => + } + }) + ))) + .remoteAddress(address.getAddress, address.getPort) + .connect().channel().closeFuture().sync() + + peers + } +} From fd9ad8df6800aade472375a8734296683b8d3a95 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Wed, 25 Oct 2017 09:29:44 +0300 Subject: [PATCH 04/14] cleanup --- .../DiscoveryApp.scala | 155 ++---------------- .../network/HandshakeHandler.scala | 29 ++++ .../network/LegacyFrameCodec.scala | 64 ++++++++ .../network/MessageCodec.scala | 39 +++++ .../network/MessageHandler.scala | 9 + .../com.wavesplatform.discovery/package.scala | 1 + 6 files changed, 155 insertions(+), 142 deletions(-) create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/network/HandshakeHandler.scala create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/network/LegacyFrameCodec.scala create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/network/MessageHandler.scala diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala index 1b44830c680..5c4dce6b2b9 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala @@ -1,139 +1,23 @@ package com.wavesplatform.discovery -import java.util import java.util.concurrent.TimeUnit import akka.NotUsed -import akka.actor.{Actor, ActorRef, ActorSystem, AllForOneStrategy, OneForOneStrategy, Props, SupervisorStrategy} +import akka.actor.{ActorSystem, Props} import akka.http.scaladsl.Http import akka.http.scaladsl.model.ws.TextMessage -import akka.stream.{ActorMaterializer, OverflowStrategy} import akka.stream.scaladsl.{Flow, Sink, Source} -import com.wavesplatform.Version -import com.wavesplatform.discovery.actors.MainActor.WebSocketConnected +import akka.stream.{ActorMaterializer, OverflowStrategy} import com.wavesplatform.discovery.actors.MainActor -import com.wavesplatform.network.{BasicMessagesRepo, BlockForged, BlockMessageSpec, GetBlock, GetBlockSpec, GetPeers, GetPeersSpec, GetSignatures, GetSignaturesSpec, Handshake, KnownPeers, LocalScoreChanged, Message, MicroBlockInv, MicroBlockInvMessageSpec, MicroBlockRequest, MicroBlockRequestMessageSpec, MicroBlockResponse, MicroBlockResponseMessageSpec, PeerDatabase, PeersSpec, PipelineInitializer, RawBytes, ScoreMessageSpec, Signatures, SignaturesSpec, id} -import com.wavesplatform.settings.Constants -import io.netty.buffer.ByteBuf -import io.netty.buffer.Unpooled.wrappedBuffer -import io.netty.channel.ChannelHandler.Sharable -import io.netty.channel.{ChannelHandlerContext, ChannelInboundHandlerAdapter, EventLoopGroup} -import io.netty.handler.codec._ -import scorex.crypto.hash.FastCryptographicHash -import scorex.network.message.Message.ChecksumLength -import scorex.network.message.MessageSpec -import scorex.utils.ScorexLogging +import com.wavesplatform.discovery.actors.MainActor.WebSocketConnected -import scala.concurrent.{Await, ExecutionContext, Future} +import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration import scala.io.StdIn -import scala.util.control.NonFatal -import scala.util.{Failure, Random, Success} - -class LegacyFrameCodec() extends ByteToMessageCodec[RawBytes] with ScorexLogging { - import LegacyFrameCodec._ - - override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]) = try { - require(in.readInt() == Magic, "invalid magic number") - - val code = in.readByte() - require(messageSpecs.contains(code), s"Unexpected message code $code") - - val spec = messageSpecs(code) - val length = in.readInt() - require(length <= spec.maxLength, s"${spec.messageName} length $length exceeds ${spec.maxLength}") - - val dataBytes = new Array[Byte](length) - if (length > 0) { - val declaredChecksum = in.readSlice(ChecksumLength) - in.readBytes(dataBytes) - val actualChecksum = wrappedBuffer(FastCryptographicHash.hash(dataBytes), 0, ChecksumLength) - - require(declaredChecksum.equals(actualChecksum), "invalid checksum") - actualChecksum.release() - - } - - out.add(RawBytes(code, dataBytes)) - } catch { - case NonFatal(e) => - log.warn(s"${id(ctx)} Malformed network message", e) - } - - override def encode(ctx: ChannelHandlerContext, msg: RawBytes, out: ByteBuf) = { - out.writeInt(Magic) - out.writeByte(msg.code) - if (msg.data.length > 0) { - out.writeInt(msg.data.length) - out.writeBytes(FastCryptographicHash.hash(msg.data), 0, ChecksumLength) - out.writeBytes(msg.data) - } else { - out.writeInt(0) - } - } -} - -object LegacyFrameCodec { - val Magic = 0x12345678 - - private val messageSpecs: Map[Byte, MessageSpec[_ <: AnyRef]] = - BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap -} - -@Sharable -class MessageCodec() extends MessageToMessageCodec[RawBytes, Message] with ScorexLogging { - - private val specs: Map[Byte, MessageSpec[_ <: AnyRef]] = BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap - - override def encode(ctx: ChannelHandlerContext, msg: Message, out: util.List[AnyRef]) = msg match { - case LocalScoreChanged(score) => out.add(RawBytes(ScoreMessageSpec.messageCode, ScoreMessageSpec.serializeData(score))) - case GetPeers => out.add(RawBytes(GetPeersSpec.messageCode, Array[Byte]())) - case k: KnownPeers => out.add(RawBytes(PeersSpec.messageCode, PeersSpec.serializeData(k))) - case gs: GetSignatures => out.add(RawBytes(GetSignaturesSpec.messageCode, GetSignaturesSpec.serializeData(gs))) - case s: Signatures => out.add(RawBytes(SignaturesSpec.messageCode, SignaturesSpec.serializeData(s))) - case g: GetBlock => out.add(RawBytes(GetBlockSpec.messageCode, GetBlockSpec.serializeData(g))) - case BlockForged(b) => out.add(RawBytes(BlockMessageSpec.messageCode, b.bytes)) - case m: MicroBlockInv => out.add(RawBytes(MicroBlockInvMessageSpec.messageCode, MicroBlockInvMessageSpec.serializeData(m))) - case m: MicroBlockRequest => out.add(RawBytes(MicroBlockRequestMessageSpec.messageCode, MicroBlockRequestMessageSpec.serializeData(m))) - case m: MicroBlockResponse => out.add(RawBytes(MicroBlockResponseMessageSpec.messageCode, MicroBlockResponseMessageSpec.serializeData(m))) - case r: RawBytes => out.add(r) - } - - override def decode(ctx: ChannelHandlerContext, msg: RawBytes, out: util.List[AnyRef]): Unit = { - specs(msg.code).deserializeData(msg.data) match { - case Success(x) => out.add(x) - case Failure(e) => println(e.getMessage) - } - } -} - -class MessageHandler(handler: PartialFunction[(Any, ChannelHandlerContext), Unit]) extends ChannelInboundHandlerAdapter{ - override def channelRead(ctx: ChannelHandlerContext, msg: scala.Any): Unit = { - handler((msg, ctx)) - } -} - -class HandshakeHandler() extends ReplayingDecoder[Void] with ScorexLogging { - private val handshake = - Handshake(Constants.ApplicationName + Settings.default.chainId, Version.VersionTuple, - "discovery", new Random().nextLong(), None) - - override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]): Unit = { - out.add(Handshake.decode(in)) - ctx.pipeline().remove(this) - } - - override def channelActive(ctx: ChannelHandlerContext): Unit = { - ctx.writeAndFlush(handshake.encode(ctx.alloc().buffer())) - } -} - object DiscoveryApp extends App { - import io.netty.channel.nio.NioEventLoopGroup - - implicit val system = ActorSystem("Default") - implicit val flowMaterializer = ActorMaterializer() + implicit val system: ActorSystem = ActorSystem("Default") + implicit val flowMaterializer: ActorMaterializer = ActorMaterializer() val mainActor = system.actorOf(Props[MainActor], name = "main") @@ -142,46 +26,33 @@ object DiscoveryApp extends App { implicit val ec: ExecutionContext = ExecutionContext.global system.scheduler.schedule(FiniteDuration(1, TimeUnit.SECONDS),FiniteDuration(1, TimeUnit.SECONDS), mainActor, MainActor.Discover) - //var results = Await.result(getPeersFromNode(Settings.default.initialPeers(2)), FiniteDuration(1, TimeUnit.MINUTES)) - //print(results) - - //workerGroup.shutdownGracefully() - val interface = "localhost" val port = 8080 -// import akka.http.scaladsl.server.Directives._ -// + val route = get { pathEndOrSingleSlash { - complete("Welcome to websocket server") - } - } ~ get { - path("ws-echo") { val sink: Sink[akka.http.scaladsl.model.ws.Message, _] = Sink.ignore - val source: Source[akka.http.scaladsl.model.ws.Message, NotUsed] = Source.actorRef[String](1, OverflowStrategy.dropTail) .mapMaterializedValue { actor => mainActor ! WebSocketConnected(actor) NotUsed }.map( - // transform domain message to web socket message (outMsg: String) => TextMessage(outMsg)) handleWebSocketMessages(Flow.fromSinkAndSource(sink, source)) } } -// -// + + val binding = Http().bindAndHandle(route, interface, port) println(s"Server is now online at http://$interface:$port\nPress RETURN to stop...") StdIn.readLine() -// -// import actorSystem.dispatcher -// -// binding.flatMap(_.unbind()).onComplete(_ => actorSystem.terminate()) -// println("Server is down...") + binding.flatMap(_.unbind()).onComplete(_ => { + system.terminate() + workerGroup.shutdownGracefully() + }) } diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/network/HandshakeHandler.scala b/discovery/src/main/scala/com.wavesplatform.discovery/network/HandshakeHandler.scala new file mode 100644 index 00000000000..3ef5001eb26 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/network/HandshakeHandler.scala @@ -0,0 +1,29 @@ +package com.wavesplatform.discovery.network + +import java.util + +import com.wavesplatform.Version +import com.wavesplatform.discovery.Settings +import com.wavesplatform.network.Handshake +import com.wavesplatform.settings.Constants +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.ReplayingDecoder +import scorex.utils.ScorexLogging + +import scala.util.Random + +class HandshakeHandler() extends ReplayingDecoder[Void] with ScorexLogging { + private val handshake = + Handshake(Constants.ApplicationName + Settings.default.chainId, Version.VersionTuple, + "discovery", new Random().nextLong(), None) + + override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]): Unit = { + out.add(Handshake.decode(in)) + ctx.pipeline().remove(this) + } + + override def channelActive(ctx: ChannelHandlerContext): Unit = { + ctx.writeAndFlush(handshake.encode(ctx.alloc().buffer())) + } +} diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/network/LegacyFrameCodec.scala b/discovery/src/main/scala/com.wavesplatform.discovery/network/LegacyFrameCodec.scala new file mode 100644 index 00000000000..ff7fa12c64c --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/network/LegacyFrameCodec.scala @@ -0,0 +1,64 @@ +package com.wavesplatform.discovery.network + +import java.util + +import com.wavesplatform.network.{BasicMessagesRepo, RawBytes, id} +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled.wrappedBuffer +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec._ +import scorex.crypto.hash.FastCryptographicHash +import scorex.network.message.Message.ChecksumLength +import scorex.network.message.MessageSpec +import scorex.utils.ScorexLogging + +import scala.util.control.NonFatal + +object LegacyFrameCodec { + val Magic = 0x12345678 + + private val messageSpecs: Map[Byte, MessageSpec[_ <: AnyRef]] = + BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap +} + +class LegacyFrameCodec() extends ByteToMessageCodec[RawBytes] with ScorexLogging { + import LegacyFrameCodec._ + + override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]) = try { + require(in.readInt() == Magic, "invalid magic number") + + val code = in.readByte() + require(messageSpecs.contains(code), s"Unexpected message code $code") + + val spec = messageSpecs(code) + val length = in.readInt() + require(length <= spec.maxLength, s"${spec.messageName} length $length exceeds ${spec.maxLength}") + + val dataBytes = new Array[Byte](length) + if (length > 0) { + val declaredChecksum = in.readSlice(ChecksumLength) + in.readBytes(dataBytes) + val actualChecksum = wrappedBuffer(FastCryptographicHash.hash(dataBytes), 0, ChecksumLength) + + require(declaredChecksum.equals(actualChecksum), "invalid checksum") + actualChecksum.release() + } + + out.add(RawBytes(code, dataBytes)) + } catch { + case NonFatal(e) => + log.warn(s"${id(ctx)} Malformed network message", e) + } + + override def encode(ctx: ChannelHandlerContext, msg: RawBytes, out: ByteBuf) = { + out.writeInt(Magic) + out.writeByte(msg.code) + if (msg.data.length > 0) { + out.writeInt(msg.data.length) + out.writeBytes(FastCryptographicHash.hash(msg.data), 0, ChecksumLength) + out.writeBytes(msg.data) + } else { + out.writeInt(0) + } + } +} \ No newline at end of file diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala b/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala new file mode 100644 index 00000000000..09324d6161a --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala @@ -0,0 +1,39 @@ +package com.wavesplatform.discovery.network + +import java.util + +import com.wavesplatform.network.{BasicMessagesRepo, BlockForged, BlockMessageSpec, GetBlock, GetBlockSpec, GetPeers, GetPeersSpec, GetSignatures, GetSignaturesSpec, KnownPeers, LocalScoreChanged, Message, MicroBlockInv, MicroBlockInvMessageSpec, MicroBlockRequest, MicroBlockRequestMessageSpec, MicroBlockResponse, MicroBlockResponseMessageSpec, PeersSpec, RawBytes, ScoreMessageSpec, Signatures, SignaturesSpec} +import io.netty.channel.ChannelHandler.Sharable +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.MessageToMessageCodec +import scorex.network.message.MessageSpec +import scorex.utils.ScorexLogging + +import scala.util.{Failure, Success} + +@Sharable +class MessageCodec() extends MessageToMessageCodec[RawBytes, Message] with ScorexLogging { + + private val specs: Map[Byte, MessageSpec[_ <: AnyRef]] = BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap + + override def encode(ctx: ChannelHandlerContext, msg: Message, out: util.List[AnyRef]) = msg match { + case LocalScoreChanged(score) => out.add(RawBytes(ScoreMessageSpec.messageCode, ScoreMessageSpec.serializeData(score))) + case GetPeers => out.add(RawBytes(GetPeersSpec.messageCode, Array[Byte]())) + case k: KnownPeers => out.add(RawBytes(PeersSpec.messageCode, PeersSpec.serializeData(k))) + case gs: GetSignatures => out.add(RawBytes(GetSignaturesSpec.messageCode, GetSignaturesSpec.serializeData(gs))) + case s: Signatures => out.add(RawBytes(SignaturesSpec.messageCode, SignaturesSpec.serializeData(s))) + case g: GetBlock => out.add(RawBytes(GetBlockSpec.messageCode, GetBlockSpec.serializeData(g))) + case BlockForged(b) => out.add(RawBytes(BlockMessageSpec.messageCode, b.bytes)) + case m: MicroBlockInv => out.add(RawBytes(MicroBlockInvMessageSpec.messageCode, MicroBlockInvMessageSpec.serializeData(m))) + case m: MicroBlockRequest => out.add(RawBytes(MicroBlockRequestMessageSpec.messageCode, MicroBlockRequestMessageSpec.serializeData(m))) + case m: MicroBlockResponse => out.add(RawBytes(MicroBlockResponseMessageSpec.messageCode, MicroBlockResponseMessageSpec.serializeData(m))) + case r: RawBytes => out.add(r) + } + + override def decode(ctx: ChannelHandlerContext, msg: RawBytes, out: util.List[AnyRef]): Unit = { + specs(msg.code).deserializeData(msg.data) match { + case Success(x) => out.add(x) + case Failure(e) => println(e.getMessage) + } + } +} diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageHandler.scala b/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageHandler.scala new file mode 100644 index 00000000000..1f9e0184182 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageHandler.scala @@ -0,0 +1,9 @@ +package com.wavesplatform.discovery.network + +import io.netty.channel.{ChannelHandlerContext, ChannelInboundHandlerAdapter} + +class MessageHandler(handler: PartialFunction[(Any, ChannelHandlerContext), Unit]) extends ChannelInboundHandlerAdapter{ + override def channelRead(ctx: ChannelHandlerContext, msg: scala.Any): Unit = { + handler((msg, ctx)) + } +} diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/package.scala b/discovery/src/main/scala/com.wavesplatform.discovery/package.scala index a4358e8bbe2..cae18c53fd8 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/package.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/package.scala @@ -2,6 +2,7 @@ package com.wavesplatform import java.net.InetSocketAddress +import com.wavesplatform.discovery.network.{HandshakeHandler, LegacyFrameCodec, MessageCodec, MessageHandler} import com.wavesplatform.network.{GetPeers, Handshake, KnownPeers, PipelineInitializer} import io.netty.bootstrap.Bootstrap import io.netty.channel.EventLoopGroup From 4f76978b7c00ebaf676d65a7b7cf3f2ce5bf81f0 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Wed, 25 Oct 2017 18:11:09 +0300 Subject: [PATCH 05/14] WIP --- discovery/src/main/resources/application.conf | 4 + .../DiscoveryApp.scala | 16 +- .../Settings.scala | 7 +- .../actors/MainActor.scala | 65 ++-- .../actors/PeerDiscoveryActor.scala | 49 ++- .../collections/ExpirationSet.scala | 26 ++ .../network/ExceptionHandler.scala | 39 ++ .../com.wavesplatform.discovery/package.scala | 36 +- ...lestMailboxWithThresholdRoutingLogic.scala | 18 + discovery/src/main/web/app.css | 56 +++ discovery/src/main/web/data.json | 337 ++++++++++++++++++ discovery/src/main/web/index.html | 13 + discovery/src/main/web/index.js | 57 +++ discovery/src/main/web/test.js | 315 ++++++++++++++++ 14 files changed, 938 insertions(+), 100 deletions(-) create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/network/ExceptionHandler.scala create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/routers/SmallestMailboxWithThresholdRoutingLogic.scala create mode 100644 discovery/src/main/web/app.css create mode 100644 discovery/src/main/web/data.json create mode 100644 discovery/src/main/web/index.html create mode 100644 discovery/src/main/web/index.js create mode 100644 discovery/src/main/web/test.js diff --git a/discovery/src/main/resources/application.conf b/discovery/src/main/resources/application.conf index 74a5c9ab30d..d1b9b21938c 100644 --- a/discovery/src/main/resources/application.conf +++ b/discovery/src/main/resources/application.conf @@ -2,4 +2,8 @@ discovery { chain-id = W //initial-peers = ["52.30.47.67:6863", "52.28.66.217:6863", "52.77.111.219:6863", "52.51.92.182:6863"] initial-peers = ["138.201.152.163:6868", "138.201.152.164:6868", "138.201.152.165:6868", "35.156.19.4:6868", "52.50.69.247:6868", "52.57.147.71:6868"] + web-socket-host = "localhost" + web-socket-port = 8080 + workers-count = 10 + discovery-interval = 500ms } diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala index 5c4dce6b2b9..9e80e13c4f8 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala @@ -3,7 +3,7 @@ package com.wavesplatform.discovery import java.util.concurrent.TimeUnit import akka.NotUsed -import akka.actor.{ActorSystem, Props} +import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.http.scaladsl.model.ws.TextMessage import akka.stream.scaladsl.{Flow, Sink, Source} @@ -11,7 +11,6 @@ import akka.stream.{ActorMaterializer, OverflowStrategy} import com.wavesplatform.discovery.actors.MainActor import com.wavesplatform.discovery.actors.MainActor.WebSocketConnected -import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration import scala.io.StdIn @@ -19,15 +18,11 @@ object DiscoveryApp extends App { implicit val system: ActorSystem = ActorSystem("Default") implicit val flowMaterializer: ActorMaterializer = ActorMaterializer() - val mainActor = system.actorOf(Props[MainActor], name = "main") + val mainActor = MainActor(Settings.default.workersCount) mainActor ! MainActor.Peers(Settings.default.initialPeers.toSet) - implicit val ec: ExecutionContext = ExecutionContext.global - system.scheduler.schedule(FiniteDuration(1, TimeUnit.SECONDS),FiniteDuration(1, TimeUnit.SECONDS), mainActor, MainActor.Discover) - - val interface = "localhost" - val port = 8080 + system.scheduler.schedule(FiniteDuration(0, TimeUnit.SECONDS),FiniteDuration(500, TimeUnit.MILLISECONDS), mainActor, MainActor.Discover) import akka.http.scaladsl.server.Directives._ @@ -47,9 +42,8 @@ object DiscoveryApp extends App { } } - - val binding = Http().bindAndHandle(route, interface, port) - println(s"Server is now online at http://$interface:$port\nPress RETURN to stop...") + val binding = Http().bindAndHandle(route, Settings.default.webSocketHost, Settings.default.webSocketPort) + println(s"Server is now online at http://${Settings.default.webSocketHost}:${Settings.default.webSocketPort}\nPress RETURN to stop...") StdIn.readLine() binding.flatMap(_.unbind()).onComplete(_ => { system.terminate() diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala b/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala index 92d7abe32a2..b74dbc1ee3e 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala @@ -6,8 +6,11 @@ import net.ceedubs.ficus.Ficus._ import com.typesafe.config.{Config, ConfigFactory} import net.ceedubs.ficus.readers.{NameMapper, ValueReader} -case class Settings(chainId: Char, initialPeers: Seq[InetSocketAddress]) - +case class Settings(chainId: Char, + initialPeers: Seq[InetSocketAddress], + webSocketHost: String, + webSocketPort: Int, + workersCount: Int) object Settings{ implicit val readConfigInHyphen: NameMapper = net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase // IDEA bug diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala b/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala index c9a08c3c798..9eb462c6668 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala @@ -1,78 +1,52 @@ package com.wavesplatform.discovery.actors import java.net.InetSocketAddress -import java.time.LocalDateTime -import akka.actor.SupervisorStrategy.Restart -import akka.actor.{Actor, ActorRef, OneForOneStrategy, Props, SupervisorStrategy} -import akka.routing.{ActorRefRoutee, Router, SmallestMailboxRoutingLogic} -import com.wavesplatform.discovery.collections.Pool +import akka.actor.SupervisorStrategy.Resume +import akka.actor.{Actor, ActorRef, ActorSystem, OneForOneStrategy, Props, SupervisorStrategy} +import akka.routing.{ActorRefRoutee, Router} +import com.wavesplatform.discovery.collections.{ExpirationSet, Pool} +import com.wavesplatform.discovery.routers.SmallestMailboxWithThresholdRoutingLogic import play.api.libs.json._ -class ExpirationSet[T](val expirationTimeMilis: Long) extends scala.collection.mutable.Set[T]{ - private var inner = Map.empty[T, Long] - private def freshInner = { - val time = System.currentTimeMillis() - inner = inner.filter({ case (k, v) => - time - v > expirationTimeMilis }) - - inner - } - - override def +=(elem: T) = { - inner += ((elem, System.currentTimeMillis())) - this - } - - override def -=(elem: T) = { - inner = inner.-(elem) - this - } - - override def contains(elem: T) = freshInner.contains(elem) - - override def iterator = freshInner.keys.iterator -} - -class MainActor extends Actor { +class MainActor(val workersCount: Int) extends Actor { import MainActor._ - var router = { - val routees = Vector.fill(5) { - val r = context.actorOf(Props[PeerDiscoveryActor]) - context watch r - ActorRefRoutee(r) + private val router = { + val routes = Vector.fill(workersCount) { + ActorRefRoutee(context.actorOf(Props[PeerDiscoveryActor])) } - Router(SmallestMailboxRoutingLogic(), routees) + Router(SmallestMailboxWithThresholdRoutingLogic(5), routes) } private val alivePeers = new Pool[InetSocketAddress] - private val deadPeers = new ExpirationSet[InetSocketAddress](1000*60*60*5) + private val deadPeers = new ExpirationSet[InetSocketAddress](1000*60*60*1) private val peerResponses = scala.collection.mutable.Map.empty[InetSocketAddress, Set[InetSocketAddress]] private val connections = scala.collection.mutable.Set.empty[ActorRef] override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy() { - case _: Exception => Restart + case _: Exception => Resume } def receive: PartialFunction[Any, Unit] = { - case Peers(p) => p.foreach(alivePeers.add) + case Peers(p) => (p -- deadPeers).foreach(alivePeers.add) case Discover => alivePeers.next().foreach(peer => router.route(PeerDiscoveryActor.GetPeersFrom(peer), self)) case PeerInfo(peer, peers) => { self ! Peers(peers) - + deadPeers.remove(peer) peerResponses.put(peer, peers) match { case Some(oldValue) if oldValue == peers => //nothing changed - case _ if peers.nonEmpty => broadcastPeerInfo(peer, peers) + case _ if (peers -- deadPeers).nonEmpty => broadcastPeerInfo(peer, peers -- deadPeers) case _ => } } case PeerProblem(peer) => { + println("PeerProblem") alivePeers.remove(peer) deadPeers.add(peer) } @@ -83,10 +57,10 @@ class MainActor extends Actor { } } - private def jsonPeersData = peerResponses.foldLeft(Json.obj())((json, keyValue) => json.+(keyValue._1.toString, JsArray(keyValue._2.map(v => JsString(v.toString)).toSeq))).toString() + private def jsonPeersData = peerResponses.foldLeft(Json.obj())((json, keyValue) => json.+(keyValue._1.getHostString, JsArray(keyValue._2.map(v => JsString(v.getHostString)).toSeq))).toString() private def broadcastPeerInfo(peer: InetSocketAddress, peers: Set[InetSocketAddress]): Unit = { - val response = Json.obj(peer.toString -> JsArray(peers.map(p => JsString(p.toString)).toSeq)).toString() + val response = Json.obj(peer.getHostString -> JsArray(peers.map(p => JsString(p.getHostString)).toSeq)).toString() connections.foreach(c => c ! response) } } @@ -103,4 +77,7 @@ object MainActor { case object Discover + def apply(workersCount: Int)(implicit system: ActorSystem): ActorRef = { + system.actorOf(Props(classOf[MainActor], workersCount), "main") + } } \ No newline at end of file diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala index 004d659b0fb..342482c5a0d 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala @@ -4,25 +4,58 @@ import java.net.InetSocketAddress import java.util.concurrent.TimeUnit import akka.actor.Actor +import com.wavesplatform.discovery._ +import com.wavesplatform.discovery.network._ +import com.wavesplatform.network.{GetPeers, Handshake, KnownPeers, PipelineInitializer} +import io.netty.bootstrap.Bootstrap +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.{LengthFieldBasedFrameDecoder, LengthFieldPrepender} import scala.concurrent.Await -import scala.concurrent.duration._ -import com.wavesplatform.discovery._ +import scala.concurrent.duration.FiniteDuration object PeerDiscoveryActor { case class GetPeersFrom(peer: InetSocketAddress) } class PeerDiscoveryActor extends Actor { + import PeerDiscoveryActor._ + def receive: PartialFunction[Any, Unit] = { - case GetPeersFrom(peer) => context.parent ! MainActor.PeerInfo(peer, Await.result(getPeersFromNode(peer), FiniteDuration(1, TimeUnit.MINUTES))) + case GetPeersFrom(peer) => context.parent ! MainActor.PeerInfo(peer, getPeersFromNode(peer)) } - override def preRestart(reason: Throwable, message: Option[Any]): Unit = { - message match { - case Some(GetPeersFrom(peer)) => context.parent ! MainActor.PeerInfo(peer, Set.empty) - case _ => - } + private def getPeersFromNode(address: InetSocketAddress): Set[InetSocketAddress]= { + var peers: Set[InetSocketAddress] = Set.empty + + val exceptionHandler = new ExceptionHandler() + + new Bootstrap() + .group(workerGroup) + .channel(classOf[NioSocketChannel]) + .handler(new PipelineInitializer[SocketChannel](Seq( + exceptionHandler, + new HandshakeHandler(), + new LengthFieldPrepender(4), + new LengthFieldBasedFrameDecoder(100 * 1024 * 1024, 0, 4, 0, 4), + new LegacyFrameCodec(), + new MessageCodec(), + new MessageHandler({ case (msg, ctx) => + msg match { + case hs: Handshake => ctx.writeAndFlush(GetPeers) + case KnownPeers(p) => peers = p.toSet; ctx.close() + case _ => + } + }) + ))) + .remoteAddress(address.getAddress, address.getPort) + .connect() + + Await.result(exceptionHandler.closed, new FiniteDuration(10, TimeUnit.SECONDS)) + + peers } + } \ No newline at end of file diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala b/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala new file mode 100644 index 00000000000..4af8ffba9b7 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala @@ -0,0 +1,26 @@ +package com.wavesplatform.discovery.collections + +class ExpirationSet[T](expirationTimeMilis: Long) extends scala.collection.mutable.Set[T]{ + private var inner = Map.empty[T, Long] + private def freshInner = { + val time = System.currentTimeMillis() + inner = inner.filter({ case (k, v) => + time - v > expirationTimeMilis }) + + inner + } + + override def +=(elem: T): ExpirationSet.this.type = { + inner += ((elem, System.currentTimeMillis())) + this + } + + override def -=(elem: T): ExpirationSet.this.type = { + inner = inner.-(elem) + this + } + + override def contains(elem: T): Boolean = freshInner.contains(elem) + + override def iterator: Iterator[T] = freshInner.keys.iterator +} diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/network/ExceptionHandler.scala b/discovery/src/main/scala/com.wavesplatform.discovery/network/ExceptionHandler.scala new file mode 100644 index 00000000000..00a2bff5877 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/network/ExceptionHandler.scala @@ -0,0 +1,39 @@ +package com.wavesplatform.discovery.network + +import java.net.SocketAddress + +import io.netty.channel.{ChannelDuplexHandler, ChannelFuture, ChannelHandlerContext, ChannelPromise} + +import scala.concurrent.{Future, Promise} + +class ExceptionHandler extends ChannelDuplexHandler { + + private val p = Promise[Boolean]() + val closed: Future[Boolean] = p.future + + override def close(ctx: ChannelHandlerContext, promise: ChannelPromise): Unit = { + super.close(ctx, promise) + if (!p.isCompleted) + p.success(true) + } + + override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = { + p.failure(cause) + } + + override def connect(ctx: ChannelHandlerContext, remoteAddress: SocketAddress, localAddress: SocketAddress, promise: ChannelPromise): Unit = { + ctx.connect(remoteAddress, localAddress, promise.addListener((future: ChannelFuture) => { + if (!future.isSuccess) { + p.failure(future.cause()) + } + })) + } + + override def write(ctx: ChannelHandlerContext, msg: Any, promise: ChannelPromise): Unit = { + ctx.write(msg, promise.addListener((future: ChannelFuture) => { + if (!future.isSuccess) { + p.failure(future.cause()) + } + })) + } +} diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/package.scala b/discovery/src/main/scala/com.wavesplatform.discovery/package.scala index cae18c53fd8..7dbcacb478f 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/package.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/package.scala @@ -1,46 +1,12 @@ package com.wavesplatform -import java.net.InetSocketAddress - -import com.wavesplatform.discovery.network.{HandshakeHandler, LegacyFrameCodec, MessageCodec, MessageHandler} -import com.wavesplatform.network.{GetPeers, Handshake, KnownPeers, PipelineInitializer} -import io.netty.bootstrap.Bootstrap -import io.netty.channel.EventLoopGroup import io.netty.channel.nio.NioEventLoopGroup -import io.netty.channel.socket.SocketChannel -import io.netty.channel.socket.nio.NioSocketChannel -import io.netty.handler.codec.{LengthFieldBasedFrameDecoder, LengthFieldPrepender} -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.ExecutionContext package object discovery { implicit val workerGroup = new NioEventLoopGroup implicit val ec: ExecutionContext = ExecutionContext.global - def getPeersFromNode(address: InetSocketAddress)(implicit eventLoopGroup: EventLoopGroup): scala.concurrent.Future[Set[InetSocketAddress]] = Future { - var peers: Set[InetSocketAddress] = Set.empty - - new Bootstrap() - .group(workerGroup) - .channel(classOf[NioSocketChannel]) - .handler(new PipelineInitializer[SocketChannel](Seq( - new HandshakeHandler(), - new LengthFieldPrepender(4), - new LengthFieldBasedFrameDecoder(100 * 1024 * 1024, 0, 4, 0, 4), - new LegacyFrameCodec(), - new MessageCodec(), - new MessageHandler({ case (msg, ctx) => - msg match { - case hs: Handshake => ctx.writeAndFlush(GetPeers) - case KnownPeers(p) => peers = p.toSet; ctx.close() - case _ => - } - }) - ))) - .remoteAddress(address.getAddress, address.getPort) - .connect().channel().closeFuture().sync() - - peers - } } diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/routers/SmallestMailboxWithThresholdRoutingLogic.scala b/discovery/src/main/scala/com.wavesplatform.discovery/routers/SmallestMailboxWithThresholdRoutingLogic.scala new file mode 100644 index 00000000000..6875ed182c2 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/routers/SmallestMailboxWithThresholdRoutingLogic.scala @@ -0,0 +1,18 @@ +package com.wavesplatform.discovery.routers + +import akka.actor.ActorRef +import akka.routing.{Routee, SmallestMailboxRoutingLogic} + +import scala.collection.immutable + +case class SmallestMailboxWithThresholdRoutingLogic(mailboxThreshold: Int) extends SmallestMailboxRoutingLogic { + override def select(message: Any, routees: immutable.IndexedSeq[Routee]): Routee = { + + if (routees.exists(numberOfMessages(_) < mailboxThreshold)) { + super.select(message, routees) + } + else { + (_: Any, _: ActorRef) => () + } + } +} diff --git a/discovery/src/main/web/app.css b/discovery/src/main/web/app.css new file mode 100644 index 00000000000..8ef0ce08e8b --- /dev/null +++ b/discovery/src/main/web/app.css @@ -0,0 +1,56 @@ +svg { + background-color: #FFF; + cursor: default; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +svg:not(.active):not(.ctrl) { + cursor: crosshair; +} + +path.link { + fill: none; + stroke: #000; + stroke-width: 2px; + cursor: default; +} + +svg:not(.active):not(.ctrl) path.link { + cursor: pointer; +} + +path.link.selected { + stroke-dasharray: 10,2; +} + +path.link.dragline { + pointer-events: none; +} + +path.link.hidden { + stroke-width: 0; +} + +circle.node { + stroke-width: 1.5px; + cursor: pointer; +} + +circle.node.reflexive { + stroke: #000 !important; + stroke-width: 2.5px; +} + +text { + font: 12px sans-serif; + pointer-events: none; +} + +text.id { + text-anchor: middle; + font-weight: bold; +} \ No newline at end of file diff --git a/discovery/src/main/web/data.json b/discovery/src/main/web/data.json new file mode 100644 index 00000000000..693327f00d5 --- /dev/null +++ b/discovery/src/main/web/data.json @@ -0,0 +1,337 @@ +{ + "nodes": [ + {"id": "Myriel", "group": 1}, + {"id": "Napoleon", "group": 1}, + {"id": "Mlle.Baptistine", "group": 1}, + {"id": "Mme.Magloire", "group": 1}, + {"id": "CountessdeLo", "group": 1}, + {"id": "Geborand", "group": 1}, + {"id": "Champtercier", "group": 1}, + {"id": "Cravatte", "group": 1}, + {"id": "Count", "group": 1}, + {"id": "OldMan", "group": 1}, + {"id": "Labarre", "group": 2}, + {"id": "Valjean", "group": 2}, + {"id": "Marguerite", "group": 3}, + {"id": "Mme.deR", "group": 2}, + {"id": "Isabeau", "group": 2}, + {"id": "Gervais", "group": 2}, + {"id": "Tholomyes", "group": 3}, + {"id": "Listolier", "group": 3}, + {"id": "Fameuil", "group": 3}, + {"id": "Blacheville", "group": 3}, + {"id": "Favourite", "group": 3}, + {"id": "Dahlia", "group": 3}, + {"id": "Zephine", "group": 3}, + {"id": "Fantine", "group": 3}, + {"id": "Mme.Thenardier", "group": 4}, + {"id": "Thenardier", "group": 4}, + {"id": "Cosette", "group": 5}, + {"id": "Javert", "group": 4}, + {"id": "Fauchelevent", "group": 0}, + {"id": "Bamatabois", "group": 2}, + {"id": "Perpetue", "group": 3}, + {"id": "Simplice", "group": 2}, + {"id": "Scaufflaire", "group": 2}, + {"id": "Woman1", "group": 2}, + {"id": "Judge", "group": 2}, + {"id": "Champmathieu", "group": 2}, + {"id": "Brevet", "group": 2}, + {"id": "Chenildieu", "group": 2}, + {"id": "Cochepaille", "group": 2}, + {"id": "Pontmercy", "group": 4}, + {"id": "Boulatruelle", "group": 6}, + {"id": "Eponine", "group": 4}, + {"id": "Anzelma", "group": 4}, + {"id": "Woman2", "group": 5}, + {"id": "MotherInnocent", "group": 0}, + {"id": "Gribier", "group": 0}, + {"id": "Jondrette", "group": 7}, + {"id": "Mme.Burgon", "group": 7}, + {"id": "Gavroche", "group": 8}, + {"id": "Gillenormand", "group": 5}, + {"id": "Magnon", "group": 5}, + {"id": "Mlle.Gillenormand", "group": 5}, + {"id": "Mme.Pontmercy", "group": 5}, + {"id": "Mlle.Vaubois", "group": 5}, + {"id": "Lt.Gillenormand", "group": 5}, + {"id": "Marius", "group": 8}, + {"id": "BaronessT", "group": 5}, + {"id": "Mabeuf", "group": 8}, + {"id": "Enjolras", "group": 8}, + {"id": "Combeferre", "group": 8}, + {"id": "Prouvaire", "group": 8}, + {"id": "Feuilly", "group": 8}, + {"id": "Courfeyrac", "group": 8}, + {"id": "Bahorel", "group": 8}, + {"id": "Bossuet", "group": 8}, + {"id": "Joly", "group": 8}, + {"id": "Grantaire", "group": 8}, + {"id": "MotherPlutarch", "group": 9}, + {"id": "Gueulemer", "group": 4}, + {"id": "Babet", "group": 4}, + {"id": "Claquesous", "group": 4}, + {"id": "Montparnasse", "group": 4}, + {"id": "Toussaint", "group": 5}, + {"id": "Child1", "group": 10}, + {"id": "Child2", "group": 10}, + {"id": "Brujon", "group": 4}, + {"id": "Mme.Hucheloup", "group": 8} + ], + "links": [ + {"source": "Napoleon", "target": "Myriel", "value": 1}, + {"source": "Mlle.Baptistine", "target": "Myriel", "value": 8}, + {"source": "Mme.Magloire", "target": "Myriel", "value": 10}, + {"source": "Mme.Magloire", "target": "Mlle.Baptistine", "value": 6}, + {"source": "CountessdeLo", "target": "Myriel", "value": 1}, + {"source": "Geborand", "target": "Myriel", "value": 1}, + {"source": "Champtercier", "target": "Myriel", "value": 1}, + {"source": "Cravatte", "target": "Myriel", "value": 1}, + {"source": "Count", "target": "Myriel", "value": 2}, + {"source": "OldMan", "target": "Myriel", "value": 1}, + {"source": "Valjean", "target": "Labarre", "value": 1}, + {"source": "Valjean", "target": "Mme.Magloire", "value": 3}, + {"source": "Valjean", "target": "Mlle.Baptistine", "value": 3}, + {"source": "Valjean", "target": "Myriel", "value": 5}, + {"source": "Marguerite", "target": "Valjean", "value": 1}, + {"source": "Mme.deR", "target": "Valjean", "value": 1}, + {"source": "Isabeau", "target": "Valjean", "value": 1}, + {"source": "Gervais", "target": "Valjean", "value": 1}, + {"source": "Listolier", "target": "Tholomyes", "value": 4}, + {"source": "Fameuil", "target": "Tholomyes", "value": 4}, + {"source": "Fameuil", "target": "Listolier", "value": 4}, + {"source": "Blacheville", "target": "Tholomyes", "value": 4}, + {"source": "Blacheville", "target": "Listolier", "value": 4}, + {"source": "Blacheville", "target": "Fameuil", "value": 4}, + {"source": "Favourite", "target": "Tholomyes", "value": 3}, + {"source": "Favourite", "target": "Listolier", "value": 3}, + {"source": "Favourite", "target": "Fameuil", "value": 3}, + {"source": "Favourite", "target": "Blacheville", "value": 4}, + {"source": "Dahlia", "target": "Tholomyes", "value": 3}, + {"source": "Dahlia", "target": "Listolier", "value": 3}, + {"source": "Dahlia", "target": "Fameuil", "value": 3}, + {"source": "Dahlia", "target": "Blacheville", "value": 3}, + {"source": "Dahlia", "target": "Favourite", "value": 5}, + {"source": "Zephine", "target": "Tholomyes", "value": 3}, + {"source": "Zephine", "target": "Listolier", "value": 3}, + {"source": "Zephine", "target": "Fameuil", "value": 3}, + {"source": "Zephine", "target": "Blacheville", "value": 3}, + {"source": "Zephine", "target": "Favourite", "value": 4}, + {"source": "Zephine", "target": "Dahlia", "value": 4}, + {"source": "Fantine", "target": "Tholomyes", "value": 3}, + {"source": "Fantine", "target": "Listolier", "value": 3}, + {"source": "Fantine", "target": "Fameuil", "value": 3}, + {"source": "Fantine", "target": "Blacheville", "value": 3}, + {"source": "Fantine", "target": "Favourite", "value": 4}, + {"source": "Fantine", "target": "Dahlia", "value": 4}, + {"source": "Fantine", "target": "Zephine", "value": 4}, + {"source": "Fantine", "target": "Marguerite", "value": 2}, + {"source": "Fantine", "target": "Valjean", "value": 9}, + {"source": "Mme.Thenardier", "target": "Fantine", "value": 2}, + {"source": "Mme.Thenardier", "target": "Valjean", "value": 7}, + {"source": "Thenardier", "target": "Mme.Thenardier", "value": 13}, + {"source": "Thenardier", "target": "Fantine", "value": 1}, + {"source": "Thenardier", "target": "Valjean", "value": 12}, + {"source": "Cosette", "target": "Mme.Thenardier", "value": 4}, + {"source": "Cosette", "target": "Valjean", "value": 31}, + {"source": "Cosette", "target": "Tholomyes", "value": 1}, + {"source": "Cosette", "target": "Thenardier", "value": 1}, + {"source": "Javert", "target": "Valjean", "value": 17}, + {"source": "Javert", "target": "Fantine", "value": 5}, + {"source": "Javert", "target": "Thenardier", "value": 5}, + {"source": "Javert", "target": "Mme.Thenardier", "value": 1}, + {"source": "Javert", "target": "Cosette", "value": 1}, + {"source": "Fauchelevent", "target": "Valjean", "value": 8}, + {"source": "Fauchelevent", "target": "Javert", "value": 1}, + {"source": "Bamatabois", "target": "Fantine", "value": 1}, + {"source": "Bamatabois", "target": "Javert", "value": 1}, + {"source": "Bamatabois", "target": "Valjean", "value": 2}, + {"source": "Perpetue", "target": "Fantine", "value": 1}, + {"source": "Simplice", "target": "Perpetue", "value": 2}, + {"source": "Simplice", "target": "Valjean", "value": 3}, + {"source": "Simplice", "target": "Fantine", "value": 2}, + {"source": "Simplice", "target": "Javert", "value": 1}, + {"source": "Scaufflaire", "target": "Valjean", "value": 1}, + {"source": "Woman1", "target": "Valjean", "value": 2}, + {"source": "Woman1", "target": "Javert", "value": 1}, + {"source": "Judge", "target": "Valjean", "value": 3}, + {"source": "Judge", "target": "Bamatabois", "value": 2}, + {"source": "Champmathieu", "target": "Valjean", "value": 3}, + {"source": "Champmathieu", "target": "Judge", "value": 3}, + {"source": "Champmathieu", "target": "Bamatabois", "value": 2}, + {"source": "Brevet", "target": "Judge", "value": 2}, + {"source": "Brevet", "target": "Champmathieu", "value": 2}, + {"source": "Brevet", "target": "Valjean", "value": 2}, + {"source": "Brevet", "target": "Bamatabois", "value": 1}, + {"source": "Chenildieu", "target": "Judge", "value": 2}, + {"source": "Chenildieu", "target": "Champmathieu", "value": 2}, + {"source": "Chenildieu", "target": "Brevet", "value": 2}, + {"source": "Chenildieu", "target": "Valjean", "value": 2}, + {"source": "Chenildieu", "target": "Bamatabois", "value": 1}, + {"source": "Cochepaille", "target": "Judge", "value": 2}, + {"source": "Cochepaille", "target": "Champmathieu", "value": 2}, + {"source": "Cochepaille", "target": "Brevet", "value": 2}, + {"source": "Cochepaille", "target": "Chenildieu", "value": 2}, + {"source": "Cochepaille", "target": "Valjean", "value": 2}, + {"source": "Cochepaille", "target": "Bamatabois", "value": 1}, + {"source": "Pontmercy", "target": "Thenardier", "value": 1}, + {"source": "Boulatruelle", "target": "Thenardier", "value": 1}, + {"source": "Eponine", "target": "Mme.Thenardier", "value": 2}, + {"source": "Eponine", "target": "Thenardier", "value": 3}, + {"source": "Anzelma", "target": "Eponine", "value": 2}, + {"source": "Anzelma", "target": "Thenardier", "value": 2}, + {"source": "Anzelma", "target": "Mme.Thenardier", "value": 1}, + {"source": "Woman2", "target": "Valjean", "value": 3}, + {"source": "Woman2", "target": "Cosette", "value": 1}, + {"source": "Woman2", "target": "Javert", "value": 1}, + {"source": "MotherInnocent", "target": "Fauchelevent", "value": 3}, + {"source": "MotherInnocent", "target": "Valjean", "value": 1}, + {"source": "Gribier", "target": "Fauchelevent", "value": 2}, + {"source": "Mme.Burgon", "target": "Jondrette", "value": 1}, + {"source": "Gavroche", "target": "Mme.Burgon", "value": 2}, + {"source": "Gavroche", "target": "Thenardier", "value": 1}, + {"source": "Gavroche", "target": "Javert", "value": 1}, + {"source": "Gavroche", "target": "Valjean", "value": 1}, + {"source": "Gillenormand", "target": "Cosette", "value": 3}, + {"source": "Gillenormand", "target": "Valjean", "value": 2}, + {"source": "Magnon", "target": "Gillenormand", "value": 1}, + {"source": "Magnon", "target": "Mme.Thenardier", "value": 1}, + {"source": "Mlle.Gillenormand", "target": "Gillenormand", "value": 9}, + {"source": "Mlle.Gillenormand", "target": "Cosette", "value": 2}, + {"source": "Mlle.Gillenormand", "target": "Valjean", "value": 2}, + {"source": "Mme.Pontmercy", "target": "Mlle.Gillenormand", "value": 1}, + {"source": "Mme.Pontmercy", "target": "Pontmercy", "value": 1}, + {"source": "Mlle.Vaubois", "target": "Mlle.Gillenormand", "value": 1}, + {"source": "Lt.Gillenormand", "target": "Mlle.Gillenormand", "value": 2}, + {"source": "Lt.Gillenormand", "target": "Gillenormand", "value": 1}, + {"source": "Lt.Gillenormand", "target": "Cosette", "value": 1}, + {"source": "Marius", "target": "Mlle.Gillenormand", "value": 6}, + {"source": "Marius", "target": "Gillenormand", "value": 12}, + {"source": "Marius", "target": "Pontmercy", "value": 1}, + {"source": "Marius", "target": "Lt.Gillenormand", "value": 1}, + {"source": "Marius", "target": "Cosette", "value": 21}, + {"source": "Marius", "target": "Valjean", "value": 19}, + {"source": "Marius", "target": "Tholomyes", "value": 1}, + {"source": "Marius", "target": "Thenardier", "value": 2}, + {"source": "Marius", "target": "Eponine", "value": 5}, + {"source": "Marius", "target": "Gavroche", "value": 4}, + {"source": "BaronessT", "target": "Gillenormand", "value": 1}, + {"source": "BaronessT", "target": "Marius", "value": 1}, + {"source": "Mabeuf", "target": "Marius", "value": 1}, + {"source": "Mabeuf", "target": "Eponine", "value": 1}, + {"source": "Mabeuf", "target": "Gavroche", "value": 1}, + {"source": "Enjolras", "target": "Marius", "value": 7}, + {"source": "Enjolras", "target": "Gavroche", "value": 7}, + {"source": "Enjolras", "target": "Javert", "value": 6}, + {"source": "Enjolras", "target": "Mabeuf", "value": 1}, + {"source": "Enjolras", "target": "Valjean", "value": 4}, + {"source": "Combeferre", "target": "Enjolras", "value": 15}, + {"source": "Combeferre", "target": "Marius", "value": 5}, + {"source": "Combeferre", "target": "Gavroche", "value": 6}, + {"source": "Combeferre", "target": "Mabeuf", "value": 2}, + {"source": "Prouvaire", "target": "Gavroche", "value": 1}, + {"source": "Prouvaire", "target": "Enjolras", "value": 4}, + {"source": "Prouvaire", "target": "Combeferre", "value": 2}, + {"source": "Feuilly", "target": "Gavroche", "value": 2}, + {"source": "Feuilly", "target": "Enjolras", "value": 6}, + {"source": "Feuilly", "target": "Prouvaire", "value": 2}, + {"source": "Feuilly", "target": "Combeferre", "value": 5}, + {"source": "Feuilly", "target": "Mabeuf", "value": 1}, + {"source": "Feuilly", "target": "Marius", "value": 1}, + {"source": "Courfeyrac", "target": "Marius", "value": 9}, + {"source": "Courfeyrac", "target": "Enjolras", "value": 17}, + {"source": "Courfeyrac", "target": "Combeferre", "value": 13}, + {"source": "Courfeyrac", "target": "Gavroche", "value": 7}, + {"source": "Courfeyrac", "target": "Mabeuf", "value": 2}, + {"source": "Courfeyrac", "target": "Eponine", "value": 1}, + {"source": "Courfeyrac", "target": "Feuilly", "value": 6}, + {"source": "Courfeyrac", "target": "Prouvaire", "value": 3}, + {"source": "Bahorel", "target": "Combeferre", "value": 5}, + {"source": "Bahorel", "target": "Gavroche", "value": 5}, + {"source": "Bahorel", "target": "Courfeyrac", "value": 6}, + {"source": "Bahorel", "target": "Mabeuf", "value": 2}, + {"source": "Bahorel", "target": "Enjolras", "value": 4}, + {"source": "Bahorel", "target": "Feuilly", "value": 3}, + {"source": "Bahorel", "target": "Prouvaire", "value": 2}, + {"source": "Bahorel", "target": "Marius", "value": 1}, + {"source": "Bossuet", "target": "Marius", "value": 5}, + {"source": "Bossuet", "target": "Courfeyrac", "value": 12}, + {"source": "Bossuet", "target": "Gavroche", "value": 5}, + {"source": "Bossuet", "target": "Bahorel", "value": 4}, + {"source": "Bossuet", "target": "Enjolras", "value": 10}, + {"source": "Bossuet", "target": "Feuilly", "value": 6}, + {"source": "Bossuet", "target": "Prouvaire", "value": 2}, + {"source": "Bossuet", "target": "Combeferre", "value": 9}, + {"source": "Bossuet", "target": "Mabeuf", "value": 1}, + {"source": "Bossuet", "target": "Valjean", "value": 1}, + {"source": "Joly", "target": "Bahorel", "value": 5}, + {"source": "Joly", "target": "Bossuet", "value": 7}, + {"source": "Joly", "target": "Gavroche", "value": 3}, + {"source": "Joly", "target": "Courfeyrac", "value": 5}, + {"source": "Joly", "target": "Enjolras", "value": 5}, + {"source": "Joly", "target": "Feuilly", "value": 5}, + {"source": "Joly", "target": "Prouvaire", "value": 2}, + {"source": "Joly", "target": "Combeferre", "value": 5}, + {"source": "Joly", "target": "Mabeuf", "value": 1}, + {"source": "Joly", "target": "Marius", "value": 2}, + {"source": "Grantaire", "target": "Bossuet", "value": 3}, + {"source": "Grantaire", "target": "Enjolras", "value": 3}, + {"source": "Grantaire", "target": "Combeferre", "value": 1}, + {"source": "Grantaire", "target": "Courfeyrac", "value": 2}, + {"source": "Grantaire", "target": "Joly", "value": 2}, + {"source": "Grantaire", "target": "Gavroche", "value": 1}, + {"source": "Grantaire", "target": "Bahorel", "value": 1}, + {"source": "Grantaire", "target": "Feuilly", "value": 1}, + {"source": "Grantaire", "target": "Prouvaire", "value": 1}, + {"source": "MotherPlutarch", "target": "Mabeuf", "value": 3}, + {"source": "Gueulemer", "target": "Thenardier", "value": 5}, + {"source": "Gueulemer", "target": "Valjean", "value": 1}, + {"source": "Gueulemer", "target": "Mme.Thenardier", "value": 1}, + {"source": "Gueulemer", "target": "Javert", "value": 1}, + {"source": "Gueulemer", "target": "Gavroche", "value": 1}, + {"source": "Gueulemer", "target": "Eponine", "value": 1}, + {"source": "Babet", "target": "Thenardier", "value": 6}, + {"source": "Babet", "target": "Gueulemer", "value": 6}, + {"source": "Babet", "target": "Valjean", "value": 1}, + {"source": "Babet", "target": "Mme.Thenardier", "value": 1}, + {"source": "Babet", "target": "Javert", "value": 2}, + {"source": "Babet", "target": "Gavroche", "value": 1}, + {"source": "Babet", "target": "Eponine", "value": 1}, + {"source": "Claquesous", "target": "Thenardier", "value": 4}, + {"source": "Claquesous", "target": "Babet", "value": 4}, + {"source": "Claquesous", "target": "Gueulemer", "value": 4}, + {"source": "Claquesous", "target": "Valjean", "value": 1}, + {"source": "Claquesous", "target": "Mme.Thenardier", "value": 1}, + {"source": "Claquesous", "target": "Javert", "value": 1}, + {"source": "Claquesous", "target": "Eponine", "value": 1}, + {"source": "Claquesous", "target": "Enjolras", "value": 1}, + {"source": "Montparnasse", "target": "Javert", "value": 1}, + {"source": "Montparnasse", "target": "Babet", "value": 2}, + {"source": "Montparnasse", "target": "Gueulemer", "value": 2}, + {"source": "Montparnasse", "target": "Claquesous", "value": 2}, + {"source": "Montparnasse", "target": "Valjean", "value": 1}, + {"source": "Montparnasse", "target": "Gavroche", "value": 1}, + {"source": "Montparnasse", "target": "Eponine", "value": 1}, + {"source": "Montparnasse", "target": "Thenardier", "value": 1}, + {"source": "Toussaint", "target": "Cosette", "value": 2}, + {"source": "Toussaint", "target": "Javert", "value": 1}, + {"source": "Toussaint", "target": "Valjean", "value": 1}, + {"source": "Child1", "target": "Gavroche", "value": 2}, + {"source": "Child2", "target": "Gavroche", "value": 2}, + {"source": "Child2", "target": "Child1", "value": 3}, + {"source": "Brujon", "target": "Babet", "value": 3}, + {"source": "Brujon", "target": "Gueulemer", "value": 3}, + {"source": "Brujon", "target": "Thenardier", "value": 3}, + {"source": "Brujon", "target": "Gavroche", "value": 1}, + {"source": "Brujon", "target": "Eponine", "value": 1}, + {"source": "Brujon", "target": "Claquesous", "value": 1}, + {"source": "Brujon", "target": "Montparnasse", "value": 1}, + {"source": "Mme.Hucheloup", "target": "Bossuet", "value": 1}, + {"source": "Mme.Hucheloup", "target": "Joly", "value": 1}, + {"source": "Mme.Hucheloup", "target": "Grantaire", "value": 1}, + {"source": "Mme.Hucheloup", "target": "Bahorel", "value": 1}, + {"source": "Mme.Hucheloup", "target": "Courfeyrac", "value": 1}, + {"source": "Mme.Hucheloup", "target": "Gavroche", "value": 1}, + {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1} + ] +} \ No newline at end of file diff --git a/discovery/src/main/web/index.html b/discovery/src/main/web/index.html new file mode 100644 index 00000000000..3b6fab66480 --- /dev/null +++ b/discovery/src/main/web/index.html @@ -0,0 +1,13 @@ + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/discovery/src/main/web/index.js b/discovery/src/main/web/index.js new file mode 100644 index 00000000000..553bc39bd3f --- /dev/null +++ b/discovery/src/main/web/index.js @@ -0,0 +1,57 @@ + function print(text) { + var p = document.createElement("P") + p.appendChild(document.createTextNode(text)) + document.getElementById("root").appendChild(p) + //window.scrollTo(0,document.body.scrollHeight); + } + + function WebSocketTest() + { + if ("WebSocket" in window) + { + var ws = new WebSocket("ws://localhost:8080/"); + var wasConnected = false + + ws.onopen = function() + { + wasConnected = true + print("WebSocket connected...") + }; + + ws.onmessage = function (evt) + { + var received_msg = evt.data + var msg = JSON.parse(evt.data); + + for(var peer in msg) { + addPeerWithPeers(peer, msg[peer]) + } + + restart() + + print(received_msg) + }; + + ws.onclose = function() + { + if(!wasConnected) { + WebSocketTest() + } + else { + print("WebSocket closed.") + } + }; + + window.onbeforeunload = function(event) { + socket.close() + }; + } + + else + { + print("WebSocket NOT supported by your Browser!"); + } + } + + print("Waiting for host...") + WebSocketTest(); diff --git a/discovery/src/main/web/test.js b/discovery/src/main/web/test.js new file mode 100644 index 00000000000..855f1965ece --- /dev/null +++ b/discovery/src/main/web/test.js @@ -0,0 +1,315 @@ +var width = 960, + height = 500, + colors = d3.scale.category10(); +var svg = d3.select('svg').attr('oncontextmenu', 'return false;').attr('width', width).attr('height', height); +var nodes = [], + lastNodeId = 0, + links = []; + +var nodesSet = {} +var linksSet = {} + +function addPeerWithPeers(peer, peers) { + addPeer(peer) + peers.forEach(function(p) { + addPeer(p) + addLink(peer,p) + }); +} + +function addPeer(peer) { + if(nodesSet[peer]) + return + + node = { + id: peer, + reflexive: false + }; + + nodesSet[peer] = node + nodes.push(node); +} + +function addLink(from, to) { + + if(linksSet[from] && linksSet[from][to]) + return + + if(!linksSet[from]) + linksSet[from] = {} + + if(linksSet[to] && linksSet[to][from]) { + linksSet[to][from].left = true + } + else if(linksSet[from][to]) { + } + else { + link = { + source: nodesSet[from], + target: nodesSet[to], + left: false, + right: true + }; + links.push(link); + linksSet[from][to] = link + } +} + + +var force = d3.layout.force().nodes(nodes).links(links).size([width, height]).linkDistance(300).charge(1).on('tick', tick) +svg.append('svg:defs').append('svg:marker').attr('id', 'end-arrow').attr('viewBox', '0 -5 10 10').attr('refX', 6).attr('markerWidth', 3).attr('markerHeight', 3).attr('orient', 'auto').append('svg:path').attr('d', 'M0,-5L10,0L0,5').attr('fill', '#000'); +svg.append('svg:defs').append('svg:marker').attr('id', 'start-arrow').attr('viewBox', '0 -5 10 10').attr('refX', 4).attr('markerWidth', 3).attr('markerHeight', 3).attr('orient', 'auto').append('svg:path').attr('d', 'M10,-5L0,0L10,5').attr('fill', '#000'); +var drag_line = svg.append('svg:path').attr('class', 'link dragline hidden').attr('d', 'M0,0L0,0'); +var path = svg.append('svg:g').selectAll('path'), + circle = svg.append('svg:g').selectAll('g'); +var selected_node = null, + selected_link = null, + mousedown_link = null, + mousedown_node = null, + mouseup_node = null; + +function resetMouseVars() { + mousedown_node = null; + mouseup_node = null; + mousedown_link = null; +} + +function tick() { + path.attr('d', function(d) { + var deltaX = d.target.x - d.source.x, + deltaY = d.target.y - d.source.y, + dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY), + normX = deltaX / dist, + normY = deltaY / dist, + sourcePadding = d.left ? 17 : 12, + targetPadding = d.right ? 17 : 12, + sourceX = d.source.x + (sourcePadding * normX), + sourceY = d.source.y + (sourcePadding * normY), + targetX = d.target.x - (targetPadding * normX), + targetY = d.target.y - (targetPadding * normY); + return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY; + }); + circle.attr('transform', function(d) { + return 'translate(' + d.x + ',' + d.y + ')'; + }); +} + +function restart() { + path = path.data(links); + path.classed('selected', function(d) { + return d === selected_link; + }).style('marker-start', function(d) { + return d.left ? 'url(#start-arrow)' : ''; + }).style('marker-end', function(d) { + return d.right ? 'url(#end-arrow)' : ''; + }); + path.enter().append('svg:path').attr('class', 'link').classed('selected', function(d) { + return d === selected_link; + }).style('marker-start', function(d) { + return d.left ? 'url(#start-arrow)' : ''; + }).style('marker-end', function(d) { + return d.right ? 'url(#end-arrow)' : ''; + }).on('mousedown', function(d) { + if (d3.event.ctrlKey) return; + mousedown_link = d; + if (mousedown_link === selected_link) selected_link = null; + else selected_link = mousedown_link; + selected_node = null; + restart(); + }); + path.exit().remove(); + circle = circle.data(nodes, function(d) { + return d.id; + }); + circle.selectAll('circle').style('fill', function(d) { + return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); + }).classed('reflexive', function(d) { + return d.reflexive; + }); + var g = circle.enter().append('svg:g'); + g.append('svg:circle').attr('class', 'node').attr('r', 12).style('fill', function(d) { + return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); + }).style('stroke', function(d) { + return d3.rgb(colors(d.id)).darker().toString(); + }).classed('reflexive', function(d) { + return d.reflexive; + }).on('mouseover', function(d) { + if (!mousedown_node || d === mousedown_node) return; + d3.select(this).attr('transform', 'scale(1.1)'); + }).on('mouseout', function(d) { + if (!mousedown_node || d === mousedown_node) return; + d3.select(this).attr('transform', ''); + }).on('mousedown', function(d) { + if (d3.event.ctrlKey) return; + mousedown_node = d; + if (mousedown_node === selected_node) selected_node = null; + else selected_node = mousedown_node; + selected_link = null; + drag_line.style('marker-end', 'url(#end-arrow)').classed('hidden', false).attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + mousedown_node.x + ',' + mousedown_node.y); + restart(); + }).on('mouseup', function(d) { + if (!mousedown_node) return; + drag_line.classed('hidden', true).style('marker-end', ''); + mouseup_node = d; + if (mouseup_node === mousedown_node) { + resetMouseVars(); + return; + } + d3.select(this).attr('transform', ''); + var source, target, direction; + if (mousedown_node.id < mouseup_node.id) { + source = mousedown_node; + target = mouseup_node; + direction = 'right'; + } else { + source = mouseup_node; + target = mousedown_node; + direction = 'left'; + } + var link; + link = links.filter(function(l) { + return (l.source === source && l.target === target); + })[0]; + if (link) { + link[direction] = true; + } else { + link = { + source: source, + target: target, + left: false, + right: false + }; + link[direction] = true; + links.push(link); + } + selected_link = link; + selected_node = null; + restart(); + }); + g.append('svg:text').attr('x', 0).attr('y', -20).attr('class', 'id').text(function(d) { + return d.id; + }); + circle.exit().remove(); + force.start(); +} +Array.prototype.remByVal = function(val) { + for (var i = 0; i < this.length; i++) { + if (this[i] === val) { + this.splice(i, 1); + i--; + } + } + return this; +} +//Call like + +function mousedown() { + +var foo = []; +var n = 70 +for (var i = 1; i <= n; i++) { + foo.push(i); +} + +for (var j = 1; j <= n; j++) { + addPeerWithPeers(j, foo.slice().remByVal(j)) +} + +restart() + +return + svg.classed('active', true); + if (d3.event.ctrlKey || mousedown_node || mousedown_link) return; + var point = d3.mouse(this), + node = { + id: ++lastNodeId, + reflexive: false + }; + node.x = point[0]; + node.y = point[1]; + nodes.push(node); + restart(); +} + +function mousemove() { + if (!mousedown_node) return; + drag_line.attr('d', 'M' + mousedown_node.x + ',' + mousedown_node.y + 'L' + d3.mouse(this)[0] + ',' + d3.mouse(this)[1]); + restart(); +} + +function mouseup() { + if (mousedown_node) { + drag_line.classed('hidden', true).style('marker-end', ''); + } + svg.classed('active', false); + resetMouseVars(); +} + +function spliceLinksForNode(node) { + var toSplice = links.filter(function(l) { + return (l.source === node || l.target === node); + }); + toSplice.map(function(l) { + links.splice(links.indexOf(l), 1); + }); +} +var lastKeyDown = -1; + +function keydown() { + d3.event.preventDefault(); + if (lastKeyDown !== -1) return; + lastKeyDown = d3.event.keyCode; + if (d3.event.keyCode === 17) { + circle.call(force.drag); + svg.classed('ctrl', true); + } + if (!selected_node && !selected_link) return; + switch (d3.event.keyCode) { + case 8: + case 46: + if (selected_node) { + nodes.splice(nodes.indexOf(selected_node), 1); + spliceLinksForNode(selected_node); + } else if (selected_link) { + links.splice(links.indexOf(selected_link), 1); + } + selected_link = null; + selected_node = null; + restart(); + break; + case 66: + if (selected_link) { + selected_link.left = true; + selected_link.right = true; + } + restart(); + break; + case 76: + if (selected_link) { + selected_link.left = true; + selected_link.right = false; + } + restart(); + break; + case 82: + if (selected_node) { + selected_node.reflexive = !selected_node.reflexive; + } else if (selected_link) { + selected_link.left = false; + selected_link.right = true; + } + restart(); + break; + } +} + +function keyup() { + lastKeyDown = -1; + if (d3.event.keyCode === 17) { + circle.on('mousedown.drag', null).on('touchstart.drag', null); + svg.classed('ctrl', false); + } +} +svg.on('mousedown', mousedown).on('mousemove', mousemove).on('mouseup', mouseup); +d3.select(window).on('keydown', keydown).on('keyup', keyup); +restart(); \ No newline at end of file From 9b7f4ca0196a900a44cabe79b530db636345352e Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Thu, 26 Oct 2017 19:29:53 +0300 Subject: [PATCH 06/14] WIP --- discovery/src/main/resources/application.conf | 1 + discovery/src/main/web/app.css | 5 +- discovery/src/main/web/data.js | 76 ++++ discovery/src/main/web/data.json | 337 ------------------ discovery/src/main/web/{test.js => graph.js} | 92 ++++- discovery/src/main/web/index.html | 26 +- discovery/src/main/web/index.js | 154 +++++--- 7 files changed, 278 insertions(+), 413 deletions(-) create mode 100644 discovery/src/main/web/data.js delete mode 100644 discovery/src/main/web/data.json rename discovery/src/main/web/{test.js => graph.js} (77%) diff --git a/discovery/src/main/resources/application.conf b/discovery/src/main/resources/application.conf index d1b9b21938c..9abe39f023b 100644 --- a/discovery/src/main/resources/application.conf +++ b/discovery/src/main/resources/application.conf @@ -1,4 +1,5 @@ discovery { + //chain-id = T chain-id = W //initial-peers = ["52.30.47.67:6863", "52.28.66.217:6863", "52.77.111.219:6863", "52.51.92.182:6863"] initial-peers = ["138.201.152.163:6868", "138.201.152.164:6868", "138.201.152.165:6868", "35.156.19.4:6868", "52.50.69.247:6868", "52.57.147.71:6868"] diff --git a/discovery/src/main/web/app.css b/discovery/src/main/web/app.css index 8ef0ce08e8b..cdfc26e57a4 100644 --- a/discovery/src/main/web/app.css +++ b/discovery/src/main/web/app.css @@ -15,7 +15,7 @@ svg:not(.active):not(.ctrl) { path.link { fill: none; stroke: #000; - stroke-width: 2px; + stroke-width: 0.1px; cursor: default; } @@ -46,7 +46,8 @@ circle.node.reflexive { } text { - font: 12px sans-serif; + font: 6px sans-serif; + font-weight:lighter; pointer-events: none; } diff --git a/discovery/src/main/web/data.js b/discovery/src/main/web/data.js new file mode 100644 index 00000000000..87c899865d7 --- /dev/null +++ b/discovery/src/main/web/data.js @@ -0,0 +1,76 @@ +Array.prototype.except = function(arr) { + var self = this.slice() + return this.filter(x => !arr.includes(x)) +} + +var peerSet = {} +var peerStats = {} +var peerHops = {} + +function peers(peer) { + if(!peerSet[peer]) + return [] + + return peerSet[peer] +} + +function addPeerWithPeers(peer, peers) { + peerSet[peer] = peers +} + +function isEmpty(obj) { + for(var key in obj) { + if(obj.hasOwnProperty(key)) + return false; + } + return true; +} + +function calculateHops() { + var result = [] + for(peer in peerSet) { + var byLevel = hopsByLevel(calculatePeerHops(peer, 0, 1, 4)) + byLevel.peer = peer + result.push(byLevel) + } + + return result +} + +function calculatePeerHops(peer, peerHops = {}, depth = 1, max = 1) { + + if(depth > max) + return peerHops + + if(isEmpty(peerHops)) { + peerHops = {} + peers(peer).forEach(hop => peerHops[hop] = depth) + } else { + for(hop in peerHops) { + if(peerHops[hop] == depth - 1) { + peers(hop).filter(p => peerHops[p] ? false : true).forEach(hop => peerHops[hop] = depth) + } + } + } + + return calculatePeerHops(peer, peerHops, depth + 1, max) +} + +function hopsByLevel(peerHops) { + byLevel = {} + for(hop in peerHops) { + var level = peerHops[hop] + + if(!byLevel[level]) + byLevel[level] = [] + + byLevel[level].push(hop) + } + + return byLevel +} + +function firstHopPeers(peer) { + return peers(peer).length +} + diff --git a/discovery/src/main/web/data.json b/discovery/src/main/web/data.json deleted file mode 100644 index 693327f00d5..00000000000 --- a/discovery/src/main/web/data.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "nodes": [ - {"id": "Myriel", "group": 1}, - {"id": "Napoleon", "group": 1}, - {"id": "Mlle.Baptistine", "group": 1}, - {"id": "Mme.Magloire", "group": 1}, - {"id": "CountessdeLo", "group": 1}, - {"id": "Geborand", "group": 1}, - {"id": "Champtercier", "group": 1}, - {"id": "Cravatte", "group": 1}, - {"id": "Count", "group": 1}, - {"id": "OldMan", "group": 1}, - {"id": "Labarre", "group": 2}, - {"id": "Valjean", "group": 2}, - {"id": "Marguerite", "group": 3}, - {"id": "Mme.deR", "group": 2}, - {"id": "Isabeau", "group": 2}, - {"id": "Gervais", "group": 2}, - {"id": "Tholomyes", "group": 3}, - {"id": "Listolier", "group": 3}, - {"id": "Fameuil", "group": 3}, - {"id": "Blacheville", "group": 3}, - {"id": "Favourite", "group": 3}, - {"id": "Dahlia", "group": 3}, - {"id": "Zephine", "group": 3}, - {"id": "Fantine", "group": 3}, - {"id": "Mme.Thenardier", "group": 4}, - {"id": "Thenardier", "group": 4}, - {"id": "Cosette", "group": 5}, - {"id": "Javert", "group": 4}, - {"id": "Fauchelevent", "group": 0}, - {"id": "Bamatabois", "group": 2}, - {"id": "Perpetue", "group": 3}, - {"id": "Simplice", "group": 2}, - {"id": "Scaufflaire", "group": 2}, - {"id": "Woman1", "group": 2}, - {"id": "Judge", "group": 2}, - {"id": "Champmathieu", "group": 2}, - {"id": "Brevet", "group": 2}, - {"id": "Chenildieu", "group": 2}, - {"id": "Cochepaille", "group": 2}, - {"id": "Pontmercy", "group": 4}, - {"id": "Boulatruelle", "group": 6}, - {"id": "Eponine", "group": 4}, - {"id": "Anzelma", "group": 4}, - {"id": "Woman2", "group": 5}, - {"id": "MotherInnocent", "group": 0}, - {"id": "Gribier", "group": 0}, - {"id": "Jondrette", "group": 7}, - {"id": "Mme.Burgon", "group": 7}, - {"id": "Gavroche", "group": 8}, - {"id": "Gillenormand", "group": 5}, - {"id": "Magnon", "group": 5}, - {"id": "Mlle.Gillenormand", "group": 5}, - {"id": "Mme.Pontmercy", "group": 5}, - {"id": "Mlle.Vaubois", "group": 5}, - {"id": "Lt.Gillenormand", "group": 5}, - {"id": "Marius", "group": 8}, - {"id": "BaronessT", "group": 5}, - {"id": "Mabeuf", "group": 8}, - {"id": "Enjolras", "group": 8}, - {"id": "Combeferre", "group": 8}, - {"id": "Prouvaire", "group": 8}, - {"id": "Feuilly", "group": 8}, - {"id": "Courfeyrac", "group": 8}, - {"id": "Bahorel", "group": 8}, - {"id": "Bossuet", "group": 8}, - {"id": "Joly", "group": 8}, - {"id": "Grantaire", "group": 8}, - {"id": "MotherPlutarch", "group": 9}, - {"id": "Gueulemer", "group": 4}, - {"id": "Babet", "group": 4}, - {"id": "Claquesous", "group": 4}, - {"id": "Montparnasse", "group": 4}, - {"id": "Toussaint", "group": 5}, - {"id": "Child1", "group": 10}, - {"id": "Child2", "group": 10}, - {"id": "Brujon", "group": 4}, - {"id": "Mme.Hucheloup", "group": 8} - ], - "links": [ - {"source": "Napoleon", "target": "Myriel", "value": 1}, - {"source": "Mlle.Baptistine", "target": "Myriel", "value": 8}, - {"source": "Mme.Magloire", "target": "Myriel", "value": 10}, - {"source": "Mme.Magloire", "target": "Mlle.Baptistine", "value": 6}, - {"source": "CountessdeLo", "target": "Myriel", "value": 1}, - {"source": "Geborand", "target": "Myriel", "value": 1}, - {"source": "Champtercier", "target": "Myriel", "value": 1}, - {"source": "Cravatte", "target": "Myriel", "value": 1}, - {"source": "Count", "target": "Myriel", "value": 2}, - {"source": "OldMan", "target": "Myriel", "value": 1}, - {"source": "Valjean", "target": "Labarre", "value": 1}, - {"source": "Valjean", "target": "Mme.Magloire", "value": 3}, - {"source": "Valjean", "target": "Mlle.Baptistine", "value": 3}, - {"source": "Valjean", "target": "Myriel", "value": 5}, - {"source": "Marguerite", "target": "Valjean", "value": 1}, - {"source": "Mme.deR", "target": "Valjean", "value": 1}, - {"source": "Isabeau", "target": "Valjean", "value": 1}, - {"source": "Gervais", "target": "Valjean", "value": 1}, - {"source": "Listolier", "target": "Tholomyes", "value": 4}, - {"source": "Fameuil", "target": "Tholomyes", "value": 4}, - {"source": "Fameuil", "target": "Listolier", "value": 4}, - {"source": "Blacheville", "target": "Tholomyes", "value": 4}, - {"source": "Blacheville", "target": "Listolier", "value": 4}, - {"source": "Blacheville", "target": "Fameuil", "value": 4}, - {"source": "Favourite", "target": "Tholomyes", "value": 3}, - {"source": "Favourite", "target": "Listolier", "value": 3}, - {"source": "Favourite", "target": "Fameuil", "value": 3}, - {"source": "Favourite", "target": "Blacheville", "value": 4}, - {"source": "Dahlia", "target": "Tholomyes", "value": 3}, - {"source": "Dahlia", "target": "Listolier", "value": 3}, - {"source": "Dahlia", "target": "Fameuil", "value": 3}, - {"source": "Dahlia", "target": "Blacheville", "value": 3}, - {"source": "Dahlia", "target": "Favourite", "value": 5}, - {"source": "Zephine", "target": "Tholomyes", "value": 3}, - {"source": "Zephine", "target": "Listolier", "value": 3}, - {"source": "Zephine", "target": "Fameuil", "value": 3}, - {"source": "Zephine", "target": "Blacheville", "value": 3}, - {"source": "Zephine", "target": "Favourite", "value": 4}, - {"source": "Zephine", "target": "Dahlia", "value": 4}, - {"source": "Fantine", "target": "Tholomyes", "value": 3}, - {"source": "Fantine", "target": "Listolier", "value": 3}, - {"source": "Fantine", "target": "Fameuil", "value": 3}, - {"source": "Fantine", "target": "Blacheville", "value": 3}, - {"source": "Fantine", "target": "Favourite", "value": 4}, - {"source": "Fantine", "target": "Dahlia", "value": 4}, - {"source": "Fantine", "target": "Zephine", "value": 4}, - {"source": "Fantine", "target": "Marguerite", "value": 2}, - {"source": "Fantine", "target": "Valjean", "value": 9}, - {"source": "Mme.Thenardier", "target": "Fantine", "value": 2}, - {"source": "Mme.Thenardier", "target": "Valjean", "value": 7}, - {"source": "Thenardier", "target": "Mme.Thenardier", "value": 13}, - {"source": "Thenardier", "target": "Fantine", "value": 1}, - {"source": "Thenardier", "target": "Valjean", "value": 12}, - {"source": "Cosette", "target": "Mme.Thenardier", "value": 4}, - {"source": "Cosette", "target": "Valjean", "value": 31}, - {"source": "Cosette", "target": "Tholomyes", "value": 1}, - {"source": "Cosette", "target": "Thenardier", "value": 1}, - {"source": "Javert", "target": "Valjean", "value": 17}, - {"source": "Javert", "target": "Fantine", "value": 5}, - {"source": "Javert", "target": "Thenardier", "value": 5}, - {"source": "Javert", "target": "Mme.Thenardier", "value": 1}, - {"source": "Javert", "target": "Cosette", "value": 1}, - {"source": "Fauchelevent", "target": "Valjean", "value": 8}, - {"source": "Fauchelevent", "target": "Javert", "value": 1}, - {"source": "Bamatabois", "target": "Fantine", "value": 1}, - {"source": "Bamatabois", "target": "Javert", "value": 1}, - {"source": "Bamatabois", "target": "Valjean", "value": 2}, - {"source": "Perpetue", "target": "Fantine", "value": 1}, - {"source": "Simplice", "target": "Perpetue", "value": 2}, - {"source": "Simplice", "target": "Valjean", "value": 3}, - {"source": "Simplice", "target": "Fantine", "value": 2}, - {"source": "Simplice", "target": "Javert", "value": 1}, - {"source": "Scaufflaire", "target": "Valjean", "value": 1}, - {"source": "Woman1", "target": "Valjean", "value": 2}, - {"source": "Woman1", "target": "Javert", "value": 1}, - {"source": "Judge", "target": "Valjean", "value": 3}, - {"source": "Judge", "target": "Bamatabois", "value": 2}, - {"source": "Champmathieu", "target": "Valjean", "value": 3}, - {"source": "Champmathieu", "target": "Judge", "value": 3}, - {"source": "Champmathieu", "target": "Bamatabois", "value": 2}, - {"source": "Brevet", "target": "Judge", "value": 2}, - {"source": "Brevet", "target": "Champmathieu", "value": 2}, - {"source": "Brevet", "target": "Valjean", "value": 2}, - {"source": "Brevet", "target": "Bamatabois", "value": 1}, - {"source": "Chenildieu", "target": "Judge", "value": 2}, - {"source": "Chenildieu", "target": "Champmathieu", "value": 2}, - {"source": "Chenildieu", "target": "Brevet", "value": 2}, - {"source": "Chenildieu", "target": "Valjean", "value": 2}, - {"source": "Chenildieu", "target": "Bamatabois", "value": 1}, - {"source": "Cochepaille", "target": "Judge", "value": 2}, - {"source": "Cochepaille", "target": "Champmathieu", "value": 2}, - {"source": "Cochepaille", "target": "Brevet", "value": 2}, - {"source": "Cochepaille", "target": "Chenildieu", "value": 2}, - {"source": "Cochepaille", "target": "Valjean", "value": 2}, - {"source": "Cochepaille", "target": "Bamatabois", "value": 1}, - {"source": "Pontmercy", "target": "Thenardier", "value": 1}, - {"source": "Boulatruelle", "target": "Thenardier", "value": 1}, - {"source": "Eponine", "target": "Mme.Thenardier", "value": 2}, - {"source": "Eponine", "target": "Thenardier", "value": 3}, - {"source": "Anzelma", "target": "Eponine", "value": 2}, - {"source": "Anzelma", "target": "Thenardier", "value": 2}, - {"source": "Anzelma", "target": "Mme.Thenardier", "value": 1}, - {"source": "Woman2", "target": "Valjean", "value": 3}, - {"source": "Woman2", "target": "Cosette", "value": 1}, - {"source": "Woman2", "target": "Javert", "value": 1}, - {"source": "MotherInnocent", "target": "Fauchelevent", "value": 3}, - {"source": "MotherInnocent", "target": "Valjean", "value": 1}, - {"source": "Gribier", "target": "Fauchelevent", "value": 2}, - {"source": "Mme.Burgon", "target": "Jondrette", "value": 1}, - {"source": "Gavroche", "target": "Mme.Burgon", "value": 2}, - {"source": "Gavroche", "target": "Thenardier", "value": 1}, - {"source": "Gavroche", "target": "Javert", "value": 1}, - {"source": "Gavroche", "target": "Valjean", "value": 1}, - {"source": "Gillenormand", "target": "Cosette", "value": 3}, - {"source": "Gillenormand", "target": "Valjean", "value": 2}, - {"source": "Magnon", "target": "Gillenormand", "value": 1}, - {"source": "Magnon", "target": "Mme.Thenardier", "value": 1}, - {"source": "Mlle.Gillenormand", "target": "Gillenormand", "value": 9}, - {"source": "Mlle.Gillenormand", "target": "Cosette", "value": 2}, - {"source": "Mlle.Gillenormand", "target": "Valjean", "value": 2}, - {"source": "Mme.Pontmercy", "target": "Mlle.Gillenormand", "value": 1}, - {"source": "Mme.Pontmercy", "target": "Pontmercy", "value": 1}, - {"source": "Mlle.Vaubois", "target": "Mlle.Gillenormand", "value": 1}, - {"source": "Lt.Gillenormand", "target": "Mlle.Gillenormand", "value": 2}, - {"source": "Lt.Gillenormand", "target": "Gillenormand", "value": 1}, - {"source": "Lt.Gillenormand", "target": "Cosette", "value": 1}, - {"source": "Marius", "target": "Mlle.Gillenormand", "value": 6}, - {"source": "Marius", "target": "Gillenormand", "value": 12}, - {"source": "Marius", "target": "Pontmercy", "value": 1}, - {"source": "Marius", "target": "Lt.Gillenormand", "value": 1}, - {"source": "Marius", "target": "Cosette", "value": 21}, - {"source": "Marius", "target": "Valjean", "value": 19}, - {"source": "Marius", "target": "Tholomyes", "value": 1}, - {"source": "Marius", "target": "Thenardier", "value": 2}, - {"source": "Marius", "target": "Eponine", "value": 5}, - {"source": "Marius", "target": "Gavroche", "value": 4}, - {"source": "BaronessT", "target": "Gillenormand", "value": 1}, - {"source": "BaronessT", "target": "Marius", "value": 1}, - {"source": "Mabeuf", "target": "Marius", "value": 1}, - {"source": "Mabeuf", "target": "Eponine", "value": 1}, - {"source": "Mabeuf", "target": "Gavroche", "value": 1}, - {"source": "Enjolras", "target": "Marius", "value": 7}, - {"source": "Enjolras", "target": "Gavroche", "value": 7}, - {"source": "Enjolras", "target": "Javert", "value": 6}, - {"source": "Enjolras", "target": "Mabeuf", "value": 1}, - {"source": "Enjolras", "target": "Valjean", "value": 4}, - {"source": "Combeferre", "target": "Enjolras", "value": 15}, - {"source": "Combeferre", "target": "Marius", "value": 5}, - {"source": "Combeferre", "target": "Gavroche", "value": 6}, - {"source": "Combeferre", "target": "Mabeuf", "value": 2}, - {"source": "Prouvaire", "target": "Gavroche", "value": 1}, - {"source": "Prouvaire", "target": "Enjolras", "value": 4}, - {"source": "Prouvaire", "target": "Combeferre", "value": 2}, - {"source": "Feuilly", "target": "Gavroche", "value": 2}, - {"source": "Feuilly", "target": "Enjolras", "value": 6}, - {"source": "Feuilly", "target": "Prouvaire", "value": 2}, - {"source": "Feuilly", "target": "Combeferre", "value": 5}, - {"source": "Feuilly", "target": "Mabeuf", "value": 1}, - {"source": "Feuilly", "target": "Marius", "value": 1}, - {"source": "Courfeyrac", "target": "Marius", "value": 9}, - {"source": "Courfeyrac", "target": "Enjolras", "value": 17}, - {"source": "Courfeyrac", "target": "Combeferre", "value": 13}, - {"source": "Courfeyrac", "target": "Gavroche", "value": 7}, - {"source": "Courfeyrac", "target": "Mabeuf", "value": 2}, - {"source": "Courfeyrac", "target": "Eponine", "value": 1}, - {"source": "Courfeyrac", "target": "Feuilly", "value": 6}, - {"source": "Courfeyrac", "target": "Prouvaire", "value": 3}, - {"source": "Bahorel", "target": "Combeferre", "value": 5}, - {"source": "Bahorel", "target": "Gavroche", "value": 5}, - {"source": "Bahorel", "target": "Courfeyrac", "value": 6}, - {"source": "Bahorel", "target": "Mabeuf", "value": 2}, - {"source": "Bahorel", "target": "Enjolras", "value": 4}, - {"source": "Bahorel", "target": "Feuilly", "value": 3}, - {"source": "Bahorel", "target": "Prouvaire", "value": 2}, - {"source": "Bahorel", "target": "Marius", "value": 1}, - {"source": "Bossuet", "target": "Marius", "value": 5}, - {"source": "Bossuet", "target": "Courfeyrac", "value": 12}, - {"source": "Bossuet", "target": "Gavroche", "value": 5}, - {"source": "Bossuet", "target": "Bahorel", "value": 4}, - {"source": "Bossuet", "target": "Enjolras", "value": 10}, - {"source": "Bossuet", "target": "Feuilly", "value": 6}, - {"source": "Bossuet", "target": "Prouvaire", "value": 2}, - {"source": "Bossuet", "target": "Combeferre", "value": 9}, - {"source": "Bossuet", "target": "Mabeuf", "value": 1}, - {"source": "Bossuet", "target": "Valjean", "value": 1}, - {"source": "Joly", "target": "Bahorel", "value": 5}, - {"source": "Joly", "target": "Bossuet", "value": 7}, - {"source": "Joly", "target": "Gavroche", "value": 3}, - {"source": "Joly", "target": "Courfeyrac", "value": 5}, - {"source": "Joly", "target": "Enjolras", "value": 5}, - {"source": "Joly", "target": "Feuilly", "value": 5}, - {"source": "Joly", "target": "Prouvaire", "value": 2}, - {"source": "Joly", "target": "Combeferre", "value": 5}, - {"source": "Joly", "target": "Mabeuf", "value": 1}, - {"source": "Joly", "target": "Marius", "value": 2}, - {"source": "Grantaire", "target": "Bossuet", "value": 3}, - {"source": "Grantaire", "target": "Enjolras", "value": 3}, - {"source": "Grantaire", "target": "Combeferre", "value": 1}, - {"source": "Grantaire", "target": "Courfeyrac", "value": 2}, - {"source": "Grantaire", "target": "Joly", "value": 2}, - {"source": "Grantaire", "target": "Gavroche", "value": 1}, - {"source": "Grantaire", "target": "Bahorel", "value": 1}, - {"source": "Grantaire", "target": "Feuilly", "value": 1}, - {"source": "Grantaire", "target": "Prouvaire", "value": 1}, - {"source": "MotherPlutarch", "target": "Mabeuf", "value": 3}, - {"source": "Gueulemer", "target": "Thenardier", "value": 5}, - {"source": "Gueulemer", "target": "Valjean", "value": 1}, - {"source": "Gueulemer", "target": "Mme.Thenardier", "value": 1}, - {"source": "Gueulemer", "target": "Javert", "value": 1}, - {"source": "Gueulemer", "target": "Gavroche", "value": 1}, - {"source": "Gueulemer", "target": "Eponine", "value": 1}, - {"source": "Babet", "target": "Thenardier", "value": 6}, - {"source": "Babet", "target": "Gueulemer", "value": 6}, - {"source": "Babet", "target": "Valjean", "value": 1}, - {"source": "Babet", "target": "Mme.Thenardier", "value": 1}, - {"source": "Babet", "target": "Javert", "value": 2}, - {"source": "Babet", "target": "Gavroche", "value": 1}, - {"source": "Babet", "target": "Eponine", "value": 1}, - {"source": "Claquesous", "target": "Thenardier", "value": 4}, - {"source": "Claquesous", "target": "Babet", "value": 4}, - {"source": "Claquesous", "target": "Gueulemer", "value": 4}, - {"source": "Claquesous", "target": "Valjean", "value": 1}, - {"source": "Claquesous", "target": "Mme.Thenardier", "value": 1}, - {"source": "Claquesous", "target": "Javert", "value": 1}, - {"source": "Claquesous", "target": "Eponine", "value": 1}, - {"source": "Claquesous", "target": "Enjolras", "value": 1}, - {"source": "Montparnasse", "target": "Javert", "value": 1}, - {"source": "Montparnasse", "target": "Babet", "value": 2}, - {"source": "Montparnasse", "target": "Gueulemer", "value": 2}, - {"source": "Montparnasse", "target": "Claquesous", "value": 2}, - {"source": "Montparnasse", "target": "Valjean", "value": 1}, - {"source": "Montparnasse", "target": "Gavroche", "value": 1}, - {"source": "Montparnasse", "target": "Eponine", "value": 1}, - {"source": "Montparnasse", "target": "Thenardier", "value": 1}, - {"source": "Toussaint", "target": "Cosette", "value": 2}, - {"source": "Toussaint", "target": "Javert", "value": 1}, - {"source": "Toussaint", "target": "Valjean", "value": 1}, - {"source": "Child1", "target": "Gavroche", "value": 2}, - {"source": "Child2", "target": "Gavroche", "value": 2}, - {"source": "Child2", "target": "Child1", "value": 3}, - {"source": "Brujon", "target": "Babet", "value": 3}, - {"source": "Brujon", "target": "Gueulemer", "value": 3}, - {"source": "Brujon", "target": "Thenardier", "value": 3}, - {"source": "Brujon", "target": "Gavroche", "value": 1}, - {"source": "Brujon", "target": "Eponine", "value": 1}, - {"source": "Brujon", "target": "Claquesous", "value": 1}, - {"source": "Brujon", "target": "Montparnasse", "value": 1}, - {"source": "Mme.Hucheloup", "target": "Bossuet", "value": 1}, - {"source": "Mme.Hucheloup", "target": "Joly", "value": 1}, - {"source": "Mme.Hucheloup", "target": "Grantaire", "value": 1}, - {"source": "Mme.Hucheloup", "target": "Bahorel", "value": 1}, - {"source": "Mme.Hucheloup", "target": "Courfeyrac", "value": 1}, - {"source": "Mme.Hucheloup", "target": "Gavroche", "value": 1}, - {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1} - ] -} \ No newline at end of file diff --git a/discovery/src/main/web/test.js b/discovery/src/main/web/graph.js similarity index 77% rename from discovery/src/main/web/test.js rename to discovery/src/main/web/graph.js index 855f1965ece..dfd78abed66 100644 --- a/discovery/src/main/web/test.js +++ b/discovery/src/main/web/graph.js @@ -6,6 +6,8 @@ var nodes = [], lastNodeId = 0, links = []; + + var nodesSet = {} var linksSet = {} @@ -31,6 +33,8 @@ function addPeer(peer) { } function addLink(from, to) { + if(from == to) + return if(linksSet[from] && linksSet[from][to]) return @@ -55,13 +59,61 @@ function addLink(from, to) { } } +var radius = 3 -var force = d3.layout.force().nodes(nodes).links(links).size([width, height]).linkDistance(300).charge(1).on('tick', tick) +var force = d3.layout.force().nodes(nodes).links(links).size([width, height]).linkDistance(3).linkStrength(0.1).friction(0.1).theta(0.8).charge(-3).on('tick', tick) svg.append('svg:defs').append('svg:marker').attr('id', 'end-arrow').attr('viewBox', '0 -5 10 10').attr('refX', 6).attr('markerWidth', 3).attr('markerHeight', 3).attr('orient', 'auto').append('svg:path').attr('d', 'M0,-5L10,0L0,5').attr('fill', '#000'); svg.append('svg:defs').append('svg:marker').attr('id', 'start-arrow').attr('viewBox', '0 -5 10 10').attr('refX', 4).attr('markerWidth', 3).attr('markerHeight', 3).attr('orient', 'auto').append('svg:path').attr('d', 'M10,-5L0,0L10,5').attr('fill', '#000'); var drag_line = svg.append('svg:path').attr('class', 'link dragline hidden').attr('d', 'M0,0L0,0'); -var path = svg.append('svg:g').selectAll('path'), - circle = svg.append('svg:g').selectAll('g'); + + var g = svg.append("g"); + +var x = d3.scale.linear() + .domain([0, width]) + .range([0, width]); + +var y = d3.scale.linear() + .domain([0, height]) + .range([height, 0]); + + var zoom = d3.behavior.zoom().x(x).y(y) + // only scale up, e.g. between 1x and 50x + .scaleExtent([1, 50]) + .on("zoom", function() { + // the "zoom" event populates d3.event with an object that has + // a "translate" property (a 2-element Array in the form [x, y]) + // and a numeric "scale" property + var e = d3.event, + // now, constrain the x and y components of the translation by the + // dimensions of the viewport + tx = Math.min(0, Math.max(e.translate[0], width - width * e.scale)), + ty = Math.min(0, Math.max(e.translate[1], height - height * e.scale)); + // then, update the zoom behavior's internal translation, so that + // it knows how to properly manipulate it on the next movement + zoom.translate([tx, ty]); + // and finally, update the element's transform attribute with the + // correct translation and scale (in reverse order) + + g.attr("transform", [ + "translate(" + [tx, ty] + ")", + "scale(" + e.scale + ")" + ].join(" ")); + + g.selectAll(".node").attr("transform", "scale(" + 1.0 / e.scale + ")"); + g.selectAll(".link").style("stroke-width", 0.1 / e.scale + "px"); + svg.selectAll("text").attr("transform", "scale(" + 1.0 / e.scale + ")"); + }); + + // then, call the zoom behavior on the svg element, which will add + // all of the necessary mouse and touch event handlers. + // remember that if you call this on the element, the even handlers + // will only trigger when the mouse or touch cursor intersects with the + // elements' children! + svg.call(zoom); + + +var path = g.selectAll('path'), + circle = g.selectAll('g'); var selected_node = null, selected_link = null, mousedown_link = null, @@ -81,8 +133,8 @@ function tick() { dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY), normX = deltaX / dist, normY = deltaY / dist, - sourcePadding = d.left ? 17 : 12, - targetPadding = d.right ? 17 : 12, + sourcePadding = 0, // d.left ? radius * 1.3 : radius, + targetPadding = 0 // d.right ? radius * 1.3 : radius, sourceX = d.source.x + (sourcePadding * normX), sourceY = d.source.y + (sourcePadding * normY), targetX = d.target.x - (targetPadding * normX), @@ -127,7 +179,10 @@ function restart() { return d.reflexive; }); var g = circle.enter().append('svg:g'); - g.append('svg:circle').attr('class', 'node').attr('r', 12).style('fill', function(d) { + + + + g.append('svg:circle').attr('class', 'node').attr('r', radius).style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id); }).style('stroke', function(d) { return d3.rgb(colors(d.id)).darker().toString(); @@ -186,11 +241,12 @@ function restart() { selected_node = null; restart(); }); - g.append('svg:text').attr('x', 0).attr('y', -20).attr('class', 'id').text(function(d) { + g.append('svg:text').attr('x', 0).attr('y', -radius * 1.8).attr('class', 'id').text(function(d) { return d.id; }); circle.exit().remove(); force.start(); + //for (var i = 0; i < 100; ++i) force.tick(); } Array.prototype.remByVal = function(val) { for (var i = 0; i < this.length; i++) { @@ -205,17 +261,17 @@ Array.prototype.remByVal = function(val) { function mousedown() { -var foo = []; -var n = 70 -for (var i = 1; i <= n; i++) { - foo.push(i); -} - -for (var j = 1; j <= n; j++) { - addPeerWithPeers(j, foo.slice().remByVal(j)) -} - -restart() +//var foo = []; +//var n = 70 +//for (var i = 1; i <= n; i++) { +// foo.push(i); +//} +// +//for (var j = 1; j <= n; j++) { +// addPeerWithPeers(j, foo.slice()) +//} +// +//restart() return svg.classed('active', true); diff --git a/discovery/src/main/web/index.html b/discovery/src/main/web/index.html index 3b6fab66480..8a285836778 100644 --- a/discovery/src/main/web/index.html +++ b/discovery/src/main/web/index.html @@ -1,13 +1,33 @@ - + + + + - + + + +
+
+ + + + + + + - + + + \ No newline at end of file diff --git a/discovery/src/main/web/index.js b/discovery/src/main/web/index.js index 553bc39bd3f..d164b464973 100644 --- a/discovery/src/main/web/index.js +++ b/discovery/src/main/web/index.js @@ -1,57 +1,105 @@ - function print(text) { - var p = document.createElement("P") - p.appendChild(document.createTextNode(text)) - document.getElementById("root").appendChild(p) - //window.scrollTo(0,document.body.scrollHeight); +var table + +function print(text) { + var p = document.createElement("P") + p.appendChild(document.createTextNode(text)) + document.getElementById("root").appendChild(p) + //window.scrollTo(0,document.body.scrollHeight); +} + +function WebSocketTest() +{ + if ("WebSocket" in window) + { + var ws = new WebSocket("ws://localhost:8080/"); + var wasConnected = false + + ws.onopen = function() + { + wasConnected = true + print("WebSocket connected...") + }; + + ws.onmessage = function (evt) + { + var received_msg = evt.data + var msg = JSON.parse(evt.data); + + for(var peer in msg) { + addPeerWithPeers(peer, msg[peer]) + } + + var hops = calculateHops() + + table.clear() + + for (var i = 0; i < hops.length; i++){ + table.row.add(hops[i]) } - function WebSocketTest() - { - if ("WebSocket" in window) - { - var ws = new WebSocket("ws://localhost:8080/"); - var wasConnected = false - - ws.onopen = function() - { - wasConnected = true - print("WebSocket connected...") - }; - - ws.onmessage = function (evt) - { - var received_msg = evt.data - var msg = JSON.parse(evt.data); - - for(var peer in msg) { - addPeerWithPeers(peer, msg[peer]) - } - - restart() - - print(received_msg) - }; - - ws.onclose = function() - { - if(!wasConnected) { - WebSocketTest() - } - else { - print("WebSocket closed.") - } - }; - - window.onbeforeunload = function(event) { - socket.close() - }; - } - - else - { - print("WebSocket NOT supported by your Browser!"); - } + table.draw() + + $('.label') + .popup() + ; + + print(received_msg) + }; + + ws.onclose = function() + { + if(!wasConnected) { + WebSocketTest() } + else { + print("WebSocket closed.") + } + }; + + window.onbeforeunload = function(event) { + socket.close() + }; + } + + else + { + print("WebSocket NOT supported by your Browser!"); + } +} + +var columnDefinitions = [{ + title: "node", + targets: 0, + data: "peer", + render: function ( data, type, row, meta ) { + return data + } +}] + +for(var i = 1; i <= 3; i++) { + columnDefinitions.push({ + title: "hop#" + i, + targets: i, + data: i, + render: function ( data, type, row, meta ) { + if(!data) + return 0 + return data.length + } + }) +} + + +$(document).ready(function() { +table = $('#example').DataTable({ + paging: false, + columnDefs: columnDefinitions + }); + +print("Waiting for host...") +WebSocketTest(); +}); + + + - print("Waiting for host...") - WebSocketTest(); From 8edb8897d1dc58df74d817eddbf85e2f972528f2 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Fri, 27 Oct 2017 17:45:29 +0300 Subject: [PATCH 07/14] Many networks simultaneous support --- discovery/src/main/resources/application.conf | 14 ++- .../CancellableExt.scala | 12 ++ .../DiscoveryApp.scala | 41 +++---- .../Settings.scala | 11 +- .../actors/MainActor.scala | 11 +- .../actors/PeerDiscoveryActor.scala | 4 +- .../network/HandshakeHandler.scala | 5 +- discovery/src/main/web/data.js | 106 +++++++++--------- discovery/src/main/web/graph.js | 20 ---- discovery/src/main/web/index.html | 28 +++-- discovery/src/main/web/index.js | 68 +++++------ 11 files changed, 158 insertions(+), 162 deletions(-) create mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/CancellableExt.scala diff --git a/discovery/src/main/resources/application.conf b/discovery/src/main/resources/application.conf index 9abe39f023b..9ea933ba4c2 100644 --- a/discovery/src/main/resources/application.conf +++ b/discovery/src/main/resources/application.conf @@ -1,8 +1,14 @@ discovery { - //chain-id = T - chain-id = W - //initial-peers = ["52.30.47.67:6863", "52.28.66.217:6863", "52.77.111.219:6863", "52.51.92.182:6863"] - initial-peers = ["138.201.152.163:6868", "138.201.152.164:6868", "138.201.152.165:6868", "35.156.19.4:6868", "52.50.69.247:6868", "52.57.147.71:6868"] + chains = [ + { + chain-id = W + initial-peers = ["138.201.152.163:6868", "138.201.152.164:6868", "138.201.152.165:6868", "35.156.19.4:6868", "52.50.69.247:6868", "52.57.147.71:6868"] + }, + { + chain-id = T + initial-peers = ["52.30.47.67:6863", "52.28.66.217:6863", "52.77.111.219:6863", "52.51.92.182:6863"] + } + ] web-socket-host = "localhost" web-socket-port = 8080 workers-count = 10 diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/CancellableExt.scala b/discovery/src/main/scala/com.wavesplatform.discovery/CancellableExt.scala new file mode 100644 index 00000000000..8eec3d34d55 --- /dev/null +++ b/discovery/src/main/scala/com.wavesplatform.discovery/CancellableExt.scala @@ -0,0 +1,12 @@ +package com.wavesplatform.discovery + +import akka.actor.Cancellable + +object CancellableExt { + implicit def Ext(self: Cancellable) = new { + def combine(other: Cancellable): Cancellable = new Cancellable { + override def cancel() = self.cancel() & other.cancel() + override def isCancelled = self.isCancelled && other.isCancelled + } + } +} \ No newline at end of file diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala index 9e80e13c4f8..a09b75c8c75 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala @@ -10,6 +10,7 @@ import akka.stream.scaladsl.{Flow, Sink, Source} import akka.stream.{ActorMaterializer, OverflowStrategy} import com.wavesplatform.discovery.actors.MainActor import com.wavesplatform.discovery.actors.MainActor.WebSocketConnected +import com.wavesplatform.discovery.CancellableExt._ import scala.concurrent.duration.FiniteDuration import scala.io.StdIn @@ -17,35 +18,37 @@ import scala.io.StdIn object DiscoveryApp extends App { implicit val system: ActorSystem = ActorSystem("Default") implicit val flowMaterializer: ActorMaterializer = ActorMaterializer() - - val mainActor = MainActor(Settings.default.workersCount) - - mainActor ! MainActor.Peers(Settings.default.initialPeers.toSet) - - system.scheduler.schedule(FiniteDuration(0, TimeUnit.SECONDS),FiniteDuration(500, TimeUnit.MILLISECONDS), mainActor, MainActor.Discover) - import akka.http.scaladsl.server.Directives._ - val route = get { - pathEndOrSingleSlash { + val (route, timer) = Settings.default.chains.map {cs => + val mainActor = MainActor(cs.chainId, Settings.default.workersCount) + mainActor ! MainActor.Peers(cs.initialPeers.toSet) - val sink: Sink[akka.http.scaladsl.model.ws.Message, _] = Sink.ignore - val source: Source[akka.http.scaladsl.model.ws.Message, NotUsed] = - Source.actorRef[String](1, OverflowStrategy.dropTail) - .mapMaterializedValue { actor => - mainActor ! WebSocketConnected(actor) - NotUsed - }.map( - (outMsg: String) => TextMessage(outMsg)) + val route = get { + path(cs.chainId.toLower.toString) { - handleWebSocketMessages(Flow.fromSinkAndSource(sink, source)) + val sink: Sink[akka.http.scaladsl.model.ws.Message, _] = Sink.ignore + val source: Source[akka.http.scaladsl.model.ws.Message, NotUsed] = + Source.actorRef[String](1, OverflowStrategy.dropTail) + .mapMaterializedValue { actor => + mainActor ! WebSocketConnected(actor) + NotUsed + }.map( + (outMsg: String) => TextMessage(outMsg)) + + handleWebSocketMessages(Flow.fromSinkAndSource(sink, source)) + } } - } + + (route, system.scheduler.schedule(FiniteDuration(0, TimeUnit.SECONDS), Settings.default.discoveryInterval, mainActor, MainActor.Discover)) + }.reduce((a,b) => (a._1 ~ b._1, a._2.combine(b._2))) val binding = Http().bindAndHandle(route, Settings.default.webSocketHost, Settings.default.webSocketPort) + println(s"Server is now online at http://${Settings.default.webSocketHost}:${Settings.default.webSocketPort}\nPress RETURN to stop...") StdIn.readLine() binding.flatMap(_.unbind()).onComplete(_ => { + timer.cancel() system.terminate() workerGroup.shutdownGracefully() }) diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala b/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala index b74dbc1ee3e..90f701b132b 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/Settings.scala @@ -6,11 +6,16 @@ import net.ceedubs.ficus.Ficus._ import com.typesafe.config.{Config, ConfigFactory} import net.ceedubs.ficus.readers.{NameMapper, ValueReader} -case class Settings(chainId: Char, - initialPeers: Seq[InetSocketAddress], +import scala.concurrent.duration.FiniteDuration + +case class ChainSettings(chainId: Char, initialPeers: Seq[InetSocketAddress]) + +case class Settings(chains: Seq[ChainSettings], webSocketHost: String, webSocketPort: Int, - workersCount: Int) + workersCount: Int, + discoveryInterval: FiniteDuration) + object Settings{ implicit val readConfigInHyphen: NameMapper = net.ceedubs.ficus.readers.namemappers.implicits.hyphenCase // IDEA bug diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala b/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala index 9eb462c6668..7ee0724bf49 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala @@ -9,13 +9,12 @@ import com.wavesplatform.discovery.collections.{ExpirationSet, Pool} import com.wavesplatform.discovery.routers.SmallestMailboxWithThresholdRoutingLogic import play.api.libs.json._ -class MainActor(val workersCount: Int) extends Actor { - +class MainActor(chainId: Char, workersCount: Int) extends Actor { import MainActor._ private val router = { val routes = Vector.fill(workersCount) { - ActorRefRoutee(context.actorOf(Props[PeerDiscoveryActor])) + ActorRefRoutee(context.actorOf(Props(classOf[PeerDiscoveryActor], chainId))) } Router(SmallestMailboxWithThresholdRoutingLogic(5), routes) } @@ -57,7 +56,7 @@ class MainActor(val workersCount: Int) extends Actor { } } - private def jsonPeersData = peerResponses.foldLeft(Json.obj())((json, keyValue) => json.+(keyValue._1.getHostString, JsArray(keyValue._2.map(v => JsString(v.getHostString)).toSeq))).toString() + private def jsonPeersData = peerResponses.foldLeft(Json.obj())((json, keyValue) => json + (keyValue._1.getHostString, JsArray(keyValue._2.map(v => JsString(v.getHostString)).toSeq))).toString() private def broadcastPeerInfo(peer: InetSocketAddress, peers: Set[InetSocketAddress]): Unit = { val response = Json.obj(peer.getHostString -> JsArray(peers.map(p => JsString(p.getHostString)).toSeq)).toString() @@ -77,7 +76,7 @@ object MainActor { case object Discover - def apply(workersCount: Int)(implicit system: ActorSystem): ActorRef = { - system.actorOf(Props(classOf[MainActor], workersCount), "main") + def apply(chainId: Char, workersCount: Int)(implicit system: ActorSystem): ActorRef = { + system.actorOf(Props(classOf[MainActor], chainId, workersCount)) } } \ No newline at end of file diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala index 342482c5a0d..5907fffa62b 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala @@ -19,7 +19,7 @@ object PeerDiscoveryActor { case class GetPeersFrom(peer: InetSocketAddress) } -class PeerDiscoveryActor extends Actor { +class PeerDiscoveryActor(chainId: Char) extends Actor { import PeerDiscoveryActor._ @@ -37,7 +37,7 @@ class PeerDiscoveryActor extends Actor { .channel(classOf[NioSocketChannel]) .handler(new PipelineInitializer[SocketChannel](Seq( exceptionHandler, - new HandshakeHandler(), + new HandshakeHandler(chainId), new LengthFieldPrepender(4), new LengthFieldBasedFrameDecoder(100 * 1024 * 1024, 0, 4, 0, 4), new LegacyFrameCodec(), diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/network/HandshakeHandler.scala b/discovery/src/main/scala/com.wavesplatform.discovery/network/HandshakeHandler.scala index 3ef5001eb26..99b777a15f6 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/network/HandshakeHandler.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/network/HandshakeHandler.scala @@ -3,7 +3,6 @@ package com.wavesplatform.discovery.network import java.util import com.wavesplatform.Version -import com.wavesplatform.discovery.Settings import com.wavesplatform.network.Handshake import com.wavesplatform.settings.Constants import io.netty.buffer.ByteBuf @@ -13,9 +12,9 @@ import scorex.utils.ScorexLogging import scala.util.Random -class HandshakeHandler() extends ReplayingDecoder[Void] with ScorexLogging { +class HandshakeHandler(chainId: Char) extends ReplayingDecoder[Void] with ScorexLogging { private val handshake = - Handshake(Constants.ApplicationName + Settings.default.chainId, Version.VersionTuple, + Handshake(Constants.ApplicationName + chainId, Version.VersionTuple, "discovery", new Random().nextLong(), None) override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]): Unit = { diff --git a/discovery/src/main/web/data.js b/discovery/src/main/web/data.js index 87c899865d7..5e9d0b08953 100644 --- a/discovery/src/main/web/data.js +++ b/discovery/src/main/web/data.js @@ -3,74 +3,74 @@ Array.prototype.except = function(arr) { return this.filter(x => !arr.includes(x)) } -var peerSet = {} -var peerStats = {} -var peerHops = {} - -function peers(peer) { - if(!peerSet[peer]) - return [] - - return peerSet[peer] -} - -function addPeerWithPeers(peer, peers) { - peerSet[peer] = peers -} - function isEmpty(obj) { for(var key in obj) { - if(obj.hasOwnProperty(key)) - return false; + if(obj.hasOwnProperty(key)) + return false; } return true; } -function calculateHops() { - var result = [] - for(peer in peerSet) { - var byLevel = hopsByLevel(calculatePeerHops(peer, 0, 1, 4)) - byLevel.peer = peer - result.push(byLevel) - } +function PeersInfo() +{ + var peerSet = {} + var peerStats = {} + var peerHops = {} - return result -} + this.peers = function(peer) { + if(!peerSet[peer]) + return [] -function calculatePeerHops(peer, peerHops = {}, depth = 1, max = 1) { + return peerSet[peer] + } - if(depth > max) - return peerHops - if(isEmpty(peerHops)) { - peerHops = {} - peers(peer).forEach(hop => peerHops[hop] = depth) - } else { - for(hop in peerHops) { - if(peerHops[hop] == depth - 1) { - peers(hop).filter(p => peerHops[p] ? false : true).forEach(hop => peerHops[hop] = depth) - } - } - } + this.hopsByLevel = function(peerHops) { + byLevel = {} + for(hop in peerHops) { + var level = peerHops[hop] - return calculatePeerHops(peer, peerHops, depth + 1, max) -} + if(!byLevel[level]) + byLevel[level] = [] -function hopsByLevel(peerHops) { - byLevel = {} - for(hop in peerHops) { - var level = peerHops[hop] + byLevel[level].push(hop) + } - if(!byLevel[level]) - byLevel[level] = [] + return byLevel + } - byLevel[level].push(hop) - } + this.addPeerWithPeers = function(peer, peers) { + peerSet[peer] = peers + } - return byLevel -} + this.calculatePeerHops = function(peer, peerHops = {}, depth = 1, max = 1) { + + if(depth > max) + return peerHops + + if(isEmpty(peerHops)) { + peerHops = {} + this.peers(peer).forEach(hop => peerHops[hop] = depth) + } else { + for(hop in peerHops) { + if(peerHops[hop] == depth - 1) { + this.peers(hop).filter(p => peerHops[p] ? false : true).forEach(hop => peerHops[hop] = depth) + } + } + } + + return this.calculatePeerHops(peer, peerHops, depth + 1, max) + } + + this.calculateHops = function() { + var result = [] + for(peer in peerSet) { + var byLevel = this.hopsByLevel(this.calculatePeerHops(peer, 0, 1, 6)) + byLevel.peer = peer + result.push(byLevel) + } -function firstHopPeers(peer) { - return peers(peer).length + return result + } } diff --git a/discovery/src/main/web/graph.js b/discovery/src/main/web/graph.js index dfd78abed66..4da6e3c1053 100644 --- a/discovery/src/main/web/graph.js +++ b/discovery/src/main/web/graph.js @@ -6,8 +6,6 @@ var nodes = [], lastNodeId = 0, links = []; - - var nodesSet = {} var linksSet = {} @@ -77,23 +75,12 @@ var y = d3.scale.linear() .range([height, 0]); var zoom = d3.behavior.zoom().x(x).y(y) - // only scale up, e.g. between 1x and 50x .scaleExtent([1, 50]) .on("zoom", function() { - // the "zoom" event populates d3.event with an object that has - // a "translate" property (a 2-element Array in the form [x, y]) - // and a numeric "scale" property var e = d3.event, - // now, constrain the x and y components of the translation by the - // dimensions of the viewport tx = Math.min(0, Math.max(e.translate[0], width - width * e.scale)), ty = Math.min(0, Math.max(e.translate[1], height - height * e.scale)); - // then, update the zoom behavior's internal translation, so that - // it knows how to properly manipulate it on the next movement zoom.translate([tx, ty]); - // and finally, update the element's transform attribute with the - // correct translation and scale (in reverse order) - g.attr("transform", [ "translate(" + [tx, ty] + ")", "scale(" + e.scale + ")" @@ -103,15 +90,8 @@ var y = d3.scale.linear() g.selectAll(".link").style("stroke-width", 0.1 / e.scale + "px"); svg.selectAll("text").attr("transform", "scale(" + 1.0 / e.scale + ")"); }); - - // then, call the zoom behavior on the svg element, which will add - // all of the necessary mouse and touch event handlers. - // remember that if you call this on the element, the even handlers - // will only trigger when the mouse or touch cursor intersects with the - // elements' children! svg.call(zoom); - var path = g.selectAll('path'), circle = g.selectAll('g'); var selected_node = null, diff --git a/discovery/src/main/web/index.html b/discovery/src/main/web/index.html index 8a285836778..85ef1fc81cb 100644 --- a/discovery/src/main/web/index.html +++ b/discovery/src/main/web/index.html @@ -2,22 +2,28 @@ - - - -
- -
+ +
+
+
+
+
+
+
+
+
- - @@ -26,8 +32,8 @@ \ No newline at end of file diff --git a/discovery/src/main/web/index.js b/discovery/src/main/web/index.js index d164b464973..59ad13027c5 100644 --- a/discovery/src/main/web/index.js +++ b/discovery/src/main/web/index.js @@ -1,4 +1,3 @@ -var table function print(text) { var p = document.createElement("P") @@ -7,29 +6,37 @@ function print(text) { //window.scrollTo(0,document.body.scrollHeight); } -function WebSocketTest() -{ +function Init(chainId) { + $(document).ready(function() { + var table = $('#' + chainId + 'Table').DataTable({ + paging: false, + columnDefs: columnDefinitions + }); + + Connect(chainId, table, new PeersInfo()) + }); +} + +function Connect(chainId, table, peersInfo) { + if ("WebSocket" in window) { - var ws = new WebSocket("ws://localhost:8080/"); - var wasConnected = false + var ws = new WebSocket("ws://localhost:8080/" + chainId); - ws.onopen = function() - { + ws.onopen = function() { wasConnected = true - print("WebSocket connected...") + print("WebSocket [" + chainId + "] connected...") }; - ws.onmessage = function (evt) - { + ws.onmessage = function (evt) { var received_msg = evt.data var msg = JSON.parse(evt.data); for(var peer in msg) { - addPeerWithPeers(peer, msg[peer]) + peersInfo.addPeerWithPeers(peer, msg[peer]) } - var hops = calculateHops() + var hops = peersInfo.calculateHops() table.clear() @@ -39,30 +46,18 @@ function WebSocketTest() table.draw() - $('.label') - .popup() - ; - - print(received_msg) + print("[" + chainId + "]: " + received_msg) }; - ws.onclose = function() - { - if(!wasConnected) { - WebSocketTest() - } - else { - print("WebSocket closed.") - } + ws.onclose = function() { + Connect(chainId, table, peersInfo) }; window.onbeforeunload = function(event) { socket.close() }; } - - else - { + else { print("WebSocket NOT supported by your Browser!"); } } @@ -76,7 +71,7 @@ var columnDefinitions = [{ } }] -for(var i = 1; i <= 3; i++) { +for(var i = 1; i <= 5; i++) { columnDefinitions.push({ title: "hop#" + i, targets: i, @@ -90,16 +85,7 @@ for(var i = 1; i <= 3; i++) { } -$(document).ready(function() { -table = $('#example').DataTable({ - paging: false, - columnDefs: columnDefinitions - }); - -print("Waiting for host...") -WebSocketTest(); -}); - - - +print("Waiting for hosts...") +Init("w"); +Init("t"); From c88de8d0f6e406cdb31e240025985e92b23eaed3 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Wed, 1 Nov 2017 17:07:01 +0300 Subject: [PATCH 08/14] WIP --- discovery/devnet.conf | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 discovery/devnet.conf diff --git a/discovery/devnet.conf b/discovery/devnet.conf deleted file mode 100644 index 8cc645f16dc..00000000000 --- a/discovery/devnet.conf +++ /dev/null @@ -1,25 +0,0 @@ -generator { - chainId = D - accounts = [ - "EB52Qiw82RE1mtQpVu73d92t6CcpT8FtXGeWigidJCET", - "788h8WCsVet6sMceHT8vn9VqRy3Ms929BwdW24eFp1r7", - "8UhM36mbnGFS2tRWDqtEX2tReVKq6Ww6jyMCekFjqoDQ", - ] - n = 100 - every = 500 ms - send-to { - address = 34.251.200.245 - port = 6864 - } - probabilities { - payment: 0.1 - issue: 0.1 - transfer: 0.3 - reissue: 0.05 - burn: 0.075 - exchange: 0.1 - lease: 0.1 - lease-cancel: 0.075 - create-alias: 0.1 - } -} From 89b653179f5ceceab431fff2204e1ae24318954e Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Wed, 1 Nov 2017 18:46:58 +0300 Subject: [PATCH 09/14] review fixea --- discovery/build.sbt | 2 +- .../DiscoveryApp.scala | 5 +- .../actors/PeerDiscoveryActor.scala | 29 ++++++++- .../network/LegacyFrameCodec.scala | 64 ------------------- .../network/MessageCodec.scala | 9 --- 5 files changed, 30 insertions(+), 79 deletions(-) delete mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/network/LegacyFrameCodec.scala diff --git a/discovery/build.sbt b/discovery/build.sbt index 1c7ea2deafa..9e5f5796e16 100755 --- a/discovery/build.sbt +++ b/discovery/build.sbt @@ -1,3 +1,3 @@ -libraryDependencies += "com.typesafe.akka" % "akka-http-experimental_2.11" % "2.4.11.2" +libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.0.9" libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.8.0" libraryDependencies += "com.typesafe.akka" % "akka-actor_2.10" % "2.2-M1" diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala index a09b75c8c75..c8ac1add444 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala @@ -11,11 +11,12 @@ import akka.stream.{ActorMaterializer, OverflowStrategy} import com.wavesplatform.discovery.actors.MainActor import com.wavesplatform.discovery.actors.MainActor.WebSocketConnected import com.wavesplatform.discovery.CancellableExt._ +import scorex.utils.ScorexLogging import scala.concurrent.duration.FiniteDuration import scala.io.StdIn -object DiscoveryApp extends App { +object DiscoveryApp extends App with ScorexLogging { implicit val system: ActorSystem = ActorSystem("Default") implicit val flowMaterializer: ActorMaterializer = ActorMaterializer() import akka.http.scaladsl.server.Directives._ @@ -45,7 +46,7 @@ object DiscoveryApp extends App { val binding = Http().bindAndHandle(route, Settings.default.webSocketHost, Settings.default.webSocketPort) - println(s"Server is now online at http://${Settings.default.webSocketHost}:${Settings.default.webSocketPort}\nPress RETURN to stop...") + log.info(s"Server is now online at http://${Settings.default.webSocketHost}:${Settings.default.webSocketPort}\nPress RETURN to stop...") StdIn.readLine() binding.flatMap(_.unbind()).onComplete(_ => { timer.cancel() diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala index 5907fffa62b..55e20ee5286 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala @@ -1,12 +1,12 @@ package com.wavesplatform.discovery.actors -import java.net.InetSocketAddress +import java.net.{InetAddress, InetSocketAddress} import java.util.concurrent.TimeUnit import akka.actor.Actor import com.wavesplatform.discovery._ import com.wavesplatform.discovery.network._ -import com.wavesplatform.network.{GetPeers, Handshake, KnownPeers, PipelineInitializer} +import com.wavesplatform.network.{GetPeers, Handshake, KnownPeers, LegacyFrameCodec, PeerDatabase, PipelineInitializer} import io.netty.bootstrap.Bootstrap import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioSocketChannel @@ -17,6 +17,29 @@ import scala.concurrent.duration.FiniteDuration object PeerDiscoveryActor { case class GetPeersFrom(peer: InetSocketAddress) + + val peerDatabaseStub = new PeerDatabase {override def suspend(host: InetAddress): Unit = {} + + override def knownPeers: Map[InetSocketAddress, Long] = Map.empty + + override def randomPeer(excluded: Set[InetSocketAddress]): Option[InetSocketAddress] = None + + override def blacklist(host: InetAddress, reason: String): Unit = {} + + override def touch(socketAddress: InetSocketAddress): Unit = {} + + override def suspendedHosts: Set[InetAddress] = Set.empty + + override def blacklistedHosts: Set[InetAddress] = Set.empty + + override def detailedBlacklist: Map[InetAddress, (Long, String)] = Map.empty + + override def clearBlacklist(): Unit = {} + + override def detailedSuspended: Map[InetAddress, Long] = Map.empty + + override def addCandidate(socketAddress: InetSocketAddress): Unit = {} + } } class PeerDiscoveryActor(chainId: Char) extends Actor { @@ -40,7 +63,7 @@ class PeerDiscoveryActor(chainId: Char) extends Actor { new HandshakeHandler(chainId), new LengthFieldPrepender(4), new LengthFieldBasedFrameDecoder(100 * 1024 * 1024, 0, 4, 0, 4), - new LegacyFrameCodec(), + new LegacyFrameCodec(peerDatabaseStub), new MessageCodec(), new MessageHandler({ case (msg, ctx) => msg match { diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/network/LegacyFrameCodec.scala b/discovery/src/main/scala/com.wavesplatform.discovery/network/LegacyFrameCodec.scala deleted file mode 100644 index ff7fa12c64c..00000000000 --- a/discovery/src/main/scala/com.wavesplatform.discovery/network/LegacyFrameCodec.scala +++ /dev/null @@ -1,64 +0,0 @@ -package com.wavesplatform.discovery.network - -import java.util - -import com.wavesplatform.network.{BasicMessagesRepo, RawBytes, id} -import io.netty.buffer.ByteBuf -import io.netty.buffer.Unpooled.wrappedBuffer -import io.netty.channel.ChannelHandlerContext -import io.netty.handler.codec._ -import scorex.crypto.hash.FastCryptographicHash -import scorex.network.message.Message.ChecksumLength -import scorex.network.message.MessageSpec -import scorex.utils.ScorexLogging - -import scala.util.control.NonFatal - -object LegacyFrameCodec { - val Magic = 0x12345678 - - private val messageSpecs: Map[Byte, MessageSpec[_ <: AnyRef]] = - BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap -} - -class LegacyFrameCodec() extends ByteToMessageCodec[RawBytes] with ScorexLogging { - import LegacyFrameCodec._ - - override def decode(ctx: ChannelHandlerContext, in: ByteBuf, out: util.List[AnyRef]) = try { - require(in.readInt() == Magic, "invalid magic number") - - val code = in.readByte() - require(messageSpecs.contains(code), s"Unexpected message code $code") - - val spec = messageSpecs(code) - val length = in.readInt() - require(length <= spec.maxLength, s"${spec.messageName} length $length exceeds ${spec.maxLength}") - - val dataBytes = new Array[Byte](length) - if (length > 0) { - val declaredChecksum = in.readSlice(ChecksumLength) - in.readBytes(dataBytes) - val actualChecksum = wrappedBuffer(FastCryptographicHash.hash(dataBytes), 0, ChecksumLength) - - require(declaredChecksum.equals(actualChecksum), "invalid checksum") - actualChecksum.release() - } - - out.add(RawBytes(code, dataBytes)) - } catch { - case NonFatal(e) => - log.warn(s"${id(ctx)} Malformed network message", e) - } - - override def encode(ctx: ChannelHandlerContext, msg: RawBytes, out: ByteBuf) = { - out.writeInt(Magic) - out.writeByte(msg.code) - if (msg.data.length > 0) { - out.writeInt(msg.data.length) - out.writeBytes(FastCryptographicHash.hash(msg.data), 0, ChecksumLength) - out.writeBytes(msg.data) - } else { - out.writeInt(0) - } - } -} \ No newline at end of file diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala b/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala index 09324d6161a..b3b5d1b6721 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala @@ -17,16 +17,7 @@ class MessageCodec() extends MessageToMessageCodec[RawBytes, Message] with Score private val specs: Map[Byte, MessageSpec[_ <: AnyRef]] = BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap override def encode(ctx: ChannelHandlerContext, msg: Message, out: util.List[AnyRef]) = msg match { - case LocalScoreChanged(score) => out.add(RawBytes(ScoreMessageSpec.messageCode, ScoreMessageSpec.serializeData(score))) case GetPeers => out.add(RawBytes(GetPeersSpec.messageCode, Array[Byte]())) - case k: KnownPeers => out.add(RawBytes(PeersSpec.messageCode, PeersSpec.serializeData(k))) - case gs: GetSignatures => out.add(RawBytes(GetSignaturesSpec.messageCode, GetSignaturesSpec.serializeData(gs))) - case s: Signatures => out.add(RawBytes(SignaturesSpec.messageCode, SignaturesSpec.serializeData(s))) - case g: GetBlock => out.add(RawBytes(GetBlockSpec.messageCode, GetBlockSpec.serializeData(g))) - case BlockForged(b) => out.add(RawBytes(BlockMessageSpec.messageCode, b.bytes)) - case m: MicroBlockInv => out.add(RawBytes(MicroBlockInvMessageSpec.messageCode, MicroBlockInvMessageSpec.serializeData(m))) - case m: MicroBlockRequest => out.add(RawBytes(MicroBlockRequestMessageSpec.messageCode, MicroBlockRequestMessageSpec.serializeData(m))) - case m: MicroBlockResponse => out.add(RawBytes(MicroBlockResponseMessageSpec.messageCode, MicroBlockResponseMessageSpec.serializeData(m))) case r: RawBytes => out.add(r) } From d7e5fb535bac3925f7d84be70e5cfc726a9cf7d6 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Thu, 2 Nov 2017 15:21:01 +0300 Subject: [PATCH 10/14] review fixes --- .../com.wavesplatform.discovery/DiscoveryApp.scala | 4 +++- .../actors/MainActor.scala | 4 +++- .../actors/PeerDiscoveryActor.scala | 9 +++++++-- .../network/MessageCodec.scala | 5 +++-- .../scala/com.wavesplatform.discovery/package.scala | 12 ------------ 5 files changed, 16 insertions(+), 18 deletions(-) delete mode 100644 discovery/src/main/scala/com.wavesplatform.discovery/package.scala diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala index c8ac1add444..1df45154a90 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala @@ -13,10 +13,13 @@ import com.wavesplatform.discovery.actors.MainActor.WebSocketConnected import com.wavesplatform.discovery.CancellableExt._ import scorex.utils.ScorexLogging +import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration import scala.io.StdIn object DiscoveryApp extends App with ScorexLogging { + + implicit val ec: ExecutionContext = ExecutionContext.global implicit val system: ActorSystem = ActorSystem("Default") implicit val flowMaterializer: ActorMaterializer = ActorMaterializer() import akka.http.scaladsl.server.Directives._ @@ -51,6 +54,5 @@ object DiscoveryApp extends App with ScorexLogging { binding.flatMap(_.unbind()).onComplete(_ => { timer.cancel() system.terminate() - workerGroup.shutdownGracefully() }) } diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala b/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala index 7ee0724bf49..9349563c9c1 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/actors/MainActor.scala @@ -12,14 +12,16 @@ import play.api.libs.json._ class MainActor(chainId: Char, workersCount: Int) extends Actor { import MainActor._ + private val mailboxThreshold = 5 private val router = { val routes = Vector.fill(workersCount) { ActorRefRoutee(context.actorOf(Props(classOf[PeerDiscoveryActor], chainId))) } - Router(SmallestMailboxWithThresholdRoutingLogic(5), routes) + Router(SmallestMailboxWithThresholdRoutingLogic(mailboxThreshold), routes) } private val alivePeers = new Pool[InetSocketAddress] + private val deadPeersCacheTimeout = 5 private val deadPeers = new ExpirationSet[InetSocketAddress](1000*60*60*1) private val peerResponses = scala.collection.mutable.Map.empty[InetSocketAddress, Set[InetSocketAddress]] private val connections = scala.collection.mutable.Set.empty[ActorRef] diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala index 55e20ee5286..5786087581b 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/actors/PeerDiscoveryActor.scala @@ -8,6 +8,7 @@ import com.wavesplatform.discovery._ import com.wavesplatform.discovery.network._ import com.wavesplatform.network.{GetPeers, Handshake, KnownPeers, LegacyFrameCodec, PeerDatabase, PipelineInitializer} import io.netty.bootstrap.Bootstrap +import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioSocketChannel import io.netty.handler.codec.{LengthFieldBasedFrameDecoder, LengthFieldPrepender} @@ -50,11 +51,15 @@ class PeerDiscoveryActor(chainId: Char) extends Actor { case GetPeersFrom(peer) => context.parent ! MainActor.PeerInfo(peer, getPeersFromNode(peer)) } + private val getPeersTimeout = 10 + private def getPeersFromNode(address: InetSocketAddress): Set[InetSocketAddress]= { var peers: Set[InetSocketAddress] = Set.empty val exceptionHandler = new ExceptionHandler() + implicit val workerGroup: NioEventLoopGroup = new NioEventLoopGroup + new Bootstrap() .group(workerGroup) .channel(classOf[NioSocketChannel]) @@ -76,8 +81,8 @@ class PeerDiscoveryActor(chainId: Char) extends Actor { .remoteAddress(address.getAddress, address.getPort) .connect() - Await.result(exceptionHandler.closed, new FiniteDuration(10, TimeUnit.SECONDS)) - + Await.result(exceptionHandler.closed, new FiniteDuration(getPeersTimeout, TimeUnit.SECONDS)) + workerGroup.shutdownGracefully() peers } diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala b/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala index b3b5d1b6721..c3b3dd47425 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/network/MessageCodec.scala @@ -16,15 +16,16 @@ class MessageCodec() extends MessageToMessageCodec[RawBytes, Message] with Score private val specs: Map[Byte, MessageSpec[_ <: AnyRef]] = BasicMessagesRepo.specs.map(s => s.messageCode -> s).toMap - override def encode(ctx: ChannelHandlerContext, msg: Message, out: util.List[AnyRef]) = msg match { + override def encode(ctx: ChannelHandlerContext, msg: Message, out: util.List[AnyRef]): Unit = msg match { case GetPeers => out.add(RawBytes(GetPeersSpec.messageCode, Array[Byte]())) case r: RawBytes => out.add(r) + case _ => } override def decode(ctx: ChannelHandlerContext, msg: RawBytes, out: util.List[AnyRef]): Unit = { specs(msg.code).deserializeData(msg.data) match { case Success(x) => out.add(x) - case Failure(e) => println(e.getMessage) + case Failure(e) => log.error(e.getMessage) } } } diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/package.scala b/discovery/src/main/scala/com.wavesplatform.discovery/package.scala deleted file mode 100644 index 7dbcacb478f..00000000000 --- a/discovery/src/main/scala/com.wavesplatform.discovery/package.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.wavesplatform - -import io.netty.channel.nio.NioEventLoopGroup - -import scala.concurrent.ExecutionContext - -package object discovery { - - implicit val workerGroup = new NioEventLoopGroup - implicit val ec: ExecutionContext = ExecutionContext.global - -} From 80d67eb299801e29738a6b8e92364c8d3397fbe3 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Tue, 7 Nov 2017 16:09:09 +0300 Subject: [PATCH 11/14] unused dependencies removed --- discovery/build.sbt | 3 --- 1 file changed, 3 deletions(-) delete mode 100755 discovery/build.sbt diff --git a/discovery/build.sbt b/discovery/build.sbt deleted file mode 100755 index 9e5f5796e16..00000000000 --- a/discovery/build.sbt +++ /dev/null @@ -1,3 +0,0 @@ -libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.0.9" -libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.8.0" -libraryDependencies += "com.typesafe.akka" % "akka-actor_2.10" % "2.2-M1" From a45ef68aedfa62d5a31fd66a281918e6e67b9ebf Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Tue, 7 Nov 2017 16:24:12 +0300 Subject: [PATCH 12/14] expiration set is now guava wrapper on cache --- .../collections/ExpirationSet.scala | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala b/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala index 4af8ffba9b7..6488cbf66c4 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala @@ -1,26 +1,26 @@ package com.wavesplatform.discovery.collections -class ExpirationSet[T](expirationTimeMilis: Long) extends scala.collection.mutable.Set[T]{ - private var inner = Map.empty[T, Long] - private def freshInner = { - val time = System.currentTimeMillis() - inner = inner.filter({ case (k, v) => - time - v > expirationTimeMilis }) +import java.util.concurrent.TimeUnit +import scala.collection.JavaConverters._ - inner - } +import com.google.common.cache.CacheBuilder + +class ExpirationSet[T](expirationTimeMilis: Long) extends scala.collection.mutable.Set[T]{ + private var inner = CacheBuilder.newBuilder() + .expireAfterWrite(expirationTimeMilis, TimeUnit.MILLISECONDS) + .build[T, Boolean]() override def +=(elem: T): ExpirationSet.this.type = { - inner += ((elem, System.currentTimeMillis())) + inner.put(elem, true) this } override def -=(elem: T): ExpirationSet.this.type = { - inner = inner.-(elem) + inner.invalidate(elem) this } - override def contains(elem: T): Boolean = freshInner.contains(elem) + override def contains(elem: T): Boolean = inner.asMap().containsKey(elem) - override def iterator: Iterator[T] = freshInner.keys.iterator + override def iterator: Iterator[T] = inner.asMap().keySet().iterator().asScala } From f04c98694e61b55945050942370745e504157839 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Tue, 7 Nov 2017 16:56:33 +0300 Subject: [PATCH 13/14] shu --- .../DiscoveryApp.scala | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala index 1df45154a90..8f2998b9391 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/DiscoveryApp.scala @@ -15,7 +15,6 @@ import scorex.utils.ScorexLogging import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration -import scala.io.StdIn object DiscoveryApp extends App with ScorexLogging { @@ -49,10 +48,12 @@ object DiscoveryApp extends App with ScorexLogging { val binding = Http().bindAndHandle(route, Settings.default.webSocketHost, Settings.default.webSocketPort) - log.info(s"Server is now online at http://${Settings.default.webSocketHost}:${Settings.default.webSocketPort}\nPress RETURN to stop...") - StdIn.readLine() - binding.flatMap(_.unbind()).onComplete(_ => { - timer.cancel() - system.terminate() - }) + sys.addShutdownHook { + binding.flatMap(_.unbind()).onComplete(_ => { + timer.cancel() + system.terminate() + }) + } + + log.info(s"Server is now online at http://${Settings.default.webSocketHost}:${Settings.default.webSocketPort}") } From d495b9505e40175cf47a679352f3e0fc24617999 Mon Sep 17 00:00:00 2001 From: Yuriy Naydenov Date: Tue, 7 Nov 2017 17:10:26 +0300 Subject: [PATCH 14/14] shutdown + set fixes --- .../collections/ExpirationSet.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala b/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala index 6488cbf66c4..915b3c2815e 100644 --- a/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala +++ b/discovery/src/main/scala/com.wavesplatform.discovery/collections/ExpirationSet.scala @@ -5,13 +5,15 @@ import scala.collection.JavaConverters._ import com.google.common.cache.CacheBuilder -class ExpirationSet[T](expirationTimeMilis: Long) extends scala.collection.mutable.Set[T]{ +class ExpirationSet[T <: Object](expirationTimeMilis: Long) extends scala.collection.mutable.Set[T]{ + private val emptyItem = new Object() + private var inner = CacheBuilder.newBuilder() .expireAfterWrite(expirationTimeMilis, TimeUnit.MILLISECONDS) - .build[T, Boolean]() + .build[T, Object]() override def +=(elem: T): ExpirationSet.this.type = { - inner.put(elem, true) + inner.put(elem, emptyItem) this }