Skip to content

Incorrect usage of IOptionsMonitor causes memory leaks #20709

@SteveVaneeckhout

Description

@SteveVaneeckhout

Which Umbraco version are you using?

13.10.1

Bug summary

For months we've been observing increased memory usage and crashes on some of our Umbraco websites. Over a period of time (days, sometimes weeks) we see memory usage steadily rising until the websites crash because the server is out of memory.
I analysed multiple memory dumps and noticed that there were millions of instances of Action<Umbraco.Cms.Core.Configuration.Models.ContentSettings>.

Image

Searching our code for the usage of ContentSettings didn't return any results. In the end, I let Copilot analyse a class from Umbraco that uses IOptionsMonitor and it mentioned that there is a leak when you don't dispose the returned object from the OnChange method.

When I search through the code base I see there is only 1 file (2nd result, DatabaseBuilder.cs) that disposes the returned object correctly. All the other 66 files cause a memory leak.

I'm not an expert in Umbraco's codebase but personally I don't think most classes benefit from using an IOptionsMonitor since the lifetime is often very short lived.

Specifics

To summarize, this is the correct usage for IOptionsMonitor:

using Microsoft.Extensions.Options;
using System;

public class SomeGoodService : IDisposable
{
    private readonly IDisposable _onChangeToken;
    private ContentSettings _contentSettings;

    public SomeGoodService(IOptionsMonitor<ContentSettings> monitor)
    {
        _contentSettings = monitor.CurrentValue;

        // Keep token to dispose later
        _onChangeToken = monitor.OnChange(x => _contentSettings = x);
    }

    public void Dispose()
    {
        _onChangeToken?.Dispose();
    }
}

Make sure that Dispose() is actually called from outside otherwise it would still leak.

This is NOT correct:

public class SomeBadService
{
    private ContentSettings _contentSettings;

    public SomeBadService(IOptionsMonitor<ContentSettings> monitor)
    {
        _contentSettings = monitor.CurrentValue;
	
        // DO NOT USE: This leaks memory
        monitor.OnChange(x => _contentSettings = x);
    }
}

Steps to reproduce

Inject a class in your code that uses IOptionsMonitor for each request and notice that memory keeps rising.

Expected result / actual result

No memory leaks when using IOptionsMonitor.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions