Add optional parameter to configuration providers for non-blocking startup#1745
Add optional parameter to configuration providers for non-blocking startup#1745GeorgeTsiokos wants to merge 5 commits intodapr:masterfrom
Conversation
|
Do note that I just merged a PR to support xUnit v3 so your unit/integration tests may need a refresh. |
…artup 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>
7acf13b to
50765d7
Compare
Tests verify three scenarios using secretstores.local.file (no external deps): - optional:true with delayed sidecar: config populates once sidecar starts - optional:false (default): Build() blocks until sidecar ready, secrets populated - Disposal during background load: exits cleanly with no hang Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: George Tsiokos <george@tsiokos.com>
50765d7 to
d33da80
Compare
|
A few items I'm intentionally leaving as-is — noting them here for visibility: Pre-existing
Exponential backoff — The flat Splitting the Non-optional blocking-failure test for config store — Pre-existing gap (the non-optional path throws on WaitForSidecar timeout, but there's no unit test for it). Out of scope for this PR.
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds an optional parameter to Dapr configuration/secret store configuration providers so apps can start without blocking on sidecar availability, and expands test coverage (unit + integration) for the new behavior.
Changes:
- Add
optional/IsOptionalplumbing across configuration sources, providers, and extension methods for configuration store, streaming configuration store, and secret store. - Introduce background loading logic and tighten exception handling/cancellation disposal behavior in providers.
- Add new unit/integration tests (and a new integration test project + testcontainers harness) validating late sidecar startup and clean disposal.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs |
Adds optional parameter to configuration store extension methods and passes it into the source. |
src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs |
Adds IsOptional to the source and forwards it to the provider. |
src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs |
Implements optional non-blocking startup via background loading; updates cancellation/disposal behavior. |
src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs |
Adds optional parameter across secret-store extension overloads and sets IsOptional. |
src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs |
Adds IsOptional and forwards it to the provider. |
src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs |
Implements optional non-blocking startup via background loading; adds IDisposable and refactors fetch logic. |
test/Dapr.Extensions.Configuration.Test/DaprConfigurationStoreProviderTest.cs |
Adds unit tests covering optional behavior for config store providers. |
test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs |
Adds unit tests covering optional behavior for secret store provider. |
test/Dapr.IntegrationTest.Configuration/Dapr.IntegrationTest.Configuration.csproj |
New integration test project for configuration/secret optional behavior. |
test/Dapr.IntegrationTest.Configuration/SecretStoreOptionalTests.cs |
New integration tests validating optional startup, late sidecar, and disposal behavior. |
src/Dapr.Testcontainers/Common/DaprHarnessBuilder.cs |
Adds a builder method for the new secret store harness. |
src/Dapr.Testcontainers/Harnesses/SecretStoreHarness.cs |
New harness for local file-based secret store used by integration tests. |
all.sln |
Adds the new integration test project; also introduces x64/x86 solution configurations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private async Task FetchDataAsync() | ||
| { | ||
| if (isStreaming) | ||
| { | ||
| subscribeTask = Task.Run(async () => |
There was a problem hiding this comment.
In the streaming path, the subscription loop relies on broad exception handling and reconnection logic. Consider narrowing exception handling to expected transient failures and adding a backoff/delay before reconnecting to avoid hot-looping under persistent failures, and handle cancellation (OperationCanceledException) explicitly so shutdown doesn’t trigger unnecessary unsubscribe/retry behavior.
| private async Task FetchSecretsAsync() | ||
| { | ||
| var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| if (secretDescriptors != null) | ||
| { | ||
| foreach (var secretDescriptor in secretDescriptors) |
There was a problem hiding this comment.
Background loading is cancel-driven via cts, but FetchSecretsAsync() uses Dapr client calls without passing a cancellation token. To make disposal/shutdown reliable (especially with optional: true), consider passing cts.Token (or a linked token with a timeout) into the GetSecretAsync/GetBulkSecretAsync calls so in-flight fetches can be cancelled.
| public void Dispose() | ||
| { | ||
| cts.Cancel(); | ||
| cts.Dispose(); | ||
| } |
There was a problem hiding this comment.
Dispose() cancels and disposes the shared CancellationTokenSource, but background tasks started via Task.Run (and the streaming subscription task) may still be executing and attempting to register with cts.Token. Disposing the CTS while work is still running can cause ObjectDisposedException races inside token registration APIs. Consider storing the background task(s) and waiting for completion after cancellation (or avoid disposing the CTS if you can’t guarantee completion).
| public void Dispose() | ||
| { | ||
| cts.Cancel(); | ||
| cts.Dispose(); |
There was a problem hiding this comment.
Similar to the configuration store provider, Dispose() cancels and disposes the CancellationTokenSource while a fire-and-forget background task may still be running and registering with cts.Token. To avoid potential ObjectDisposedException races/unobserved exceptions, consider tracking the background task and ensuring it completes after cancellation before disposing the CTS (or skip CTS disposal if you can’t safely wait).
| cts.Dispose(); |
| Debug|x64 = Debug|x64 | ||
| Debug|x86 = Debug|x86 | ||
| Release|Any CPU = Release|Any CPU | ||
| Release|x64 = Release|x64 | ||
| Release|x86 = Release|x86 | ||
| EndGlobalSection | ||
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|x64.ActiveCfg = Debug|Any CPU | ||
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|x64.Build.0 = Debug|Any CPU | ||
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|x86.ActiveCfg = Debug|Any CPU | ||
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|x86.Build.0 = Debug|Any CPU |
There was a problem hiding this comment.
This change adds x64/x86 solution configurations and propagates them across nearly every project configuration entry. If this wasn’t intentional, it’s a large unrelated churn that will increase merge-conflict risk and make future diffs noisier; consider reverting the solution-platform expansion and limiting changes here to adding the new test project entry.
| Debug|x64 = Debug|x64 | |
| Debug|x86 = Debug|x86 | |
| Release|Any CPU = Release|Any CPU | |
| Release|x64 = Release|x64 | |
| Release|x86 = Release|x86 | |
| EndGlobalSection | |
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | |
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|Any CPU.Build.0 = Debug|Any CPU | |
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|x64.ActiveCfg = Debug|Any CPU | |
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|x64.Build.0 = Debug|Any CPU | |
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|x86.ActiveCfg = Debug|Any CPU | |
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|x86.Build.0 = Debug|Any CPU | |
| Release|Any CPU = Release|Any CPU | |
| EndGlobalSection | |
| GlobalSection(ProjectConfigurationPlatforms) = postSolution | |
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |
| {C2DB4B64-B7C3-4FED-8753-C040F677C69A}.Debug|Any CPU.Build.0 = Debug|Any CPU |
| var harness = new DaprHarnessBuilder(componentsDir) | ||
| .WithEnvironment(environment) | ||
| .BuildSecretStore(); | ||
|
|
There was a problem hiding this comment.
The harness is disposed manually at the end of the test; if an assertion fails earlier, the harness won’t be disposed and containers/network resources may leak for the remainder of the test run. Consider declaring the harness with await using (it implements IAsyncDisposable) so cleanup happens even on failure.
| public override void Load() | ||
| { | ||
| if (isOptional) | ||
| { | ||
| Data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); | ||
| _ = Task.Run(() => LoadInBackgroundAsync()); | ||
| } |
There was a problem hiding this comment.
With optional: true, Load() returns immediately and LoadInBackgroundAsync will populate Data on a background thread while the app may already be reading configuration. Please ensure FetchDataAsync() updates Data atomically (swap in a new dictionary) rather than mutating the existing dictionary in-place (not thread-safe), especially in the non-streaming code path.
Summary
Resolves #1748
optionalparameter toAddDaprConfigurationStore,AddStreamingDaprConfigurationStore, andAddDaprSecretStoreextension methods so applications can start without blocking on Dapr sidecar availabilityLoadInBackgroundAsyncin both providers with narrowed exception handling (OperationCanceledException+DaprException) instead of a broadcatch (Exception)that silently swallowed programming errorsCancellationTokenSourcein both providersStringComparerinconsistency (InvariantCultureIgnoreCase→OrdinalIgnoreCase) inFetchSecretsAsyncreadonlyinDaprConfigurationStoreProviderfor consistency withDaprSecretStoreConfigurationProvider.editorconfigand adds missing trailing newlinesTest plan
GetReloadToken()callbacks for deterministic assertions (no fragileTask.Delaypolling)optional: truewith delayed sidecar — config populates once sidecar starts (ShouldPopulateSecretsWhenSidecarStartsLate)optional: false(default) behavior unchanged —Build()blocks until sidecar ready (ShouldBlockAndPopulateSecretsWhenNotOptional)ShouldExitCleanlyWhenDisposedDuringBackgroundLoad)TestContext.Current.CancellationTokenpattern)🤖 Generated with Claude Code