diff --git a/src/Microsoft.Actions.Actors/ActionsHttpInteractor.cs b/src/Microsoft.Actions.Actors/ActionsHttpInteractor.cs index 15225d73..d631fd98 100644 --- a/src/Microsoft.Actions.Actors/ActionsHttpInteractor.cs +++ b/src/Microsoft.Actions.Actors/ActionsHttpInteractor.cs @@ -271,6 +271,44 @@ namespace Microsoft.Actions.Actors return this.SendAsync(RequestFunc, relativeUrl, requestId, cancellationToken); } + public Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default(CancellationToken)) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorTimerRelativeUrlFormat, actorType, actorId, timerName); + var requestId = Guid.NewGuid().ToString(); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Put, + Content = new StringContent(data, Encoding.UTF8), + }; + + request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); + return request; + } + + return this.SendAsync(RequestFunc, relativeUrl, requestId, cancellationToken); + } + + public Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default(CancellationToken)) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.Timers, actorType, actorId, timerName); + var requestId = Guid.NewGuid().ToString(); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Delete, + }; + + return request; + } + + return this.SendAsync(RequestFunc, relativeUrl, requestId, cancellationToken); + } + /// /// Sends an HTTP get request to Actions. /// diff --git a/src/Microsoft.Actions.Actors/AspNetCore/ActionsActorSetupFilter.cs b/src/Microsoft.Actions.Actors/AspNetCore/ActionsActorSetupFilter.cs index 0be5c29d..ea85fed7 100644 --- a/src/Microsoft.Actions.Actors/AspNetCore/ActionsActorSetupFilter.cs +++ b/src/Microsoft.Actions.Actors/AspNetCore/ActionsActorSetupFilter.cs @@ -24,6 +24,7 @@ namespace Microsoft.Actions.Actors.AspNetCore actorRouteBuilder.AddActorDeactivationRoute(); actorRouteBuilder.AddActorMethodRoute(); actorRouteBuilder.AddReminderRoute(); + actorRouteBuilder.AddTimerRoute(); app.UseRouter(actorRouteBuilder.Build()); next(app); diff --git a/src/Microsoft.Actions.Actors/AspNetCore/RouterBuilderExtensions.cs b/src/Microsoft.Actions.Actors/AspNetCore/RouterBuilderExtensions.cs index bd589858..31565d55 100644 --- a/src/Microsoft.Actions.Actors/AspNetCore/RouterBuilderExtensions.cs +++ b/src/Microsoft.Actions.Actors/AspNetCore/RouterBuilderExtensions.cs @@ -89,5 +89,18 @@ namespace Microsoft.Actions.Actors.AspNetCore return ActorRuntime.FireReminderAsync(actorTypeName, actorId, reminderName, request.Body); }); } + + public static void AddTimerRoute(this IRouteBuilder routeBuilder) + { + routeBuilder.MapPut("actors/{actorTypeName}/{actorId}/method/timer/{timerName}", (request, response, routeData) => + { + var actorTypeName = (string)routeData.Values["actorTypeName"]; + var actorId = (string)routeData.Values["actorId"]; + var timerName = (string)routeData.Values["timerName"]; + + // read dueTime, period and data from Request Body. + return ActorRuntime.FireTimerAsync(actorTypeName, actorId, timerName); + }); + } } } diff --git a/src/Microsoft.Actions.Actors/Constants.cs b/src/Microsoft.Actions.Actors/Constants.cs index a5646b6c..fdb3e81b 100644 --- a/src/Microsoft.Actions.Actors/Constants.cs +++ b/src/Microsoft.Actions.Actors/Constants.cs @@ -24,6 +24,7 @@ namespace Microsoft.Actions.Actors public const string ActionsVersion = "v1.0"; public const string Method = "method"; public const string Reminders = "reminders"; + public const string Timers = "timers"; /// /// Gets string format for Actors state management relative url. @@ -44,5 +45,10 @@ namespace Microsoft.Actions.Actors /// Gets string format for Actors reminder registration relative url.. /// public static string ActorReminderRelativeUrlFormat => $"{ActionsVersion}/{Actors}/{{0}}/{{1}}/{Reminders}/{{2}}"; + + /// + /// Gets string format for Actors timer registration relative url.. + /// + public static string ActorTimerRelativeUrlFormat => $"{ActionsVersion}/{Actors}/{{0}}/{{1}}/{Timers}/{{2}}"; } } diff --git a/src/Microsoft.Actions.Actors/IActionsInteractor.cs b/src/Microsoft.Actions.Actors/IActionsInteractor.cs index a38d2a78..5b34fc0f 100644 --- a/src/Microsoft.Actions.Actors/IActionsInteractor.cs +++ b/src/Microsoft.Actions.Actors/IActionsInteractor.cs @@ -79,7 +79,7 @@ namespace Microsoft.Actions.Actors Task InvokeActorMethodWithRemotingAsync(IActorRequestMessage remotingRequestRequestMessage, CancellationToken cancellationToken = default(CancellationToken)); /// - /// Invokes Actor method. + /// Register a reminder. /// /// Type of actor. /// ActorId. @@ -90,7 +90,7 @@ namespace Microsoft.Actions.Actors Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default(CancellationToken)); /// - /// Invokes Actor method. + /// Unregisters a reminder. /// /// Type of actor. /// ActorId. @@ -98,5 +98,26 @@ namespace Microsoft.Actions.Actors /// Cancels the operation. /// A representing the result of the asynchronous operation. Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Registers a timer. + /// + /// Type of actor. + /// ActorId. + /// Name of timer to register. + /// Json reminder data as per the actions spec. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task RegisterTimerAsync(string actorType, string actorId, string timerName, string data, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Unegisters a timer. + /// + /// Type of actor. + /// ActorId. + /// Name of timer to register. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task UnregisterTimerAsync(string actorType, string actorId, string timerName, CancellationToken cancellationToken = default(CancellationToken)); } } diff --git a/src/Microsoft.Actions.Actors/Runtime/Actor.cs b/src/Microsoft.Actions.Actors/Runtime/Actor.cs index 6a48f05e..452e781d 100644 --- a/src/Microsoft.Actions.Actors/Runtime/Actor.cs +++ b/src/Microsoft.Actions.Actors/Runtime/Actor.cs @@ -6,6 +6,7 @@ namespace Microsoft.Actions.Actors.Runtime { using System; + using System.Collections.Generic; using System.Threading.Tasks; /// @@ -22,6 +23,11 @@ namespace Microsoft.Actions.Actors.Runtime private readonly string traceId; private readonly string actorImplementaionTypeName; + /// + /// Contains timers to be invoked. + /// + private Dictionary timers = new Dictionary(); + /// /// Initializes a new instance of the class. /// @@ -97,6 +103,12 @@ namespace Microsoft.Actions.Actors.Runtime return this.StateManager.ClearCacheAsync(); } + internal Task FireTimerAsync(string timerName) + { + var timer = this.timers[timerName]; + return timer.AsyncCallback.Invoke(timer.State); + } + /// /// Saves all the state changes (add/update/remove) that were made since last call to /// , @@ -201,9 +213,9 @@ namespace Microsoft.Actions.Actors.Runtime TimeSpan dueTime, TimeSpan period) { - var reminderData = new ReminderData(state, dueTime, period); - var reminder = new ActorReminder(this.Id, reminderName, reminderData); - await ActorRuntime.ActionsInteractor.RegisterReminderAsync(this.actorImplementaionTypeName, this.Id.ToString(), reminderName, reminderData.SerializeToJson()); + var reminderInfo = new ReminderInfo(state, dueTime, period); + var reminder = new ActorReminder(this.Id, reminderName, reminderInfo); + await ActorRuntime.ActionsInteractor.RegisterReminderAsync(this.actorImplementaionTypeName, this.Id.ToString(), reminderName, reminderInfo.SerializeToJson()); return reminder; } @@ -230,5 +242,97 @@ namespace Microsoft.Actions.Actors.Runtime { return ActorRuntime.ActionsInteractor.UnregisterReminderAsync(this.actorImplementaionTypeName, this.Id.ToString(), reminderName); } + + /// + /// Registers a Timer for the actor. A timer name is autogenerated by the runtime to keep track of it. + /// + /// + /// A delegate that specifies a method to be called when the timer fires. + /// It has one parameter: the state object passed to RegisterTimer. + /// It returns a representing the asynchronous operation. + /// + /// An object containing information to be used by the callback method, or null. + /// The amount of time to delay before the async callback is first invoked. + /// Specify negative one (-1) milliseconds to prevent the timer from starting. + /// Specify zero (0) to start the timer immediately. + /// + /// + /// The time interval between invocations of the async callback. + /// Specify negative one (-1) milliseconds to disable periodic signaling. + /// Returns IActorTimer object. + protected Task RegisterTimerAsync( + Func asyncCallback, + object state, + TimeSpan dueTime, + TimeSpan period) + { + return this.RegisterTimerAsync(null, asyncCallback, state, dueTime, period); + } + + /// + /// Registers a Timer for the actor. If a timer name is not provided, a timer is autogenerated. + /// + /// Timer Name. If a timer name is not provided, a timer is autogenerated. + /// + /// A delegate that specifies a method to be called when the timer fires. + /// It has one parameter: the state object passed to RegisterTimer. + /// It returns a representing the asynchronous operation. + /// + /// An object containing information to be used by the callback method, or null. + /// The amount of time to delay before the async callback is first invoked. + /// Specify negative one (-1) milliseconds to prevent the timer from starting. + /// Specify zero (0) to start the timer immediately. + /// + /// + /// The time interval between invocations of the async callback. + /// Specify negative one (-1) milliseconds to disable periodic signaling. + /// Returns IActorTimer object. + protected async Task RegisterTimerAsync( + string timerName, + Func asyncCallback, + object state, + TimeSpan dueTime, + TimeSpan period) + { + // create a timer name to register with Actions runtime. + if (string.IsNullOrEmpty(timerName)) + { + timerName = $"{this.Id.ToString()}_Timer_{this.timers.Count + 1}"; + } + + var actorTimer = new ActorTimer(this, timerName, asyncCallback, state, dueTime, period); + await ActorRuntime.ActionsInteractor.RegisterTimerAsync(this.actorImplementaionTypeName, this.Id.ToString(), timerName, actorTimer.SerializeToJson()); + + this.timers[timerName] = actorTimer; + return actorTimer; + } + + /// + /// Unregisters a Timer previously set on this actor. + /// + /// An IActorTimer representing timer that needs to be unregistered. + /// Task representing the Unregister timer operation. + protected async Task UnregisterTimerAsync(IActorTimer timer) + { + await ActorRuntime.ActionsInteractor.UnregisterTimerAsync(this.actorImplementaionTypeName, this.Id.ToString(), timer.Name); + if (this.timers.ContainsKey(timer.Name)) + { + this.timers.Remove(timer.Name); + } + } + + /// + /// Unregisters a Timer previously set on this actor. + /// + /// Name of timer to unregister. + /// Task representing the Unregister timer operation. + protected async Task UnregisterTimerAsync(string timerName) + { + await ActorRuntime.ActionsInteractor.UnregisterTimerAsync(this.actorImplementaionTypeName, this.Id.ToString(), timerName); + if (this.timers.ContainsKey(timerName)) + { + this.timers.Remove(timerName); + } + } } } diff --git a/src/Microsoft.Actions.Actors/Runtime/ActorManager.cs b/src/Microsoft.Actions.Actors/Runtime/ActorManager.cs index 8728413a..b79c3ff7 100644 --- a/src/Microsoft.Actions.Actors/Runtime/ActorManager.cs +++ b/src/Microsoft.Actions.Actors/Runtime/ActorManager.cs @@ -24,9 +24,11 @@ namespace Microsoft.Actions.Actors.Runtime { private const string TraceType = "ActorManager"; private const string ReceiveReminderMethodName = "ReceiveReminderAsync"; + private const string TimerMethodName = "FireTimerAsync"; private readonly ActorService actorService; private readonly ConcurrentDictionary activeActors; private readonly ActorMethodContext reminderMethodContext; + private readonly ActorMethodContext timerMethodContext; private readonly ActorMessageSerializersManager serializersManager; private IActorMessageBodyFactory messageBodyFactory; @@ -47,6 +49,7 @@ namespace Microsoft.Actions.Actors.Runtime this.actorMethodInfoMap = new ActorMethodInfoMap(this.actorService.ActorTypeInfo.InterfaceTypes); this.activeActors = new ConcurrentDictionary(); this.reminderMethodContext = ActorMethodContext.CreateForReminder(ReceiveReminderMethodName); + this.timerMethodContext = ActorMethodContext.CreateForReminder(TimerMethodName); this.serializersManager = IntializeSerializationManager(null); this.messageBodyFactory = new DataContractMessageFactory(); } @@ -133,11 +136,18 @@ namespace Microsoft.Actions.Actors.Runtime await awaitable; + // Write Response back if method's return type is other than Task. // Serialize result if it has result (return type was not just Task.) - var resultProperty = awaitable.GetType().GetProperty("Result"); - - // already await, Getting result will be non blocking. - return resultProperty == null ? default(object) : awaitable.GetAwaiter().GetResult(); + if (methodInfo.ReturnType.Name != typeof(Task).Name) + { + // already await, Getting result will be non blocking. + var x = awaitable.GetAwaiter().GetResult(); + return x; + } + else + { + return default(object); + } } var result = await this.DispatchInternalAsync(actorId, actorMethodContext, RequestFunc, cancellationToken); @@ -156,7 +166,7 @@ namespace Microsoft.Actions.Actors.Runtime // Only FireReminder if its IRemindable, else ignore it. if (this.ActorTypeInfo.IsRemindable) { - var reminderdata = ReminderData.Deserialize(requestBodyStream); + var reminderdata = ReminderInfo.Deserialize(requestBodyStream); // Create a Func to be invoked by common method. async Task RequestFunc(Actor actor, CancellationToken ct) @@ -177,6 +187,20 @@ namespace Microsoft.Actions.Actors.Runtime return Task.CompletedTask; } + internal Task FireTimerAsync(ActorId actorId, string timerName, CancellationToken cancellationToken = default(CancellationToken)) + { + // Create a Func to be invoked by common method. + async Task RequestFunc(Actor actor, CancellationToken ct) + { + await + actor.FireTimerAsync(timerName); + + return null; + } + + return this.DispatchInternalAsync(actorId, this.timerMethodContext, RequestFunc, cancellationToken); + } + internal async Task ActivateActor(ActorId actorId) { // An actor is activated by "actions" runtime when a call is to be made for an actor. diff --git a/src/Microsoft.Actions.Actors/Runtime/ActorReminder.cs b/src/Microsoft.Actions.Actors/Runtime/ActorReminder.cs index 9d9df42e..c1275990 100644 --- a/src/Microsoft.Actions.Actors/Runtime/ActorReminder.cs +++ b/src/Microsoft.Actions.Actors/Runtime/ActorReminder.cs @@ -15,31 +15,31 @@ namespace Microsoft.Actions.Actors.Runtime public ActorReminder( ActorId actorId, string reminderName, - ReminderData reminderData) + ReminderInfo reminderInfo) { this.OwnerActorId = actorId; this.Name = reminderName; - this.ReminderData = reminderData; + this.ReminderInfo = reminderInfo; } public string Name { get; } public byte[] State { - get { return this.ReminderData.Data; } + get { return this.ReminderInfo.Data; } } public TimeSpan DueTime { - get { return this.ReminderData.DueTime; } + get { return this.ReminderInfo.DueTime; } } public TimeSpan Period { - get { return this.ReminderData.Period; } + get { return this.ReminderInfo.Period; } } - internal ReminderData ReminderData { get; } + internal ReminderInfo ReminderInfo { get; } internal ActorId OwnerActorId { get; } } diff --git a/src/Microsoft.Actions.Actors/Runtime/ActorRuntime.cs b/src/Microsoft.Actions.Actors/Runtime/ActorRuntime.cs index 2c72baf9..eade4851 100644 --- a/src/Microsoft.Actions.Actors/Runtime/ActorRuntime.cs +++ b/src/Microsoft.Actions.Actors/Runtime/ActorRuntime.cs @@ -129,6 +129,19 @@ namespace Microsoft.Actions.Actors.Runtime return GetActorManager(actorTypeName).FireReminderAsync(new ActorId(actorId), reminderName, requestBodyStream, cancellationToken); } + /// + /// Fires a timer for the Actor. + /// + /// Actor type name to invokde the method for. + /// Actor id for the actor for which method will be invoked. + /// The name of timer provided during registration. + /// The token to monitor for cancellation requests. + /// A representing the result of the asynchronous operation. + internal static Task FireTimerAsync(string actorTypeName, string actorId, string timerName, CancellationToken cancellationToken = default(CancellationToken)) + { + return GetActorManager(actorTypeName).FireTimerAsync(new ActorId(actorId), timerName, cancellationToken); + } + private static ActorManager GetActorManager(string actorTypeName) { if (!actorManagers.TryGetValue(actorTypeName, out var actorManager)) diff --git a/src/Microsoft.Actions.Actors/Runtime/ActorTimer.cs b/src/Microsoft.Actions.Actors/Runtime/ActorTimer.cs new file mode 100644 index 00000000..3caf89cf --- /dev/null +++ b/src/Microsoft.Actions.Actors/Runtime/ActorTimer.cs @@ -0,0 +1,72 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +namespace Microsoft.Actions.Actors.Runtime +{ + using System; + using System.IO; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + using Newtonsoft.Json; + + internal class ActorTimer : IActorTimer + { + private readonly Actor owner; + + public ActorTimer( + Actor owner, + string timerName, + Func asyncCallback, + object state, + TimeSpan dueTime, + TimeSpan period) + { + this.owner = owner; + this.Name = timerName; + this.AsyncCallback = asyncCallback; + this.State = state; + this.Period = period; + this.DueTime = dueTime; + } + + public string Name { get; } + + public TimeSpan DueTime { get; } + + public TimeSpan Period { get; } + + public object State { get; } + + public Func AsyncCallback { get; } + + internal string SerializeToJson() + { + string content; + using (var sw = new StringWriter()) + { + using (var writer = new JsonTextWriter(sw)) + { + writer.WriteStartObject(); + if (this.DueTime != null) + { + writer.WriteProperty((TimeSpan?)this.DueTime, "dueTime", JsonWriterExtensions.WriteTimeSpanValueActionsFormat); + } + + if (this.Period != null) + { + writer.WriteProperty((TimeSpan?)this.Period, "period", JsonWriterExtensions.WriteTimeSpanValueActionsFormat); + } + + // Do not serialize state and call back, it will be kept with actor instance. + writer.WriteEndObject(); + content = sw.ToString(); + } + } + + return content; + } + } +} diff --git a/src/Microsoft.Actions.Actors/Runtime/IActorTimer.cs b/src/Microsoft.Actions.Actors/Runtime/IActorTimer.cs new file mode 100644 index 00000000..ca960f7f --- /dev/null +++ b/src/Microsoft.Actions.Actors/Runtime/IActorTimer.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------ + +namespace Microsoft.Actions.Actors.Runtime +{ + using System; + using System.Threading.Tasks; + + /// + /// Represents the timer set on an Actor. + /// + public interface IActorTimer + { + /// + /// Gets the time when timer is first due. + /// + /// Time as when timer is first due. + TimeSpan DueTime { get; } + + /// + /// Gets the periodic time when timer will be invoked. + /// + /// Periodic time as when timer will be invoked. + TimeSpan Period { get; } + + /// + /// Gets the name of the Timer. The name is unique per actor. + /// + /// The name of the timer. + string Name { get; } + + /// + /// Gets a delegate that specifies a method to be called when the timer fires. + /// It has one parameter: the state object passed to RegisterTimer. + /// It returns a representing the asynchronous operation. + /// + Func AsyncCallback { get; } + + /// + /// Gets state containing information to be used by the callback method, or null. + /// + object State { get; } + } +} diff --git a/src/Microsoft.Actions.Actors/Runtime/ReminderData.cs b/src/Microsoft.Actions.Actors/Runtime/ReminderInfo.cs similarity index 88% rename from src/Microsoft.Actions.Actors/Runtime/ReminderData.cs rename to src/Microsoft.Actions.Actors/Runtime/ReminderInfo.cs index e0333556..ac9f0958 100644 --- a/src/Microsoft.Actions.Actors/Runtime/ReminderData.cs +++ b/src/Microsoft.Actions.Actors/Runtime/ReminderInfo.cs @@ -15,20 +15,20 @@ namespace Microsoft.Actions.Actors.Runtime using Microsoft.Actions.Actors.Resources; using Newtonsoft.Json; - internal class ReminderData + internal class ReminderInfo { private readonly TimeSpan minTimePeriod = Timeout.InfiniteTimeSpan; - public ReminderData( - byte[] reminderState, - TimeSpan reminderDueTime, - TimeSpan reminderPeriod) + public ReminderInfo( + byte[] state, + TimeSpan dueTime, + TimeSpan period) { - this.ValidateDueTime("DueTime", reminderDueTime); - this.ValidatePeriod("Period", reminderPeriod); - this.Data = reminderState; - this.DueTime = reminderDueTime; - this.Period = reminderPeriod; + this.ValidateDueTime("DueTime", dueTime); + this.ValidatePeriod("Period", period); + this.Data = state; + this.DueTime = dueTime; + this.Period = period; } public TimeSpan DueTime { get; private set; } @@ -37,7 +37,7 @@ namespace Microsoft.Actions.Actors.Runtime public byte[] Data { get; private set; } - internal static ReminderData Deserialize(Stream stream) + internal static ReminderInfo Deserialize(Stream stream) { // Deserialize using JsonReader as we know the property names in response. Deserializing using JsonReader is most performant. using (var streamReader = new StreamReader(stream)) @@ -81,7 +81,7 @@ namespace Microsoft.Actions.Actors.Runtime return content; } - private static ReminderData GetFromJsonProperties(JsonReader reader) + private static ReminderInfo GetFromJsonProperties(JsonReader reader) { var dueTime = default(TimeSpan); var period = default(TimeSpan); @@ -110,7 +110,7 @@ namespace Microsoft.Actions.Actors.Runtime } while (reader.TokenType != JsonToken.EndObject); - return new ReminderData(data, dueTime, period); + return new ReminderInfo(data, dueTime, period); } private void ValidateDueTime(string argName, TimeSpan value)