Skip to content

Commit f17d73f

Browse files
GeorgeTsiokosclaude
andcommitted
Add optional parameter to configuration providers for non-blocking startup
Configuration and secret store providers now accept an `optional` parameter that allows applications to start without blocking on sidecar availability. When optional, configuration loads in the background with resilient retry logic that handles transient Dapr errors gracefully. - Add `optional` parameter to AddDaprConfigurationStore and AddDaprSecretStore - Harden LoadInBackgroundAsync to catch OperationCanceledException and DaprException separately, avoiding silent swallowing of programming errors - Dispose CancellationTokenSource in both providers to prevent resource leaks - Fix StringComparer inconsistency (InvariantCultureIgnoreCase → OrdinalIgnoreCase) - Mark fields readonly in DaprConfigurationStoreProvider for consistency - Restore UTF-8 BOM per .editorconfig and add missing trailing newlines - Add deterministic tests using reload tokens instead of fragile Task.Delay Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: George Tsiokos <george@tsiokos.com>
1 parent a29a853 commit f17d73f

8 files changed

Lines changed: 528 additions & 40 deletions

src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ public static class DaprConfigurationStoreExtension
3434
/// <param name="client">The <see cref="DaprClient"/> used for the request.</param>
3535
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
3636
/// <param name="metadata">Optional metadata sent to the configuration store.</param>
37+
/// <param name="optional">When true, does not block startup waiting for the sidecar. Configuration is loaded in the background once the sidecar becomes available.</param>
3738
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
3839
public static IConfigurationBuilder AddDaprConfigurationStore(
3940
this IConfigurationBuilder configurationBuilder,
4041
string store,
4142
IReadOnlyList<string> keys,
4243
DaprClient client,
4344
TimeSpan sidecarWaitTimeout,
44-
IReadOnlyDictionary<string, string>? metadata = default)
45+
IReadOnlyDictionary<string, string>? metadata = default,
46+
bool optional = false)
4547
{
4648
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
4749
ArgumentVerifier.ThrowIfNull(keys, nameof(keys));
@@ -54,7 +56,8 @@ public static IConfigurationBuilder AddDaprConfigurationStore(
5456
Client = client,
5557
SidecarWaitTimeout = sidecarWaitTimeout,
5658
IsStreaming = false,
57-
Metadata = metadata
59+
Metadata = metadata,
60+
IsOptional = optional
5861
});
5962

6063
return configurationBuilder;
@@ -71,14 +74,16 @@ public static IConfigurationBuilder AddDaprConfigurationStore(
7174
/// <param name="client">The <see cref="DaprClient"/> used for the request.</param>
7275
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
7376
/// <param name="metadata">Optional metadata sent to the configuration store.</param>
77+
/// <param name="optional">When true, does not block startup waiting for the sidecar. Configuration is loaded in the background once the sidecar becomes available.</param>
7478
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
7579
public static IConfigurationBuilder AddStreamingDaprConfigurationStore(
7680
this IConfigurationBuilder configurationBuilder,
7781
string store,
7882
IReadOnlyList<string> keys,
7983
DaprClient client,
8084
TimeSpan sidecarWaitTimeout,
81-
IReadOnlyDictionary<string, string>? metadata = default)
85+
IReadOnlyDictionary<string, string>? metadata = default,
86+
bool optional = false)
8287
{
8388
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
8489
ArgumentVerifier.ThrowIfNull(keys, nameof(keys));
@@ -91,7 +96,8 @@ public static IConfigurationBuilder AddStreamingDaprConfigurationStore(
9196
Client = client,
9297
SidecarWaitTimeout = sidecarWaitTimeout,
9398
IsStreaming = true,
94-
Metadata = metadata
99+
Metadata = metadata,
100+
IsOptional = optional
95101
});
96102

97103
return configurationBuilder;

src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ namespace Dapr.Extensions.Configuration;
2626
/// </summary>
2727
internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposable
2828
{
29-
private string store;
30-
private IReadOnlyList<string> keys;
31-
private DaprClient daprClient;
32-
private TimeSpan sidecarWaitTimeout;
33-
private bool isStreaming;
34-
private IReadOnlyDictionary<string, string>? metadata;
35-
private CancellationTokenSource cts;
29+
private readonly string store;
30+
private readonly IReadOnlyList<string> keys;
31+
private readonly DaprClient daprClient;
32+
private readonly TimeSpan sidecarWaitTimeout;
33+
private readonly bool isStreaming;
34+
private readonly bool isOptional;
35+
private readonly IReadOnlyDictionary<string, string>? metadata;
36+
private readonly CancellationTokenSource cts;
3637
private Task subscribeTask = Task.CompletedTask;
3738

3839
/// <summary>
@@ -44,30 +45,83 @@ internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposab
4445
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
4546
/// <param name="isStreaming">Determines if the source is streaming or not.</param>
4647
/// <param name="metadata">Optional metadata sent to the configuration store.</param>
48+
/// <param name="isOptional">When true, does not block startup waiting for the sidecar.</param>
4749
public DaprConfigurationStoreProvider(
4850
string store,
4951
IReadOnlyList<string> keys,
5052
DaprClient daprClient,
5153
TimeSpan sidecarWaitTimeout,
5254
bool isStreaming = false,
53-
IReadOnlyDictionary<string, string>? metadata = default)
55+
IReadOnlyDictionary<string, string>? metadata = default,
56+
bool isOptional = false)
5457
{
5558
this.store = store;
5659
this.keys = keys;
5760
this.daprClient = daprClient;
5861
this.sidecarWaitTimeout = sidecarWaitTimeout;
5962
this.isStreaming = isStreaming;
63+
this.isOptional = isOptional;
6064
this.metadata = metadata ?? new Dictionary<string, string>();
6165
this.cts = new CancellationTokenSource();
6266
}
6367

6468
public void Dispose()
6569
{
6670
cts.Cancel();
71+
cts.Dispose();
6772
}
6873

6974
/// <inheritdoc/>
70-
public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
75+
public override void Load()
76+
{
77+
if (isOptional)
78+
{
79+
Data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
80+
_ = Task.Run(() => LoadInBackgroundAsync());
81+
}
82+
else
83+
{
84+
LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
85+
}
86+
}
87+
88+
private async Task LoadInBackgroundAsync()
89+
{
90+
while (!cts.Token.IsCancellationRequested)
91+
{
92+
try
93+
{
94+
using var tokenSource = new CancellationTokenSource(sidecarWaitTimeout);
95+
using var linked = CancellationTokenSource.CreateLinkedTokenSource(tokenSource.Token, cts.Token);
96+
await daprClient.WaitForSidecarAsync(linked.Token);
97+
98+
await FetchDataAsync();
99+
OnReload();
100+
return;
101+
}
102+
catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
103+
{
104+
return;
105+
}
106+
catch (OperationCanceledException)
107+
{
108+
// Sidecar wait timed out — retry after delay.
109+
}
110+
catch (DaprException)
111+
{
112+
// Transient Dapr error — retry after delay.
113+
}
114+
115+
try
116+
{
117+
await Task.Delay(sidecarWaitTimeout, cts.Token);
118+
}
119+
catch (OperationCanceledException)
120+
{
121+
return;
122+
}
123+
}
124+
}
71125

72126
private async Task LoadAsync()
73127
{
@@ -77,6 +131,11 @@ private async Task LoadAsync()
77131
await daprClient.WaitForSidecarAsync(tokenSource.Token);
78132
}
79133

134+
await FetchDataAsync();
135+
}
136+
137+
private async Task FetchDataAsync()
138+
{
80139
if (isStreaming)
81140
{
82141
subscribeTask = Task.Run(async () =>

src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,16 @@ public class DaprConfigurationStoreSource : IConfigurationSource
5353
/// </summary>
5454
public IReadOnlyDictionary<string, string>? Metadata { get; set; } = default;
5555

56+
/// <summary>
57+
/// Gets or sets a value indicating whether this configuration source is optional.
58+
/// When <c>true</c>, the provider will not block startup waiting for the Dapr sidecar and will
59+
/// instead load configuration in the background once the sidecar becomes available.
60+
/// </summary>
61+
public bool IsOptional { get; set; }
62+
5663
/// <inheritdoc/>
5764
public IConfigurationProvider Build(IConfigurationBuilder builder)
5865
{
59-
return new DaprConfigurationStoreProvider(Store, Keys, Client, SidecarWaitTimeout, IsStreaming, Metadata);
66+
return new DaprConfigurationStoreProvider(Store, Keys, Client, SidecarWaitTimeout, IsStreaming, Metadata, IsOptional);
6067
}
61-
}
68+
}

src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ public static class DaprSecretStoreConfigurationExtensions
3232
/// <param name="store">Dapr secret store name.</param>
3333
/// <param name="secretDescriptors">The secrets to retrieve.</param>
3434
/// <param name="client">The Dapr client</param>
35+
/// <param name="optional">When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.</param>
3536
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
3637
public static IConfigurationBuilder AddDaprSecretStore(
3738
this IConfigurationBuilder configurationBuilder,
3839
string store,
3940
IEnumerable<DaprSecretDescriptor> secretDescriptors,
40-
DaprClient client)
41+
DaprClient client,
42+
bool optional = false)
4143
{
4244
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
4345
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
@@ -47,7 +49,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
4749
{
4850
Store = store,
4951
SecretDescriptors = secretDescriptors,
50-
Client = client
52+
Client = client,
53+
IsOptional = optional
5154
});
5255

5356
return configurationBuilder;
@@ -61,13 +64,15 @@ public static IConfigurationBuilder AddDaprSecretStore(
6164
/// <param name="secretDescriptors">The secrets to retrieve.</param>
6265
/// <param name="client">The Dapr client.</param>
6366
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
67+
/// <param name="optional">When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.</param>
6468
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
6569
public static IConfigurationBuilder AddDaprSecretStore(
6670
this IConfigurationBuilder configurationBuilder,
6771
string store,
6872
IEnumerable<DaprSecretDescriptor> secretDescriptors,
6973
DaprClient client,
70-
TimeSpan sidecarWaitTimeout)
74+
TimeSpan sidecarWaitTimeout,
75+
bool optional = false)
7176
{
7277
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
7378
ArgumentVerifier.ThrowIfNull(secretDescriptors, nameof(secretDescriptors));
@@ -78,7 +83,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
7883
Store = store,
7984
SecretDescriptors = secretDescriptors,
8085
Client = client,
81-
SidecarWaitTimeout = sidecarWaitTimeout
86+
SidecarWaitTimeout = sidecarWaitTimeout,
87+
IsOptional = optional
8288
});
8389

8490
return configurationBuilder;
@@ -89,14 +95,16 @@ public static IConfigurationBuilder AddDaprSecretStore(
8995
/// </summary>
9096
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
9197
/// <param name="store">Dapr secret store name.</param>
92-
/// <param name="metadata">A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.</param>
9398
/// <param name="client">The Dapr client</param>
99+
/// <param name="metadata">A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.</param>
100+
/// <param name="optional">When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.</param>
94101
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
95102
public static IConfigurationBuilder AddDaprSecretStore(
96103
this IConfigurationBuilder configurationBuilder,
97104
string store,
98105
DaprClient client,
99-
IReadOnlyDictionary<string, string>? metadata = null)
106+
IReadOnlyDictionary<string, string>? metadata = null,
107+
bool optional = false)
100108
{
101109
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
102110
ArgumentVerifier.ThrowIfNull(client, nameof(client));
@@ -105,7 +113,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
105113
{
106114
Store = store,
107115
Metadata = metadata,
108-
Client = client
116+
Client = client,
117+
IsOptional = optional
109118
});
110119

111120
return configurationBuilder;
@@ -116,16 +125,18 @@ public static IConfigurationBuilder AddDaprSecretStore(
116125
/// </summary>
117126
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
118127
/// <param name="store">Dapr secret store name.</param>
119-
/// <param name="metadata">A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.</param>
120128
/// <param name="client">The Dapr client</param>
121129
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
130+
/// <param name="metadata">A collection of metadata key-value pairs that will be provided to the secret store. The valid metadata keys and values are determined by the type of secret store used.</param>
131+
/// <param name="optional">When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.</param>
122132
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
123133
public static IConfigurationBuilder AddDaprSecretStore(
124134
this IConfigurationBuilder configurationBuilder,
125135
string store,
126136
DaprClient client,
127137
TimeSpan sidecarWaitTimeout,
128-
IReadOnlyDictionary<string, string>? metadata = null)
138+
IReadOnlyDictionary<string, string>? metadata = null,
139+
bool optional = false)
129140
{
130141
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
131142
ArgumentVerifier.ThrowIfNull(client, nameof(client));
@@ -135,7 +146,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
135146
Store = store,
136147
Metadata = metadata,
137148
Client = client,
138-
SidecarWaitTimeout = sidecarWaitTimeout
149+
SidecarWaitTimeout = sidecarWaitTimeout,
150+
IsOptional = optional
139151
});
140152

141153
return configurationBuilder;
@@ -146,22 +158,25 @@ public static IConfigurationBuilder AddDaprSecretStore(
146158
/// </summary>
147159
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
148160
/// <param name="store">Dapr secret store name.</param>
149-
/// <param name="keyDelimiters">A collection of delimiters that will be replaced by ':' in the key of every secret.</param>
150161
/// <param name="client">The Dapr client</param>
162+
/// <param name="keyDelimiters">A collection of delimiters that will be replaced by ':' in the key of every secret.</param>
163+
/// <param name="optional">When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.</param>
151164
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
152165
public static IConfigurationBuilder AddDaprSecretStore(
153166
this IConfigurationBuilder configurationBuilder,
154167
string store,
155168
DaprClient client,
156-
IEnumerable<string>? keyDelimiters)
169+
IEnumerable<string>? keyDelimiters,
170+
bool optional = false)
157171
{
158172
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
159173
ArgumentVerifier.ThrowIfNull(client, nameof(client));
160174

161175
var source = new DaprSecretStoreConfigurationSource
162176
{
163177
Store = store,
164-
Client = client
178+
Client = client,
179+
IsOptional = optional
165180
};
166181

167182
if (keyDelimiters != null)
@@ -179,16 +194,18 @@ public static IConfigurationBuilder AddDaprSecretStore(
179194
/// </summary>
180195
/// <param name="configurationBuilder">The <see cref="IConfigurationBuilder"/> to add to.</param>
181196
/// <param name="store">Dapr secret store name.</param>
182-
/// <param name="keyDelimiters">A collection of delimiters that will be replaced by ':' in the key of every secret.</param>
183197
/// <param name="client">The Dapr client</param>
198+
/// <param name="keyDelimiters">A collection of delimiters that will be replaced by ':' in the key of every secret.</param>
184199
/// <param name="sidecarWaitTimeout">The <see cref="TimeSpan"/> used to configure the timeout waiting for Dapr.</param>
200+
/// <param name="optional">When true, does not block startup waiting for the sidecar. Secrets are loaded in the background once the sidecar becomes available.</param>
185201
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
186202
public static IConfigurationBuilder AddDaprSecretStore(
187203
this IConfigurationBuilder configurationBuilder,
188204
string store,
189205
DaprClient client,
190206
IEnumerable<string>? keyDelimiters,
191-
TimeSpan sidecarWaitTimeout)
207+
TimeSpan sidecarWaitTimeout,
208+
bool optional = false)
192209
{
193210
ArgumentVerifier.ThrowIfNullOrEmpty(store, nameof(store));
194211
ArgumentVerifier.ThrowIfNull(client, nameof(client));
@@ -197,7 +214,8 @@ public static IConfigurationBuilder AddDaprSecretStore(
197214
{
198215
Store = store,
199216
Client = client,
200-
SidecarWaitTimeout = sidecarWaitTimeout
217+
SidecarWaitTimeout = sidecarWaitTimeout,
218+
IsOptional = optional
201219
};
202220

203221
if (keyDelimiters != null)
@@ -218,4 +236,4 @@ public static IConfigurationBuilder AddDaprSecretStore(
218236
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
219237
public static IConfigurationBuilder AddDaprSecretStore(this IConfigurationBuilder configurationBuilder, Action<DaprSecretStoreConfigurationSource> configureSource)
220238
=> configurationBuilder.Add(configureSource);
221-
}
239+
}

0 commit comments

Comments
 (0)