Harin Sandhoo's Blog

June 26, 2012

Using Windows Azure IaaS to host SharePoint 2010

Filed under: Azure, SharePoint — Harin @ 1:48 pm

This post is a follow-up to one I wrote awhile back, where we hosted SharePoint 2010 in a standalone VM Role (the Compute offering). That obviously was not supported / durable and was really just as a POC to see if it could be done.

Now that Microsoft has announced their Infrastructure as a Service (IaaS) offering (aka durable VM instances) and I’ve had some time to play with it, I thought it would be a good idea to share a powershell script I use to host SharePoint Farms in Microsoft’s Azure data centers in a way that will eventually be supported (once out of preview).

All this stuff is in preview, so proceed at your own risk.

I’ve created a sample powershell script after watching a great Tech Ed talk by Paul Stubbs. I don’t want to re-create good content for the sake of it, so I’ll just point you at his talk and give you the script to hopefully save you some time (since I couldn’t find his).

This script also assumes a lot of manual steps like rdp’ing into your domain controller box and running dcpromo, installing the SharePoint bits, configuring the SharePoint environment. It’s main purpose is to get you a physical environment where you can host SharePoint outside of your datacenter, so don’t expect to run it and have a fully configured SharePoint environment up and running.

The hands on labs are also good to get a feel of how to work with the powershell scripts.

Warning: The script sets up a lot of VMs (6 ->; 12 cores) and disks (14 ->; 6 OS disks, 7 100 GB data disks [for logs and data], and 1 15 GB data disk for the AD box). You will be charged for these, so make sure you delete the VMs, Disks, Virtual Network, services, once you are done.

In a production environment, you’ll likely want to add a redundant AD box, but it’s easy enough to do with the sample script.

I hope this saves you all some time.

Link to script: http://sdrv.ms/KBRxcf

Alt hosted on my company’s GitHub page:  https://github.com/AppliedIS/sp-azure-iaas

June 13, 2011

Integrating SharePoint and Mobile devices using Azure AppFabric Service Bus

Filed under: Azure, SharePoint — Harin @ 8:30 am

Although the WP7 for Azure toolkit has been released, I recently ran into an issue exposing SharePoint data to a WP7 app that I thought was worth sharing.  In this case, we have a SharePoint 2010 farm inside a corporate firewall, needing to expose data from the SharePoint User Profile Service to an application written on a WP7 mobile device.  To implement this, we created a Windows Service that hosts a webHttpRelayBinding endpoint so the WP7 application can make simple REST calls to get at the data.  By using a REST based endpoint, that also allowed us to write equivalent client applications for Android and iOS.  Going forward, the windows service can be brought in and managed in SharePoint using the Service Application model, but for a demo, simply installing the windows service on the SharePoint box was sufficient. 

High level architecture:

 

 

WCF Service Contract:

 For our service we have a service contract defined to as:

    [ServiceContract]
    public interface IProfileService
    {
        [OperationContract,
        WebGet(
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Xml,
            UriTemplate = "/FarmInfo")
        ]
        FarmInfo GetFarmInfo();

        [OperationContract, 
        WebGet(
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Xml,
            UriTemplate = "/Users?q={searchTerms}")
        ]
        UserList GetUsers(string searchTerms);

        [OperationContract, 
        WebGet(
            BodyStyle = WebMessageBodyStyle.Bare,
            ResponseFormat = WebMessageFormat.Xml,
            UriTemplate = "/Users/{userId}")
        ]
        Profile GetProfile(string userId);
    }
 

WCF Service Implementation:

I created a class called SPProfileReader which encapsulates the call to the SharePont UserProfileManager.  The service implementation looks something like this:

    public class ProfileService : IProfileService
    {
        private static object _synch = new object();
        private static SPProfileReader reader = null;

        private SPProfileReader Instance
        {
            get
            {
                return new SPProfileReader(System.Configuration.ConfigurationManager.AppSettings["SiteUrl"]);
            }
        }

        public FarmInfo GetFarmInfo()
        {
            FarmInfo retVal = null;
            try
            {
                retVal = new FarmInfo()
                {
                    id = "AIS Demos Farm",
                    url = "http://<demo farm url>",
                    centraladminurl = "<central admin url>",
                    description = "This farm is a demo environment meant to showcase Azure and SharePoint integration scenarios."
                };
            }
            catch (Exception e)
            {
                Trace.WriteLine(e.ToString());
            }
            return retVal;
        }

        public UserList GetUsers(string searchTerms)
        {
            UserList retVal = null;
            try
            {
                using (var rdr = Instance)
                {
                    retVal = rdr.GetUsers(searchTerms);
                }
            }
            catch (Exception e)
            {
                Trace.WriteLine(e.ToString());
            }
            return retVal;
        }

        public Profile GetProfile(string userId)
        {
            Profile retVal = null;
            try
            {
                using (var rdr = Instance)
                {
                    retVal = rdr.GetUserProfile(userId);
                }
            }
            catch (Exception e)
            {
                Trace.WriteLine(e.ToString());
            }
            return retVal;
        }
    }
 

Hosting the WCF Service:

Hosting the WCF Service is pretty straight forward and for completeness looks like this:

        public void StartHost()
        {
            try
            {
                //// Build the endpoint URI
                string serviceNamespace = System.Configuration.ConfigurationManager.AppSettings["SBNamespace"];
                string endPoint = System.Configuration.ConfigurationManager.AppSettings["SBEndpoint"];
                Uri address = ServiceBusEnvironment.CreateServiceUri("https", serviceNamespace, endPoint);

                //// Build the service bus credential
                string serviceUser = System.Configuration.ConfigurationManager.AppSettings["SBUser"];
                string serviceKey = System.Configuration.ConfigurationManager.AppSettings["SBKey"];
                TransportClientEndpointBehavior sharedSecretServiceBusCredential = new TransportClientEndpointBehavior();
                sharedSecretServiceBusCredential.CredentialType = TransportClientCredentialType.SharedSecret;
                sharedSecretServiceBusCredential.Credentials.SharedSecret.IssuerName = serviceUser;
                sharedSecretServiceBusCredential.Credentials.SharedSecret.IssuerSecret = serviceKey;

                Trace.WriteLine("Opening service bus connection: " + address.ToString());

                //// Create the wcf service host
                host = new WebServiceHost(typeof(ProfileService), address);

                //// Set the binding to WebHttpRelayBinding
                Microsoft.ServiceBus.WebHttpRelayBinding binding = new WebHttpRelayBinding();
                binding.Security.RelayClientAuthenticationType = RelayClientAuthenticationType.None;

                //// Create the endpoint
                ServiceEndpoint se = host.AddServiceEndpoint(typeof(IProfileService), binding, address);
                se.Behaviors.Add(sharedSecretServiceBusCredential);
                ServiceDebugBehavior sdb = host.Description.Behaviors.Find<ServiceDebugBehavior>();
                sdb.HttpHelpPageEnabled = false;

                //// Open host
                host.Open();
                Trace.WriteLine("Host open.");
            }
            catch(Exception e)
            {
                Trace.WriteLine("Exception occured while opening host:");
                Trace.WriteLine(e.ToString());
            }
        }

 

Sample WP7 Client Code:

Since the relay service is exposing the endpoint as via REST, integrating with mobile devices becomes trivial.  Note, if this is sensitive / production data, I would recommend utilizing the ACS to secure access to the endpoint (examples of how to do that can be seen in the Azure WP7 SDK).  Here is a bit of code showing how to access some data from the REST endpoint.

    public class ServiceProxy
    {
        private const string SBUri = "https://<myaccount>.servicebus.windows.net/SPProfileViewer/";

        public static void GetAllUsers(DownloadStringCompletedEventHandler callback)
        {
            WebClient client = new WebClient();
            string uri = string.Format("{0}Users",SBUri);
            client.DownloadStringCompleted += callback;
            client.DownloadStringAsync(new Uri(uri));
        }

        public static void SearchForUsers(string queryTerm, DownloadStringCompletedEventHandler callback)
        {
            WebClient client = new WebClient();
            string uri = string.Format("{0}Users?q={1}", SBUri, queryTerm);
            client.DownloadStringCompleted += callback;
            client.DownloadStringAsync(new Uri(uri));
        }

        public static void GetUserProfile(string userId, DownloadStringCompletedEventHandler callback)
        {
            WebClient client = new WebClient();
            string uri = string.Format("{0}Users/{1}", SBUri,userId);
            client.DownloadStringCompleted += callback;
            client.DownloadStringAsync(new Uri(uri), userId);
        }

        public static void GetFarmInfo(DownloadStringCompletedEventHandler callback)
        {
            WebClient client = new WebClient();
            string uri = string.Format("{0}FarmInfo", SBUri);
            client.DownloadStringCompleted += callback;
            client.DownloadStringAsync(new Uri(uri));
        }
    }
 

An Important ‘gotcha’ when working with SharePoint and the AppFabric Service Bus:

When hosting our relay service, we noticed that after a while the service host lost the connection to the Service Bus and our service was no longer accessible from the clients.  Working off and on with Microsoft Support for a few weeks, we finally tracked down what the issue was.  When the service creates a connection to the AppFabric Service Bus, it requests a security token.  Every ten minutes or so, the host attempts to renew that token.  The trouble comes in when you try and integrate in with SharePoint.  When you access the SPServiceContext and UserProfileManager and probably a bunch of other SharePoint API calls (as we did in this case), the ServicePointManager.ServerCertificateValidationCallback delegate is changed so that it only trusts the certificates that SharePoint knows about.  Unfortunately, the GTE CyberTrust Root certificate that ultimately signed our Service Bus certificate is not trusted by SharePoint.  If you are experiencing this problem, you may notice errors in your WCF / System.NET trace log such as:

System.Net Error: 0 : [6256] Exception in the HttpWebRequest#3038911:: – The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

System.Net Error: 0 : [6256] Exception in the HttpWebRequest#3038911::EndGetRequestStream – The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

 

The work around is as follows (these are from Mog Liang from Microsoft’s support team):

  1. Download the ACS server certificate’s root CA ‘GTE CyberTrust Global Root’ by accessing ACS service ‘https://<myaccount&gt;.accesscontrol.windows.net/WRAPv0.9/
  2. Open ‘SharePoint 2010 Central Administration’ tool
  3. Open ‘Security -> Manage Trust’ page  
  4. Click ‘new’ button, in popup page, input the ‘GTE CyberTrust Global Root’ certificate, give a name, and click OK

 

May 26, 2010

SharePoint 2010 Pluggable Workflow Services with Correlation and External Callbacks

Filed under: SharePoint, Workflow — Harin @ 5:35 pm

Paul Andrew recently wrote an article on MSDN outlining Pluggable Workflow Services. This blog posting extends that example to explain how to include correlation tokens as well as an external callback mechanism. The point of including correlation tokens is if you want to let multiple events of the same type wait for an external event. The point of adding a custom callback mechanism would be if you wanted to have a web part, service application, or custom web service that allows users to progress workflows directly instead of modifying a task list item. For example, if you wanted to provide users with the ability to progress the workflow just by sending an email, you could use this mechanism. Wouter van Vugt has a good Channel 9 video explaining most of these concepts which I’d recommend watching as well.

Correlation Tokens

If you’ve ever created a workflow in SharePoint, then you’ve used correlation tokens with almost every SharePoint related activity, such as CreateTask. So why is correlation needed in your custom local communication service definition? Basically it is a mechanism that allows the workflow runtime to route inbound messages to the correct instance of the persisted activities, if you have more than one activity that is waiting for external communication events. If you are writing a pluggable workflow service and have multiple HandleExternalEvent activities of the same interface type and operation, then you need to provide correlation token capabilities. If you don’t have more than one activity instance in the same workflow persisted and waiting on a particular event, then you don’t have to worry about correlation tokens. Here’s how to add them.

Add a parameter to the ExternalDataEventArgs

This parameter is used to pass the correlation token id between the host and the client.  

   1: [Serializable]

   2: public class PrimeCalculatorEventArgs : ExternalDataEventArgs

   3: {

   4:     public PrimeCalculatorEventArgs(Guid id, string tokenId) : base(id)

   5:     {

   6:         this.TokenID = tokenId;

   7:     }

   8:

   9:     public string Answer;

  10:     public string TokenID;

  11: }

Modify the ExternalDataExchange interface definition

There are a number of things you’ll need to do here.

  1. Annotate the interface with a CorrelationParameterAttribute. In this you specify the token parameter name as defined in the interface method signature.
  2. Mark each outbound method that is in the CallExternalMethod and HandleExternalEvent pair with a CorrelationInitializerAttribute. The parameters of the outbound methods need to have the token parameter name specified in Step 1.
  3. Once that’s done, you need to mark the inbound method with CorrelationAliasAttribute. The first parameter in the CorrelationAliasAttribute attribute needs to match the token parameter name in Step 1. The second parameter in the CorrelationAliasAttribute needs to match the public property specified in the ExternalDataEventArgs (see above section). Note, with the second parameter the first part (the “e”) doesn’t really matter AFAIK, it’s really the second part of the parameter (the TokenID) that needs to match.
   1: [ExternalDataExchange]

   2: [CorrelationParameter("tokenId")]  // Step 1

   3: public interface IPrimeCalculatorService

   4: {

   5:     [CorrelationInitializer()]  // Step 2

   6:     void CalculatePrimes(string tokenId, int topNumberExclusive);

   7:

   8:     [CorrelationInitializer()]  // Step 2

   9:     void RequestValidation(string tokenId);

  10:

  11:

  12:     [CorrelationAlias("tokenId","e.TokenID")]  // Step 3

  13:     event EventHandler<PrimeCalculatorEventArgs> GetAnswer;

  14:

  15:     [CorrelationAlias("tokenId", "e.TokenID")]  // Step 3

  16:     event EventHandler<PrimeCalculatorEventArgs> ValidateAnswer;

  17: }

Implement your SPWorkflowExternalDataExchangeService

The main differences related to correlation here are that you need to pass along the token id. In CallEventHandler, when you invoke your event handler, the token id is passed along with the ExternalDataEventArgs. This is used internally by the workflow runtime to route the call to the correct activity instance in your workflow.

 

   1: /// <summary>

   2: /// Implementation of ExternalDataExchangeService / Local Communication Service

   3: /// </summary>

   4: class PrimeCalculatorService: SPWorkflowExternalDataExchangeService, IPrimeCalculatorService

   5: {

   6:

   7:     public const string SubscriptionsListName = "SubscriptionsList";

   8:

   9:     #region IPrimeCalculatorService Members

  10:

  11:     public void CalculatePrimes(string tokenId, int topNumberExclusive)

  12:     {

  13:         ThreadPool.QueueUserWorkItem(

  14:             state =>

  15:             {

  16:                 FactoringState factoringState = state as FactoringState;

  17:                 PrimeCalculatorWS.Calculate ws = new PrimeCalculatorWS.Calculate();

  18:                 string answer = ws.CalculatePrimes(factoringState.topNumberExclusive);

  19:

  20:                 SPWorkflowExternalDataExchangeService.RaiseEvent(

  21:                     factoringState.web,

  22:                     factoringState.instanceId,

  23:                     typeof(IPrimeCalculatorService),

  24:                     "GetAnswer",

  25:                     new object[] { tokenId, answer }  // NOTE:  tokenId is passed along as a parameter for the callback.

  26:                 );

  27:

  28:             },

  29:             new FactoringState(WorkflowEnvironment.WorkflowInstanceId, this.CurrentWorkflow.ParentWeb, topNumberExclusive)

  30:         );

  31:     }

  32:

  33:     public void RequestValidation(string tokenId)

  34:     {

  35:     }

  36:

  37:     public event EventHandler<PrimeCalculatorEventArgs> GetAnswer;

  38:     public event EventHandler<PrimeCalculatorEventArgs> ValidateAnswer;

  39:

  40:     #endregion

  41:

  42:     #region SPWorkflowExternalDataExchangeService Overrides

  43:

  44:     public override void CallEventHandler(Type eventType, string eventName, object[] eventData, SPWorkflow workflow, string identity, IPendingWork workHandler, object workItem)

  45:     {

  46:         string tokenId = (string)eventData[0];  // NOTE:  tokenId is extracted from eventData and passed along with the ExternalDataEventArgs

  47:         var msg = new PrimeCalculatorEventArgs(workflow.InstanceId, tokenId);

  48:         msg.Answer = eventData[1].ToString();

  49:         msg.WorkHandler = workHandler;

  50:         msg.WorkItem = workItem;

  51:         msg.Identity = identity;

  52:

  53:         if (eventName == "GetAnswer" && GetAnswer != null)

  54:         {

  55:             this.GetAnswer(null, msg);

  56:         }

  57:         else if (eventName == "ValidateAnswer" && ValidateAnswer != null)

  58:         {

  59:             this.ValidateAnswer(null, msg);

  60:         }

  61:     }

  62:

  63:

  64:     #endregion

  65: }

  66:

  67: /// <summary>

  68: /// This is a utility class to store information needed by the queued thread work item.  

  69: /// This isn't relevant to correlation directly, just used by the example and included for completeness.

  70: /// </summary>

  71: class FactoringState

  72: {

  73:     public Guid instanceId;

  74:     public SPWeb web;

  75:     public int topNumberExclusive;

  76:

  77:     public FactoringState(Guid instanceId, SPWeb web, int topNumberExclusive)

  78:     {

  79:         this.instanceId = instanceId;

  80:         this.web = web;

  81:         this.topNumberExclusive = topNumberExclusive;

  82:     }

  83: }

Generate strongly typed CallExternalMethod and HandleExternalEvent activities using WCA.exe

EDIT:  I neglected to mention that the tool was called wca.exe.  It’s used to generate the CallExternalMethod and HandleExternalEvent activities that are specific to your interface.  Here is an msdn page describing the tool.

This step is optional, but generally a good idea. The tool creates activities for that are strongly typed to your interface definition.

It is located at: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin

Add your Workflow Service / Local Communication Service to the web.config

Here is a sample snippet from Paul Andrew’s msdn article mentioned above:

<WorkflowServices><WorkflowService Assembly=”WorkflowProject1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=YOURPUBLICKEY”>
</WorkflowService></WorkflowServices>

Create a workflow and start using your correlated activites!

External Callback Mechanism

This section tries to explain how you can create an external callback mechanism that doesn’t live directly within the workflow. Examples would be a web part, custom field type, custom service application, SharePoint hosted WCF service, etc, etc. To accomplish this, you first need to make a few small modifications to your SPWorkflowExternalDataExchange service. The next step is that you need write some code to call your call backs, in this example I’ll use a web part, but it can be anything that is executed within the context of a SharePoint process. The italicized point is key. AFAIK, you can’t create a standalone application to progress the workflows since the SharePoint workflow runtime isn’t loaded in those processes. Ok, so let’s get started. In the example above there was a RequestValidation and ValidateAnswer event handler I defined in my service contract. This section uses them as defined above.

SPWorkflowExternalDataExchange modifications

SPWorkflowExternalDataExchange defines two methods called CreateSubscription and DeleteSubscription. These methods are called by the workflow runtime when you use CallExternalMethod and HandleExternalEvent activities. They allow us to write information about our workflow, the Subscription ID, Workflow ID, and any correlation parameters, to some sort of persistence store so we can access it externally when we want to progress our workflows. Note: the example here was taken from Wouter van Vugt’s Channel 9 video. In this example, I’m just writing the information to a SharePoint list, but you can write it to a database / any other persistance store you like. Just keep in mind that there are fidelity concerns that come in to play here, so a SharePoint list may not be the best place if you need high performance / have lots of workflows.

   1: /// <summary>

   2: /// This method's goal is to just persist the subscription.CorrelationProperties to a list.

   3: /// NOTE:  This method uses linq to sharepoint where I to defined my own DataContext using sqlmetal (not shown here).

   4: /// This is called automatically by the workflow runtime.

   5: /// </summary>

   6: /// <param name="subscription"></param>

   7: public override void CreateSubscription(MessageEventSubscription subscription)

   8: {

   9:     EnsureSubscriptionsList(base.CurrentWorkflow.ParentWeb);

  10:

  11:     CorrelationProperty correlationProperty = subscription.CorrelationProperties.Where(p => p.Name != string.Empty).First();

  12:     using (DataContext context = new DataContext(base.CurrentWorkflow.ParentWeb.Url))

  13:     {

  14:         EntityList<SubscriptionsListItem> subscriptions = context.GetList<SubscriptionsListItem>(SubscriptionsListName);

  15:         SubscriptionsListItem entity = new SubscriptionsListItem()

  16:         {

  17:             SubscriptionId = subscription.SubscriptionId.ToString(),

  18:             WorkflowId = subscription.WorkflowInstanceId.ToString(),

  19:             CustomId = correlationProperty.Value !=null ? correlationProperty.Value.ToString() : string.Empty

  20:         };

  21:         subscriptions.InsertOnSubmit(entity);

  22:         context.SubmitChanges();

  23:     }

  24: }

  25:

  26: /// <summary>

  27: /// This method's goal is to delete the SharePoint list item when we’re done with it.  

  28: /// NOTE:  This method uses linq to sharepoint where I to defined my own DataContext using sqlmetal (not shown here).

  29: /// This is called automatically by the workflow runtime.

  30: /// </summary>

  31: /// <param name="subscriptionId"></param>

  32: public override void DeleteSubscription(Guid subscriptionId)

  33: {

  34:

  35:     using (DataContext context = new DataContext(base.CurrentWorkflow.ParentWeb.Url))

  36:     {

  37:         EntityList<SubscriptionsListItem> subscriptions = context.GetList<SubscriptionsListItem>(SubscriptionsListName);

  38:         SubscriptionsListItem entity = subscriptions

  39:             .Where(s => s.SubscriptionId == subscriptionId.ToString())

  40:             .FirstOrDefault();

  41:         if (entity != null)

  42:         {

  43:             subscriptions.DeleteOnSubmit(entity);

  44:             context.SubmitChanges();

  45:         }

  46:     }

  47: }

  48:

  49: /// Helper method to create the SharePoint list.

  50: private void EnsureSubscriptionsList(SPWeb web)

  51: {

  52:     SPList list = web.Lists.TryGetList(SubscriptionsListName);

  53:     if (list == null)

  54:     {

  55:         Guid listId = web.Lists.Add(SubscriptionsListName, "", SPListTemplateType.GenericList);

  56:         list = web.Lists.GetList(listId, true);

  57:         string subscriptionIDFieldName = list.Fields.Add("SubscriptionId", SPFieldType.Text, true);

  58:         string workflowIDFieldName = list.Fields.Add("WorkflowId", SPFieldType.Text, true);

  59:         string customIDFieldName = list.Fields.Add("CustomId", SPFieldType.Text, true);

  60:         list.Update();

  61:

  62:         SPView view = list.DefaultView;

  63:         view.ViewFields.Add(subscriptionIDFieldName);

  64:         view.ViewFields.Add(workflowIDFieldName);

  65:         view.ViewFields.Add(customIDFieldName);

  66:         view.Update();

  67:     }

  68:

  69: }

Implement your web part / custom code to progress the workflow

The key method here is SPWorkflowExternalDataExchangeService.RaiseEvent. This calls in to the workflow as we did above, passing in the workflow instance ID and the correlation token id in the ExternalDataEventArgs if you are using correlation. These parameters can be found wherever we persisted them in the CreateSubscription method defined above. In this example, that information was persisted to a list. A more robust example could look up the correct parameter ids, but this one requires users to enter them manually.

   1: [ToolboxItemAttribute(false)]

   2: public class WebPart1 : WebPart

   3: {

   4:     protected TextBox subId;

   5:     protected TextBox wfInstId;

   6:     protected TextBox corrTokenId;

   7:     protected Button validate;

   8:     protected Button invalidate;

   9:

  10:     protected override void CreateChildControls()

  11:     {

  12:         subId = new TextBox();

  13:         wfInstId = new TextBox();

  14:         corrTokenId = new TextBox();

  15:         validate = new Button()

  16:         {

  17:             Text = "Valid"

  18:         };

  19:

  20:         validate.Click += new EventHandler((s, e) =>

  21:         {

  22:             SPWorkflowExternalDataExchangeService.RaiseEvent(

  23:                                    SPContext.Current.Web,

  24:                                    new Guid(wfInstId.Text),

  25:                                    typeof(IPrimeCalculatorService),

  26:                                    "ValidateAnswer",

  27:                                    new object[] { corrTokenId.Text, "Valid answer." }

  28:                            );

  29:

  30:         });

  31:

  32:

  33:         invalidate = new Button()

  34:         {

  35:             Text = "Invalid"

  36:         };

  37:

  38:         invalidate.Click += new EventHandler((s, e) =>

  39:         {

  40:             SPWorkflowExternalDataExchangeService.RaiseEvent(

  41:                                    SPContext.Current.Web,

  42:                                    new Guid(wfInstId.Text),

  43:                                    typeof(IPrimeCalculatorService),

  44:                                    "ValidateAnswer",

  45:                                    new object[] { corrTokenId.Text, "In valid answer." }

  46:                            );

  47:

  48:         });

  49:

  50:         Controls.Add(subId);

  51:         Controls.Add(wfInstId);

  52:         Controls.Add(corrTokenId);

  53:         Controls.Add(validate);

  54:         Controls.Add(invalidate);

  55:

  56:

  57:     }

  58:

  59: }

Deploy, create your workflow, and use your web part to progress your workflows
That’s all there is to it! Hope this helps!

July 19, 2009

SharePoint Workflow Deadlocks

Filed under: SharePoint — Tags: , , — Harin @ 11:51 pm

I’ve been working on an application built on MOSS that is extremely workflow intensive.  The application has a series of infopath forms that progress through a custom approval process workflow we developed using Visual Studio 2005.  After a couple weeks, when we were getting about 1000 forms going through this workflow a day, we noticed that we were getting about 10 or so that would just fail inexplicably.  When we terminated and restarted the workflows, they went through the approval process with no issues.

We then started to really dig into our diagnostics.  We checked our custom logging, the SharePoint logs, Event Logs, the workflow trace logs, SQL Server logs, and all the performance counters we could think of.  We noticed that under load we were seeing a number of lock escalations and periodic deadlock errors on our SQL profiler trace and sure enough, if the WF runtime process hosted inside MOSS was the one terminated by SQL we would see a corresponding entry in our workflow runtime trace file. [1

We also saw quite a few different processes involved in our deadlock graphs, but they all could be traced back to the proc_UpdateListItemWorkflowInstanceData stored procedure in the content db, which is called when the workflows start and at each persist point.

So, we did the next logical thing and tried to reproduce the error in our testing environment so we could troubleshoot and deploy a fix.  The only thing was, we couldn’t reproduce it.  It turns out, because we were performing functional testing on a virtualized environment there was never enough load put on the SQL box to see the problem.  We had to pave a machine and install a native 64 bit SQL Server 2005 Enterprise Edition to see it.  Our infrastructure was set up in a large farm topology with 2 WFEs, 1 Index, 1 Search, and a 2 Node active-passive clustered SQL Server 2005 instance.  All the MOSS boxes in production were 32 bit and the SQL nodes were 64 bit, and our testing environment was virtualized so all 32 bit.

I’ll spare you the details of the trial and error process we went through to track these down, but to get rid of most of our deadlocks we ultimately had to:

1. Ensure our lists do not contain a large number of items.
2. Set the maximum degree of parallelism on our SQL box to 1 (MAXDOP=1)
3. Periodically run DBCC FREEPROCCACHE, especially after the weekly SharePoint update statistics timer job is run.

[1] Error in the WF Trace File:
System.Workflow.Runtime.Hosting Error: 0 : DefaultWorkflowCommitWorkBatchService caught exception from commitWorkBatchCallback: System.Data.SqlClient.SqlException: Transaction (Process ID 152) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
   at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
   at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
   at System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
   at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async)
   at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
   at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe)
   at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   at Microsoft.SharePoint.Utilities.SqlSession.ExecuteNonQuery(SqlCommand command)
   at Microsoft.SharePoint.Workflow.SPWorkflowManager.SaveInstanceData(Guid trackingId, Stream instanceStream, DateTime wakeupTime, Boolean workflowCompleted, Boolean workflowSuspended, Boolean workflowFaulting, Boolean workflowTerminated, Boolean workflowCanceled, Boolean unlockInstance)
   at Microsoft.SharePoint.Workflow.SPWorkflowHostServiceBase.SaveInstanceData(Guid instanceId, Stream instanceStream, DateTime wakeupTime, Boolean bWorkflowCompleted, Boolean bWorkflowSuspended, Boolean bWorkflowFaulting, Boolean bWorkflowTerminated, Boolean bWorkflowCanceled, Boolean unlockInstance)
   at Microsoft.SharePoint.Workflow.SPWinOePersistenceService.Commit(Transaction transaction, ICollection items)
   at System.Workflow.Runtime.WorkBatch.PendingWorkCollection.Commit(Transaction transaction)
   at System.Workflow.Runtime.WorkBatch.Commit(Transaction transaction)
   at System.Workflow.Runtime.VolatileResourceManager.Commit()
   at System.Workflow.Runtime.WorkflowExecutor.DoResourceManagerCommit()
   at System.Workflow.Runtime.Hosting.WorkflowCommitWorkBatchService.CommitWorkBatch(CommitWorkBatchCallback commitWorkBatchCallback)
   at System.Workflow.Runtime.Hosting.DefaultWorkflowCommitWorkBatchService.CommitWorkBatch(CommitWorkBatchCallback commitWorkBatchCallback)

System.Workflow.Runtime Error: 0 : Workflow Runtime: WorkflowExecutor: Persist attempt on instance ‘1d973253-8b24-4270-9065-4f3923e87374’ threw an exception ‘Transaction (Process ID 152) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.’ 
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection)
   at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
   at System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
   at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async)
   at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
   at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe)
   at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   at Microsoft.SharePoint.Utilities.SqlSession.ExecuteNonQuery(SqlCommand command)
   at Microsoft.SharePoint.Workflow.SPWorkflowManager.SaveInstanceData(Guid trackingId, Stream instanceStream, DateTime wakeupTime, Boolean workflowCompleted, Boolean workflowSuspended, Boolean workflowFaulting, Boolean workflowTerminated, Boolean workflowCanceled, Boolean unlockInstance)
   at Microsoft.SharePoint.Workflow.SPWorkflowHostServiceBase.SaveInstanceData(Guid instanceId, Stream instanceStream, DateTime wakeupTime, Boolean bWorkflowCompleted, Boolean bWorkflowSuspended, Boolean bWorkflowFaulting, Boolean bWorkflowTerminated, Boolean bWorkflowCanceled, Boolean unlockInstance)
   at Microsoft.SharePoint.Workflow.SPWinOePersistenceService.Commit(Transaction transaction, ICollection items)
   at System.Workflow.Runtime.WorkBatch.PendingWorkCollection.Commit(Transaction transaction)
   at System.Workflow.Runtime.WorkBatch.Commit(Transaction transaction)
   at System.Workflow.Runtime.VolatileResourceManager.Commit()
   at System.Workflow.Runtime.WorkflowExecutor.DoResourceManagerCommit()
   at System.Workflow.Runtime.Hosting.WorkflowCommitWorkBatchService.CommitWorkBatch(CommitWorkBatchCallback commitWorkBatchCallback)
   at System.Workflow.Runtime.Hosting.DefaultWorkflowCommitWorkBatchService.CommitWorkBatch(CommitWorkBatchCallback commitWorkBatchCallback)
   at System.Workflow.Runtime.WorkflowExecutor.CommitTransaction(Activity activityContext)
   at System.Workflow.Runtime.WorkflowExecutor.Persist(Activity dynamicActivity, Boolean unlock, Boolean needsCompensation)

Blog at WordPress.com.