Skip to content

Commit f8eb5e1

Browse files
ggnaegiGuillaume Gnaegiraman-m
authored
#1634 #1487 #1329 #1304 #1294 #793 Consul polling of services: enhancements and fix errors (#1670)
* fixing some issues in poll consul: - Timer is not thread safe, avoiding usage of it - No Ressources are returned for first call - Using a providers pool, instead of creating a new provider instance * line endings * adding some test cases * Using a lock instead of SemaphoreSlim * Improve code readability * CA2211: Non-constant fields should not be visible * Use IOcelotLogger to remove warnings & messages of static code analysis (aka IDE0052) * Fix errors with unit tests discovery. Remove legacy life hacks of discovering tests on .NET Core * Update unit tests * Also refactoring the kubernetes provider factory (like consul and eureka) * shorten references... * const before... * Some minor fixes, using Equals Ordinal ignore case and a string constant for provider type definition instead of string litterals. Fixing usings. * waiting a bit longer then? * @RaynaldM code review * renaming PollKubernetes to PollKube * ... odd... * ... very odd, we have an issue with configuration update duration... * IDE0002: Name can be simplified * All tests passing locally, hopefully it works online * just a bit of cleanup * Some missing braces and commas * Update servicediscovery.rst: Review and update "Consul" section --------- Co-authored-by: Guillaume Gnaegi <[email protected]> Co-authored-by: raman-m <[email protected]>
1 parent ab29442 commit f8eb5e1

22 files changed

+610
-422
lines changed

docs/features/servicediscovery.rst

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,45 @@
33
Service Discovery
44
=================
55

6-
Ocelot allows you to specify a service discovery provider and will use this to find the host and port for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the
7-
GlobalConfiguration section which means the same service discovery provider will be used for all Routes you specify a ServiceName for at Route level.
6+
Ocelot allows you to specify a *service discovery* provider and will use this to find the host and port for the downstream service to which Ocelot forwards the request.
7+
At the moment this is only supported in the **GlobalConfiguration** section, which means the same *service discovery* provider will be used for all Routes for which you specify a ``ServiceName`` at Route level.
88

99
Consul
1010
------
1111

12-
The first thing you need to do is install the NuGet package that provides Consul support in Ocelot.
12+
| **Namespace**: `Ocelot.Provider.Consul <https://github.com/ThreeMammals/Ocelot/tree/develop/src/Ocelot.Provider.Consul>`_
13+
14+
The first thing you need to do is install `the NuGet package <https://www.nuget.org/packages/Ocelot.Provider.Consul>`_ that provides `Consul <https://www.consul.io/>`_ support in Ocelot.
1315

1416
.. code-block:: powershell
1517
1618
Install-Package Ocelot.Provider.Consul
1719
18-
Then add the following to your ConfigureServices method.
20+
Then add the following to your ``ConfigureServices`` method:
1921

2022
.. code-block:: csharp
2123
22-
s.AddOcelot()
23-
.AddConsul();
24+
ConfigureServices(services =>
25+
{
26+
services.AddOcelot()
27+
.AddConsul();
28+
});
29+
30+
Currently there are 2 types of Consul *service discovery* providers: ``Consul`` and ``PollConsul``.
31+
The default provider is ``Consul``, which means that if ``ConsulProviderFactory`` cannot read, understand, or parse the **Type** property of the ``ServiceProviderConfiguration`` object, then a ``Consul`` provider instance is created by the factory.
32+
33+
Explore these types of providers and understand the differences in the subsections below.
34+
35+
Consul Provider Type
36+
^^^^^^^^^^^^^^^^^^^^
37+
38+
| **Class**: `Ocelot.Provider.Consul.Consul <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+Consul&type=code>`_
2439
25-
The following is required in the GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default
26-
will be used.
40+
The following is required in the `GlobalConfiguration <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22FileGlobalConfiguration+GlobalConfiguration%22&type=code>`_.
41+
The **ServiceDiscoveryProvider** property is required, and if you do not specify a host and port, the Consul default ones will be used.
2742

28-
Please note the Scheme option defaults to HTTP. It was added in this `PR <https://github.com/ThreeMammals/Ocelot/pull/1154>`_. It defaults to HTTP to not introduce a breaking change.
43+
Please note the `Scheme <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22public+string+Scheme+%7B+get%3B+%7D%22+path%3A%2F%5Esrc%5C%2FOcelot%5C%2FConfiguration%5C%2F%2F&type=code>`_ option defaults to HTTP.
44+
It was added in `PR 1154 <https://github.com/ThreeMammals/Ocelot/pull/1154>`_. It defaults to HTTP to not introduce a breaking change.
2945

3046
.. code-block:: json
3147
@@ -38,7 +54,10 @@ Please note the Scheme option defaults to HTTP. It was added in this `PR <https:
3854
3955
In the future we can add a feature that allows Route specfic configuration.
4056

41-
In order to tell Ocelot a Route is to use the service discovery provider for its host and port you must add the ServiceName and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests.
57+
In order to tell Ocelot a Route is to use the *service discovery* provider for its host and port you must add the ServiceName and load balancer you wish to use when making requests downstream.
58+
At the moment Ocelot has a `RoundRobin <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20RoundRobin&type=code>`_
59+
and `LeastConnection <https://github.com/search?q=repo%3AThreeMammals%2FOcelot+LeastConnection&type=code>`_ algorithm you can use.
60+
If no load balancer is specified Ocelot will not load balance requests.
4261

4362
.. code-block:: json
4463
@@ -53,9 +72,15 @@ In order to tell Ocelot a Route is to use the service discovery provider for its
5372
},
5473
}
5574
56-
When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services.
75+
When this is set up Ocelot will lookup the downstream host and port from the *service discovery* provider and load balance requests across any available services.
5776

58-
A lot of people have asked me to implement a feature where Ocelot polls Consul for latest service information rather than per request. If you want to poll Consul for the latest services rather than per request (default behaviour) then you need to set the following configuration.
77+
PollConsul Provider Type
78+
^^^^^^^^^^^^^^^^^^^^^^^^
79+
80+
| **Class**: `Ocelot.Provider.Consul.PollConsul <https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20PollConsul&type=code>`_
81+
82+
A lot of people have asked me to implement a feature where Ocelot *polls Consul* for latest service information rather than per request.
83+
If you want to *poll Consul* for the latest services rather than per request (default behaviour) then you need to set the following configuration:
5984

6085
.. code-block:: json
6186
@@ -68,11 +93,19 @@ A lot of people have asked me to implement a feature where Ocelot polls Consul f
6893
6994
The polling interval is in milliseconds and tells Ocelot how often to call Consul for changes in service configuration.
7095

71-
Please note there are tradeoffs here. If you poll Consul it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volatile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling Consul per request (as sidecar agent). If you are calling a remote Consul agent then polling will be a good performance improvement.
96+
Please note there are tradeoffs here. If you *poll Consul* it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volatile your services are. I doubt it will matter for most people and polling may give a tiny performance improvement over calling Consul per request (as sidecar agent). If you are calling a remote Consul agent then polling will be a good performance improvement.
7297

73-
Your services need to be added to Consul something like below (C# style but hopefully this make sense)...The only important thing to note is not to add http or https to the Address field. I have been contacted before about not accepting scheme in Address and accepting scheme in address. After reading `this <https://www.consul.io/docs/agent/services.html>`_ I don't think the scheme should be in there.
98+
Service Definition
99+
^^^^^^^^^^^^^^^^^^
74100

75-
.. code-block: csharp
101+
Your services need to be added to Consul something like below (C# style but hopefully this make sense)...
102+
The only important thing to note is not to add ``http`` or ``https`` to the Address field.
103+
I have been contacted before about not accepting scheme in Address and accepting scheme in address.
104+
After reading `this <https://developer.hashicorp.com/consul/docs/agent/config>`_ I don't think the scheme should be in there.
105+
106+
In C#
107+
108+
.. code-block:: csharp
76109
77110
new AgentService()
78111
{
@@ -82,21 +115,22 @@ Your services need to be added to Consul something like below (C# style but hope
82115
ID = "some-id",
83116
}
84117
85-
Or
118+
Or, in JSON
86119

87120
.. code-block:: json
88121
89-
"Service": {
90-
"ID": "some-id",
91-
"Service": "some-service-name",
92-
"Address": "localhost",
93-
"Port": 8080
94-
}
122+
"Service": {
123+
"ID": "some-id",
124+
"Service": "some-service-name",
125+
"Address": "localhost",
126+
"Port": 8080
127+
}
95128
96129
ACL Token
97130
^^^^^^^^^
98131

99-
If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below.
132+
If you are using `ACL <https://developer.hashicorp.com/consul/commands/acl/token>`_ with Consul, Ocelot supports adding the "X-Consul-Token" header.
133+
In order so this to work you must add the additional property below:
100134

101135
.. code-block:: json
102136
@@ -248,7 +282,7 @@ This configuration means that if you have a request come into Ocelot on /product
248282
Please take a look through all of the docs to understand these options.
249283

250284
Custom Providers
251-
----------------------------------
285+
----------------
252286

253287
Ocelot also allows you to create your own ServiceDiscovery implementation.
254288
This is done by implementing the ``IServiceDiscoveryProvider`` interface, as shown in the following example:
Lines changed: 55 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,78 @@
1-
using Consul;
2-
using Ocelot.Infrastructure.Extensions;
1+
using Ocelot.Infrastructure.Extensions;
32
using Ocelot.Logging;
43
using Ocelot.ServiceDiscovery.Providers;
54
using Ocelot.Values;
65

7-
namespace Ocelot.Provider.Consul
8-
{
9-
public class Consul : IServiceDiscoveryProvider
10-
{
11-
private readonly ConsulRegistryConfiguration _config;
12-
private readonly IOcelotLogger _logger;
13-
private readonly IConsulClient _consul;
14-
private const string VersionPrefix = "version-";
6+
namespace Ocelot.Provider.Consul;
157

16-
public Consul(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory)
17-
{
18-
_config = config;
19-
_logger = factory.CreateLogger<Consul>();
20-
_consul = clientFactory.Get(_config);
21-
}
8+
public class Consul : IServiceDiscoveryProvider
9+
{
10+
private const string VersionPrefix = "version-";
11+
private readonly ConsulRegistryConfiguration _config;
12+
private readonly IConsulClient _consul;
13+
private readonly IOcelotLogger _logger;
2214

23-
public async Task<List<Service>> Get()
24-
{
25-
var consulAddress = (_consul as ConsulClient)?.Config.Address;
26-
_logger.LogDebug($"Querying Consul {consulAddress} about a service: {_config.KeyOfServiceInConsul}");
15+
public Consul(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory)
16+
{
17+
_logger = factory.CreateLogger<Consul>();
18+
_config = config;
19+
_consul = clientFactory.Get(_config);
20+
}
2721

28-
var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true);
22+
public async Task<List<Service>> Get()
23+
{
24+
var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true);
2925

30-
var services = new List<Service>();
26+
var services = new List<Service>();
3127

32-
foreach (var serviceEntry in queryResult.Response)
28+
foreach (var serviceEntry in queryResult.Response)
29+
{
30+
if (IsValid(serviceEntry))
3331
{
34-
var address = serviceEntry.Service.Address;
35-
var port = serviceEntry.Service.Port;
36-
37-
if (IsValid(serviceEntry))
32+
var nodes = await _consul.Catalog.Nodes();
33+
if (nodes.Response == null)
3834
{
39-
var nodes = await _consul.Catalog.Nodes();
40-
if (nodes.Response == null)
41-
{
42-
services.Add(BuildService(serviceEntry, null));
43-
}
44-
else
45-
{
46-
var serviceNode = nodes.Response.FirstOrDefault(n => n.Address == address);
47-
services.Add(BuildService(serviceEntry, serviceNode));
48-
}
49-
50-
_logger.LogDebug($"Consul answer: Address: {address}, Port: {port}");
35+
services.Add(BuildService(serviceEntry, null));
5136
}
5237
else
5338
{
54-
_logger.LogWarning($"Unable to use service Address: {address} and Port: {port} as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0");
39+
var serviceNode = nodes.Response.FirstOrDefault(n => n.Address == serviceEntry.Service.Address);
40+
services.Add(BuildService(serviceEntry, serviceNode));
5541
}
5642
}
57-
58-
return services.ToList();
43+
else
44+
{
45+
_logger.LogWarning(
46+
$"Unable to use service Address: {serviceEntry.Service.Address} and Port: {serviceEntry.Service.Port} as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0");
47+
}
5948
}
6049

61-
private static Service BuildService(ServiceEntry serviceEntry, Node serviceNode)
62-
{
63-
return new Service(
64-
serviceEntry.Service.Service,
65-
new ServiceHostAndPort(serviceNode == null ? serviceEntry.Service.Address : serviceNode.Name, serviceEntry.Service.Port),
66-
serviceEntry.Service.ID,
67-
GetVersionFromStrings(serviceEntry.Service.Tags),
68-
serviceEntry.Service.Tags ?? Enumerable.Empty<string>());
69-
}
50+
return services.ToList();
51+
}
7052

71-
private static bool IsValid(ServiceEntry serviceEntry)
72-
{
73-
if (string.IsNullOrEmpty(serviceEntry.Service.Address) || serviceEntry.Service.Address.Contains("http://") || serviceEntry.Service.Address.Contains("https://") || serviceEntry.Service.Port <= 0)
74-
{
75-
return false;
76-
}
53+
private static Service BuildService(ServiceEntry serviceEntry, Node serviceNode)
54+
{
55+
return new Service(
56+
serviceEntry.Service.Service,
57+
new ServiceHostAndPort(serviceNode == null ? serviceEntry.Service.Address : serviceNode.Name,
58+
serviceEntry.Service.Port),
59+
serviceEntry.Service.ID,
60+
GetVersionFromStrings(serviceEntry.Service.Tags),
61+
serviceEntry.Service.Tags ?? Enumerable.Empty<string>());
62+
}
7763

78-
return true;
79-
}
64+
private static bool IsValid(ServiceEntry serviceEntry)
65+
{
66+
return !string.IsNullOrEmpty(serviceEntry.Service.Address)
67+
&& !serviceEntry.Service.Address.Contains("http://")
68+
&& !serviceEntry.Service.Address.Contains("https://")
69+
&& serviceEntry.Service.Port > 0;
70+
}
8071

81-
private static string GetVersionFromStrings(IEnumerable<string> strings)
82-
=> strings?
83-
.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal))
84-
.TrimStart(VersionPrefix);
72+
private static string GetVersionFromStrings(IEnumerable<string> strings)
73+
{
74+
return strings
75+
?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal))
76+
.TrimStart(VersionPrefix);
8577
}
8678
}
Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
1-
using Consul;
1+
namespace Ocelot.Provider.Consul;
22

3-
namespace Ocelot.Provider.Consul
3+
public class ConsulClientFactory : IConsulClientFactory
44
{
5-
public class ConsulClientFactory : IConsulClientFactory
5+
public IConsulClient Get(ConsulRegistryConfiguration config)
66
{
7-
public IConsulClient Get(ConsulRegistryConfiguration config)
7+
return new ConsulClient(c =>
88
{
9-
return new ConsulClient(c =>
10-
{
11-
c.Address = new Uri($"{config.Scheme}://{config.Host}:{config.Port}");
9+
c.Address = new Uri($"{config.Scheme}://{config.Host}:{config.Port}");
1210

13-
if (!string.IsNullOrEmpty(config?.Token))
14-
{
15-
c.Token = config.Token;
16-
}
17-
});
18-
}
11+
if (!string.IsNullOrEmpty(config?.Token)) c.Token = config.Token;
12+
});
1913
}
2014
}

0 commit comments

Comments
 (0)