CSS 390: Notes from Lecture 10 (DRAFT)

Recap

Bug Tracking

What's next once you have change management, unit testing, and code reviews? Bug tracking systems!

The bug tracking system is essentially a glorified TODO list.

Structured Data

The bug or ticket is a structured form with fields suitable for searching and reporting. Most systems are customizable so they can be adapted site-specific requirements.

Tracking ticket: ticket to hold links to several related issues.

Workflow

A bug/ticket codifies a workflow

  1. initial report
  2. triage (sorting/prioritization)
  3. assign
  4. accept
  5. fixed/done
  6. QA verification

Triage

The most important part of the bug tracking system is triage: a periodic meeting to (re-)set priorities.

Good Bug Reports

Include detail:

Ideally, testers will spend the time to characterize and narrow down the scope of the problem.

Additional advice:

More on Templates

It is important to encode some sort of version information into the executable program so that the production build can be traced back to the source for trouble-shooting.

To date, used a constant in the file containing the main function, which we had to remember to manually edit.

This is both tedious and error prone.

An alternate approach is to put the version string into a separate package:

package main

import (
	"fmt"
	"version"
)

func main() {
	fmt.Printf("This is version \"%s\"\n", version.Version())
}

Then, we can writ a program to generate the version package. Like this:

package main

import {
	"fmt"
	"io/ioutil"
	"os"
	"os/user"
	"strings"
	"text/template"
	"time"
)

type VersionInfo struct {
	VersionNumber string
	Time string
	User string
	Host string
}

const version = `{{define "version"}}package version
func Version() string {
	return "{{.}}"
}
{{end}}`

func main() {
	rawVersionNumber, err := ioutil.ReadFile("etc/version.txt")
	if err != nil {
		fmt.Fprintf(os.Stderr, "Reading version file")
		os.Exit(1)
	}

	host, err := os.Hostname()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Extracting hostname: %s\n", err)
		os.Exit(1)
	}

	userInfo, err := user.Current()
	if err != nil || userInfo == nil {
		userInfo = &user.User{Name: "unknown"}
	}

	now := time.Now().Format(time.RFC3339Nano)

	tmpl, err := template.New("version").Parse(version)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Parsing token: %s\n", err)
		os.Exit(1)
	}
	tmpl.ExecuteTemplate(os.Stdout, "version",
		&VersionInfo{
			VersionNumber: strings.TrimRight(string(rawVersionNumber), "\n"),
			Time: now,
			User: userInfo.Name,
			Host: host,
		})
}

Sample output:

package version

func Version() string {
	return "{01.00.00 2015-02-01T14:50:02.679727009-08:00 morris yogi}"
}

Thread Pool Revisited

The thread pool implementation presented in lecture 8 was refactored, extracting the inline function literals out to separate functions. Please visit the lecture notes to compare the two versions.

Mutexes vs. Channels

Channels are new and shiny. Their synchonization semantics provide an alternative to conventional mutexes. The general advice tends toward pragmatism: use whatever is simplest for the problem at hand.

The Go wiki offers more specific advice:

Our approach of guarding shared dictionaries and counters using a mutex is sound, but an alternative aproach is to have a single goroutine manage the data and communicate with it via a channel. This may sound like it will be bottleneck, but on closer examination, the bottleneck is no better or worse than using a mutual exclusion. The critical section is the same whether or not it runs in a separate thread.

Benchmarking experiments would have to be conducted to determine which implementation performs best.

While counters and maps are best protected by a mutex, here is an alternate counter implementation that uses a separate thread and communication via channels:

package counter

type request struct {
	// response changgel for Get requests
	resp chan int
	// flag to determine which operation is requested: increment
	// or get
	increment bool
	// variable name
	key string
	// amount to increment (or decrement if negative)
	delta int
}

type Counter struct {
	req chan request
}

// New creates a Counter object
// A counter is a map between name and value, where each value can be
// incremented atomically.
//
// There is also an unimplemented Dump function to return a copy of
// the dictionary (for exporting to another service..
func New() *Counter {
	// A counter object just holds a (private) channel for
	// communication with the goroutine service.
	c := &Counter{
		req: make(chan request),
	}
	// spawn off the service goroutine, passing the counter
	// object to establish communication
	go counter(c)
	// And return the counter object.
	return c
}

// Get returns the value of the given counter object.  It provides a
// conventional interface, so the calling function does not need to
// know that this is implemented by communicating with another
// thread.
func (c *Counter) Get(key string) int {
	// Create a channel to receive the response.
	resp := make(chan int)
	// Send a request to the service routine via the counter's
	// request field.
	c.req <- request{
		// Create channel to receive response
		resp: resp,
		// Operation is get, not increment
		increment: false,
		// Variable to get value of.
		key: key,
	}
	// And return the response when you get it.
	return <-resp
}

// Incr increments (or decrements if delta is negative) a couunter.
// Again, we have a convtional interface that hides the fact that it
// is implemented by communication with another thread (goroutine).
func (c *Counter) Incr(key string, delta int) {
	c.req <- request{
		resp:      nil,
		increment: true,
		key:       key,
		delta:     delta,
	}
}

// counter runs in its own goroutine to service Counter object
// requests.
func counter(c *Counter) {
	// private variable to hold the map
	data := make(map[string]int)
	// range on the channel means we can exit the loop and hence
	// terminate the thread by closing the channel
	for req := range c.req {
		// Service the request
		if req.increment {
			data[req.key] = data[req.key] + req.delta
		} else {
			req.resp <- data[req.key]
		}
	}
}

Here is the corresponding test routine:

package counter

import (
	"sync"
	"testing"
)

const (
	// Symbolic constants to let the compiler find typos.
	Zeus   = "zeus"
	Hera   = "hera"
	Ares   = "ares"
	Athena = "athena"
)

func body(c *Counter, t *testing.T) {
	// Spawn off 4 concurrent threads and wait until they
	// complete.
	var wg sync.WaitGroup
	wg.Add(4)
	go func() {
		c.Incr(Zeus, 2)
		c.Incr(Hera, 1)
		c.Incr(Athena, 1)
		c.Incr(Ares, 1)
		wg.Done()
	}()
	go func() {
		c.Incr("hera", 21)
		c.Incr("zeus", 6)
		c.Incr("ares", 1)
		c.Incr("athena", 4)
		wg.Done()
	}()
	go func() {
		c.Incr("zeus", 2)
		c.Incr("hera", 6)
		c.Incr("athena", 1)
		c.Incr("ares", 1)
		wg.Done()
	}()
	go func() {
		c.Incr("athena", 2)
		c.Incr("hera", 1)
		c.Incr("zeus", 3)
		c.Incr("ares", 1)
		wg.Done()
	}()
	// sync.WaitGroups: wait until all 4 threads report Done.
	// See the documentation.
	wg.Wait()

	expected := map[string]int{
		"zeus":   13,
		"hera":   29,
		"ares":   4,
		"athena": 8,
	}
	for k, v := range expected {
		if v != c.Get(k) {
			t.Errorf("counter %s: expected %d, got %d", k, v, c.Get(k))
		}
	}
}

func TestCounter(t *testing.T) {
	c := New()
	body(c, t)
}

Interface design: More on Counters

The reason for maintaining counters via a map is so the set of counters may be dumped and passed to another service for further analysis. But it does make the interface a little clunky. Serviceable, but slightly clunky.

Here is an alternate implementation which preserve the ability to dump the map while providing a more convenient (and less error-prone) interface. Back to the mutex implementation.

package countervar

import "sync"

type Counter struct {
	name string
	value int
}

var (
	symbolTable = make(map[string]*Counter)
	mutex       sync.RWMutex
)

// New returns a new counter variable (or the existing one of the
// given name if there is one already).
func New(name string) *Counter {
	mutex.Lock()
	defer mutex.Unlock()
	// Get the existing counter if there is one; otherwise create
	// a new counter.  Note the idomatic use of map sematics:
	// returns the zero value (nil for pointer) when the key is
	// not present.
	c := symbolTable[name]
	if c == nil {
		c = &Counter{
			name:  name,
			value: 0,
		}
		symbolTable[name] = c
	}
	return c
}

// Dump returns a copy of the map of counter variables.
func Dump() map[string]int {
	mutex.RLock()
	defer mutex.RUnlock()
	dump := make(map[string]int)
	for k, v := range symbolTable {
		dump[k] = v.value
	}
	return dump
}

func (c *Counter) Get() int {
	mutex.RLock()
	defer mutex.RUnlock()
	return c.value
}

func (c *Counter) Incr(delta int) {
	mutex.Lock()
	defer mutex.Unlock()
	c.value += delta
}

Here is the corresponding test routine. Compare how the counters are used versus the usage of the previous test.

package countervar

import (
	"sync"
	"testing"
)

func body(t *testing.T) {
	zeus := New("zeus")
	hera := New("hera")
	athena := New("athena")
	ares := New("ares")

	var wg sync.WaitGroup
	wg.Add(4)
	go func() {
		zeus.Incr(2)
		hera.Incr(1)
		athena.Incr(1)
		ares.Incr(1)
		wg.Done()
	}()
	go func() {
		hera.Incr(21)
		zeus.Incr(6)
		ares.Incr(1)
		athena.Incr(4)
		wg.Done()
	}()
	go func() {
		zeus.Incr(2)
		hera.Incr(6)
		athena.Incr(1)
		ares.Incr(1)
		wg.Done()
	}()
	go func() {
		athena.Incr(2)
		hera.Incr(1)
		zeus.Incr(3)
		ares.Incr(1)
		wg.Done()
	}()
	wg.Wait()

	expected := map[string]int{
		"zeus":   13,
		"hera":   29,
		"ares":   4,
		"athena": 8,
	}
	actual := Dump()
	for k, v := range expected {
		if v != actual[k] {
			t.Errorf("counter %s: expected %d, got %d", k, v, actual[k])
		}
	}
}

func TestCounter(t *testing.T) {
	body(t)
}