Skip to content

Commit

Permalink
Add support for repetition and ISO 8601 for reminders (dapr#974)
Browse files Browse the repository at this point in the history
Signed-off-by: Yash Nisar <yashnisar@microsoft.com>

Signed-off-by: Yash Nisar <yashnisar@microsoft.com>
  • Loading branch information
yash-nisar authored Jan 27, 2023
1 parent 1605ecd commit 6e77f12
Show file tree
Hide file tree
Showing 18 changed files with 691 additions and 19 deletions.
9 changes: 9 additions & 0 deletions examples/Actor/ActorClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ public static async Task Main(string[] args)
await proxy.UnregisterTimer();
Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted.");
await proxy.UnregisterReminder();

Console.WriteLine("Registering reminder with repetitions - The reminder will repeat 3 times.");
await proxy.RegisterReminderWithRepetitions(3);
Console.WriteLine("Waiting so the reminder can be triggered");
await Task.Delay(5000);
Console.WriteLine("Registering reminder with ttl and repetitions, i.e. reminder stops when either condition is met - The reminder will repeat 2 times.");
await proxy.RegisterReminderWithTtlAndRepetitions(TimeSpan.FromSeconds(5), 2);
Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted.");
await proxy.UnregisterReminder();

Console.WriteLine("Registering reminder and Timer with TTL - The reminder will self delete after 10 seconds.");
await proxy.RegisterReminderWithTtl(TimeSpan.FromSeconds(10));
Expand Down
10 changes: 10 additions & 0 deletions examples/Actor/DemoActor/DemoActor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ public async Task RegisterReminderWithTtl(TimeSpan ttl)
{
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), ttl);
}

public async Task RegisterReminderWithRepetitions(int repetitions)
{
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions);
}

public async Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions)
{
await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions, ttl);
}

public Task UnregisterReminder()
{
Expand Down
15 changes: 15 additions & 0 deletions examples/Actor/IDemoActor/IDemoActor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ public interface IDemoActor : IActor
/// <param name="ttl">Optional TimeSpan that dictates when the timer expires.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
Task RegisterTimerWithTtl(TimeSpan ttl);

/// <summary>
/// Registers a reminder with repetitions.
/// </summary>
/// <param name="repetitions">The number of repetitions for which the reminder should be invoked.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
Task RegisterReminderWithRepetitions(int repetitions);

/// <summary>
/// Registers a reminder with ttl and repetitions.
/// </summary>
/// <param name="ttl">TimeSpan that dictates when the timer expires.</param>
/// <param name="repetitions">The number of repetitions for which the reminder should be invoked.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions);

/// <summary>
/// Unregisters the registered timer.
Expand Down
10 changes: 10 additions & 0 deletions src/Dapr.Actors/Resources/SR.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Dapr.Actors/Resources/SR.resx
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,7 @@
<data name="TimerArgumentOutOfRange" xml:space="preserve">
<value>TimeSpan TotalMilliseconds specified value must be between {0} and {1} </value>
</data>
<data name="RepetitionsArgumentOutOfRange" xml:space="preserve">
<value>The repetitions {0} specified must be a valid positive integer.</value>
</data>
</root>
78 changes: 77 additions & 1 deletion src/Dapr.Actors/Runtime/Actor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ namespace Dapr.Actors.Runtime
{
using System;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
using Dapr.Actors.Client;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -264,6 +263,83 @@ protected async Task<IActorReminder> RegisterReminderAsync(
Ttl = ttl
});
}

/// <summary>
/// Registers a reminder with the actor.
/// </summary>
/// <param name="reminderName">The name of the reminder to register. The name must be unique per actor.</param>
/// <param name="state">User state passed to the reminder invocation.</param>
/// <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>
/// <param name="period">
/// The time interval between reminder invocations after the first invocation.
/// </param>
/// <param name="repetitions">The number of repetitions for which the reminder should be invoked.</param>
/// <param name="ttl">The time interval after which the reminder will expire.</param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// </remarks>
protected async Task<IActorReminder> RegisterReminderAsync(
string reminderName,
byte[] state,
TimeSpan dueTime,
TimeSpan period,
int repetitions,
TimeSpan ttl)
{
return await RegisterReminderAsync(new ActorReminderOptions
{
ActorTypeName = this.actorTypeName,
Id = this.Id,
ReminderName = reminderName,
State = state,
DueTime = dueTime,
Period = period,
Repetitions = repetitions,
Ttl = ttl
});
}

/// <summary>
/// Registers a reminder with the actor.
/// </summary>
/// <param name="reminderName">The name of the reminder to register. The name must be unique per actor.</param>
/// <param name="state">User state passed to the reminder invocation.</param>
/// <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>
/// <param name="period">
/// The time interval between reminder invocations after the first invocation.
/// </param>
/// <param name="repetitions">The number of repetitions for which the reminder should be invoked.</param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// </remarks>
protected async Task<IActorReminder> RegisterReminderAsync(
string reminderName,
byte[] state,
TimeSpan dueTime,
TimeSpan period,
int repetitions)
{
return await RegisterReminderAsync(new ActorReminderOptions
{
ActorTypeName = this.actorTypeName,
Id = this.Id,
ReminderName = reminderName,
State = state,
DueTime = dueTime,
Period = period,
Repetitions = repetitions
});
}

/// <summary>
/// Registers a reminder with the actor.
Expand Down
79 changes: 79 additions & 0 deletions src/Dapr.Actors/Runtime/ActorReminder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,71 @@ public ActorReminder(
{
}

/// <summary>
/// Initializes a new instance of <see cref="ActorReminder" />.
/// </summary>
/// <param name="actorType">The actor type.</param>
/// <param name="actorId">The actor id.</param>
/// <param name="name">The reminder name.</param>
/// <param name="state">The state associated with the reminder.</param>
/// <param name="dueTime">The reminder due time.</param>
/// <param name="period">The reminder period.</param>
/// <param name="repetitions">The number of times reminder should be invoked.</param>
/// <param name="ttl">The reminder ttl.</param>
public ActorReminder(
string actorType,
ActorId actorId,
string name,
byte[] state,
TimeSpan dueTime,
TimeSpan period,
int? repetitions,
TimeSpan? ttl)
: this(new ActorReminderOptions
{
ActorTypeName = actorType,
Id = actorId,
ReminderName = name,
State = state,
DueTime = dueTime,
Period = period,
Repetitions = repetitions,
Ttl = ttl
})
{
}

/// <summary>
/// Initializes a new instance of <see cref="ActorReminder" />.
/// </summary>
/// <param name="actorType">The actor type.</param>
/// <param name="actorId">The actor id.</param>
/// <param name="name">The reminder name.</param>
/// <param name="state">The state associated with the reminder.</param>
/// <param name="dueTime">The reminder due time.</param>
/// <param name="period">The reminder period.</param>
/// <param name="repetitions">The number of times reminder should be invoked.</param>
public ActorReminder(
string actorType,
ActorId actorId,
string name,
byte[] state,
TimeSpan dueTime,
TimeSpan period,
int? repetitions)
: this(new ActorReminderOptions
{
ActorTypeName = actorType,
Id = actorId,
ReminderName = name,
State = state,
DueTime = dueTime,
Period = period,
Repetitions = repetitions
})
{
}

/// <summary>
/// Initializes a new instance of <see cref="ActorReminder" />.
/// </summary>
Expand Down Expand Up @@ -118,11 +183,20 @@ internal ActorReminder(ActorReminderOptions options)
options.DueTime,
TimeSpan.MaxValue.TotalMilliseconds));
}

if (options.Repetitions != null && options.Repetitions <= 0)
{
throw new ArgumentOutOfRangeException(nameof(options.Repetitions), string.Format(
CultureInfo.CurrentCulture,
SR.RepetitionsArgumentOutOfRange,
options.Repetitions));
}

this.State = options.State;
this.DueTime = options.DueTime;
this.Period = options.Period;
this.Ttl = options.Ttl;
this.Repetitions = options.Repetitions;
}

/// <summary>
Expand All @@ -144,5 +218,10 @@ internal ActorReminder(ActorReminderOptions options)
/// The optional <see cref="TimeSpan"/> that states when the reminder will expire.
/// </summary>
public TimeSpan? Ttl { get; }

/// <summary>
/// The optional property that gets the number of invocations of the reminder left.
/// </summary>
public int? Repetitions { get; }
}
}
5 changes: 5 additions & 0 deletions src/Dapr.Actors/Runtime/ActorReminderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,10 @@ internal class ActorReminderOptions
/// An optional <see cref="TimeSpan"/> that determines when the reminder will expire.
/// </summary>
public TimeSpan? Ttl { get; set; }

/// <summary>
/// The number of repetitions for which the reminder should be invoked.
/// </summary>
public int? Repetitions { get; set; }
}
}
76 changes: 76 additions & 0 deletions src/Dapr.Actors/Runtime/ConverterUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
namespace Dapr.Actors.Runtime
{
using System;
using System.Text;
using System.Text.RegularExpressions;

internal class ConverterUtils
{
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);
public static TimeSpan ConvertTimeSpanFromDaprFormat(string valueString)
{
if (string.IsNullOrEmpty(valueString))
Expand Down Expand Up @@ -68,5 +71,78 @@ public static string ConvertTimeSpanValueInDaprFormat(TimeSpan? value)

return stringValue;
}

public static string ConvertTimeSpanValueInISO8601Format(TimeSpan value, int? repetitions)
{
StringBuilder builder = new StringBuilder();

if (repetitions == null)
{
return ConvertTimeSpanValueInDaprFormat(value);
}

if (value.Milliseconds > 0)
{
throw new ArgumentException("The TimeSpan value, combined with repetition cannot be in milliseconds.", nameof(value));
}

builder.AppendFormat("R{0}/P", repetitions);

if(value.Days > 0)
{
builder.AppendFormat("{0}D", value.Days);
}

builder.Append("T");

if(value.Hours > 0)
{
builder.AppendFormat("{0}H", value.Hours);
}

if(value.Minutes > 0)
{
builder.AppendFormat("{0}M", value.Minutes);
}

if(value.Seconds > 0)
{
builder.AppendFormat("{0}S", value.Seconds);
}
return builder.ToString();
}

public static (TimeSpan, int?) ConvertTimeSpanValueFromISO8601Format(string valueString)
{
// ISO 8601 format can be Rn/PaYbMcHTdHeMfS or PaYbMcHTdHeMfS so if it does
// not start with R or P then assuming it to default Dapr format without repetition
if (!(valueString.StartsWith('R') || valueString.StartsWith('P')))
{
return (ConvertTimeSpanFromDaprFormat(valueString), -1);
}

var matches = regex.Match(valueString);

var repetition = matches.Groups["repetition"].Success ? int.Parse(matches.Groups["repetition"].Value) : (int?)null;

var days = 0;
var year = matches.Groups["year"].Success ? int.Parse(matches.Groups["year"].Value) : 0;
days = year * 365;

var month = matches.Groups["month"].Success ? int.Parse(matches.Groups["month"].Value) : 0;
days += month * 30;

var week = matches.Groups["week"].Success ? int.Parse(matches.Groups["week"].Value) : 0;
days += week * 7;

var day = matches.Groups["day"].Success ? int.Parse(matches.Groups["day"].Value) : 0;
days += day;

var hour = matches.Groups["hour"].Success ? int.Parse(matches.Groups["hour"].Value) : 0;
var minute = matches.Groups["minute"].Success ? int.Parse(matches.Groups["minute"].Value) : 0;
var second = matches.Groups["second"].Success ? int.Parse(matches.Groups["second"].Value) : 0;

return (new TimeSpan(days, hour, minute, second), repetition);
}
}
}
3 changes: 2 additions & 1 deletion src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ public override async Task UnregisterTimerAsync(ActorTimerToken timer)

private async ValueTask<string> SerializeReminderAsync(ActorReminder reminder)
{
var info = new ReminderInfo(reminder.State, reminder.DueTime, reminder.Period, reminder.Ttl);
var info = new ReminderInfo(reminder.State, reminder.DueTime, reminder.Period, reminder.Repetitions,
reminder.Ttl);
return await info.SerializeAsync();
}
}
Expand Down
Loading

0 comments on commit 6e77f12

Please sign in to comment.