Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 20, 2025

Problem

During Azure resource provisioning, there's a race condition that causes deployment state corruption when AzureProvisioner and ParameterProcessor save state concurrently. This results in lost parameter values and the error:

Deployment failed: An item with the same key has already been added. Key: Deployments

Root Cause

Both components follow a load-modify-save pattern without synchronization:

  1. AzureProvisioner loads state: {}
  2. ParameterProcessor loads state: {}
  3. ParameterProcessor saves: {"Parameters": {"param1": "value1"}}
  4. AzureProvisioner saves: {"Azure": {"SubscriptionId": "...", "Deployments": {...}}}

The last write wins, causing AzureProvisioner to overwrite the parameters saved by ParameterProcessor.

Solution

This PR implements a merge-on-write pattern in both UserSecretsDeploymentStateManager and FileDeploymentStateManager:

  1. Before saving, reload the current state from disk
  2. Merge incoming changes into the current state
  3. Save the merged result

This ensures concurrent writes from different components preserve each other's data:

// Load current state to get any concurrent changes
var currentState = await LoadStateAsync(cancellationToken).ConfigureAwait(false);

// Merge incoming state into current state (preserves concurrent writes)
MergeJsonObjects(currentState, state);

// Save merged state
var flattenedSecrets = FlattenJsonObject(currentState);

Example

Before (Broken):

Time 1: AzureProvisioner loads:     {}
Time 2: ParameterProcessor saves:   {"Parameters": {"p1": "value"}}
Time 3: AzureProvisioner saves:     {"Azure": {...}}
Result:                             {"Azure": {...}} ❌ Parameters lost!

After (Fixed):

Time 1: AzureProvisioner loads:     {}
Time 2: ParameterProcessor saves:   {"Parameters": {"p1": "value"}}
Time 3: AzureProvisioner:
        - Loads current:            {"Parameters": {"p1": "value"}}
        - Merges:                   {"Azure": {...}}
        - Saves:                    {"Parameters": {"p1": "value"}, "Azure": {...}}
Result:                             {"Parameters": {"p1": "value"}, "Azure": {...}} ✅

Changes

  • UserSecretsDeploymentStateManager.cs: Added MergeJsonObjects() helper and updated SaveStateAsync() to merge before saving
  • FileDeploymentStateManager.cs: Added MergeJsonObjects() helper and updated SaveStateAsync() to merge before saving
  • DefaultUserSecretsManagerTests.cs: Added unit tests validating merge behavior for concurrent saves

Testing

Added three unit tests that validate:

  1. Both Parameters and Azure deployment data are preserved when saved concurrently
  2. Existing keys not in the new state are preserved
  3. New values properly overwrite old values for the same keys

Related

Addresses the race condition mentioned in #[issue_number] that remained after changes in #11974.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • aka.ms
    • Triggering command: /usr/local/bin/bicep build /tmp/aspireFeGNq2/env.module.bicep --stdout (dns block)
    • Triggering command: /usr/local/bin/bicep build /tmp/aspire00YuLv/teststorage.module.bicep --stdout (dns block)
    • Triggering command: /usr/local/bin/bicep build /tmp/aspire8clKI3/env.module.bicep --stdout (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>Race condition when saving deployment state during Bicep provisioning</issue_title>
<issue_description>There's a sporadic issue with saving deployment state when concurrently provisioning Azure resources during deployment, even after the changes in #11974.

22:33:17 (deploy-compute) ✗ Deploying api: Failed to deploy api: Deployment failed: An item with the same key has already 
been added. Key: Deployments (Parameter 'key') (20.8s)
``` </issue_description>

<agent_instructions># File-Level Synchronization Changes - Executive Summary

   ## Problem
   When Azure resources are being provisioned and parameters are being resolved concurrently, parameter values saved to user secrets get lost because `AzureProvisioner` overwrites them with a stale copy of the deployment state.

   ## Solution
   Implement **merge-on-write** pattern: before saving, reload the current state from disk and merge incoming changes into it.

   ---

   ## Code Changes Required

   ### Files to Modify (2 files)

   #### 1. `src/Aspire.Hosting/Publishing/Internal/UserSecretsDeploymentStateManager.cs`

   **Change in `SaveStateAsync()` method (line 54):**

   Add 3 lines before flattening:
   ```csharp
   // Load current state from disk to merge with incoming state
   var currentState = await LoadStateAsync(cancellationToken).ConfigureAwait(false);

   // Merge incoming state into current state (preserves concurrent writes)
   MergeJsonObjects(currentState, state);

Then use currentState instead of state when flattening:

var flattenedUserSecrets = FlattenJsonObject(currentState);  // was: state

Add new helper method:

/// <summary>
/// Merges the source JsonObject into the target JsonObject.
/// Top-level keys from source overwrite corresponding keys in target.
/// </summary>
private static void MergeJsonObjects(JsonObject target, JsonObject source)
{
    foreach (var kvp in source)
    {
        target[kvp.Key] = kvp.Value?.DeepClone();
    }
}

2. src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs

Identical changes as above:

Add 3 lines in SaveStateAsync() before flattening (line 92):

// Load current state from disk to merge with incoming state
var currentState = await LoadStateAsync(cancellationToken).ConfigureAwait(false);

// Merge incoming state into current state (preserves concurrent writes)
MergeJsonObjects(currentState, state);

var flattenedSecrets = FlattenJsonObject(currentState);  // was: state

Add the same helper method:

private static void MergeJsonObjects(JsonObject target, JsonObject source)
{
    foreach (var kvp in source)
    {
        target[kvp.Key] = kvp.Value?.DeepClone();
    }
}

How It Works

Before (Current - Broken):

Time 1: AzureProvisioner loads:     { }
Time 2: ParameterProcessor saves:   { "Parameters": { "p1": "value" } }
Time 3: AzureProvisioner saves:     { "Azure": { ... } }
Result:                             { "Azure": { ... } } ❌ Parameters lost!

After (With Merge - Fixed):

Time 1: AzureProvisioner loads:     { }
Time 2: ParameterProcessor saves:   { "Parameters": { "p1": "value" } }
Time 3: AzureProvisioner:
        - Loads current:             { "Parameters": { "p1": "value" } }
        - Merges with:              { "Azure": { ... } }
        - Saves:                    { "Parameters": { "p1": "value" }, "Azure": { ... } }
Result:                             { "Parameters": { "p1": "value" }, "Azure": { ... } } ✅

Testing Recommendations

  1. Unit Test: Verify concurrent saves preserve both sets of data
  2. Integration Test: Run app with both Azure resources and parameters, verify both are saved
  3. Regression Test: Run existing test suite to ensure no breaks</agent_instructions>

Comments on the Issue (you are @copilot in this section)

Fixes #12201


💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.

Copilot AI self-assigned this Oct 20, 2025
Copilot AI changed the title [WIP] Fix race condition when saving deployment state during Bicep provisioning Fix race condition when saving deployment state during concurrent Azure provisioning Oct 21, 2025
Copilot AI requested a review from captainsafia October 21, 2025 00:20
@dotnet-policy-service dotnet-policy-service bot added this to the 13.0 milestone Oct 21, 2025
@github-actions github-actions bot locked and limited conversation to collaborators Nov 20, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Race condition when saving deployment state during Bicep provisioning

2 participants