webapp

Cache-aside behind a load balancer (api, lb, app, cache, db)

What to watch

Hit rate warming up; what happens when the cache crashes and there is no timeout

Seed: 9. 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/webapp/api
use github.com/synodlang/examples/webapp/app
use github.com/synodlang/examples/webapp/cache
use github.com/synodlang/examples/webapp/db
use github.com/synodlang/examples/webapp/lb
use std/lesson
use std/lists
use std/random
use std/sim
use std/time

define main
  sim.Seed 9
  set balancer to (sim.Spawn lb)
  set servers to (sim.Cluster of 3 running app)
  set store to (sim.Spawn cache)
  set database to (sim.Spawn db)
  lb.Balance servers on balancer
  each s in servers
    app.Wire s to store and database
  set keys to {"home", "cart", "profile", "search", "help"}
  each i in (lists.Range 1 to 150)
    after (i times 20ms)
      deliver (message api.Get with Key (random.Pick keys), Id i, Start time.Now) to balancer
  lesson.Note "Misses go to the database and fill the cache; hits come back fast."
  after 2s
    sim.Crash store
    lesson.Ask "The cache just crashed. What happens to new requests?" choosing {"They go straight to the database instead", "They hang: nothing answers and nothing times out", "The load balancer notices and retries on another cache", "They are served, only more slowly"}
  sim.StopAt 4s
# package api
Get has
  set Key to ""
  set Id to 0
  set Start to 0s

Lookup has
  set Key to ""
  set Id to 0

CacheReply has
  set Key to ""
  set Id to 0
  set Value to ""
  set Found to false

Query has
  set Key to ""
  set Id to 0

Rows has
  set Key to ""
  set Id to 0
  set Value to ""

Fill has
  set Key to ""
  set Value to ""

Served has
  set Id to 0
  set Hit to false
  set Latency to 0s
# package app
use github.com/synodlang/examples/webapp/api
use std/maps
use std/net
use std/sim
use std/time

Node has
  set Cache to sim.Nobody
  set Db to sim.Nobody
  set pending to {}

define Wire [n: Node] to [c: node] and [d: node]
  set n.Cache to c
  set n.Db to d

define [n: Node] Finish [r: api.Get] hit [h: bool]
  set n.pending to (maps.Without r.Id from n.pending)
  emit (message api.Served with Id r.Id, Hit h, Latency (time.Now minus r.Start)) by n

# Cache-aside: ask the cache first
when [n: Node] (net.Received api.Get from balancer)
  set item api.Get.Id of n.pending to api.Get
  net.Send (message api.Lookup with Key api.Get.Key, Id api.Get.Id) from n to n.Cache

# On a miss, query the database
when [n: Node] (net.Received api.CacheReply from cache)
  set request to (item api.CacheReply.Id of n.pending)
  if api.CacheReply.Found
    n.Finish request hit true
  else
    net.Send (message api.Query with Key request.Key, Id request.Id) from n to n.Db

# Fill the cache with what the database returned
when [n: Node] (net.Received api.Rows from db)
  set request to (item api.Rows.Id of n.pending)
  net.Send (message api.Fill with Key api.Rows.Key, Value api.Rows.Value) from n to n.Cache
  n.Finish request hit false
# package cache
use github.com/synodlang/examples/webapp/api
use std/net

Node has
  set Entries to {}

when [n: Node] (net.Received api.Lookup from asker)
  set k to api.Lookup.Key
  if (k is in n.Entries)
    net.Send (message api.CacheReply with Key k, Id api.Lookup.Id, Value (item k of n.Entries), Found true) from n to asker
  else
    net.Send (message api.CacheReply with Key k, Id api.Lookup.Id) from n to asker

when [n: Node] (net.Received api.Fill from filler)
  set item api.Fill.Key of n.Entries to api.Fill.Value
# package db
use github.com/synodlang/examples/webapp/api
use std/net
use std/text

Node has
  set Queries to 0

# The database is slow: every query takes 40ms of work
when [n: Node] (net.Received api.Query from asker)
  set n.Queries to (n.Queries plus 1)
  after 40ms
    net.Send (message api.Rows with Key api.Query.Key, Id api.Query.Id, Value (text.Join {"row for ", api.Query.Key})) from n to asker
# package lb
use github.com/synodlang/examples/webapp/api
use std/lists
use std/net
use std/sim

Node has
  set Backends to {}
  set next to 0

define Balance [servers: list of node] on [n: Node]
  set n.Backends to servers

# Round robin over the application servers
when [n: Node] (sim.Delivered api.Get)
  net.Send api.Get from n to (item n.next of n.Backends)
  set n.next to (n.next plus 1)
  if (n.next is (lists.Size n.Backends))
    set n.next to 0

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