we're looking into connecting Azure websites with on-premises WCF services. We want to use the Azure Relay Bus for this.
There is a cost involved in setting up the TCP channel for the first time, about 3 seconds in our case. This is understood. We do cache the channel, so a next call goes faster. ( 250ms ) The channel is cached in ThreadLocal storage.
When multiple users are using the website, for every new thread used, a new instance of a channel is created, and we have to pay the price again for setting up the TCP channel.
My question are:
How do we prevent that a real user is confronted with the delay of setting up the channel? Is it normal practice to have a TCP channel for each client thread?
Note: On-premises we use IIS auto-start or NT services to 'warm up' our WCF services, but is this the way to go for a website running on Azure? The fact that each thread has its own channel doesn't make it easier.
Code is shown below.
public static class NetTcpRelayClient<TChannel>
{
/// <summary>
/// A ThreadLocal instance of the channel, so that each thread can have an open TCP channel
/// </summary>
private static ThreadLocal<TChannel> staticChannel = new ThreadLocal<TChannel>();
/// <summary>
/// A shared instance of the ChannelFactory
/// </summary>
private static ChannelFactory<TChannel> cf = null;
/// <summary>
/// Creates the channel.
/// </summary>
/// <returns>The created channel</returns>
private static TChannel CreateChannel()
{
// get the url and access parameters from the configuration service
var address = ServiceSecurityInspector.GetNetTcpRelayBindingAddress(typeof(TContract));
string issuerName = ConfigurationManager.AppSettings["Shared.appsettings.AzureServiceBus.IssuerName"];
string issuerSecret = ConfigurationManager.AppSettings["Shared.appsettings.AzureServiceBus.IssuerSecret"];
// create a NetTcpRelayBinding,
if (cf == null)
{
cf = new ChannelFactory<TChannel>(new NetTcpRelayBinding(), new EndpointAddress(address));
cf.Endpoint.Behaviors.Add(new TransportClientEndpointBehavior
{
TokenProvider = TokenProvider.CreateSharedSecretTokenProvider(issuerName, issuerSecret)
});
}
TChannel channel = cf.CreateChannel();
// open the channel
IClientChannel clientChannel = channel as IClientChannel;
if (clientChannel != null)
{
clientChannel.Open();
}
return channel;
}
/// <summary>
/// Gets the channel for making a call over the relay bus.
/// Note that the channel is cached.
/// And that each thread has it's own channnel.
/// </summary>
/// <returns>The channel</returns>
public static TChannel GetChannel()
{
// check if we have a channel instance already
if (!staticChannel.IsValueCreated)
{
// no, create one
staticChannel.Value = CreateChannel();
}
else
{
// if the channel exists already
IClientChannel clientChannel = staticChannel as IClientChannel;
// check if it is open, if not, make a new one
if (clientChannel != null)
{
CommunicationState state = clientChannel.State;
// check its state
if (state == CommunicationState.Faulted)
{
// channel is in faulted state, close and recreate it
CloseChannel();
staticChannel.Value = CreateChannel();
}
else if ((state == CommunicationState.Closed) || (state == CommunicationState.Closing))
{
// channel is closed or closing, recreate it
staticChannel.Value = CreateChannel();
}
}
}
return staticChannel.Value;
}
/// <summary>
/// Closes the channel in a proper way
/// </summary>
private static void CloseChannel()
{
// always check if we still have a valid channel
if (staticChannel != null)
{
IClientChannel clientChannel = staticChannel as IClientChannel;
if (clientChannel != null)
{
// if the channel is open, we close it
if (clientChannel.State != CommunicationState.Closed)
{
clientChannel.Abort();
}
}
// and we set the static variable back to it's default ( = null )
staticChannel = null;
}
}
}