all Technical posts

Sentinet Extensibility – Custom Routing

Sentinet is highly extendable through standard Microsoft .NET, WCF and WIF extensibility points, and through the Sentinet API interfaces.

In this 4th post I want to continue the Sentinet Extensibility series exploring another possible customization, the routing.

Routing

The Routing feature allows to deliver the messages received on the virtual inbound endpoint to more than one alternative endpoint of the same backend service. When the backend service exposes multiple endpoints, some of them (or all) can be included in message routing by marking them using the Sentinet Administrative Console. Notice that, to activate the Routing at least two backend endpoints must be selected.

The Sentinet routing feature improves the API availability with an automatic failover. This means that, in case of communication failure, the Sentinet Node falls back to the next available endpoint (this does not happen in case of the SOAP Fault because it’s considered as a valid response).

Sentinet supports four router types:

  • Round-Robin with priority or equal distribution of the load. This is the default routing mode, the fallback is automatic.
  • Simple Fault Tolerance. The routing mechanism always hit the endpoint with the highest priority then, in case of communication failure, it falls back to the endpoint with lowest priority.
  • Multicast. A copy of the incoming message is delivered to all the endpoints.
  • Custom. The routing rules are defined in a custom .NET component.

Scenario

Here are  the requirements for this scenario: 

  • Sentinet is deployed behind the network load balancer and the customer doesn’t want to pass again through the NLB.
  • The virtualized backend service has nine endpoints (three per continent) and the load should be routed depending on which continent the request is coming from.
  • The load routed to Europe and to North America should be equally distributed between the endpoints (simple round robin). 
  • The load routed to Asia should always hit a specific endpoint and in case of error must fall back to the others (simple fault tolerance). 

In short, we want to build a geography-based custom router that merges the Round-Robin and the Fault-Tolerance types. To build the GeoRouter I started with the example that I found in the Sentinet SDK.

Scenario

Build the custom Router

A Sentinet custom Router is a regular .NET component that implements the IRouter interface (ref. Nevatech.Vbs.Repository.dll) and MessageFilter abstract class.

The IRouter  inteface contains three methods: 
– GetRoutes – Where to define the routing rules. 
– ImportConfiguration – Read the (and apply) the component’s configuration.    
– ExportConfiguration – Save the component its configuration.

 

The custom router reads the component configurations where we define which endpoint is contained in which region (continent) and the type of routing to be applied. Based on this XML the GetRoutes method creates the Route object which is responsible for the message delivery.

<Regions>
  <Region code="NA" roundRobin="true">
    <!-- North America -->
    <Endpoint>net.tcp://northamerica1/CustomerSearch/4</Endpoint>
    <Endpoint>net.tcp://northamerica2/CustomerSearch/5</Endpoint>
    <Endpoint>net.tcp://northamerica3/CustomerSearch/6</Endpoint>
  </Region>
  <Region code="AS" roundRobin="false">
    <!-- Asia -->
    <Endpoint>net.tcp://asia1/CustomerSearch/7</Endpoint>
    <Endpoint>net.tcp://asia2/CustomerSearch/8</Endpoint>
    <Endpoint>net.tcp://asia3/CustomerSearch/9</Endpoint>
  </Region>
  <Region code="EU" roundRobin="true">
    <!-- Europe -->
    <Endpoint>net.tcp://europe1/CustomerSearch/1</Endpoint>
    <Endpoint>net.tcp://europe2/CustomerSearch/2</Endpoint>
    <Endpoint>net.tcp://europe3/CustomerSearch/3</Endpoint>
  </Region>
</Regions>

 

The GetRoutes method, returns a collection of Route objects. A Route is composed by a Filter expression, an EndpointCollection and a Priority.

Route

 

How the sentinet engine processes the Collection<Route> object?

The Sentinet engine processes the routes one by one according to the order defined (priority field) untill a first match occurs. Then, when the filter criteria is matched, the request message is sent to the first endpoint in the EndpointCollection. If the current endpoint throws an exception Sentinet fallbacks to the next endpoint in the collection.

 

How to populate the Collection<Route> to achieve our goals?

The fallback is automatically implemented by Sentinet every time that in the Route’s endpoint collection there are more than one endpoint. So the creation of a route which contains one single endpoint disables the fallback mechanism.

 

The round robin mechanism implemented in this demo is very simple. Basically the distribution of the load between the endponts is achieved :

– Creating a number of routes equal to the number of the endpoint in that region (e.g. in europe we have 3 endpoint so 3 routes are created and added to the collection).

– Every route has a different a filter expression based on a random number.

– In every route’s endpoint collection, the items are sorted in a different order to prioritize a different endpoint at every iteration.

 

Here a visual representation of the Routes collection to achieve the RoundRobin + Automatic fallback

Route 2

Automatic fallback without round robin

Route 3

Round robin without the automatic fallback (not implemented in this example)

Route 4

 

So what does the code do? Basically, it reads the collection of the endpoint that we checkmarked during the virtual service design and if the endpoint is contained in the XML configuration it is added to the continent-based route object.

 

Here the GetRoutes code

        public IEnumerable<Route> GetRoutes(IEnumerable<EndpointDefinition> backendEndpoints)
        {
            if (backendEndpoints == null) throw new ArgumentNullException("backendEndpoints");

            // Validate router configuration
            if (!Validate()) throw new ValidationException(ErrorMessage);

            // Collection of routes to be returned
            Collection<Route> routes = new Collection<Route>();

            // Ordered collection of outbound endpoints used in a single route
            Collection<EndpointDefinition> routeEndpoints = new Collection<EndpointDefinition>();

            // The order of a route in a routing table 
            byte priority = Byte.MaxValue;

            foreach (Region region in Regions)
            {
                // Collection can be reused as endpoints are copied in Route() constructor
                routeEndpoints.Clear();

                // collection of the backend endpoint per region 
                foreach (string endpointUri in region.Endpoints)
                {
                    // Find outbound endpoint by its AbsoluteURI
                    EndpointDefinition endpoint = backendEndpoints.FirstOrDefault(e => String.Equals(e.LogicalAddress.AbsoluteUri, endpointUri, StringComparison.OrdinalIgnoreCase));
                    if (endpoint == null) throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, InvalidRouterConfiguration, endpointUri));
                    routeEndpoints.Add(endpoint);
                }

                if (region.EnableRoundRobin)
                {
                    // build a route for each endpoint in the region
                    var iEndpointIndex = 0;
                    foreach (string endpointUri in region.Endpoints)
                    {
                        // change the backend's endpoint order 
                        if (iEndpointIndex > 0) SortEndpoints(routeEndpoints, iEndpointIndex - 1);

                        // Configure message filter for the current route
                        var rrFilter = new GeoMessageFilter
                        {
                            ContinentCode = region.Code,
                            RoundRobin = region.EnableRoundRobin,
                            BalancingFactor = GetBalancingFactor(iEndpointIndex)
                        };

                        routes.Add(new Route(rrFilter, routeEndpoints, priority));
                        iEndpointIndex++;
                        priority--;
                    }
                }
                else
                {
                    // build a route for each region
                    var filter = new GeoMessageFilter
                    {
                        ContinentCode = region.Code,
                        RoundRobin = false
                    };
                    // endpoint Fallback scenario
                    routes.Add(new Route(filter, routeEndpoints, priority));
                }
                priority--;
            }

            return routes;
        }

And the messageFilter class

    public sealed class GeoMessageFilter : MessageFilter
    {
        #region Properties

        public String ContinentCode { get; set; }
        public bool RoundRobin { get; set; }
        public double BalanceFactor { get; set; }

        private static Random random = new Random(); 
        #endregion

        #region Methods

        public override bool Match(Message message)
        {
            var remoteProps = (RemoteEndpointMessageProperty) message.Properties[RemoteEndpointMessageProperty.Name];
            return Match(remoteProps.Address, ContinentCode);
        }


        private bool Match(string ipAddress, string continentCode)
        {
            var requestCountryCode = GeoLocation.GetCountryCode(ipAddress);
            var matchTrue = (CountryMap.GetContinentByCountryCode(requestCountryCode) == continentCode.ToUpperInvariant());

            if (matchTrue && RoundRobin)
            {
                if (random.Next(0, 100) > BalanceFactor) return false;
            }
            return matchTrue;
        }

        #endregion
    }

Register and configure

The custom component can be registered and graphically configured using the Sentinet Administrative Console. Go to the design tab of the virtual service and click modify, then select the endpoint you want to be managed by the Routing component. On the endpoint tree node click the ellipsis button.

Register En Configure

Add new Custom Router, specifying few parameters:

  • Name. The fiendly name of the custom router (GeoRouter)
  • Assembly. The fully qualified assembly name that contains the Router implementation (Codit.Demo.Sentinet.Router,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null)
  • Type. The .NET class that implement the IRouter interface (Codit.Demo.Sentinet.Router.Geo.GeoRouter)
  • Default Configuration. In this example I let it blank and I will specify the parameters when I use the

 

Select the router and set the custom configuration.

Router Configuration

Save the process and wait for the next heartbeat so that the modifications will be applied.

Test

To test the virtual service with the brand new custom router, this time I tried WcfStorm.Rest.

Test case #1 – All the nine endpoints were available.

The messages have been routed to the specific continent and load has been distributed among the backend services as expected.

I collected in this image the backend services monitor (top left) and the map displays the sources of the service calls.

As you can see the the basic load balancer is not bullet proof, but the load is spread almost equally which is acceptable for this proof of concept.

Test

 

Test case #2 –  Fall back test on the European region.

I shut down the europe1 and europe2 services, so only the europe3 service was active.

Thanks to the fallback mechanism, the virtual service always responded. In the monitor tab you can check the fallback in action.

Test With Fallback

 

Test case #3 – All the European backend services were stopped.

Means that a route had a valid matchfilter, Sentinet tried to contact all the endpoints in the endpoint collection but without any success in evey attempt. Here under, it’s reported the error message we got. Notice that the address reported will be different depending on the route has been hit.

<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">
The message could not be dispatched because the service at the endpoint address 
'net.tcp://europe3/CustomerSearch/3' is unavailable for the protocol of the address.
</string>

Test case #4 – No matching rules.

If there are no matching rules (e.g. sending messages from South America) this following error message is returned.

<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">
No matching MessageFilter was found for the given Message.</string>

Conclusion

Sentinet is designed to be extensible in multiple areas of the product. In this post I’ve demonstrated how to create a Geography-based custom Router that combines the round-robin and the fault tolerance features. In the next post I will discuss about the Sentinet management APIs.

 

Cheers,

Massimo

Subscribe to our RSS feed

Thanks, we've sent the link to your inbox

Invalid email address

Submit

Your download should start shortly!

Stay in Touch - Subscribe to Our Newsletter

Keep up to date with industry trends, events and the latest customer stories

Invalid email address

Submit

Great you’re on the list!