How to Write a Robust TFS Server Plugin, with Job Extensions

29 ביולי 2011

no comments

A client of mine came to me with the following problem: She has several server plugins that manipulate work items, one which I wrote, and another that was downloaded from codeplex. When applied to one work item, e.g. via the Team Explorer, everything works fine. However, when applying to a bulk of work items (via Excel publishing, for example), the process freezes for several minutes, until it completes and is only then freed.

I came up with the following solution: A server plugin that queues a job for the TFS Job Agent, and a Job Extension to handle the work, at its leisure.

The Server Plugin

A server plugin runs before and/or after an event, depending on the Notification Type you choose to use. It does so synchronously, so it doesn’t free the resource or the process until it completes. Therefore, like any other event you write, you will want to free it as soon as possible, and hand off any long-running work to a separate thread or process.

The template for writing a server plugin is rather  simple and straight forward. This is the template I use.

First you have to add some references, and “using” statements. Most blog posts I read don’t mention them, which is a bit of a pain, in my opinion, so I’ll add them here:

image

  1. using System;
  2. using System.IO;
  3. using System.Xml;
  4. using Microsoft.TeamFoundation.Common;
  5. using Microsoft.TeamFoundation.Framework.Server;
  6. using Microsoft.TeamFoundation.WorkItemTracking.Server;

Next, we need to take care of a few properties. This is simple enough, and rather standard:

  1. namespace ServerPlugins
  2. {
  3.     public class SampleWorkItemChangedServerPlugin : ISubscriber
  4.     {
  5.         #region Implementation of ISubscriber
  6.  
  7.         public Type[] SubscribedTypes()
  8.         {
  9.             return new[]{typeof(WorkItemChangedEvent)}; // For Example
  10.         }
  11.  
  12.         public string Name
  13.         {
  14.             get { return "Sample WI Server Plugin"; }
  15.         }
  16.  
  17.         public SubscriberPriority Priority
  18.         {
  19.             get { return SubscriberPriority.Low; }
  20.         }

Finally, we’ve got the Process Event callback method – this is where we handle the event. We start by making sure that we’re handling the right event. That’s right: Every ISubscriber class gets called for every event. You want to make sure as soon as possible that you’re dealing with the right type of event:

  1. public EventNotificationStatus ProcessEvent(TeamFoundationRequestContext requestContext, NotificationType notificationType, object notificationEventArgs, out int statusCode, out string statusMessage, out ExceptionPropertyCollection properties)
  2. {
  3.     statusCode = 0;
  4.     properties = null;
  5.     statusMessage = string.Empty;
  6.  
  7.     // Check whether the notification is what we are waiting for
  8.     if (notificationType != NotificationType.DecisionPoint)
  9.     {
  10.         return EventNotificationStatus.ActionPermitted;
  11.     }
  12.     var notification = notificationEventArgs as WorkItemChangedEvent;
  13.     if (notification == null)
  14.     {
  15.         return EventNotificationStatus.ActionPermitted;
  16.     }

Note that since we’re queuing a job to be handled asynchronously our notification type will always be a Notification, rather than a DecisionPoint. It doesn’t make much sense to queue something that must happen before the original event occurs. You do see that, right?

I prefer to make multiple return points in my code to reduce cyclamatic complexity, rather than nesting if statements. It is purely a matter of style, but I find code easier to read if you can separate control from logic.

Next comes the interesting part. This is where you would normally place your logic. However, in order to be able to return the control as soon as possible, you will instead queue a job to handle your logic, as follows:

  1. try
  2. {
  3.     // Prepare the data we need for handling the notification in a job
  4.     var workItemId = notification.CoreFields.IntegerFields[0].NewValue;
  5.     var reader = XmlReader.Create(new StringReader("<WorkItem>" + workItemId.ToString() + "</WorkItem>"));
  6.     var xmlData = new XmlDocument().ReadNode(reader);
  7.  
  8.     // Handle the notification by queueing the information we need for a job
  9.     var jobService = requestContext.GetService<TeamFoundationJobService>();
  10.     jobService.QueueOneTimeJob(requestContext, "Sample TFS Job", "JobExtentions.SampleWorkItemJob", xmlData,
  11.                                false);
  12. }

In this code block we do two things: First, in lines 4-6, we prepare the data we want to pass to the job. The QueueOneTimeJob method accepts an XmlNode of data that can be passed on to the job. For my example, as I am listening to the WorkItemChangedEvent, I will pass the Work Item ID, in an XmlElement. In lines 9-11, we queue a job.

Two things need to be mentioned. First is that since we are going to queue each job with different data, we use the QueueOneTimeJob. It is used when we don’t have a known job that we wish to activate (that would require knowing its GUID), therefore each call creates a new job, and two successive calls won’t merge accidentally, causing one job to get lost. Once again, this is an implementation issue that needs to be circumvented – and luckily can be done so easily.

The other thing, is that you need to pass it the domain name and  class of the job extension (see line 10). This tells the TFS Job Agent which extension to activate for each job. Unfortunately, it is passed as text, and therefore runs the risk of causing a runtime error.Double check your plugin name. Don’t say I didn’t warn you!

Another important thing to note, is that you must not throw an exception. If you do, your plugin will get disabled, and will not get restarted until you restart TFS. make sure you catch and swallow every exception your code raises. Log it, and check it, but don’t allow it to escape.

  1. catch (Exception exception)
  2. {
  3.     TeamFoundationApplication.LogException("Sample Server Plugin failed while processing", exception);
  4. }

Finally, return a result. Given you won’t be handling the event here, you will always return the same status result:

  1. return EventNotificationStatus.ActionPermitted;

The Job Extension

Next comes the job extension. Similarly, there’s an interface to implement. Remember to make sure that the namespace and class match the one used in the server plugin when queuing the job:

As above, here are the references and “using” statements you’ll need:

image

  1. using System;
  2. using System.Linq;
  3. using System.Xml.Linq;
  4. using Microsoft.TeamFoundation.Client;
  5. using Microsoft.TeamFoundation.Framework.Server;
  6. using Microsoft.TeamFoundation.WorkItemTracking.Client;

Note that the Client namespaces are used because of my specific example. Yours might vary. Of course, the Linq to XML namespaces are also a matter of personal choice. If you decide to parse your XML differently, you’ll use another library. You really didn’t need me to tell you that, right?

So now you should define your class (did I mention that you should make sure you got the FQDN right?):

  1. namespace JobExtensions
  2. {
  3.     public class SampleWorkItemJob : ITeamFoundationJobExtension
  4.     {
  5.         private static object _jobLock = new object();

Wait a minute! What’s that lock about in line #5? I'll get to it later.

Anyway, the ITeamFoundationJobExtension has one method to implement, conveniently named Run:

  1. public TeamFoundationJobExecutionResult Run(TeamFoundationRequestContext requestContext, TeamFoundationJobDefinition jobDefinition, DateTime queueTime, out string resultMessage)
  2. {
  3.     resultMessage = string.Empty;

Once again, we have an out parameter so we set it up. We will change it if there’s some information we wish to log (such as an exception).

Next we write the logic to handle the job.

  1. try
  2. {
  3.     lock (_jobLock)
  4.     {
  5.         var jobDataXmlNode = jobDefinition.Data;
  6.  
  7.         // Expects node like <WorkItem>31</WorkItem>
  8.         var workItemIdString = (from wi in XDocument.Parse(jobDataXmlNode.OuterXml).Elements()
  9.                                 select wi.Value).First();
  10.         var workItemId = int.Parse(workItemIdString);
  11.  
  12.         // In this example we want to work on the work item whose ID we added to the job, so let's get it
  13.         var tpc = new TfsTeamProjectCollection(new Uri("http://localhost:8080/tfs/defaultcollectiton"));
  14.         var store = tpc.GetService<WorkItemStore>();
  15.         var workItem = store.GetWorkItem(workItemId);
  16.  
  17.         // Do something with the work item
  18.     }
  19. }

Like with the server plugin, if we let an exception slip through the cracks, so we have to surround our handler with a try..catch clause here as well. As for the lock, in order to avoid collisions such as from trying to write to the work item store 50 times at once from 50 different job extensions, you’ll need to protect your resources. Job extensions will run at the same time, each in its own thread. They will all however run in the same process, the TFS Job Agent, of which you have only one on each Application Tier. Therefore a static lock object will do the job nicely.

the rest of the code in the block is of little interest; I use Linq to parse my XML node and deserialize the work item ID, which I need for my example. Your mileage may vary.

Not much more to it – You have your exception handling:

  1. catch (RequestCanceledException)
  2. {
  3.     return TeamFoundationJobExecutionResult.Stopped;
  4. }
  5. catch (Exception exception)
  6. {
  7.     resultMessage = exception.ToString();
  8.     return TeamFoundationJobExecutionResult.Failed;
  9. }

What you want to do is to return a status of failure (or stopped if the request got cancelled). Additionally, you can return the exception details in the result message.

Finally, if everything went well, you’ll return a success status:

  1. return TeamFoundationJobExecutionResult.Succeeded;

That’s all there is to it!

You can download the complete solution here: RobustServerPlugin.zip.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*