Fix MaxSilences limit causes incomplete updates of existing silences (#3877)

* Fix MaxSilences limit causes incomplete updates of existing silences

This commit fixes a bug where the MaxSilences limit can cause an
incomplete update of existing silences, where the old silence can
be expired but the new silence cannot be created because it would
exceeded the maximum number of silences.

Signed-off-by: George Robinson <george.robinson@grafana.com>

---------

Signed-off-by: George Robinson <george.robinson@grafana.com>
This commit is contained in:
George Robinson 2024-06-24 15:11:10 +01:00 committed by GitHub
parent 36d653ab9f
commit b4443817e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 69 additions and 41 deletions

View File

@ -564,6 +564,13 @@ func (s *Silences) getSilence(id string) (*pb.Silence, bool) {
return msil.Silence, true
}
func (s *Silences) toMeshSilence(sil *pb.Silence) *pb.MeshSilence {
return &pb.MeshSilence{
Silence: sil,
ExpiresAt: sil.EndsAt.Add(s.retention),
}
}
func (s *Silences) setSilence(sil *pb.Silence, now time.Time, skipValidate bool) error {
sil.UpdatedAt = now
@ -573,10 +580,7 @@ func (s *Silences) setSilence(sil *pb.Silence, now time.Time, skipValidate bool)
}
}
msil := &pb.MeshSilence{
Silence: sil,
ExpiresAt: sil.EndsAt.Add(s.retention),
}
msil := s.toMeshSilence(sil)
b, err := marshalMeshSilence(msil)
if err != nil {
return err
@ -612,25 +616,27 @@ func (s *Silences) Set(sil *pb.Silence) error {
return ErrNotFound
}
if ok {
if canUpdate(prev, sil, now) {
if ok && canUpdate(prev, sil, now) {
return s.setSilence(sil, now, false)
}
if getState(prev, s.nowUTC()) != types.SilenceStateExpired {
// We cannot update the silence, expire the old one.
if err := s.expire(prev.Id); err != nil {
return fmt.Errorf("expire previous silence: %w", err)
}
}
}
// If we got here it's either a new silence or a replacing one.
// If we got here it's either a new silence or a replacing one (which would
// also create a new silence) so we need to make sure we have capacity for
// the new silence.
if s.limits.MaxSilences != nil {
if m := s.limits.MaxSilences(); m > 0 && len(s.st)+1 > m {
return fmt.Errorf("exceeded maximum number of silences: %d (limit: %d)", len(s.st), m)
}
}
if ok && getState(prev, s.nowUTC()) != types.SilenceStateExpired {
// We cannot update the silence, expire the old one to leave a history of
// the silence before modification.
if err := s.expire(prev.Id); err != nil {
return fmt.Errorf("expire previous silence: %w", err)
}
}
uid, err := uuid.NewV4()
if err != nil {
return fmt.Errorf("generate uuid: %w", err)

View File

@ -15,6 +15,7 @@ package silence
import (
"bytes"
"fmt"
"os"
"runtime"
"sort"
@ -470,32 +471,24 @@ func TestSilenceLimits(t *testing.T) {
EndsAt: time.Now().Add(5 * time.Minute),
}
require.NoError(t, s.Set(sil1))
require.NotEqual(t, "", sil1.Id)
// Insert sil2 should fail because maximum number of silences
// has been exceeded.
// Insert sil2 should fail because maximum number of silences has been
// exceeded.
sil2 := &pb.Silence{
Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}},
Matchers: []*pb.Matcher{{Name: "c", Pattern: "d"}},
StartsAt: time.Now(),
EndsAt: time.Now().Add(5 * time.Minute),
}
require.EqualError(t, s.Set(sil2), "exceeded maximum number of silences: 1 (limit: 1)")
require.Equal(t, "", sil2.Id)
// Expire sil1 and run the GC. This should allow sil2 to be
// inserted.
// Expire sil1 and run the GC. This should allow sil2 to be inserted.
require.NoError(t, s.Expire(sil1.Id))
n, err := s.GC()
require.NoError(t, err)
require.Equal(t, 1, n)
require.NoError(t, s.Set(sil2))
require.NotEqual(t, "", sil2.Id)
// Should be able to update sil2 without hitting the limit.
require.NoError(t, s.Set(sil2))
// Expire sil2.
// Expire sil2 and run the GC.
require.NoError(t, s.Expire(sil2.Id))
n, err = s.GC()
require.NoError(t, err)
@ -505,26 +498,55 @@ func TestSilenceLimits(t *testing.T) {
sil3 := &pb.Silence{
Matchers: []*pb.Matcher{
{
Name: strings.Repeat("a", 2<<9),
Pattern: strings.Repeat("b", 2<<9),
Name: strings.Repeat("e", 2<<9),
Pattern: strings.Repeat("f", 2<<9),
},
{
Name: strings.Repeat("c", 2<<9),
Pattern: strings.Repeat("d", 2<<9),
Name: strings.Repeat("g", 2<<9),
Pattern: strings.Repeat("h", 2<<9),
},
},
CreatedBy: strings.Repeat("e", 2<<9),
Comment: strings.Repeat("f", 2<<9),
CreatedBy: strings.Repeat("i", 2<<9),
Comment: strings.Repeat("j", 2<<9),
StartsAt: time.Now(),
EndsAt: time.Now().Add(5 * time.Minute),
}
err = s.Set(sil3)
require.Error(t, err)
// Do not check the exact size as it can change between consecutive runs
// due to padding.
require.Contains(t, err.Error(), "silence exceeded maximum size")
// TODO: Once we fix https://github.com/prometheus/alertmanager/issues/3878 this should be require.Equal.
require.NotEqual(t, "", sil3.Id)
require.EqualError(t, s.Set(sil3), fmt.Sprintf("silence exceeded maximum size: %d bytes (limit: 4096 bytes)", s.toMeshSilence(sil3).Size()))
// Should be able to insert sil4.
sil4 := &pb.Silence{
Matchers: []*pb.Matcher{{Name: "k", Pattern: "l"}},
StartsAt: time.Now(),
EndsAt: time.Now().Add(5 * time.Minute),
}
require.NoError(t, s.Set(sil4))
// Should be able to update sil4 without modifications. It is expected to
// keep the same ID.
sil5 := cloneSilence(sil4)
require.NoError(t, s.Set(sil5))
require.Equal(t, sil4.Id, sil5.Id)
// Should be able to update the comment. It is also expected to keep the
// same ID.
sil6 := cloneSilence(sil5)
sil6.Comment = "m"
require.NoError(t, s.Set(sil6))
require.Equal(t, sil5.Id, sil6.Id)
// Should not be able to update the start and end time as this requires
// sil6 to be expired and a new silence to be created. However, this would
// exceed the maximum number of silences, which counts both active and
// expired silences.
sil7 := cloneSilence(sil6)
sil7.StartsAt = time.Now().Add(5 * time.Minute)
sil7.EndsAt = time.Now().Add(10 * time.Minute)
require.EqualError(t, s.Set(sil7), "exceeded maximum number of silences: 1 (limit: 1)")
// sil6 should not be expired because the update failed.
sil6, err = s.QueryOne(QIDs(sil6.Id))
require.NoError(t, err)
require.Equal(t, types.SilenceStateActive, getState(sil6, s.nowUTC()))
}
func TestSilenceNoLimits(t *testing.T) {