Build an IRC bot in Go with RethinkDB changefeeds

Dan Cannon’s project, GoRethink, is among the most popular and well-maintained third-party client drivers for RethinkDB. Dan recently updated the driver to make it compatible with RethinkDB 1.16, adding support for changefeeds. The language’s native concurrency features make it easy to consume changefeeds in a realtime Go application.

To see GoRethink in action, I built a simple IRC bot that monitors a RethinkDB cluster and sends notifications to an IRC channel when issues are detected. I built the bot with Go, using Dan’s driver and an IRC client library called GoIRC.

Monitor the issues table

As I described in my last blog post, RethinkDB 1.16 introduced a new set of system tables that you can use to monitor and configure a RethinkDB cluster. You can interact the system tables using ReQL queries, just like you would with any other RethinkDB table.

The current_issues table contains a list of problems that currently affect the operation of the cluster. RethinkDB adds items to this table when servers drop from the cluster or other similar incidents occur. When a user intervenes to resolve an issue, the cluster will remove it from the table.

RethinkDB changefeeds provide a way to subscribe to a stream of realtime database updates. I used the following Go code to attach a changefeed to the current_issues table, watching for new issues that are characterized as critical. When issues are found, it prints them to the terminal:

type Issue struct {
    Description, Type string
}

db, err := r.Connect(r.ConnectOpts{Address: "localhost:28015"})
if err != nil {
    log.Fatal("Database connection failed:", err)
}

issues, _ := r.Db("rethinkdb").Table("current_issues").Filter(
    r.Row.Field("critical").Eq(true)).Changes().Field("new_val").Run(db)

go func() {
    var issue Issue
    for issues.Next(&issue) {
      if issue.Type != "" {
        log.Println(issue.Description)
      }
    }
}()

The ReQL expression uses the filter command to match only the issues in which the critical property carries the value true. The changefeed attached to the query will only emit documents that match the filter condition.

When consuming the output of the changefeed, you can wrap the handler in a goroutine (as demonstrated in the code example above) so that it will operate asynchronously in the background instead of blocking execution. Using goroutines and channels for asynchronous programming can simplify the architecture of your realtime application.

The Go driver can unmarshal JSON data returned by your ReQL queries and map the document properties to struct fields. In the example above, I defined a struct called Issue that has Description and Type fields. When I use the Next method to pull a document from the changefeed and assign it to a variable of type Issue, the fields map to the document properties with the same names. You can also optionally use struct field tags to manually associate fields with specific properties.

Make an IRC bot

The GoIRC library makes it relatively easy to create a simple IRC bot. The following code connects to an IRC server and instructs the bot to join a specific channel:

ircConf := irc.NewConfig("mybot")
ircConf.Server = "localhost:6667" 
bot := irc.Client(ircConf)

bot.HandleFunc("connected", func(conn *irc.Conn, line *irc.Line) {
    log.Println("Connected to IRC server")
    conn.Join("#mychannel")
})

To make the IRC bot push cluster issue notifications into the desired channel, I just had to add a few lines to the changefeed handler in the previous code example:

issues, _ := r.Db("rethinkdb").Table("current_issues").Filter(
    r.Row.Field("critical").Eq(true)).Changes().Field("new_val").Run(db)

go func() {
    var issue Issue
    for issues.Next(&issue) {
        if issue.Type != "" {
            text := strings.Split(issue.Description, "\n")[0]
            message := fmt.Sprintf("(%s) %s ...", issue.Type, text)
            bot.Privmsg("#mychannel", message)
        }
    }
}()

I also wanted to give my bot the ability to handle some basic commands from the user. Specifically, I wanted the program to continue running until a user in the IRC channel tells the bot to quit. I created a handler for the privmsg event and set up a channel to keep the bot running until it receives the command:

quit := make(chan bool, 1)

...

bot.HandleFunc("privmsg", func(conn *irc.Conn, line *irc.Line) {
    log.Println("Received:", line.Nick, line.Text())
    if strings.HasPrefix(line.Text(), config.IRC.Nickname) {
        command := strings.Split(line.Text(), " ")[1]
        switch command {
        case "quit":
            log.Println("Received command to quit")
            quit <- true
        }
        ...
    }
})

...

<- quit

I used a switch statement so that I can easily introduce new commands in the future by adding additional cases that match other strings. For now, I’ll keep it simple. The whole bot is implemented in just 80 lines of code, which you can see on GitHub. You can easily adapt this example to make IRC bots that pipe any data you want from your RethinkDB applications into an IRC channel.

Build realtime web apps with Go and RethinkDB

IRC integration is a great exercise, but I also wanted to see what it is like to build realtime web applications with the Go driver. I decided to build a Go version of the simple cluster monitoring application that I demonstrated in my previous blog post.

The new version written in Go is just as succinct as the original Node.js implementation. I used a third-party Socket.io library to broadcast data from a changefeed that monitors RethinkDB’s stats table:

server, _ := socketio.NewServer(nil)

conn, _ := r.Connect(r.ConnectOpts{Address: "localhost:28015"})
stats, _ := r.Db("rethinkdb").Table("stats").Filter(
    r.Row.Field("id").AtIndex(0).Eq("cluster")).Changes().Run(conn)

go func() {
    var change r.WriteChanges
    for stats.Next(&change) {
        server.BroadcastTo("monitor", "stats", change.NewValue)
    }
}()

http.Handle("/socket.io/", server)
http.Handle("/", http.FileServer(http.Dir("public")))
log.Fatal(http.ListenAndServe(":8091", nil))

The frontend, as detailed in the previous blog post, receives the data from Socket.io and graphs it in realtime with Fastly’s Epoch library. You can see the complete source code of the Go version of the cluster monitoring demo on GitHub.

Concluding thoughts

Go is ostensibly a systems language, but it is fairly conducive to web application development. The Go library ecosystem has much of what you need to build modern web applications, including template processors and URL routing frameworks.

Working with JSON in conventional statically-typed languages is often a painful exercise—but it’s not as painful in Go, because you can naturally map complex JSON documents to nested structs. That capability is fairly compelling when working with the output of ReQL queries.

If you’d like to see a more complete example of a realtime web application built with RethinkDB and Go, you can check out Dan’s Todo List demo on GitHub.

Want to try it yourself? Install RethinkDB and check out the thirty-second quickstart.

Resources: