简体   繁体   中英

WCF right solution for Publish/subscribe pattern implementation

I implemented my own code of Publish/subscribe pattern implementation with WCF with WSDualHttpBinding, but i've a little problem with timeouts that i explain later, for now let me show what i'm doing:

public interface IEventSubscriber
{
    [OperationContract]
    void NotifyEvent(EventNotification notification);
    [OperationContract]
    void NotifyServiceDisconnecting();
}

[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IEventSubscriber))]
public interface IEventPublisherService
{
    [OperationContract(IsOneWay = false, IsInitiating = true)]
    void Subscribe(string culture);
    [OperationContract(IsOneWay = false, IsInitiating = false, IsTerminating = true)]
    void Unsubscribe();
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
internal class EventPublisherServiceImpl : IEventPublisherService
{
    ServiceHost host;
    public bool StartService()
    {
        bool ret = false;
        try
        {
            Uri baseAddress = new Uri(ConfigurationManager.AppSettings[GlobalConstants.CfgKeyConfigEventPublishserServiceBaseAddress].ToString());
            EventHelper.AddEvent(string.Format("Event Publisher Service on: {0}", baseAddress.ToString()));

            host = new ServiceHost(this, baseAddress);

            // duplex session enable http binding
            WSDualHttpBinding httpBinding = new WSDualHttpBinding(WSDualHttpSecurityMode.None);
            httpBinding.ReceiveTimeout = TimeSpan.FromMinutes(10);
            httpBinding.ReliableSession = new ReliableSession();
            httpBinding.ReliableSession.Ordered = true;
            httpBinding.ReliableSession.InactivityTimeout = TimeSpan.FromMinutes(10);
            host.AddServiceEndpoint(typeof(IEventPublisherService), httpBinding, baseAddress);

            // Enable metadata publishing.
            ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
            smb.HttpGetEnabled = true;
            smb.MetadataExporter.PolicyVersion = PolicyVersion.Policy15;
            host.Description.Behaviors.Add(smb);

            // Open the ServiceHost to start listening for messages.
            host.Open();
            ret = true;
        }
        catch (Exception e)
        {
            EventHelper.AddException(e.Message);
        }

        return ret;
    }
 ...

}

now in my implementation class i have a list of subscribers that are stored in memory, when a new notification arrived the following code is performed for each subscriber:

 ...
    /// <summary>
    /// List of active subscribers
    /// </summary>
    private static Dictionary<IEventSubscriber, string> subscribers = new Dictionary<IEventSubscriber, string>();

 ...

that i use it like this:

    internal void Subscribe(string culture)
    {
        lock (subscribers)
        {
            // Get callback contract as specified on the service definition
            IEventSubscriber callback = OperationContext.Current.GetCallbackChannel<IEventSubscriber>();

            // Add subscriber to the list of active subscribers
            if (!subscribers.ContainsKey(callback))
            {
                subscribers.Add(callback, culture);
            }
        }
    }

 ...

    private void OnNotificationEvent(NormalEvent notification)
    {
        lock (subscribers)
        {
            List<IEventSubscriber> listToRemove = new List<IEventSubscriber>();
            //  Method signature: Parallel.ForEach(IEnumerable<TSource> source, Action<TSource> body)
            Parallel.ForEach(subscribers, kvp =>
            {
                try
                {
                    kvp.Key.NotifyEvent(new EventNotification(notification, kvp.Value));
                }
                catch (Exception ex)
                {
                    EventHelper.AddException(string.Format("Error notifying event notification to client: {0} - removing this one", ex.Message));
                    listToRemove.Add(kvp.Key);
                }

            } //close lambda expression
            ); //close method invocation

            Parallel.ForEach(listToRemove, subs =>
            {
                try
                {
                    subs.NotifyServiceDisconnecting();
                }
                catch (Exception ex) {
                    EventHelper.AddException(string.Format("Failed to notify client that is to be removed: {0}", 
                        ex.Message));
                }
                subscribers.Remove(subs);
            }
            );
        }
    }

What is the problem with this, when timeouts are achieved (note that i set 10 minutes for ReceiveTimeout and inactive timeout) the subscribers that are in the list go to fault state, and the following exception is catched in the OnNotificationEvent

* The operation 'NotifyEvent' could not be completed because the sessionful channel timed out waiting to receive a message. To increase the timeout, either set the receiveTimeout property on the binding in your configuration file, or set the ReceiveTimeout property on the Binding directly. *

Ok i can increase the timeout value, but if i do this it will happen some time, eventually.

My question are: i'm doing something wrong when trying to implement this pattern? or exists any other way of implementing this pattern a better way that avoid this problem? or exist any way of reconnecting the faulted callback channel (for what i'm reading it's not possible, but due to that i can't notify the client that is connection was lost, letting the client blind not knowing that the communication ended!? or is a way to give knowledge that he lost communication with the publisher!?)

Of course solution like ping messages are out of date :) but ok, if nothing better appears look like i've to implement something like that...

Thanks

For now the solution was to change the timeouts to have a infinite value:

            // duplex session enable http binding
            WSDualHttpBinding httpBinding = new WSDualHttpBinding(WSDualHttpSecurityMode.None);
            httpBinding.ReceiveTimeout = TimeSpan.MaxValue;
            httpBinding.ReliableSession = new ReliableSession();
            httpBinding.ReliableSession.Ordered = true;
            httpBinding.ReliableSession.InactivityTimeout = TimeSpan.MaxValue;

You are using Parallel.ForEach but I'm not sure that this is enough. AFAIR Parallel.ForEach does not execute each iteration in the separate thread.

I would like to suggest to start separate threads for each subscriber in the OnNotificationEvent and use lock to make sure that foreach will not be breaked by Collection modified exception:

lock (_subscribersSync)
         foreach (var chatter in subscribers)
         {
                 Logger.Log.DebugFormat("putting all users to {0}", subscribers.Key.Name);
                 Thread th = new Thread(PublishAllUserMessage);
                 th.Start(new MessageData() { Message = "", Subscriber = chatter.Key};
         }

void PublishAllUserMessage(object messageData)
{
        MessageData md = (MessageData)messageData;
        try
        {
                md.Subscriber.NotifyEvent(...event parameters here ...);
        }
        catch (Exception ex)
        {
                Logger.Log.Error(string.Format("failed to publish message to '{0}'", md.Subscriber.Name), ex);
                KickOff(md.Subscriber);
        }
}
object _subscribersSync = new object();
void KickOff(IEventSubscriber p)
{
        lock (_subscribersSync)
        {
                subscribers.Remove(p);
                Logger.Log.WarnFormat("'{0}' kicked off", p.Name);
        }
}
 public class MessageData
 {
         public string Message;
         public IEventSubscriber Subscriber;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM