Search code examples
c#.netmultithreadingthreadpoolexchangewebservices

Sending Exchange WS emails - Not enough free threads in the ThreadPool to complete the operation


I need to overcome this error:

There were not enough free threads in the ThreadPool to complete the operation.

I get it 80% of the time when queuing a bunch of Correspondence objects (with SSRS Reports) and sending them via Exchange WS. The reports are generated and emailed using a ThreadPool.

This is what the code looks like, it was built on .Net 2.0 hence no usage of TPL or Async/Await.

public static void QueueCorrespondence(ref Correspondence corr)
{
    Queue corrQ = CorrespondenceQueue(corr.Medium);
    // Create an AsyncOperation, using the CorrespondenceID as a unique id
    AsyncOperation asOp = AsyncOperationManager.CreateOperation(corr.CorrespondenceID);
    SendCorrespondencesInCurrentQueueArgs sendCorrespondencesInCurrentQueueArgs = new SendCorrespondencesInCurrentQueueArgs();
    sendCorrespondencesInCurrentQueueArgs.AsOpArg = asOp;
    sendCorrespondencesInCurrentQueueArgs.CorrQueueArg = corrQ;
    lock (_correspondenceQs) {
        if (corrQ.Count == 0) {
            corrQ.Enqueue(corr);
            ThreadPool.QueueUserWorkItem(new WaitCallback(SendCorrespondencesInCurrentQueue), sendCorrespondencesInCurrentQueueArgs);
        } else {
            corrQ.Enqueue(corr);
        }
    }
}

/// <summary>Do the processing necessary on the new thread</summary>
/// <param name="scicqa"></param>
/// <remarks>This runs on a background thread in the threadpool</remarks>
private static void SendCorrespondencesInCurrentQueue(SendCorrespondencesInCurrentQueueArgs scicqa)
{
    Correspondence corr = null;
    bool allReportsSuccessfullySent = false;
    //Dequeues the correspondence from the queue and returns it (ByRef)
    while (DequeueCorrespondence(ref corr, ref scicqa.CorrQueueArg)) {
        try {
            //This calls Exchange Web Services to send emails
            Send(corr);
            allReportsSuccessfullySent = true;
        } catch (Exception ex) {
            Incident.NewIncident("Unexpected Error", Incident.IncidentLevelEnum.ErrorLevel, ex, true, string.Format("Sending correspondence {0} via Medium {1}", corr.CorrespondenceID.ToString, corr.Medium));
        } finally {
            MailRoomArgs mra = new MailRoomArgs();
            mra.CorrArg = corr;
            mra.TransferSuccessArg = allReportsSuccessfullySent;
            //Success or not, call the RaiseCorrespondenceSentEvent subroutine via this delegate.
            scicqa.AsOpArg.Post(onCorrespondenceSentDelegate, mra);
        }
    }
}

/// <summary>Pull the next correspondence item off the queue, returning true if success</summary>
private static bool DequeueCorrespondence(ref Correspondence corr, ref Queue corrQ)
{
    lock (_correspondenceQs) {
        if (corrQ.Count > 0) {
            corr = corrQ.Dequeue;
            return true;
        } else {
            corr = null;
            return false;
        }
    }
}

Here is the stack trace. See the above functions call into Exchange WS and BeginGetRequestStream throws an exception due to lack of threads in the ThreadPool.

   at System.Net.HttpWebRequest.BeginGetRequestStream(AsyncCallback callback, Object state)
   at Microsoft.Exchange.WebServices.Data.EwsHttpWebRequest.Microsoft.Exchange.WebServices.Data.IEwsHttpWebRequest.BeginGetRequestStream(AsyncCallback callback, Object state)
   at Microsoft.Exchange.WebServices.Data.ServiceRequestBase.GetWebRequestStream(IEwsHttpWebRequest request)
   at Microsoft.Exchange.WebServices.Data.ServiceRequestBase.EmitRequest(IEwsHttpWebRequest request)
   at Microsoft.Exchange.WebServices.Data.ServiceRequestBase.BuildEwsHttpWebRequest()
   at Microsoft.Exchange.WebServices.Data.ServiceRequestBase.ValidateAndEmitRequest(IEwsHttpWebRequest& request)
   at Microsoft.Exchange.WebServices.Data.SimpleServiceRequestBase.InternalExecute()
   at Microsoft.Exchange.WebServices.Data.MultiResponseServiceRequest`1.Execute()
   at Microsoft.Exchange.WebServices.Data.ExchangeService.InternalCreateItems(IEnumerable`1 items, FolderId parentFolderId, Nullable`1 messageDisposition, Nullable`1 sendInvitationsMode, ServiceErrorHandling errorHandling)
   at Microsoft.Exchange.WebServices.Data.ExchangeService.CreateItem(Item item, FolderId parentFolderId, Nullable`1 messageDisposition, Nullable`1 sendInvitationsMode)
   at Microsoft.Exchange.WebServices.Data.Item.InternalCreate(FolderId parentFolderId, Nullable`1 messageDisposition, Nullable`1 sendInvitationsMode)
   at Microsoft.Exchange.WebServices.Data.Item.Save(WellKnownFolderName parentFolderName)
   at XYZ.WM.CIMS.BusinessObjects.ExchangeServer.SendCorrespondence(Correspondence& corr)
   at XYZ.WM.CIMS.BusinessObjects.Email.SendCorrespondence(Correspondence& corr)
   at XYZ.WM.CIMS.BusinessObjects.CorrespondenceMediumBase.Send(Correspondence& corr)
   at XYZ.WM.CIMS.BusinessObjects.CorrespondenceMediumBase.SendCorrespondencesInCurrentQueue(SendCorrespondencesInCurrentQueueArgs scicqa)

Sending emails on the main thread works fine. After a couple of days on this I'm pretty certain the problem could be avoided if I didn't call Exchange WS on a worker thread as shown below.

enter image description here

It doesn't matter if I Save, Send or SendAndSave emails I still get the error:

private  exchangeService = new ExchangeService(3); //Exchange2010 SP2
public void SendCorrespondence(ref Correspondence corr)
{
    exchangeService.Url = new Uri("https://webmail.XYZ.com.au/EWS/Exchange.asmx");
    exchangeService.UseDefaultCredentials = true;
    string[] emailAddresses = corr.SubscriptionObject.SubscriptionRecipients.EmailAddresses;

    EmailMessage message = default(EmailMessage);
    message = new EmailMessage(exchangeService);

    if (emailAddresses.Count > 1) {
        message.BccRecipients.Add(string.Join(RECIPIENTS_SEPARATOR, emailAddresses));
    } 
    else if (emailAddresses.Count == 1) {
        message.ToRecipients.Add(emailAddresses(0));
    }

    message.Subject = corr.SubscriptionObject.EmailSubject;
    EmailAddress fromSender = new EmailAddress();
    fromSender.Address = _instOpsSharedOutlookMailAccount;
    fromSender.Name = _instOpsSharedMailAccountName;
    fromSender.MailboxType = MailboxType.Mailbox;
    message.From = fromSender;

    foreach (ReportBase generatedReport in corr.GeneratedReports) {
        message.Attachments.AddFileAttachment(generatedReport.GeneratedDocument.FullName);
    }

    message.Body = new BodyType();
    message.Body.BodyType = BodyType.HTML;
    message.Body.Text = corr.SubscriptionObject.EmailTemplate;
    if (corr.SubscriptionObject.SendToDraft) {
        //Saving causes the problem !!!!!!!!!!!
        message.Save(WellKnownFolderName.Drafts); 
    } else if (Convert.ToBoolean(Code.GetCodeTextValue(PARENT_CODE, "ExchangeSaveMsgOnSend", RETURN_DEFAULT_STRING_IF_NO_DATA, "True"))) {
        //Sending and Saving causes the problem !!!!!!!!!!!
        message.SendAndSaveCopy();      
    } else {
        //Sending causes the problem !!!!!!!!!!!
        message.Send(); 
    }
}

In fact using any Exchange methods, eg: ExchangeService.ResolveName(logonID) produces the error when called from a thread in the pool.

Does anyone know how I can send the emails without exhausting the ThreadPool? I don't want to re-architect this code too much, but would be happy to implement a simple way to avoid sending the email on a thread if anyone knows how.

The problem does not occur with Lotus Notes. I was using Exchange WS to get around the Outlook nag when an external program uses it to send emails. I would prefer not to use the Outlook Client but if there is no good solution I guess I will have to. Using Smtp client is not on the cards (as saving drafts in group folders is a major feature). I have also tried reducing the maximum number of threads in the ThreadPool but as I suspected that doesn't make a difference.


Solution

  • You should check the ThreadPool.GetAvailableThreads() and see if there is still available threads. Then you have two options.

    1) expand the max number of thread (but you should avoid it)

    2) just wait until some of the thread are done

    int availableThreads = 0; 
    int completionPortThreads = 0; 
    System.Threading.ThreadPool.GetAvailableThreads(out availableThreads, out completionPortThreads); 
    while (!(availableThreads > 1)) 
    { 
        System.Threading.Thread.Sleep(100); 
        System.Threading.ThreadPool.GetAvailableThreads(out availableThreads, out completionPortThreads); 
    }
    

    Good luck!