Embedding Lua in Go

Command line parameters, environment variables, and configuration files are common ways to change the behavior of software. However, sometimes that is just not enough and an embedded language can be the solution. In this case we will embed Lua using gopher-lua.

Godoc: http://godoc.org/github.com/yuin/gopher-lua

All code in the post can be found at http://github.com/otm/embedding-lua-in-go

Running Lua Code in Go

First lets set up the environment and test that it works, start by install gopher-lua:

go get github.com/yuin/gopher-lua

Secondly let’s create a minimal implementation:


package main

import "github.com/yuin/gopher-lua"

func main() {
	L := lua.NewState()
	defer L.Close()
	if err := L.DoString(`print("Hello World")`); err != nil {
		panic(err)
	}
}
hello.go

lua.NewState() creates our Lua VM, and it is though L (*lua.LState) we will interact with Lua in the future. Throughout the post L will denote a pointer to lua.LState. L.DoString runs the Lua code in the VM. Running the Go code will yield:

$ go run hello.go
Hello World

To run a Lua file, instead of a string, call lua.DoFile

L := lua.NewState()
defer L.Close()
if err := L.DoFile("hello.lua"); err != nil {
		panic(err)
}

Embedding Lua Code as Strings

DoFile and DoString can be called several times, and thus it can be utilized to expose Lua function. In the example bellow sayHello function is first defined, and then called in the second call to DoString


func main() {
	L := lua.NewState()
	defer L.Close()
	if err := L.DoString(`function sayHello() print("Hello Again") end`); err != nil {
		panic(err)
	}

	if err := L.DoString(`sayHello()`); err != nil {
		panic(err)
	}
}
hello2.go

Calling Go Code from Lua

Exposing Go functions to Lua is essential to create custom a custom API. A Go function should implement the LGFunction type to be callable from Lua.

type LGFunction func(*LState) int

It receives a *lua.LState and returns an integer. The LState is needed for interacting with the Lua VM, most commonly for retrieving function arguments. The returned integer defines how many return values has been pushed to the Lua stack. A complete example looks like this:


func square(L *lua.LState) int {  //*
	i := L.ToInt(1)          // get first (1) function argument and convert to int
	ln := lua.LNumber(i * i) // make calculation and cast to LNumber
	L.Push(ln)               // Push it to the stack
	return 1                 // Notify that we pushed one value to the stack
}

func main() {
	L := lua.NewState()
	defer L.Close()

	L.SetGlobal("square", L.NewFunction(square)) // Register our function in Lua
	if err := L.DoString(`print("4 * 4 = " .. square(4))`); err != nil {
		panic(err)
	}
}
calling-go.go

The LState defines some convenience functions, in the example above we are using L.ToInt(1) to fetch the first function argument.

Note: Lua isn’t zero-indexed, so the first function argument is fetched with L.ToInt(1), second argument with L.ToInt(2). In Lua, all arrays are 1-indexed, however t[0] is still valid but that would result in the lenght of the array to be off-by-one.

There are a number of To...(n int) functions available. These functions does not throw errors, but will return Go default values if conversion is not possible. To get automatic errors the L.Check...(n int) family of functions can be used; They throw a Lua error if the type check fails. For optional arguments the L.Opt...(n int, default T) functions can be used. Example:

L.GetTop() returns the number of arguments that was used when the function was called. And to fetch an argument without conversion the L.Get(n int) function can be used.

If an argument to a function can be of more then one type the L.CheckTypes(n int, types ...LValueType) function can be used to check and yield an error to the user. Using the L.CheckTypes function equate to checking the type manually and then calling L.TypeError(n int, message string) if there is an error.

// Second argument can be string or function
L.CheckTypes(2, lua.LTString, lua.LTFunction)
switch lv := L.Get(2).(type) {
case LString:
	// use as string
case Lfunction:
  // use as function
}

Calling Lua from Go

Calling Lua code is done through L.CallByParam, which takes a parameters object, P, and arguments as variadic parameters. The parameters object takes three important parameters:

  • Fn - the lua.LFunction to call
  • Nret - The number of returned values
  • Protect - If protect is true an error is returned, otherwise a panic will occur.

The following code defines a “concat” function in Lua. Calls the concat function with the arguments “Go” and “Lua” and prints the resulting string to stdout.


// luaCode is the Lua code we want to call from Go
var luaCode = `
function concat(a, b)
	return a .. " + " .. b
end
`

func main() {
	L := lua.NewState()
	defer L.Close()

	if err := L.DoString(luaCode); err != nil {
		panic(err)
	}

	// Call the Lua function concat
	// Lua analogue:
	//	str = concat("Go", "Lua")
	//	print(str)
	if err := L.CallByParam(lua.P{
		Fn:      L.GetGlobal("concat"), // name of Lua function
		NRet:    1,                     // number of returned values
		Protect: true,                  // return err or panic
	}, lua.LString("Go"), lua.LString("Lua")); err != nil {
		panic(err)
	}

	// Get the returned value from the stack and cast it to a lua.LString
	if str, ok := L.Get(-1).(lua.LString); ok {
		fmt.Println(str)
	}

	// Pop the returned value from the stack
	L.Pop(1)
}
calling-lua.go

Good to Know

Gopher-Lua Types

The gopher-lua library operates on a interface called LValue.

type LValue interface {
  String() string
  Type()   LValueType
}

Objects implementing this interface are LNilType, LBool, LNumber, LString, LFunction, LUserData, LState, LTable, and LChannel. Calling the LValue.Type() returns the corresponding LValueType.

const (
  LTNil LValueType = iota
  LTBool
  LTNumber
  LTString
  LTFunction
  LTUserData
  LTThread
  LTTable
  LTChannel
)

Covertion and Check functions

There are some practical functions to convert and check lua.LValue objects.

  • lua.LVAsBool(v LValue) - Convert to bool, nil and false becomes false. Otherwise true.

  • lua.LVAsString(v LValue) - Converts LString and LNumber to string. Otherwise empty string.

  • lua.CanConvToString(v LValue) - True if LString or LNumber. Otherwise false.

  • lua.LVIsFalse(v LValue) - Returns true if nil or false.

The LTable type

One of the most versatile and used data structures in Lua is LTable (actually it is the only one). The LTable type can be used for emulating namespaces and classes. However, the basic API is quite simple, and the advanced features deserves its own post. See http://godoc.org/github.com/yuin/gopher-lua#LTable for the LTable Go API.

Bash Prompt for Git

Hacking a bash prompt that adapts when entering a git repository.

Disclaimer Disclaimer: This is a 15min hack which might include bugs and can definitely be made more performant.

This bash hack will keep the normal prompt, and only change it when entering a directory that is tracked by git. In a git repository it will display the basename of the directory and branch. The branch name is colored accordingly:

  • Red - uncommitted working
  • Yellow - your brancgh is ahead
  • Green - nothing to commit
  • Ochre - all other statuses

Examples

nils@ratchet:~/project$
Normal Directory
[go](master)$
Non Commited
[go](master)$
Clean

Code

The following code should be included in the .bashrc file

COLOR_RED="\033[0;31m"
COLOR_YELLOW="\033[0;33m"
COLOR_GREEN="\033[0;32m"
COLOR_OCHRE="\033[38;5;95m"
COLOR_BLUE="\033[0;34m"
COLOR_WHITE="\033[0;37m"
COLOR_RESET="\033[0m"
COLOR_RESET2="\e[0m"

function git_color {
  local git_status="$(git status 2> /dev/null)"

  if [[ ! $git_status =~ "working directory clean" ]]; then
    echo -e $COLOR_RED
  elif [[ $git_status =~ "Your branch is ahead of" ]]; then
    echo -e $COLOR_YELLOW
  elif [[ $git_status =~ "nothing to commit" ]]; then
    echo -e $COLOR_GREEN
  else
    echo -e $COLOR_OCHRE
  fi
}

function git_branch {
  local git_status="$(git status 2> /dev/null)"
  local on_branch="On branch ([^${IFS}]*)"
  local on_commit="HEAD detached at ([^${IFS}]*)"

  if [[ $git_status =~ $on_branch ]]; then
    local branch=${BASH_REMATCH[1]}
    echo "($branch)"
  elif [[ $git_status =~ $on_commit ]]; then
    local commit=${BASH_REMATCH[1]}
    echo "($commit)"
  fi
}

function git_dir {
  echo "$(basename `git rev-parse --show-toplevel`)/$(git rev-parse --show-prefix)"
}

PS1='$(git status &> /dev/null || printf "\u@\h:\w\$ ")' # other dir
PS1+='$(git status &> /dev/null && printf "' # if we are in git dir
PS1+="\[$COLOR_RESET\]"         # reset color
PS1+="[\$(git_dir)]"            # get git dir + path
PS1+="\[\$(git_color)\]"        # colors git status
PS1+="\$(git_branch)"           # prints current branch
PS1+="\[$BLUE\]\[$COLOR_RESET2\]\\$ "   # '#' for root, else '$'
PS1+='")'
export PS1


Raft: A First Implementation

Series: Raft 101

tl;dr In this part we will implement a very simplistic cluster node using the etcd raft implementation. And with the node implementation we will start up a small cluster.

The complete source to this post can be found at http://github.com/otm/raft-part-1, and documentation for the etcd raft at https://godoc.org/github.com/coreos/etcd/raft

Creating a Basic Node

Clusters are built by nodes, so lets start of by defining a node. Initially the node only needs to keep track of the raft and the persistent storage. Below is a minimal implementation of the node type and its constructor function.

import (
  "github.com/coreos/etcd/raft"
  "github.com/coreos/etcd/raft/raftpb"
  "golang.org/x/net/context"
)

  const hb = 1

  type node struct {
    // id is the node id in the cluster
    id        unint64

    // the raft that the cluster node will use
    // this includes the WAL
    raft      raft.Node

    // the raft configuration
    cfg      *raft.Config

    // pstore is a fake implementation of a persistent storage
    // that will be used side-by-side with the WAL in the raft
    pstore    map[string]string
  }

  func newNode(id int, peers []raft.Peer) *node {
    n := &node{
      id: id,
      cfg: &raft.Config{
        // ID in the etcd raft
        ID:              uint64(id),

        // ElectionTick controls the time before a new election starts
        ElectionTick:    10 * hb,

        // HeartbeatTick controls the time between heartbeats
        HeartbeatTick:   hb,

        // Storage contains the log
        Storage:         raft.NewMemoryStorage(),

        MaxSizePerMsg:   math.MaxUint16,
        MaxInflightMsgs: 256,
      },
      pstore: make(map[string]string),
    }

    n.raft = raft.StartNode(n.cfg, peers)

    return n
  }

With the node comes some responsibilities:

  1. Read from Node.Ready() channel and process the updates.
  2. All persisted log entries must be made available via an implementation of the Storage interface. This can be solved by using the provided MemoryStorage type, if it is repopulated upon restart; or by implementing a disked backed implementation. In this example we will not implement this part.
  3. Call Node.Step() when receiving a message from another node.
  4. Call Node.Tick() at regular intervals. Internally the raft time is represented by an abstract tick, that controls two important timeouts, the heartbeat and election timeout.

So the state machine main loop will look like this:

func (n *node) run() {
  n.ticker = time.Tick(time.Second)
  for {
    select {
    case <-n.ticker:
      // Point (4) above
      n.raft.Tick()
    case rd := <-n.raft.Ready():
      // Point (2) above
    case <-n.done:
      return
    }
  }
}

Handling Raft Ready Events

Above a large part of the control loop is left out, that is the processing of updates from the Node.Ready() channel. When receiving a message there are four important tasks:

  1. Write HardState, Entries and Snapshot to persistent storage. Note! When writing to storage it is important to check the Entry Index (i). If previously persisted entries with Index >= i exist, those entries needs to be discarded. For instance this can happen if we get a cluster split with the leader in the minority part; because then the cluster can advance in the other part.

  2. Send all messages to the nodes named in the To field. Note! It is important to not send any messages until:

    • the latest HardState has been persisted
    • all Entries from previous batch have been persisted (messages from the current batch can be sent and persisted in parallel)
    • call Node.ReportSnapshot() if any message has type MsgSnap and the snapshot has been sent
  3. Apply Snapshot and CommitedEntries to the state machine. If any committed Entry has the type EntryConfChange call Node.ApplyConfChange to actually apply it to the node. The configuration change can be canceled at this point by setting the NodeId field to zero before calling ApplyConfChange. Either way, ApplyConfChange must be called; and the decision to cancel must be based solely on the state machine and not on external information, for instance observed health state of a node.

  4. Call Node.Advance() to signal readiness for the next batch. This can be done any time after step 1 is finished. Note! All updates must be processed in the order they were received by Node.Ready()

The four tasks above can be done in parallel as long as all notices above are fulfilled. In the example below rd is of the type raft.Ready

n.saveToStorage(rd.HardState, rd.Entries, rd.Snapshot) // (1)
n.send(rd.Messages) // (2)
if !raft.IsEmptySnap(rd.Snapshot) {
  n.processSnapshot(rd.Snapshot)  //(3)
}
for _, entry := range rd.CommittedEntries {
  n.process(entry) // (3)
  if entry.Type == raftpb.EntryConfChange {
    var cc raftpb.ConfChange
    cc.Unmarshal(entry.Data)
    n.node.ApplyConfChange(cc) // (3)
  }
}
n.raft.Advance() // (4)

Below is the raft.Ready type, please note that pb is actually raftpb.

type Ready struct {
    // The current volatile state of a Node.
    // SoftState will be nil if there is no update.
    // It is not required to consume or store SoftState
    // It is useful for logging and debugging
    *SoftState

    // The current state of a Node to be saved to stable storage BEFORE
    // Messages are sent.
    // HardState will be equal to empty state if there is no update.
    pb.HardState

    // Entries specifies entries to be saved to stable storage BEFORE
    // Messages are sent.
    Entries []pb.Entry

    // Snapshot specifies the snapshot to be saved to stable storage.
    Snapshot pb.Snapshot

    // CommittedEntries specifies entries to be committed to a
    // store/state-machine. These have previously been committed to stable
    // store.
    CommittedEntries []pb.Entry

    // Messages specifies outbound messages to be sent AFTER Entries are
    // committed to stable storage.
    // If it contains a MsgSnap message, the application MUST report back to raft
    // when the snapshot has been received or has failed by calling ReportSnapshot.
    Messages []pb.Message
}

Save To Storage

Saving to storage is not so advanced in a simplistic example like this:

  1. Append the entries - this is basically the message to be committed
  2. Set the hard state - this will commit the message
  3. Apply the snapshot - this overwrites the storage with the given snapshot
func (n *node) saveToStorage(hardState raftpb.HardState, entries []raftpb.Entry, snapshot raftpb.Snapshot) {
  n.store.Append(entries)

  if !raft.IsEmptyHardState(hardState) {
    n.store.SetHardState(hardState)
  }

  if !raft.IsEmptySnap(snapshot) {
    n.store.ApplySnapshot(snapshot)
  }
}

Send

For now the RPC will be simulated with a global map which holds the nodes. With the send function there is also a matching receive function. Note, the receive() function calls raft.Step(), and that is crucial to advance the state machine.

func (n *node) send(messages []raftpb.Message) {
  for _, m := range messages {
    // Inspect the message (just for fun)
    log.Println(raft.DescribeMessage(m, nil))

    // send message to other node
    nodes[m.To].receive(n.ctx, m)
  }
}

func (n *node) receive(ctx context.Context, message raftpb.Message) {
  n.raft.Step(ctx, message)
}

Process Raft Entry

If entry.Type of the raftpb.Entry is raftpb.EntryNormal the message should be processed. The message will be encoded in entry.Data. The protocol below is very simple, and is a string on the form key:value

func (n *node) process(entry raftpb.Entry) {

  if entry.Type == raftpb.EntryNormal && entry.Data != nil {
    log.Println("normal message:", string(entry.Data))

    parts := bytes.SplitN(entry.Data, []byte(":"), 2)
    n.pstore[string(parts[0])] = string(parts[1])
  }
}

Process Snapshot

For now processSnapshot will only be a dummy implementation. But basically it should only overwrite the the current persistent storage.

func (n *node) processSnapshot(snapshot raftpb.Snapshot) {
  log.Printf("Applying snapshot on %v is not implemenetd yet")
}

Advance the Raft

At this point the only thing left to do is calling Node.Advance(), this signals that the node is ready for the next batch. Please note that all updates must be processed in the order they were received from Node.Ready. In our example it looks like this:

n.raft.Advance()

Using the Raft

Finally it is time to start up a couple of nodes and connect them to a cluster. Below the nodes are started with predefined peers. Lastly Node.Campaign is called to start an election campaign; it is not necessary but it saves some time, as we do not need to wait for the election timeout. Note! At this point we do not have a resilient cluster, because if a node is lost there is no way to reach a majority consensus.

nodes[1] = newNode(1, []raft.Peer{{ID: 1}, {ID: 2}})
go nodes[1].run()

nodes[2] = newNode(2, []raft.Peer{{ID: 1}, {ID: 2}})
go nodes[2].run()

nodes[1].raft.Campaign(nodes[1].ctx)

However, all nodes in the cluster can not be known in advance. Below a node is created and added to a running cluster. It is created without any peers and a configuration change is proposed to the cluster. When the configuration change has been committed, the cluster is fault tolerant.

nodes[3] = newNode(3, []raft.Peer{})
go nodes[3].run()
nodes[2].raft.ProposeConfChange(nodes[2].ctx, raftpb.ConfChange{
  ID:      3,
  Type:    raftpb.ConfChangeAddNode,
  NodeID:  3,
  Context: []byte(""),
})

Next up is writing data to the cluster, that is also done by proposing a change to the cluster. Each of the writes are done to different nodes in the cluster.

nodes[1].node.Propose(nodes[1].ctx, []byte("key1:value1"))
nodes[2].node.Propose(nodes[2].ctx, []byte("key2:value2"))
nodes[3].node.Propose(nodes[3].ctx, []byte("key3:value3"))

Dumping the data on the nodes reveals if they have been synchronized properly.

for i, node := range nodes {
  fmt.Printf("** Node %v **\n", i)
  for k, v := range node.pstore {
    fmt.Printf("%v = %v", k, v)
  }
  fmt.Printf("*************\n")
}

If a node is added to the cluster, data will be replicated to the newly added node by replaying the log.

The Raft Algorithm

Series: Raft 101

The Raft consensus algorithm provides an understandable, easy to use, and generic way to distribute a state machine in a cluster. The Raft is a successor, or an alternative, to an algorithm called Paxos.

A node in the cluster can have three states: follower, candidate, or leader. Consensus in the cluster is maintained by the leader, which is responsible for log replication. To become the leader a node will change its status from follower to candidate.

Leader Election

The leader in the cluster sends heartbeat messages to the followers. If a follower does not receive a heartbeat within the election timeout it will transition state from follower to candidate. The candidate starts a new election term and increases the term counter, votes for itself and sends a RequestVote message to the cluster members. A member node will vote on the first candidate that sends a RequestVote message, and it will only vote once during a term. At this point there are three scenarios in the cluster:

  1. The candidate receives a message from a leader with equal or higher term - and transitions from candidate to flower.
  2. The candidate receives a majority of the votes - and transitions from candidate to leader.
  3. The candidate’s election times out - and transitions from candidate to follower.

Log Replication

All changes go through the leader, leader gets proposal, creates a log entry (uncommited), then replicates the entry to the followers, when a majority of the followers have written the log entry the leader commits it (the leaders state has now changed), the leader notifies the followers that the entry has been committed. At this point the cluster is in consensus.

Log replication

Once there is a leader the cluster can move forward. All changes go through the leader, however followers can propose a change to the leader. When the leader is updated it creates a log entry, which is uncommited; the leader replicates the log entry to the followers in an AppendEntries message. When a majority of followers has written the log entry, the leader will commit the pending entry; the leader state has now changed and notifies the followers that the entry is committed. At this point the cluster is in consensus and has moved forward.

Cluster Split

Cluster consensus requires at least two nodes; thus three nodes are needed to create a resilient cluster. In case of a cluster split there will be one part that can achieve consensus, and move forward. However, there are two scenarios that are interesting to look into.

Leader in Majority Part

The leader is in the part of the cluster that can reach consensus - That part will continue to work and move forward. In the other part a new leader can not be elected as consensus can not be reached. When the cluster split is resolved the log entries will replicate to the follower that was on the “wrong” side of the split.

Leader in Minority Part

The leader is in the part of the cluster that can not reach consensus - Proposed changes to the original leader will be entered in the log and replicated to the followers; however the change can not be committed and that part of the cluster can not move forward. At the same time, on the other side of the cluster a leader election will start, due to the lack of leader, and the term will increment and a leader will be elected. The increment in election term here is important. Now when there is a leader, and enough nodes for consensus, this part of the cluster can start to move forward.

When the cluster split is resolved there will be two leaders. However, the leader with the lower term will now have uncommitted entries in the log. When it reseives a message from the leader with a higher term it will roll back the log entries and start to commit the log entries from the leader with higher term.

Next part will be a practical introduction to the Raft algorithm in etcd.