sql >> Base de Datos >  >> NoSQL >> MongoDB

System.TimeoutException:se produjo un tiempo de espera después de 30000 ms al seleccionar un servidor mediante CompositeServerSelector

Este es un problema muy complicado relacionado con la Biblioteca de tareas. En definitiva, hay demasiadas tareas creadas y programadas como para que una de las tareas que está esperando el controlador de MongoDB no se vaya a poder terminar. Me tomó mucho tiempo darme cuenta de que no es un punto muerto, aunque parece que lo es.

Este es el paso para reproducir:

  1. Descargue el código fuente del controlador CSharp de MongoDB .
  2. Abra esa solución y cree un proyecto de consola dentro y haciendo referencia al proyecto del controlador.
  3. En la función Principal, cree un System.Threading.Timer que llamará a TestTask a tiempo. Configure el temporizador para que comience inmediatamente una vez. Al final, agregue Console.Read().
  4. En TestTask, use un bucle for para crear 300 tareas llamando a Task.Factory.StartNew(DoOneThing). Agregue todas esas tareas a una lista y use Task.WaitAll para esperar a que terminen todas.
  5. En la función DoOneThing, cree un MongoClient y realice una consulta simple.
  6. Ahora ejecútelo.

Esto fallará en el mismo lugar que mencionaste:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)

Si coloca algunos puntos de interrupción, sabrá que WaitForDescriptionChangedHelper creó una tarea de tiempo de espera. A continuación, espera a que se complete cualquiera de las tareas de actualización de descripción o de tiempo de espera. Sin embargo, la actualización de la descripción nunca ocurre, pero ¿por qué?

Ahora, volviendo a mi ejemplo, hay una parte interesante:puse en marcha un temporizador. Si llama directamente a TestTask, se ejecutará sin ningún problema. Al compararlos con la ventana Tareas de Visual Studio, notará que la versión con temporizador creará muchas más tareas que la versión sin temporizador. Voy a explicar esta parte un poco más tarde. Hay otra diferencia importante. Debe agregar líneas de depuración en Cluster.cs :

    protected void UpdateClusterDescription(ClusterDescription newClusterDescription)
    {
        ClusterDescription oldClusterDescription = null;
        TaskCompletionSource<bool> oldDescriptionChangedTaskCompletionSource = null;

        Console.WriteLine($"Before UpdateClusterDescription {_descriptionChangedTaskCompletionSource?.Task.Id}, {_descriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
        lock (_descriptionLock)
        {
            oldClusterDescription = _description;
            _description = newClusterDescription;

            oldDescriptionChangedTaskCompletionSource = _descriptionChangedTaskCompletionSource;
            _descriptionChangedTaskCompletionSource = new TaskCompletionSource<bool>();
        }

        OnDescriptionChanged(oldClusterDescription, newClusterDescription);
        Console.WriteLine($"Setting UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
        oldDescriptionChangedTaskCompletionSource.TrySetResult(true);
        Console.WriteLine($"Set UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
    }

    private void WaitForDescriptionChanged(IServerSelector selector, ClusterDescription description, Task descriptionChangedTask, TimeSpan timeout, CancellationToken cancellationToken)
    {
        using (var helper = new WaitForDescriptionChangedHelper(this, selector, description, descriptionChangedTask, timeout, cancellationToken))
        {
            Console.WriteLine($"Waiting {descriptionChangedTask?.Id}, {descriptionChangedTask?.GetHashCode().ToString("F8")}");
            var index = Task.WaitAny(helper.Tasks);
            helper.HandleCompletedTask(helper.Tasks[index]);
        }
    }

Al agregar estas líneas, también descubrirá que la versión sin temporizador se actualizará dos veces, pero la versión con temporizador solo se actualizará una vez. Y el segundo proviene de "MonitorServerAsync" en ServerMonitor.cs. Resultó que, en la versión del temporizador, MontiorServerAsync se ejecutó, pero después de pasar por ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync y TcpStreamFactory.CreateStreamAsync, finalmente llegó a TcpStreamFactory.ResolveEndPointsAsync. Lo malo sucede aquí:Dns.GetHostAddressesAsync . Este nunca se ejecuta. Si modifica ligeramente el código y lo convierte en:

    var task = Dns.GetHostAddressesAsync(dnsInitial.Host).ConfigureAwait(false);

    return (await task)
        .Select(x => new IPEndPoint(x, dnsInitial.Port))
        .OrderBy(x => x, new PreferredAddressFamilyComparer(preferred))
        .ToArray();

Podrá encontrar la identificación de la tarea. Al mirar en la ventana Tareas de Visual Studio, es bastante obvio que hay alrededor de 300 tareas frente a ella. Solo varios de ellos se están ejecutando pero bloqueados. Si agrega un Console.Writeline en la función DoOneThing, verá que el programador de tareas inicia varias de ellas casi al mismo tiempo, pero luego se ralentiza a alrededor de una por segundo. Entonces, esto significa que debe esperar alrededor de 300 segundos antes de que comience a ejecutarse la tarea de resolver el dns. Es por eso que excede el tiempo de espera de 30 segundos.

Ahora, aquí viene una solución rápida si no estás haciendo locuras:

Task.Factory.StartNew(DoOneThing, TaskCreationOptions.LongRunning);

Esto obligará a ThreadPoolScheduler a iniciar un hilo inmediatamente en lugar de esperar un segundo antes de crear uno nuevo.

Sin embargo, esto no funcionará si estás haciendo cosas realmente locas como yo. Cambiemos el ciclo for de 300 a 30000, incluso esta solución también podría fallar. La razón es que crea demasiados hilos. Esto consume recursos y tiempo. Y podría comenzar el proceso de GC. En conjunto, es posible que no pueda terminar de crear todos esos hilos antes de que se acabe el tiempo.

La manera perfecta es dejar de crear muchas tareas y usar el programador predeterminado para programarlas. Puede intentar crear un elemento de trabajo y ponerlo en una ConcurrentQueue y luego crear varios subprocesos como trabajadores para consumir los elementos.

Sin embargo, si no desea cambiar demasiado la estructura original, puede probar de la siguiente manera:

Cree un ThrottledTaskScheduler derivado de TaskScheduler.

  1. Este ThrottledTaskScheduler acepta un TaskScheduler como el subyacente que ejecutará la tarea real.
  2. Vuelque las tareas al programador subyacente, pero si excede el límite, colóquelo en una cola.
  3. Si alguna de las tareas finalizó, verifique la cola e intente descargarlas en el programador subyacente dentro del límite.
  4. Use el siguiente código para iniciar todas esas locas tareas nuevas:

·

var taskScheduler = new ThrottledTaskScheduler(
    TaskScheduler.Default,
    128,
    TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler,
    logger
    );
var taskFactory = new TaskFactory(taskScheduler);
for (var i = 0; i < 30000; i++)
{
    tasks.Add(taskFactory.StartNew(DoOneThing))
}
Task.WaitAll(tasks.ToArray());

Puede tomar System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler como referencia. Es un poco más complicado que el que necesitamos. Es para algún otro propósito. Por lo tanto, no se preocupe por las partes que van y vienen con la función dentro de la clase ConcurrentExclusiveSchedulerPair. Sin embargo, no puede usarlo directamente ya que no pasa TaskCreationOptions.LongRunning cuando está creando la tarea de ajuste.

Esto funciona para mi. ¡Buena suerte!

PD:La razón por la que hay muchas tareas en la versión del temporizador probablemente se encuentre dentro de TaskScheduler.TryExecuteTaskInline. Si está en el subproceso principal donde se crea ThreadPool, podrá ejecutar algunas de las tareas sin ponerlas en la cola.