Skip to content

Commit c14fcea

Browse files
authored
Actor reminder deserialization bugfix (#1483)
Signed-off-by: Whit Waldo <[email protected]>
1 parent bb47132 commit c14fcea

File tree

7 files changed

+444
-111
lines changed

7 files changed

+444
-111
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2025 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
using System;
15+
using System.Globalization;
16+
using System.Linq;
17+
using System.Text.RegularExpressions;
18+
19+
namespace Dapr.Actors.Extensions;
20+
21+
internal static class DurationExtensions
22+
{
23+
/// <summary>
24+
/// Used to parse the duration string accompanying an @every expression.
25+
/// </summary>
26+
private static readonly Regex durationRegex = new(@"(?<value>\d+(\.\d+)?)(?<unit>ns|us|µs|ms|s|m|h)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
27+
/// <summary>
28+
/// A regular expression used to evaluate whether a given prefix period embodies an @every statement.
29+
/// </summary>
30+
private static readonly Regex isEveryExpression = new(@"^@every (\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$");
31+
/// <summary>
32+
/// The various acceptable duration values for a period expression.
33+
/// </summary>
34+
private static readonly string[] acceptablePeriodValues =
35+
{
36+
"yearly", "monthly", "weekly", "daily", "midnight", "hourly"
37+
};
38+
39+
private const string YearlyPrefixPeriod = "@yearly";
40+
private const string MonthlyPrefixPeriod = "@monthly";
41+
private const string WeeklyPrefixPeriod = "@weekly";
42+
private const string DailyPrefixPeriod = "@daily";
43+
private const string MidnightPrefixPeriod = "@midnight";
44+
private const string HourlyPrefixPeriod = "@hourly";
45+
private const string EveryPrefixPeriod = "@every";
46+
47+
/// <summary>
48+
/// Indicates that the schedule represents a prefixed period expression.
49+
/// </summary>
50+
/// <param name="expression"></param>
51+
/// <returns></returns>
52+
public static bool IsDurationExpression(this string expression) => expression.StartsWith('@') &&
53+
(isEveryExpression.IsMatch(expression) ||
54+
expression.EndsWithAny(acceptablePeriodValues, StringComparison.InvariantCulture));
55+
56+
/// <summary>
57+
/// Creates a TimeSpan value from the prefixed period value.
58+
/// </summary>
59+
/// <param name="period">The prefixed period value to parse.</param>
60+
/// <returns>A TimeSpan value matching the provided period.</returns>
61+
public static TimeSpan FromPrefixedPeriod(this string period)
62+
{
63+
if (period.StartsWith(YearlyPrefixPeriod))
64+
{
65+
var dateTime = DateTime.UtcNow;
66+
return dateTime.AddYears(1) - dateTime;
67+
}
68+
69+
if (period.StartsWith(MonthlyPrefixPeriod))
70+
{
71+
var dateTime = DateTime.UtcNow;
72+
return dateTime.AddMonths(1) - dateTime;
73+
}
74+
75+
if (period.StartsWith(MidnightPrefixPeriod))
76+
{
77+
return new TimeSpan();
78+
}
79+
80+
if (period.StartsWith(WeeklyPrefixPeriod))
81+
{
82+
return TimeSpan.FromDays(7);
83+
}
84+
85+
if (period.StartsWith(DailyPrefixPeriod) || period.StartsWith(MidnightPrefixPeriod))
86+
{
87+
return TimeSpan.FromDays(1);
88+
}
89+
90+
if (period.StartsWith(HourlyPrefixPeriod))
91+
{
92+
return TimeSpan.FromHours(1);
93+
}
94+
95+
if (period.StartsWith(EveryPrefixPeriod))
96+
{
97+
//A sequence of decimal numbers each with an optional fraction and unit suffix
98+
//Valid time units are: 'ns', 'us'/'µs', 'ms', 's', 'm', and 'h'
99+
double totalMilliseconds = 0;
100+
var durationString = period.Split(' ').Last().Trim();
101+
102+
foreach (Match match in durationRegex.Matches(durationString))
103+
{
104+
var value = double.Parse(match.Groups["value"].Value, CultureInfo.InvariantCulture);
105+
var unit = match.Groups["unit"].Value.ToLower();
106+
107+
totalMilliseconds += unit switch
108+
{
109+
"ns" => value / 1_000_000,
110+
"us" or "µs" => value / 1_000,
111+
"ms" => value,
112+
"s" => value * 1_000,
113+
"m" => value * 1_000 * 60,
114+
"h" => value * 1_000 * 60 * 60,
115+
_ => throw new ArgumentException($"Unknown duration unit: {unit}")
116+
};
117+
}
118+
119+
return TimeSpan.FromMilliseconds(totalMilliseconds);
120+
}
121+
122+
throw new ArgumentException($"Unknown prefix period expression: {period}");
123+
}
124+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// ------------------------------------------------------------------------
2+
// Copyright 2025 The Dapr Authors
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ------------------------------------------------------------------------
13+
14+
using System;
15+
using System.Collections.Generic;
16+
using System.Linq;
17+
using System.Text.RegularExpressions;
18+
19+
namespace Dapr.Actors.Extensions;
20+
21+
internal static class StringExtensions
22+
{
23+
/// <summary>
24+
/// Extension method that validates a string against a list of possible matches.
25+
/// </summary>
26+
/// <param name="value">The string value to evaluate.</param>
27+
/// <param name="possibleValues">The possible values to look for a match within.</param>
28+
/// <param name="comparisonType">The type of string comparison to perform.</param>
29+
/// <returns>True if the value ends with any of the possible values; otherwise false.</returns>
30+
public static bool EndsWithAny(this string value, IReadOnlyList<string> possibleValues, StringComparison comparisonType )
31+
=> possibleValues.Any(val => value.EndsWith(val, comparisonType));
32+
}

0 commit comments

Comments
 (0)