Oct 07

I’m happy to say that we’ve just tied a bow around the latest release of MassTransit. Release 0.4 includes a number of new features and some tweaks to the internals as well. I’m going to describe a few of those features below, but you can grab the latest from the trunk or download the 0.4 release.

Building MassTransit

Since Visual Studio 2008 has been out for almost a year, it is now required to open the updated solution for MassTransit. In the main folder, the MassTransit-2008.sln is the one to use to build and run the unit tests. Many of the samples solutions are also 2008 solutions. The assemblies, however, are still targeting the .NET 2.0 framework, making them usable on both 2005 and 2008 projects. With only the .NET 3.5 framework installed, you should be able to run the build.bat to build the project without Visual Studio (our CI server does this).

Timeout Service

To enable automated support for timeouts in sagas, a new timeout service is available. This is a general service that can be used to schedule timeouts for whatever purpose may be needed. To schedule a timeout, the application should publish a ScheduleTimeout message with the duration or time when a response should be sent. The application/service can then consume the TimeoutExpired message, which will be published by the timeout service when the timeout period expires.

Message Deferral Service

One of the scenarios I often find in our systems is the need to poll a remote resource to determine if an operation has completed. To support this behavior without custom code in each instance, a new message deferral service has been added. This can be used to defer the delivery of a message until a period of time expires. The deferral services leverages the timeout service for scheduling and we republish a message after that timeout expires.

For example, we have a CheckRemoteResponseStatus message in one of our systems. This is initially published after a request is submitted to a remote system and a remote transaction id is returned. The first time the consumer gets the message, it checks the remote system for a response. In most cases, the response is immediately available and the saga continues. However, sometimes the remote system is too busy to respond and returns a pending status. In this case, the same CheckRemoteResponseStatus message is published within a DeferMessage. The deferred message service handles that message and will republish the original CheckRemoteResponseStatus message when the timeout expires. The saga will then handle the message to see if a response is now available. The saga keeps track of how many times the remote status has been checked and uses a sliding interval that increases as the retry count increases. Eventually, the final retry results in a failed transaction and is handle appropriately.

The nice thing about this is there was no custom retry logic required, and a common timeout and message deferral service were used. There are likely other cases within the application that will benefit from this shared functionality.

Transactional Queue Support

With 0.4, the entire method of reading from the endpoints has been redesigned. Previously, a single receive thread was used to receive from the endpoint which then dispatched the message handling to the dispatcher inside the service bus. This has been redesigned to use the dispatcher threads to perform the actual receive from the endpoint, using a transaction (ala System.Transactions) to handle the message reception. This keeps the transaction to a single thread while at the same time allowing concurrent message reception.

The transaction carries over into actions that are part of the message consumer. If a database update is part of the consumer, that database update can cause the entire message to rollback if it fails. If any exception is thrown, the entire reception of the message, any additional database operations, new messages sent, etc. will all be rolled back with the transaction.

Performance Improvements

Dru spent some time in NYC with Ayende Rahien reviewing the MT source code and Oren recommended changing from using locks to ReaderWriterLocks to improve concurrency. The changes in the threading system, along with the elimination of a lot of locking in favor of reader/writer locks has nearly doubled the throughput of messages when using a multi-core system using MSMQ. There have been a number of other internal tweaks as well to improve the concurrency of the bus dispatcher.

Control Bus

To enable competing consumer in a publish/subscribe environment, the control messages need to be on a separate bus from the data messages. To allow multiple services to compete against a single data channel (single MSMQ) in order to load balance and handle failure scenarios, the services cannot compete on the control messages such as subscriptions. The subscription client has been tweaked to allow it to operate on a separate bus from the data bus, at the same time notifying the subscription service of messages handled by the local endpoints of the service.

It’s easy to setup a single service that consumes messages from multiple buses (which in turn each have a specific endpoint being serviced). When a component is created to consume a message, the specific bus/endpoint that received the message is injected into the component (via setter injection) so that any subsequent messages can be published to the appropriate bus.

Health Service

The health service has been added making it easy to monitor endpoints and identify when an endpoint goes down. Periodic heartbeats are sent to the service and when a heartbeat hasn’t been received in a while, it marks that endpoint as down and attempts to directly ping it to get a response. The heartbeats can be subscribed, so a monitoring tool can keep track of which endpoints are there and what they are handling.

Configuration Model

To make it easier to use the bus in different containers, a new configuration model has been added to build and configure a service bus instance. This will ultimately result in moving a lot of the code used by build a service bus out of the container-specific facilities (such as the Windsor container).

Host Improvements

The ability to create and deploy Windows services has gotten easier with the updates to the Host assembly in MassTransit. With only a few files to define the lifecycle of a service, it is easy to get the ability to run, test, install and deploy a service. This includes services that are using the service bus. There is little to no coupling between the host and service bus, making it usable for a variety of purposes.

Learning MassTransit

A lot of requests have come for information on how to learn to use MassTransit. During Tulsa TechFest this week, we’re going to record our presentation and make it available online within a few days. This should give at least some introduction on how to use MassTransit (the presentation is mainly on distributed architecture, but we’re using MT for the demo bits). We’re also talking about doing a couple of podcasts on how to use it as well. Depending upon how that goes, we’ll try to do a couple of screencasts on “creating your first project with MT.”

The best way to discover how to use the code is to review the samples. The WinFormSample gives an overall example of how a variety of features are used. The HeavyLoad shows how many of the pieces work as well. The samples folder has a few others that demonstrate how to use MT in other scenarios.

So, check out the new release and give us some feedback on how the new features are working. We’ve already got a few backlog items that we’re slating for 0.5 based on some other contexts that have come up in our applications. Feel free to post on the message group or send either Dru or I an e-mail or tweet if you have any questions.

Oct 01

With the latest trunk of MassTransit, we are using Visual Studio 2008 for development. The resulting assemblies are still targeting .NET 2.0, allowing them to be used with Visual Studio 2005. The reason is that some of the newer features like WCF support were built with 3.5, along with the use of lambda expressions and other features in the source files.

My understanding is that the command-line build should still work even if you don’t have Visual Studio 2008 as long as you have installed the Framework 3.5 via Windows Update.

We are using the assemblies built from 2008 in our development (which is still 2005) without any issues targeting .NET 2.0 and have recently upgraded our development trunk where we are using MassTransit to the latest build (previously we were on 0.1x). It was an interesting upgrade with a few changes required, but nothing too painful. Early testing shows everything is working as expected and I’m looking forward to taking advantage of some of the new features supported by the latest release.

Sep 24

In two weeks, the 2008 installment of Tulsa TechFest will be upon us. For two days, Tulsa is going to unleash an impressive array of sessions on all aspects of IT, security, and software development. As I review the broad list of presenters I can’t help but see conflicting sessions where I’m going to have to make some tough choices.

If you are going to be anywhere near the Tulsa area and can manage to slip away from work for a couple of days I highly recommend making an appearance. The breadth of learning opportunities at the unbelievable price ($2/day) make this an incredible way to learn some new skills and sharpen your existing ones.

I will be presenting in two sessions this year. The first session will be on building distributed application using MassTransit (co-hosted by Dru Sellers) and other open-source frameworks for .NET. This is doing to be a deep view on how to build loosely-coupled systems on top of a messaging service (in this case, MSMQ). Advanced topics include asynchronous messaging and sagas (long-lived transactions).

The second session will be an introduction to iPhone development. I’m not a seasoned expert here, but I’m impressed with the platform provided by Apple (Xcode) free of charge for building applications for Mac OS X and the iPhone. This introduction will cover the tools and application structure for building iPhone applications in Objective-C.

If you happen to see me there, feel free to stop me and say hello.

Sep 17

VMware Fusion 2.0 was released yesterday and I was anxious to upgrade. Rather than feel the full pain myself, I let brewbie go first. He got it setup and installed and said all was good, so I figured I’d give it a shot.

I was in a VM working on some new code that I had neither saved to disk nor commited to Subversion. I figured I’d live on the edge and just suspend the VM so I could upgrade Fusion. Once Fusion had exited, I downloaded the update and fired up the installer. Less than a minute later version 2.0 was installed and ready to go (no reboot required). I clicked the icon for my 2003 Server VM and it fired up just a few seconds, picking up right where I left off. Needless to say, I was very impressed.

The release notes said that previous VMs should update the VMware Tools to take advantage of the new features available in 2.0 so I went ahead and restarted Windows 2003 (1st time). Once it was back up, the new tools package installed and prompted for another reboot (2nd time). The reboots were amazingly quick and I was back up and running in no time.

Since it was all up and running, I started to configure the new features in Fusion 2.0. There is a new keyboard mapping feature so you can map certain key combinations to work within your VM. It works pretty slick, making it possible finally map a key for INSERT. There is also a new feature to make it so that clicking mailto links in the VM opens the Compose Mail window in Mac Mail. VERY COOL! I was less impressed with the browser linking since it changed my default browser to EverNote (WTF?). I changed it back to Safari and then disabled the feature.

After the short interruption, I felt that it was time to create a new VM for Windows Server 2008. I was following the guide on the Windows 2008 Workstation site to build a workstation-grade installation of Server 2008. Since VMware 2.0 supports things like Shader Model 2.0 with 3D graphics, I wanted to see it work. It took a couple of hours to get everything setup and configured, but the end result was a clean install of 2008 server configured for a workstation environment.

I then installed Steam, and downloaded a fresh copy of TeamFortress 2. I fired it up and it failed. It turns out that the tools by default only enable a certain level of hardware acceleration. I went to the advanced troubleshooting tab and cranked up the acceleration. I could now run everything, including Microsoft PhotoSynth and TeamFortress 2. The frame rate was pretty amazing considering it was running in a VM — seriously impressive actually. I experienced a lot of sound breakup when IO was being performed, so there is still some tweaking to do there I suppose.

Once play time was over, I installed Visual Studio 2008, TortoiseSVN, VisualSVN, Resharper, and updated. I pulled down the latest trunk of MassTransit and built it. The build took 16 seconds, compared to 21 seconds on my 2003 VM. However, when I ran the load test I was surprised to see that MSMQ performance was about half of what it was with 2003 Server. I ran the test a couple of times with the same result. I’m not sure what is at work here, but it seems like MSMQ4 has some different performance numbers than MSMQ3. I’ll do some additional tests and post more on this later. Maybe MSMQ was slowed down to make WCF look better (joking, of course).

In the end, the upgrade to 2.0 was painless - completely painless infact. I highly recommend it to take advantage of all the new features.

Sep 03

Download MassTransit 0.3 Here

Last night, Dru and I released version 0.3 of MassTransit. This is a pretty hefty milestone release with a lot of new functionality. I’m going to run down a list of some of the new features that made it into this version.

  • Fault Messages
    If a Consumer throws an exception, a Fault<T> message will be published. The Fault envelope will contain the exception that was thrown, along with the message that caused the exception. 
  • Loopback Endpoint
    To support in-process communication, a loopback endpoint was created. This is entirely in memory and does not have any persistence support. But it makes it easy to test features of the system without requiring MSMQ and also makes for some interesting application design choices for dispatching work.
  • Thread Pool Configuration
    As part of the Windsor facility, the thread pool size for dispatching messages can be configured.
  • Batch Message Threading
    Dispatching batch messages using Batch<T> now uses a separate thread for each batch being consumed instead of using one of the dispatcher threads. This should avoid thread starvation under heavy message loads.
  • Message Groups
    To allow messages to be grouped and sent as a single message (a coarse-grained style of messaging outlined in Fowler’s PoEAA), a new container-style MessageGroup allows a set of messages to be combined into one message and delivered. The consumer can then choose to handle the messages as a group (for unit-of-work style operations) or to split the messages out and dispatch them individually.
  • Saga Support
    Long-lived transactions (Saga) are now supported and can be built using MassTransit. You can read all about that on my previous blog entry
  • Grid Support
    New support for distributed tasks via MassTransit has been added. This makes it easy to create distributed applications that process large amounts of data. A good sample is in the works, but presently it uses some fairly proprietary and confidential data so I can’t just hand that out as a sample. The structure makes it fairly easy to write a MapReduce style log analyzer though, if that gives you any ideas.
  • Internal Changes
    A number of internal, under-the-hood changes took place. The dispatchers were rewritten to be insulated from each bus instance to enable a shared type information cache. There were also some tweaks in how dispatchers are plugged into the pipeline to make it easier to open the extensibility model for new types of dispatchers (such as the saga dispatcher that was just added).
  • WinForm Sample
    A new sample showing how the use the service bus in a WinForm application was built. This includes a nice visual SubscriptionManager GUI to help see the subscriptions as they are added and removed by an application.
  • Host Changes
    To make it easier to build Windows services containing message handlers, the Host project was updated with a cleaner style. The PubSub and WinFormSample/SubscriptionManagerGUI have been converted to this new structure and it was pretty easy for me to add (having never seen it before).

Those are the big hits, I’m sure there are some minor things I’ve missed. We’ll try to update the road map this week to give a picture of where we are and what we have left to get done. It’s likely that 0.3 will get a few days of soak time before we gear up for the next release anyway.

It’s important to note that all development is now being done in VS2008 (using .NET 3.5 for the test projects) but the main assemblies are still .NET 2.0 targets. It’s like that this will change in the next version or two considering most of the projects are trending towards the new platform.

Aug 28

One of the first applications we built with MassTransit provides messaging for a long-running transaction started by an application submitting a request. The request is formatted into a X12 envelope and sent to a web service. An intermediate response is returned (a X12 997) with a correlation identifier for the request. Another web service is polled for the response, which can be the result or an indication that the request is still pending. When the response is received, the X12 document is translated and stored in the database. Finally, the user is notified that the transaction is complete and the result is displayed.

As work proceeded on this application, I started to recognize the need for something to coordinate the different steps in a transaction involving multiple loosely-coupled services. Due to the duration of these transactions (the example above can take anywhere from three to sixty seconds), it is unreasonable to keep a single System.Transactions style transaction open the entire time. I started researching how others approached the problem and found a couple of articles that helped. After reading Sagas by Hector Garcaa-Molrna and Kenneth Salem (© 1987 ACM, PDF) and the chapter on sagas in the upcoming book Practical SOA by Arnon Rotem-Gal-Oz (PDF), I started to think about how this could be implemented within MassTransit.

I should note that NServiceBus (another open source service bus) also supports sagas, but I purposely avoiding taking a look at how Udi Dahan implemented them. Once saga support in MassTransit is complete I plan to review the source for NServiceBus to see how the implementations differ. I spoke with Udi at ALT.NET Seattle and his writing has been both educational and inspirational. A lot of great discussions in the NServiceBus mailing list have been an excellent resource as well.

So after a few weeks of trying to flesh out the structure (using TDD, of course), I finally arrived at what I think will be a highly usable infrastructure for handling sagas. In the project MassTransit.Saga.Tests, I’ve created a test that simulates a user registering for a web site. The class for the registration is shown below.


public class RegisterUserSaga :
	InitiatedBy< RegisterUser >,
	Orchestrates< UserVerificationEmailSent >,
	Orchestrates< UserValidated >,
	ISaga< RegisterUserSaga >
{
	private string _displayName;
	private string _email;
	private string _password;
	private string _username;

	public RegisterUserSaga(Guid correlationId)
	{
		CorrelationId = correlationId;
	}

	public Guid CorrelationId { get; private set; }
	public IObjectBuilder Builder { get; set; }
	public IServiceBus Bus { get; set; }
	public Action< RegisterUserSaga > Save { get; set; }

	public void Consume(RegisterUser message)
	{
		_displayName = message.DisplayName;
		_username = message.Username;
		_password = message.Password;
		_email = message.Email;

		Save(this);
		Bus.Publish(new SendUserVerificationEmail(CorrelationId, _email));
	}

	public void Consume(UserVerificationEmailSent message)
	{
		// once the verification e-mail has been sent, we allow 24 hours to pass before we
		// remove this transaction from the registration queue
		Bus.Publish(new UserRegistrationPending(CorrelationId));
		Bus.Publish(new UpdateSagaTimeout(CorrelationId, TimeSpan.FromHours(24)));
	}

	public void Consume(UserValidated message)
	{
		// at this point, the user has clicked the link in the validation e-mail
		Bus.Publish(new UserRegistrationComplete(CorrelationId));
		Bus.Publish(new CompleteSaga(CorrelationId));
	}
}

At the top of the class, the messages consumed by the saga are specified. InitiatedBy indicates the message initiates a new instance of the saga. Orchestrates is for messages that are part of the saga once it has been initiated. All saga instances are identified by a Guid and all messages consumed by the saga should have the CorrelatedBy< Guid > interface. A saga must also implement the ISaga generic interface to allow certain properties to be set giving the saga instance access to the bus and the object builder.

Once the saga class is added to the bus (via the AddComponent method), any messages consumed by the saga will be dispatched to the saga instance. A generic ISagaRepository must also be registered in the container so that sagas can be persisted between messages. The saga dispatcher uses the repository to either load or create the instance of the saga. Since instances of the saga class are saved, the class can expect the members to also be persisted between messages allowing state to be retained.

There is still some work to be done, including a service to handle timeouts and retries. It will be up to the developer to handle any compensating actions that need to be taken in the case of a failure. Therefore, it is highly suggested that the saga also consume any Fault< T > messages that are published when a message consumer throws an exception — particularly if the consumer is not part of the saga (such as an application or domain service).

The code is currently in the trunk and slated to be part of the 0.3 release.

Aug 22

I was reading through the Xgrid documentation for OS X yesterday after reading an article on Integrating Xgrid Into Cocoa Applications. The article gave me some ideas and I decided to see what it would take to build a distributed processing system on top of MassTransit. The result is a new MassTransit.Grid namespace that includes support for building distributed task processing into an application. The following sections define the language used in the distributed task classes.

Distributed Tasks

A distributed task contains one or more subtasks that need to be processed concurrently across multiple systems. To create a distributed task, create a class that implements IDistributedTask. The input and output types for the subtasks must also be defined by the distributed task class.


public interface IDistributedTask< TTask , TInput, TOutput >
{
    int SubTaskCount { get; }
    TInput GetSubTaskInput(int subTaskId);
    void DeliverSubTaskOutput(int subTaskId, TOutput output);
    void NotifySubTaskException(int subTaskId, Exception ex);
    void WhenCompleted(Action<ttask> action);
}

Subtasks

A subtask is an individual unit of work within a distributed task. Each subtask should be completely standalone and not depend upon the completion of any other subtask within the distributedtask. There is no attempt to execute the subtasks within a distributed task in order. A subtask has specific input and output types, each of which are defined by a class (POCO style). These input types are used to determine which workers are used to process the subtasks.

DistributedTaskController

To insulate the application from the details of coordinating the subtasks, a generic DistributedTaskController is used. This class is built from the class that implements IDistributedTask, along with the input and output types. Once created, the application can call .Start() to being processing the distributed task. The controller performs any initial identification of workers that are available to process the subtasks, along with the coordination to ensure that workers are not overloaded.


public class DistributedTaskController< TTask , TInput, TOutput >

TTask is the class that implements IDistributedTask, TInput is the subtask input type, and TOutput is the subtask output type.

Workers

To make it easy to create workers to handle subtasks, a default worker implementation is available. This worker handles the coordination with the DistributedTaskController, along with the delegation of the messages to the actual subtask worker. For example, a worker that accepts a GenerateFileHash object and outputs a FileHashGenerated object would be setup as shown:


public class FileHashGenerator :
       ISubTaskWorker<  GenerateFileHash , FileHashGenerated >
   {
       public void ExecuteTask(GenerateFileHash input, Action< FileHashGenerated > output)
       {
           string path = input.Path;
 
           // do work here
 
           output(new FileHashGenerated());
       }
   }

The worker can then be added to the container for servers that will be processing the subtasks using:


_container.AddComponent<  FileHashGenerator >();
_bus.AddComponent< SubTaskWorker < FileHashGenerator, GenerateFileHash, FileHashGenerated > >();

This will register the SubTaskWorker for the worker as a message handler for the messages that are used on the transport to transfer the input and output data between the controller and the subtask workers.

Exception Handling

If an exception occurs in a subtask, the worker and controller leverage the built-in fault handling support of MassTransit to notify the distributed task that an exception has occurred. The controller will call the NotifySubTaskException method with the subTaskId and the exception that was thrown by the worker allowing the distributed task to determine the next course of action based on that failure. Options would include simply aborting the distributed task, fixing the input data and adding it to the end of the subtask list, or some other application-defined behavior.

Dynamically Adding Subtasks

To reduce the impact of setup time on the overall duration of a distributed task, it is not necessary to have all of the subtasks loaded before starting the distributed task. This also allows additional subtasks to be added based on the output from other subtasks. For example, a task to parse a remote file system may identify additional folders that need scanned for content. The distributed task could just add those folders to the end of the subtask list and they would be picked up by the controller. By allowing this, the distributed task is responsible for calling the delegate set by the controller to indicate that all of the subtasks have completed. The DistributedTaskController will then release any resources that were in use.

Sample in Unit Tests

A quick sample was built in the unit tests (MassTransit.Grid.Test) that shows an integer factoring service. The distributed task creates a bunch of very large integers and processes them as a distributed task between the workers that are available. Hopefully this demonstrates how the classes are hooked together since this was used to drive out the feature set.

Wrapping Up

This is just a brief introduction to the distributed processing capabilities that were added to MassTransit. There are likely some additional features to add that will hopefully be identified as the feature it put to use. Therefore, it is important to note that this feature is still in development and should go through some considerable testing before putting it into use in a production application. Any feedback is always welcome (including patches) so try it out!

Aug 17

When deploying an application, it is important to consider how the application performance will be monitored. For interactive applications, this may include measurements such as the time to load a page, how long it takes the page to be ready for user input, or even how much data is transferred to the client on each request. In the case of a service, it may be important to know the duration for individual transactions, a batch of transactions, or another unit of work.

The benefits of being able to measure the performance of an application are numerous. First, it helps to track performance over time as updates are made to the application. If a new version of the application “feels slower” to users, the developers would be able to compare the performance of the current version to the previous version. This will either identify where the system has degraded or (unlikely as it may be) show that the user is just mistaken.

Another benefit of knowing the performance criteria for an application related to service-level agreements. If the response time of an application can be measured, those metrics can be used to establish expected performance numbers to share with customers. For example, if a certain operation takes less than two seconds on 97% of all requests, customer expectations could be set using that information. Without accurate measurement, it would be impossible to answer that type of question.

An Example

An customer service application has a feature that submits user input to a remote service. When the user clicks submit, a request is built containing the user input and credentials retrieved from the customer configuration. The request is then sent to the remote service. When the response is received, the data returned is formatted for display and shown to the requesting user.
Within this example, there are several points that could be measured.

  • The overall duration of the request from the time the user clicks submit until the response is displayed could be tracked. This would be useful in tracking the overall user response time.
  • The time taken to retrieve the credentials from the configuration store could be tracked. This would help identify a slowdown in the database (or whatever type of storage is being used).
  • The elapsed time between sending the request to the remote service and receiving the response could be measured. This would help identify latency issues with the remote service.

All of these values could also be compared to each other to separate the total time for the operation into the amount of time for each individual operation.

Method Timer

To measure a method on a class, a stopwatch can be used to measure the elapsed time of the method. To make it easy, I created a class that encapsulates the functionality of the Stopwatch class (from the System.Diagnostics namespace). My class implements IDisposable, allowing it to be automatically disposed at the end of the function with a using {…} block.


public string LoadUpdates(string url, string name)
{
    using (AutoFunctionTimer timer = new AutoFunctionTimer(url, x => _log.Info(x)))
    {
        string updatePage;
        using (timer.Mark())
            updatePage = new PageLoader(url).GetBody();
        using (timer.Mark())
            return GetUpdates(name, updatePage);
    }
}

The delegate specified on the constructor is called when the stopwatch is disposed and the timing measurements are complete. The delegate is passed the stopwatch so that it can be output to a log file for processing by the measurement code. The string output includes the time the method started (in UTC), the duration of the method, and the string passed to the constructor.

Timer Sections

Sections make it possible to measure individual operations within a method. The time for each section can be logged, making it easy to identify the largest time consumers in a method. In the code example above, the timer.Mark method is used to create a section. This section time is then output after the total time, allowing it to be compared to the duration of the method. If multiple sections are created, they are output in creation order for consistency.

The code for the entire class is shown below.


namespace MyNamespace.Core
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Text;

    public class AutoFunctionTimer : IDisposable
    {
        private readonly Action<string> _action;
        private readonly string _description;
        private readonly List<stopwatch> _marks = new List</stopwatch><stopwatch>(10);
        private readonly Stopwatch _stopwatch;
        private DateTime _started;

        public AutoFunctionTimer(string description, Action<string> action)
        {
            _description = description;
            _action = action;
            _started = DateTime.UtcNow;

            _stopwatch = Stopwatch.StartNew();
        }

        public void Dispose()
        {
            _stopwatch.Stop();

            _action(ToString());
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder(256);

            sb.Append(_started.ToString("yyyy-MM-dd HH:mm:ss ")).Append(_stopwatch.ElapsedMilliseconds);

            foreach (Stopwatch mark in _marks)
            {
                sb.Append(' ').Append(mark.ElapsedMilliseconds);
            }

            if (!string.IsNullOrEmpty(_description))
                sb.Append(" ").Append(_description);

            return sb.ToString();
        }

        public CheckPoint Mark()
        {
            Stopwatch watch = Stopwatch.StartNew();
            _marks.Add(watch);

            CheckPoint point = new CheckPoint(watch);

            return point;
        }

        public class CheckPoint : IDisposable
        {
            private readonly Stopwatch _stopwatch;

            public CheckPoint(Stopwatch stopwatch)
            {
                _stopwatch = stopwatch;
            }

            public void Dispose()
            {
                _stopwatch.Stop();
            }
        }
    }
}

Logging Performance Metrics

For measuring performance within a single application or service, I recommend using log4net to output performance metrics to a file. It’s easy to setup and configure post-deployment, making it an obvious choice. Additionally, each server should log to a directory on the local drive. Not only does this eliminate the need for a network share and the associated permissions, but it makes it less likely that network latency will impact system performance due to logging.

For distributed applications, the story changes quite a bit. Because of the nature of distributed operations, I’ll cover them in another post.