Skip to content

Commit 6e77f12

Browse files
authored
Add support for repetition and ISO 8601 for reminders (#974)
Signed-off-by: Yash Nisar <yashnisar@microsoft.com> Signed-off-by: Yash Nisar <yashnisar@microsoft.com>
1 parent 1605ecd commit 6e77f12

File tree

18 files changed

+691
-19
lines changed

18 files changed

+691
-19
lines changed

examples/Actor/ActorClient/Program.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ public static async Task Main(string[] args)
100100
await proxy.UnregisterTimer();
101101
Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted.");
102102
await proxy.UnregisterReminder();
103+
104+
Console.WriteLine("Registering reminder with repetitions - The reminder will repeat 3 times.");
105+
await proxy.RegisterReminderWithRepetitions(3);
106+
Console.WriteLine("Waiting so the reminder can be triggered");
107+
await Task.Delay(5000);
108+
Console.WriteLine("Registering reminder with ttl and repetitions, i.e. reminder stops when either condition is met - The reminder will repeat 2 times.");
109+
await proxy.RegisterReminderWithTtlAndRepetitions(TimeSpan.FromSeconds(5), 2);
110+
Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted.");
111+
await proxy.UnregisterReminder();
103112

104113
Console.WriteLine("Registering reminder and Timer with TTL - The reminder will self delete after 10 seconds.");
105114
await proxy.RegisterReminderWithTtl(TimeSpan.FromSeconds(10));

examples/Actor/DemoActor/DemoActor.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ public async Task RegisterReminderWithTtl(TimeSpan ttl)
7474
{
7575
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), ttl);
7676
}
77+
78+
public async Task RegisterReminderWithRepetitions(int repetitions)
79+
{
80+
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions);
81+
}
82+
83+
public async Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions)
84+
{
85+
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions, ttl);
86+
}
7787

7888
public Task UnregisterReminder()
7989
{

examples/Actor/IDemoActor/IDemoActor.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ public interface IDemoActor : IActor
7878
/// <param name="ttl">Optional TimeSpan that dictates when the timer expires.</param>
7979
/// <returns>A task that represents the asynchronous save operation.</returns>
8080
Task RegisterTimerWithTtl(TimeSpan ttl);
81+
82+
/// <summary>
83+
/// Registers a reminder with repetitions.
84+
/// </summary>
85+
/// <param name="repetitions">The number of repetitions for which the reminder should be invoked.</param>
86+
/// <returns>A task that represents the asynchronous save operation.</returns>
87+
Task RegisterReminderWithRepetitions(int repetitions);
88+
89+
/// <summary>
90+
/// Registers a reminder with ttl and repetitions.
91+
/// </summary>
92+
/// <param name="ttl">TimeSpan that dictates when the timer expires.</param>
93+
/// <param name="repetitions">The number of repetitions for which the reminder should be invoked.</param>
94+
/// <returns>A task that represents the asynchronous save operation.</returns>
95+
Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions);
8196

8297
/// <summary>
8398
/// Unregisters the registered timer.

src/Dapr.Actors/Resources/SR.Designer.cs

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Dapr.Actors/Resources/SR.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,7 @@
223223
<data name="TimerArgumentOutOfRange" xml:space="preserve">
224224
<value>TimeSpan TotalMilliseconds specified value must be between {0} and {1} </value>
225225
</data>
226+
<data name="RepetitionsArgumentOutOfRange" xml:space="preserve">
227+
<value>The repetitions {0} specified must be a valid positive integer.</value>
228+
</data>
226229
</root>

src/Dapr.Actors/Runtime/Actor.cs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ namespace Dapr.Actors.Runtime
1515
{
1616
using System;
1717
using System.Reflection;
18-
using System.Text.Json;
1918
using System.Threading.Tasks;
2019
using Dapr.Actors.Client;
2120
using Microsoft.Extensions.Logging;
@@ -264,6 +263,83 @@ protected async Task<IActorReminder> RegisterReminderAsync(
264263
Ttl = ttl
265264
});
266265
}
266+
267+
/// <summary>
268+
/// Registers a reminder with the actor.
269+
/// </summary>
270+
/// <param name="reminderName">The name of the reminder to register. The name must be unique per actor.</param>
271+
/// <param name="state">User state passed to the reminder invocation.</param>
272+
/// <param name="dueTime">The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration.</param>
273+
/// <param name="period">
274+
/// The time interval between reminder invocations after the first invocation.
275+
/// </param>
276+
/// <param name="repetitions">The number of repetitions for which the reminder should be invoked.</param>
277+
/// <param name="ttl">The time interval after which the reminder will expire.</param>
278+
/// <returns>
279+
/// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync.
280+
/// </returns>
281+
/// <remarks>
282+
/// <para>
283+
/// The class deriving from <see cref="Dapr.Actors.Runtime.Actor" /> must implement <see cref="Dapr.Actors.Runtime.IRemindable" /> to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by <paramref name="reminderName" />. Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks.
284+
/// </para>
285+
/// </remarks>
286+
protected async Task<IActorReminder> RegisterReminderAsync(
287+
string reminderName,
288+
byte[] state,
289+
TimeSpan dueTime,
290+
TimeSpan period,
291+
int repetitions,
292+
TimeSpan ttl)
293+
{
294+
return await RegisterReminderAsync(new ActorReminderOptions
295+
{
296+
ActorTypeName = this.actorTypeName,
297+
Id = this.Id,
298+
ReminderName = reminderName,
299+
State = state,
300+
DueTime = dueTime,
301+
Period = period,
302+
Repetitions = repetitions,
303+
Ttl = ttl
304+
});
305+
}
306+
307+
/// <summary>
308+
/// Registers a reminder with the actor.
309+
/// </summary>
310+
/// <param name="reminderName">The name of the reminder to register. The name must be unique per actor.</param>
311+
/// <param name="state">User state passed to the reminder invocation.</param>
312+
/// <param name="dueTime">The amount of time to delay before invoking the reminder for the first time. Specify negative one (-1) milliseconds to disable invocation. Specify zero (0) to invoke the reminder immediately after registration.</param>
313+
/// <param name="period">
314+
/// The time interval between reminder invocations after the first invocation.
315+
/// </param>
316+
/// <param name="repetitions">The number of repetitions for which the reminder should be invoked.</param>
317+
/// <returns>
318+
/// A task that represents the asynchronous registration operation. The result of the task provides information about the registered reminder and is used to unregister the reminder using UnregisterReminderAsync.
319+
/// </returns>
320+
/// <remarks>
321+
/// <para>
322+
/// The class deriving from <see cref="Dapr.Actors.Runtime.Actor" /> must implement <see cref="Dapr.Actors.Runtime.IRemindable" /> to consume reminder invocations. Multiple reminders can be registered at any time, uniquely identified by <paramref name="reminderName" />. Existing reminders can also be updated by calling this method again. Reminder invocations are synchronized both with other reminders and other actor method callbacks.
323+
/// </para>
324+
/// </remarks>
325+
protected async Task<IActorReminder> RegisterReminderAsync(
326+
string reminderName,
327+
byte[] state,
328+
TimeSpan dueTime,
329+
TimeSpan period,
330+
int repetitions)
331+
{
332+
return await RegisterReminderAsync(new ActorReminderOptions
333+
{
334+
ActorTypeName = this.actorTypeName,
335+
Id = this.Id,
336+
ReminderName = reminderName,
337+
State = state,
338+
DueTime = dueTime,
339+
Period = period,
340+
Repetitions = repetitions
341+
});
342+
}
267343

268344
/// <summary>
269345
/// Registers a reminder with the actor.

src/Dapr.Actors/Runtime/ActorReminder.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,71 @@ public ActorReminder(
8585
{
8686
}
8787

88+
/// <summary>
89+
/// Initializes a new instance of <see cref="ActorReminder" />.
90+
/// </summary>
91+
/// <param name="actorType">The actor type.</param>
92+
/// <param name="actorId">The actor id.</param>
93+
/// <param name="name">The reminder name.</param>
94+
/// <param name="state">The state associated with the reminder.</param>
95+
/// <param name="dueTime">The reminder due time.</param>
96+
/// <param name="period">The reminder period.</param>
97+
/// <param name="repetitions">The number of times reminder should be invoked.</param>
98+
/// <param name="ttl">The reminder ttl.</param>
99+
public ActorReminder(
100+
string actorType,
101+
ActorId actorId,
102+
string name,
103+
byte[] state,
104+
TimeSpan dueTime,
105+
TimeSpan period,
106+
int? repetitions,
107+
TimeSpan? ttl)
108+
: this(new ActorReminderOptions
109+
{
110+
ActorTypeName = actorType,
111+
Id = actorId,
112+
ReminderName = name,
113+
State = state,
114+
DueTime = dueTime,
115+
Period = period,
116+
Repetitions = repetitions,
117+
Ttl = ttl
118+
})
119+
{
120+
}
121+
122+
/// <summary>
123+
/// Initializes a new instance of <see cref="ActorReminder" />.
124+
/// </summary>
125+
/// <param name="actorType">The actor type.</param>
126+
/// <param name="actorId">The actor id.</param>
127+
/// <param name="name">The reminder name.</param>
128+
/// <param name="state">The state associated with the reminder.</param>
129+
/// <param name="dueTime">The reminder due time.</param>
130+
/// <param name="period">The reminder period.</param>
131+
/// <param name="repetitions">The number of times reminder should be invoked.</param>
132+
public ActorReminder(
133+
string actorType,
134+
ActorId actorId,
135+
string name,
136+
byte[] state,
137+
TimeSpan dueTime,
138+
TimeSpan period,
139+
int? repetitions)
140+
: this(new ActorReminderOptions
141+
{
142+
ActorTypeName = actorType,
143+
Id = actorId,
144+
ReminderName = name,
145+
State = state,
146+
DueTime = dueTime,
147+
Period = period,
148+
Repetitions = repetitions
149+
})
150+
{
151+
}
152+
88153
/// <summary>
89154
/// Initializes a new instance of <see cref="ActorReminder" />.
90155
/// </summary>
@@ -118,11 +183,20 @@ internal ActorReminder(ActorReminderOptions options)
118183
options.DueTime,
119184
TimeSpan.MaxValue.TotalMilliseconds));
120185
}
186+
187+
if (options.Repetitions != null && options.Repetitions <= 0)
188+
{
189+
throw new ArgumentOutOfRangeException(nameof(options.Repetitions), string.Format(
190+
CultureInfo.CurrentCulture,
191+
SR.RepetitionsArgumentOutOfRange,
192+
options.Repetitions));
193+
}
121194

122195
this.State = options.State;
123196
this.DueTime = options.DueTime;
124197
this.Period = options.Period;
125198
this.Ttl = options.Ttl;
199+
this.Repetitions = options.Repetitions;
126200
}
127201

128202
/// <summary>
@@ -144,5 +218,10 @@ internal ActorReminder(ActorReminderOptions options)
144218
/// The optional <see cref="TimeSpan"/> that states when the reminder will expire.
145219
/// </summary>
146220
public TimeSpan? Ttl { get; }
221+
222+
/// <summary>
223+
/// The optional property that gets the number of invocations of the reminder left.
224+
/// </summary>
225+
public int? Repetitions { get; }
147226
}
148227
}

src/Dapr.Actors/Runtime/ActorReminderOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,10 @@ internal class ActorReminderOptions
4040
/// An optional <see cref="TimeSpan"/> that determines when the reminder will expire.
4141
/// </summary>
4242
public TimeSpan? Ttl { get; set; }
43+
44+
/// <summary>
45+
/// The number of repetitions for which the reminder should be invoked.
46+
/// </summary>
47+
public int? Repetitions { get; set; }
4348
}
4449
}

src/Dapr.Actors/Runtime/ConverterUtils.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
namespace Dapr.Actors.Runtime
1515
{
1616
using System;
17+
using System.Text;
18+
using System.Text.RegularExpressions;
1719

1820
internal class ConverterUtils
1921
{
22+
private static Regex regex = new Regex("^(R(?<repetition>\\d+)/)?P((?<year>\\d+)Y)?((?<month>\\d+)M)?((?<week>\\d+)W)?((?<day>\\d+)D)?(T((?<hour>\\d+)H)?((?<minute>\\d+)M)?((?<second>\\d+)S)?)?$", RegexOptions.Compiled);
2023
public static TimeSpan ConvertTimeSpanFromDaprFormat(string valueString)
2124
{
2225
if (string.IsNullOrEmpty(valueString))
@@ -68,5 +71,78 @@ public static string ConvertTimeSpanValueInDaprFormat(TimeSpan? value)
6871

6972
return stringValue;
7073
}
74+
75+
public static string ConvertTimeSpanValueInISO8601Format(TimeSpan value, int? repetitions)
76+
{
77+
StringBuilder builder = new StringBuilder();
78+
79+
if (repetitions == null)
80+
{
81+
return ConvertTimeSpanValueInDaprFormat(value);
82+
}
83+
84+
if (value.Milliseconds > 0)
85+
{
86+
throw new ArgumentException("The TimeSpan value, combined with repetition cannot be in milliseconds.", nameof(value));
87+
}
88+
89+
builder.AppendFormat("R{0}/P", repetitions);
90+
91+
if(value.Days > 0)
92+
{
93+
builder.AppendFormat("{0}D", value.Days);
94+
}
95+
96+
builder.Append("T");
97+
98+
if(value.Hours > 0)
99+
{
100+
builder.AppendFormat("{0}H", value.Hours);
101+
}
102+
103+
if(value.Minutes > 0)
104+
{
105+
builder.AppendFormat("{0}M", value.Minutes);
106+
}
107+
108+
if(value.Seconds > 0)
109+
{
110+
builder.AppendFormat("{0}S", value.Seconds);
111+
}
112+
return builder.ToString();
113+
}
114+
115+
public static (TimeSpan, int?) ConvertTimeSpanValueFromISO8601Format(string valueString)
116+
{
117+
// ISO 8601 format can be Rn/PaYbMcHTdHeMfS or PaYbMcHTdHeMfS so if it does
118+
// not start with R or P then assuming it to default Dapr format without repetition
119+
if (!(valueString.StartsWith('R') || valueString.StartsWith('P')))
120+
{
121+
return (ConvertTimeSpanFromDaprFormat(valueString), -1);
122+
}
123+
124+
var matches = regex.Match(valueString);
125+
126+
var repetition = matches.Groups["repetition"].Success ? int.Parse(matches.Groups["repetition"].Value) : (int?)null;
127+
128+
var days = 0;
129+
var year = matches.Groups["year"].Success ? int.Parse(matches.Groups["year"].Value) : 0;
130+
days = year * 365;
131+
132+
var month = matches.Groups["month"].Success ? int.Parse(matches.Groups["month"].Value) : 0;
133+
days += month * 30;
134+
135+
var week = matches.Groups["week"].Success ? int.Parse(matches.Groups["week"].Value) : 0;
136+
days += week * 7;
137+
138+
var day = matches.Groups["day"].Success ? int.Parse(matches.Groups["day"].Value) : 0;
139+
days += day;
140+
141+
var hour = matches.Groups["hour"].Success ? int.Parse(matches.Groups["hour"].Value) : 0;
142+
var minute = matches.Groups["minute"].Success ? int.Parse(matches.Groups["minute"].Value) : 0;
143+
var second = matches.Groups["second"].Success ? int.Parse(matches.Groups["second"].Value) : 0;
144+
145+
return (new TimeSpan(days, hour, minute, second), repetition);
146+
}
71147
}
72148
}

src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public override async Task UnregisterTimerAsync(ActorTimerToken timer)
7373

7474
private async ValueTask<string> SerializeReminderAsync(ActorReminder reminder)
7575
{
76-
var info = new ReminderInfo(reminder.State, reminder.DueTime, reminder.Period, reminder.Ttl);
76+
var info = new ReminderInfo(reminder.State, reminder.DueTime, reminder.Period, reminder.Repetitions,
77+
reminder.Ttl);
7778
return await info.SerializeAsync();
7879
}
7980
}

0 commit comments

Comments
 (0)