dashboard: general update messages and graceful exit

This commit is contained in:
Kurkó Mihály 2017-08-03 09:30:39 +03:00
parent bd416effe8
commit 1d298a8338

View file

@ -41,26 +41,26 @@ const (
) )
var ( var (
nextId uint32 = 0 // Next connection id nextId uint32 // Next connection id
) )
type dashboard struct { type dashboard struct {
config *Config config *Config
listener net.Listener listener net.Listener
conns []*client // Currently live websocket connections conns []*client // Currently live websocket connections
Metrics *metricSamples `json:"metrics,omitempty"`
Stats *status `json:"stats,omitempty"`
lock sync.RWMutex // Lock protecting the dashboard's internals
Metrics *metricSamples `json:"metrics,omitempty"` closing map[*chan chan error]bool // Channels used for graceful exit
Stats *status `json:"stats,omitempty"` mapLock sync.RWMutex // Lock protecting the closing map's internals
lock sync.RWMutex // Lock protecting the dashboard's internals
quit chan struct{} // Channel used for graceful exit
} }
type client struct { type client struct {
conn *websocket.Conn // Particular live websocket connection conn *websocket.Conn // Particular live websocket connection
logger log.Logger // Logger for the particular live websocket connection msg chan *map[string]interface{} // Message queue for the update messages
logger log.Logger // Logger for the particular live websocket connection
} }
type metricSamples struct { type metricSamples struct {
@ -83,7 +83,7 @@ func New(config *Config) (*dashboard, error) {
return &dashboard{ return &dashboard{
config: config, config: config,
Metrics: &metricSamples{}, Metrics: &metricSamples{},
quit: make(chan struct{}), closing: make(map[*chan chan error]bool),
}, nil }, nil
} }
@ -126,7 +126,12 @@ func (db *dashboard) Stop() error {
log.Warn("Failed to close listener", "err", err) log.Warn("Failed to close listener", "err", err)
} }
close(db.quit) // Notifies collectData and apiHandler errc := make(chan error)
for closing := range db.closing {
*closing <- errc
<-errc
close(*closing)
}
for _, c := range db.conns { for _, c := range db.conns {
if err := c.conn.Close(); err != nil { if err := c.conn.Close(); err != nil {
@ -143,68 +148,86 @@ func (db *dashboard) webHandler(w http.ResponseWriter, r *http.Request) {
log.Info("Request", "URL", r.URL) log.Info("Request", "URL", r.URL)
path := r.URL.String() path := r.URL.String()
if path == "/" {
path = "/dashboard.html"
}
// If the path of the assets is manually set // If the path of the assets is manually set
if db.config.Assets != "" { if db.config.Assets != "" {
// Create the filename for ReadFile // Create the filename for ReadFile
var buffer bytes.Buffer var buffer bytes.Buffer
buffer.WriteString(db.config.Assets) buffer.WriteString(db.config.Assets)
switch path { buffer.WriteString(path)
case "/":
buffer.WriteString("/dashboard.html")
default:
buffer.WriteString(path)
}
file, err := ioutil.ReadFile(buffer.String()) file, err := ioutil.ReadFile(buffer.String())
if err != nil { if err != nil {
// TODO (kurkomisi): Warn or Crit?
log.Warn("Failed to read file", "err", err) log.Warn("Failed to read file", "err", err)
// TODO (kurkomisi): Should I inform the client? http.Error(w, "not found", http.StatusNotFound)
return return
} }
w.Write(file) w.Write(file)
return return
} }
webapp, err := Asset(path[1:])
switch path { if err != nil {
case "/": log.Warn("Failed to load the asset", "path", path, "err", err)
index, err := Asset("dashboard.html") http.Error(w, "not found", http.StatusNotFound)
if err != nil { return
log.Warn("Failed to load the index", "err", err)
return
}
w.Write(index)
default:
webapp, err := Asset(path[1:])
if err != nil {
log.Warn("Failed to load the asset", "path", path, "err", err)
return
}
w.Write(webapp)
} }
w.Write(webapp)
} }
// apiHandler handles requests for the dashboard. // apiHandler handles requests for the dashboard.
func (db *dashboard) apiHandler(conn *websocket.Conn) { func (db *dashboard) apiHandler(conn *websocket.Conn) {
client := &client{ client := &client{
conn: conn, conn: conn,
msg: make(chan *map[string]interface{}, 128),
logger: log.New("id", atomic.AddUint32(&nextId, 1)), logger: log.New("id", atomic.AddUint32(&nextId, 1)),
} }
// Start tracking the connection and drop at connection loss loss := make(chan int)
// Start listening for messages to send.
go func() {
closing := db.addClosing()
defer db.removeClosing(closing)
for {
select {
case errc := <-*closing:
errc <- nil
return
case val := <-loss:
loss <- val - 1
return
case msg := <-client.msg:
if err := websocket.JSON.Send(client.conn, msg); err != nil {
client.logger.Warn("Failed to send the message", "msg", msg, "err", err)
// TODO (kurkomisi): Handle message loss
}
}
}
}()
// Send the past data.
client.msg <- &map[string]interface{}{
"metrics": db.Metrics,
}
// Start tracking the connection and drop at connection loss.
db.lock.Lock() db.lock.Lock()
db.conns = append(db.conns, client) db.conns = append(db.conns, client)
db.lock.Unlock() db.lock.Unlock()
db.sendHistory(client)
closed := make(chan bool)
go func() { go func() {
closing := db.addClosing()
defer db.removeClosing(closing)
select { select {
case <-db.quit: case errc := <-*closing:
case <-closed: errc <- nil
case val := <-loss:
loss <- val - 1
db.lock.Lock() db.lock.Lock()
for i, c := range db.conns { for i, c := range db.conns {
if c.conn == client.conn { if c.conn == client.conn {
@ -219,7 +242,10 @@ func (db *dashboard) apiHandler(conn *websocket.Conn) {
for { for {
fail := []byte{} fail := []byte{}
if _, err := conn.Read(fail); err != nil { if _, err := conn.Read(fail); err != nil {
closed <- true loss <- 2
if val := <-loss; val > 0 {
loss <- val
}
return return
} }
// Ignore all messages // Ignore all messages
@ -228,12 +254,15 @@ func (db *dashboard) apiHandler(conn *websocket.Conn) {
// collectData collects the required data to plot on the dashboard. // collectData collects the required data to plot on the dashboard.
func (db *dashboard) collectData() { func (db *dashboard) collectData() {
closing := db.addClosing()
defer db.removeClosing(closing)
for { for {
select { select {
case <-db.quit: case errc := <-*closing:
errc <- nil
return return
case <-time.After(db.config.Refresh): case <-time.After(db.config.Refresh):
now := time.Now() now := time.Now()
traffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1() traffic := metrics.DefaultRegistry.Get("p2p/InboundTraffic").(metrics.Meter).Rate1()
memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1() memoryInUse := metrics.DefaultRegistry.Get("system/memory/inuse").(metrics.Meter).Rate1()
@ -246,9 +275,8 @@ func (db *dashboard) collectData() {
Value: memoryInUse, Value: memoryInUse,
} }
// TODO (kurkomisi): do I need to ensure the correct order?
// TODO (kurkomisi): do not mix traffic with processor! // TODO (kurkomisi): do not mix traffic with processor!
go db.update(traff, memory) db.update(traff, memory)
} }
} }
} }
@ -270,24 +298,30 @@ func (db *dashboard) update(processor *data, memory *data) {
} }
db.Metrics.Memory = append(db.Metrics.Memory[first:], memory) db.Metrics.Memory = append(db.Metrics.Memory[first:], memory)
for _, c := range db.conns {
msg := &map[string]interface{}{
"processor": processor,
"memory": memory,
}
if err := websocket.JSON.Send(c.conn, msg); err != nil {
c.logger.Warn("Failed to update dashboard", "msg", msg, "err", err)
}
}
}
// sendHistory sends the past data through a newly registered websocket connection.
func (db *dashboard) sendHistory(c *client) {
msg := &map[string]interface{}{ msg := &map[string]interface{}{
"metrics": db.Metrics, "processor": processor,
"memory": memory,
} }
if err := websocket.JSON.Send(c.conn, msg); err != nil {
c.logger.Warn("Failed to send history", "err", err) for _, c := range db.conns {
select {
case c.msg <- msg:
default:
c.logger.Warn("Client message queue is full")
}
} }
} }
func (db *dashboard) addClosing() *chan chan error {
closing := make(chan chan error)
db.mapLock.Lock()
db.closing[&closing] = true
db.mapLock.Unlock()
return &closing
}
func (db *dashboard) removeClosing(closing *chan chan error) {
db.mapLock.Lock()
delete(db.closing, closing)
db.mapLock.Unlock()
}