Interrole Communication Example in Windows Azure

There are many scenarios where you will need to do interrole communication when creating a solution using Windows Azure platform such as multiple web roles that need to recycle when a controller triggers an event.

One way to achieve this goal is to create an event queue, modify the controller to enqueue a message that indicates a recycle is requested and make your web roles listen to this queue. Your controller can also enqueue 3 messages if you have 3 Web Roles to recycle. Below is the basic design for such an approach.

image

While you can use a queue as to mediate your messaging needs, you can also use WCF services to do interrole communication. In this approach, your roles would expose an internal endpoint, bind a WCF service that would listen on this endpoint. Your controller would use the RoleEnvironment to identify each role instance and invoke the service endpoints to notify each instance that a recycle is required. The design for this approach is below.

image

In this post I will try to provide a simple example of interrole communication using WCF – in other words, I will try to realize the design above. The code examples will not have much error handling, instead I will try to concentrate on the basics. Our Tier 1 Role Instances will expose a WCF Service that will use TCP transportation. The service interface will be a simple one that will include a one-way function called RecycleYourself. Upon retrieval of this message, our Tier 1 Role instances will recycle themselves.

We will start with adding input endpoints for our Tier 1 Role instances. This is pretty easy to do using Visual Studio. We will start with right clicking on the Worker Role definition and selecting properties.

image
In the endpoints section of the Tier 1 Worker Role properties screen, we will click on the “Add Endpoint” button to add a new internal endpoint. Let’s name it “NotificationServiceEndPoint”, select “Internal” as the type (we are selecting the “Internal Endpoint Type” since we will not be exposing this endpoint to external world) and select Protocol to be TCP.

image

Next step is to create an interface for the service that will accept the recycle notification. This interface will expose a one-way endpoint. You can modify the contracts to suit your needs. I would suggest creating a class library project and putting the contracts in this project since we will be referencing it both for hosting and consumption purposes. The project that you create for your service and contracts should reference System.Servicemodel and System.Runtime.Serialization assemblies.

image

The code for the datacontract is provided below. This will be our message that is passed as a parameter to the host.

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Runtime.Serialization;
  
 namespace InterroleCommunicationContracts
 {
     [DataContract(Namespace = "urn:InterroleCommunicationTest:2010:06:24")]
     public class NotificationMessage
     {
         [DataMember]
         public string RoleName { get; set; }
  
         [DataMember]
         public string Tag { get; set; }
     }
 }

The service contract is a very simple one. The only function will be a void one called RecycleYourself with a parameter of type NotificationMessage.

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.ServiceModel;
  
 namespace InterroleCommunicationContracts
 {
     [ServiceContract(Namespace="urn:InterroleCommunicationTest:2010:06:24")]
     public interface INotifiyService
     {
         [OperationContract]
         void RecycleYourself(NotificationMessage message);
     }
 }

Next step is to host a WCF Service in our Tier 1 Worker Roles. This service host will expose an event that will be raised whenever our service receives a notification message. We will later handle that event in our Worker Role and take appropriate actions. The hosting code is provided below.

 

 using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using InterroleCommunicationContracts;
 using System.ServiceModel;
  
 namespace Tier1WorkerRole
 {
     public delegate void RecycleNotificationRecievedEventHandler(object sender, NotificationMessage e);
  
     [ServiceBehavior(
         InstanceContextMode = InstanceContextMode.Single,
         ConcurrencyMode = ConcurrencyMode.Multiple,
         IncludeExceptionDetailInFaults = true,
         AddressFilterMode = AddressFilterMode.Any)]
     public class NotificationServiceHost : INotifiyService
     {
         public event RecycleNotificationRecievedEventHandler RecycleNotificationRecieved;
  
         public void RecycleYourself(NotificationMessage sender)
         {
             this.OnRecycleNotificationRecieved(sender);
         }
         
         protected virtual void OnRecycleNotificationRecieved(NotificationMessage e)
         {
             RecycleNotificationRecievedEventHandler handler = RecycleNotificationRecieved;
             if (handler != null)
             {
                 // Invokes the delegates.
                 handler(this, e);
             }
         }
  
     }
 }

As you can see in the code snippet above, our service host exposes an event called RecycleNotificationRecieved. Whenever we receive a RecycleYourself call, we raise this event. There are also some attributes in our service host where one of them is very important for our case. As we will need only one instance of this service, we have decorated it as InstanceContextMode.Single. The reason why we will need only one instance (a singleton pattern) is because we will be hosting this service as a singleton so that we can hook into it’s event.

In order to host this service in our Tier 1 worker role, we will first reference to the project where we have created our interfaces. We will later need to get the endpoint that we created above, create an instance of our host and instantiate a service host around this instance. We will also hook into the event and handle it in our worker role. We will create and run the service in our Run() method. Below is the full listing of our Tier 1 worker role.

 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
 using System.Net;
 using System.Threading;
 using Microsoft.WindowsAzure;
 using Microsoft.WindowsAzure.Diagnostics;
 using Microsoft.WindowsAzure.ServiceRuntime;
 using Microsoft.WindowsAzure.StorageClient;
 using InterroleCommunicationContracts;
 using System.ServiceModel;
  
 namespace Tier1WorkerRole
 {
     public class WorkerRole : RoleEntryPoint
     {
         public ServiceHost ServiceHost { get; private set; }
  
         private void StartServiceHost()
         {
             Trace.WriteLine("Starting Service Host", "Information");
  
             NotificationServiceHost serviceHostBase = new NotificationServiceHost();
             serviceHostBase.RecycleNotificationRecieved += new RecycleNotificationRecievedEventHandler(ServiceHost_RecycleNotificationRecieved);
  
             ServiceHost = new ServiceHost(serviceHostBase);
  
             this.ServiceHost.Faulted += (sender, e) =>
             {
                 Trace.TraceError("Service Host fault occured");
                 this.ServiceHost.Abort();
                 Thread.Sleep(500);
                 this.StartServiceHost();
             };
  
             NetTcpBinding binding = new NetTcpBinding(SecurityMode.None);
  
             RoleInstanceEndpoint notificationServiceHostEndPoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["NotificationServiceEndPoint"];
  
             this.ServiceHost.AddServiceEndpoint(
                 typeof(INotifiyService),
                 binding,
                 String.Format("net.tcp://{0}/NotifyService", notificationServiceHostEndPoint.IPEndpoint)
                 );
  
             try
             {
                 this.ServiceHost.Open();
                 Trace.TraceInformation("Service Host Opened");
             }
             catch (TimeoutException timeoutException)
             {
                 Trace.TraceError("Service Host open failure, Time Out: " + timeoutException.Message);
             }
             catch (CommunicationException communicationException)
             {
                 Trace.TraceError("Service Host open failure, Communication Error: " + communicationException.Message);
             }
  
             Trace.WriteLine("Service Host Started", "Information");
         }
  
         void ServiceHost_RecycleNotificationRecieved(object sender, NotificationMessage e)
         {
             Trace.WriteLine("RecycleNotification Recieved From : " + e.RoleName, "Warning");
             // TODO: Recycle My Instance
             // RoleEnvironment.RequestRecycle();
         }
  
         public override void Run()
         {
             // This is a sample worker implementation. Replace with your logic.
             Trace.WriteLine("Tier1WorkerRole entry point called", "Information");
  
             this.StartServiceHost();
  
             while (true)
             {
                 Thread.Sleep(10000);
                 Trace.WriteLine("Working", "Information");
             }
         }
  
         public override bool OnStart()
         {
             // Set the maximum number of concurrent connections 
             ServicePointManager.DefaultConnectionLimit = 12;
  
             DiagnosticMonitor.Start("DiagnosticsConnectionString");
  
             // For information on handling configuration changes
             // see the MSDN topic at https://go.microsoft.com/fwlink/?LinkId=166357.
             RoleEnvironment.Changing += RoleEnvironmentChanging;
  
             return base.OnStart();
         }
  
         private void RoleEnvironmentChanging(object sender, RoleEnvironmentChangingEventArgs e)
         {
             // If a configuration setting is changing
             if (e.Changes.Any(change => change is RoleEnvironmentConfigurationSettingChange))
             {
                 // Set e.Cancel to true to restart this role instance
                 e.Cancel = true;
             }
         }
     }
 }

As our Tier 1 instance is ready to host our service, now is the time modify our controller. We will call our controller worker role, Tier 2 worker role. In this worker role, we will create a proxy for our service, find out all instances of Tier 1 Role, get their service endpoints and invoke the appropriate endpoints. The code for this process is below.

 using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
 using System.Net;
 using System.Threading;
 using Microsoft.WindowsAzure;
 using Microsoft.WindowsAzure.Diagnostics;
 using Microsoft.WindowsAzure.ServiceRuntime;
 using Microsoft.WindowsAzure.StorageClient;
 using System.ServiceModel;
  
 namespace Tier2WorkerRole
 {
     public class WorkerRole : RoleEntryPoint
     {
         public override void Run()
         {
             // This is a sample worker implementation. Replace with your logic.
             Trace.WriteLine("Tier2WorkerRole entry point called", "Information");
  
             while (true)
             {
                 NotifyRolesForRecycle();
  
                 Thread.Sleep(10000);
                 Trace.WriteLine("Working", "Information");
             }
         }
  
         public override bool OnStart()
         {
             // Set the maximum number of concurrent connections 
             ServicePointManager.DefaultConnectionLimit = 12;
  
             DiagnosticMonitor.Start("DiagnosticsConnectionString");
  
             // For information on handling configuration changes
             // see the MSDN topic at https://go.microsoft.com/fwlink/?LinkId=166357.
             RoleEnvironment.Changing += RoleEnvironmentChanging;
  
             return base.OnStart();
         }
  
         private void RoleEnvironmentChanging(object sender, RoleEnvironmentChangingEventArgs e)
         {
             // If a configuration setting is changing
             if (e.Changes.Any(change => change is RoleEnvironmentConfigurationSettingChange))
             {
                 // Set e.Cancel to true to restart this role instance
                 e.Cancel = true;
             }
         }
  
         public void NotifyRolesForRecycle()
         {
             var role = RoleEnvironment.Roles["Tier1WorkerRole"];
             Trace.WriteLine("Number of Instances found: " + role.Instances.Count, "Information");
  
             foreach (var instance in role.Instances)
             {
                 RoleInstanceEndpoint notificationServiceHostEndPoint = instance.InstanceEndpoints["NotificationServiceEndPoint"];
  
                 NetTcpBinding binding = new NetTcpBinding(SecurityMode.None, false);
  
                 EndpointAddress myEndpoint = new EndpointAddress(
                     String.Format("net.tcp://{0}/NotifyService", notificationServiceHostEndPoint.IPEndpoint)
                     );
  
                 try
                 {
                     ChannelFactory<InterroleCommunicationContracts.INotifiyService> myChanFac = new ChannelFactory<InterroleCommunicationContracts.INotifiyService>(binding, myEndpoint);
                     InterroleCommunicationContracts.INotifiyService myClient = myChanFac.CreateChannel();
                     myClient.RecycleYourself(new InterroleCommunicationContracts.NotificationMessage() { RoleName = "Tier2", Tag = "none" });
                 }
                 catch (Exception e)
                 {
                     Trace.WriteLine("An error occured trying to notify the instances: " + e.Message, "Warning");
                 }
  
             }
         }
     }
 }

As you can see above, the NotiyRolesForRecycle is the function where we do most of the work. In this function we get the Role for Tier 1 Worker Role using the RoleEnvironment.Roles dictionary. Once we get the Role object, we iterate through its instances. For each instance we get the endpoint using InstanceEndPoints dictionary of RoleInstance object. After creating a basic TCP binding and an endpoint to use with our channel factory, we create a client and invoke the RecycleYourself function.

Allow me to underline that the example I have provided above lacks error handling, tracing and any other decorations to make it robust. Again, for example purposes I tried to keep the interfaces simple. You can enhance this example to use more complex data contracts, add more functionality to your services. You can also use Duplex Bindings which will be very handy if you need check the status of other worker roles.