Reactors

Road to Composable Distributed Computing

Aleksandar Prokopec / @alexprokopec


What makes a good programming model?



There are no standard metrics to agree on.

Comprehensible



Sufficiently simple and minimal to be easily understandable.

x86 assembly

    mov $0x0C0C, %ax
    mov $0x01, %bh
    mov $0x0001, %cx
    mov $0x0001, %dx
    int $0x10
    inc %cx
    inc %dx
    cmp $201, %dx
    jz end
          

x86 assembly

Lambda calculus

  • λ x. b     abstraction
  • f x         application
  • x           variable

(λ x. x) t

(λ x. x) t

t

Concise



Programs written in the model must be quick and easy to understand.

Lambda calculus



λ b. (λ c. λ t. λ f. c t f) b (λ x. λ y. y) (λ x. λ y. x)

High-level functional programming



b => if (b) false else true
          

High-level functional programming



(b: Boolean) => !b
          

Location-transparency



The program must run correctly irrespective of the computers it is deployed on, and their relationships.

Actor model

class Ping extends Actor {
  def receive = {
    case "pong" => sender ! "ping"
  }
}
            

Actor model

class Ping extends Actor {
  def receive = {
    case "pong" => sender ! "ping"
  }
}
            

Actor model

class Ping extends Actor {
  def receive = {
    case "pong" => sender ! "ping"
  }
}
            
class Pong extends Actor {
  def receive = {
    case "ping" => sender ! "pong"
  }
}
          

Composable



Programs can be built from smaller independent programs.

Generic Server

class Server[T, S](f: T => S)
extends Actor {
  def receive = {
    case x: T => sender ! f(x)
  }
}
          

Generic Client

class Client[T, S](
  server: ActorRef,
  req: T,
  action: S => Unit
) extends Actor {
  server ! req
  def receive = {
    case x: S => action(x)
  }
}
          

Name Server

val actors = Map[String, ActorRef]
val ns = actorOf(Server(actors))

Name Server

val actors = Map[String, ActorRef]
val ns = actorOf(Server(actors))
actorOf(Client(ns, "p", println))

Name Server

val actors = Map[String, ActorRef]
val ns = actorOf(Server(actors))
actorOf(Client(ns, "p", println))

Name Server

val actors = Map[String, ActorRef]
val ns = actorOf(Server(actors))
actorOf(Client(ns, "p", println))

Name Server

val actors = Map[String, ActorRef]
val ns = actorOf(Server(actors))
actorOf(Client(ns, "p", println))

Name Server Cache

class Cache(var c: ActorRef = null)
extends Client(ns, "p", r => c=r)




Name Server Cache

class Cache(var c: ActorRef = null)
extends Client(ns, "p", r => c=r)




Name Server Cache

class Cache(var c: ActorRef = null)
extends Client(ns, "p", r => c=r) {
  def receive = super.receive orElse {
    case "p" => sender ! c
  }
}

Name Server Cache

class Cache(var c: ActorRef = null)
extends Client(ns, "p", r => c=r) {
  def receive = super.receive orElse {
    case "p" => sender ! c
    case ns: ActorRef => ns ! "p"
  }
}




Actor model does not compose well.

Reactor model

  • Expressing concurrency in the system
  • Information exchange



Expressing concurrency



class Cache
extends Reactor[String] {
  var cached = _
}
          



Sending and receiving information

Channels and event streams

val (events, ch) = open[String]

events.onEvent {
  s => println(s)
}

ch ! ""

Server protocol

type Req[T, S] =
  Channel[(T, Channel[S])]

def server[T, S](f: T => S): Req[T, S] = {
  val (events, ch) = open[(T, Channel[S])]
  events onMatch {
    case (x, sender) => sender ! f(x)
  }
  ch
}
          

Client protocol

type Req[T, S] =
  Channel[(T, Channel[S])]

def ?[T,S](r: Req[T, S], x: T): Events[S] ={
  val (events, ch) = open[S]
  r ! (x, ch)
  events
}
          

Cache protocol

def cache[T,S](s:Req[T,S], x:T): Req[T,S] = {
  var cached: S = _

  (s ? x) onEvent {
    y => cached = y
  }

  server(y => cached)
}

Cache reactor

type Server[T, S] =
  Reactor[(T, Channel[S])]

class Cache
extends Server[String, Channel[_]] {
  events.forward(cache(ns, "p"))
}
          



More complex protocols

Broadcast


Communication pattern in which the sender sends the same message to multiple targets.



However, broadcast does not guarantee too much ordering.




Operations must be commutative.




import broadcast.bestEffort



import broadcast.totalOrder



import replication.crdt



Thank you!



http://github.com/reactors-io/reactors