Empowered by the Composite Windows Azure Web Role

| CWAW |
In this post I am showing one of the great features one get from using the Composite Windows Azure Webrole for developers who want that little bit extra.

I wanted to create a service that could convert a NuGet package to a composite c1 package to work with my NuGet Package Manager for Composite C1. Idea is that someone requests that a given NuGet package should be converted to a c1 package and the result is a c1 package that contains the meta data given in the NuGet and creates the install file such it can be installed like normal c1 packages and having dependencies pulled in by my NuGet Package Manager.  

I am not a big fan of putting long running code into a WebAPI controller and spinning up a Worker Role on Windows Azure for something that will be used rarely seems a bit over engineered. Therefore I am going to show how I created my little task scheduler / runner as a background plugin to our Composite Windows Azure Web Role. I find this a perfect place to do things as I can get as much value for my money by reusing the same VM that already hosts my websites.

Try it out

You can try it out here or continue reading below. The service will tell in the mail you get with your package if it can be installed into C1, as I have not completed that part yet. I will write a new blog post about the package creator when it is finished, and this post is just about, how to make background worker do tasks. You can fill in some NuGet feed, package id, version and an email to return the package to below. (I will also update the inputs fields when the backend is working better). For Composite C1 readers, the form here is just a razor function that puts the user inputs on the azure service bus.

Composite Windows Azure Web Role Plugins

The interface for plugins are as follows:

    public interface IWebRolePlugin : IDisposable
    {
        /// <summary>
        /// The priority of the plugin. All plugins are run asynchronously
        /// in synchronously groups.
        /// Priorities [0:99] runs in parallel, then [100:199].
        /// 
        /// WebrolePluginPriority.LOW (200), MEDIUM(100), HIGH(1) is avalible.
        /// 
        /// So using difference priorities you can ensure one plugin to be run before another.
        /// </summary>
        int Priority { get; }

        /// <summary>
        /// This is a periodic callback called by the PluginManager with 
        /// updates about Added,Deleted,Updated websites.
        /// 
        /// <seealso cref="WebsiteSettingsChangedEventArgs"/>
        /// 
        /// You can do your periodic callbacks at other intervals in your plugin,
        /// but this is where you get information from the system.
        /// </summary>
        Task WebsitesPeriodeCheckAsync(object sender, WebsiteSettingsChangedEventArgs args);

        /// <summary>
        /// This is called in the webrole startup periode in groups with other 
        /// plugins as described above.
        /// </summary>
        Task InitializePluginAsync();
    }

If you want to use your own plugins as I do here for background processing, you will have to build your cloud service your self. This also means you can configure additional webrole settings and remote desktop access at the same time. I have just pushed some sample code to the Github repository for Composite Windows Azure Webrole. The sample shows how one can create a plugin that listens to a Windows Azure Service Bus Topic and you can configure your own logic for when messages are received. 

using Composite.WindowsAzure.WebRole;
using System.Diagnostics;
using Webrole;

namespace WebRole
{
    public class WebRole : CompositeWebRole
    {
        public override void Run()
        {
            // This is a sample worker implementation. Replace with your logic.
            Trace.TraceInformation("Webrole entry point called", "Information");

            base.Run();
        }

        public override bool OnStart()
        {
            CompositeWebRole.DependencyResolver.RegisterPlugin<MyWorkerPlugin>();

            // For information on handling configuration changes
            // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.

            return base.OnStart();
        }
    }
}

In the code above you see how to register your plugin and below is the full sample code of my worker part.

using Composite.WindowsAzure.WebRole.Plugins;
using Composite.WindowsAzure.WebRole.Websites;
using Microsoft.ServiceBus;
using Microsoft.ServiceBus.Messaging;
using Microsoft.WindowsAzure;
using SInnovations.WebRole.QueueMessages;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using WebRole.Composite;

namespace Webrole
{
    public class MyWorkerPlugin : IWebRolePlugin
    {

        private const string TopicName = "SInnovations";
        private const string AllMessages = "AllMessages";

        private ManualResetEvent CompletedEvent = new ManualResetEvent(false);
        private SubscriptionClient Client;
        private Task Runner;

        public int Priority
        {
            get
            {
                return WebrolePluginPriority.LOW;
            }
        }

        TaskCompletionSource<object> source;
        public Task InitializePluginAsync()
        {
            Trace.WriteLine("Starting MyWorkerPlugin");
            Runner = Task.Factory.StartNew(StartSubscriptionClient, TaskCreationOptions.LongRunning);
            source = new TaskCompletionSource<object>();
            
            return source.Task;

        }

        private void StartSubscriptionClient()
        {
            try
            {
                string connectionString = CloudConfigurationManager.GetSetting("SInnovations.Servicebus.ConnectionString");

                var namespaceManager =
                    NamespaceManager.CreateFromConnectionString(connectionString);

                TopicDescription td = new TopicDescription(TopicName);
                td.MaxSizeInMegabytes = 1024;
                td.DefaultMessageTimeToLive = new TimeSpan(0, 5, 0);

                if (!namespaceManager.TopicExists(TopicName))
                {
                    namespaceManager.CreateTopic(td);
                }
                if (!namespaceManager.SubscriptionExists(TopicName, AllMessages))
                {
                    namespaceManager.CreateSubscription(TopicName, AllMessages);
                }


                Client = SubscriptionClient.CreateFromConnectionString
                    (connectionString, TopicName, AllMessages);
                var options = new OnMessageOptions { AutoComplete = false, MaxConcurrentCalls = 10 };
                options.ExceptionReceived += options_ExceptionReceived;

                Client.OnMessageAsync(OnMessageAsync, options);
                if (source != null)
                {
                    source.SetResult(null);
                    source = null;
                }
                CompletedEvent.WaitOne();
            }catch(Exception ex)
            {
                source.SetException(ex);
                
            }
        }

        void options_ExceptionReceived(object sender, ExceptionReceivedEventArgs e)
        {
            Trace.TraceError(e.Exception.ToString());
        }


        private async Task OnMessageAsync(BrokeredMessage message)
        {
            try
            {
                Trace.TraceInformation(message.MessageId);
                if (message.Properties.ContainsKey("type"))
                {
                    switch (message.Properties["type"] as string)
                    {
                        case "CreateC1PackageRequest":
                            await PackageCreator.ProcessMessageAsync(message.GetBody<CreateC1PackageRequest>());
                            break;

                        default:
                            break;
                    }
                }
            }catch(Exception ex)
            {
                Trace.TraceError(ex.ToString());
            }
          
            await message.CompleteAsync();
        }
        public Task WebsitesPeriodeCheckAsync(object sender, WebsiteSettingsChangedEventArgs args)
        {

            if (Runner.Status == TaskStatus.Faulted)
            {
                Client.Close();
                CompletedEvent.Set();
                CompletedEvent = new ManualResetEvent(false);
                Trace.TraceInformation("Restarting MyWorkerPlugin");
                Runner = Task.Factory.StartNew(StartSubscriptionClient, TaskCreationOptions.LongRunning);
            }
            //We dont need this method.
            return Task.FromResult<object>(null);
        }

        public void Dispose()
        {
            Client.Close();
            CompletedEvent.Set();
        }
    }
}

Therefore, this should be as simple as if you wanted to do a worker role for windows azure. The benefit here is you can create your generic hosting role, have these plugins being part of your host, and add websites dynamic to it. Future plugins I want to create is a backup plugin that makes a snapshot of the websites once a day giving me a nice way to fallback to an earlier website version if I make something stupid.

You should be able to find everything on the Github  page to try something yourself. The sample just pushed uses the 2.0 prerelease version of the web role and we are just missing a few key things before that can go live.



comments powered by Disqus