Lru Cache With a Memcache-Like Interface

Lately, I’ve been messing around with Janestreet’s core and async libraries by reimplementing an old interview question that has been posed to me before. The problem statement itself is from my memory alone so this isn’t 100% what I’ve been asked but it should be extremely close.

Problem

Implement a simple memcache like TCP interface to an Lru cache. All commands in the server are of the form:

QUIT\r\n
GET key\r\n
SET key value\r\n

where key, value must not contain spaces.

Responses are of the form:

INSERTED\r\n
OK\r\nvalue\r\n
NOT_FOUND\r\n

It should be obvious enough to see the request-response correspondence.

Solution

First we’d like to separate the protocol from any particular server implementation by defining a Protocol module encapsulating the definitions above.

module Protocol = struct
  module Client_message = struct
    type t =
      | Get of string
      | Set of string * string
      | Quit with sexp
    let of_string s =
      Option.try_with @@ fun () ->
        match String.split ~on:' ' s with
        | "QUIT"::[] -> Quit
        | "GET"::k::[] -> Get k
        | "SET"::k::v::[] -> Set (k,v)
        | _ -> failwith "bad command"
  end
  module Server_message = struct
    type t =
      | Ok of string
      | Inserted
      | Not_found with sexp
    let to_string = function
      | Inserted -> "INSERTED\r\n"
      | Ok m -> "OK\r\n" ^ m ^ "\r\n"
      | Not_found -> "NOT_FOUND\r\n"
  end
end

the particular of the cache itself is immaterial so there’s no reason why our server should depend on it. The only real restrictions we’d like to have is that the cache must have have a bounded number of elements so that our server doesn’t run out of memory (Note that to achieve this we’d need restrict the size of the keys and values as well but we’ll skip it). We could functorize our server over the protocol as well if we include quitting as a return type for of_string for example. The following interface is satisfactory:

module type BoundedCache = sig
  type t
  val create : size:int -> t
  val get : t -> key:string -> string option
  val set : t -> key:string -> data:string -> unit
end

The Cache.Lru module in the core_extended package does not quite fit our interface but can be very simply adapted with:

module Lru = struct
  module L = Cache.Lru
  type t = (string, string) L.t
  let create ~size = L.create None size
  let get t ~key = L.find t key
  let set = L.add
end

Of course, I’m aware that original intent of the interview question was not to test how well you put together ready made lego blocks to solve the problem but more of designing the data structure for the Lru cache. The aim of this blogpost is more to show how well the lego blocks in core/async fit together.

Finally we can start writing our Cache server module functorized over the cache signature. We will start with the function process which simply processes a request into a response in the context of the server’s state - the cache in our case. We’d expect a type signature such as:

module P = Protocol
val process : Cache.t -> P.Client_message.t -> P.Server_message.t

Functions of this sort are very concisely expressed with OCaml’s pattern matching:

module CacheServer (Cache : BoundedCache) = struct
  open Protocol
  let process t req =
    let open Client_message in
    match req with
    | Get key ->
      Server_message.(match Cache.get t ~key with
          | None -> Not_found
          | Some x -> Ok x)
    | Set (key, data) ->
      Cache.set t ~key ~data;
      Server_message.Inserted
    | Quit -> assert false
end

The server runs by simply binding to an address and passing input to the function above in the main loop. The only special handling we need is for the quit message and bad input (which we just ignore).

let run_server ~host ~port ~size =
    let cache = Cache.create ~size in
    Tcp.Server.create (Tcp.on_port port) @@ fun _ reader writer ->
      Deferred.create (fun finished ->
          let rec loop () =
            Reader.read_line reader >>> function
            | `Ok query ->
              (match Client_message.of_string query with
               | None -> loop ()
               | Some Client_message.Quit -> Ivar.fill finished ()
               | Some req ->
                 let resp = process cache req in
                 resp |> Server_message.to_string |> Writer.write writer;
                 loop ())
            | `Eof -> Ivar.fill finished ()
          in loop ())

Finally, to run the server we must instantiate our functor and run the async scheduler.

let server =
  let module CS = CacheServer(Lru) in
  CS.run_server ~host:"127.0.0.1" ~port:12345 ~size:100

let () = Scheduler.go () |> never_returns

Later versions of core include a wrapper around ocamlbuild called corebuild which makes compiling the code much easier. Assuming your source is named tinycache.ml

$ corebuild tinycache.native -pkg core_extended

Full source as a gist.

Comments

comments powered by Disqus