Remake exvar package to be more Go-ish.

It now exports a Var interface (anyone can export their own custom var types now), so users need to create and manage their own vars and mark them as exportable via the Publish function. They are exposed via /debug/vars.

R=r,rsc
APPROVED=r
DELTA=605  (314 added, 186 deleted, 105 changed)
OCL=28143
CL=28239
This commit is contained in:
David Symonds 2009-05-04 15:14:22 -07:00
parent 19c239c9af
commit 2f284948af
4 changed files with 229 additions and 273 deletions

View File

@ -99,7 +99,7 @@ test: test.files
bignum.6: fmt.dirinstall
bufio.6: io.dirinstall os.dirinstall
exec.6: os.dirinstall strings.install
exvar.6: fmt.dirinstall http.dirinstall
exvar.6: fmt.dirinstall http.dirinstall log.install strconv.dirinstall sync.dirinstall
flag.6: fmt.dirinstall os.dirinstall strconv.dirinstall
log.6: fmt.dirinstall io.dirinstall os.dirinstall time.dirinstall
path.6: io.dirinstall

View File

@ -3,231 +3,200 @@
// license that can be found in the LICENSE file.
// The exvar package provides a standardized interface to public variables,
// such as operation counters in servers.
// such as operation counters in servers. It exposes these variables via
// HTTP at /debug/vars in JSON format.
package exvar
import (
"fmt";
"http";
"io";
"log";
"strconv";
"sync";
)
// If mismatched names are used (e.g. calling IncrementInt on a mapVar), the
// var name is silently mapped to these. We will consider variables starting
// with reservedPrefix to be reserved by this package, and so we avoid the
// possibility of a user doing IncrementInt("x-mismatched-map", 1).
// TODO(dsymonds): Enforce this.
const (
reservedPrefix = "x-";
mismatchedInt = reservedPrefix + "mismatched-int";
mismatchedMap = reservedPrefix + "mismatched-map";
mismatchedStr = reservedPrefix + "mismatched-str";
)
// exVar is an abstract type for all exported variables.
type exVar interface {
// Var is an abstract type for all exported variables.
type Var interface {
String() string;
}
// intVar is an integer variable, and satisfies the exVar interface.
type intVar int;
func (i intVar) String() string {
return fmt.Sprint(int(i))
// Int is a 64-bit integer variable, and satisfies the Var interface.
type Int struct {
i int64;
mu sync.Mutex;
}
// mapVar is a map variable, and satisfies the exVar interface.
type mapVar map[string] int;
func (v *Int) String() string {
return strconv.Itoa64(v.i)
}
func (m mapVar) String() string {
s := "map:x"; // TODO(dsymonds): the 'x' should be user-specified!
for k, v := range m {
s += fmt.Sprintf(" %s:%v", k, v)
func (v *Int) Add(delta int64) {
v.mu.Lock();
defer v.mu.Unlock();
v.i += delta;
}
// Map is a string-to-Var map variable, and satisfies the Var interface.
type Map struct {
m map[string] Var;
mu sync.Mutex;
}
// KeyValue represents a single entry in a Map.
type KeyValue struct {
Key string;
Value Var;
}
func (v *Map) String() string {
v.mu.Lock();
defer v.mu.Unlock();
b := new(io.ByteBuffer);
fmt.Fprintf(b, "{");
first := true;
for key, val := range v.m {
if !first {
fmt.Fprintf(b, ", ");
}
fmt.Fprintf(b, "\"%s\": %v", key, val.String());
first = false;
}
return s
fmt.Fprintf(b, "}");
return string(b.Data())
}
// strVar is a string variable, and satisfies the exVar interface.
type strVar string;
func (s strVar) String() string {
return fmt.Sprintf("%q", s)
}
// TODO(dsymonds):
// - dynamic lookup vars (via chan?)
type exVars struct {
vars map[string] exVar;
// TODO(dsymonds): docstrings
}
// Singleton worker goroutine.
// Functions needing access to the global state have to pass a closure to the
// worker channel, which is read by a single workerFunc running in a goroutine.
// Nil values are silently ignored, so you can send nil to the worker channel
// after the closure if you want to block until your work is done. This risks
// blocking you, though. The workSync function wraps this as a convenience.
type workFunction func(*exVars);
// The main worker function that runs in a goroutine.
// It never ends in normal operation.
func startWorkerFunc() <-chan workFunction {
ch := make(chan workFunction);
state := &exVars{ make(map[string] exVar) };
go func() {
for f := range ch {
if f != nil {
f(state)
}
}
}();
return ch
}
var worker = startWorkerFunc();
// workSync will enqueue the given workFunction and wait for it to finish.
func workSync(f workFunction) {
worker <- f;
worker <- nil // will only be sent after f() completes.
}
// getOrInitIntVar either gets or initializes an intVar called name.
func (state *exVars) getOrInitIntVar(name string) *intVar {
if v, ok := state.vars[name]; ok {
// Existing var
if iv, ok := v.(*intVar); ok {
return iv
}
// Type mismatch.
return state.getOrInitIntVar(mismatchedInt)
func (v *Map) Get(key string) Var {
v.mu.Lock();
defer v.mu.Unlock();
if av, ok := v.m[key]; ok {
return av
}
// New var
iv := new(intVar);
state.vars[name] = iv;
return iv
return nil
}
// getOrInitMapVar either gets or initializes a mapVar called name.
func (state *exVars) getOrInitMapVar(name string) *mapVar {
if v, ok := state.vars[name]; ok {
// Existing var
if mv, ok := v.(*mapVar); ok {
return mv
}
// Type mismatch.
return state.getOrInitMapVar(mismatchedMap)
func (v *Map) Set(key string, av Var) {
v.mu.Lock();
defer v.mu.Unlock();
v.m[key] = av;
}
func (v *Map) Add(key string, delta int64) {
v.mu.Lock();
defer v.mu.Unlock();
av, ok := v.m[key];
if !ok {
av = new(Int);
v.m[key] = av;
}
// New var
var m mapVar = make(map[string] int);
state.vars[name] = &m;
return &m
}
// getOrInitStrVar either gets or initializes a strVar called name.
func (state *exVars) getOrInitStrVar(name string) *strVar {
if v, ok := state.vars[name]; ok {
// Existing var
if mv, ok := v.(*strVar); ok {
return mv
}
// Type mismatch.
return state.getOrInitStrVar(mismatchedStr)
// Add to Int; ignore otherwise.
if iv, ok := av.(*Int); ok {
iv.Add(delta);
}
// New var
sv := new(strVar);
state.vars[name] = sv;
return sv
}
// IncrementInt adds inc to the integer-valued var called name.
func IncrementInt(name string, inc int) {
workSync(func(state *exVars) {
*state.getOrInitIntVar(name) += inc
})
// TODO(rsc): Make sure map access in separate thread is safe.
func (v *Map) iterate(c <-chan KeyValue) {
for k, v := range v.m {
c <- KeyValue{ k, v };
}
close(c);
}
// IncrementMapInt adds inc to the keyed value in the map-valued var called name.
func IncrementMapInt(name string, key string, inc int) {
workSync(func(state *exVars) {
mv := state.getOrInitMapVar(name);
if v, ok := mv[key]; ok {
mv[key] += inc
} else {
mv[key] = inc
func (v *Map) Iter() <-chan KeyValue {
c := make(chan KeyValue);
go v.iterate(c);
return c
}
// String is a string variable, and satisfies the Var interface.
type String struct {
s string;
}
func (v *String) String() string {
return strconv.Quote(v.s)
}
func (v *String) Set(value string) {
v.s = value;
}
// All published variables.
var vars map[string] Var = make(map[string] Var);
var mutex sync.Mutex;
// Publish declares an named exported variable. This should be called from a
// package's init function when it creates its Vars. If the name is already
// registered then this will log.Crash.
func Publish(name string, v Var) {
mutex.Lock();
defer mutex.Unlock();
if _, existing := vars[name]; existing {
log.Crash("Reuse of exported var name:", name);
}
vars[name] = v;
}
// Get retrieves a named exported variable.
func Get(name string) Var {
if v, ok := vars[name]; ok {
return v
}
return nil
}
// Convenience functions for creating new exported variables.
func NewInt(name string) *Int {
v := new(Int);
Publish(name, v);
return v
}
func NewMap(name string) *Map {
v := new(Map);
v.m = make(map[string] Var);
Publish(name, v);
return v
}
func NewString(name string) *String {
v := new(String);
Publish(name, v);
return v
}
// TODO(rsc): Make sure map access in separate thread is safe.
func iterate(c <-chan KeyValue) {
for k, v := range vars {
c <- KeyValue{ k, v };
}
close(c);
}
func Iter() <-chan KeyValue {
c := make(chan KeyValue);
go iterate(c);
return c
}
func exvarHandler(c *http.Conn, req *http.Request) {
c.SetHeader("content-type", "application/json; charset=utf-8");
fmt.Fprintf(c, "{\n");
first := true;
for name, value := range vars {
if !first {
fmt.Fprintf(c, ",\n");
}
})
first = false;
fmt.Fprintf(c, " %q: %s", name, value);
}
fmt.Fprintf(c, "\n}\n");
}
// SetInt sets the integer-valued var called name to value.
func SetInt(name string, value int) {
workSync(func(state *exVars) {
*state.getOrInitIntVar(name) = value
})
}
// SetMapInt sets the keyed value in the map-valued var called name.
func SetMapInt(name string, key string, value int) {
workSync(func(state *exVars) {
state.getOrInitMapVar(name)[key] = value
})
}
// SetStr sets the string-valued var called name to value.
func SetStr(name string, value string) {
workSync(func(state *exVars) {
*state.getOrInitStrVar(name) = value
})
}
// GetInt retrieves an integer-valued var called name.
func GetInt(name string) int {
var i int;
workSync(func(state *exVars) {
i = *state.getOrInitIntVar(name)
});
return i
}
// GetMapInt retrieves the keyed value for a map-valued var called name.
func GetMapInt(name string, key string) int {
var i int;
var ok bool;
workSync(func(state *exVars) {
i, ok = state.getOrInitMapVar(name)[key]
});
return i
}
// GetStr retrieves a string-valued var called name.
func GetStr(name string) string {
var s string;
workSync(func(state *exVars) {
s = *state.getOrInitStrVar(name)
});
return s
}
// String produces a string of all the vars in textual format.
func String() string {
s := "";
workSync(func(state *exVars) {
for name, value := range state.vars {
s += fmt.Sprintln(name, value)
}
});
return s
}
// ExvarHandler is a HTTP handler that displays exported variables.
// Use it like this:
// http.Handle("/exvar", http.HandlerFunc(exvar.ExvarHandler));
func ExvarHandler(c *http.Conn, req *http.Request) {
// TODO(dsymonds): Support different output= args.
c.SetHeader("content-type", "text/plain; charset=utf-8");
io.WriteString(c, String());
func init() {
http.Handle("/debug/vars", http.HandlerFunc(exvarHandler));
}

View File

@ -7,99 +7,74 @@ package exvar
import (
"exvar";
"fmt";
"json";
"testing";
)
func TestSimpleCounter(t *testing.T) {
// Unknown exvar should be zero.
x := GetInt("requests");
if x != 0 {
t.Errorf("GetInt(nonexistent) = %v, want 0", x)
func TestInt(t *testing.T) {
reqs := NewInt("requests");
if reqs.i != 0 {
t.Errorf("reqs.i = %v, want 4", reqs.i)
}
if reqs != Get("requests").(*Int) {
t.Errorf("Get() failed.")
}
IncrementInt("requests", 1);
IncrementInt("requests", 3);
x = GetInt("requests");
if x != 4 {
t.Errorf("GetInt('requests') = %v, want 4", x)
reqs.Add(1);
reqs.Add(3);
if reqs.i != 4 {
t.Errorf("reqs.i = %v, want 4", reqs.i)
}
out := String();
if out != "requests 4\n" {
t.Errorf("String() = \"%v\", want \"requests 4\n\"",
out);
if s := reqs.String(); s != "4" {
t.Errorf("reqs.String() = %q, want \"4\"", s);
}
}
func TestStringVar(t *testing.T) {
// Unknown exvar should be empty string.
if s := GetStr("name"); s != "" {
t.Errorf("GetStr(nonexistent) = %q, want ''", s)
func TestString(t *testing.T) {
name := NewString("my-name");
if name.s != "" {
t.Errorf("name.s = %q, want \"\"", name.s)
}
SetStr("name", "Mike");
if s := GetStr("name"); s != "Mike" {
t.Errorf("GetStr('name') = %q, want 'Mike'", s)
}
}
func TestMismatchedCounters(t *testing.T) {
// Make sure some vars exist.
GetInt("requests");
GetMapInt("colours", "red");
GetStr("name");
IncrementInt("colours", 1);
if x := GetInt("x-mismatched-int"); x != 1 {
t.Errorf("GetInt('x-mismatched-int') = %v, want 1", x)
name.Set("Mike");
if name.s != "Mike" {
t.Errorf("name.s = %q, want \"Mike\"", name.s)
}
IncrementMapInt("requests", "orange", 1);
if x := GetMapInt("x-mismatched-map", "orange"); x != 1 {
t.Errorf("GetMapInt('x-mismatched-map', 'orange') = %v, want 1", x)
}
SetStr("requests", "apple");
if s := GetStr("x-mismatched-str"); s != "apple" {
t.Errorf("GetStr('x-mismatched-str') = %q, want 'apple'", s)
if s := name.String(); s != "\"Mike\"" {
t.Errorf("reqs.String() = %q, want \"\"Mike\"\"", s);
}
}
func TestMapCounter(t *testing.T) {
// Unknown exvar should be zero.
if x := GetMapInt("colours", "red"); x != 0 {
t.Errorf("GetMapInt(non, existent) = %v, want 0", x)
colours := NewMap("bike-shed-colours");
colours.Add("red", 1);
colours.Add("red", 2);
colours.Add("blue", 4);
if x := colours.m["red"].(*Int).i; x != 3 {
t.Errorf("colours.m[\"red\"] = %v, want 3", x)
}
if x := colours.m["blue"].(*Int).i; x != 4 {
t.Errorf("colours.m[\"blue\"] = %v, want 4", x)
}
IncrementMapInt("colours", "red", 1);
IncrementMapInt("colours", "red", 2);
IncrementMapInt("colours", "blue", 4);
if x := GetMapInt("colours", "red"); x != 3 {
t.Errorf("GetMapInt('colours', 'red') = %v, want 3", x)
// colours.String() should be '{"red":3, "blue":4}',
// though the order of red and blue could vary.
s := colours.String();
j, ok, errtok := json.StringToJson(s);
if !ok {
t.Errorf("colours.String() isn't valid JSON: %v", errtok)
}
if x := GetMapInt("colours", "blue"); x != 4 {
t.Errorf("GetMapInt('colours', 'blue') = %v, want 4", x)
if j.Kind() != json.MapKind {
t.Error("colours.String() didn't produce a map.")
}
// TODO(dsymonds): Test String()
}
func hammer(name string, total int, done chan <- int) {
for i := 0; i < total; i++ {
IncrementInt(name, 1)
red := j.Get("red");
if red.Kind() != json.NumberKind {
t.Error("red.Kind() is not a NumberKind.")
}
done <- 1
}
func TestHammer(t *testing.T) {
SetInt("hammer-times", 0);
sync := make(chan int);
hammer_times := int(1e5);
go hammer("hammer-times", hammer_times, sync);
go hammer("hammer-times", hammer_times, sync);
<-sync;
<-sync;
if final := GetInt("hammer-times"); final != 2 * hammer_times {
t.Errorf("hammer-times = %v, want %v", final, 2 * hammer_times)
if x := red.Number(); x != 3 {
t.Error("red = %v, want 3", x)
}
}

View File

@ -17,8 +17,9 @@ import (
// hello world, the web server
var helloRequests = exvar.NewInt("hello-requests");
func HelloServer(c *http.Conn, req *http.Request) {
exvar.IncrementInt("hello-requests", 1);
helloRequests.Add(1);
io.WriteString(c, "hello, world!\n");
}
@ -27,16 +28,23 @@ type Counter struct {
n int;
}
// This makes Counter satisfy the exvar.Var interface, so we can export
// it directly.
func (ctr *Counter) String() string {
return fmt.Sprintf("%d", ctr.n)
}
func (ctr *Counter) ServeHTTP(c *http.Conn, req *http.Request) {
exvar.IncrementInt("counter-requests", 1);
fmt.Fprintf(c, "counter = %d\n", ctr.n);
ctr.n++;
}
// simple file server
var webroot = flag.String("root", "/home/rsc", "web root directory")
var pathVar = exvar.NewMap("file-requests");
func FileServer(c *http.Conn, req *http.Request) {
c.SetHeader("content-type", "text/plain; charset=utf-8");
pathVar.Add(req.Url.Path, 1);
path := *webroot + req.Url.Path; // TODO: insecure: use os.CleanName
f, err := os.Open(path, os.O_RDONLY, 0);
if err != nil {
@ -89,13 +97,17 @@ func (ch Chan) ServeHTTP(c *http.Conn, req *http.Request) {
func main() {
flag.Parse();
http.Handle("/counter", new(Counter));
// The counter is published as a variable directly.
ctr := new(Counter);
http.Handle("/counter", ctr);
exvar.Publish("counter", ctr);
http.Handle("/go/", http.HandlerFunc(FileServer));
http.Handle("/flags", http.HandlerFunc(FlagServer));
http.Handle("/args", http.HandlerFunc(ArgServer));
http.Handle("/go/hello", http.HandlerFunc(HelloServer));
http.Handle("/chan", ChanCreate());
http.Handle("/exvar", http.HandlerFunc(exvar.ExvarHandler));
err := http.ListenAndServe(":12345", nil);
if err != nil {
panic("ListenAndServe: ", err.String())