Skip to content

ConfigureFunctionsApplicationInsights is not idempotent and multiple calls can lead to runtime crashes #3416

@yasmoradi

Description

@yasmoradi

Summary

AppServiceEnvironmentVariableMonitor uses a plain Dictionary<string, string?> that is mutated inside ExecuteAsync without any lock or concurrent collection. Under certain conditions (described below), this might cause:

System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access.
   at System.Collections.Generic.Dictionary`2.TryInsert(...)
   at Microsoft.Azure.Functions.Worker.ApplicationInsights.Initializers.AppServiceEnvironmentVariableMonitor+<ExecuteAsync>d__9.MoveNext()

Because BackgroundService defaults to BackgroundServiceExceptionBehavior.StopHost, this unhandled exception would tear down the entire Function App host.

Affected file

src/DotNetWorker.ApplicationInsights/Initializers/AppServiceEnvironmentVariableMonitor.cs

Details

The class is registered as a singleton IHostedService:

services.AddSingleton<AppServiceEnvironmentVariableMonitor>();
services.AddSingleton<IHostedService>(p => p.GetRequiredService<AppServiceEnvironmentVariableMonitor>());

The _monitoredVariableCache field is declared as a non-thread-safe Dictionary:

private readonly Dictionary<string, string?> _monitoredVariableCache = new(StringComparer.OrdinalIgnoreCase);

The dictionary is both read and written inside ExecuteAsync without synchronization:

_monitoredVariableCache.TryGetValue(envVar, out string? cachedVal);
// ...
_monitoredVariableCache[envVar] = currentVal;

It is worth noting that the fields immediately below in the same class — _cancellationTokenSource and _changeToken — are correctly protected against concurrent access using Interlocked.Exchange:

var oldTokenSource = Interlocked.Exchange(ref _cancellationTokenSource, new CancellationTokenSource());
Interlocked.Exchange(ref _changeToken, new CancellationChangeToken(_cancellationTokenSource.Token));

The dictionary, however, has no equivalent protection.

The FunctionEnvironmentReloadRequest or something similar could, directly or indirectly, cause the hosting infrastructure to invoke StartAsync more than once on the same singleton instance, resulting in two concurrent ExecuteAsync loops writing to the same dictionary. We were unable to confirm this from the code alone and would appreciate any insight into whether there is another path that could cause concurrent access.

Impact

  • The unhandled exception propagates out of ExecuteAsync, triggering StopHost behavior.
  • The host begins tearing down. The logging infrastructure is disposed early in this process, causing subsequent log attempts from application code to throw AggregateException, masking the original root cause.
  • In-flight HTTP requests time out or drop, producing server error spikes.

Suggested fix

Replace the plain Dictionary with ConcurrentDictionary<string, string?>:

// Before
private readonly Dictionary<string, string?> _monitoredVariableCache = new(StringComparer.OrdinalIgnoreCase);

// After
private readonly ConcurrentDictionary<string, string?> _monitoredVariableCache = new(StringComparer.OrdinalIgnoreCase);

The write (_monitoredVariableCache[envVar] = currentVal) is valid on ConcurrentDictionary via the indexer, so no other changes to the loop body would be required.

Workaround (until a fix is released)

Callers can prevent the host from being torn down by configuring HostOptions:

builder.Services.Configure<HostOptions>(options =>
{
    options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
});

Note: this suppresses the crash but does not fix the race condition. If the exception fires repeatedly, the monitor will silently stop functioning and environment variable changes will no longer propagate to the telemetry initializers.

Package version

Microsoft.Azure.Functions.Worker.ApplicationInsights observed on v2.50.0.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions