Remotely

Last updated on March 09, 2016


  1. Home
  2. Manual
  3. Internals

Manual

Remotely is a sophisticated but simple system. There are a few core concepts and then a set of associated details. Some of these details are important to know, and others might just catch your interest.

Codecs

One of the most important elements of Remotely is its fast, lightweight serialisation system built atop Scodec. This section reviews the default behavior that ships with Remotely and then explains how to extend these with your own custom codecs if needed.

By default, Remotely ships with the ability to serialise / deserialise the following "primitive" types:

Often these built-in defaults will be all you need, but there might be times where it feels like it would be more appropriate do provide a "wire type" (that is, a datatype that represents the external wire API - NOT a data type that forms part of your core domain model). Typically this happens when you have a convoluted structure or a very "stringly-typed" interface (e.g. Map[String, Map[String, Int]] - who knows what on earth the author intended here!). In this cases, implementing custom codecs for your protocol seems attractive, and fortunately its really simple to do:

package oncue.example

import remotely.codecs._
import scodec.{Codec,codecs => C}
import scodec.bits.BitVector
import java.nio.charset.Charset

case class ComponentW(kind: String, metadata: Map[String,String])

package object myapp {

  implicit val charset: Charset = Charset.forName("UTF-8")

  implicit val componentCodec: Codec[ComponentW] =
    (utf8 ~~ map[String,String]).pxmap(
      ComponentW.apply,
      ComponentW.unapply
    )
}

In this example, ComponentW is part of our wire protocol definition, and provides some application-specific semantic that is meaningful for API consumers (assuming the fields kind and metadata have "meaning" together). To make this item serializable, we simply need to tell scodec about the shape of the structure (in this case, String and Map[String,String]) and then supply a function Shape => A and then A => Option[(Shape)]. At runtime Remotely will use this codec to take the bytes from the wire and convert it into the ComponentW datatype using the defined shape and the associated functions.

// TODO: add more sophisticated examples?

Remotes

One of the interesting design points with Remotely is that remote server functions are modeled as local functions using a "remote reference". That's quite an opaque statement, so let's illustrate it with an example:

import remotely._

Remote.ref[Int => Int]("factorial")

Notice how this is just a reference - it doesn't actually do anything. At this point we have told the system that here's an immutable handle to a function that might later be available on an arbitrary endpoint, and the type of function being provided is Int => Int and its name is "factorial". This is interesting (and useful) because it entirely decouples the understanding about a given piece of functionality on the client side, and the system actor that will ultimately fulfil that request. Remotely clients will automatically model the server functions in this manner, so lets take a look at actually calling one of these functions:

scala> FactorialClient.factorial(1)
<console>:11: error: type mismatch;
 found   : Int(1)
 required: remotely.Remote[Int]
              FactorialClient.factorial(1)

That didn't go as planned! As it turns out, Remote function references can only be applied using values that have been explicitly lifted into a Remote context, and Remotely comes with several convenient combinators to do that:

You can choose to either use these functions directly, or have them implicitly applied by adding the following implicit conversion:

import remotely._, codecs._, Remote.implicits._

With this in scope, these functions will be automatically applied. One word of caution: you must ensure that you have a Codec in scope for whatever A you are trying to convert to a Remote value.

Endpoints

Now that you have a Remote function and you know how to apply arguments (applying the function inside the Remote monad), we need to explore the next important primitive in Remotely: Endpoint. An Endpoint models the network location of a specific server on a specific TCP port which can service function calls. Internally, Endpoint instances are modeled as a stream of Endpoint; doing this allows for a range of flexibility around circuit breaking and load balancing. Users can either embrace this Process[Task, Endpoint.Connection] directly, or use some of the convenience functions outlined below:

Using these basic combinators, we can now execute the Remote against a given endpoint. In order to do this, you have to elect what "context" the remote call will carry with it.

Resiliency

In addition to the simpler functions outlined above, we have also built in some resilience functions around Endpoint to make working with large systems more practical. One of the most important resilience functions on Endpoint is circuitBroken. This adds a circuit breaker to the endpoint. Consider the following usage example:

// ADD EXAMPLE HERE

The following are the primary functions of interest on the Endpoint object:

Execution Context

A Context is essentially a primitive data type that allows a given function invocation to carry along some metadata. When designing Remotely, we envisaged the following use cases:

Given any Remote function that you have applied, you can opt to execute it against an Endpoint with or without a context. Consider the following examples:

import remotely._, codecs._, Response.Context

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

val endpoint = Endpoint.single(address)(system)

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

/******** without a context ********/

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

/******** with a context ********/

val ctx = Context.empty.entries("foo" -> "bar")

val t2: Task[Int] = f.runWithContext(endpoint, ctx)

If you elect to use a Context or not, the result is the same from the client perspective - contexts are simply a runtime value that can be propagated for use by the server or not.

Monitoring

It's also worth noting at this point that when running a Remote with runWithContext or plain run, you have the option to pass in a remotely.Monitoring instance which will then be used to pass-in sampling information about the requests and responses being serviced by a Remotely service. Let's consider the Monitoring interface:


trait Monitoring { self =>
  /**
   * Invoked with the request, the request context,
   * the set of names referenced by that request,
   * the result, and how long it took.
   */
  def handled[A](
    ctx: Response.Context,
    req: Remote[A],
    references: Iterable[String],
    result: Throwable \/ A,
    took: Duration): Unit

  /**
   * Return a new `Monitoring` instance that send statistics
   * to both `this` and `other`.
   */
  def ++(other: Monitoring): Monitoring

  /**
   * Returns a `Monitoring` instance that records at most one
   * update for `every` elapsed duration.
   */
  def sample(every: Duration): Monitoring
}

As you can see, the interface is incredibly simple, and it serves two primary functions:

  1. To allow logging or tracing information to be dumped to a third-party system via the handled function, which contains the entire round-trip information.

  2. To provide sampling information about the duration of requests being serviced by this endpoint implementation.

Responses

When implementing a server...