New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Purge old Rendered Templates #2167
Changes from 9 commits
1540699
715187a
6c686e5
585869c
0cf74c5
ee01906
84695a1
1f2a929
2dd0610
a268e7b
2e1bf94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,6 +67,9 @@ type StateDataAccess interface { | |
|
||
SetRenderedTemplates(incidentId int64, rt *models.RenderedTemplates) error | ||
GetRenderedTemplates(incidentId int64) (*models.RenderedTemplates, error) | ||
GetRenderedTemplateKeys() ([]string, error) | ||
CleanupOldRenderedTemplates(olderThan time.Duration) | ||
DeleteRenderedTemplates(incidentIds []int64) error | ||
|
||
Forget(ak models.AlertKey) error | ||
SetUnevaluated(ak models.AlertKey, uneval bool) error | ||
|
@@ -93,16 +96,80 @@ func (d *dataAccess) GetRenderedTemplates(incidentId int64) (*models.RenderedTem | |
defer conn.Close() | ||
|
||
b, err := redis.Bytes(conn.Do("GET", renderedTemplatesKey(incidentId))) | ||
renderedT := &models.RenderedTemplates{} | ||
if err != nil { | ||
if err == redis.ErrNil { | ||
return renderedT, nil | ||
} | ||
return nil, slog.Wrap(err) | ||
} | ||
renderedT := &models.RenderedTemplates{} | ||
if err = json.Unmarshal(b, renderedT); err != nil { | ||
return nil, slog.Wrap(err) | ||
} | ||
return renderedT, nil | ||
} | ||
|
||
func (d *dataAccess) GetRenderedTemplateKeys() ([]string, error) { | ||
conn := d.Get() | ||
defer conn.Close() | ||
|
||
//ledis uses XSCAN cursor "KV" MATCH foo | ||
//redis uses SCAN cursor MATCH foo | ||
cmd := "SCAN" | ||
args := []interface{}{"0", "MATCH", "renderedTemplatesById:*"} | ||
cursorIdx := 0 | ||
if !d.isRedis { | ||
cmd = "XSCAN" | ||
args = append([]interface{}{"KV"}, args...) | ||
cursorIdx = 1 | ||
} | ||
found := []string{} | ||
for { | ||
vals, err := redis.Values(conn.Do(cmd, args...)) | ||
if err != nil { | ||
return nil, slog.Wrap(err) | ||
} | ||
cursor, err := redis.String(vals[0], nil) | ||
if err != nil { | ||
return nil, slog.Wrap(err) | ||
} | ||
args[cursorIdx] = cursor | ||
keys, err := redis.Strings(vals[1], nil) | ||
if err != nil { | ||
return nil, slog.Wrap(err) | ||
} | ||
found = append(found, keys...) | ||
if cursor == "" || cursor == "0" { | ||
break | ||
} | ||
} | ||
return found, nil | ||
} | ||
|
||
func (d *dataAccess) DeleteRenderedTemplates(incidentIds []int64) error { | ||
conn := d.Get() | ||
defer conn.Close() | ||
const batchSize = 1000 | ||
args := make([]interface{}, 0, batchSize) | ||
for len(incidentIds) > 0 { | ||
size := len(incidentIds) | ||
if size > batchSize { | ||
size = batchSize | ||
} | ||
thisBatch := incidentIds[:size] | ||
incidentIds = incidentIds[size:] | ||
args = args[:0] | ||
for _, id := range thisBatch { | ||
args = append(args, renderedTemplatesKey(id)) | ||
} | ||
_, err := conn.Do("DEL", args...) | ||
if err != nil { | ||
return slog.Wrap(err) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (d *dataAccess) State() StateDataAccess { | ||
return d | ||
} | ||
|
@@ -380,10 +447,12 @@ func (d *dataAccess) Forget(ak models.AlertKey) error { | |
return slog.Wrap(err) | ||
} | ||
for _, id := range ids { | ||
|
||
if _, err = conn.Do("DEL", incidentStateKey(id)); err != nil { | ||
return slog.Wrap(err) | ||
} | ||
if _, err = conn.Do("DEL", renderedTemplatesKey(id)); err != nil { | ||
return slog.Wrap(err) | ||
} | ||
} | ||
if _, err := conn.Do(d.LCLEAR(), incidentsForAlertKeyKey(ak)); err != nil { | ||
return slog.Wrap(err) | ||
|
@@ -447,3 +516,58 @@ func (d *dataAccess) transact(conn redis.Conn, f func() error) error { | |
} | ||
return nil | ||
} | ||
|
||
// CleanupCleanupOldRenderedTemplates will in a loop purge any old rendered templates | ||
func (d *dataAccess) CleanupOldRenderedTemplates(olderThan time.Duration) { | ||
// run after 5 minutes (to let bosun stabilize) | ||
// and then every hour | ||
time.Sleep(time.Minute * 5) | ||
for { | ||
conn := d.Get() | ||
slog.Infof("Cleaning out old rendered templates") | ||
earliestOk := time.Now().UTC().Add(-1 * olderThan) | ||
func() { | ||
toPurge := []int64{} | ||
keys, err := d.GetRenderedTemplateKeys() | ||
if err != nil { | ||
slog.Error(err) | ||
return | ||
} | ||
for _, key := range keys { | ||
parts := strings.Split(key, ":") | ||
if len(parts) != 2 { | ||
slog.Errorf("Invalid rendered template redis key found: %s", key) | ||
continue | ||
} | ||
id, err := strconv.ParseInt(parts[1], 10, 64) | ||
if err != nil { | ||
slog.Error(err) | ||
continue | ||
} | ||
state, err := d.getIncident(id, conn) | ||
if err != nil { | ||
if strings.Contains(err.Error(), "nil returned") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. strings.Contains on error, combined with the match not being a constant doesn't seem safe in the long run. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the problem there is that the error will already be wrapped at that point, and the inner error is not accessible. I guess I can also make a package function for that test though. |
||
toPurge = append(toPurge, id) | ||
continue | ||
} | ||
slog.Error(err) | ||
continue | ||
} | ||
if state.End != nil && (*state.End).Before(earliestOk) { | ||
toPurge = append(toPurge, id) | ||
} | ||
} | ||
if len(toPurge) == 0 { | ||
return | ||
} | ||
slog.Infof("Deleting %d old rendered templates", len(toPurge)) | ||
if err = d.DeleteRenderedTemplates(toPurge); err != nil { | ||
slog.Error(err) | ||
return | ||
} | ||
}() | ||
conn.Close() | ||
slog.Info("Done cleaning rendered templates") | ||
time.Sleep(time.Hour) | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe Redis and Ledis SCAN could be a function outside of this that can be reused (and make this func shorter)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we did that with some others, but the re-ordering of the args makes it tricky. I will factor out.