Remotely

Last updated on March 09, 2016


  1. Home
  2. Manual
  3. Internals

Introduction

Remotely is an elegant, reasonable, purely functional remoting system. Remotely is fast, lightweight and models network operations as explicit monadic computations. Remotely is ideally suited for:

NB: Remotely is currently an experimental project under active development. Feedback and contributions are welcomed as we look to improve the project.

Rationale

Before talking about how to use Remotely, it's worth discussing why we made this project in the first place. For large distributed service platforms there is typically a large degree of inter-service communication. In this scenario, several factors become really important:

Getting Started

Using remotely is straightforward, and getting started on a new project could not be simpler!

Dependency Information

Remotely has the following dependencies:

This is an important factor to keep in mind, as if you have clashing versions of these libraries on your classpath you will encounter strange runtime failures due to binary incompatibility.

Project & Build Setup

Typically you want to separate your wire protocol from your core domain logic (this is true even in HTTP applications of course). With remotely this matters because the protocol definition is designed to be depended on by callers, so they can achieve compile-time failures if a particular service breaks its API in an incompatible fashion. With this in mind, the following layout is advised to ensure the interface JAR is also published:

.
├── CHANGELOG
├── README.md
├── core
│   ├── build.sbt
│   └── src
│       ├── main
│       └── test
├── project
│   ├── build.properties
│   └── plugins.sbt
├── project.sbt
├── rpc
│   ├── build.sbt
│   └── src
│       └── main
├── rpc-protocol
│   ├── build.sbt
│   └── src
│       └── main
└── version.sbt

The structure breaks down like this:

Once you have the layout configured, using remotely is just like using any other library within SBT; simply add the dependency to your rpc-protocol module:

resolvers += Resolver.bintrayRepo("oncue", "releases")

libraryDependencies += "oncue.remotely" %% "core" % "x.x.+"

Protocol Definition

The first thing that Remotely needs is to define a "protocol". A protocol is essentially a definition of the runtime contracts this server should enforce on callers. Consider this example from rpc-protocol/src/main/scala/protocol.scala:

scala> import remotely._, codecs._
import remotely._
import codecs._

scala> object protocol {
     |   val definition = Protocol.empty
     |     .codec[Int]
     |     .specify1[Int, Int]("factorial")
     | }
defined module protocol

Protocols are the core of the remotely project. They represent the contract between the client and the server, and then define all the plumbing constrains needed to make that service work properly. The protocol object supports the following operations:

Server & Client Definition

Remotely makes use of compile-time macros to build out interfaces for server implementations and client objects. Because of limitations of the scala macro system, the use of the macros must be in a separate compilation unit (separate project or sub-project) then your protocol implementation. Now that your your service Protocol is defined in the rpc-protocol module, You can make a dependent rpc module which can access that protocol as a total value, enabling us to generate servers and clients. This rpc module will need to include the Macro Paradise compiler plugin, which you can do by adding the following to the build definition for the rpc project:

addCompilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full)

Unfortunately if you forget this step, compilation of the rpc module will succeed, however the macro will not run. Now we are ready to generate a remotely server. Here's an example:

package oncue.svc.example

import remotely._

// NB: The GenServer macro needs to receive the FQN of all types, or import them
// explicitly. The target of the macro needs to be an abstract class.
@GenServer(oncue.svc.example.protocol.definition)
abstract class FactorialServer

class FactorialServer0 extends FactorialServer {
  val factorial: Int => Response[Int] = n =>
    Response.now { (1 to n).product }
}

The GenServer macro does require all the FQCN of all the inputs, so here we must use oncue.svc.example.protocol.definition. For example, using just protocol.definition after import oncue.svc.example._ would not work, as in that case, the macro would then only see the AST: protocol.definition which doesn't have a value in the compiler context, but provided you organize your project in the manner described earlier in this document.

In a similar fashion, clients are also very simple. The difference here is that clients are fully complete, and do not require any implementation as the function arguments defined in the protocol are entirely known at compile time.

package oncue.svc.example

import remotely._

// The `GenClient` macro needs to receive the FQN of all types, or import them
// explicitly. The target needs to be an object declaration.
@GenClient(oncue.svc.example.protocol.definition.signatures)
object FactorialClient

That's all that is needed to define both a client and a server.

Putting it Together

With everything defined, you can now make choices about how best to wire all the things together. For this getting started guide we'll focus on a simple implementation, but the detailed documentation on this site covers lots more information on endpoints, circuit breakers, monitoring, TLS configuration etc.

Here's the main for the server side:

package oncue.svc.example

import java.net.InetSocketAddress
import remotely._, codecs._

object Main {

  def main(args: Array[String]): Unit = {

    val address  = new InetSocketAddress("localhost", 8080)

    val service = new FactorialServer0

    val env = service.environment

    val startServer = env.serve(address)

    val shutdown = startServer.run

  }
}

This is super straightforward, but lets step through the values one by one.

The client on the other hand is similar:

package oncue.svc.example

import scalaz.concurrent.Task
import java.net.InetSocketAddress
import remotely._, codecs._, transport.netty._

object Main {
  import Remote.implicits._

  def main(args: Array[String]): Unit = {

    val address  = new InetSocketAddress("localhost", 8080)

    val transport = NettyTransport.single(address)

    val endpoint = Endpoint.single(transport).run

    val f: Remote[Int] = FactorialClient.reduce(2 :: 4 :: 8 :: Nil)

    val task: Task[Int] = f.runWithoutContext(endpoint)

    // then at the edge of the world, run it and print to the console
    task.map(println(_)).runAsync(_ => ())
  }
}

Whilst address is the same value from the server, the typical case here of course is that the server and the client are not in the same source file so I've repeated it here for completeness. Let's explore the other values:

Finally, the function does network I/O to talk to the server when the Task is executed (using the runAsync method here). You can learn more about the Task monad in this blog post.