In this section, we'll quickly look at our sample Appointments Calendar application, which attempts to control consistency of specific elements to avoid obvious race conditions. The following is the full code, including the routing and templating:
package main import( "net/http" "html/template" "fmt" "github.com/gorilla/mux" "sync" "strconv" ) type User struct { Name string Times map[int] bool DateHTML template.HTML } type Page struct { Title string Body template.HTML Users map[string] User } var usersInit map[string] bool var userIndex int var validTimes []int var mutex sync.Mutex var Users map[string]User var templates = template.Must(template.New("template").ParseFiles("view_users.html", "register.html")) func register(w http.ResponseWriter, r *http.Request){ fmt.Println("Request to /register") params := mux.Vars(r) name := params["name"] if _,ok := Users[name]; ok { t,_ := template.ParseFiles("generic.txt") page := &Page{ Title: "User already exists", Body: template.HTML("User " + name + " already exists")} t.Execute(w, page) } else { newUser := User { Name: name } initUser(&newUser) Users[name] = newUser t,_ := template.ParseFiles("generic.txt") page := &Page{ Title: "User created!", Body: template.HTML("You have created user "+name)} t.Execute(w, page) } } func dismissData(st1 int, st2 bool) { // Does nothing in particular for now other than avoid Go compiler errors } func formatTime(hour int) string { hourText := hour ampm := "am" if (hour > 11) { ampm = "pm" } if (hour > 12) { hourText = hour - 12; } fmt.Println(ampm) outputString := strconv.FormatInt(int64(hourText),10) + ampm return outputString } func (u User) FormatAvailableTimes() template.HTML { HTML := "" HTML += "<b>"+u.Name+"</b> - " for k,v := range u.Times { dismissData(k,v) if (u.Times[k] == true) { formattedTime := formatTime(k) HTML += "<a href='/schedule/"+u.Name+"/"+strconv.FormatInt(int64(k),10)+"' class='button'>"+formattedTime+"</a> " } else { } } return template.HTML(HTML) } func users(w http.ResponseWriter, r *http.Request) { fmt.Println("Request to /users") t,_ := template.ParseFiles("users.txt") page := &Page{ Title: "View Users", Users: Users} t.Execute(w, page) } func schedule(w http.ResponseWriter, r *http.Request) { fmt.Println("Request to /schedule") params := mux.Vars(r) name := params["name"] time := params["hour"] timeVal,_ := strconv.ParseInt( time, 10, 0 ) intTimeVal := int(timeVal) createURL := "/register/"+name if _,ok := Users[name]; ok { if Users[name].Times[intTimeVal] == true { mutex.Lock() Users[name].Times[intTimeVal] = false mutex.Unlock() fmt.Println("User exists, variable should be modified") t,_ := template.ParseFiles("generic.txt") page := &Page{ Title: "Successfully Scheduled!", Body: template.HTML("This appointment has been scheduled. <a href='/users'>Back to users</a>")} t.Execute(w, page) } else { fmt.Println("User exists, spot is taken!") t,_ := template.ParseFiles("generic.txt") page := &Page{ Title: "Booked!", Body: template.HTML("Sorry, "+name+" is booked for "+time+" <a href='/users'>Back to users</a>")} t.Execute(w, page) } } else { fmt.Println("User does not exist") t,_ := template.ParseFiles("generic.txt") page := &Page{ Title: "User Does Not Exist!", Body: template.HTML( "Sorry, that user does not exist. Click <a href='"+createURL+"'>here</a> to create it. <a href='/users'>Back to users</a>")} t.Execute(w, page) } fmt.Println(name,time) } func defaultPage(w http.ResponseWriter, r *http.Request) { } func initUser(user *User) { user.Times = make(map[int] bool) for i := 9; i < 18; i ++ { user.Times[i] = true } } func main() { Users = make(map[string] User) userIndex = 0 bill := User {Name: "Bill" } initUser(&bill) Users["Bill"] = bill userIndex++ r := mux.NewRouter() r.HandleFunc("/", defaultPage) r.HandleFunc("/users", users) r.HandleFunc("/register/{name:[A-Za-z]+}", register) r.HandleFunc("/schedule/{name:[A-Za-z]+}/{hour:[0-9]+}", schedule) http.Handle("/", r) err := http.ListenAndServe(":1900", nil) if err != nil { // log.Fatal("ListenAndServe:", err) } }
Note that we seeded our application with a user, Bill. If you attempt to hit /register/bill|[email protected]
, the application will report that the user exists.
As we control the most sensitive data through channels, we avoid any race conditions. We can test this in a couple of ways. The first and easiest way is to keep a log of how many successful appointments are registered, and run this with Bill as the default user.
We can then run a concurrent load tester against the action. There are a number of such testers available, including Apache's ab and Siege. For our purposes, we'll use JMeter, primarily because it permits us to test against multiple URLs concurrently.
Although we're not necessarily using JMeter for load testing (rather, we use it to run concurrent tests), load testers can be extraordinarily valuable ways to find bottlenecks in applications at scales that don't yet exist.
For example, if you built a web application that had a blocking element and had 5,000-10,000 requests per day, you may not notice it. But at 5 million-10 million requests per day, it might result in the application crashing.
In the dawn of network servers, this is what happened; servers scaled until one day, suddenly, they couldn't scale further. Load/stress testers allow you to simulate traffic in order to better detect these issues and inefficiencies.
Given that we have one user and eight hours in a day, we should end our script with no more than eight total successful appointments. Of course, if you hit the /register
endpoint, you will see eight times as many users as you've added. The following screenshot shows our benchmark test plan in JMeter:
When you run your application, keep an eye on your console; at the end of our load test, we should see the following message:
Total registered appointments: 8
Had we designed our application as per the initial graphical mockup representation in this chapter (with race conditions), it's plausible—and in fact likely—that we'd register far more appointments than actually existed.
By isolating potential race conditions, we guarantee data consistency and ensure that nobody is waiting on an appointment with an otherwise occupied attendee. The following screenshot is the list we present of all the users and their available appointment times:
The previous screenshot is our initial view that shows us available users and their available time slots. By selecting a timeslot for a user, we'll attempt to book them for that particular time. We'll start with Nathan at 5 p.m.
The following screenshot shows what happens when we attempt to schedule with an available user:
However, if we attempt to book again (even simultaneously), we'll be greeted with a sad message that Nathan cannot see us at 5 p.m, as shown in the following screenshot:
With that, we have a multiuser calendar app that allows for creating new users, scheduling, and blocking double-bookings.
Let's look at a few interesting new points in this application.
First, you will notice that we use a template called generic.txt
for most parts of the application. There's not much to this, only a page title and body filled in by each handler. However, on the /users
endpoint, we use users.txt
as follows:
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf- 8"> <title>{{.Title}}</title> </head> <body> <h1>{{.Title}}</h1> {{range .Users}} <div class="user-row"> {{.FormatAvailableTimes}} </div> {{end}} </body> </html>
We mentioned the range-based functionality in templates, but how does {{.FormatAvailableTimes}}
work? In any given context, we can have type-specific functions that process the data in more complex ways than are available strictly in the template lexer.
In this case, the User
struct is passed to the following line of code:
func (u User) FormatAvailableTimes() template.HTML {
This line of code then performs some conditional analysis and returns a string with some time conversion.
In this example, you can use either a channel to control the flow of User.times
or an explicit mutex as we have here. We don't want to limit all locks, unless absolutely necessary, so we only invoke the Lock()
function if we've determined the request has passed the tests necessary to modify the status of any given user/time pair. The following code shows where we set the availability of a user within a mutual exclusion:
if _,ok := Users[name]; ok { if Users[name].Times[intTimeVal] == true { mutex.Lock() Users[name].Times[intTimeVal] = false mutex.Unlock()
The outer evaluation checks that a user by that name (key) exists. The second evaluation checks that the time availability exists (true). If it does, we lock the variable, set it to false
, and then move onto output rendering.
Without the Lock()
function, many concurrent connections can compromise the consistency of data and cause the user to have more than one appointment in a given hour.