grafana/pkg/services/ngalert/models/instance_labels.go
Joe Blubaugh b476ae62fb
Alerting: Write and Delete multiple alert instances. (#55350)
Prior to this change, all alert instance writes and deletes happened
individually, in their own database transaction. This change batches up
writes or deletes for a given rule's evaluation loop into a single
transaction before applying it.

These new transactions are off by default, guarded by the feature toggle "alertingBigTransactions"

Before:

```
goos: darwin
goarch: arm64
pkg: github.com/grafana/grafana/pkg/services/ngalert/store
BenchmarkAlertInstanceOperations-8           398           2991381 ns/op         1133537 B/op      27703 allocs/op
--- BENCH: BenchmarkAlertInstanceOperations-8
    util.go:127: alert definition: {orgID: 1, UID: FovKXiRVzm} with title: "an alert definition FTvFXmRVkz" interval: 60 created
    util.go:127: alert definition: {orgID: 1, UID: foDFXmRVkm} with title: "an alert definition fovFXmRVkz" interval: 60 created
    util.go:127: alert definition: {orgID: 1, UID: VQvFuigVkm} with title: "an alert definition VwDKXmR4kz" interval: 60 created
PASS
ok      github.com/grafana/grafana/pkg/services/ngalert/store   1.619s
```

After:

```
goos: darwin
goarch: arm64
pkg: github.com/grafana/grafana/pkg/services/ngalert/store
BenchmarkAlertInstanceOperations-8          1440            816484 ns/op          352297 B/op       6529 allocs/op
--- BENCH: BenchmarkAlertInstanceOperations-8
    util.go:127: alert definition: {orgID: 1, UID: 302r_igVzm} with title: "an alert definition q0h9lmR4zz" interval: 60 created
    util.go:127: alert definition: {orgID: 1, UID: 71hrlmR4km} with title: "an alert definition nJ29_mR4zz" interval: 60 created
    util.go:127: alert definition: {orgID: 1, UID: Cahr_mR4zm} with title: "an alert definition ja2rlmg4zz" interval: 60 created
PASS
ok      github.com/grafana/grafana/pkg/services/ngalert/store   1.383s
```

So we cut time by about 75% and memory allocations by about 60% when
storing and deleting 100 instances.
2022-10-06 14:22:58 +08:00

112 lines
3.2 KiB
Go

package models
import (
// nolint:gosec
"crypto/sha1"
"encoding/json"
"fmt"
"sort"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// InstanceLabels is an extension to data.Labels with methods
// for database serialization.
type InstanceLabels data.Labels
// FromDB loads labels stored in the database as json tuples into InstanceLabels.
// FromDB is part of the xorm Conversion interface.
func (il *InstanceLabels) FromDB(b []byte) error {
tl := &tupleLabels{}
err := json.Unmarshal(b, tl)
if err != nil {
return err
}
labels, err := tupleLablesToLabels(*tl)
if err != nil {
return err
}
*il = labels
return nil
}
// ToDB is not implemented as serialization is handled with manual SQL queries).
// ToDB is part of the xorm Conversion interface.
func (il *InstanceLabels) ToDB() ([]byte, error) {
// Currently handled manually in sql command, needed to fulfill the xorm
// converter interface it seems
return []byte{}, fmt.Errorf("database serialization of alerting ng Instance labels is not implemented")
}
func (il *InstanceLabels) StringKey() (string, error) {
tl := labelsToTupleLabels(*il)
b, err := json.Marshal(tl)
if err != nil {
return "", fmt.Errorf("could not generate key due to failure to encode labels: %w", err)
}
return string(b), nil
}
// StringAndHash returns a the json representation of the labels as tuples
// sorted by key. It also returns the a hash of that representation.
func (il *InstanceLabels) StringAndHash() (string, string, error) {
tl := labelsToTupleLabels(*il)
b, err := json.Marshal(tl)
if err != nil {
return "", "", fmt.Errorf("could not generate key for alert instance due to failure to encode labels: %w", err)
}
h := sha1.New()
if _, err := h.Write(b); err != nil {
return "", "", err
}
return string(b), fmt.Sprintf("%x", h.Sum(nil)), nil
}
// The following is based on SDK code, copied for now
// tupleLables is an alternative representation of Labels (map[string]string) that can be sorted
// and then marshalled into a consistent string that can be used a map key. All tupleLabel objects
// in tupleLabels should have unique first elements (keys).
type tupleLabels []tupleLabel
// tupleLabel is an element of tupleLabels and should be in the form of [2]{"key", "value"}.
type tupleLabel [2]string
// Sort tupleLabels by each elements first property (key).
func (t *tupleLabels) sortByKey() {
if t == nil {
return
}
sort.Slice((*t)[:], func(i, j int) bool {
return (*t)[i][0] < (*t)[j][0]
})
}
// labelsToTupleLabels converts Labels (map[string]string) to tupleLabels.
func labelsToTupleLabels(l InstanceLabels) tupleLabels {
t := make(tupleLabels, 0, len(l))
for k, v := range l {
t = append(t, tupleLabel{k, v})
}
t.sortByKey()
return t
}
// tupleLabelsToLabels converts tupleLabels to Labels (map[string]string), erroring if there are duplicate keys.
func tupleLablesToLabels(tuples tupleLabels) (InstanceLabels, error) {
if tuples == nil {
return InstanceLabels{}, nil
}
labels := make(map[string]string)
for _, tuple := range tuples {
if key, ok := labels[tuple[0]]; ok {
return nil, fmt.Errorf("duplicate key '%v' in lables: %v", key, tuples)
}
labels[tuple[0]] = tuple[1]
}
return labels, nil
}