Thursday, August 26, 2010

Improve CRM Performance with Kerberos Auth Persistence for IIS

[UPDATE:  Thanks to Carsten Groth, who discovered the IIS 7.0 way to do things!  Also, my problem isn’t fixed.  So, this article is simply about performance improvements.]

I recently became engaged with Microsoft Support trying to fix a problem that occurred when the Application Pool for the CRM Site recycled its process worker(s).  I have a split-server deployment of CRM, and the affected machine was the Platform server.  When it would recycle its workers, many System Jobs and Workflows would become stuck in the “Waiting” state with an error message, which claimed an exception of either HTTP Status 400 (Bad Request), or HTTP Status 401 (Unauthorized).

This would occur for all jobs that ran in a 4 or 5 hour period after the recycle, but the situation would always ultimately resolve itself.  Every morning, I would have to resume a large number of jobs.  Also, I found that if I forced an application pool recycle, or restarted IIS, the problem would resurface immediately.

The technicians over at Microsoft reviewed my configurations and logs, and made a recommendation to apply KB 917557 to IIS on my web server.  The server runs Windows 2003 and IIS 6.0, and the logs would show that each request to the CRM website would first encounter an HTTP Status 401 message, which would force the client to submit authentication for a second connection—that would result in an HTTP Status 200 (OK).  Thinking that this could be causing my problem, I was asked to enable Kerberos Auth Persistence using the article above.

One thing to note about enabling Kerberos Auth Persistence is that it effectively cuts the number of requests being processed by IIS in half.  When I inquired about security considerations regarding this setting, I was told that there were none as the feature is inherently bound to HTTP Keep-Alive sessions.  For IIS 7.0, as pointed out by Carston Groth, reference KB 954873.

Friday, August 13, 2010

Limiting the Records in a BulkDeleteRequest

A recent inquiry to the CRM Developer forum got me thinking: since the BulkDeleteRequest.QuerySet property ignores the PagingInfo property of the query instance, how would one limit the operation to a certain number of records?  The answer: use two queries.

It’s not always simple to construct a QueryExpression or QueryByAttribute instance to limit the set of records returned, without using the PagingInfo.  If all you really want to do is take a hatchet to a certain number of records, then the best approach is as follows:

  1. Establish a QueryExpression or QueryByAttribute with the parameters required to gather the records you desire to delete.  Be sure to include the record’s “Key” attribute in the ColumnSet.
  2. Implement paging on this query.
  3. Establish an auxiliary QueryExpression instance.
  4. Gather the “Key” values from the desired “page” of the primary query, and establish them as values—in the auxiliary query—as a part of a condition upon the “Key” attribute.
  5. Pass the auxiliary query to the BulkDeleteRequest.

The solution here is straight-forward and easy to understand.  When the web service gives us lemons, all we need is sugar water.  Example code below:

// Initialize our first query
QueryByAttribute queryForInactiveLeads = new QueryByAttribute();

// Set the entity to "lead"
queryForInactiveLeads.EntityName = EntityName.lead.ToString();

// Retrieve only the GUID of the records
queryForInactiveLeads.ColumnSet = new ColumnSet(new string[] { "leadid" });

// Query for the "statecode" attribute
queryForInactiveLeads.Attributes = new string[] {"statecode"};

// Specifically, disqualified ones
queryForInactiveLeads.Values = new object[] { 2 };

// Now limit the pages to 10 records each
queryForInactiveLeads.PageInfo = new PagingInfo();
queryForInactiveLeads.PageInfo.Count = 10;

// And then call out the first page, only
queryForInactiveLeads.PageInfo.PageNumber = 1;

// Retrieve the matching records
BusinessEntityCollection disqualifiedLeads = this.crmService.RetrieveMultiple(queryForInactiveLeads);

// Don't want to bulk delete nothing
if (disqualifiedLeads.BusinessEntities.Count > 0)
  // Initialize the array that holds the Guids returned from the first query
  Guid[] disqualifiedLeadIds = new Guid[disqualifiedLeads.BusinessEntities.Count];

  // Initialize our array indexer
  int dLIndex = 0;

  // Iterate through each returned record and place its Guid into the array
  foreach (lead disqualifiedLead in disqualifiedLeads.BusinessEntities)
    disqualifiedLeadIds[dLIndex] = disqualifiedLead.leadid.Value;

  // Initialize our second query
  QueryExpression queryToBulkDelete10Leads = new QueryExpression(EntityName.lead.ToString());

  // Initialize our query criteria
  queryToBulkDelete10Leads.Criteria = new FilterExpression();

  // Set the filter operator to "Or", though it doesn't matter
  queryToBulkDelete10Leads.Criteria.FilterOperator = LogicalOperator.Or;

  // Initialize a new condtion for the filter that uses our Guid array
  ConditionExpression leadIdCondition = new ConditionExpression("leadid", ConditionOperator.In, disqualifiedLeadIds);

  // Load the condition into the filter

  // Initialize the BulkDeleteRequest
  BulkDeleteRequest bulkDeleteLeads = new BulkDeleteRequest();

  bulkDeleteLeads.JobName = "Delete 10 Disqualified Leads";
  bulkDeleteLeads.QuerySet = new QueryBase[] { queryToBulkDelete10Leads };
  bulkDeleteLeads.StartDateTime = CrmDateTime.Now;
  bulkDeleteLeads.SendEmailNotification = false;
  bulkDeleteLeads.ToRecipients = new Guid[] { };
  bulkDeleteLeads.CCRecipients = new Guid[] { };
  bulkDeleteLeads.RecurrencePattern = string.Empty;

  catch (SoapException sException)
    throw new ApplicationException(sException.Detail.InnerText);