diff --git a/akka-docs/src/main/paradox/cluster-client.md b/akka-docs/src/main/paradox/cluster-client.md index 4816380077..188eded6aa 100644 --- a/akka-docs/src/main/paradox/cluster-client.md +++ b/akka-docs/src/main/paradox/cluster-client.md @@ -1,9 +1,9 @@ # Cluster Client An actor system that is not part of the cluster can communicate with actors -somewhere in the cluster via this @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)]. The client can of course be part of +somewhere in the cluster via this @unidoc[ClusterClient]. The client can of course be part of another cluster. It only needs to know the location of one (or more) nodes to use as initial -contact points. It will establish a connection to a @scala[@scaladoc[`ClusterReceptionist`](akka.cluster.client.ClusterReceptionist)]@java[@javadoc[`ClusterReceptionist`](akka.cluster.client.ClusterReceptionist)] somewhere in +contact points. It will establish a connection to a @unidoc[akka.cluster.client.ClusterReceptionist] somewhere in the cluster. It will monitor the connection to the receptionist and establish a new connection if the link goes down. When looking for a new receptionist it uses fresh contact points retrieved from previous establishment, or periodically refreshed contacts, @@ -11,8 +11,8 @@ i.e. not necessarily the initial contact points. @@@ note -@scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] should not be used when sending messages to actors that run -within the same cluster. Similar functionality as the @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] is +@unidoc[ClusterClient] should not be used when sending messages to actors that run +within the same cluster. Similar functionality as the @unidoc[ClusterClient] is provided in a more efficient way by @ref:[Distributed Publish Subscribe in Cluster](distributed-pub-sub.md) for actors that belong to the same cluster. @@ -23,23 +23,23 @@ to `remote` or `cluster` when using the cluster client. The receptionist is supposed to be started on all nodes, or all nodes with specified role, -in the cluster. The receptionist can be started with the @scala[@scaladoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]@java[@javadoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)] extension +in the cluster. The receptionist can be started with the @unidoc[akka.cluster.client.ClusterReceptionist] extension or as an ordinary actor. -You can send messages via the @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] to any actor in the cluster that is registered -in the @scala[@scaladoc[`DistributedPubSubMediator`](akka.cluster.pubsub.DistributedPubSubMediator)]@java[@javadoc[`DistributedPubSubMediator`](akka.cluster.pubsub.DistributedPubSubMediator)] used by the @scala[@scaladoc[`ClusterReceptionist`](akka.cluster.client.ClusterReceptionist)]@java[@javadoc[`ClusterReceptionist`](akka.cluster.client.ClusterReceptionist)]. -The @scala[@scaladoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]@java[@javadoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)] provides methods for registration of actors that +You can send messages via the @unidoc[ClusterClient] to any actor in the cluster that is registered +in the @unidoc[DistributedPubSubMediator] used by the @unidoc[akka.cluster.client.ClusterReceptionist]. +The @unidoc[ClusterClientReceptionist] provides methods for registration of actors that should be reachable from the client. Messages are wrapped in `ClusterClient.Send`, @scala[@scaladoc[`ClusterClient.SendToAll`](akka.cluster.client.ClusterClient$)]@java[`ClusterClient.SendToAll`] or @scala[@scaladoc[`ClusterClient.Publish`](akka.cluster.client.ClusterClient$)]@java[`ClusterClient.Publish`]. -Both the @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] and the @scala[@scaladoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClient)] emit events that can be subscribed to. -The @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] sends out notifications in relation to having received a list of contact points -from the @scala[@scaladoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]@java[@javadoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]. One use of this list might be for the client to record its +Both the @unidoc[ClusterClient] and the @unidoc[ClusterClientReceptionist] emit events that can be subscribed to. +The @unidoc[ClusterClient] sends out notifications in relation to having received a list of contact points +from the @unidoc[ClusterClientReceptionist]. One use of this list might be for the client to record its contact points. A client that is restarted could then use this information to supersede any previously configured contact points. -The @scala[@scaladoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]@java[@javadoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)] sends out notifications in relation to having received a contact -from a @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)]. This notification enables the server containing the receptionist to become aware of +The @unidoc[ClusterClientReceptionist] sends out notifications in relation to having received a contact +from a @unidoc[ClusterClient]. This notification enables the server containing the receptionist to become aware of what clients are connected. 1. **ClusterClient.Send** @@ -68,13 +68,13 @@ to avoid inbound connections from other cluster nodes to the client: * @scala[@scaladoc[`sender()`](akka.actor.Actor)] @java[@javadoc[`getSender()`](akka.actor.Actor)] of the response messages, sent back from the destination and seen by the client, is `deadLetters` -since the client should normally send subsequent messages via the @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)]. +since the client should normally send subsequent messages via the @unidoc[ClusterClient]. It is possible to pass the original sender inside the reply messages if the client is supposed to communicate directly to the actor in the cluster. -While establishing a connection to a receptionist the @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] will buffer +While establishing a connection to a receptionist the @unidoc[ClusterClient] will buffer messages and send them when the connection is established. If the buffer is full -the @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] will drop old messages when new messages are sent via the client. +the @unidoc[ClusterClient] will drop old messages when new messages are sent via the client. The size of the buffer is configurable and it can be disabled by using a buffer size of 0. It's worth noting that messages can always be lost because of the distributed nature @@ -98,7 +98,7 @@ Scala Java : @@snip [ClusterClientTest.java]($akka$/akka-cluster-tools/src/test/java/akka/cluster/client/ClusterClientTest.java) { #server } -On the client you create the @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] actor and use it as a gateway for sending +On the client you create the @unidoc[ClusterClient] actor and use it as a gateway for sending messages to the actors identified by their path (without address information) somewhere in the cluster. @@ -130,7 +130,7 @@ That is convenient and perfectly fine in most cases, but it can be good to know start the `akka.cluster.client.ClusterReceptionist` actor as an ordinary actor and you can have several different receptionists at the same time, serving different types of clients. -Note that the @scala[@scaladoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]@java[@javadoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)] uses the @scala[@scaladoc[`DistributedPubSub`](akka.cluster.pubsub.DistributedPubSub)]@java[@javadoc[`DistributedPubSub`](akka.cluster.pubsub.DistributedPubSub)] extension, which is described +Note that the @unidoc[ClusterClientReceptionist] uses the @unidoc[DistributedPubSub] extension, which is described in @ref:[Distributed Publish Subscribe in Cluster](distributed-pub-sub.md). It is recommended to load the extension when the actor system is started by defining it in the @@ -142,9 +142,9 @@ akka.extensions = ["akka.cluster.client.ClusterClientReceptionist"] ## Events -As mentioned earlier, both the @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] and @scala[@scaladoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]@java[@javadoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)] emit events that can be subscribed to. +As mentioned earlier, both the @unidoc[ClusterClient] and @unidoc[ClusterClientReceptionist] emit events that can be subscribed to. The following code snippet declares an actor that will receive notifications on contact points (addresses to the available -receptionists), as they become available. The code illustrates subscribing to the events and receiving the @scala[@scaladoc[`ClusterClient`](akka.cluster.client.ClusterClient)]@java[@javadoc[`ClusterClient`](akka.cluster.client.ClusterClient)] +receptionists), as they become available. The code illustrates subscribing to the events and receiving the @unidoc[ClusterClient] initial state. Scala @@ -153,7 +153,7 @@ Scala Java : @@snip [ClusterClientTest.java]($akka$/akka-cluster-tools/src/test/java/akka/cluster/client/ClusterClientTest.java) { #clientEventsListener } -Similarly we can have an actor that behaves in a similar fashion for learning what cluster clients are connected to a @scala[@scaladoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]@java[@javadoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]: +Similarly we can have an actor that behaves in a similar fashion for learning what cluster clients are connected to a @unidoc[ClusterClientReceptionist]: Scala : @@snip [ClusterClientSpec.scala]($akka$/akka-cluster-tools/src/multi-jvm/scala/akka/cluster/client/ClusterClientSpec.scala) { #receptionistEventsListener } @@ -186,14 +186,14 @@ Maven ## Configuration -The @scala[@scaladoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)]@java[@javadoc[`ClusterClientReceptionist`](akka.cluster.client.ClusterClientReceptionist)] extension (or @scala[@scaladoc[`ClusterReceptionistSettings`](akka.cluster.client.ClusterReceptionistSettings)]@java[@javadoc[`ClusterReceptionistSettings`](akka.cluster.client.ClusterReceptionistSettings)]) can be configured +The @unidoc[ClusterClientReceptionist] extension (or @unidoc[ClusterReceptionistSettings]) can be configured with the following properties: @@snip [reference.conf]($akka$/akka-cluster-tools/src/main/resources/reference.conf) { #receptionist-ext-config } -The following configuration properties are read by the @scala[@scaladoc[`ClusterClientSettings`](akka.cluster.client.ClusterClientSettings)]@java[@javadoc[`ClusterClientSettings`](akka.cluster.client.ClusterClientSettings)] -when created with a @scala[@scaladoc[`ActorSystem`](akka.actor.ActorSystem)]@java[@javadoc[`ActorSystem`](akka.actor.ActorSystem)] parameter. It is also possible to amend the @scala[@scaladoc[`ClusterClientSettings`](akka.cluster.client.ClusterClientSettings)]@java[@javadoc[`ClusterClientSettings`](akka.cluster.client.ClusterClientSettings)] -or create it from another config section with the same layout as below. @scala[@scaladoc[`ClusterClientSettings`](akka.cluster.client.ClusterClientSettings)]@java[@javadoc[`ClusterClientSettings`](akka.cluster.client.ClusterClientSettings)] is +The following configuration properties are read by the @unidoc[ClusterClientSettings] +when created with a @scala[@scaladoc[`ActorSystem`](akka.actor.ActorSystem)]@java[@javadoc[`ActorSystem`](akka.actor.ActorSystem)] parameter. It is also possible to amend the @unidoc[ClusterClientSettings] +or create it from another config section with the same layout as below. @unidoc[ClusterClientSettings] is a parameter to the @scala[@scaladoc[`ClusterClient.props`](akka.cluster.client.ClusterClient$)]@java[@javadoc[`ClusterClient.props`](akka.cluster.client.ClusterClient$)] factory method, i.e. each client can be configured with different settings if needed. diff --git a/build.sbt b/build.sbt index 3b3024ebc4..c8123c837a 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -import akka.AutomaticModuleName +import akka.{ParadoxSupport, AutomaticModuleName} enablePlugins(UnidocRoot, TimeStampede, UnidocWithPrValidation, NoPublish) disablePlugins(MimaPlugin) @@ -231,6 +231,7 @@ lazy val docs = akkaModule("akka-docs") deployRsyncArtifact := List((paradox in Compile).value -> s"www/docs/akka/${version.value}") ) .enablePlugins(AkkaParadoxPlugin, DeployRsync, NoPublish, ParadoxBrowse, ScaladocNoVerificationOfDiagrams) + .settings(ParadoxSupport.paradoxWithCustomDirectives) .disablePlugins(MimaPlugin, WhiteSourcePlugin) lazy val multiNodeTestkit = akkaModule("akka-multi-node-testkit") diff --git a/project/ParadoxSupport.scala b/project/ParadoxSupport.scala new file mode 100644 index 0000000000..f23ece217f --- /dev/null +++ b/project/ParadoxSupport.scala @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016-2018 Lightbend Inc. + */ + +package akka + +import _root_.io.github.lukehutch.fastclasspathscanner.FastClasspathScanner +import com.lightbend.paradox.markdown._ +import com.lightbend.paradox.sbt.ParadoxPlugin.autoImport._ +import org.pegdown.Printer +import org.pegdown.ast._ +import sbt.Keys._ +import sbt._ + +import scala.collection.JavaConverters._ + +object ParadoxSupport { + val paradoxWithCustomDirectives = Seq( + paradoxDirectives ++= Def.taskDyn { + val classpath = (fullClasspath in Compile).value.files.map(_.toURI.toURL).toArray + val classloader = new java.net.URLClassLoader(classpath, this.getClass().getClassLoader()) + lazy val scanner = new FastClasspathScanner("akka").addClassLoader(classloader).scan() + val allClasses = scanner.getNamesOfAllClasses.asScala.toVector + Def.task { Seq( + { _: Writer.Context ⇒ new UnidocDirective(allClasses) } + )} + }.value + ) + + class UnidocDirective(allClasses: IndexedSeq[String]) extends InlineDirective("unidoc") { + def render(node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = { + if (node.label.split('[')(0).contains('.')) { + val fqcn = node.label + if (allClasses.contains(fqcn)) { + val label = fqcn.split('.').last + syntheticNode("java", javaLabel(label), fqcn, node).accept(visitor) + syntheticNode("scala", label, fqcn, node).accept(visitor) + } else { + throw new java.lang.IllegalStateException(s"fqcn not found by @unidoc[$fqcn]") + } + } + else { + renderByClassName(node.label, node, visitor, printer) + } + } + + def javaLabel(label: String): String = + label.replaceAll("\\[", "<").replaceAll("\\]", ">").replace('_', '?') + + def syntheticNode(group: String, label: String, fqcn: String, node: DirectiveNode): DirectiveNode = { + val syntheticSource = new DirectiveNode.Source.Direct(fqcn) + val attributes = new org.pegdown.ast.DirectiveAttributes.AttributeMap() + new DirectiveNode(DirectiveNode.Format.Inline, group, null, null, attributes, null, + new DirectiveNode(DirectiveNode.Format.Inline, group + "doc", label, syntheticSource, node.attributes, fqcn, + new TextNode(label) + )) + } + + def renderByClassName(label: String, node: DirectiveNode, visitor: Visitor, printer: Printer): Unit = { + val label = node.label.replaceAll("\\\\_", "_") + val labelWithoutGenericParameters = label.split("\\[")(0) + val labelWithJavaGenerics = javaLabel(label) + val matches = allClasses.filter(_.endsWith('.' + labelWithoutGenericParameters)) + matches.size match { + case 0 => + throw new java.lang.IllegalStateException(s"No matches found for $label") + case 1 if matches(0).contains("adsl") => + throw new java.lang.IllegalStateException(s"Match for $label only found in one language: ${matches(0)}") + case 1 => + syntheticNode("scala", label, matches(0), node).accept(visitor) + syntheticNode("java", labelWithJavaGenerics, matches(0), node).accept(visitor) + case 2 if matches.forall(_.contains("adsl")) => + matches.foreach(m => { + if (!m.contains("javadsl")) + syntheticNode("scala", label, m, node).accept(visitor) + if (!m.contains("scaladsl")) + syntheticNode("java", labelWithJavaGenerics, m, node).accept(visitor) + }) + case n => + throw new java.lang.IllegalStateException( + s"$n matches found for @unidoc[$label], but not javadsl/scaladsl: ${matches.mkString(", ")}. " + + s"You may want to use the fully qualified class name as @unidoc[fqcn] instead of @unidoc[${label}]." + ) + } + } + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 03c5d755cc..8ac29d6a38 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -22,3 +22,6 @@ addSbtPlugin("com.lightbend.akka" % "sbt-paradox-akka" % "0.6") addSbtPlugin("com.lightbend" % "sbt-whitesource" % "0.1.10") addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.9.3") addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.0") // for advanced PR validation features + +// used for @unidoc directive +libraryDependencies += "io.github.lukehutch" % "fast-classpath-scanner" % "2.12.3" \ No newline at end of file