Skip to content

Commit 9d94025

Browse files
authored
Allow clients to specify a waiting time before retrying when watching. (#72)
1 parent 42b8edc commit 9d94025

10 files changed

Lines changed: 78 additions & 27 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplica
8282
An `Action<ConsulLoadExceptionContext>` that can be used to configure how exceptions thrown during the first load should be handled.
8383
* **`OnWatchException`**
8484

85-
An `Action<ConsulWatchExceptionContext>` that can be used to configure how exceptions thrown when watching for changes should be handled.
85+
A `Func<ConsulWatchExceptionContext, TimeSpan>` that can be used to configure how exceptions thrown when watching for changes should be handled.
86+
The `TimeSpan` that is returned is used to set a delay before retrying.
87+
The `ConsulWatchExceptionContext` provides data that can be used to implement a backoff strategy or to cancel watching altogether.
8688
* **`Optional`**
8789

8890
A `bool` that indicates whether the config is optional. If `false` then it will throw during the first load if the config is missing for the given key. Defaults to `false`.

src/Winton.Extensions.Configuration.Consul/ConsulConfigurationClient.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ public async Task<QueryResult<KVPair[]>> GetConfig(string key, CancellationToken
3333

3434
public IChangeToken Watch(
3535
string key,
36-
Action<ConsulWatchExceptionContext> onException,
36+
Func<ConsulWatchExceptionContext, TimeSpan> onException,
3737
CancellationToken cancellationToken)
3838
{
39-
Task.Run(() => PollForChanges(key, onException, cancellationToken));
39+
Task.Run(() => PollForChanges(key, onException, cancellationToken), cancellationToken);
4040
return _reloadToken;
4141
}
4242

@@ -79,9 +79,10 @@ private async Task<bool> HasValueChanged(string key, CancellationToken cancellat
7979

8080
private async Task PollForChanges(
8181
string key,
82-
Action<ConsulWatchExceptionContext> onException,
82+
Func<ConsulWatchExceptionContext, TimeSpan> onException,
8383
CancellationToken cancellationToken)
8484
{
85+
var consecutiveFailureCount = 0;
8586
while (!cancellationToken.IsCancellationRequested)
8687
{
8788
try
@@ -94,11 +95,16 @@ private async Task PollForChanges(
9495
previousToken.OnReload();
9596
return;
9697
}
98+
99+
consecutiveFailureCount = 0;
97100
}
98101
catch (Exception exception)
99102
{
100-
var exceptionContext = new ConsulWatchExceptionContext(cancellationToken, exception);
101-
onException?.Invoke(exceptionContext);
103+
TimeSpan wait =
104+
onException?.Invoke(
105+
new ConsulWatchExceptionContext(cancellationToken, exception, ++consecutiveFailureCount)) ??
106+
TimeSpan.FromSeconds(5);
107+
await Task.Delay(wait, cancellationToken);
102108
}
103109
}
104110
}

src/Winton.Extensions.Configuration.Consul/ConsulConfigurationProvider.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ public override void Load()
4848
}
4949
catch (AggregateException aggregateException)
5050
{
51-
throw aggregateException.InnerException;
51+
if (aggregateException.InnerException != null)
52+
{
53+
throw aggregateException.InnerException;
54+
}
55+
56+
throw;
5257
}
5358
}
5459

src/Winton.Extensions.Configuration.Consul/ConsulConfigurationSource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public ConsulConfigurationSource(string key, CancellationToken cancellationToken
3939

4040
public Action<ConsulLoadExceptionContext> OnLoadException { get; set; }
4141

42-
public Action<ConsulWatchExceptionContext> OnWatchException { get; set; }
42+
public Func<ConsulWatchExceptionContext, TimeSpan> OnWatchException { get; set; }
4343

4444
public bool Optional { get; set; } = false;
4545

src/Winton.Extensions.Configuration.Consul/ConsulWatchExceptionContext.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ namespace Winton.Extensions.Configuration.Consul
1111
/// </summary>
1212
public sealed class ConsulWatchExceptionContext
1313
{
14-
internal ConsulWatchExceptionContext(CancellationToken cancellationToken, Exception exception)
14+
internal ConsulWatchExceptionContext(
15+
CancellationToken cancellationToken,
16+
Exception exception,
17+
int consecutiveFailureCount)
1518
{
1619
Exception = exception;
20+
ConsecutiveFailureCount = consecutiveFailureCount;
1721
CancellationToken = cancellationToken;
1822
}
1923

@@ -22,6 +26,14 @@ internal ConsulWatchExceptionContext(CancellationToken cancellationToken, Except
2226
/// </summary>
2327
public CancellationToken CancellationToken { get; }
2428

29+
/// <summary>
30+
/// Gets the number of consecutive failures that have occurred while watching for configuration changes.
31+
/// </summary>
32+
/// <remarks>
33+
/// This can be used to vary the time between retries, for example to create an exponential backoff algorithm.
34+
/// </remarks>
35+
public int ConsecutiveFailureCount { get; }
36+
2537
/// <summary>
2638
/// Gets the <see cref="Exception" /> that occured.
2739
/// </summary>

src/Winton.Extensions.Configuration.Consul/IConsulConfigurationClient.cs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,44 @@
99

1010
namespace Winton.Extensions.Configuration.Consul
1111
{
12-
/// <summary>Provides client access for getting and watching config values in Consul.</summary>
12+
/// <summary>
13+
/// Provides client access for getting and watching config values in Consul.
14+
/// </summary>
1315
internal interface IConsulConfigurationClient
1416
{
15-
/// <summary>Gets the config from consul asynchronously.</summary>
16-
/// <param name="key">The key at which the config is located.</param>
17-
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
18-
/// <returns>A task containing the result of the query for the config.</returns>
17+
/// <summary>
18+
/// Gets the config from consul asynchronously.
19+
/// </summary>
20+
/// <param name="key">
21+
/// The key at which the config is located.
22+
/// </param>
23+
/// <param name="cancellationToken">
24+
/// A cancellation token that can be used to cancel the operation.
25+
/// </param>
26+
/// <returns>
27+
/// A task containing the result of the query for the config.
28+
/// </returns>
1929
Task<QueryResult<KVPair[]>> GetConfig(string key, CancellationToken cancellationToken);
2030

21-
/// <summary>Watches for config changes at a specified key.</summary>
22-
/// <param name="key">The key whose value should be watched for changes.</param>
23-
/// <param name="onException">An action to be invoked if an exception occurs during the watch.</param>
24-
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
25-
/// <returns>An <see cref="IChangeToken" /> that will indicated when changes have occured.</returns>
31+
/// <summary>
32+
/// Watches for config changes at a specified key.
33+
/// </summary>
34+
/// <param name="key">
35+
/// The key whose value should be watched for changes.
36+
/// </param>
37+
/// <param name="onException">
38+
/// A function to be invoked if an exception occurs during the watch which returns the time to wait before making
39+
/// another attempt.
40+
/// </param>
41+
/// <param name="cancellationToken">
42+
/// A cancellation token that can be used to cancel the operation.
43+
/// </param>
44+
/// <returns>
45+
/// An <see cref="IChangeToken" /> that will indicated when changes have occured.
46+
/// </returns>
2647
IChangeToken Watch(
2748
string key,
28-
Action<ConsulWatchExceptionContext> onException,
49+
Func<ConsulWatchExceptionContext, TimeSpan> onException,
2950
CancellationToken cancellationToken);
3051
}
3152
}

src/Winton.Extensions.Configuration.Consul/IConsulConfigurationSource.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,14 @@ public interface IConsulConfigurationSource : IConfigurationSource
6565
Action<ConsulLoadExceptionContext> OnLoadException { get; set; }
6666

6767
/// <summary>
68-
/// Gets or sets an <see cref="Action" /> that is invoked when an exception is raised whilst watching.
69-
/// Used by clients to acknowledge the excetion and possibly cancel the watcher.
68+
/// Gets or sets a <see cref="Func{ConsulWatchException, TimeSpan}" /> that is invoked when an exception is raised whilst watching.
69+
/// The <see cref="TimeSpan"/> returned by the function is waited before trying again.
7070
/// </summary>
71-
Action<ConsulWatchExceptionContext> OnWatchException { get; set; }
71+
/// <remarks>
72+
/// This function is useful for implementing backoff strategies.
73+
/// It also provides access to the <see cref="CancellationToken"/> which can be used to cancel the watch task.
74+
/// </remarks>
75+
Func<ConsulWatchExceptionContext, TimeSpan> OnWatchException { get; set; }
7276

7377
/// <summary>
7478
/// Gets or sets a value indicating whether the config is optional.

src/Winton.Extensions.Configuration.Consul/Winton.Extensions.Configuration.Consul.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
<ItemGroup>
2828
<AdditionalFiles Include="../../stylecop.json" />
29-
<None Include="../../LICENSE" Pack="true" PackagePath=""/>
29+
<None Include="../../LICENSE" Pack="true" PackagePath="" />
3030
</ItemGroup>
3131

3232
<ItemGroup>

test/Winton.Extensions.Configuration.Consul.Test/ConsulConfigurationClientTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ private async Task ShouldCallReloadOnChangeTokenIfIndexForKeyHasUpdated()
130130
}
131131

132132
[Fact]
133-
private async Task ShouldInvokeExceptionActionWhenWatchThrowsException()
133+
private async Task ShouldInvokeOnExceptionWhenWatchThrowsException()
134134
{
135135
Exception actualException = null;
136136
var expectedException = new Exception();
@@ -147,6 +147,7 @@ private async Task ShouldInvokeExceptionActionWhenWatchThrowsException()
147147
{
148148
actualException = exceptionContext.Exception;
149149
configChangedCompletion.SetResult(true);
150+
return TimeSpan.FromSeconds(5);
150151
},
151152
default(CancellationToken));
152153

@@ -200,7 +201,7 @@ private async Task ShouldUseLongPollingToPollForChanges()
200201

201202
await watchCompletion.Task;
202203

203-
Action verifying = () => _kvMock
204+
_kvMock
204205
.Verify(
205206
kv => kv.List("Test", It.IsAny<QueryOptions>(), default(CancellationToken)),
206207
Times.Exactly(6));

test/Winton.Extensions.Configuration.Consul.Test/ConsulConfigurationProviderTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ public Reload()
260260
ccc =>
261261
ccc.Watch(
262262
"Test",
263-
It.IsAny<Action<ConsulWatchExceptionContext>>(),
263+
It.IsAny<Func<ConsulWatchExceptionContext, TimeSpan>>(),
264264
default(CancellationToken)))
265265
.Returns(_firstChangeToken)
266266
.Returns(new ConfigurationReloadToken());

0 commit comments

Comments
 (0)