mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Update scheduler to get updates only from database (#64635)
* stop using the scheduler's Update and Delete methods all communication must be via the database * update scheduler's registry to calculate diff before re-setting the cache * update fetcher to return the diff generated by registry * update processTick to update rule eval routine if the rule was updated and it is not going to be evaluated at this tick. * remove references to the scheduler from api package * remove unused methods in the scheduler
This commit is contained in:
parent
10c809a00a
commit
85a954cd81
@ -18,7 +18,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/sender"
|
"github.com/grafana/grafana/pkg/services/ngalert/sender"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/state"
|
"github.com/grafana/grafana/pkg/services/ngalert/state"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
@ -66,7 +65,6 @@ type API struct {
|
|||||||
DatasourceService datasources.DataSourceService
|
DatasourceService datasources.DataSourceService
|
||||||
RouteRegister routing.RouteRegister
|
RouteRegister routing.RouteRegister
|
||||||
QuotaService quota.Service
|
QuotaService quota.Service
|
||||||
Schedule schedule.ScheduleService
|
|
||||||
TransactionManager provisioning.TransactionManager
|
TransactionManager provisioning.TransactionManager
|
||||||
ProvenanceStore provisioning.ProvisioningStore
|
ProvenanceStore provisioning.ProvisioningStore
|
||||||
RuleStore RuleStore
|
RuleStore RuleStore
|
||||||
@ -116,7 +114,6 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
|
|||||||
&RulerSrv{
|
&RulerSrv{
|
||||||
conditionValidator: api.EvaluatorFactory,
|
conditionValidator: api.EvaluatorFactory,
|
||||||
QuotaService: api.QuotaService,
|
QuotaService: api.QuotaService,
|
||||||
scheduleService: api.Schedule,
|
|
||||||
store: api.RuleStore,
|
store: api.RuleStore,
|
||||||
provenanceStore: api.ProvenanceStore,
|
provenanceStore: api.ProvenanceStore,
|
||||||
xactManager: api.TransactionManager,
|
xactManager: api.TransactionManager,
|
||||||
|
@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
||||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
"github.com/grafana/grafana/pkg/services/quota"
|
"github.com/grafana/grafana/pkg/services/quota"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
@ -37,7 +36,6 @@ type RulerSrv struct {
|
|||||||
provenanceStore provisioning.ProvisioningStore
|
provenanceStore provisioning.ProvisioningStore
|
||||||
store RuleStore
|
store RuleStore
|
||||||
QuotaService quota.Service
|
QuotaService quota.Service
|
||||||
scheduleService schedule.ScheduleService
|
|
||||||
log log.Logger
|
log log.Logger
|
||||||
cfg *setting.UnifiedAlertingSettings
|
cfg *setting.UnifiedAlertingSettings
|
||||||
ac accesscontrol.AccessControl
|
ac accesscontrol.AccessControl
|
||||||
@ -144,12 +142,6 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceT
|
|||||||
}
|
}
|
||||||
return ErrResp(http.StatusInternalServerError, err, "failed to delete rule group")
|
return ErrResp(http.StatusInternalServerError, err, "failed to delete rule group")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug("rules have been deleted from the store. updating scheduler")
|
|
||||||
for _, ruleKeys := range deletedGroups {
|
|
||||||
srv.scheduleService.DeleteAlertRule(ruleKeys...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rules deleted"})
|
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rules deleted"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,21 +413,6 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey
|
|||||||
return ErrResp(http.StatusInternalServerError, err, "failed to update rule group")
|
return ErrResp(http.StatusInternalServerError, err, "failed to update rule group")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rule := range finalChanges.Update {
|
|
||||||
srv.scheduleService.UpdateAlertRule(ngmodels.AlertRuleKey{
|
|
||||||
OrgID: c.SignedInUser.OrgID,
|
|
||||||
UID: rule.Existing.UID,
|
|
||||||
}, rule.Existing.Version+1, rule.New.IsPaused)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(finalChanges.Delete) > 0 {
|
|
||||||
keys := make([]ngmodels.AlertRuleKey, 0, len(finalChanges.Delete))
|
|
||||||
for _, rule := range finalChanges.Delete {
|
|
||||||
keys = append(keys, rule.GetKey())
|
|
||||||
}
|
|
||||||
srv.scheduleService.DeleteAlertRule(keys...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if finalChanges.IsEmpty() {
|
if finalChanges.IsEmpty() {
|
||||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "no changes detected in the rule group"})
|
return response.JSON(http.StatusAccepted, util.DynMap{"message": "no changes detected in the rule group"})
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ import (
|
|||||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
@ -47,7 +46,7 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
assertRulesDeleted := func(t *testing.T, expectedRules []*models.AlertRule, ruleStore *fakes.RuleStore, scheduler *schedule.FakeScheduleService) {
|
assertRulesDeleted := func(t *testing.T, expectedRules []*models.AlertRule, ruleStore *fakes.RuleStore) {
|
||||||
deleteCommands := getRecordedCommand(ruleStore)
|
deleteCommands := getRecordedCommand(ruleStore)
|
||||||
require.Len(t, deleteCommands, 1)
|
require.Len(t, deleteCommands, 1)
|
||||||
cmd := deleteCommands[0]
|
cmd := deleteCommands[0]
|
||||||
@ -56,20 +55,6 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
for _, rule := range expectedRules {
|
for _, rule := range expectedRules {
|
||||||
require.Containsf(t, actualUIDs, rule.UID, "Rule %s was expected to be deleted but it wasn't", rule.UID)
|
require.Containsf(t, actualUIDs, rule.UID, "Rule %s was expected to be deleted but it wasn't", rule.UID)
|
||||||
}
|
}
|
||||||
|
|
||||||
notDeletedRules := make(map[models.AlertRuleKey]struct{}, len(expectedRules))
|
|
||||||
for _, rule := range expectedRules {
|
|
||||||
notDeletedRules[rule.GetKey()] = struct{}{}
|
|
||||||
}
|
|
||||||
for _, call := range scheduler.Calls {
|
|
||||||
require.Equal(t, "DeleteAlertRule", call.Method)
|
|
||||||
keys, ok := call.Arguments.Get(0).([]models.AlertRuleKey)
|
|
||||||
require.Truef(t, ok, "Expected AlertRuleKey but got something else")
|
|
||||||
for _, key := range keys {
|
|
||||||
delete(notDeletedRules, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
require.Emptyf(t, notDeletedRules, "Not all rules were deleted")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
orgID := rand.Int63()
|
orgID := rand.Int63()
|
||||||
@ -89,14 +74,10 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
ruleStore := initFakeRuleStore(t)
|
ruleStore := initFakeRuleStore(t)
|
||||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
|
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
|
||||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
|
||||||
|
|
||||||
request := createRequestContext(orgID, org.RoleViewer, nil)
|
request := createRequestContext(orgID, org.RoleViewer, nil)
|
||||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request, folder.Title, "")
|
response := createService(ac, ruleStore).RouteDeleteAlertRules(request, folder.Title, "")
|
||||||
require.Equalf(t, 401, response.Status(), "Expected 401 but got %d: %v", response.Status(), string(response.Body()))
|
require.Equalf(t, 401, response.Status(), "Expected 401 but got %d: %v", response.Status(), string(response.Body()))
|
||||||
|
|
||||||
scheduler.AssertNotCalled(t, "DeleteAlertRule")
|
|
||||||
require.Empty(t, getRecordedCommand(ruleStore))
|
require.Empty(t, getRecordedCommand(ruleStore))
|
||||||
})
|
})
|
||||||
t.Run("editor should be able to delete all non-provisioned rules in folder", func(t *testing.T) {
|
t.Run("editor should be able to delete all non-provisioned rules in folder", func(t *testing.T) {
|
||||||
@ -104,14 +85,10 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
rulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))
|
rulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))
|
||||||
ruleStore.PutRule(context.Background(), rulesInFolder...)
|
ruleStore.PutRule(context.Background(), rulesInFolder...)
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
|
||||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
|
||||||
|
|
||||||
request := createRequestContext(orgID, org.RoleEditor, nil)
|
request := createRequestContext(orgID, org.RoleEditor, nil)
|
||||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request, folder.Title, "")
|
response := createService(ac, ruleStore).RouteDeleteAlertRules(request, folder.Title, "")
|
||||||
|
|
||||||
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
||||||
assertRulesDeleted(t, rulesInFolder, ruleStore, scheduler)
|
|
||||||
})
|
})
|
||||||
t.Run("editor should be able to delete rules group if it is not provisioned", func(t *testing.T) {
|
t.Run("editor should be able to delete rules group if it is not provisioned", func(t *testing.T) {
|
||||||
groupName := util.GenerateShortUID()
|
groupName := util.GenerateShortUID()
|
||||||
@ -124,26 +101,19 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
// rules in the same group but different folder
|
// rules in the same group but different folder
|
||||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withGroup(groupName)))...)
|
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withGroup(groupName)))...)
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
|
||||||
scheduler.On("DeleteAlertRule", mock.Anything).Return()
|
|
||||||
|
|
||||||
request := createRequestContext(orgID, org.RoleEditor, nil)
|
request := createRequestContext(orgID, org.RoleEditor, nil)
|
||||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request, folder.Title, groupName)
|
response := createService(ac, ruleStore).RouteDeleteAlertRules(request, folder.Title, groupName)
|
||||||
|
|
||||||
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
||||||
assertRulesDeleted(t, rulesInFolderInGroup, ruleStore, scheduler)
|
assertRulesDeleted(t, rulesInFolderInGroup, ruleStore)
|
||||||
})
|
})
|
||||||
t.Run("should return 202 if folder is empty", func(t *testing.T) {
|
t.Run("should return 202 if folder is empty", func(t *testing.T) {
|
||||||
ruleStore := initFakeRuleStore(t)
|
ruleStore := initFakeRuleStore(t)
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
|
||||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
|
||||||
|
|
||||||
requestCtx := createRequestContext(orgID, org.RoleEditor, nil)
|
requestCtx := createRequestContext(orgID, org.RoleEditor, nil)
|
||||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(requestCtx, folder.Title, "")
|
response := createService(ac, ruleStore).RouteDeleteAlertRules(requestCtx, folder.Title, "")
|
||||||
|
|
||||||
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
||||||
scheduler.AssertNotCalled(t, "DeleteAlertRule")
|
|
||||||
require.Empty(t, getRecordedCommand(ruleStore))
|
require.Empty(t, getRecordedCommand(ruleStore))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -155,25 +125,18 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
ruleStore := initFakeRuleStore(t)
|
ruleStore := initFakeRuleStore(t)
|
||||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
|
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
|
||||||
scheduler.On("DeleteAlertRule", mock.Anything).Panic("should not be called")
|
|
||||||
|
|
||||||
ac := acMock.New()
|
ac := acMock.New()
|
||||||
request := createRequestContext(orgID, "None", nil)
|
request := createRequestContext(orgID, "None", nil)
|
||||||
|
|
||||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request, folder.Title, "")
|
response := createService(ac, ruleStore).RouteDeleteAlertRules(request, folder.Title, "")
|
||||||
require.Equalf(t, 401, response.Status(), "Expected 401 but got %d: %v", response.Status(), string(response.Body()))
|
require.Equalf(t, 401, response.Status(), "Expected 401 but got %d: %v", response.Status(), string(response.Body()))
|
||||||
|
|
||||||
scheduler.AssertNotCalled(t, "DeleteAlertRule")
|
|
||||||
require.Empty(t, getRecordedCommand(ruleStore))
|
require.Empty(t, getRecordedCommand(ruleStore))
|
||||||
})
|
})
|
||||||
t.Run("delete only non-provisioned groups that user is authorized", func(t *testing.T) {
|
t.Run("delete only non-provisioned groups that user is authorized", func(t *testing.T) {
|
||||||
ruleStore := initFakeRuleStore(t)
|
ruleStore := initFakeRuleStore(t)
|
||||||
provisioningStore := provisioning.NewFakeProvisioningStore()
|
provisioningStore := provisioning.NewFakeProvisioningStore()
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
|
||||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
|
||||||
|
|
||||||
authorizedRulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup("authz_"+util.GenerateShortUID())))
|
authorizedRulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup("authz_"+util.GenerateShortUID())))
|
||||||
|
|
||||||
provisionedRulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup("provisioned_"+util.GenerateShortUID())))
|
provisionedRulesInFolder := models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup("provisioned_"+util.GenerateShortUID())))
|
||||||
@ -187,10 +150,10 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
|
|
||||||
ac := acMock.New().WithPermissions(createPermissionsForRules(append(authorizedRulesInFolder, provisionedRulesInFolder...)))
|
ac := acMock.New().WithPermissions(createPermissionsForRules(append(authorizedRulesInFolder, provisionedRulesInFolder...)))
|
||||||
|
|
||||||
response := createServiceWithProvenanceStore(ac, ruleStore, scheduler, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.Title, "")
|
response := createServiceWithProvenanceStore(ac, ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.Title, "")
|
||||||
|
|
||||||
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
||||||
assertRulesDeleted(t, authorizedRulesInFolder, ruleStore, scheduler)
|
assertRulesDeleted(t, authorizedRulesInFolder, ruleStore)
|
||||||
})
|
})
|
||||||
t.Run("return 400 if all rules user can access are provisioned", func(t *testing.T) {
|
t.Run("return 400 if all rules user can access are provisioned", func(t *testing.T) {
|
||||||
ruleStore := initFakeRuleStore(t)
|
ruleStore := initFakeRuleStore(t)
|
||||||
@ -204,15 +167,11 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
// more rules in the same namespace but user does not have access to them
|
// more rules in the same namespace but user does not have access to them
|
||||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(util.GenerateShortUID())))...)
|
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(util.GenerateShortUID())))...)
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
|
||||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
|
||||||
|
|
||||||
ac := acMock.New().WithPermissions(createPermissionsForRules(provisionedRulesInFolder))
|
ac := acMock.New().WithPermissions(createPermissionsForRules(provisionedRulesInFolder))
|
||||||
|
|
||||||
response := createServiceWithProvenanceStore(ac, ruleStore, scheduler, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.Title, "")
|
response := createServiceWithProvenanceStore(ac, ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.Title, "")
|
||||||
|
|
||||||
require.Equalf(t, 400, response.Status(), "Expected 400 but got %d: %v", response.Status(), string(response.Body()))
|
require.Equalf(t, 400, response.Status(), "Expected 400 but got %d: %v", response.Status(), string(response.Body()))
|
||||||
scheduler.AssertNotCalled(t, "DeleteAlertRule")
|
|
||||||
require.Empty(t, getRecordedCommand(ruleStore))
|
require.Empty(t, getRecordedCommand(ruleStore))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -226,15 +185,11 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
// more rules in the same group but user is not authorized to access them
|
// more rules in the same group but user is not authorized to access them
|
||||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))...)
|
ruleStore.PutRule(context.Background(), models.GenerateAlertRulesSmallNonEmpty(models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))...)
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
|
||||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
|
||||||
|
|
||||||
ac := acMock.New().WithPermissions(createPermissionsForRules(authorizedRulesInGroup))
|
ac := acMock.New().WithPermissions(createPermissionsForRules(authorizedRulesInGroup))
|
||||||
|
|
||||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(requestCtx, folder.Title, groupName)
|
response := createService(ac, ruleStore).RouteDeleteAlertRules(requestCtx, folder.Title, groupName)
|
||||||
|
|
||||||
require.Equalf(t, 401, response.Status(), "Expected 401 but got %d: %v", response.Status(), string(response.Body()))
|
require.Equalf(t, 401, response.Status(), "Expected 401 but got %d: %v", response.Status(), string(response.Body()))
|
||||||
scheduler.AssertNotCalled(t, "DeleteAlertRule", mock.Anything)
|
|
||||||
deleteCommands := getRecordedCommand(ruleStore)
|
deleteCommands := getRecordedCommand(ruleStore)
|
||||||
require.Empty(t, deleteCommands)
|
require.Empty(t, deleteCommands)
|
||||||
})
|
})
|
||||||
@ -248,15 +203,11 @@ func TestRouteDeleteAlertRules(t *testing.T) {
|
|||||||
|
|
||||||
ruleStore.PutRule(context.Background(), provisionedRulesInFolder...)
|
ruleStore.PutRule(context.Background(), provisionedRulesInFolder...)
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
|
||||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
|
||||||
|
|
||||||
ac := acMock.New().WithPermissions(createPermissionsForRules(provisionedRulesInFolder))
|
ac := acMock.New().WithPermissions(createPermissionsForRules(provisionedRulesInFolder))
|
||||||
|
|
||||||
response := createServiceWithProvenanceStore(ac, ruleStore, scheduler, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.Title, groupName)
|
response := createServiceWithProvenanceStore(ac, ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.Title, groupName)
|
||||||
|
|
||||||
require.Equalf(t, 400, response.Status(), "Expected 400 but got %d: %v", response.Status(), string(response.Body()))
|
require.Equalf(t, 400, response.Status(), "Expected 400 but got %d: %v", response.Status(), string(response.Body()))
|
||||||
scheduler.AssertNotCalled(t, "DeleteAlertRule", mock.Anything)
|
|
||||||
deleteCommands := getRecordedCommand(ruleStore)
|
deleteCommands := getRecordedCommand(ruleStore)
|
||||||
require.Empty(t, deleteCommands)
|
require.Empty(t, deleteCommands)
|
||||||
})
|
})
|
||||||
@ -277,7 +228,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) {
|
|||||||
ac := acMock.New().WithPermissions(createPermissionsForRules(expectedRules))
|
ac := acMock.New().WithPermissions(createPermissionsForRules(expectedRules))
|
||||||
|
|
||||||
req := createRequestContext(orgID, "", nil)
|
req := createRequestContext(orgID, "", nil)
|
||||||
response := createService(ac, ruleStore, nil).RouteGetNamespaceRulesConfig(req, folder.Title)
|
response := createService(ac, ruleStore).RouteGetNamespaceRulesConfig(req, folder.Title)
|
||||||
|
|
||||||
require.Equal(t, http.StatusAccepted, response.Status())
|
require.Equal(t, http.StatusAccepted, response.Status())
|
||||||
result := &apimodels.NamespaceConfigResponse{}
|
result := &apimodels.NamespaceConfigResponse{}
|
||||||
@ -312,7 +263,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) {
|
|||||||
ac := acMock.New().WithDisabled()
|
ac := acMock.New().WithDisabled()
|
||||||
|
|
||||||
req := createRequestContext(orgID, org.RoleViewer, nil)
|
req := createRequestContext(orgID, org.RoleViewer, nil)
|
||||||
response := createService(ac, ruleStore, nil).RouteGetNamespaceRulesConfig(req, folder.Title)
|
response := createService(ac, ruleStore).RouteGetNamespaceRulesConfig(req, folder.Title)
|
||||||
|
|
||||||
require.Equal(t, http.StatusAccepted, response.Status())
|
require.Equal(t, http.StatusAccepted, response.Status())
|
||||||
result := &apimodels.NamespaceConfigResponse{}
|
result := &apimodels.NamespaceConfigResponse{}
|
||||||
@ -345,7 +296,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) {
|
|||||||
ruleStore.PutRule(context.Background(), expectedRules...)
|
ruleStore.PutRule(context.Background(), expectedRules...)
|
||||||
ac := acMock.New().WithDisabled()
|
ac := acMock.New().WithDisabled()
|
||||||
|
|
||||||
svc := createService(ac, ruleStore, nil)
|
svc := createService(ac, ruleStore)
|
||||||
|
|
||||||
// add provenance to the first generated rule
|
// add provenance to the first generated rule
|
||||||
rule := &models.AlertRule{
|
rule := &models.AlertRule{
|
||||||
@ -389,7 +340,7 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) {
|
|||||||
ruleStore.PutRule(context.Background(), expectedRules...)
|
ruleStore.PutRule(context.Background(), expectedRules...)
|
||||||
ac := acMock.New().WithDisabled()
|
ac := acMock.New().WithDisabled()
|
||||||
|
|
||||||
response := createService(ac, ruleStore, nil).RouteGetNamespaceRulesConfig(createRequestContext(orgID, org.RoleViewer, nil), folder.Title)
|
response := createService(ac, ruleStore).RouteGetNamespaceRulesConfig(createRequestContext(orgID, org.RoleViewer, nil), folder.Title)
|
||||||
|
|
||||||
require.Equal(t, http.StatusAccepted, response.Status())
|
require.Equal(t, http.StatusAccepted, response.Status())
|
||||||
result := &apimodels.NamespaceConfigResponse{}
|
result := &apimodels.NamespaceConfigResponse{}
|
||||||
@ -441,7 +392,7 @@ func TestRouteGetRulesConfig(t *testing.T) {
|
|||||||
request := createRequestContext(orgID, "", nil)
|
request := createRequestContext(orgID, "", nil)
|
||||||
t.Run("and do not return group if user does not have access to one of rules", func(t *testing.T) {
|
t.Run("and do not return group if user does not have access to one of rules", func(t *testing.T) {
|
||||||
ac := acMock.New().WithPermissions(createPermissionsForRules(append(group1, group2[1:]...)))
|
ac := acMock.New().WithPermissions(createPermissionsForRules(append(group1, group2[1:]...)))
|
||||||
response := createService(ac, ruleStore, nil).RouteGetRulesConfig(request)
|
response := createService(ac, ruleStore).RouteGetRulesConfig(request)
|
||||||
require.Equal(t, http.StatusOK, response.Status())
|
require.Equal(t, http.StatusOK, response.Status())
|
||||||
|
|
||||||
result := &apimodels.NamespaceConfigResponse{}
|
result := &apimodels.NamespaceConfigResponse{}
|
||||||
@ -471,7 +422,7 @@ func TestRouteGetRulesConfig(t *testing.T) {
|
|||||||
ruleStore.PutRule(context.Background(), expectedRules...)
|
ruleStore.PutRule(context.Background(), expectedRules...)
|
||||||
ac := acMock.New().WithDisabled()
|
ac := acMock.New().WithDisabled()
|
||||||
|
|
||||||
response := createService(ac, ruleStore, nil).RouteGetRulesConfig(createRequestContext(orgID, org.RoleViewer, nil))
|
response := createService(ac, ruleStore).RouteGetRulesConfig(createRequestContext(orgID, org.RoleViewer, nil))
|
||||||
|
|
||||||
require.Equal(t, http.StatusOK, response.Status())
|
require.Equal(t, http.StatusOK, response.Status())
|
||||||
result := &apimodels.NamespaceConfigResponse{}
|
result := &apimodels.NamespaceConfigResponse{}
|
||||||
@ -522,13 +473,13 @@ func TestRouteGetRulesGroupConfig(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("and return 401 if user does not have access one of rules", func(t *testing.T) {
|
t.Run("and return 401 if user does not have access one of rules", func(t *testing.T) {
|
||||||
ac := acMock.New().WithPermissions(createPermissionsForRules(expectedRules[1:]))
|
ac := acMock.New().WithPermissions(createPermissionsForRules(expectedRules[1:]))
|
||||||
response := createService(ac, ruleStore, nil).RouteGetRulesGroupConfig(request, folder.Title, groupKey.RuleGroup)
|
response := createService(ac, ruleStore).RouteGetRulesGroupConfig(request, folder.Title, groupKey.RuleGroup)
|
||||||
require.Equal(t, http.StatusUnauthorized, response.Status())
|
require.Equal(t, http.StatusUnauthorized, response.Status())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("and return rules if user has access to all of them", func(t *testing.T) {
|
t.Run("and return rules if user has access to all of them", func(t *testing.T) {
|
||||||
ac := acMock.New().WithPermissions(createPermissionsForRules(expectedRules))
|
ac := acMock.New().WithPermissions(createPermissionsForRules(expectedRules))
|
||||||
response := createService(ac, ruleStore, nil).RouteGetRulesGroupConfig(request, folder.Title, groupKey.RuleGroup)
|
response := createService(ac, ruleStore).RouteGetRulesGroupConfig(request, folder.Title, groupKey.RuleGroup)
|
||||||
|
|
||||||
require.Equal(t, http.StatusAccepted, response.Status())
|
require.Equal(t, http.StatusAccepted, response.Status())
|
||||||
result := &apimodels.RuleGroupConfigResponse{}
|
result := &apimodels.RuleGroupConfigResponse{}
|
||||||
@ -551,7 +502,7 @@ func TestRouteGetRulesGroupConfig(t *testing.T) {
|
|||||||
ruleStore.PutRule(context.Background(), expectedRules...)
|
ruleStore.PutRule(context.Background(), expectedRules...)
|
||||||
ac := acMock.New().WithDisabled()
|
ac := acMock.New().WithDisabled()
|
||||||
|
|
||||||
response := createService(ac, ruleStore, nil).RouteGetRulesGroupConfig(createRequestContext(orgID, org.RoleViewer, nil), folder.Title, groupKey.RuleGroup)
|
response := createService(ac, ruleStore).RouteGetRulesGroupConfig(createRequestContext(orgID, org.RoleViewer, nil), folder.Title, groupKey.RuleGroup)
|
||||||
|
|
||||||
require.Equal(t, http.StatusAccepted, response.Status())
|
require.Equal(t, http.StatusAccepted, response.Status())
|
||||||
result := &apimodels.RuleGroupConfigResponse{}
|
result := &apimodels.RuleGroupConfigResponse{}
|
||||||
@ -638,19 +589,18 @@ func TestVerifyProvisionedRulesNotAffected(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func createServiceWithProvenanceStore(ac *acMock.Mock, store *fakes.RuleStore, scheduler schedule.ScheduleService, provenanceStore provisioning.ProvisioningStore) *RulerSrv {
|
func createServiceWithProvenanceStore(ac *acMock.Mock, store *fakes.RuleStore, provenanceStore provisioning.ProvisioningStore) *RulerSrv {
|
||||||
svc := createService(ac, store, scheduler)
|
svc := createService(ac, store)
|
||||||
svc.provenanceStore = provenanceStore
|
svc.provenanceStore = provenanceStore
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
func createService(ac *acMock.Mock, store *fakes.RuleStore, scheduler schedule.ScheduleService) *RulerSrv {
|
func createService(ac *acMock.Mock, store *fakes.RuleStore) *RulerSrv {
|
||||||
return &RulerSrv{
|
return &RulerSrv{
|
||||||
xactManager: store,
|
xactManager: store,
|
||||||
store: store,
|
store: store,
|
||||||
QuotaService: nil,
|
QuotaService: nil,
|
||||||
provenanceStore: provisioning.NewFakeProvisioningStore(),
|
provenanceStore: provisioning.NewFakeProvisioningStore(),
|
||||||
scheduleService: scheduler,
|
|
||||||
log: log.New("test"),
|
log: log.New("test"),
|
||||||
cfg: nil,
|
cfg: nil,
|
||||||
ac: ac,
|
ac: ac,
|
||||||
|
@ -228,7 +228,7 @@ func (ng *AlertNG) init() error {
|
|||||||
|
|
||||||
// if it is required to include folder title to the alerts, we need to subscribe to changes of alert title
|
// if it is required to include folder title to the alerts, we need to subscribe to changes of alert title
|
||||||
if !ng.Cfg.UnifiedAlerting.ReservedLabels.IsReservedLabelDisabled(models.FolderTitleLabel) {
|
if !ng.Cfg.UnifiedAlerting.ReservedLabels.IsReservedLabelDisabled(models.FolderTitleLabel) {
|
||||||
subscribeToFolderChanges(context.Background(), ng.Log, ng.bus, store, scheduler)
|
subscribeToFolderChanges(ng.Log, ng.bus, store)
|
||||||
}
|
}
|
||||||
|
|
||||||
ng.stateManager = stateManager
|
ng.stateManager = stateManager
|
||||||
@ -248,7 +248,6 @@ func (ng *AlertNG) init() error {
|
|||||||
DatasourceCache: ng.DataSourceCache,
|
DatasourceCache: ng.DataSourceCache,
|
||||||
DatasourceService: ng.DataSourceService,
|
DatasourceService: ng.DataSourceService,
|
||||||
RouteRegister: ng.RouteRegister,
|
RouteRegister: ng.RouteRegister,
|
||||||
Schedule: ng.schedule,
|
|
||||||
DataProxy: ng.DataProxy,
|
DataProxy: ng.DataProxy,
|
||||||
QuotaService: ng.QuotaService,
|
QuotaService: ng.QuotaService,
|
||||||
TransactionManager: store,
|
TransactionManager: store,
|
||||||
@ -296,26 +295,18 @@ func (ng *AlertNG) init() error {
|
|||||||
return DeclareFixedRoles(ng.accesscontrolService)
|
return DeclareFixedRoles(ng.accesscontrolService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribeToFolderChanges(ctx context.Context, logger log.Logger, bus bus.Bus, dbStore api.RuleStore, scheduler schedule.ScheduleService) {
|
func subscribeToFolderChanges(logger log.Logger, bus bus.Bus, dbStore api.RuleStore) {
|
||||||
// if folder title is changed, we update all alert rules in that folder to make sure that all peers (in HA mode) will update folder title and
|
// if folder title is changed, we update all alert rules in that folder to make sure that all peers (in HA mode) will update folder title and
|
||||||
// clean up the current state
|
// clean up the current state
|
||||||
bus.AddEventListener(func(ctx context.Context, e *events.FolderTitleUpdated) error {
|
bus.AddEventListener(func(ctx context.Context, e *events.FolderTitleUpdated) error {
|
||||||
// do not block the upstream execution
|
// do not block the upstream execution
|
||||||
go func(evt *events.FolderTitleUpdated) {
|
go func(evt *events.FolderTitleUpdated) {
|
||||||
logger.Info("Got folder title updated event. updating rules in the folder", "folderUID", evt.UID)
|
logger.Info("Got folder title updated event. updating rules in the folder", "folderUID", evt.UID)
|
||||||
updated, err := dbStore.IncreaseVersionForAllRulesInNamespace(ctx, evt.OrgID, evt.UID)
|
_, err := dbStore.IncreaseVersionForAllRulesInNamespace(ctx, evt.OrgID, evt.UID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to update alert rules in the folder after its title was changed", "error", err, "folderUID", evt.UID, "folder", evt.Title)
|
logger.Error("Failed to update alert rules in the folder after its title was changed", "error", err, "folderUID", evt.UID, "folder", evt.Title)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(updated) > 0 {
|
|
||||||
logger.Info("Rules that belong to the folder have been updated successfully. Clearing their status", "folderUID", evt.UID, "updatedRules", len(updated))
|
|
||||||
for _, key := range updated {
|
|
||||||
scheduler.UpdateAlertRule(key.AlertRuleKey, key.Version, key.IsPaused)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.Debug("No alert rules found in the folder. nothing to update", "folderUID", evt.UID, "folder", evt.Title)
|
|
||||||
}
|
|
||||||
}(e)
|
}(e)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
@ -19,7 +18,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/folder"
|
"github.com/grafana/grafana/pkg/services/folder"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
@ -39,10 +37,7 @@ func Test_subscribeToFolderChanges(t *testing.T) {
|
|||||||
db.Folders[orgID] = append(db.Folders[orgID], folder)
|
db.Folders[orgID] = append(db.Folders[orgID], folder)
|
||||||
db.PutRule(context.Background(), rules...)
|
db.PutRule(context.Background(), rules...)
|
||||||
|
|
||||||
scheduler := &schedule.FakeScheduleService{}
|
subscribeToFolderChanges(log.New("test"), bus, db)
|
||||||
scheduler.On("UpdateAlertRule", mock.Anything, mock.Anything, mock.Anything).Return()
|
|
||||||
|
|
||||||
subscribeToFolderChanges(context.Background(), log.New("test"), bus, db, scheduler)
|
|
||||||
|
|
||||||
err := bus.Publish(context.Background(), &events.FolderTitleUpdated{
|
err := bus.Publish(context.Background(), &events.FolderTitleUpdated{
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
@ -62,20 +57,6 @@ func Test_subscribeToFolderChanges(t *testing.T) {
|
|||||||
return c, true
|
return c, true
|
||||||
})) > 0
|
})) > 0
|
||||||
}, time.Second, 10*time.Millisecond, "expected to call db store method but nothing was called")
|
}, time.Second, 10*time.Millisecond, "expected to call db store method but nothing was called")
|
||||||
|
|
||||||
var calledTimes int
|
|
||||||
require.Eventuallyf(t, func() bool {
|
|
||||||
for _, call := range scheduler.Calls {
|
|
||||||
if call.Method == "UpdateAlertRule" {
|
|
||||||
calledTimes++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return calledTimes == len(rules)
|
|
||||||
}, time.Second, 10*time.Millisecond, "scheduler was expected to be called %d times but called %d", len(rules), calledTimes)
|
|
||||||
|
|
||||||
for _, rule := range rules {
|
|
||||||
scheduler.AssertCalled(t, "UpdateAlertRule", rule.GetKey(), rule.Version, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigureHistorianBackend(t *testing.T) {
|
func TestConfigureHistorianBackend(t *testing.T) {
|
||||||
|
@ -34,9 +34,9 @@ func sortedUIDs(alertRules []*models.AlertRule) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateSchedulableAlertRules updates the alert rules for the scheduler.
|
// updateSchedulableAlertRules updates the alert rules for the scheduler.
|
||||||
// It returns an error if the database is unavailable or the query returned
|
// It returns diff that contains rule keys that were updated since the last poll,
|
||||||
// an error.
|
// and an error if the database query encountered problems.
|
||||||
func (sch *schedule) updateSchedulableAlertRules(ctx context.Context) error {
|
func (sch *schedule) updateSchedulableAlertRules(ctx context.Context) (diff, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
sch.metrics.UpdateSchedulableAlertRulesDuration.Observe(
|
sch.metrics.UpdateSchedulableAlertRulesDuration.Observe(
|
||||||
@ -46,21 +46,21 @@ func (sch *schedule) updateSchedulableAlertRules(ctx context.Context) error {
|
|||||||
if !sch.schedulableAlertRules.isEmpty() {
|
if !sch.schedulableAlertRules.isEmpty() {
|
||||||
keys, err := sch.ruleStore.GetAlertRulesKeysForScheduling(ctx)
|
keys, err := sch.ruleStore.GetAlertRulesKeysForScheduling(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return diff{}, err
|
||||||
}
|
}
|
||||||
if !sch.schedulableAlertRules.needsUpdate(keys) {
|
if !sch.schedulableAlertRules.needsUpdate(keys) {
|
||||||
sch.log.Debug("No changes detected. Skip updating")
|
sch.log.Debug("No changes detected. Skip updating")
|
||||||
return nil
|
return diff{}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// At this point, we know we need to re-fetch rules as there are changes.
|
||||||
q := models.GetAlertRulesForSchedulingQuery{
|
q := models.GetAlertRulesForSchedulingQuery{
|
||||||
PopulateFolders: !sch.disableGrafanaFolder,
|
PopulateFolders: !sch.disableGrafanaFolder,
|
||||||
}
|
}
|
||||||
if err := sch.ruleStore.GetAlertRulesForScheduling(ctx, &q); err != nil {
|
if err := sch.ruleStore.GetAlertRulesForScheduling(ctx, &q); err != nil {
|
||||||
return fmt.Errorf("failed to get alert rules: %w", err)
|
return diff{}, fmt.Errorf("failed to get alert rules: %w", err)
|
||||||
}
|
}
|
||||||
sch.log.Debug("Alert rules fetched", "rulesCount", len(q.ResultRules), "foldersCount", len(q.ResultFoldersTitles))
|
d := sch.schedulableAlertRules.set(q.ResultRules, q.ResultFoldersTitles)
|
||||||
sch.schedulableAlertRules.set(q.ResultRules, q.ResultFoldersTitles)
|
sch.log.Debug("Alert rules fetched", "rulesCount", len(q.ResultRules), "foldersCount", len(q.ResultFoldersTitles), "updatedRules", len(d.updated))
|
||||||
return nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package schedule
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -32,19 +31,6 @@ func (r *alertRuleInfoRegistry) getOrCreateInfo(context context.Context, key mod
|
|||||||
return info, !ok
|
return info, !ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// get returns the channel for the specific alert rule
|
|
||||||
// if the key does not exist returns an error
|
|
||||||
func (r *alertRuleInfoRegistry) get(key models.AlertRuleKey) (*alertRuleInfo, error) {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
info, ok := r.alertRuleInfo[key]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("%v key not found", key)
|
|
||||||
}
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *alertRuleInfoRegistry) exists(key models.AlertRuleKey) bool {
|
func (r *alertRuleInfoRegistry) exists(key models.AlertRuleKey) bool {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
@ -169,16 +155,19 @@ func (r *alertRulesRegistry) get(k models.AlertRuleKey) *models.AlertRule {
|
|||||||
return r.rules[k]
|
return r.rules[k]
|
||||||
}
|
}
|
||||||
|
|
||||||
// set replaces all rules in the registry.
|
// set replaces all rules in the registry. Returns difference between previous and the new current version of the registry
|
||||||
func (r *alertRulesRegistry) set(rules []*models.AlertRule, folders map[string]string) {
|
func (r *alertRulesRegistry) set(rules []*models.AlertRule, folders map[string]string) diff {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
r.rules = make(map[models.AlertRuleKey]*models.AlertRule)
|
rulesMap := make(map[models.AlertRuleKey]*models.AlertRule)
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
r.rules[rule.GetKey()] = rule
|
rulesMap[rule.GetKey()] = rule
|
||||||
}
|
}
|
||||||
|
d := r.getDiff(rulesMap)
|
||||||
|
r.rules = rulesMap
|
||||||
// return the map as is without copying because it is not mutated
|
// return the map as is without copying because it is not mutated
|
||||||
r.folderTitles = folders
|
r.folderTitles = folders
|
||||||
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// update inserts or replaces a rule in the registry.
|
// update inserts or replaces a rule in the registry.
|
||||||
@ -219,3 +208,28 @@ func (r *alertRulesRegistry) needsUpdate(keys []models.AlertRuleKeyWithVersion)
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type diff struct {
|
||||||
|
updated map[models.AlertRuleKey]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d diff) IsEmpty() bool {
|
||||||
|
return len(d.updated) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDiff calculates difference between the list of rules fetched previously and provided keys. Returns diff where
|
||||||
|
// updated - a list of keys that exist in the registry but with different version,
|
||||||
|
func (r *alertRulesRegistry) getDiff(rules map[models.AlertRuleKey]*models.AlertRule) diff {
|
||||||
|
result := diff{
|
||||||
|
updated: map[models.AlertRuleKey]struct{}{},
|
||||||
|
}
|
||||||
|
for key, newRule := range rules {
|
||||||
|
oldRule, ok := r.rules[key]
|
||||||
|
if !ok || newRule.Version == oldRule.Version {
|
||||||
|
// a new rule or not updated
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.updated[key] = struct{}{}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
@ -318,3 +318,53 @@ func TestSchedulableAlertRulesRegistry(t *testing.T) {
|
|||||||
assert.False(t, ok)
|
assert.False(t, ok)
|
||||||
assert.Nil(t, deleted)
|
assert.Nil(t, deleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSchedulableAlertRulesRegistry_set(t *testing.T) {
|
||||||
|
_, initialRules := models.GenerateUniqueAlertRules(100, models.AlertRuleGen())
|
||||||
|
init := make(map[models.AlertRuleKey]*models.AlertRule, len(initialRules))
|
||||||
|
for _, rule := range initialRules {
|
||||||
|
init[rule.GetKey()] = rule
|
||||||
|
}
|
||||||
|
r := alertRulesRegistry{rules: init}
|
||||||
|
t.Run("should return empty diff if exactly the same rules", func(t *testing.T) {
|
||||||
|
newRules := make([]*models.AlertRule, 0, len(initialRules))
|
||||||
|
for _, rule := range initialRules {
|
||||||
|
newRules = append(newRules, models.CopyRule(rule))
|
||||||
|
}
|
||||||
|
diff := r.set(newRules, map[string]string{})
|
||||||
|
require.Truef(t, diff.IsEmpty(), "Diff is not empty. Probably we check something else than key + version")
|
||||||
|
})
|
||||||
|
t.Run("should return empty diff if version does not change", func(t *testing.T) {
|
||||||
|
newRules := make([]*models.AlertRule, 0, len(initialRules))
|
||||||
|
// generate random and then override rule key + version
|
||||||
|
_, randomNew := models.GenerateUniqueAlertRules(len(initialRules), models.AlertRuleGen())
|
||||||
|
for i := 0; i < len(initialRules); i++ {
|
||||||
|
rule := randomNew[i]
|
||||||
|
oldRule := initialRules[i]
|
||||||
|
rule.UID = oldRule.UID
|
||||||
|
rule.OrgID = oldRule.OrgID
|
||||||
|
rule.Version = oldRule.Version
|
||||||
|
newRules = append(newRules, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
diff := r.set(newRules, map[string]string{})
|
||||||
|
require.Truef(t, diff.IsEmpty(), "Diff is not empty. Probably we check something else than key + version")
|
||||||
|
})
|
||||||
|
t.Run("should return key in diff if version changes", func(t *testing.T) {
|
||||||
|
newRules := make([]*models.AlertRule, 0, len(initialRules))
|
||||||
|
expectedUpdated := map[models.AlertRuleKey]struct{}{}
|
||||||
|
for i, rule := range initialRules {
|
||||||
|
cp := models.CopyRule(rule)
|
||||||
|
if i%2 == 0 {
|
||||||
|
cp.Version++
|
||||||
|
expectedUpdated[cp.GetKey()] = struct{}{}
|
||||||
|
}
|
||||||
|
newRules = append(newRules, cp)
|
||||||
|
}
|
||||||
|
require.NotEmptyf(t, expectedUpdated, "Input parameters have changed. Nothing to assert")
|
||||||
|
|
||||||
|
diff := r.set(newRules, map[string]string{})
|
||||||
|
require.Falsef(t, diff.IsEmpty(), "Diff is empty but should not be")
|
||||||
|
require.Equal(t, expectedUpdated, diff.updated)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -29,16 +29,10 @@ import (
|
|||||||
|
|
||||||
// ScheduleService is an interface for a service that schedules the evaluation
|
// ScheduleService is an interface for a service that schedules the evaluation
|
||||||
// of alert rules.
|
// of alert rules.
|
||||||
//
|
|
||||||
//go:generate mockery --name ScheduleService --structname FakeScheduleService --inpackage --filename schedule_mock.go --unroll-variadic=False
|
|
||||||
type ScheduleService interface {
|
type ScheduleService interface {
|
||||||
// Run the scheduler until the context is canceled or the scheduler returns
|
// Run the scheduler until the context is canceled or the scheduler returns
|
||||||
// an error. The scheduler is terminated when this function returns.
|
// an error. The scheduler is terminated when this function returns.
|
||||||
Run(context.Context) error
|
Run(context.Context) error
|
||||||
// UpdateAlertRule notifies scheduler that a rule has been changed
|
|
||||||
UpdateAlertRule(key ngmodels.AlertRuleKey, lastVersion int64, isPaused bool)
|
|
||||||
// DeleteAlertRule notifies scheduler that rules have been deleted
|
|
||||||
DeleteAlertRule(keys ...ngmodels.AlertRuleKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlertsSender is an interface for a service that is responsible for sending notifications to the end-user.
|
// AlertsSender is an interface for a service that is responsible for sending notifications to the end-user.
|
||||||
@ -148,17 +142,8 @@ func (sch *schedule) Run(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAlertRule looks for the active rule evaluation and commands it to update the rule
|
// deleteAlertRule stops evaluation of the rule, deletes it from active rules, and cleans up state cache.
|
||||||
func (sch *schedule) UpdateAlertRule(key ngmodels.AlertRuleKey, lastVersion int64, isPaused bool) {
|
func (sch *schedule) deleteAlertRule(keys ...ngmodels.AlertRuleKey) {
|
||||||
ruleInfo, err := sch.registry.get(key)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ruleInfo.update(ruleVersionAndPauseStatus{ruleVersion(lastVersion), isPaused})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAlertRule stops evaluation of the rule, deletes it from active rules, and cleans up state cache.
|
|
||||||
func (sch *schedule) DeleteAlertRule(keys ...ngmodels.AlertRuleKey) {
|
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
// It can happen that the scheduler has deleted the alert rule before the
|
// It can happen that the scheduler has deleted the alert rule before the
|
||||||
// Ruler API has called DeleteAlertRule. This can happen as requests to
|
// Ruler API has called DeleteAlertRule. This can happen as requests to
|
||||||
@ -230,12 +215,22 @@ func (sch *schedule) updateRulesMetrics(alertRules []*ngmodels.AlertRule) {
|
|||||||
sch.metrics.SchedulableAlertRulesHash.Set(float64(hashUIDs(alertRules)))
|
sch.metrics.SchedulableAlertRulesHash.Set(float64(hashUIDs(alertRules)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup.Group, tick time.Time) ([]readyToRunItem, map[ngmodels.AlertRuleKey]struct{}) {
|
// TODO refactor to accept a callback for tests that will be called with things that are returned currently, and return nothing.
|
||||||
|
// Returns a slice of rules that were scheduled for evaluation, map of stopped rules, and a slice of updated rules
|
||||||
|
func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup.Group, tick time.Time) ([]readyToRunItem, map[ngmodels.AlertRuleKey]struct{}, []ngmodels.AlertRuleKeyWithVersion) {
|
||||||
tickNum := tick.Unix() / int64(sch.baseInterval.Seconds())
|
tickNum := tick.Unix() / int64(sch.baseInterval.Seconds())
|
||||||
|
|
||||||
if err := sch.updateSchedulableAlertRules(ctx); err != nil {
|
// update the local registry. If there was a difference between the previous state and the current new state, rulesDiff will contains keys of rules that were updated.
|
||||||
|
rulesDiff, err := sch.updateSchedulableAlertRules(ctx)
|
||||||
|
updated := rulesDiff.updated
|
||||||
|
if updated == nil { // make sure map is not nil
|
||||||
|
updated = map[ngmodels.AlertRuleKey]struct{}{}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
sch.log.Error("Failed to update alert rules", "error", err)
|
sch.log.Error("Failed to update alert rules", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this is the new current state. rulesDiff contains the previously existing rules that were different between this state and the previous state.
|
||||||
alertRules, folderTitles := sch.schedulableAlertRules.all()
|
alertRules, folderTitles := sch.schedulableAlertRules.all()
|
||||||
|
|
||||||
// registeredDefinitions is a map used for finding deleted alert rules
|
// registeredDefinitions is a map used for finding deleted alert rules
|
||||||
@ -247,6 +242,7 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup.
|
|||||||
sch.updateRulesMetrics(alertRules)
|
sch.updateRulesMetrics(alertRules)
|
||||||
|
|
||||||
readyToRun := make([]readyToRunItem, 0)
|
readyToRun := make([]readyToRunItem, 0)
|
||||||
|
updatedRules := make([]ngmodels.AlertRuleKeyWithVersion, 0, len(updated)) // this is needed for tests only
|
||||||
missingFolder := make(map[string][]string)
|
missingFolder := make(map[string][]string)
|
||||||
for _, item := range alertRules {
|
for _, item := range alertRules {
|
||||||
key := item.GetKey()
|
key := item.GetKey()
|
||||||
@ -274,7 +270,8 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup.
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemFrequency := item.IntervalSeconds / int64(sch.baseInterval.Seconds())
|
itemFrequency := item.IntervalSeconds / int64(sch.baseInterval.Seconds())
|
||||||
if item.IntervalSeconds != 0 && tickNum%itemFrequency == 0 {
|
isReadyToRun := item.IntervalSeconds != 0 && tickNum%itemFrequency == 0
|
||||||
|
if isReadyToRun {
|
||||||
var folderTitle string
|
var folderTitle string
|
||||||
if !sch.disableGrafanaFolder {
|
if !sch.disableGrafanaFolder {
|
||||||
title, ok := folderTitles[item.NamespaceUID]
|
title, ok := folderTitles[item.NamespaceUID]
|
||||||
@ -290,6 +287,20 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup.
|
|||||||
folderTitle: folderTitle,
|
folderTitle: folderTitle,
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
if _, isUpdated := updated[key]; isUpdated && !isReadyToRun {
|
||||||
|
// if we do not need to eval the rule, check the whether rule was just updated and if it was, notify evaluation routine about that
|
||||||
|
sch.log.Debug("Rule has been updated. Notifying evaluation routine", key.LogContext()...)
|
||||||
|
go func(ri *alertRuleInfo, rule *ngmodels.AlertRule) {
|
||||||
|
ri.update(ruleVersionAndPauseStatus{
|
||||||
|
Version: ruleVersion(rule.Version),
|
||||||
|
IsPaused: rule.IsPaused,
|
||||||
|
})
|
||||||
|
}(ruleInfo, item)
|
||||||
|
updatedRules = append(updatedRules, ngmodels.AlertRuleKeyWithVersion{
|
||||||
|
Version: item.Version,
|
||||||
|
AlertRuleKey: item.GetKey(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// remove the alert rule from the registered alert rules
|
// remove the alert rule from the registered alert rules
|
||||||
delete(registeredDefinitions, key)
|
delete(registeredDefinitions, key)
|
||||||
@ -327,8 +338,8 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup.
|
|||||||
for key := range registeredDefinitions {
|
for key := range registeredDefinitions {
|
||||||
toDelete = append(toDelete, key)
|
toDelete = append(toDelete, key)
|
||||||
}
|
}
|
||||||
sch.DeleteAlertRule(toDelete...)
|
sch.deleteAlertRule(toDelete...)
|
||||||
return readyToRun, registeredDefinitions
|
return readyToRun, registeredDefinitions, updatedRules
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertRuleKey, evalCh <-chan *evaluation, updateCh <-chan ruleVersionAndPauseStatus) error {
|
func (sch *schedule) ruleRoutine(grafanaCtx context.Context, key ngmodels.AlertRuleKey, evalCh <-chan *evaluation, updateCh <-chan ruleVersionAndPauseStatus) error {
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
|
||||||
|
|
||||||
package schedule
|
|
||||||
|
|
||||||
import (
|
|
||||||
context "context"
|
|
||||||
time "time"
|
|
||||||
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
|
||||||
|
|
||||||
models "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FakeScheduleService is an autogenerated mock type for the ScheduleService type
|
|
||||||
type FakeScheduleService struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAlertRule provides a mock function with given fields: keys
|
|
||||||
func (_m *FakeScheduleService) DeleteAlertRule(keys ...models.AlertRuleKey) {
|
|
||||||
_m.Called(keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run provides a mock function with given fields: _a0
|
|
||||||
func (_m *FakeScheduleService) Run(_a0 context.Context) error {
|
|
||||||
ret := _m.Called(_a0)
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
|
||||||
r0 = rf(_a0)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateAlertRule provides a mock function with given fields: key, lastVersion
|
|
||||||
func (_m *FakeScheduleService) UpdateAlertRule(key models.AlertRuleKey, lastVersion int64, isPaused bool) {
|
|
||||||
_m.Called(key, lastVersion, isPaused)
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalApplied provides a mock function with given fields: _a0, _a1
|
|
||||||
func (_m *FakeScheduleService) evalApplied(_a0 models.AlertRuleKey, _a1 time.Time) {
|
|
||||||
_m.Called(_a0, _a1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// overrideCfg provides a mock function with given fields: cfg
|
|
||||||
func (_m *FakeScheduleService) overrideCfg(cfg SchedulerCfg) {
|
|
||||||
_m.Called(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stopApplied provides a mock function with given fields: _a0
|
|
||||||
func (_m *FakeScheduleService) stopApplied(_a0 models.AlertRuleKey) {
|
|
||||||
_m.Called(_a0)
|
|
||||||
}
|
|
@ -104,13 +104,13 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
t.Run("on 1st tick alert rule should be evaluated", func(t *testing.T) {
|
t.Run("on 1st tick alert rule should be evaluated", func(t *testing.T) {
|
||||||
tick = tick.Add(cfg.BaseInterval)
|
tick = tick.Add(cfg.BaseInterval)
|
||||||
|
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
require.Len(t, scheduled, 1)
|
require.Len(t, scheduled, 1)
|
||||||
require.Equal(t, alertRule1, scheduled[0].rule)
|
require.Equal(t, alertRule1, scheduled[0].rule)
|
||||||
require.Equal(t, tick, scheduled[0].scheduledAt)
|
require.Equal(t, tick, scheduled[0].scheduledAt)
|
||||||
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -132,12 +132,13 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("on 2nd tick first alert rule should be evaluated", func(t *testing.T) {
|
t.Run("on 2nd tick first alert rule should be evaluated", func(t *testing.T) {
|
||||||
tick = tick.Add(cfg.BaseInterval)
|
tick = tick.Add(cfg.BaseInterval)
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
require.Len(t, scheduled, 1)
|
require.Len(t, scheduled, 1)
|
||||||
require.Equal(t, alertRule1, scheduled[0].rule)
|
require.Equal(t, alertRule1, scheduled[0].rule)
|
||||||
require.Equal(t, tick, scheduled[0].scheduledAt)
|
require.Equal(t, tick, scheduled[0].scheduledAt)
|
||||||
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -155,7 +156,7 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("on 3rd tick two alert rules should be evaluated", func(t *testing.T) {
|
t.Run("on 3rd tick two alert rules should be evaluated", func(t *testing.T) {
|
||||||
tick = tick.Add(cfg.BaseInterval)
|
tick = tick.Add(cfg.BaseInterval)
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
require.Len(t, scheduled, 2)
|
require.Len(t, scheduled, 2)
|
||||||
var keys []models.AlertRuleKey
|
var keys []models.AlertRuleKey
|
||||||
for _, item := range scheduled {
|
for _, item := range scheduled {
|
||||||
@ -166,19 +167,19 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
require.Contains(t, keys, alertRule2.GetKey())
|
require.Contains(t, keys, alertRule2.GetKey())
|
||||||
|
|
||||||
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
assertEvalRun(t, evalAppliedCh, tick, keys...)
|
assertEvalRun(t, evalAppliedCh, tick, keys...)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("on 4th tick only one alert rule should be evaluated", func(t *testing.T) {
|
t.Run("on 4th tick only one alert rule should be evaluated", func(t *testing.T) {
|
||||||
tick = tick.Add(cfg.BaseInterval)
|
tick = tick.Add(cfg.BaseInterval)
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
require.Len(t, scheduled, 1)
|
require.Len(t, scheduled, 1)
|
||||||
require.Equal(t, alertRule1, scheduled[0].rule)
|
require.Equal(t, alertRule1, scheduled[0].rule)
|
||||||
require.Equal(t, tick, scheduled[0].scheduledAt)
|
require.Equal(t, tick, scheduled[0].scheduledAt)
|
||||||
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -187,13 +188,13 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
|
|
||||||
alertRule1.IsPaused = true
|
alertRule1.IsPaused = true
|
||||||
|
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
require.Len(t, scheduled, 1)
|
require.Len(t, scheduled, 1)
|
||||||
require.Equal(t, alertRule1, scheduled[0].rule)
|
require.Equal(t, alertRule1, scheduled[0].rule)
|
||||||
require.Equal(t, tick, scheduled[0].scheduledAt)
|
require.Equal(t, tick, scheduled[0].scheduledAt)
|
||||||
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -214,7 +215,7 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
|
|
||||||
alertRule2.IsPaused = true
|
alertRule2.IsPaused = true
|
||||||
|
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
require.Len(t, scheduled, 2)
|
require.Len(t, scheduled, 2)
|
||||||
var keys []models.AlertRuleKey
|
var keys []models.AlertRuleKey
|
||||||
@ -226,7 +227,7 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
require.Contains(t, keys, alertRule2.GetKey())
|
require.Contains(t, keys, alertRule2.GetKey())
|
||||||
|
|
||||||
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
assertEvalRun(t, evalAppliedCh, tick, keys...)
|
assertEvalRun(t, evalAppliedCh, tick, keys...)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -248,13 +249,13 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
alertRule1.IsPaused = false
|
alertRule1.IsPaused = false
|
||||||
alertRule2.IsPaused = false
|
alertRule2.IsPaused = false
|
||||||
|
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
require.Len(t, scheduled, 1)
|
require.Len(t, scheduled, 1)
|
||||||
require.Equal(t, alertRule1, scheduled[0].rule)
|
require.Equal(t, alertRule1, scheduled[0].rule)
|
||||||
require.Equal(t, tick, scheduled[0].scheduledAt)
|
require.Equal(t, tick, scheduled[0].scheduledAt)
|
||||||
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
assertEvalRun(t, evalAppliedCh, tick, alertRule1.GetKey())
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -275,11 +276,11 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
|
|
||||||
ruleStore.DeleteRule(alertRule1)
|
ruleStore.DeleteRule(alertRule1)
|
||||||
|
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
require.Empty(t, scheduled)
|
require.Empty(t, scheduled)
|
||||||
require.Len(t, stopped, 1)
|
require.Len(t, stopped, 1)
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
require.Contains(t, stopped, alertRule1.GetKey())
|
require.Contains(t, stopped, alertRule1.GetKey())
|
||||||
|
|
||||||
assertStopRun(t, stopAppliedCh, alertRule1.GetKey())
|
assertStopRun(t, stopAppliedCh, alertRule1.GetKey())
|
||||||
@ -300,31 +301,54 @@ func TestProcessTicks(t *testing.T) {
|
|||||||
t.Run("on 9th tick one alert rule should be evaluated", func(t *testing.T) {
|
t.Run("on 9th tick one alert rule should be evaluated", func(t *testing.T) {
|
||||||
tick = tick.Add(cfg.BaseInterval)
|
tick = tick.Add(cfg.BaseInterval)
|
||||||
|
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
require.Len(t, scheduled, 1)
|
require.Len(t, scheduled, 1)
|
||||||
require.Equal(t, alertRule2, scheduled[0].rule)
|
require.Equal(t, alertRule2, scheduled[0].rule)
|
||||||
require.Equal(t, tick, scheduled[0].scheduledAt)
|
require.Equal(t, tick, scheduled[0].scheduledAt)
|
||||||
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
assertEvalRun(t, evalAppliedCh, tick, alertRule2.GetKey())
|
assertEvalRun(t, evalAppliedCh, tick, alertRule2.GetKey())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("on 10th tick a new alert rule should be evaluated", func(t *testing.T) {
|
|
||||||
// create alert rule with one base interval
|
// create alert rule with one base interval
|
||||||
alertRule3 := models.AlertRuleGen(models.WithOrgID(mainOrgID), models.WithInterval(cfg.BaseInterval), models.WithTitle("rule-3"))()
|
alertRule3 := models.AlertRuleGen(models.WithOrgID(mainOrgID), models.WithInterval(cfg.BaseInterval), models.WithTitle("rule-3"))()
|
||||||
ruleStore.PutRule(ctx, alertRule3)
|
ruleStore.PutRule(ctx, alertRule3)
|
||||||
|
|
||||||
|
t.Run("on 10th tick a new alert rule should be evaluated", func(t *testing.T) {
|
||||||
tick = tick.Add(cfg.BaseInterval)
|
tick = tick.Add(cfg.BaseInterval)
|
||||||
|
|
||||||
scheduled, stopped := sched.processTick(ctx, dispatcherGroup, tick)
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
require.Len(t, scheduled, 1)
|
require.Len(t, scheduled, 1)
|
||||||
require.Equal(t, alertRule3, scheduled[0].rule)
|
require.Equal(t, alertRule3, scheduled[0].rule)
|
||||||
require.Equal(t, tick, scheduled[0].scheduledAt)
|
require.Equal(t, tick, scheduled[0].scheduledAt)
|
||||||
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
require.Emptyf(t, updated, "None rules are expected to be updated")
|
||||||
assertEvalRun(t, evalAppliedCh, tick, alertRule3.GetKey())
|
assertEvalRun(t, evalAppliedCh, tick, alertRule3.GetKey())
|
||||||
})
|
})
|
||||||
|
t.Run("on 11th tick rule2 should be updated", func(t *testing.T) {
|
||||||
|
newRule2 := models.CopyRule(alertRule2)
|
||||||
|
newRule2.Version++
|
||||||
|
expectedUpdated := models.AlertRuleKeyWithVersion{
|
||||||
|
Version: newRule2.Version,
|
||||||
|
AlertRuleKey: newRule2.GetKey(),
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleStore.PutRule(context.Background(), newRule2)
|
||||||
|
|
||||||
|
tick = tick.Add(cfg.BaseInterval)
|
||||||
|
scheduled, stopped, updated := sched.processTick(ctx, dispatcherGroup, tick)
|
||||||
|
|
||||||
|
require.Len(t, scheduled, 1)
|
||||||
|
require.Equal(t, alertRule3, scheduled[0].rule)
|
||||||
|
require.Equal(t, tick, scheduled[0].scheduledAt)
|
||||||
|
|
||||||
|
require.Emptyf(t, stopped, "None rules are expected to be stopped")
|
||||||
|
|
||||||
|
require.Len(t, updated, 1)
|
||||||
|
require.Equal(t, expectedUpdated, updated[0])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSchedule_ruleRoutine(t *testing.T) {
|
func TestSchedule_ruleRoutine(t *testing.T) {
|
||||||
@ -719,49 +743,14 @@ func TestSchedule_ruleRoutine(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSchedule_UpdateAlertRule(t *testing.T) {
|
func TestSchedule_deleteAlertRule(t *testing.T) {
|
||||||
t.Run("when rule exists", func(t *testing.T) {
|
|
||||||
t.Run("it should call Update", func(t *testing.T) {
|
|
||||||
sch := setupScheduler(t, nil, nil, nil, nil, nil)
|
|
||||||
key := models.GenerateRuleKey(rand.Int63())
|
|
||||||
info, _ := sch.registry.getOrCreateInfo(context.Background(), key)
|
|
||||||
version := rand.Int63()
|
|
||||||
go func() {
|
|
||||||
sch.UpdateAlertRule(key, version, false)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case v := <-info.updateCh:
|
|
||||||
require.Equal(t, ruleVersionAndPauseStatus{ruleVersion(version), false}, v)
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
t.Fatal("No message was received on update channel")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("should exit if rule is being stopped", func(t *testing.T) {
|
|
||||||
sch := setupScheduler(t, nil, nil, nil, nil, nil)
|
|
||||||
key := models.GenerateRuleKey(rand.Int63())
|
|
||||||
info, _ := sch.registry.getOrCreateInfo(context.Background(), key)
|
|
||||||
info.stop(nil)
|
|
||||||
sch.UpdateAlertRule(key, rand.Int63(), false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
t.Run("when rule does not exist", func(t *testing.T) {
|
|
||||||
t.Run("should exit", func(t *testing.T) {
|
|
||||||
sch := setupScheduler(t, nil, nil, nil, nil, nil)
|
|
||||||
key := models.GenerateRuleKey(rand.Int63())
|
|
||||||
sch.UpdateAlertRule(key, rand.Int63(), false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSchedule_DeleteAlertRule(t *testing.T) {
|
|
||||||
t.Run("when rule exists", func(t *testing.T) {
|
t.Run("when rule exists", func(t *testing.T) {
|
||||||
t.Run("it should stop evaluation loop and remove the controller from registry", func(t *testing.T) {
|
t.Run("it should stop evaluation loop and remove the controller from registry", func(t *testing.T) {
|
||||||
sch := setupScheduler(t, nil, nil, nil, nil, nil)
|
sch := setupScheduler(t, nil, nil, nil, nil, nil)
|
||||||
rule := models.AlertRuleGen()()
|
rule := models.AlertRuleGen()()
|
||||||
key := rule.GetKey()
|
key := rule.GetKey()
|
||||||
info, _ := sch.registry.getOrCreateInfo(context.Background(), key)
|
info, _ := sch.registry.getOrCreateInfo(context.Background(), key)
|
||||||
sch.DeleteAlertRule(key)
|
sch.deleteAlertRule(key)
|
||||||
require.ErrorIs(t, info.ctx.Err(), errRuleDeleted)
|
require.ErrorIs(t, info.ctx.Err(), errRuleDeleted)
|
||||||
require.False(t, sch.registry.exists(key))
|
require.False(t, sch.registry.exists(key))
|
||||||
})
|
})
|
||||||
@ -770,7 +759,7 @@ func TestSchedule_DeleteAlertRule(t *testing.T) {
|
|||||||
t.Run("should exit", func(t *testing.T) {
|
t.Run("should exit", func(t *testing.T) {
|
||||||
sch := setupScheduler(t, nil, nil, nil, nil, nil)
|
sch := setupScheduler(t, nil, nil, nil, nil, nil)
|
||||||
key := models.GenerateRuleKey(rand.Int63())
|
key := models.GenerateRuleKey(rand.Int63())
|
||||||
sch.DeleteAlertRule(key)
|
sch.deleteAlertRule(key)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user