1
votes

I'm trying to migrate Azure Web Roles running WCF, to stateless services in Azure Service Fabric, using the WCF Communication Listener. Everything works in my local service cluster. After publishing to Azure, other services within the cluster are able to access the stateless WCF service, but external (internet) clients (including my dev machine) are unable to connect with a transient network error.

I verified that the load balancer in the resource group has rules/probes for ports 80 and 8080, and have tested with TCP and HTTP. I also tried to setup the partition resolver on the WCF client to point the "client connection endpoint" on the service cluster (from default, which works within the service cluster).

At this point, I'm not sure if I have a configuration issue, or if it's even possible for external (internet) clients to connect to stateless services running the WCF Communication Listener.

Here is my config:

WCF Communication Listener

    private Func<StatelessServiceContext, ICommunicationListener> CreateListener()
    {
        return delegate (StatelessServiceContext context)
        {

            var host = new WcfCommunicationListener<IHello>(
                wcfServiceObject: this,
                serviceContext: context,
                endpointResourceName: "ServiceEndpoint",
                listenerBinding: CreateDefaultHttpBinding()
            );
            return host;
        };
    }

WCF Binding

    public static Binding CreateDefaultHttpBinding()
    {
        var binding = new WSHttpBinding(SecurityMode.None)
        {
            CloseTimeout = new TimeSpan(00, 05, 00),
            OpenTimeout = new TimeSpan(00, 05, 00),
            ReceiveTimeout = new TimeSpan(00, 05, 00),
            SendTimeout = new TimeSpan(00, 05, 00),
            MaxReceivedMessageSize = int.MaxValue,
        };
        var quota = new XmlDictionaryReaderQuotas
        {
            MaxArrayLength = int.MaxValue,
            MaxDepth = int.MaxValue
        };
        binding.ReaderQuotas = quota;
        return binding;
    }

ServiceManifest.xml (I've also used the default TCP binding with various ports)

<Endpoints>
  <Endpoint Name="ServiceEndpoint" Protocol="http" Port="8080" />
</Endpoints>

WCF Console App

var address = new Uri("fabric:/ServiceFabricWcf.Azure/ServiceFabricWcf");
var client = GetClient(address, CreateDefaultHttpBinding());

try
  {
     var results = client.InvokeWithRetry(x => x.Channel.Hello());
     System.WriteLine($"Results from WCF Service: '{results}'");
     Console.ReadKey();
  }
  catch (Exception e)
  {
     System.Console.WriteLine("Exception calling WCF Service: '{e}'");
  }

WCF Client

    public static WcfServiceFabricCommunicationClient<IHello> GetClient(Uri address, Binding binding)
    {
        //ServicePartitionResolver.GetDefault(); Works with other services in cluster
        var partitionResolver = new ServicePartitionResolver("<clientConnectionEndpointOfServiceCluster>:8080");
        var wcfClientFactory = new WcfCommunicationClientFactory<IHello>(binding, null, partitionResolver);
        var sfclient = new WcfServiceFabricCommunicationClient<IHello>(wcfClientFactory, address, ServicePartitionKey.Singleton);
        return sfclient;
    }

WCF Client Factory

    public class WcfServiceFabricCommunicationClient<T> : ServicePartitionClient<WcfCommunicationClient<T>> where T : class
{
    public WcfServiceFabricCommunicationClient(ICommunicationClientFactory<WcfCommunicationClient<T>> communicationClientFactory,
                                               Uri serviceUri,
                                               ServicePartitionKey partitionKey = null,
                                               TargetReplicaSelector targetReplicaSelector = TargetReplicaSelector.Default,
                                               string listenerName = null,
                                               OperationRetrySettings retrySettings = null
                                               )
        : base(communicationClientFactory, serviceUri, partitionKey, targetReplicaSelector, listenerName, retrySettings)
    {

    }
}
3

3 Answers

0
votes

Here's a way that works for a WCF Service with a WebHttpBinding: https://github.com/loekd/ServiceFabric.WcfCalc

Try changing your code so it doesn't use endpointResourceName but address containing an explicit URL instead. The URL should be the public name of your cluster, like mycluster.region.cloudapp.azure.com.

Edit: url should use node name, which is easier.

string host = context.NodeContext.IPAddressOrFQDN;
  var endpointConfig = context.CodePackageActivationContext.GetEndpoint    
    ("CalculatorEndpoint");
  int port = endpointConfig.Port;
  string scheme = endpointConfig.Protocol.ToString();
  string uri = string.Format(CultureInfo.InvariantCulture, 
    "{0}://{1}:{2}/", scheme, host, port);
0
votes

Here is my updated code, based on LoekD's answer.

Service Changes: To make the service available to internet clients, you have to add an "Address" property to the WCFCommunicationListener to tell the service what endpoint to listen on (http://mycluster.region.azure.com or http://localhost)

Client Changes: Use the normal WCF client, without any of the WCFCommunicationListener references. Only use the WCFCommunicationListener client when inside of the service fabric (my original code works fine in this scenario).

WCF Server Listener

return delegate (StatelessServiceContext context)
        {
            string host = HostFromConfig(context);
            if (string.IsNullOrWhiteSpace(host))
            {
                host = context.NodeContext.IPAddressOrFQDN;
            }

            var endpointConfig = context.CodePackageActivationContext.GetEndpoint("ServiceEndpoint");
            int port = endpointConfig.Port;
            string scheme = endpointConfig.Protocol.ToString();
            //http://mycluster.region.cloudapp.azure.com or http://localhost
            string uri = string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}", scheme, host, port);

            var listener = new WcfCommunicationListener<IHello>(
                wcfServiceObject: this,
                serviceContext: context,
                listenerBinding: CreateDefaultHttpBinding(),
                address: new EndpointAddress(uri)
            );
            return listener;
        };

WCF Client Application

static void Main(string[] args)
    {
        System.Console.WriteLine("\nPress any key to start the wcf client.");
        System.Console.ReadKey();

        System.Console.WriteLine("*************Calling Hello Service*************");
        try
        {
            var binding = CreateDefaultHttpBinding();
            var address = new EndpointAddress("http://cluster.region.cloudapp.azure.com/"); //new EndpointAddress("http://localhost");
            var results = WcfWebClient<IHello>.InvokeRestMethod(x => x.Hello(),binding, address ); 

            System.Console.WriteLine($"*************Results from Hello Service: '{results}'*************");
            Console.ReadKey();
        }
        catch (Exception e)
        {
            System.Console.WriteLine($"*************Exception calling Hello Service: '{e}'*************");
        }
    }

WCF Binding

public static Binding CreateDefaultHttpBinding()
{
    var binding = new WSHttpBinding(SecurityMode.None)
    {
        CloseTimeout = new TimeSpan(00, 05, 00),
        OpenTimeout = new TimeSpan(00, 05, 00),
        ReceiveTimeout = new TimeSpan(00, 05, 00),
        SendTimeout = new TimeSpan(00, 05, 00),
        MaxReceivedMessageSize = int.MaxValue,
    };
    var quota = new XmlDictionaryReaderQuotas
    {
        MaxArrayLength = int.MaxValue,
        MaxDepth = int.MaxValue
    };
    binding.ReaderQuotas = quota;
    return binding;
}

Sample External/Internet WCF Client:

public abstract class WcfWebClient<T> where T : class
{
    public static TResult InvokeRestMethod<TResult>(Func<T, TResult> method, Binding binding, EndpointAddress address)
    {
        var myChannelFactory = new ChannelFactory<T>(binding, address);
        var wcfClient = myChannelFactory.CreateChannel();

        try
        {
            var result = method(wcfClient);
            ((IClientChannel)wcfClient).Close();
            return result;
        }
        catch (TimeoutException e)
        {
            Trace.TraceError("WCF Client Timeout Exception" + e.Message);
            // Handle the timeout exception.
            ((IClientChannel)wcfClient).Abort();
            throw;
        }
        catch (CommunicationException e)
        {
            Trace.TraceError("WCF Client Communication Exception" + e.Message);
            // Handle the communication exception.
            ((IClientChannel)wcfClient).Abort();
            throw;
        }
    }
}
0
votes

You can also try to enable any address binding mode for the WCF service by putting the following attribute on the service implementation:

[ServiceBehavior(AddressFilterMode=AddressFilterMode.Any)]