cassandra

A Cassandra-style cluster: gossip + ring + replication factor 3 + QUORUM + hinted handoff

What to watch

A write succeeding with a replica down, and the hint replayed after restart

Seed: 3. The program asks a question at the interesting moment; with Predict on, the run stops there so you can answer before you see what happens.

Run it

Edit any file and press Run. The tabs hold the program and the packages it uses.

# package main
use github.com/synodlang/examples/cassandra
use github.com/synodlang/examples/gossip
use github.com/synodlang/examples/ring
use std/lesson
use std/sim

define main
  sim.Seed 3
  set nodes to (sim.Cluster of 6 running cassandra)
  each i, n in nodes
    if (i more than 0)
      gossip.Join n to (item 0 of nodes)
  set replicas to (ring.Owners of "user:42" among nodes count 3)
  set coordinator to (item 0 of replicas)
  set victim to (item 1 of replicas)
  after 5s
    deliver (message cassandra.Write with Key "user:42", Value "Ada") to coordinator
  after 6s
    sim.Crash victim
  after 12s
    deliver (message cassandra.Write with Key "user:42", Value "Grace") to coordinator
    lesson.Ask "One replica is down. Does the write succeed, and how will that replica catch up?" choosing {"It fails until the replica returns", "It succeeds; a hint replays it when the replica returns", "It succeeds, but the replica never gets it"}
  after 14s
    deliver (message cassandra.Read with Key "user:42") to (item 2 of replicas)
  after 18s
    sim.Restart victim
    lesson.Note "Data lives in memory here, so the restarted replica is refilled only by hints."
  sim.StopAt 30s
# package cassandra
use github.com/synodlang/examples/gossip
use github.com/synodlang/examples/ring
use std/lists
use std/maps
use std/net
use std/sim
use std/text
use std/time

Write has
  set Key to ""
  set Value to ""

Read has
  set Key to ""

Written has
  set Key to ""
  set Acks to 0

ReadDone has
  set Key to ""
  set Value to ""

Mutation has
  set key to ""
  set value to ""
  set stamp to 0s
  set request to ""

MutationAck has
  set request to ""

Fetch has
  set key to ""
  set request to ""

FetchReply has
  set request to ""
  set value to ""
  set stamp to 0s

Hint has
  set target to sim.Nobody
  set mutation to (message Mutation with key "")

Node has
  set Data to {}
  set PendingHints to 0
  set versions to {}
  set hints to {}
  set nextRequest to 0
  set acks to {}
  set keys to {}
  set replies to {}

define Factor
  result 3

define Quorum
  result 2

# Membership comes from gossip, never from a global list
define [n: Node] Members
  result (maps.Keys n.gossip.Known)

define [n: Node] ReplicasOf [k: text]
  result (ring.Owners of k among n.Members count Factor)

define [n: Node] NewRequest [k: text]
  set n.nextRequest to (n.nextRequest plus 1)
  set id to (text.From n.nextRequest)
  set item id of n.keys to k
  result id

define [n: Node] start
  time.Every 1s
    set waiting to {}
    each h in n.hints
      if (h.target is in n.gossip.Down)
        set waiting to (lists.Append h to waiting)
      else
        net.Send h.mutation from n to h.target
    set n.hints to waiting
    set n.PendingHints to (lists.Size n.hints)

# Coordinator: write to every replica; keep a hint for replicas that look down
when [n: Node] (sim.Delivered Write)
  set id to (n.NewRequest Write.Key)
  set item id of n.acks to 0
  set m to (message Mutation with key Write.Key, value Write.Value, stamp time.Now, request id)
  each r in (n.ReplicasOf Write.Key)
    if (r is in n.gossip.Down)
      set n.hints to (lists.Append (message Hint with target r, mutation m) to n.hints)
      set n.PendingHints to (lists.Size n.hints)
    else
      net.Send m from n to r

define [n: Node] Store [m: Mutation]
  set item m.key of n.Data to m.value
  set item m.key of n.versions to m.stamp

# Replica: keep the newest value per key (last write wins)
define [n: Node] Apply [m: Mutation]
  set k to m.key
  if (k is in n.versions)
    if (m.stamp more than (item k of n.versions))
      n.Store m
  else
    n.Store m

when [n: Node] (net.Received Mutation from coordinator)
  n.Apply Mutation
  net.Send (message MutationAck with request Mutation.request) from n to coordinator

# Coordinator: the write succeeds at QUORUM acknowledgements
when [n: Node] (net.Received MutationAck from replica)
  set id to MutationAck.request
  set item id of n.acks to ((item id of n.acks) plus 1)
  if ((item id of n.acks) is Quorum)
    emit (message Written with Key (item id of n.keys), Acks Quorum) by n

# Coordinator: read from the live replicas
when [n: Node] (sim.Delivered Read)
  set id to (n.NewRequest Read.Key)
  set item id of n.replies to {}
  each r in (n.ReplicasOf Read.Key)
    if (r is not in n.gossip.Down)
      net.Send (message Fetch with key Read.Key, request id) from n to r

when [n: Node] (net.Received Fetch from coordinator)
  set k to Fetch.key
  if (k is in n.Data)
    net.Send (message FetchReply with request Fetch.request, value (item k of n.Data), stamp (item k of n.versions)) from n to coordinator
  else
    net.Send (message FetchReply with request Fetch.request) from n to coordinator

# Coordinator: at QUORUM replies, answer with the newest value
when [n: Node] (net.Received FetchReply from replica)
  set id to FetchReply.request
  set item id of n.replies to (lists.Append FetchReply to (item id of n.replies))
  if ((lists.Size (item id of n.replies)) is Quorum)
    set best to (item 0 of (item id of n.replies))
    each r in (item id of n.replies)
      if (r.stamp more than best.stamp)
        set best to r
    emit (message ReadDone with Key (item id of n.keys), Value best.value) by n
# package gossip
use std/disk
use std/lists
use std/net
use std/random
use std/time

digest has
  set known to {}

Node has
  set Generation to 0
  set Heartbeat to 0
  set Known to {}
  set Down to {}
  set peers to {}
  set seenAt to {}

define Join [n: Node] to [seed: node]
  set n.peers to {seed}

# Versions combine generation and heartbeat, so a restarted node is believed again
define [n: Node] Version
  result ((n.Generation times 1000000) plus n.Heartbeat)

define [n: Node] Learn [k: node] at [v: number]
  set item k of n.Known to v
  set item k of n.seenAt to time.Now
  if ((k is not n) and (k is not in n.peers))
    set n.peers to (lists.Append k to n.peers)

define [n: Node] CheckPeers
  set suspects to {}
  each k, t in n.seenAt
    if ((time.Now minus t) more than 4s)
      set suspects to (lists.Append k to suspects)
  set n.Down to suspects

define [n: Node] start
  set n.Generation to ((disk.Read "generation" on n or 0) plus 1)
  disk.Write n.Generation at "generation" on n
  time.Every 1s
    set n.Heartbeat to (n.Heartbeat plus 1)
    n.Learn n at n.Version
    n.CheckPeers
    if ((lists.Size n.peers) more than 0)
      net.Send (message digest with known n.Known) from n to (random.Pick n.peers)

when [n: Node] (net.Received digest from sender)
  each k, v in digest.known
    if (k is in n.Known)
      if (v more than (item k of n.Known))
        n.Learn k at v
    else
      n.Learn k at v
  if (sender is not in n.peers)
    set n.peers to (lists.Append sender to n.peers)
# package ring
use std/lists
use std/maps
use std/math
use std/sim
use std/text

Placed has
  set Key to ""
  set Owner to sim.Nobody

define Vnodes
  result 8

# Each node owns several points on a ring of [0, 1), derived from its name
define TokensOf [n: node]
  set tokens to {}
  each i in (lists.Range 1 to Vnodes)
    set tokens to (lists.Append (math.Hash (text.Join {(sim.Name n), "#", (text.From i)})) to tokens)
  result tokens

# Clockwise distance from a key's point to the nearest point of a node
define Distance from [h: number] to [n: node]
  set best to 1
  each t in (TokensOf n)
    set d to (math.Remainder of (t minus h) by 1)
    if (d less than best)
      set best to d
  result best

# The first r nodes clockwise from the key own it
define Owners of [k: text] among [ns: list of node] count [r: number]
  set h to (math.Hash k)
  set distances to {}
  each n in ns
    set item n of distances to (Distance from h to n)
  result (lists.Take r from (maps.KeysByValue distances))

On your own machine: synod run ./cassandra. Source on GitHub · all examples