A multiuser Appointments Calendar

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.

Tip

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:

A multiuser Appointments Calendar

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:

A multiuser Appointments Calendar

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:

A multiuser Appointments Calendar

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:

A multiuser Appointments Calendar

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.172.146