Your first Reliable Actors application

Now that we have understood all the carious concepts of a Reliable Actors Application in detail, let us cement our learning by building a sample that demonstrates all the key concepts that we have learned till now.

Create a new Service Fabric application named HelloWorldActorsApplication in Visual Studio and choose the Reliable Actors template.

You'd need to specify a name for the Actor service in the template dialog, let's name the service HelloWorldActor:

Creating HelloWorldActor service

After the template finishes unfolding, you would find three projects loaded in your solution as follows:

  • HelloWorldActorsApplication: This is the application project that packages all the applications together for deployment. This project contains the deployment PowerShell script and the manifest file – ApplicationManifest.xml, that contains the name of packages that need to be deployed on Service Fabric cluster among other settings.
  • HelloWorldActor.Interfaces: This project contains the communication contract for the Actor application and the clients. The clients of an Actor application uses these contracts to communicate with the application and the application implements those contracts. Although, it is not a requirement of Service Fabric to create a separate assembly to store the contracts, designing them in such a manner is useful since this project might be shared between the application and the clients.
  • HelloWorldActor: This is the Actor service project which defines the Service Fabric service that is going to host our Actor. This project contains implementation of Actor interfaces defined in the Actor interfaces project. Let's take a deeper look into the primary class of this project – HelloWorldActor in HelloWorldActor.cs file.

This class derives from the Actor base class and implements the communication contract interfaces defined in the HelloWorldActor.Interfaces assembly. This class implements some of the Actor lifecycle events that we previously discussed. This class has a constructor that accepts an ActorService instance and an ActorId and passes them to the base Actor class.

        [StatePersistence(StatePersistence.Persisted)] 
internal class HelloWorldActor : Actor, IHelloWorldActor
{
public HelloWorldActor(ActorService actorService, ActorId
actorId)
: base(actorService, actorId)
{
}
...
}

When the Actor service gets activated, the OnActivateAsync is invoked. In the default template code, this method instantiates a state variable with count:

        protected override Task OnActivateAsync() 
{
ActorEventSource.Current.ActorMessage(this, "Actor
activated.");
return this.StateManager.TryAddStateAsync("count", 0);
}

Let's now navigate to the Main method in Program.cs file which is responsible for hosting the Actor application. Every Actor service must be associated with a service type in the Service Fabric runtime. The ActorRuntime.RegisterActorAsync method registers your Actor type with the Actor service so that the Actor service can run your Actor instances. If you want to host more than one Actor types in an application, then you can add more registrations with the ActorRuntime with the following statement:

ActorRuntime.RegisterActorAsync<AnotherActor>();  

Let's modify this application to allow users to save reminders with a message and later notify the user with the message at the scheduled time with an event.

To begin, add a new contract in the Actor interface, IHelloWorldActor to set a reminder and retrieve the collection of reminders that have been set:

    public interface IHelloWorldActor : IActor 
{
Task<List<(string reminderMessage, DateTime
scheduledReminderTimeUtc)>>
GetRemindersAsync(CancellationToken
cancellationToken);

Task SetReminderAsync(string reminderMessage, DateTime
scheduledReminderTimeUtc, CancellationToken
cancellationToken);
}

The SetReminderAsync method accepts a message that should be displayed at the schceduled time. We will store the message in the Actor state in the form of a list. The GetRemindersAsync method will return a list of all the reminders that have been set by a user.

Let's navigate to the HelloWorldActor class to implement the members of this interface. First let's implement the SetReminderAsync method:

        public async Task SetReminderAsync( 
string reminderMessage,
DateTime scheduledReminderTimeUtc,
CancellationToken cancellationToken)
{
var existingReminders = await this.StateManager
.GetStateAsync<List<(string reminderMessage, DateTime
scheduledReminderTimeUtc)>>(
"reminders",
cancellationToken);
// Add another reminder.
existingReminders.Add((reminderMessage,
scheduledReminderTimeUtc));
}

In this method, using the StateManager, we have first retrieved the reminders stored in the state. In the next statement, we added another reminder to the state.

The implementation of GetReminderAsync is very straightforward as well. In this method we simply retrive whatever is stored in the state and send it back as a response:

        public async Task<List<(
string reminderMessage,
DateTime scheduledReminderTimeUtc)>> GetRemindersAsync(
CancellationToken cancellationToken)
{
return await
this.StateManager.GetStateAsync<List<(string
reminderMessage, DateTime scheduledReminderTimeUtc)>>
("reminders", cancellationToken);

}

The state is initialized in the OnActivateAsync method:

        protected override Task OnActivateAsync() 
{
ActorEventSource.Current.ActorMessage(this, "Actor
activated.");
return this.StateManager.TryAddStateAsync("reminders", new
List<(string reminderMessage, DateTime
scheduledReminderTimeUtc)>());
}

The application side logic is covered. Let's create a client for our application now. Add a new console application named HelloWorldActor.Client to the solution. For this project, make sure that you choose the same .Net framework as your Service Fabric application and set the platform to x64. Since the clients and the application need to share the communication contract, therefore add reference to the HelloWorldActor.Interfaces assembly in this project.

Now, let us create a proxy to the Actor objects and invoke the Actor methods using the proxy:

private static async Task MainAsync(string[] args, CancellationToken token) 
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Enter Your Name");
var userName = Console.ReadLine();
var actorClientProxy = ActorProxy.Create<IHelloWorldActor>(
new ActorId(userName),
"fabric:/HelloWorldActorsApplication");
await actorClientProxy.SetReminderAsync(
"Wake me up in 2 minutes.",
DateTime.UtcNow + TimeSpan.FromMinutes(2),
token);
await actorClientProxy.SetReminderAsync(
"Another reminder to wake up after a minute.",
DateTime.UtcNow + TimeSpan.FromMinutes(3),
token);
Console.WriteLine("Here are your reminders");
var reminders = await
actorClientProxy.GetRemindersAsync(token);
foreach (var reminder in reminders)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"Reminder at:
{reminder.scheduledReminderTimeUtc} with message:
{reminder.reminderMessage}");
}
}

For this example, we will use the name of user to create and identify the Actor object that we want to work with. This will ensure that all our operations activate the same Actor instance and perform operations with it. After we have accepted the name of the user, we will use the Create method of ActorProxy class to create a proxy to the desired Actor instance. We can later use this proxy to invoke the methods exposed through the Actor interface. The ActorProxy abstracts the process to locate an Actor in the cluster and abstracts failure handling and retry mechanism in case of cluster node failures.

Next, we have used the proxy object that we have created to invoke the various methods on the Actor object.

You can run the application and the client now to test the functionalities that we have built till now. Till now the application only stores the reminders in state, however doesn't notify the user at the scheduled time. Now, let us add reminders to the application that will get invoked at the scheduled time.

Head back to the HelloWorldActor class and implement the IRemindable interface. This interface has a single method ReceiveReminderAsync which gets invoked every time a reminder gets triggered. To add a reminder, add the following statement to the SetReminderAsync method that we defined earlier:

await this.RegisterReminderAsync( 
$"{this.Id}:{Guid.NewGuid()}", BitConverter.GetBytes(scheduledReminderTimeUtc.Ticks),
scheduledReminderTimeUtc - DateTime.UtcNow,
TimeSpan.FromMilliseconds(-1));

This statement will register a new reminder with the specified name, payload (which is the scheduled trigger time that we will use to identify the reminder), the scheduled occurrence time and the recurrence interval, which we have set to -1 to indicate that we don't want the reminder to recur.

Now that we have added a reminder, we need a mechanism to talk back to the client. Actor events give us the capability to do so. However, this mechanism doesn't guarantee message delivery and therefore should be used with caution in enterprise applications where guaranteed delivery might be a requirement. Also, this mechanism is designed for Actor-client communications only and is not supposed to be used for Actor-Actor communications.

To use Actor events, we need to define a class that implements the IActorEvents interface. Let's add a class named IReminderActivatedEvent in the HelloWorldActor.Interfaces which implements the IActorEvents interface. This interface doesn't contain any members and is only used by runtime to identify events:

    public interface IReminderActivatedEvent : IActorEvents 
{
void ReminderActivated(string message);
}

Now that we have defined our event, we need to make sure that the runtime knows who is the publisher of the event. The runtime recognizes an event publisher using the IActorEventPublisher interface. This interface has no members that require implementation. Let's piggyback this interface on the IHelloWorldActor so that both the application and the client know that there are Actor events involved in the communication. The new IHelloWorldActor interface declaration should now look like the following:

public interface IHelloWorldActor : IActor, IActorEventPublisher<IReminderActivatedEvent> 

To complete the application side processing of events, let's implement the ReceiveReminderAsync method in the HelloWorldActor class which gets triggered every time a reminder is scheduled to trigger. In this function, we will try to extract the reminder from the state that got triggered, raise an Actor event, and finally unregister the reminder:

        public async Task ReceiveReminderAsync(string reminderName, 
byte[] state, TimeSpan dueTime, TimeSpan period)
{
var payloadTicks = BitConverter.ToInt64(state, 0);
// Get the reminder from state.
var cancellationToken = CancellationToken.None;
var existingReminders = await this.StateManager
.GetStateAsync<List<(string reminderMessage, DateTime
scheduledReminderTimeUtc)>>(
"reminders",
cancellationToken);
var reminderMessage =
existingReminders.FirstOrDefault(
reminder => reminder.scheduledReminderTimeUtc ==
new DateTime(payloadTicks));
if (reminderMessage.reminderMessage != string.Empty)
{
// Trigger an event for the client.
var evt = this.GetEvent<IReminderActivatedEvent>(); evt.ReminderActivated(reminderMessage.reminderMessage);
}

// Unregister the reminder.
var thisReminder = this.GetReminder(reminderName);
await this.UnregisterReminderAsync(thisReminder);
}

We do not need to construct an object of IReminderActivatedEvent, the base class Actor provides a GetEvent method that does it for us. By invoking the member function of IReminderActivatedEvent, we raise an event that gets published to the client.

Now let's tie things together on the client side. In the client we need to build an event handler and finally listen to the Actor event that gets raised by the application. It's time to head back to the HelloWorldActor.Client application and add a new class in the project named ReminderHandler. This class needs to implement the Actor event IReminderActivatedEvent which will get invoked when the application raises this event. For this sample, we will simply redirect the message to console when the event is raised:

    public class ReminderHandler : IReminderActivatedEvent 
{
public void ReminderActivated(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Reminder Activated: {message}");
Console.ResetColor();
}
}

To start listening to the event revisit the Main method which contains the implementation of the client. In this method, we are going to subscribe to the IReminderActivatedEvent and provide it with an object of the event handler that we just defined. The revised Main method should look something like the following:

        private static async Task MainAsync(string[] args, 
CancellationToken token)
{
...
Console.ResetColor();
await
actorClientProxy.SubscribeAsync<IReminderActivatedEvent>
(new ReminderHandler());
...
}

Let's start the application and the client now to see it in action. Once you start the application, you should be able to see messages appearing in the Diagnostics Events viewer console:

Diagnostic event messages from HelloWorldActor appliction

Finally, when you fire off the client application, you should be able to see new events being registered and reminders being triggered by the application and handled by the client:

HelloWorldActor client

You might have noticed that we never used the Actor ID while persisting state or while operating with it. That is because the state is local to Actor, which means that no two instances of our application that supply different user names will write data to same store. Another noticeable fact is that we never used thread locking mechanisms in our code. That is because reliable Actors are by nature single-threaded. Which means that you can't invoke multiple methods in the same Actor simultaneously, which avoids thread locking problems.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset