140 lines
4.2 KiB
Go
140 lines
4.2 KiB
Go
package main
|
|
|
|
// **Disk usage readings and peaks per data item** (novox/hq ADR 0571 clause 3, ADR 0233 §21, §27, §34): disk-load measures
|
|
// each data item the modules of its node declare and keeps the newest reading and the item's largest size in seven
|
|
// days as its own state `readings`, one key `<node>.<module>.<item>`, written only when it changes. The node-backup
|
|
// holder, mounts, nfs-server and the self-check's probes read it there; the controller keeps none of it.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"time"
|
|
)
|
|
|
|
// Version of a reading's layout.
|
|
const ReadingVersion = 1
|
|
|
|
// PeakWindow is how far back the peak looks (ADR 0233 §34).
|
|
const PeakWindow = 7 * 24 * time.Hour
|
|
|
|
// StateReadings is the state disk-load keeps.
|
|
const StateReadings = "readings"
|
|
|
|
// The ways an item is measured (ADR 0233 §22 to §25).
|
|
const (
|
|
MeasureWalk = "walk"
|
|
MeasureDataset = "dataset"
|
|
MeasureShallow = "shallow"
|
|
ClassCache = "cache"
|
|
)
|
|
|
|
// Item is one data item a module declares.
|
|
type Item struct {
|
|
Measure string `json:"measure,omitempty"`
|
|
Module string `json:"module"`
|
|
ID string `json:"item"`
|
|
Class string `json:"class"`
|
|
Path string `json:"path"`
|
|
CoveredBy string `json:"covered_by,omitempty"`
|
|
Protection string `json:"protection,omitempty"`
|
|
}
|
|
|
|
// Redundancy is the redundant storage an item is on, as read.
|
|
type Redundancy struct {
|
|
Kind string `json:"kind"`
|
|
Where string `json:"where"`
|
|
Healthy *bool `json:"healthy"`
|
|
Said string `json:"said"`
|
|
}
|
|
|
|
// Measured is what one measurement found.
|
|
type Measured struct {
|
|
SizeBytes *int64 `json:"size_bytes,omitempty"`
|
|
LastWrite *time.Time `json:"last_write,omitempty"`
|
|
MeasuredAt *time.Time `json:"measured_at,omitempty"`
|
|
// Precision says what the size is: "exact", "dataset <name>: its whole size", "partial: ..." (a lower bound,
|
|
// never compared), or "no size: ...".
|
|
Precision string `json:"precision,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Redundancy *Redundancy `json:"redundancy,omitempty"`
|
|
}
|
|
|
|
// DaySize is the largest size measured on one UTC day.
|
|
type DaySize struct {
|
|
Day string `json:"day"`
|
|
Bytes int64 `json:"bytes"`
|
|
}
|
|
|
|
// Reading is the value of one key.
|
|
type Reading struct {
|
|
V int `json:"v"`
|
|
Node string `json:"node"`
|
|
Item
|
|
Measured
|
|
// Peak is the largest size measured in the seven days up to the reading, PeakAt the day it was measured; Days are
|
|
// the day maxima it is read from.
|
|
Peak *int64 `json:"peak_bytes,omitempty"`
|
|
PeakAt string `json:"peak_day,omitempty"`
|
|
Days []DaySize `json:"days,omitempty"`
|
|
}
|
|
|
|
// ReadingKey is the key of an item on a node.
|
|
func ReadingKey(node, module, item string) string { return node + "." + module + "." + item }
|
|
|
|
// DecodeReading reads a key's value.
|
|
func DecodeReading(raw []byte) (Reading, error) {
|
|
var r Reading
|
|
err := json.Unmarshal(raw, &r)
|
|
return r, err
|
|
}
|
|
|
|
// Next is the reading after a measurement, and whether it differs from the one before in anything but the time it was
|
|
// taken (a reading that did not change is not written).
|
|
func Next(prev *Reading, node string, it Item, m Measured, now time.Time) (Reading, bool) {
|
|
r := Reading{V: ReadingVersion, Node: node, Item: it, Measured: m}
|
|
var days []DaySize
|
|
if prev != nil {
|
|
days = prev.Days
|
|
}
|
|
// A reading with no size (an error, a shallow item) carries the last size's days on, so the peak outlives one failed
|
|
// measurement.
|
|
today := now.UTC().Format("2006-01-02")
|
|
cut := now.UTC().Add(-PeakWindow).Format("2006-01-02")
|
|
var kept []DaySize
|
|
for _, d := range days {
|
|
if d.Day >= cut && d.Day != today {
|
|
kept = append(kept, d)
|
|
}
|
|
}
|
|
todays := int64(-1)
|
|
for _, d := range days {
|
|
if d.Day == today {
|
|
todays = d.Bytes
|
|
}
|
|
}
|
|
if m.SizeBytes != nil && !partial(m) && *m.SizeBytes > todays {
|
|
todays = *m.SizeBytes
|
|
}
|
|
if todays >= 0 {
|
|
kept = append(kept, DaySize{Day: today, Bytes: todays})
|
|
}
|
|
r.Days = kept
|
|
for _, d := range kept {
|
|
if r.Peak == nil || d.Bytes >= *r.Peak {
|
|
b := d.Bytes
|
|
r.Peak, r.PeakAt = &b, d.Day
|
|
}
|
|
}
|
|
if prev == nil {
|
|
return r, true
|
|
}
|
|
a, b := *prev, r
|
|
a.MeasuredAt, b.MeasuredAt = nil, nil
|
|
return r, !reflect.DeepEqual(a, b)
|
|
}
|
|
|
|
// partial says a size is only a lower bound, which a peak is never made of.
|
|
func partial(m Measured) bool {
|
|
return len(m.Precision) >= 7 && m.Precision[:7] == "partial"
|
|
}
|