Skip to content

Commit 00a68c7

Browse files
committed
Add IDiscoveryClient.InstancesFetched event
1 parent d95d9bf commit 00a68c7

13 files changed

Lines changed: 288 additions & 20 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Steeltoe.Common.Discovery;
6+
7+
/// <summary>
8+
/// Provides data for the <see cref="IDiscoveryClient.InstancesFetched" /> event.
9+
/// </summary>
10+
public sealed class DiscoveryInstancesFetchedEventArgs : EventArgs
11+
{
12+
/// <summary>
13+
/// Gets the updated list of service instances, grouped by service ID.
14+
/// </summary>
15+
public IReadOnlyDictionary<string, IReadOnlyList<IServiceInstance>> InstancesByServiceId { get; }
16+
17+
public DiscoveryInstancesFetchedEventArgs(IReadOnlyDictionary<string, IReadOnlyList<IServiceInstance>> instancesByServiceId)
18+
{
19+
ArgumentNullException.ThrowIfNull(instancesByServiceId);
20+
21+
InstancesByServiceId = instancesByServiceId;
22+
}
23+
}

src/Common/src/Common/Discovery/IDiscoveryClient.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public interface IDiscoveryClient
1414
/// </summary>
1515
string Description { get; }
1616

17+
/// <summary>
18+
/// Occurs when service instances have been fetched from the discovery server.
19+
/// </summary>
20+
public event EventHandler<DiscoveryInstancesFetchedEventArgs> InstancesFetched;
21+
1722
/// <summary>
1823
/// Gets information used to register the local service instance (this app) to the discovery server.
1924
/// </summary>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
#nullable enable
2+
Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs
3+
Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs.DiscoveryInstancesFetchedEventArgs(System.Collections.Generic.IReadOnlyDictionary<string!, System.Collections.Generic.IReadOnlyList<Steeltoe.Common.Discovery.IServiceInstance!>!>! instancesByServiceId) -> void
4+
Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs.InstancesByServiceId.get -> System.Collections.Generic.IReadOnlyDictionary<string!, System.Collections.Generic.IReadOnlyList<Steeltoe.Common.Discovery.IServiceInstance!>!>!
5+
Steeltoe.Common.Discovery.IDiscoveryClient.InstancesFetched -> System.EventHandler<Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs!>!

src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ private sealed class TestDiscoveryClient : IDiscoveryClient
114114
{
115115
public string Description => throw new NotImplementedException();
116116

117+
#pragma warning disable CS0067 // The event is never used
118+
public event EventHandler<DiscoveryInstancesFetchedEventArgs>? InstancesFetched;
119+
#pragma warning restore CS0067 // The event is never used
120+
117121
public Task<ISet<string>> GetServiceIdsAsync(CancellationToken cancellationToken)
118122
{
119123
throw new NotImplementedException();

src/Discovery/src/Configuration/ConfigurationDiscoveryClient.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.ObjectModel;
56
using Microsoft.Extensions.Options;
67
using Steeltoe.Common.Discovery;
78

@@ -10,17 +11,69 @@ namespace Steeltoe.Discovery.Configuration;
1011
/// <summary>
1112
/// A discovery client that reads service instances from app configuration.
1213
/// </summary>
13-
public sealed class ConfigurationDiscoveryClient : IDiscoveryClient
14+
public sealed class ConfigurationDiscoveryClient : IDiscoveryClient, IDisposable
1415
{
1516
private readonly IOptionsMonitor<ConfigurationDiscoveryOptions> _optionsMonitor;
17+
private readonly IDisposable? _changeTokenRegistration;
1618

1719
public string Description => "A discovery client that returns service instances from app configuration.";
1820

21+
/// <summary>
22+
/// Occurs when the configuration of service instances has been reloaded.
23+
/// </summary>
24+
public event EventHandler<DiscoveryInstancesFetchedEventArgs>? InstancesFetched;
25+
1926
public ConfigurationDiscoveryClient(IOptionsMonitor<ConfigurationDiscoveryOptions> optionsMonitor)
2027
{
2128
ArgumentNullException.ThrowIfNull(optionsMonitor);
2229

2330
_optionsMonitor = optionsMonitor;
31+
_changeTokenRegistration = optionsMonitor.OnChange(OnOptionsChanged);
32+
}
33+
34+
private void OnOptionsChanged(ConfigurationDiscoveryOptions options)
35+
{
36+
if (InstancesFetched != null)
37+
{
38+
ReadOnlyDictionary<string, IReadOnlyList<IServiceInstance>> instancesByServiceId = ToServiceInstanceMap(options.Services);
39+
var eventArgs = new DiscoveryInstancesFetchedEventArgs(instancesByServiceId);
40+
RaiseFetchEvent(eventArgs);
41+
}
42+
}
43+
44+
private static ReadOnlyDictionary<string, IReadOnlyList<IServiceInstance>> ToServiceInstanceMap(IList<ConfigurationServiceInstance> services)
45+
{
46+
// @formatter:wrap_chained_method_calls chop_always
47+
// @formatter:wrap_before_first_method_call true
48+
49+
return services
50+
.Where(service => service.ServiceId != null)
51+
.GroupBy(service => service.ServiceId!, StringComparer.OrdinalIgnoreCase)
52+
.ToDictionary(grouping => grouping.Key, grouping => (IReadOnlyList<IServiceInstance>)grouping
53+
.Select(instance => (IServiceInstance)instance)
54+
.ToList()
55+
.AsReadOnly())
56+
.AsReadOnly();
57+
58+
// @formatter:wrap_before_first_method_call restore
59+
// @formatter:wrap_chained_method_calls restore
60+
}
61+
62+
private void RaiseFetchEvent(DiscoveryInstancesFetchedEventArgs eventArgs)
63+
{
64+
// Execute on separate thread, so we won't block the configuration system in case the handler logic is expensive.
65+
ThreadPool.QueueUserWorkItem(_ =>
66+
{
67+
try
68+
{
69+
InstancesFetched?.Invoke(this, eventArgs);
70+
}
71+
catch (Exception)
72+
{
73+
// Intentionally left empty. Adding a logger to the constructor is a breaking change.
74+
// Adding an extra constructor confuses the service container.
75+
}
76+
});
2477
}
2578

2679
/// <inheritdoc />
@@ -54,4 +107,10 @@ public Task ShutdownAsync(CancellationToken cancellationToken)
54107
{
55108
return Task.CompletedTask;
56109
}
110+
111+
/// <inheritdoc />
112+
public void Dispose()
113+
{
114+
_changeTokenRegistration?.Dispose();
115+
}
57116
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.Dispose() -> void
3+
Steeltoe.Discovery.Configuration.ConfigurationDiscoveryClient.InstancesFetched -> System.EventHandler<Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs!>?

src/Discovery/src/Consul/ConsulDiscoveryClient.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ public sealed class ConsulDiscoveryClient : IDiscoveryClient
3030
/// <inheritdoc />
3131
public string Description => "A discovery client for HashiCorp Consul.";
3232

33+
/// <summary>
34+
/// This event is never raised. The Consul client doesn't implement caching.
35+
/// </summary>
36+
#pragma warning disable CS0067 // The event is never used
37+
public event EventHandler<DiscoveryInstancesFetchedEventArgs>? InstancesFetched;
38+
#pragma warning restore CS0067 // The event is never used
39+
3340
/// <summary>
3441
/// Initializes a new instance of the <see cref="ConsulDiscoveryClient" /> class.
3542
/// </summary>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
Steeltoe.Discovery.Consul.ConsulDiscoveryClient.InstancesFetched -> System.EventHandler<Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs!>?

src/Discovery/src/Eureka/EurekaDiscoveryClient.cs

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5+
using System.Collections.ObjectModel;
56
using System.Text;
67
using Microsoft.Extensions.Logging;
78
using Microsoft.Extensions.Options;
@@ -69,6 +70,9 @@ internal ApplicationInfoCollection Applications
6970
/// </summary>
7071
public event EventHandler<ApplicationsFetchedEventArgs>? ApplicationsFetched;
7172

73+
/// <inheritdoc />
74+
public event EventHandler<DiscoveryInstancesFetchedEventArgs>? InstancesFetched;
75+
7276
public EurekaDiscoveryClient(EurekaApplicationInfoManager appInfoManager, EurekaClient eurekaClient,
7377
IOptionsMonitor<EurekaClientOptions> clientOptionsMonitor, HealthCheckHandlerProvider healthCheckHandlerProvider, TimeProvider timeProvider,
7478
ILogger<EurekaDiscoveryClient> logger)
@@ -326,7 +330,8 @@ internal async Task FetchRegistryAsync(bool doFullUpdate, CancellationToken canc
326330

327331
await _registryFetchAsyncLock.WaitAsync(cancellationToken);
328332

329-
ApplicationsFetchedEventArgs eventArgs;
333+
ApplicationsFetchedEventArgs? applicationsEventArgs = null;
334+
DiscoveryInstancesFetchedEventArgs? instancesEventArgs = null;
330335

331336
try
332337
{
@@ -344,33 +349,75 @@ internal async Task FetchRegistryAsync(bool doFullUpdate, CancellationToken canc
344349

345350
UpdateLastRemoteInstanceStatusFromCache();
346351

347-
eventArgs = new ApplicationsFetchedEventArgs(_remoteApps);
352+
if (ApplicationsFetched != null)
353+
{
354+
applicationsEventArgs = new ApplicationsFetchedEventArgs(_remoteApps);
355+
}
356+
357+
if (InstancesFetched != null)
358+
{
359+
ReadOnlyDictionary<string, IReadOnlyList<IServiceInstance>> instancesByServiceId = ToServiceInstanceMap(_remoteApps);
360+
instancesEventArgs = new DiscoveryInstancesFetchedEventArgs(instancesByServiceId);
361+
}
348362
}
349363
finally
350364
{
351365
_registryFetchAsyncLock.Release();
352366
}
353367

354-
OnApplicationsFetched(eventArgs);
368+
if (applicationsEventArgs != null || instancesEventArgs != null)
369+
{
370+
RaiseFetchEvents(applicationsEventArgs, instancesEventArgs);
371+
}
355372
}
356373

357-
private void OnApplicationsFetched(ApplicationsFetchedEventArgs? args)
374+
private static ReadOnlyDictionary<string, IReadOnlyList<IServiceInstance>> ToServiceInstanceMap(ApplicationInfoCollection apps)
358375
{
359-
if (args != null)
376+
// @formatter:wrap_chained_method_calls chop_always
377+
// @formatter:wrap_before_first_method_call true
378+
379+
return apps
380+
.SelectMany(app => app.Instances)
381+
.GroupBy(instance => instance.AppName, StringComparer.OrdinalIgnoreCase)
382+
.ToDictionary(grouping => grouping.Key, grouping => (IReadOnlyList<IServiceInstance>)grouping
383+
.Select(instance => instance.ToServiceInstance())
384+
.ToList()
385+
.AsReadOnly())
386+
.AsReadOnly();
387+
388+
// @formatter:wrap_before_first_method_call restore
389+
// @formatter:wrap_chained_method_calls restore
390+
}
391+
392+
private void RaiseFetchEvents(ApplicationsFetchedEventArgs? applicationsEventArgs, DiscoveryInstancesFetchedEventArgs? instancesEventArgs)
393+
{
394+
// Execute on separate thread, so we won't block the periodic refresh in case the handler logic is expensive.
395+
ThreadPool.QueueUserWorkItem(_ =>
360396
{
361-
// Execute on separate thread, so we won't block the periodic refresh in case the handler logic is expensive.
362-
ThreadPool.QueueUserWorkItem(_ =>
397+
try
363398
{
364-
try
399+
if (applicationsEventArgs != null)
365400
{
366-
ApplicationsFetched?.Invoke(this, args);
401+
ApplicationsFetched?.Invoke(this, applicationsEventArgs);
367402
}
368-
catch (Exception exception)
403+
}
404+
catch (Exception exception)
405+
{
406+
LogFailedToHandleEvent(exception, nameof(ApplicationsFetched));
407+
}
408+
409+
try
410+
{
411+
if (instancesEventArgs != null)
369412
{
370-
LogFailedToHandleEvent(exception, nameof(ApplicationsFetched));
413+
InstancesFetched?.Invoke(this, instancesEventArgs);
371414
}
372-
});
373-
}
415+
}
416+
catch (Exception exception)
417+
{
418+
LogFailedToHandleEvent(exception, nameof(InstancesFetched));
419+
}
420+
});
374421
}
375422

376423
internal async Task<ApplicationInfoCollection> FetchFullRegistryAsync(CancellationToken cancellationToken)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
#nullable enable
22
Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.UseAspNetCoreUrls.get -> bool
33
Steeltoe.Discovery.Eureka.Configuration.EurekaInstanceOptions.UseAspNetCoreUrls.set -> void
4+
Steeltoe.Discovery.Eureka.EurekaDiscoveryClient.InstancesFetched -> System.EventHandler<Steeltoe.Common.Discovery.DiscoveryInstancesFetchedEventArgs!>?

0 commit comments

Comments
 (0)