A general-purpose service engine for unattended processing execution - CodeProject
When the time comes to develop any sort of unattended processing and you want to use .NET, the most natural choice is to create a Windows service to be left running and operating continuously. If you have to do this multiple times for different purposes, you easily feel the need to adopt some standards and to have some common functionalities (including commanding, configuration, logging) developed just once. Moreover, you probably need a simple way to debug and to setup/install the services you’re developing.
In this article I describe a piece of software (I named "PEprocessorEngine"), that arises exactly from these needs. Its goal is to provide a unified system targeted to support general-purpose unattended processing executions.
The idea is to create a unique “engine” (in the form of a unique Windows service, installed once for all), able to dynamically load and run different and multiple “modules”, that are custom specialized code snippets (in the form of .NET assemblies) in charge of executing the specific task you want to be run in an unattended fashion.
In that way, you have a robust environment where your modules can run, leveraging the common functionalities (such as logging, monitoring, etc.) provided by the engine, with the ability of starting, stopping or even adding new modules very easily and without further installations. For example, the addition of a new module will be done by creating/developing it in the form of an assembly, copying it to the destination machine (together with its own configuration info) and then telling the engine (that is there already running and supporting other modules) to load and run the brand new module.
The PEprocessorEngine system I propose is completely developed on Microsoft .NET Framework 3.5/4.0 (using VB.NET language), and is made of:
- a native .NET Windows service, named PEprocessorEngine, that manages the start/stop of the processing modules, as long as their dynamic loading/unloading, and provides logging/monitoring facilities;
- a variable and configurable set of processing modules (later named "modules" or "processors"), that can be loaded/started/stopped selectively and independently configured;
- a class library assembly, named PEprocessorLib, containing utility functions and declarations, referenced both by the PEprocessorEngine and by each single module;
- a dedicated SQL Server database (referred as the PE support database), runnable on any version of SQL Server from version 2005 upwards (including SQL Express), used as a support storage for the engine, for the logging features and for supporting possible interactions with custom applications created to monitor and control the system.
The PEprocessorEngine is a native .NET Windows service implemented in the executable file PEprocessorEngine.exe. It references directly the PEprocessorLib.DLL class library and is equipped with an XML configuration file (the App.config file, runtime-named PEprocessorEngine.exe.config) that hosts the configuration settings for the engine and the common configuration settings for the various implemented modules (each module has then its own configuration file, containing its specific settings). PEprocessorEngine.exe.config does not contain settings specific for a single PE processor module, only settings for the engine.
As stated before, the PEprocessorEngine is equipped with a support SQL Server database, basically used for these purposes:
- maintain a centralized log of the operations performed by the PEprocessorEngine service and by each module;
- maintain a "command queue", useful to submit commands to the engine through a database table;
- control the running/stopped status of the processors.
As stated before, the PEprocessorEngine is implemented as a Windows service. On startup, the engine loads its configuration from its own configuration file. Then, it immediately looks at the configuration keys identifying the modules to be automatically started; these keys are named "PEprocessor" as in:
<add key="PEprocessor" value="MyModuleA" /><br /> <add key="PEprocessor" value="MyModuleB" />
The engine tries to start all of the listed modules, each in a separate, independent thread, by loading, instantiating and running the corresponding assembly. Assemblies of the modules to be started are searched in the same folder where the engine is running; each module must be implemented in a separate assembly DLL having the same name of the module itself (see "Developing PE processor modules" for details).
Each module can be dynamically invoked thanks to the fact it implements the parameterless method MainTask exposed by the interface PEprocessorLib.IPEprocessor. After loading the module's assembly in memory and after instantiating the module's main class, the engine simply invokes on that class the MainTask method, on a dedicated thread. The MainTask module's entry point is invoked only once by the engine; this could happen in one of the following situations:
- when the engine itself starts and the module is configured to run automatically on the engine startup;
- when a START command is received by the engine (see later).
The implementation of the MainTask method is typically based on a controlled infinite polling loop, and is covered in the "Developing PE processor modules" paragraph.
When all the "autostart processors" have been started, the engine starts listening to its command queue, giving support to some incoming commands, useful for controlling the engine itself and the processors, which are described in the "Commanding the PEprocessorEngine" paragraph.
The configuration for the PEprocessorEngine is stored in the file: PEprocessorEngine.exe.config
The settings and keys hosted in this "main" configuration file are described in the following paragraphs.
PEinstanceName. Instance name for the specific instance of the PEprocessorEngine.
PEconnectionString. Connection string to the PE support database.
CommandProcessorPollingInterval. This key determines the polling interval (in milliseconds) for the processing loop for the commands issued to the engine (that is, the commands in the CommandQueue table, described below). This key determines the engine's "reactivity" to commands sent on the CommandQueue.
STOPcommandTimeout. This key determines the number of seconds a STOP command (see below) must wait for a module to terminate before aborting the corresponding thread. This key is related to the PollingSubIntervalsDuration key: its value should be always greater than the PollingSubIntervalsDuration's one (see below).
PollingSubIntervalsDuration. This key determines the duration (in seconds) of the quantized subintervals for PollingSleep function of PEprocessorLib. This key influences the frequency of the polling query on StoppingProcessors control table, described below.
CheckRestartIntervals. This key determines the number of polling CommandProcessor intervals recurring between each "check&restart" of PE modules. The "check&restart" feature guarantees the automatic restart of any module that eventually stopped unexpectedly. A value of 720 is = 1 hour if CommandProcessorPollingInterval is = 5 sec.
PEprocessor (multiple key). Keys named "PEprocessor" indicate the PE processor modules to be loaded and started automatically when the engine starts up; any key matching exactly the string "PEprocessor" is considered.
The value of these keys must match the assembly file name where the module itself is implemented.
The PE support database
The PE database is used to support the internal operations of the PEprocessorEngine and to interact with external applications. It includes three tables:
- CommandQueue table: supports the communication with controlling applications, allowing them to send commands to the engine and retrieve responses by it;
- ProcessorLog table: supports the internal logging facilities of the engine and allows to maintain a centralized log for the custom PE modules;
- StoppingProcessors table: support the internal signalling of STOP commands between the engine and the modules.
The following paragraphs describe those database tables in detail.
The CommandQueue table, used to issue the commands to the PEprocessorEngine and to retrieve responses from it (as described in the "Commanding the PEprocessorEngine" paragraph) is structured as follows:
Identity, auto-incremented key value.
Name of the host on which the target engine is running. Multiple instances of the PEprocessorEngine (running on different machines) can possibly use the same database as a support database and, so, the same command queue and the same processor log.
The text of the issued command.
The date and time when the command has been issued (that is: inserted in the CommandQueue table).
The verbose textual response the service stored after the execution of the issued command. The external application commanding the engine is in charge of reading this response after issuing the corresponding command and waiting for its actual execution. If this field contains a NULL, then the entry corresponds to a command that is still pending (and StatusQueue field equals 1).
The date and time when the command response has been stored in the ResponseText field. If this field contains a NULL, then the entry corresponds to a command that is still pending (and StatusQueue field equals 1).
Current status of the command entry:
1 = pending command (not yet processed by the engine)
2 = executed command (the ResponseText and ResponseIssued fields are populated accordingly)
The ProcessorLog table is used to keep track of the operations performed by the PEprocessorEngine and by each module, and to signal warnings and errors of the unattended execution.
Following is the description of the table structure.
Identity, auto-incremented key value.
Represents the date and time when the log entry was written. It is set by a GETDATE() T-SQL function, so it is referred to the DB server clock.
Indicates the log entry type, according to this enumeration:
1 (PEactivity): log entry generated by the engine during its operation
2 (PEcommand): log entry generated by the engine while processing an incoming command
3 (procLowImportance): log entry of low importance generated by a processor module
4 (procHighImportance): log entry of high importance generated by a processor module
Contains the name of the module that generated the entry. This field can also contain the value "PEprocessorEngine", for entries generated by the engine itself.
Contains a code indicating the type of the logged message. Currently adopted codes are:
1 (noError): non-error message (ack, ok, etc.)
2 (opError): operational error message
3 (warning): warning message
Contains the logged message text itself. Being the datatype a varchar(MAX), this field can contain any additional information that is needed to be carried with the error itself.
The events and the errors occurring inside the engine are logged automatically and are recorded in the ProcessorLog table.
Because each module could need different types of logging, with different levels of severity and/or verbosity, each module is in charge of implementing and operating its own logging facilities.
Although a module could implement its tracing and logging through its own mechanisms and on its own storage, the PEprocessorLib library exposes an API to write module-related log entries in the ProcessorLog table. This allows having a centralized log storage collecting both the engine-related and the modules-related log information (for details, see "Logging inside a module").
The PE engine uses this table to signal a module about the need to gracefully stop its operation. The StoppingProcessors table is used this way:
- during normal operation, it is empty;
- when a STOP command is issued to the engine (via the CommandQueue), the given module is enlisted in the StoppingProcessors table, indicating the machine hosting the engine ("HostName" field) and the date/time of stop request ("Inserted" field);
- during its lifetime, each processor module regularly inspects the StoppingProcessors table (by calling the Util.IsProcessorStopping method) in order to detect an entry referring itself, indicating the need to end its own processing;
- if such an entry is detected, the module has to stop gracefully its current work (that means: exiting from its main polling loop);
- after stopping its current work, the last operation performed by the module is to signal its graceful stop by removing the pertaining entry from the StoppingProcessors table, through a call to the Util.SetStoppingProcessor method.
Notice that - as described in paragraph "PE module structure" - also the Util.PollingSleep method (to be used in the processor's main polling loop to pause before the next iteration) internally calls the Util.IsProcessorStopping method to check if a STOP command has been issued while the module was waiting for the next iteration.
Installing the PEprocessorEngine
Being a standard .NET Windows service, the PEprocessorEngine can be easily installed through the standard
installutil .NET tool.
The folder where the PEprocessorEngine is installed must contain:
- DLL assembly (and corresponding CONFIG) for each installed custom PE module
- any other ancillary assembly possibly referenced by the custom PE modules
Be aware of the fact that the content of the PE installation folder varies at runtime: because the PEprocessorEngine performs the processors' assemblies loading on an alternate AppDomain, those assemblies are copied in a subfolder named LoadedAssembliesCache, automatically created in the PE installation folder.
Commanding the PEprocessorEngine
The engine can receive and process some "commands" on a database command queue (implemented in the CommandQueue table on the support database).
The command queue is periodically polled by the engine. When a pending command to be executed is detected in the queue, the engine processes it and stores the result/outcome of the executed command in a dedicated field of the command entry itself. The commanding application is then in charge of retrieve the command result/outcome by reading the command entry after the command execution happened.
The engine is currently able to process the commands listed here (for a detailed description, see the following paragraphs):
Starts a specific PE processor module. The specified module has to be correctly installed and configured in advance.
Stops a specific PE processor module, in a graceful way.
Queries modules’ status.
Lists all the installed PE processors modules (i.e. the DLL assemblies found implementing the IPEprocessor interface).
Stops all the running processor modules and exit the engine.
Monitoring the PEprocessorEngine
When you want to monitor the execution of the PEprocessorEngine, the main source of information comes from what is logged in the ProcessorLog table. In fact, not only the main operations performed by the engine are automatically tracked here, but typically also the custom PE modules are developed to log their data in that table, through the PEprocessorLib logging API.
Any custom application can easily submit a query like this to inspect recent PEprocessorEngine operations:
SELECT TOP 50 * FROM ProcessorLog WITH (NOLOCK) ORDER BY LogDate DESC
Similarly, if you want to inspect the incoming commands and their execution performed by the engine, you could easily provide a query like this:
SELECT TOP 10 * FROM CommandQueue WITH (NOLOCK) ORDER BY CommandIssued DESC
"Hot updating" PE processor modules
The functionalities provided by the engine make it possible to operate so-called "hot updates" on processor modules. An "hot update" is an update made to a specific module (=a substitution of the DLL implementing it with an upgraded version) without stopping the entire engine, hence without any impact on all the others processor modules currently running on that instance of the engine.
An "hot update" of a specific processor module can be carried on simply by the following steps:
- stop the specific module (through a STOP command);
- substitute the assembly implementing it with the upgraded version;
- start the module again (through a START command).
In theory, if all updates to processor modules are always operated as "hot updates", the need to stop the entire engine (acting through the Windows Service Control Manager or issuing a QUIT command to the engine itself) is indeed limited to the only case when the engine itself (or the PEprocessorLib) has to be upgraded.
Developing PE processors
Here is a summary description of the structure and characteristics of a typical PE processor module:
- it is an assembly DLL containing a single main class, that implements the logic of the PE processor module itself;
- the name of the PE processor must match:
- the name of the main class;
- the name of the namespace containing the main class;
- the assembly name (name.DLL);
- the name of the XML configuration file (name.CONFIG);
- it references the PEprocessorLib.DLL library (and the corresponding PEprocessorLib namespace);
- the main class must implement the IPEprocessor interface (defined in PEprocessorLib);
- the final assembly DLL containing the PE processor must be located (along with the corresponding XML configuration file) in the same folder of the PEprocessorEngine.EXE, in order to be correctly located, loaded and executed by the engine itself.
The PEprocessorLib.IPEprocessor interface exposes these properties and methods:
Sub MainTask() - This method will contain the business logic of the PE processor (see description below). This method is invoked once, when the engine needs to start this specific module; it typically contains an infinite loop controlled by a call to the IsProcessorStopping method.
ReadOnly Property Name() As String - This property returns the name of the main class implementing the PE processor (that is, the PE processor's name itself); it is used for identifying the PE processor's instances and for labeling log entries.
Property ServiceInstanceName() As String - This property is set by the engine when the module is started in order to let it know the name of the engine instance that launched it.
The MainTask() method skeleton typically consists of these parts:
- initial setup code: main settings are read from the processor's "private" XML configuration file (using the utility function ReadSetting, exposed by the PEprocessorLib library); in general, to make current any modification to the PE processor configuration, a stop & restart of the specific PE processor module is required.
- the main polling loop: an "infinite" loop (controlled by a call to the IsProcessorStopping method) consisting, for example, of these general steps:
- retrieval of items to be processed;
- for each detected item, start of a processing task, possibly managing any error that can arise;
- archive or set as "processed" the processed items;
- pause a while (depending on the polling interval specific of the module) before the next iteration. The pause period should not be implemented with a simple Thread.Sleep() call but with a call to Util.PollingSleep(). This PEprocessorLib method executes a "quantized" Sleep (made of subintervals of a duration defined by the PollingSubIntervalsDuration setting) in order to frequently listen to possible STOP commands coming from the engine; basically, the IsProcessorStopping method is called internally to check if a STOP command has been issued while the module was waiting for the next iteration: if this happens, the pause is terminated prematurely. This ensures the "graceful" module shutdown being observed as soon as possible.
- final operations executed when exiting the main polling loop: the execution goes out of the main polling loop when a graceful stop has been requested by the engine; after stopping its current work (i.e. after exiting the main polling loop and just before completely terminating the execution of the MainTask method), the last operation performed by the processor is to signal back to the engine its graceful termination through a call to the Util.SetStoppingProcessor method (this method simply removes from the StoppingProcessors table the entry pertaining to the module itself).
For example, a processor module that actually implements a processing task related to FTP-incoming or file system feeds, the polling loop steps would become something including the following:
- inspection of the monitored folder (that is: the incoming folder for feeds posted by FTP);
- for each detected file, start of the processing task, eventually managing any error that can arise;
- move the processed file away from the "incoming feed folder" (feeds can be grouped in dedicated folders, depending on the success/failure of the processing itself and on some archiving rules);
- pause a while before the next iteration.
You can find two examples of a PE module skeleton (one for C# and one for VB.NET) in the downloadable code (see the projects "PEskeletonProcessorCS" and "PEskeletonProcessorVB" in the Visual Studio solution).
PE module configuration
Each PE processor module has its own set of configuration settings, hosted in a "private" configuration XML file. The name of the configuration file of each PE processor must be the same of the processor module itself (for example: MyProcessor.config will host the configuration settings for the MyProcessor.DLL module, that implements the module as a class again named MyProcessor in the MyProcessor namespace).
Configuration files for PE modules make use of a standard .NET "AppSettings" section, containing key-value pairs as in this example:
<configuration><br /> <appSettings><br /> <add key="PollingInterval" value="123" /><br /> <add key="ConnectionString" value="server=..." /><br /> </appSettings><br /> </configuration>
Of course, if you run the same processor in different environments (such as development, integration, production), each environment will probably have its own set of configurations, so you will have to be careful while deploying your processor’s files from an environment to another.
Module's configuration settings
The set of settings contained in the "private" configuration file may obviously vary for each PE processor module, depending on the business logic it implements. It normally includes:
- a configuration key (traditionally and conventionally named "PollingInterval"), which defines the extent of the pause between the iterations of the processor's main polling loop;
- configuration keys for accessing network shares, databases, web services, FTP servers, SMTP servers;
- configuration keys for switching between test/debug mode and normal/production mode;
- configuration keys for tuning the verbosity of logging and warnings messages;
- configuration keys to force specific behaviors in special situations;
Reading module's configuration
Reading a module's configuration setting by code is very easy: simply use the Util.ReadSetting method specifying the name of the setting and the configuration file name. Keep in mind that ReadSetting always returns a string type:
string ConfFile = "ProcessorName.config";<br /> string ConnectionString;<br /> ConnectionString = Util.ReadSetting("ConnectionString", ConfFile);<br /> int PollingInterval;<br /> PollingInterval = int.Parse(Util.ReadSetting("PollingInterval", ConfFile));
As stated before, the ReadSetting method is typically used at the very beginning of the MainTask method, before and out of the main polling loop. So, they are read once and remain "active" for the whole life of the module execution (until, let's say, a STOP command is issued). Then, if you need a change on those settings, you'll have to restart the processor in order to force it to read the new values.
Depending on the specific processor logic, some settings could need instead to be read "on the fly", with no need of restarting the module, so providing a "hot configuration change" feature. To accomplish this, you simply have to move the code responsible of reading the settings (that is, the calls to ReadSetting for the specific keys you need to be changeable "on the fly") inside the processor's main polling loop. This way, the module will inspect and read the configuration file upon each polling loop iteration.
Logging inside a module
Currently, to write rows in the ProcessorLog table, the PELog class can be used. It exposes the following methods:
Public Sub LogMsg(ByVal LogCategory As LogEntryCategory, ByVal LogSource As String, ByVal LogType As LogEntryType, ByVal ErrorDescription As String)
Public Sub LogMsg(ByVal LogSource As String, ByVal LogType As LogEntryType, ByVal ErrorDescription As String)
The LogCategory parameter is used to specify the type of activity logged:
- ProcessorActivity denotes activity performed by a PE module
- EngineActivity denotes internal activity of the PE engine or processor's activity strictly related to the engine (for example, the "gracefully stopped" message emitted by a module upon a STOP command received by the engine)
- EngineCommand denotes internal processing of a command performed by the PE engine (do not use in the processor’s code)
The LogSource parameter is to be set with the name of the calling context (typically it's the name of the PE processor module that is currently writing the log entry).
The LogType parameter is used to specify the severity of the error logged:
- InfoMessage: informational message
- WarningMessage: warning message
- ErrorMessage: operational error
The ErrorDescription parameter can be used to record everything is useful to detail the nature and the source of the error. It can even be populated with a structured XML document describing all the details referred to the reported error.
The second overload has a default LogCategory = ProcessorActivity: you should use it in most cases while developing a custom processor module code.
Under some conditions, you'd like to write an error log not to the database but to the Windows Event Log. This happens for some critical errors or when the PE database is not reachable. The Util static class exposes an API to do that:
Public Shared Sub LogMsgEventLog(ByVal PEmoduleName As String, ByVal errorMesssage As String, ByVal errorSource As String, ByVal StackTrace As String)
Public Shared Sub LogMsgEventLog(ByVal PEmoduleName As String, ByVal errorMessage As String)
Logging through the PEtraceListener
Sometimes, the business logic used by a PE processor module has to be implemented in an external class packaged in an external assembly, and this class (for various reasons) must remain as independent as possible from the class implementing the IPEprocessor interface and from the PEprocessorLib library. If this kind of decoupling is needed, the logging inside this class can be done through the PEtraceListener, a trace listener implemented as a wrapper around the PELog class.
In order to use this alternative way for logging, do the following:
- inside the class implementing the IPEprocessor interface:
- instantiate (as usual) the PELog class;
- enlist the PEtraceListener as a new active trace listener, providing the PELog instance just created
PELog MyLog;<br /> ...<br /> MyLog = new PELog(mServiceInstanceName);<br /> System.Diagnostics.Trace.Listeners.Add(new PEtraceListener(MyLog, mProcessorModuleInstanceName));
- inside the referenced class that need to execute decoupled logging, emit log entries for information messages, warning messages and error messages respectively in this way:
System.Diagnostics.Trace.TraceInformation("Info message");<br /> System.Diagnostics.Trace.TraceWarning("Warning message");<br /> System.Diagnostics.Trace.TraceError("Error message");
Modules development guidelines
Some general guidelines for PE modules development:
- Make sure that your module can't abruptly end its execution due to an unexpected unhandled exception. The simplest way to do this is to protect the highest-level code execution (the MainTask entry point) with a try/catch block. Don't forget to log the unexpected exception in that try/catch block.
- Don't put the PollingSleep method call inside a finally block. This could prevent the correct module termination (also in case of STOP command timeout).
- Provide various levels of logging, with different configurable verbosity.
- Promote "hot configuration changes" support to minimize module's restart.
- Log all the managed exceptions to improve troubleshooting.
- Tune carefully the module's PollingInterval and the related engine's settings (CommandProcessorPollingInterval, STOPcommandTimeout, and so on).
Non-iterative modules are a special case of PE processor module. They don't contain the main polling loop, but just contain some actions to be carried on when the MainTask is invoked. They don't stay alive indefinitely, but when are started (during the engine startup or upon a START command), they do their task and then terminate normally.
This approach could be useful for activities to be executed only once at the engine startup, or for activities to be triggered by a START command, possibly invoked by external application interacting with the CommandQueue table. You can also think to schedule a SQL Server job that issues a START command for those modules at a specific schedule.
Debugging the service
The PEprocessorEngine executable (PEprocessorEngine.exe) is normally to be installed as a Windows service (with the .NET standard tool
installutil.exe) in order to be run. But it can be run also as a standard command line executable, just executing it while specifying the “
/local” parameter on the command line. This way, you can run it and stop it without actually installing it as a Windows service. If you configure the engine to load and run your custom processor, then (by using the “
/local” parameter) you will be able to run the engine and attach the Visual Studio debugger to the running process, in order to execute a step-by-step debugging of your processor’s code.
If you have a Visual Studio solution including both the projects of the engine and of the processor you’re developing (as the solution included in the dowloadable material), the debugging session will be even easier: you just need to set the “/local” parameter as a “Command line parameter” in the “Start Options” of the PEprocessorEngine Visual Studio project property page:
This allows the execution and the step-by-step debugging of the engine (and of the modules) directly inside the Visual Studio IDE, without the need of attaching the debugger to the running service process.
When debugging the service compiled as a Console Application, remember that the DLL assembly and the config file pertaining to the module you want to debug must be copied (with the PDB file) in the same directory where the engine's executable is running.
Points of Interest
The most interesting points I found in the development of this engine reside:
- in the management of independent threads for different modules;
- in the way modules (=assemblies) are loaded in memory in specific AppDomains to be easily unloaded to support the "hot updates" feature.
The downloadable materials is made of a Visual Studio 2012 solution including:
- the project of the PEprocessorEngine;
- the project of the PEprocessorLib;
- the SQL database creation script;
- two sample projects (one for C# and one for VB.NET) sketching a typical PE processor module skeleton.
This paragraph is aimed to list the possible improvements and enhancements to the processor engine system, resulting from discussions and suggestions collected after the initial publication of the article. Many thanks to all the colleagues and friends contributing: some of the suggested new features will be certainly included in future versions.
Dynamic configuration for autostarting modules
In the initial version, the list of modules to be automatically started when the engine starts is retrieved from the "PEprocessor" keys found in PEprocessorEngine.exe.config. If they were stored on (and retrieved from) a database table in the PE support database instead, they could be more easily edited (even by 3rd party applications) without the need of modifying the engine main configuration file.
Stop command in-memory
In the initial version, the "messaging" between the engine and the modules to be stopped as a result of a received STOP command is based on writing/retrieving records on the StoppingProcessors control table. The engine could instead implement a in-memory communication mechanism able to work cross-threads and cross-AppDomain. This would eliminate the polling on the StoppingProcessors table.
Processor versioning information
The initial version of the engine, while executing commands like LISTPROC, reports the version of an installed module in the form of the detected assembly version. It could be useful to have an option to report the file version or the assembly version.
Last errors track
It would be usefult to keep track of the last errors occurred inside the engine and the processors during their execution. Of course, those errors are also logged in the ProcessorLog table; but – being the ProcessorLog table used by all the processor modules and being a central repository for errors, warnings and informational messages – it is not always easy to focus on errors of specific severity generated by a specific processor module, especially when the ProcessorLog table is quickly populated with messages of lower severity and/or generated by other processors. And these are the situations when the "track last errors" feature would come in handy.
Heart beat feature
It would be useful if the engine was able to produce a sort of heart-beat, that is: a recurring signal of its own operational status. This could be implemented as a simple timestamp stored in the PE database and updated on a recurring basis (with a configurable frequency). This would allow any third party monitoring tool to check the running status of the engine (and, possibly, also of each loaded module), and to send proper alerts in case of failure.
It would be handy to provide a control panel for the engine, as a Windows Forms desktop application or as an ASP.NET web application, that allows the user to:
- inspect and monitor the entries of the logging tables hosted in the PE support database;
- inspect the running/stopped status of the PE modules;
- issue start/stop commands to the PE modules.
Granularity in controlling the engine
If an external application is developed to provide users with some functionalities for controlling the engine (sending commands to it, starting/stopping modules, inspecting the log), currently there is no way to differentiate and address a granular control on the ability of a user to operate on a specific PE processor. For example: some users must have access only to the log entries of some processors, other users must have the ability to start/stop only specific modules. It would be nice to have the ability to do this.
Modules' scheduled activation
It would be useful to be able to create processor modules whose execution is not simply based on a polling mechanism, but can be triggered by a "point-in-time" execution booking, that could be pre-determined or set by an external application (possibly by another processor).
These are some scenarios where you could need an external application capable of "booking" executions for a target processor:
- an event is defined in a calendar for the future, and some automatic actions have to occur at a predefined time distance before or after the event's date/time;
- an event is defined in a calendar for the future, and some automatic processing have to be performed in a period of time related to the event's date/time;
- the execution of a processor's work has to be scheduled on a recurring basis (hourly, daily, weekly, monthly);
- the execution of a processor's work has to be scheduled on a recurring basis and this scheduling must be under the control of an operator.
It would be handy if an external application could "book" the execution of a module just appending a row in database table, indicating the target processor and the date/time or the period of execution. A simple API should be exposed in order to let the target processor react properly to these external "bookings" and perform its executions accordingly.
Some flexibility should be included in that API to manage situations of possibly missed schedules.
GUI for scheduled activations
If modules' scheduled activations are implemented as described in the previous paragraph (having a new table in the support database designed to host the planned scheduled executions), then SQL Server Agent could be used as a scheduling engine for booking the modules' executions. In this scenario, the SQLjobSchedulerGUI could be leveraged as a GUI for controlling those schedules without the need of SQL Server Management Studio (see http://www.codeproject.com/Articles/376731/A-scheduler-GUI-for-SQL-Server-Agent for details).
Porting the PEprocessorEngine to Microsoft Azure
It would be great to have the PEprocessorEngine able to run as a cloud service on Microsoft Azure. This would give multiple benefits. In fact, a typical Azure Worker Role is designed to execute just one task: if you need to perform more tasks and you want to package them in a single Worker Role, you need to combine these tasks in a unique code block, completely loosing the independence of each task and the modularity of the code. This way, you are forced to stop all tasks (being implemented in a single Worker Role) if you need to modify only one of them.
Having a sort of PEprocessorEngine on Azure would give you the ability of creating independent and modular blocks of code, each implementing a specific task, and the ability of loading and running them independently inside the boundaries of the same physical Azure Worker Role.
Moreover, having the code implementing the various tasks developed in the shape of PE processor modules running in an Azure-hosted PEprocessorEngine:
- we would gain more agility in the deployment and configuration activities;
- we could have the benefits of the (suggested) scheduling facilities of PEprocessorEngine;
- we could leverage the Azure Worker Role scale-out nature, useful in heavy-load scenarios.
Azure Worker Role Engine
I decided to follow the idea of creating a version of the PEprocessorEngine targeted to Microsoft Azure, in order to have on the Microsoft cloud the benefits described above in terms of:
- ability to have modular and independent tasks updated, loaded and run independently on the same Azure Worker Role;
- agility in the deployment and configuration activities (included hot updates);
- costs reduction, due to the reduced need of Worker Role instances.
This solution makes use of SQL Azure as a supporting database and leverages Azure BLOB containers as the place where the processors' assemblies are dropped by the user and loaded by the engine.
I named this new Azure version of the PEprocessorEngine as "Azure Worker Role Engine", and it is currently a personal development project (feel free to contact me if interested in further details, even if I'll publish soon on the web more info about that "AWRE - Azure Worker Role Engine").
February 27th 2015: article creation with first version of the engine
March 2nd 2015: added "Future enhancements" section
March 23rd 2015: future enhancements updated
April 20th 2015: future enhancements updated
July 20th 2015: future enhancements updated
August 11th 2015: Azure Worker Role Engine paragraph added
September 8th 2015: News on AWRE development