Commit 2f284948 authored by David Symonds's avatar David Symonds

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
parent 19c239c9
...@@ -99,7 +99,7 @@ test: test.files ...@@ -99,7 +99,7 @@ test: test.files
bignum.6: fmt.dirinstall bignum.6: fmt.dirinstall
bufio.6: io.dirinstall os.dirinstall bufio.6: io.dirinstall os.dirinstall
exec.6: os.dirinstall strings.install 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 flag.6: fmt.dirinstall os.dirinstall strconv.dirinstall
log.6: fmt.dirinstall io.dirinstall os.dirinstall time.dirinstall log.6: fmt.dirinstall io.dirinstall os.dirinstall time.dirinstall
path.6: io.dirinstall path.6: io.dirinstall
......
...@@ -3,231 +3,200 @@ ...@@ -3,231 +3,200 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// The exvar package provides a standardized interface to public variables, // 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 package exvar
import ( import (
"fmt"; "fmt";
"http"; "http";
"io"; "io";
"log";
"strconv";
"sync";
) )
// If mismatched names are used (e.g. calling IncrementInt on a mapVar), the // Var is an abstract type for all exported variables.
// var name is silently mapped to these. We will consider variables starting type Var interface {
// 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 {
String() string; String() string;
} }
// intVar is an integer variable, and satisfies the exVar interface. // Int is a 64-bit integer variable, and satisfies the Var interface.
type intVar int; type Int struct {
i int64;
mu sync.Mutex;
}
func (i intVar) String() string { func (v *Int) String() string {
return fmt.Sprint(int(i)) return strconv.Itoa64(v.i)
} }
// mapVar is a map variable, and satisfies the exVar interface. func (v *Int) Add(delta int64) {
type mapVar map[string] int; v.mu.Lock();
defer v.mu.Unlock();
v.i += delta;
}
func (m mapVar) String() string { // Map is a string-to-Var map variable, and satisfies the Var interface.
s := "map:x"; // TODO(dsymonds): the 'x' should be user-specified! type Map struct {
for k, v := range m { m map[string] Var;
s += fmt.Sprintf(" %s:%v", k, v) mu sync.Mutex;
}
return s
} }
// strVar is a string variable, and satisfies the exVar interface. // KeyValue represents a single entry in a Map.
type strVar string; type KeyValue struct {
Key string;
Value Var;
}
func (s strVar) String() string { func (v *Map) String() string {
return fmt.Sprintf("%q", s) 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;
}
fmt.Fprintf(b, "}");
return string(b.Data())
} }
// TODO(dsymonds): func (v *Map) Get(key string) Var {
// - dynamic lookup vars (via chan?) v.mu.Lock();
defer v.mu.Unlock();
if av, ok := v.m[key]; ok {
return av
}
return nil
}
type exVars struct { func (v *Map) Set(key string, av Var) {
vars map[string] exVar; v.mu.Lock();
// TODO(dsymonds): docstrings defer v.mu.Unlock();
v.m[key] = av;
} }
// Singleton worker goroutine. func (v *Map) Add(key string, delta int64) {
// Functions needing access to the global state have to pass a closure to the v.mu.Lock();
// worker channel, which is read by a single workerFunc running in a goroutine. defer v.mu.Unlock();
// Nil values are silently ignored, so you can send nil to the worker channel av, ok := v.m[key];
// after the closure if you want to block until your work is done. This risks if !ok {
// blocking you, though. The workSync function wraps this as a convenience. av = new(Int);
v.m[key] = av;
}
type workFunction func(*exVars); // Add to Int; ignore otherwise.
if iv, ok := av.(*Int); ok {
iv.Add(delta);
}
}
// The main worker function that runs in a goroutine. // TODO(rsc): Make sure map access in separate thread is safe.
// It never ends in normal operation. func (v *Map) iterate(c <-chan KeyValue) {
func startWorkerFunc() <-chan workFunction { for k, v := range v.m {
ch := make(chan workFunction); c <- KeyValue{ k, v };
}
close(c);
}
state := &exVars{ make(map[string] exVar) }; func (v *Map) Iter() <-chan KeyValue {
c := make(chan KeyValue);
go v.iterate(c);
return c
}
go func() { // String is a string variable, and satisfies the Var interface.
for f := range ch { type String struct {
if f != nil { s string;
f(state)
}
}
}();
return ch
} }
var worker = startWorkerFunc(); func (v *String) String() string {
return strconv.Quote(v.s)
}
// workSync will enqueue the given workFunction and wait for it to finish. func (v *String) Set(value string) {
func workSync(f workFunction) { v.s = value;
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 { // All published variables.
if v, ok := state.vars[name]; ok { var vars map[string] Var = make(map[string] Var);
// Existing var var mutex sync.Mutex;
if iv, ok := v.(*intVar); ok {
return iv // 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
// Type mismatch. // registered then this will log.Crash.
return state.getOrInitIntVar(mismatchedInt) func Publish(name string, v Var) {
} mutex.Lock();
// New var defer mutex.Unlock();
iv := new(intVar); if _, existing := vars[name]; existing {
state.vars[name] = iv; log.Crash("Reuse of exported var name:", name);
return iv
}
// 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)
}
// 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)
} }
// New var vars[name] = v;
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
})
}
// 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
}
})
} }
// SetInt sets the integer-valued var called name to value. // Get retrieves a named exported variable.
func SetInt(name string, value int) { func Get(name string) Var {
workSync(func(state *exVars) { if v, ok := vars[name]; ok {
*state.getOrInitIntVar(name) = value return v
}) }
return nil
} }
// SetMapInt sets the keyed value in the map-valued var called name. // Convenience functions for creating new exported variables.
func SetMapInt(name string, key string, value int) {
workSync(func(state *exVars) { func NewInt(name string) *Int {
state.getOrInitMapVar(name)[key] = value v := new(Int);
}) Publish(name, v);
return v
} }
// SetStr sets the string-valued var called name to value. func NewMap(name string) *Map {
func SetStr(name string, value string) { v := new(Map);
workSync(func(state *exVars) { v.m = make(map[string] Var);
*state.getOrInitStrVar(name) = value Publish(name, v);
}) return v
} }
// GetInt retrieves an integer-valued var called name. func NewString(name string) *String {
func GetInt(name string) int { v := new(String);
var i int; Publish(name, v);
workSync(func(state *exVars) { return v
i = *state.getOrInitIntVar(name)
});
return i
} }
// GetMapInt retrieves the keyed value for a map-valued var called name. // TODO(rsc): Make sure map access in separate thread is safe.
func GetMapInt(name string, key string) int { func iterate(c <-chan KeyValue) {
var i int; for k, v := range vars {
var ok bool; c <- KeyValue{ k, v };
workSync(func(state *exVars) { }
i, ok = state.getOrInitMapVar(name)[key] close(c);
});
return i
} }
// GetStr retrieves a string-valued var called name. func Iter() <-chan KeyValue {
func GetStr(name string) string { c := make(chan KeyValue);
var s string; go iterate(c);
workSync(func(state *exVars) { return c
s = *state.getOrInitStrVar(name)
});
return s
} }
// String produces a string of all the vars in textual format. func exvarHandler(c *http.Conn, req *http.Request) {
func String() string { c.SetHeader("content-type", "application/json; charset=utf-8");
s := ""; fmt.Fprintf(c, "{\n");
workSync(func(state *exVars) { first := true;
for name, value := range state.vars { for name, value := range vars {
s += fmt.Sprintln(name, value) if !first {
fmt.Fprintf(c, ",\n");
} }
}); first = false;
return s fmt.Fprintf(c, " %q: %s", name, value);
}
fmt.Fprintf(c, "\n}\n");
} }
// ExvarHandler is a HTTP handler that displays exported variables. func init() {
// Use it like this: http.Handle("/debug/vars", http.HandlerFunc(exvarHandler));
// 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());
} }
...@@ -7,99 +7,74 @@ package exvar ...@@ -7,99 +7,74 @@ package exvar
import ( import (
"exvar"; "exvar";
"fmt"; "fmt";
"json";
"testing"; "testing";
) )
func TestSimpleCounter(t *testing.T) { func TestInt(t *testing.T) {
// Unknown exvar should be zero. reqs := NewInt("requests");
x := GetInt("requests"); if reqs.i != 0 {
if x != 0 { t.Errorf("reqs.i = %v, want 4", reqs.i)
t.Errorf("GetInt(nonexistent) = %v, want 0", x)
} }
if reqs != Get("requests").(*Int) {
IncrementInt("requests", 1); t.Errorf("Get() failed.")
IncrementInt("requests", 3);
x = GetInt("requests");
if x != 4 {
t.Errorf("GetInt('requests') = %v, want 4", x)
}
out := String();
if out != "requests 4\n" {
t.Errorf("String() = \"%v\", want \"requests 4\n\"",
out);
} }
}
func TestStringVar(t *testing.T) { reqs.Add(1);
// Unknown exvar should be empty string. reqs.Add(3);
if s := GetStr("name"); s != "" { if reqs.i != 4 {
t.Errorf("GetStr(nonexistent) = %q, want ''", s) t.Errorf("reqs.i = %v, want 4", reqs.i)
} }
SetStr("name", "Mike"); if s := reqs.String(); s != "4" {
if s := GetStr("name"); s != "Mike" { t.Errorf("reqs.String() = %q, want \"4\"", s);
t.Errorf("GetStr('name') = %q, want 'Mike'", s)
} }
} }
func TestMismatchedCounters(t *testing.T) { func TestString(t *testing.T) {
// Make sure some vars exist. name := NewString("my-name");
GetInt("requests"); if name.s != "" {
GetMapInt("colours", "red"); t.Errorf("name.s = %q, want \"\"", name.s)
GetStr("name");
IncrementInt("colours", 1);
if x := GetInt("x-mismatched-int"); x != 1 {
t.Errorf("GetInt('x-mismatched-int') = %v, want 1", x)
} }
IncrementMapInt("requests", "orange", 1); name.Set("Mike");
if x := GetMapInt("x-mismatched-map", "orange"); x != 1 { if name.s != "Mike" {
t.Errorf("GetMapInt('x-mismatched-map', 'orange') = %v, want 1", x) t.Errorf("name.s = %q, want \"Mike\"", name.s)
} }
SetStr("requests", "apple"); if s := name.String(); s != "\"Mike\"" {
if s := GetStr("x-mismatched-str"); s != "apple" { t.Errorf("reqs.String() = %q, want \"\"Mike\"\"", s);
t.Errorf("GetStr('x-mismatched-str') = %q, want 'apple'", s)
} }
} }
func TestMapCounter(t *testing.T) { func TestMapCounter(t *testing.T) {
// Unknown exvar should be zero. colours := NewMap("bike-shed-colours");
if x := GetMapInt("colours", "red"); x != 0 {
t.Errorf("GetMapInt(non, existent) = %v, want 0", x)
}
IncrementMapInt("colours", "red", 1); colours.Add("red", 1);
IncrementMapInt("colours", "red", 2); colours.Add("red", 2);
IncrementMapInt("colours", "blue", 4); colours.Add("blue", 4);
if x := GetMapInt("colours", "red"); x != 3 { if x := colours.m["red"].(*Int).i; x != 3 {
t.Errorf("GetMapInt('colours', 'red') = %v, want 3", x) t.Errorf("colours.m[\"red\"] = %v, want 3", x)
} }
if x := GetMapInt("colours", "blue"); x != 4 { if x := colours.m["blue"].(*Int).i; x != 4 {
t.Errorf("GetMapInt('colours', 'blue') = %v, want 4", x) t.Errorf("colours.m[\"blue\"] = %v, want 4", x)
} }
// TODO(dsymonds): Test String() // colours.String() should be '{"red":3, "blue":4}',
} // though the order of red and blue could vary.
s := colours.String();
func hammer(name string, total int, done chan <- int) { j, ok, errtok := json.StringToJson(s);
for i := 0; i < total; i++ { if !ok {
IncrementInt(name, 1) t.Errorf("colours.String() isn't valid JSON: %v", errtok)
} }
done <- 1 if j.Kind() != json.MapKind {
} t.Error("colours.String() didn't produce a map.")
}
func TestHammer(t *testing.T) { red := j.Get("red");
SetInt("hammer-times", 0); if red.Kind() != json.NumberKind {
sync := make(chan int); t.Error("red.Kind() is not a NumberKind.")
hammer_times := int(1e5); }
go hammer("hammer-times", hammer_times, sync); if x := red.Number(); x != 3 {
go hammer("hammer-times", hammer_times, sync); t.Error("red = %v, want 3", x)
<-sync;
<-sync;
if final := GetInt("hammer-times"); final != 2 * hammer_times {
t.Errorf("hammer-times = %v, want %v", final, 2 * hammer_times)
} }
} }
...@@ -17,8 +17,9 @@ import ( ...@@ -17,8 +17,9 @@ import (
// hello world, the web server // hello world, the web server
var helloRequests = exvar.NewInt("hello-requests");
func HelloServer(c *http.Conn, req *http.Request) { func HelloServer(c *http.Conn, req *http.Request) {
exvar.IncrementInt("hello-requests", 1); helloRequests.Add(1);
io.WriteString(c, "hello, world!\n"); io.WriteString(c, "hello, world!\n");
} }
...@@ -27,16 +28,23 @@ type Counter struct { ...@@ -27,16 +28,23 @@ type Counter struct {
n int; 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) { func (ctr *Counter) ServeHTTP(c *http.Conn, req *http.Request) {
exvar.IncrementInt("counter-requests", 1);
fmt.Fprintf(c, "counter = %d\n", ctr.n); fmt.Fprintf(c, "counter = %d\n", ctr.n);
ctr.n++; ctr.n++;
} }
// simple file server // simple file server
var webroot = flag.String("root", "/home/rsc", "web root directory") var webroot = flag.String("root", "/home/rsc", "web root directory")
var pathVar = exvar.NewMap("file-requests");
func FileServer(c *http.Conn, req *http.Request) { func FileServer(c *http.Conn, req *http.Request) {
c.SetHeader("content-type", "text/plain; charset=utf-8"); 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 path := *webroot + req.Url.Path; // TODO: insecure: use os.CleanName
f, err := os.Open(path, os.O_RDONLY, 0); f, err := os.Open(path, os.O_RDONLY, 0);
if err != nil { if err != nil {
...@@ -89,13 +97,17 @@ func (ch Chan) ServeHTTP(c *http.Conn, req *http.Request) { ...@@ -89,13 +97,17 @@ func (ch Chan) ServeHTTP(c *http.Conn, req *http.Request) {
func main() { func main() {
flag.Parse(); 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("/go/", http.HandlerFunc(FileServer));
http.Handle("/flags", http.HandlerFunc(FlagServer)); http.Handle("/flags", http.HandlerFunc(FlagServer));
http.Handle("/args", http.HandlerFunc(ArgServer)); http.Handle("/args", http.HandlerFunc(ArgServer));
http.Handle("/go/hello", http.HandlerFunc(HelloServer)); http.Handle("/go/hello", http.HandlerFunc(HelloServer));
http.Handle("/chan", ChanCreate()); http.Handle("/chan", ChanCreate());
http.Handle("/exvar", http.HandlerFunc(exvar.ExvarHandler));
err := http.ListenAndServe(":12345", nil); err := http.ListenAndServe(":12345", nil);
if err != nil { if err != nil {
panic("ListenAndServe: ", err.String()) panic("ListenAndServe: ", err.String())
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment