From 342211ade2bf83b0678b6875561c12ea8f2a0472 Mon Sep 17 00:00:00 2001 From: almostinf <87192879+almostinf@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:38:57 +0300 Subject: [PATCH] add get trigger checks function and merge (#944) --- database/redis/last_check.go | 22 +++++++ database/redis/last_check_test.go | 101 ++++++++++++++++++++++++++++++ database/redis/reply/check.go | 21 +++++++ datatypes.go | 10 +++ datatypes_test.go | 26 ++++++++ helpers.go | 37 +++++++++++ helpers_test.go | 75 ++++++++++++++++++++++ 7 files changed, 292 insertions(+) diff --git a/database/redis/last_check.go b/database/redis/last_check.go index 21ac48aeb..408375cfd 100644 --- a/database/redis/last_check.go +++ b/database/redis/last_check.go @@ -198,6 +198,28 @@ func (connector *DbConnector) checkDataScoreChanged(triggerID string, checkData return oldScore != float64(checkData.Score) } +// getTriggersLastCheck returns an array of trigger checks by the passed ids, if the trigger does not exist, it is nil +func (connector *DbConnector) getTriggersLastCheck(triggerIDs []string) ([]*moira.CheckData, error) { + ctx := connector.context + pipe := (*connector.client).TxPipeline() + + results := make([]*redis.StringCmd, len(triggerIDs)) + for i, id := range triggerIDs { + var result *redis.StringCmd + if id != "" { + result = pipe.Get(ctx, metricLastCheckKey(id)) + } + results[i] = result + } + + _, err := pipe.Exec(ctx) + if err != nil && err != redis.Nil { + return nil, err + } + + return reply.Checks(results) +} + var badStateTriggersKey = "moira-bad-state-triggers" var triggersChecksKey = "moira-triggers-checks" diff --git a/database/redis/last_check_test.go b/database/redis/last_check_test.go index 5d7cebceb..f4fc17e05 100644 --- a/database/redis/last_check_test.go +++ b/database/redis/last_check_test.go @@ -469,6 +469,107 @@ func TestLastCheckErrorConnection(t *testing.T) { }) } +func TestGetTriggersLastCheck(t *testing.T) { + logger, _ := logging.GetLogger("dataBase") + dataBase := NewTestDatabase(logger) + dataBase.Flush() + defer dataBase.Flush() + + _ = dataBase.SetTriggerLastCheck("test1", &moira.CheckData{ + Timestamp: 1, + }, moira.TriggerSourceNotSet) + + _ = dataBase.SetTriggerLastCheck("test2", &moira.CheckData{ + Timestamp: 2, + }, moira.TriggerSourceNotSet) + + _ = dataBase.SetTriggerLastCheck("test3", &moira.CheckData{ + Timestamp: 3, + }, moira.TriggerSourceNotSet) + + Convey("getTriggersLastCheck manipulations", t, func() { + Convey("Test with nil id array", func() { + actual, err := dataBase.getTriggersLastCheck(nil) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.CheckData{}) + }) + + Convey("Test with correct id array", func() { + actual, err := dataBase.getTriggersLastCheck([]string{"test1", "test2", "test3"}) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.CheckData{ + { + Timestamp: 1, + MetricsToTargetRelation: map[string]string{}, + }, + { + Timestamp: 2, + MetricsToTargetRelation: map[string]string{}, + }, + { + Timestamp: 3, + MetricsToTargetRelation: map[string]string{}, + }, + }) + }) + + Convey("Test with deleted trigger", func() { + dataBase.RemoveTriggerLastCheck("test2") //nolint + defer func() { + _ = dataBase.SetTriggerLastCheck("test2", &moira.CheckData{ + Timestamp: 2, + }, moira.TriggerSourceNotSet) + }() + + actual, err := dataBase.getTriggersLastCheck([]string{"test1", "test2", "test3"}) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.CheckData{ + { + Timestamp: 1, + MetricsToTargetRelation: map[string]string{}, + }, + nil, + { + Timestamp: 3, + MetricsToTargetRelation: map[string]string{}, + }, + }) + }) + + Convey("Test with a nonexistent trigger id", func() { + actual, err := dataBase.getTriggersLastCheck([]string{"test1", "test2", "test4"}) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.CheckData{ + { + Timestamp: 1, + MetricsToTargetRelation: map[string]string{}, + }, + { + Timestamp: 2, + MetricsToTargetRelation: map[string]string{}, + }, + nil, + }) + }) + + Convey("Test with an empty trigger id", func() { + actual, err := dataBase.getTriggersLastCheck([]string{"", "test2", "test3"}) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.CheckData{ + nil, + { + Timestamp: 2, + MetricsToTargetRelation: map[string]string{}, + }, + { + Timestamp: 3, + MetricsToTargetRelation: map[string]string{}, + }, + }) + }) + }) +} + func TestMaintenanceUserSave(t *testing.T) { logger, _ := logging.GetLogger("dataBase") dataBase := NewTestDatabase(logger) diff --git a/database/redis/reply/check.go b/database/redis/reply/check.go index 72f0c6167..d4e188057 100644 --- a/database/redis/reply/check.go +++ b/database/redis/reply/check.go @@ -110,6 +110,27 @@ func Check(rep *redis.StringCmd) (moira.CheckData, error) { return checkSE.toCheckData(), nil } +// Checks converts an array of redis DB reply to moira.CheckData objects, if reply is nil, then checkdata is nil +func Checks(replies []*redis.StringCmd) ([]*moira.CheckData, error) { + checks := make([]*moira.CheckData, len(replies)) + + for i, value := range replies { + if value != nil { + check, err := Check(value) + if err != nil { + if err != database.ErrNil { + return nil, err + } + continue + } + + checks[i] = &check + } + } + + return checks, nil +} + // GetCheckBytes is a function that takes moira.CheckData and turns it to bytes that will be saved in redis. func GetCheckBytes(check moira.CheckData) ([]byte, error) { checkSE := toCheckDataStorageElement(check) diff --git a/datatypes.go b/datatypes.go index 3fb1718df..543dc88bc 100644 --- a/datatypes.go +++ b/datatypes.go @@ -245,6 +245,16 @@ type ScheduledNotification struct { CreatedAt int64 `json:"created_at" example:"1594471900" format:"int64"` } +// Less is needed for the ScheduledNotification to match the Comparable interface +func (notification *ScheduledNotification) Less(other Comparable) (bool, error) { + otherNotification, ok := other.(*ScheduledNotification) + if !ok { + return false, fmt.Errorf("cannot to compare ScheduledNotification with different type") + } + + return notification.Timestamp < otherNotification.Timestamp, nil +} + // MatchedMetric represents parsed and matched metric data type MatchedMetric struct { Metric string diff --git a/datatypes_test.go b/datatypes_test.go index 0294e92ea..f55c1d21b 100644 --- a/datatypes_test.go +++ b/datatypes_test.go @@ -724,3 +724,29 @@ func testMaintenance(conveyMessage string, actualInfo MaintenanceInfo, maintenan So(lastCheckTest.Maintenance, ShouldEqual, maintenance) }) } + +func TestScheduledNotificationLess(t *testing.T) { + Convey("Test Scheduled notification Less function", t, func() { + notification := &ScheduledNotification{ + Timestamp: 5, + } + + Convey("Test Less with nil", func() { + actual, err := notification.Less(nil) + So(err, ShouldResemble, fmt.Errorf("cannot to compare ScheduledNotification with different type")) + So(actual, ShouldBeFalse) + }) + + Convey("Test Less with less notification :)", func() { + actual, err := notification.Less(&ScheduledNotification{Timestamp: 1}) + So(err, ShouldBeNil) + So(actual, ShouldBeFalse) + }) + + Convey("Test Less with greater notification", func() { + actual, err := notification.Less(&ScheduledNotification{Timestamp: 10}) + So(err, ShouldBeNil) + So(actual, ShouldBeTrue) + }) + }) +} diff --git a/helpers.go b/helpers.go index 1c4a367a5..6f38b6c54 100644 --- a/helpers.go +++ b/helpers.go @@ -213,3 +213,40 @@ func ReplaceSubstring(str, begin, end, replaced string) string { } return result } + +type Comparable interface { + Less(other Comparable) (bool, error) +} + +// Merge is a generic function that performs a merge of two sorted arrays into one sorted array +func MergeToSorted[T Comparable](arr1, arr2 []T) ([]T, error) { + merged := make([]T, 0, len(arr1)+len(arr2)) + i, j := 0, 0 + + for i < len(arr1) && j < len(arr2) { + less, err := arr1[i].Less(arr2[j]) + if err != nil { + return nil, err + } + + if less { + merged = append(merged, arr1[i]) + i++ + } else { + merged = append(merged, arr2[j]) + j++ + } + } + + for i < len(arr1) { + merged = append(merged, arr1[i]) + i++ + } + + for j < len(arr2) { + merged = append(merged, arr2[j]) + j++ + } + + return merged, nil +} diff --git a/helpers_test.go b/helpers_test.go index 18e0319c5..d9c3ca4ee 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -264,3 +264,78 @@ func TestReplaceSubstring(t *testing.T) { }) }) } + +type myInt int + +func (m myInt) Less(other Comparable) (bool, error) { + otherInt := other.(myInt) + return m < otherInt, nil +} + +type myTest struct { + value int +} + +func (test myTest) Less(other Comparable) (bool, error) { + otherTest := other.(myTest) + return test.value < otherTest.value, nil +} + +func TestMergeToSorted(t *testing.T) { + Convey("Test MergeToSorted function", t, func() { + Convey("Test with two nil arrays", func() { + merged, err := MergeToSorted[myInt](nil, nil) + So(err, ShouldBeNil) + So(merged, ShouldResemble, []myInt{}) + }) + + Convey("Test with one nil array", func() { + merged, err := MergeToSorted[myInt](nil, []myInt{1, 2, 3}) + So(err, ShouldBeNil) + So(merged, ShouldResemble, []myInt{1, 2, 3}) + }) + + Convey("Test with two arrays", func() { + merged, err := MergeToSorted[myInt]([]myInt{4, 5}, []myInt{1, 2, 3}) + So(err, ShouldBeNil) + So(merged, ShouldResemble, []myInt{1, 2, 3, 4, 5}) + }) + + Convey("Test with empty array", func() { + merged, err := MergeToSorted[myInt]([]myInt{-4, 5}, []myInt{}) + So(err, ShouldBeNil) + So(merged, ShouldResemble, []myInt{-4, 5}) + }) + + Convey("Test with sorted values but mixed up", func() { + merged, err := MergeToSorted[myInt]([]myInt{1, 9, 10}, []myInt{4, 8, 12}) + So(err, ShouldBeNil) + So(merged, ShouldResemble, []myInt{1, 4, 8, 9, 10, 12}) + }) + + Convey("Test with structure type", func() { + arr1 := []myTest{ + { + value: 1, + }, + { + value: 2, + }, + } + + arr2 := []myTest{ + { + value: -2, + }, + { + value: -1, + }, + } + + expected := append(arr2, arr1...) + merged, err := MergeToSorted[myTest](arr1, arr2) + So(err, ShouldBeNil) + So(merged, ShouldResemble, expected) + }) + }) +}