[Bug]: Cross-cache deadlock when concurrent updates notify subscribers that modify other SourceCache instances

Abierto
#1,073 2 comentarios 1 reacción 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@dwcullop ya está trabajando en esto.

Desde el 6/4/2026.

Evaluación

Este issue todavía no se ha evaluado.

Descripción

bug

Describe the bug 🐞

ObservableCache holds its internal _locker during InvokeNext, which calls _changes.OnNext synchronously while the lock is held. When a subscriber modifies a different SourceCache from inside the OnNext callback, that second cache acquires its own _locker and also calls InvokeNext under its lock. If two threads concurrently update two caches that notify into each other, the lock acquisition order inverts between threads, causing a deadlock.

This affects any scenario where SourceCache instances are wired together via Connect subscribers — including PopulateInto, MergeManyChangeSets, or any custom subscriber that writes to another cache in response to changes.

The deadlock is difficult to trigger because it requires concurrent updates to two interdependent caches with subscriber chains that cross cache boundaries.

Reproduction Test

const int iterations = 100;

for (var iter = 0; iter < iterations; iter++)
{
    using var cacheA = new SourceCache<TestItem, string>(static x => x.Key);
    using var cacheB = new SourceCache<TestItem, string>(static x => x.Key);

    using var subA = cacheA.Connect().Subscribe(changes =>
    {
        foreach (var c in changes)
        {
            if (c.Reason == ChangeReason.Add && !c.Current.Key.StartsWith("x"))
            {
                cacheB.AddOrUpdate(new TestItem("x" + c.Current.Key, c.Current.Value));
            }
        }
    });

    using var subB = cacheB.Connect().Subscribe(changes =>
    {
        foreach (var c in changes)
        {
            if (c.Reason == ChangeReason.Add && !c.Current.Key.StartsWith("x"))
            {
                cacheA.AddOrUpdate(new TestItem("x" + c.Current.Key, c.Current.Value));
            }
        }
    });

    var barrier = new Barrier(2);

    var taskA = Task.Run(() =>
    {
        barrier.SignalAndWait();
        for (var i = 0; i < 1000; i++)
        {
            cacheA.AddOrUpdate(new TestItem("a" + i, "V" + i));
        }
    });

    var taskB = Task.Run(() =>
    {
        barrier.SignalAndWait();
        for (var i = 0; i < 1000; i++)
        {
            cacheB.AddOrUpdate(new TestItem("b" + i, "V" + i));
        }
    });

    var completed = Task.WhenAll(taskA, taskB);
    var finished = await Task.WhenAny(completed, Task.Delay(TimeSpan.FromSeconds(5)));
}

private sealed record TestItem(string Key, string Value);

This test deadlocks on the current main branch.

Reproduction repository

https://gist.github.com/dwcullop/fe877f72ac7d8df4265c8affd771453d

Expected behavior

The deadlock shouldn't happen.

DynamicData Version

DynamicData 9.4.3

Lenguaje dominante
C#
Estrellas
1.9k
Forks
194
Merge medio
12 min
PR fusionados (30 d)
1

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de reactivemarbles/DynamicData

Todos los issues de reactivemarbles/DynamicData

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.