I’ve created a plugin for registering/deregistering and resolving ServiceStack services with Consul.io.
It’s pretty basic at the moment but it would be great to get some feedback on my approach so far
Sounds like a good idea, never heard of Consul before so I had to quickly look it up, seems like a nice way to maintain active service configurations. It’s reassuring that it integrates nicely with AWS:
https://aws.amazon.com/blogs/compute/service-discovery-via-consul-with-amazon-ecs/
I noticed you’re making JSON Service calls but I’ve also just read Consul also supports DNS queries. Is there something you need that would prevent using DNS queries for this? Just thinking it might be more transparent if you could use “virtual addresses” and have it resolve to IP’s/physical addresses using DNS.
Consul supports the concept of queries which I am planning to try so that I can find services on more than one dimension (both healthy services AND match a tag) in a single call.
Not sure if the query can support dns but if it does then I will investigate that.
Thanks for the feedback
As far as I can tell the DNS queries won’t work for a couple of reasons.
They won’t resolve services using a HandlerFactoryPath as DNS doesn’t support this, only IP:Port
The default service discovery strategy is based on the Request type which I’ve stored as a Tag in Consul for the service. You cannot use DNS with a tag without including the service i.e. > dig tag.service.domain
The resolver doesn’t know anything about the service, only the request type name so it cannot construct this type of DNS request.
So in consul Service = AppHost and Tag = RequestDTO
Since it is recommended practice from Consul to always interact with a local agent the overhead in hitting the http api should be minimal in terms of latency but it isn’t as transparent.
The least intrusive method I have found for wiring this up so far is to have the external base uri for the service in the config. A deployment pipeline should then hopefully be able to set this.
I’ve set this up on Nuget to make it a bit easier to try out https://www.nuget.org/packages/ServiceStack.Discovery.Consul/
ok no worries, it was worth investigating.
By the way I’ve just refactored the service clients to make this use-case easier to handle, e.g. instead of resolving a new client for each DTO:
var client = new JsonServiceClient().TryGetClientFor<ExternalDTO>();
var response = client.Send(new ExternalDTO { Custom = "bob" });
You can now just initialize the a UrlResolver
and TypedUrlResolver
hooks once with the client, e.g:
var client = new JsonServiceClient(baseUrl) {
TypedUrlResolver = (meta, httpMethod, dto) =>
Consul.Resolve(meta.BaseUrl, dto.GetType()).CombineWith(dto.ToUrl(httpMethod)))
};
Which will then get called for each request, so you should no longer need to resolve a new client for each DTO:
var response = client.Send(new ExternalDTO { Custom = "bob" });
var response = client.Send(new ExternalDTO2 { Custom = "bob" });
Allowing it to transparently work without affecting existing service client requests, decoupling the concrete dependency from client call-sites allowing an injectable strategy for url resolution.
There’s some more examples in ServiceClientResolverTests.cs
There’s also
client.UrlResolver
for API requests that provide a relative or absolute url (i.e. instead of a Request DTO).
This change is available from v4.0.53 that’s now available on MyGet.
wow! looks great. I’d played around with a few ways to try and create as easy usage as possible but the resolver approach is much cleaner.
I’m not very familiar with Func, is there a clean way for the plugin to autowire this (I’m really thinking about interceptors in the IoC sense) so that the plugin consumers don’t have to worry about calling the consul client. I’m trying to make it as ‘low ceremony’ as possible.
On the one hand injecting something into the client could get in the way of other resolvers so it’s probably bad but making the user manually wire up the resolvers for the consul plugin seems like ceremony
or am I missing a more obvious solution?
It’s just a delegate so you can just assign a function to it, e.g you can declare it with something like:
public class Consul
{
public static string ResolveTypedUrl(
IServiceClientMeta client, string httpMethod, object dto)
{
return ...;
}
}
And then assign the function, e.g:
container.Register<IServiceClient>(c => new JsonServiceClient(baseUrl) {
TypedUrlResolver = Consul.ResolveTypedUrl
});
Which will automatically get resolved when resolving from the IOC:
var client = container.Resolve<IServiceClient>();
Or injected as a dependency like normal, e.g:
public class MyServices : Service
{
public IServiceClient Client { get; set; }
...
}
Hi Scott,
Let me know if you’re able to update your ServiceStack.Discovery.Consul
plugin + docs to use the new this new feature before next weeks release so I can mention it in the release notes as an example of taking advantage of this feature?
I should be able to find a few hours in the next couple of days to update.
Hi Demis,
that’s my first effort at refactoring for the serviceclients changes now up and building. I haven’t pushed the nuget package yet, will wait until you release.
One thing I haven’t yet got to work is the fallback routes for remote dto’s. The following test fails complaining correctly that the TestDTO is not registered with the service when calling dto.GetRelativeUrl(“GET”). Any suggestions for this?
The issue is that when you call dto.ToUrl()
extension method you also need to pass the Format
that the url should be generated for. as it’s required when creating the fallback pre-defined route, I’ve updated my earlier example to include this:
var client = new JsonServiceClient(baseUrl) {
TypedUrlResolver = (meta, httpMethod, dto) => Consul.Resolve(
meta.BaseUrl, dto.GetType()).CombineWith(dto.ToUrl(httpMethod, meta.Format)))
};
Thanks,
Fixed that. One other issue I’m having is with client.Send(dto)
. Example can be seen below when ServiceB tries to call ServiceA. It throws a null path exception. If I specify a path in the ServiceClient constructor, it uses that path rather than the TypedUrlResolverDelegate
.
Works fine with client.Post(dto)
when ServiceA calls ServiceB.
The Send
API’s always use the pre-defined fallback routes by design, which makes use of SyncReplyBaseUri
and AsyncOneWayBaseUri
that’s derived from the BaseUrl, so it always needs for the ServiceClient to be configured with the BaseUrl.
You’d need to use the explicit verb to use the configured Request DTO Routes, e.g:
var remoteResponse = client.Post(new EchoA());
so Send
api’s can’t use the TypedUrlResolverDelegate
?
That would be a breaking change, the Send API’s are a way to avoid reverse routing and use the pre-defined routes, so they only use the UrlResolver that’s injected with the pre-defined relative url.
So if I understand that correctly, if I wanted to allow Send(dto)
requests, I’d have to implement my own method overloads or just have the client either throw an exception or never correctly resolve the uri for remote dto’s
Leads me back to the TryGetClientFor<T>()
approach or creating extension overload client.Send(dto, true)
public static class ServiceClientExtensions
{
public static string Send<T>(this IServiceClientMeta client, T dto, bool useTypedResolver)
{
var method = dto.GetType().FirstAttribute<RouteAttribute>()?.Verbs.SplitOnFirst(',').FirstOrDefault() ?? "GET";
return Consul.ResolveTypedUrl(client, method, dto);
}
}
Are you handling untyped UrlResolver as well? as you will be able to handle the url used with that.
The pre-defined routes are also predictable so you should also be able to fetch the Request DTO name with:
var parts = url.SplitOnFirst('?').First().Split('/');
if (parts.Length > 2 &&
(parts[parts.Length - 2] == "reply" || parts[parts.Length - 2] == "oneway"))
{
var requestDtoName = parts[parts.Length-1];
}
I’d avoid using TryGetClientFor<T>
at the call-site as it’s invasive and makes it harder to adopt it transparently in existing projects.
Not currently handling untyped at the moment but if the Send methods always use the predefined route formats, then as you say I can predict the requestDto name which I need to resolve the baseUri from consul.
I was trying to avoid having to think about registering both the requestDto names and all possible routes from the apphost but perhaps this would provide a more rounded solution.
Agree about avoiding TryGetClientFor<T>
for being invasive. Rather not go down that route (pardon the pun) if at all possible.
p.s. thanks for all the help btw, much appreciated.
Hey I’ve updated the behavior of the Send<T>
API’s to use explicit Verb API’s (e.g. Get<T>
) for Request DTO’s that are attributed with IVerb interfaces (e.g. IGet
) in this commit.
Essentially it means these 2 API’s are equivalent:
public class GetFoo : IReturn<FooResponse>, IGet { ... }
var response = client.Get(new GetFoo { ... });
var response = client.Send(new GetFoo { ... });
Or using any other IVerb
, e.g:
public class CreateFoo : IReturn<FooResponse>, IPost { ... }
var response = client.Post(new CreateFoo { ... });
var response = client.Send(new CreateFoo { ... });
This allows Request DTO’s marked with IVery interface using Send API’s to make use of the TypedUrlResolver
just as if they called the client.Get<T>
API’s directly.
This change is available from v4.0.55 that’s now available on MyGet.
Great, that should avoid the null reference exceptions when using the Send API’s without first configuring the UrlResolver delegate if the IVerb interface is implemented.
I’ve experimented with configuring the UrlResolver in the plugin to correctly resolve auto-generated routes so the Send api’s work anyway and will update the plugin once I’ve tested it, but when it comes to custom [Route]
implementations, there really is very little that can be done with consul as far as I can tell.
All I can come up with for that, is that you would have to add the entire route table to each apphost as tags, iterate all registered apphosts to merge the route tables back together when resolving, somehow know the ordering and somehow avoid clashes of overly generic or fallback routes, then get the matching route url, then look that tag up in consul. ick!
Pretty sure that would be a disaster
Internally we are going with the fact that we simply cannot use custom routes to avoid that minefield.