The full code and documentation is available on Github. Cover photograph by Samyuktha Sridhar.

Introduction


Caustic is a transactional programming language for building race-free distributed systems. It allows programmers to write applications as if they were single-threaded, but distribute them arbitrarily without error.

Suppose there exist two programs \(A\) and \(B\) that each increment a shared counter \(C\). Formally, each program reads the current value of \(C\), \(x\), and then writes \(C = x + 1\). If \(B\) reads after \(A\) writes, then \(B\) reads \(x + 1\) and writes \(C = x + 2\). However, if \(B\) reads before \(A\) writes but after \(A\) reads, then both \(A\) and \(B\) will read \(x\) and write \(C = x + 1\). This is known as a race condition, because the value of \(C\) depends on the order in which \(A\) and \(B\) are executed. Race conditions may seem relatively benign, but they can have catastrophic consequences in practical systems. What if your bank determined your balance differently depending on the order in which deposits are made?

Programmers have a variety of synchronization mechanisms at their disposal for dealing with race conditions. However, usage of these mechanisms is cumbersome and error-prone. Caustic implicitly synchronizes wherever it is required by a program. Because the language takes care of synchronization, building distributed systems in Caustic is remarkably simple. Let’s look at a few examples to see just how simple it is in practice.

Distributed Counter


Let’s revisit the distributed counter problem from before. Our algorithm is straightforward, but it is difficult to find simple implementation of a generic, distributed counter. Contrast the implementation of a GCounter in the Akka project with its equivalent in Caustic.

module caustic.example

/**
 * A distributed counter.
 */
service Counter {

  /**
   * Increments the total and returns its current value.
   *
   * @param x Reference to total.
   * @return Current value.
   */
  def increment(x: Int&): Int = {
    if (x != null) x += 1 else x = 1
    x
  }

}

Distributed Queue


Let’s examine a more complicated example. Many distributed systems rely on queues to pass information between decoupled components. Contrast the implementation of a DistributedQueue in the Curator project with its equivalent in Caustic.

module caustic.example

/**
 * A distributed message queue.
 */
service Queue {

  /**
   * Adds the message to the end of the queue.
   *
   * @param queue Queue.
   * @param message Message.
   */
  def push(queue: List[String]&, message: String): Unit =
    queue.set(queue.size, message)

  /**
   * Returns the message at the front of the queue.
   *
   * @param queue Queue.
   * @return Head.
   */
  def peek(queue: List[String]&): String =
    queue.get(0)

  /**
   * Removes and returns the message at the front of the queue.
   *
   * @param queue Queue.
   * @return Head.
   */
  def pop(queue: List[String]&): String = {
    var head = peek(queue)
    queue.set(0, null)
    head
  }

  /**
   * Returns the number of messages in the queue.
   *
   * @param queue Queue.
   * @return Length.
   */
  def size(queue: List[String]&): Int =
    queue.size

}