The final in the broad posts about the capabilities of the Routing Service, today we're going to cover Dynamic Reconfiguration. Dynamic Reconfig is really one of the neatest things about the Routing Service (in my opinion) as it opens up a whole new set of scenarios that otherwise would have required a lot of custom code and tricks to implement.
Dynamic reconfig is the result of questions like the following:
- When self or web hosting how do I get updates into the Router?
- During runtime what is it even safe to change?
- What if my deployment changes (services move)?
- What happens if I need to add a new rule to my Router to capture new traffic?
- What happens if I notice some error in my configuration that I want to adjust?
- How do I get my rules into the Routing Service if I decide to store them somewhere else?
When building the Routing Service, it was initially (and still is primarily) configuration driven. If you were self hosted, and you wanted to change your configuration, you’d have to throw together some file watcher and be able to know how not to interrupt messages and connections that were in flight before shutting down the host to pick up the new config. This is non-trivial, especially outside of the service implementation proper where user code would live. If you were webhosted (in IIS) you’d run into the “my config changed and now I have a (seemingly arbitrary) set amount of time before my host recycles and I lose any connections that were in play”. Since sorting out these questions is tricky enough, we felt like we could add some value by not making our customers deal with them. The solution we arrived at is Dynamic Reconfig.
Let’s rewind for a second and remember how we configure the Routing Service normally through code:
//set up some communication defaults
string clientAddress = "http://localhost:8000/servicemodelsamples/service";
string routerAddress = "http://localhost:8000/routingservice/service";
Binding routerBinding = new WSHttpBinding();
Binding clientBinding = new WSHttpBinding();
//add the endpoint the router will use to receive messages
serviceHost.AddServiceEndpoint(typeof(IRequestReplyRouter), routerBinding, routerAddress);
//create the client endpoint the router will route messages to
ContractDescription contract = ContractDescription.GetContract(typeof(IRequestReplyRouter));
ServiceEndpoint client = new ServiceEndpoint(contract, clientBinding, new EndpointAddress(clientAddress));
//create a new routing configuration object
RoutingConfiguration rc = new RoutingConfiguration();
//create the endpoint list that contains the service endpoints we want to route to
//in this case we have only one
List<ServiceEndpoint> endpointList = new List<ServiceEndpoint>();
//add a MatchAll filter to the Router's filter table
//map it to the endpoint list defined earlier
//when a message matches this filter, it will be sent to the endpoint contained in the list
rc.FilterTable.Add(new MatchAllMessageFilter(), endpointList);
//attach a new RoutingBehavior with this RoutingConfiguration to the service host
Note how most of the logic is actually encapsulated in one object, the RoutingConfiguration. The RoutingConfiguration object describes the message filter table that the Routing Service should use, which includes the message filters and client endpoints.
Dynamic config works by allowing you to hand the Routing Service a new RoutingConfiguration during runtime, which the Routing Service then picks up and uses. In doing so, we make sure that messages and connections that were in flight continue to use the RoutingConfiguration (and thus filters) that was in place when they started being processed.
As the diagram indicates, the API for applying a new RoutingConfiguration to the Router during runtime is (creatively) called “ApplyConfiguration”. It takes a RoutingConfiguration object, and is located on a ServiceExtention attached to the Routing Service called the RoutingExtension. We used a ServiceExtension so that you could write your own ServiceExtension code that is able to find our extension and call this method. Your ServiceExtension would have some code similar to that above: newing up a RoutingConfiguration, creating some MessageFilters and Endpoints, and then calling ApplyConfiguration on our extension.
But where do you get the information for building these filters? Well, it could be anywhere, but the key is that you probably want it to be event driven: something happens, a callback fires, and that data is used to create a new set of filters. What is that event? Well, it could be a file watcher watching some external configuration store, it could be a Discovery Announcement, it could be a SQL or Velocity Cache event notification, whatever. Note that since this can happen whenever you want, it can happen immediately on service startup, meaning that you’re free to leave the Routing Service’s App or Web.config entirely out of the process and do it all in memory dynamically.
As far as the performance of applying new rules, it’s pretty snappy, since internally it’s basically a pointer move. The far more complex step is the construction of the rules themselves. What about the impact to the memory footprint of having all these extra sets of rules hanging around? Well luckily MessageFilters and MessageFilterTables are pretty small, so by themselves they’re not a big problem. Furthermore, since a RoutingConfiguration is only referred to by the sessions that are using it, once all those sessions are closed, that RoutingConfiguration gets GC’d. In some ad-hoc tests we did where we opened connections that never died and also kept calling ApplyConfiguration() with a new RoutingConfiguration each time, we ran out of TCP connections on the box before we ran out of memory. Unless your RoutingConfiguration’s MessageFilterTable is exceptionally huge, you’re very unlikely to see any issues as a result of this feature (besides, hopefully, an increase in the flexibility of the solutions you’re able to build).