Changed configuration of first run time for health check notifier from a time string to a cron expression.

This commit is contained in:
Andy Butland
2020-10-31 22:49:12 +01:00
parent bdb8f34da3
commit a0ce44c9fc
10 changed files with 42 additions and 112 deletions

View File

@@ -42,11 +42,11 @@ namespace Umbraco.Core.Configuration.Models.Validation
return true;
}
public bool ValidateOptionalTime(string configPath, string value, out string message)
public bool ValidateOptionalCronTab(string configPath, string value, out string message)
{
if (!string.IsNullOrEmpty(value) && !value.IsValidTimeSpan())
if (!string.IsNullOrEmpty(value) && !value.IsValidCronTab())
{
message = $"Configuration entry {configPath} contains an invalid time value.";
message = $"Configuration entry {configPath} contains an invalid cron expression.";
return false;
}

View File

@@ -16,7 +16,7 @@ namespace Umbraco.Core.Configuration.Models.Validation
private bool ValidateNotificationFirstRunTime(string value, out string message)
{
return ValidateOptionalTime($"{Constants.Configuration.ConfigHealthChecks}:{nameof(HealthChecksSettings.Notification)}:{nameof(HealthChecksSettings.Notification.FirstRunTime)}", value, out message);
return ValidateOptionalCronTab($"{Constants.Configuration.ConfigHealthChecks}:{nameof(HealthChecksSettings.Notification)}:{nameof(HealthChecksSettings.Notification.FirstRunTime)}", value, out message);
}
}
}

View File

@@ -43,42 +43,5 @@ namespace Umbraco.Core
Minute,
Second
}
/// <summary>
/// Calculates the number of minutes from a date time, on a rolling daily basis (so if
/// date time is before the time, calculate onto next day).
/// </summary>
/// <param name="fromDateTime">Date to start from</param>
/// <param name="scheduledTime">Time to compare against (in Hmm form, e.g. 330, 2200)</param>
/// <returns></returns>
public static int PeriodicMinutesFrom(this DateTime fromDateTime, string scheduledTime)
{
// Ensure time provided is 4 digits long
if (scheduledTime.Length == 3)
{
scheduledTime = "0" + scheduledTime;
}
var scheduledHour = int.Parse(scheduledTime.Substring(0, 2));
var scheduledMinute = int.Parse(scheduledTime.Substring(2));
DateTime scheduledDateTime;
if (IsScheduledInRemainingDay(fromDateTime, scheduledHour, scheduledMinute))
{
scheduledDateTime = new DateTime(fromDateTime.Year, fromDateTime.Month, fromDateTime.Day, scheduledHour, scheduledMinute, 0);
}
else
{
var nextDay = fromDateTime.AddDays(1);
scheduledDateTime = new DateTime(nextDay.Year, nextDay.Month, nextDay.Day, scheduledHour, scheduledMinute, 0);
}
return (int)(scheduledDateTime - fromDateTime).TotalMinutes;
}
private static bool IsScheduledInRemainingDay(DateTime fromDateTime, int scheduledHour, int scheduledMinute)
{
return scheduledHour > fromDateTime.Hour || (scheduledHour == fromDateTime.Hour && scheduledMinute >= fromDateTime.Minute);
}
}
}

View File

@@ -1480,18 +1480,23 @@ namespace Umbraco.Core
}
/// <summary>
/// Validates a string matches a time stamp.
/// Validates a string matches a cron tab (for length only).
/// </summary>
/// <param name="input">String with timespan representation (in standard timespan format: https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings)</param>
/// <returns></returns>
public static bool IsValidTimeSpan(this string input)
/// <param name="input">String with timespan representation (in cron tab format: https://github.com/atifaziz/NCrontab/wiki/Crontab-Expression)</param>
/// <returns>True if string matches a valid cron expression, false if not.</returns>
/// <remarks>
/// Considering an expression as valid if it's supported by https://github.com/atifaziz/NCrontab/wiki/Crontab-Expression,
/// so only 5 or 6 values are expected.
/// </remarks>
public static bool IsValidCronTab(this string input)
{
if (string.IsNullOrEmpty(input))
{
return false;
}
return TimeSpan.TryParse(input, out var _);
var parts = input.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
return parts.Length == 5 || parts.Length == 6;
}
}
}

View File

@@ -1,6 +1,8 @@
using System;
using NCrontab;
using Umbraco.Core.Configuration.Models;
namespace Umbraco.Core.Configuration.Models.Extensions
namespace Umbraco.Infrastructure.Configuration.Extensions
{
public static class HealthCheckSettingsExtensions
{
@@ -14,9 +16,11 @@ namespace Umbraco.Core.Configuration.Models.Extensions
}
else
{
// Otherwise start at scheduled time.
var delay = TimeSpan.FromMinutes(now.PeriodicMinutesFrom(firstRunTime));
return (delay < defaultDelay)
// Otherwise start at scheduled time according to cron expression, unless within the default delay period.
var firstRunTimeCronExpression = CrontabSchedule.Parse(firstRunTime);
var firstRunOccurance = firstRunTimeCronExpression.GetNextOccurrence(now);
var delay = firstRunOccurance - now;
return delay < defaultDelay
? defaultDelay
: delay;
}

View File

@@ -4,11 +4,11 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Configuration.Models.Extensions;
using Umbraco.Core.HealthCheck;
using Umbraco.Core.Logging;
using Umbraco.Core.Scoping;
using Umbraco.Core.Sync;
using Umbraco.Infrastructure.Configuration.Extensions;
using Umbraco.Infrastructure.HealthCheck;
using Umbraco.Web.HealthCheck;

View File

@@ -20,6 +20,7 @@
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="3.1.8" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" />
<PackageReference Include="MiniProfiler.Shared" Version="4.2.1" />
<PackageReference Include="ncrontab" Version="3.3.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NPoco" Version="4.0.2" />
<PackageReference Include="Serilog" Version="2.10.0" />

View File

@@ -1,26 +1,30 @@
using System;
using NUnit.Framework;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Configuration.Models.Extensions;
using Umbraco.Infrastructure.Configuration.Extensions;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Configuration.Models.Extensions
{
[TestFixture]
public class HealthCheckSettingsExtensionsTests
{
[Test]
public void Returns_Notification_Delay_From_Provided_Time()
[TestCase("30 12 * * *", 30)]
[TestCase("15 18 * * *", 60 * 6 + 15)]
[TestCase("0 3 * * *", 60 * 15)]
[TestCase("0 3 2 * *", 24 * 60 * 1 + 60 * 15)]
[TestCase("0 6 * * 3", 24 * 60 * 3 + 60 * 18)]
public void Returns_Notification_Delay_From_Provided_Time(string firstRunTimeCronExpression, int expectedDelayInMinutes)
{
var settings = new HealthChecksSettings
{
Notification = new HealthChecksNotificationSettings
{
FirstRunTime = "1230",
FirstRunTime = firstRunTimeCronExpression,
}
};
var now = DateTime.Now.Date.AddHours(12);
var now = new DateTime(2020, 10, 31, 12, 0, 0);
var result = settings.GetNotificationDelay(now, TimeSpan.Zero);
Assert.AreEqual(30, result.Minutes);
Assert.AreEqual(expectedDelayInMinutes, result.TotalMinutes);
}
[Test]
@@ -30,12 +34,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Configuration.Models.Extensions
{
Notification = new HealthChecksNotificationSettings
{
FirstRunTime = "1230",
FirstRunTime = "30 12 * * *",
}
};
var now = DateTime.Now.Date.AddHours(12).AddMinutes(25);
var now = new DateTime(2020, 10, 31, 12, 25, 0);
var result = settings.GetNotificationDelay(now, TimeSpan.FromMinutes(10));
Assert.AreEqual(10, result.Minutes);
Assert.AreEqual(10, result.TotalMinutes);
}
}
}

View File

@@ -1,46 +0,0 @@
using System;
using NUnit.Framework;
using Umbraco.Core;
namespace Umbraco.Tests.UnitTests.Umbraco.Core
{
[TestFixture]
public class DateTimeExtensionsTests
{
[Test]
public void PeriodicMinutesFrom_PostTime_CalculatesMinutesBetween()
{
var nowDateTime = new DateTime(2017, 1, 1, 10, 30, 0);
var scheduledTime = "1145";
var minutesBetween = nowDateTime.PeriodicMinutesFrom(scheduledTime);
Assert.AreEqual(75, minutesBetween);
}
[Test]
public void PeriodicMinutesFrom_PriorTime_CalculatesMinutesBetween()
{
var nowDateTime = new DateTime(2017, 1, 1, 10, 30, 0);
var scheduledTime = "900";
var minutesBetween = nowDateTime.PeriodicMinutesFrom(scheduledTime);
Assert.AreEqual(1350, minutesBetween);
}
[Test]
public void PeriodicMinutesFrom_PriorTime_WithLeadingZero_CalculatesMinutesBetween()
{
var nowDateTime = new DateTime(2017, 1, 1, 10, 30, 0);
var scheduledTime = "0900";
var minutesBetween = nowDateTime.PeriodicMinutesFrom(scheduledTime);
Assert.AreEqual(1350, minutesBetween);
}
[Test]
public void PeriodicMinutesFrom_SameTime_CalculatesMinutesBetween()
{
var nowDateTime = new DateTime(2017, 1, 1, 10, 30, 0);
var scheduledTime = "1030";
var minutesBetween = nowDateTime.PeriodicMinutesFrom(scheduledTime);
Assert.AreEqual(0, minutesBetween);
}
}
}

View File

@@ -302,13 +302,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.ShortStringHelper
}
[TestCase("", false)]
[TestCase("12:34", true)]
[TestCase("1:14:23", true)]
[TestCase("25:03", false)]
[TestCase("18:61", false)]
public void IsValidTimeSpan(string input, bool expected)
[TestCase("* * * * 1", true)]
[TestCase("* * * * * 1", true)]
[TestCase("* * * 1", false)]
public void IsValidCronTab(string input, bool expected)
{
var result = input.IsValidTimeSpan();
var result = input.IsValidCronTab();
Assert.AreEqual(expected, result);
}
}