2013-12-09

SPListItem with too many SPListItemVersions

Couple days ago I had a problem on a production WFE SharePoint 2010 server which manifested itself in massive slowdown. Since this is public facing internet site we needed to react fast. By filtering the Application Event Log I quickly found a reason for slowdown. Apparently, server was running out of memory. Since production farm consists of 5 servers which are very well equiped with adequate amount of RAM, there was only a little chance that poor HW configuration caused the slowdown.


This is how entry in Event Log looked like:

Exception: System.OutOfMemoryException

StackTrace:    at Microsoft.SharePoint.Library.SPRequestInternalClass.GetListItemDataWithCallback(String bstrUrl, String bstrListName, String bstrViewName, String bstrViewXml, SAFEARRAYFLAGS fSafeArrayFlags, ISP2DSafeArrayWriter pSACallback, ISPDataCallback pPagingCallback, ISPDataCallback pSchemaCallback, Boolean& pbMaximalView)
   at Microsoft.SharePoint.Library.SPRequest.GetListItemDataWithCallback(String bstrUrl, String bstrListName, String bstrViewName, String bstrViewXml, SAFEARRAYFLAGS fSafeArrayFlags, ISP2DSafeArrayWriter pSACallback, ISPDataCallback pPagingCallback, ISPDataCallback pSchemaCallback, Boolean& pbMaximalView)
   at Microsoft.SharePoint.SPListItemVersionCollection.EnsureVersionsData()
   at Microsoft.SharePoint.SPListItemVersionCollection.get_Count()
   at In2.Vuk.Cl.SharePointHelpers.VukSPListItemHelper.GetLastPublishedVersion(SPListItem listItem)
   at In2.Vuk.Cl.Propisi.SpRepositories.SviPropisiRepository.GetParentsId(String idNodea, Boolean forAdministracijaPropisa)
   at In2.Vuk.Cl.Propisi.SpRepositories.SviPropisiRepository.GetLastVersionFromNode(String idNodea)
   at PrikazPropisaService.GetParentsId(String tipNodea, String idNodea, Boolean povj, Int32 ver)
   at SyncInvokeGetParentsId(Object , Object[] , Object[] )
   at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
   at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc& rpc)
   at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)
   at System.ServiceModel.Dispatcher.ChannelHandler.DispatchAndReleasePump(RequestContext request, Boolean cleanThread, OperationContext currentOperationContext)
   at System.ServiceModel.Dispatcher.ChannelHandler.HandleRequest(RequestContext request, OperationContext currentOperationContext)
   at System.ServiceModel.Dispatcher.ChannelHandler.AsyncMessagePump(IAsyncResult result)
   at System.ServiceModel.Diagnostics.Utility.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
   at System.ServiceModel.AsyncResult.Complete(Boolean completedSynchronously)
   at System.ServiceModel.Channels.InputQueue`1.AsyncQueueReader.Set(Item item)
   at System.ServiceModel.Channels.InputQueue`1.EnqueueAndDispatch(Item item, Boolean canDispatchOnThisThread)
   at System.ServiceModel.Channels.InputQueue`1.EnqueueAndDispatch(T item, ItemDequeuedCallback dequeuedCallback, Boolean canDispatchOnThisThread)
   at System.ServiceModel.Channels.InputQueueChannel`1.EnqueueAndDispatch(TDisposable item, ItemDequeuedCallback dequeuedCallback, Boolean canDispatchOnThisThread)
   at System.ServiceModel.Channels.SingletonChannelAcceptor`3.Enqueue(QueueItemType item, ItemDequeuedCallback dequeuedCallback, Boolean canDispatchOnThisThread)
   at System.ServiceModel.Channels.SingletonChannelAcceptor`3.Enqueue(QueueItemType item, ItemDequeuedCallback dequeuedCallback)
   at System.ServiceModel.Channels.HttpChannelListener.HttpContextReceived(HttpRequestContext context, ItemDequeuedCallback callback)
   at System.ServiceModel.Activation.HostedHttpTransportManager.HttpContextReceived(HostedHttpRequestAsyncResult result)
   at System.ServiceModel.Activation.HostedHttpRequestAsyncResult.BeginRequest()
   at System.ServiceModel.Activation.HostedHttpRequestAsyncResult.OnBeginRequest(Object state)
   at System.ServiceModel.PartialTrustHelpers.PartialTrustInvoke(ContextCallback callback, Object state)
   at System.ServiceModel.Activation.HostedHttpRequestAsyncResult.OnBeginRequestWithFlow(Object state)
   at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke2()
   at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke()
   at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ProcessCallbacks()
   at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.CompletionCallback(Object state)
   at System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ScheduledOverlapped.IOCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
   at System.ServiceModel.Diagnostics.Utility.IOCompletionThunk.UnhandledExceptionFrame(UInt32 error, UInt32 bytesRead, NativeOverlapped* nativeOverlapped)
   at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)



I immediatelly traced the error to my custom webpart, specifically to the method GetLastPublishedVersion:
     
 public static SPListItemVersion GetLastPublishedVersion(SPListItem listItem)
        {
            for (int i = 0; i < listItem.Versions.Count; i++)
            {
                if ((bool)listItem.Versions[i][Constants.Published])
                {
                    return listItem.Versions[i];
                }
            }
            return null;
        }


I figured from the stack trace that the first line in SP Server Object Model which was called was: 
Microsoft.SharePoint.SPListItemVersionCollection.get_Count()

This corresponds to listItem.Versions.Count property call within for loop condition. Next method in Server Object Model which is being called is EnsureVersionsData which is actually fetching entire SPListItemVersion collection for the SPListItem parameter of method in my webpart. When reflecting SPListItemVersionCollection class I found out there was no way to fetch a particular version of SPListItem, not even with methods GetVersionFromID nor GetVersionFromLabel!!! Since all versions of problematic SPListItem were consuming more than 2GB of memory there was no way to delete unnecesary versions because there was no way to fetch any of the versions.

I ran out of ideas, so I called my colleague who is an expert in crisis situations. When examining EnsureVersionsData method implementation using .NET Reflector he realized that there was a piece of code inside Microsoft.Sharepoint.dll which was testing private field variable named m_scFields. This field is used by SOM to set all the columns of a custom SPList that are returned from content database.

The rest was easy, by using .NET reflection we needed to set this field to limit SPListItemVersionCollection to return only columns which are not holding large data. A few other columns were added to m_scFields field in order to successfully call Delete method.


In DeleteUnneededVersions method I  test Published custom column which is specific to my custom list. If SPListItemVersion has this column set to false then it is not needed by my webpart therefore it can be safely deleted. By runing this code on every SPListItemversions have shrunk to an acceptable count. Complete method implementation is as follows:
       
private static void DeleteUnneededVersions(SPListItem item2)
        {
            SPListItemVersionCollection col = item2.Versions;
            StringCollection collection = new StringCollection();
            collection.Add("Title");
            collection.Add("Published");
            collection.Add("owshiddenversion");
            collection.Add("_UIVersion");
            collection.Add("_IsCurrentVersion");
            typeof(SPListItemVersionCollection)
               .GetField("m_scFields", BindingFlags.Instance | BindingFlags.NonPublic)
               .SetValue(col, collection);

            bool publishedReached = false;
            for (int i = 0; i < item2.Versions.Count; i++)
            {
                SPListItemVersion ver = item2.Versions[i];
                bool published = (bool)ver["Published"];
                if (published)
                {
                    publishedReached = true;
                }

                if (!published && publishedReached)
                {
                    ver.Delete();
                    i--;
                }
            }
        }