views:

7431

answers:

3

I can't get to the bottom of this error, because when the debugger is attached, it does not seem to occur. Below is the code.

This is a WCF server in a Windows service. The method NotifySubscribers is called by the service whenever there is a data event (at random intervals, but not very often - about 800 times per day).

When a Windows Forms client subscribes, the subscriber ID is added to the subscribers dictionary, and when the client unsubscribes, it is deleted from the dictionary. The error happens when (or after) a client unsubscribes. It appears that the next time the NotifySubscribers() method is called, the foreach() loop fails with the error in the subject line. The method writes the error into the application log as shown in the code below. When a debugger is attached and a client unsubscribes, the code executes fine.

Do you see a problem with this code? Do I need to make the dictionary thread-safe?

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class SubscriptionServer : ISubscriptionServer
{
    private static IDictionary<Guid, Subscriber> subscribers;

    public SubscriptionServer()
    {            
        subscribers = new Dictionary<Guid, Subscriber>();
    }

    public void NotifySubscribers(DataRecord sr)
    {
        foreach(Subscriber s in subscribers.Values)
        {
            try
            {
                s.Callback.SignalData(sr);
            }
            catch (Exception e)
            {
                DCS.WriteToApplicationLog(e.Message, 
                  System.Diagnostics.EventLogEntryType.Error);

                UnsubscribeEvent(s.ClientId);
            }
        }
    }


    public Guid SubscribeEvent(string clientDescription)
    {
        Subscriber subscriber = new Subscriber();
        subscriber.Callback = OperationContext.Current.
                GetCallbackChannel<IDCSCallback>();

        subscribers.Add(subscriber.ClientId, subscriber);

        return subscriber.ClientId;
    }


    public void UnsubscribeEvent(Guid clientId)
    {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                    e.Message);
        }
    }
}

}

+28  A: 

What's likely happening is that SignalData is indirectly changing the subscribers dictionary under the hood during the loop and leading to that message. You can verify this by changing

foreach(Subscriber s in subscribers.Values)

To

foreach(Subscriber s in subscribers.Values.ToList())

If I'm right, the problem will dissapear

JaredPar
Thanks to both of you.
cdonner
Helped me out. Thanks! :)
mattdell
BTW .ToList() is present in System.Core dll which is not compatible with .NET 2.0 applications. So you might need to change your target app to .Net 3.5
mishal153
Thank you very much! This helped me to solve a problem within my code as well.
Shane Larson
+4  A: 

When a subscriber unsubscribes you are changing contents of the collection of Subscribers during enumeration.

There are several ways to fix this, one being changing the for loop to:

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
...
Mitch Wheat
+1, but Jared was faster. Thanks to both of you.
cdonner
:) Dang! Faster, smarter AND better looking!
Mitch Wheat
I see what you are saying about unsubscribing in the catch section - but this would only be a secondary error if something else caused the call-back to fail. I will look at this more closely.
cdonner
+2  A: 

A more efficient way, in my opinion, is to have another list that you declare that you put anything that is "to be removed" into. Then after you finish your main loop (without the .ToList()), you do another loop over the "to be removed" list, removing each entry as it happens. So in your class you add:

private List toBeRemoved = new List();

Then you change it to:

public void NotifySubscribers(DataRecord sr)
{
    toBeRemoved.Clear();

    ...your unchanged code skipped...

   foreach ( Guid clientId in toBeRemoved )
   {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                e.Message);
        }
   }
}

...your unchanged code skipped...

public void UnsubscribeEvent(Guid clientId)
{
    toBeRemoved.Add( clientId );
}

This will not only solve your problem, it will prevent you from having to keep creating a list from your dictionary, which is expensive if there are a lot of subscribers in there. Assuming the list of subscribers to be removed on any given iteration is lower than the total number in the list, this should be faster. But of course feel free to profile it to be sure that's the case if there's any doubt in your specific usage situation.

x4000
yes, this makes sense.
mishal153