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:
- The GoRethink driver
- The official GoRethink docs
- A simple GoRethink example
- Dan’s GoRethink Todo List demo
- Go port of cluster status demo
- The RethinkDB IRC bot source code