- Windows Azure: Just plug-in your work: Part 1
- Windows Azure: Just plug-in your work: Part 2
In my previous post I described the idea of using plug-ins in Windows Azure. In context of this sample we implemented a general plug-in framework. As we saw in the video, we need two services: one which is taking the work orders and stores them in the repository and the other one which is processing them.
WorkOrderGatewayService
is representing a service which takes the work orders and stores them in the BLOB Storage. This service also answers the question whether the work order is done or not. WorkOrderProcessorService
is representing the service which is processing the work orders.
Web Role: WorkOrderGatewayService
The WorkOrderGatewayService
is the simple web service based on WCF technology. This service is the only service which is visible to clients. The responsibility of the WorkOrderGatewayService
is:
- store the work orders in the BLOB Storage and create work order messages in the queue
- provide the method which creates a command to refresh plugins
- provide the method to check if the work order has benn finished
public class WorkOrderGatewayService : IWorkOrderGatewayService { private readonly IWorkOrderManagement _workOrderManagementBusinessLayer; private readonly ICommandManagement _commandManagementBusinessLayer; public WorkOrderGatewayService() { var storageConnectionString = RoleEnvironment.GetConfigurationSettingValue("StorageConnectionString"); var blobStorageWorkOrdersContainer = RoleEnvironment.GetConfigurationSettingValue("BlobStorageWorkOrdersContainer"); var messageQueueName = RoleEnvironment.GetConfigurationSettingValue("MessageQueueName"); var azureRepository = new AzureRepository(storageConnectionString); var orderWorkerRepository = new WorkOrderRepository(azureRepository, blobStorageWorkOrdersContainer, messageQueueName); _workOrderManagementBusinessLayer = new WorkOrderManagement(orderWorkerRepository, azureRepository, messageQueueName); _commandManagementBusinessLayer = new CommandManagement(azureRepository, messageQueueName); } public void ExecuteWorkOrder(WorkOrder order) { _workOrderManagementBusinessLayer.SendWorkOrderToProcessingQueue(order); } public bool OrderFinished(Guid identifier, out WorkOrderResult result) { result = _workOrderManagementBusinessLayer.GetWorkOrderResult(identifier); return result != null; } public void RefreshPlugins() { _commandManagementBusinessLayer.SendCommandToProcessing(new Command {CommandText = "RefreshPlugins"}); } }
Worker Role: WorkOrderProcessorService
The WorkOrderProcessorService
is the service, which is processing the work orders.
While the plug-in’s infrastructure, we created before, is able to load the plugins from the file system, this is a problem in context of Windows Azure, because:
- we don’t have direct access to the machines file system, to be able to upload plugins there
- the local file system is not persistent
How can we solve this? We will use the BLOB Storage as plugins repository. In the BLOB Storage we are creating the container named ‘plugins’ (if you don’t create it, WorkOrderProcessorService
will do so, when the role is starting). To this container we can upload the plugins, as you can see below. How you name the “folders” is up to you.
To be able load plugins by FileSystemPluginLoader
, we have to copy the content of ‘plugins’ container to the machine’s file system. This is the most tricky part. As I wrote in my previous post, we have to ensure that all plugins are under the applications base path directory (do you remember the sense of the PrivateBinPath
?) to be able to instantiate them when we are creating new Application Domain.
You can retrieve the application’s bin directory in Windows Azure, like this.
//In Azure enviroment can be RoleRoot path something like this C: //If you don't add DirectorySeparatorChar, than path will be after Path.Combine something like this C:approot //C:approot has different meaning as C:\\approot //for more details see //http://stackoverflow.com/questions/1527942/why-path-combine-doesnt-add-the-path-directoryseparatorchar-after-the-drive-des //http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx#relative%5Fpaths var roleRootPath = Environment.GetEnvironmentVariable("RoleRoot"); if(!roleRootPath.EndsWith(Path.DirectorySeparatorChar.ToString())) //checks if directory separator exists { roleRootPath = roleRootPath + Path.DirectorySeparatorChar.ToString(); //Path.Combine deliver in this context bad result } var applicationPhysicalPath = Path.Combine(roleRootPath, "approot"); //Gets application physical path
We are copying the plugins to the file system when the role is starting with the method RefreshPlugins
of class PluginRepository
. PluginRepository
is wrapping the class PluginManager
created in the previous post and provide some extra methods.
The plugins are copied to the local file system when the role is starting
public override bool OnStart() { Trace.TraceInformation("Role is starting"); try { ... _pluginRepository = new PluginRepository(_azureRepository, applicationPhysicalPath, relativePluginsDirectoryPath, pluginsSearchPattern, blobStoragePluginsContainer); Trace.TraceInformation("Refreshing plugins"); _pluginRepository.RefreshPlugins(); Trace.TraceInformation("Plugins refreshed"); } catch (Exception ex) { Trace.TraceError(string.Format("Exception thrown on role start: {0}", ex)); throw; } Trace.TraceInformation("Role starting completed"); return base.OnStart(); }
public void RefreshPlugins() { IEnumerable thrownExceptions; if (!_pluginManager.TryToUnloadPlugins(out thrownExceptions)) { Trace.TraceError("Unloading plugins thrown exception"); foreach (var thrownException in thrownExceptions) { Trace.TraceError(thrownException.ToString()); } } DeletePluginsFromLocalDrive(); CopyPluginsToLocalDrive(); if (!_pluginManager.TryToLoadPlugins(out thrownExceptions)) { Trace.TraceError("Loading plugins thrown exception"); foreach (var thrownException in thrownExceptions) { Trace.TraceError(thrownException.ToString()); } } }
This is not the only way, how it is to possible refresh the plugins. In this example, you can send commands (and in this sample, the only one: ‘RefreshPlugins’). The command ‘RefreshPlugins’ is just simple text in the queue ‘workorders’ (created by WorkOrderProcessorService
service) which is parsed by the method GetCommand
provided by the class CommandProcessing.
public Tuple<CloudQueueMessage, Command> GetCommand(CloudQueueMessage message) { if (message != null) { Guid workOrderIdentifier; bool isGuid = Guid.TryParse(message.AsString, out workOrderIdentifier); //Only GUIDs are work order identifiers. Everything else are commands. if (!isGuid) { return new Tuple<cloudqueuemessage, command="">(message, new Command { CommandText = message.AsString }); } } return null; }
The method GetCommand
is called in the Run
method of WorkOrderProcessorService
class.
public override void Run() { Trace.TraceInformation("Role is running"); try { while (true) { CloudQueueMessage message = _azureRepository.GetNextMessage(_messageQueueName, _messageTimeout); Trace.TraceInformation("Try to get work command"); Tuple<cloudqueuemessage, command=""> command = _commandProcessingBusinessLayer.GetCommand(message); if (command == null) { Trace.TraceInformation("No command to processing."); } else { Trace.TraceInformation("Try to execute command"); _commandProcessingBusinessLayer.ExecuteCommand(command); Trace.TraceInformation("Command executed"); } Trace.TraceInformation("Try to get work order"); Tuple<cloudqueuemessage, workorder=""> workOrder = _workOrderProcessingBusinessLayer.GetWorkOrder(message); if (workOrder == null) { Trace.TraceInformation("No work order is existing to be processed."); Trace.TraceInformation("Sleeping"); Thread.Sleep(10000); } else { Trace.TraceInformation("Try to processing order worker"); StartWorkOrderProcessing(workOrder); Trace.TraceInformation("Order worker processed"); } } } catch (Exception ex) { Trace.TraceError(string.Format("Exception thrown on running: {0}", ex)); } }
This concept is definitively not the best one. Why?
- because, this is working only for one instance. After the instance of
WorkOrderProcessorService
executed the command, throws him (message) away, so no other instance has a chance to execute the command again. - because you must parse each message to check if it is a command or not
This is definitely the brain teaser and can be part of your orchestration strategy. Some ideas which can work:
- you allow machines to refresh the plugins in some defined period
- you create a separate queue for the commands for each machine
What is necessary to mention is, that you should ensure that all plugins finished thei work, before you unload them from memory, otherwise you get AppDomainUnloadedException.
If the work order is finished, the result is stored in the BLOB Storage.
How does the work order look like? The work order is the serialized object of type WorkOrder
into the workorder.xml.
<?xml version="1.0" encoding="utf-16"?> <!--?xml version="1.0" encoding="utf-16"?--> <WorkOrder xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Identifier>8da0a95c-6401-49f9-aef5-8707112a5d98</Identifier> <PluginName>RandomWordsPlugin</PluginName> <PluginVersion>1.0.0.0</PluginVersion> <InputText>Anton Kalcik</InputText> </WorkOrder>
The Identifier
is GUID and provides the way to track concrete work orders. PluginName
is the name, PluginVersion
is the version of the plugin. Both parameters should match the PluginMetadata
attribute one of the plugins loaded in the memory. InputText
is the text which should be manipulated by concrete plugins.
The result of the plugin operation (in this case is it the text manipulation) is serialized as object of type WorkOrderResult
to the workorder.result.xml. This is the same file which is checked by WorkOrderGatewayService
to ensure whenever the work order is finished, or not.
The purpose of the Identifier
is the same as by work order, the Result
contains the outcome of the plugin operation.
<?xml version="1.0" encoding="utf-16"?> <!--?xml version="1.0" encoding="utf-16"?--> <WorkOrderResult xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <Identifier>8da0a95c-6401-49f9-aef5-8707112a5d98</Identifier> <Result>kiclaK notnA</Result> </WorkOrderResult>
Deployment
To deploy the service to the Azure you have to
- add the connection string to the Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString
- add the connection string to the StorageConnectionString
All BLOB containers and queues are created when the roles are starting. In the BLOB container ‘workorders’ you can upload the plugins (some sample plugins are included already in the solution). To test the services, you can use the CloudServices.TestConsole.
I hope I gave you the idea, how you can get more from your cloud services. The latest you can find here. I will be thankful for your valuable feedback and appreciate any suggestions you might have for improvements – please share them with us with your comments below.
I really like this blog. You write about very interesting things. Thanks for all your tips and information
LikeLike
Thank you!
LikeLike
This post is great. I realy love it!
LikeLike