The availability group collector also reads sys.dm_hadr_cluster_members (empty or unavailable without a WSFC), and SQLI-AG-003 (High) reports a node or witness that is not up, with how many members are still up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
138 lines
5.9 KiB
C#
138 lines
5.9 KiB
C#
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Core.Instances;
|
|
using SQLInsight.Rules.Availability;
|
|
using SQLInsight.Rules.Engine;
|
|
using static SQLInsight.Rules.Tests.TestInstances;
|
|
|
|
namespace SQLInsight.Rules.Tests;
|
|
|
|
public class AvailabilityRuleTests
|
|
{
|
|
private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context));
|
|
|
|
private static RuleContext Hadr(AvailabilityGroupSnapshot snapshot)
|
|
{
|
|
var info = new InstanceInfo { ServerName = "n1", Version = SqlServerVersion.Parse("16.0.4295.3"), Edition = "Enterprise", EngineEdition = EngineEdition.Enterprise, IsHadrEnabled = true };
|
|
var context = new RuleContext(info, InstanceCapabilities.From(info), Now);
|
|
context.Data.Set(snapshot);
|
|
return context;
|
|
}
|
|
|
|
[Fact]
|
|
public void A_cluster_node_or_witness_that_is_not_up_is_high()
|
|
{
|
|
var snapshot = new AvailabilityGroupSnapshot
|
|
{
|
|
CapturedAt = Now, Replicas = [], Databases = [],
|
|
ClusterMembers =
|
|
[
|
|
new ClusterMember("SQL01", "CLUSTER_NODE", 1, "UP", 1),
|
|
new ClusterMember("SQL02", "CLUSTER_NODE", 0, "DOWN", 1),
|
|
new ClusterMember("Cloud Witness", "CLOUD_WITNESS", 1, "UP", 1),
|
|
],
|
|
};
|
|
|
|
var finding = Assert.Single(Run(new ClusterMemberDownRule(), Hadr(snapshot)).Findings);
|
|
|
|
Assert.Equal(Severity.High, finding.Severity);
|
|
Assert.Equal("cluster member: SQL02", finding.Subject);
|
|
Assert.Equal("2 af 3", finding.Evidence.Single(e => e.Name == "Medlemmer oppe").Value);
|
|
}
|
|
|
|
private static DatabaseReplicaState Db(string replica, bool primary, DateTime? commit = null, long redoKb = 0, long redoRate = 1000, bool suspended = false, string health = "HEALTHY") => new()
|
|
{
|
|
Group = "ag1", Database = "Sales", Replica = replica, IsPrimaryReplica = primary, LastCommitTime = commit,
|
|
RedoQueueKb = redoKb, RedoRateKbPerSecond = redoRate, IsSuspended = suspended, SuspendReason = suspended ? "SUSPEND_FROM_USER" : null,
|
|
SynchronizationHealth = health, SynchronizationState = "SYNCHRONIZED", LogSendQueueKb = 0,
|
|
};
|
|
|
|
[Fact]
|
|
public void Not_measurable_without_hadr()
|
|
{
|
|
Assert.Equal(RuleOutcome.NotMeasurable, Run(new AvailabilityGroupHealthRule(), Context("16.0.4295.3")).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Healthy_group_passes()
|
|
{
|
|
var snapshot = new AvailabilityGroupSnapshot
|
|
{
|
|
CapturedAt = Now,
|
|
Replicas = [new ReplicaState { Group = "ag1", Replica = "n1", Role = "PRIMARY", ConnectedState = "CONNECTED", SynchronizationHealth = "HEALTHY" }],
|
|
Databases = [Db("n1", true, Now.UtcDateTime), Db("n2", false, Now.UtcDateTime)],
|
|
};
|
|
|
|
Assert.Equal(RuleOutcome.Passed, Run(new AvailabilityGroupHealthRule(), Hadr(snapshot)).Outcome);
|
|
Assert.Equal(RuleOutcome.Passed, Run(new AvailabilityGroupLagRule(), Hadr(snapshot)).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Disconnected_replica_and_suspended_database_are_high()
|
|
{
|
|
var snapshot = new AvailabilityGroupSnapshot
|
|
{
|
|
CapturedAt = Now,
|
|
Replicas = [new ReplicaState { Group = "ag1", Replica = "n2", Role = "SECONDARY", ConnectedState = "DISCONNECTED", SynchronizationHealth = "NOT_HEALTHY" }],
|
|
Databases = [Db("n2", false, suspended: true, health: "NOT_HEALTHY")],
|
|
};
|
|
|
|
var findings = Run(new AvailabilityGroupHealthRule(), Hadr(snapshot)).Findings;
|
|
|
|
Assert.Equal(2, findings.Count);
|
|
Assert.All(findings, f => Assert.Equal(Severity.High, f.Severity));
|
|
Assert.Contains(findings, f => f.FixScript == "ALTER DATABASE [Sales] SET HADR RESUME;");
|
|
}
|
|
|
|
[Fact]
|
|
public void Lag_reports_data_loss_and_redo_time()
|
|
{
|
|
var snapshot = new AvailabilityGroupSnapshot
|
|
{
|
|
CapturedAt = Now,
|
|
Replicas = [],
|
|
Databases = [Db("n1", true, Now.UtcDateTime), Db("n2", false, Now.UtcDateTime.AddMinutes(-5), redoKb: 2_000_000, redoRate: 1000)],
|
|
};
|
|
|
|
var finding = Assert.Single(Run(new AvailabilityGroupLagRule(), Hadr(snapshot)).Findings);
|
|
|
|
Assert.Contains("300 s datatab", finding.Title, StringComparison.Ordinal);
|
|
Assert.Contains("redo ca. 33 min", finding.Title, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static AgentJobSnapshot Jobs(bool running, params AgentJob[] jobs) => new() { CapturedAt = Now, IsAgentRunning = running, Jobs = jobs };
|
|
|
|
[Fact]
|
|
public void Failed_backup_job_is_high_with_step_message()
|
|
{
|
|
var job = new AgentJob { Name = "DatabaseBackup - USER_DATABASES - FULL", IsEnabled = true, LastRunStatus = 0, LastFailedStepName = "Backup", LastFailedStepMessage = "Cannot open backup device 'X:\\bak'" };
|
|
|
|
var finding = Assert.Single(Run(new FailedJobRule(), With(Jobs(true, job))).Findings);
|
|
|
|
Assert.Equal(Severity.High, finding.Severity);
|
|
Assert.Contains(finding.Evidence, e => e.Value.Contains("Cannot open backup device", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void Disabled_or_succeeding_jobs_pass()
|
|
{
|
|
var jobs = Jobs(true,
|
|
new AgentJob { Name = "Old", IsEnabled = false, LastRunStatus = 0 },
|
|
new AgentJob { Name = "Fine", IsEnabled = true, LastRunStatus = 1 },
|
|
new AgentJob { Name = "Never run", IsEnabled = true });
|
|
|
|
Assert.Equal(RuleOutcome.Passed, Run(new FailedJobRule(), With(jobs)).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stopped_agent_is_high_when_jobs_exist_but_not_on_express()
|
|
{
|
|
var jobs = Jobs(false, new AgentJob { Name = "Backup", IsEnabled = true });
|
|
|
|
Assert.Equal(Severity.High, Assert.Single(Run(new AgentStoppedRule(), With(jobs)).Findings).Severity);
|
|
|
|
var express = Context("16.0.4295.3", edition: EngineEdition.Express);
|
|
express.Data.Set(jobs);
|
|
Assert.Equal(RuleOutcome.Passed, Run(new AgentStoppedRule(), express).Outcome);
|
|
}
|
|
}
|