MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

System.TimeoutException:ocorreu um tempo limite após 30000ms selecionando um servidor usando CompositeServerSelector


Este é um problema muito complicado relacionado com a Biblioteca de Tarefas. Em suma, há muitas tarefas criadas e agendadas para que uma das tarefas que o driver do MongoDB está esperando não possa ser concluída. Levei muito tempo para perceber que não é um impasse, embora pareça que é.

Aqui está o passo para reproduzir:
  1. Faça o download do código-fonte do driver CSharp do MongoDB .
  2. Abra essa solução e crie um projeto de console dentro e referenciando o projeto do driver.
  3. Na função Main, crie um System.Threading.Timer que chamará TestTask na hora. Defina o temporizador para iniciar imediatamente uma vez. No final, adicione um Console.Read().
  4. Na TestTask, use um loop for para criar 300 tarefas chamando Task.Factory.StartNew(DoOneThing). Adicione todas essas tarefas a uma lista e use Task.WaitAll para aguardar a conclusão de todas elas.
  5. Na função DoOneThing, crie um MongoClient e faça uma consulta simples.
  6. Agora execute-o.

Isso falhará no mesmo local que você mencionou:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)

Se você colocar alguns pontos de interrupção, saberá que o WaitForDescriptionChangedHelper criou uma tarefa de tempo limite. Em seguida, ele espera que qualquer uma das tarefas DescriptionUpdate ou a tarefa de tempo limite seja concluída. No entanto, o DescriptionUpdate nunca acontece, mas por quê?

Agora, de volta ao meu exemplo, há uma parte interessante:eu iniciei um cronômetro. Se você chamar o TestTask diretamente, ele será executado sem nenhum problema. Ao compará-los com a janela Tarefas do Visual Studio, você notará que a versão com timer criará muito mais tarefas do que a versão sem timer. Deixe-me explicar esta parte um pouco mais tarde. Há outra diferença importante. Você precisa adicionar linhas de depuração no 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]);
        }
    }

Ao adicionar essas linhas, você também descobrirá que a versão sem temporizador será atualizada duas vezes, mas a versão com temporizador será atualizada apenas uma vez. E o segundo vem do "MonitorServerAsync" em ServerMonitor.cs. Descobriu-se que, na versão do timer, o MontiorServerAsync foi executado, mas depois de passar por ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync e TcpStreamFactory.CreateStreamAsync, finalmente chegou a TcpStreamFactory.ResolveEndPointsAsync. A coisa ruim acontece aqui:Dns.GetHostAddressesAsync . Este nunca é executado. Se você modificar um pouco o código e transformá-lo em:
    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();

Você será capaz de encontrar o ID da tarefa. Ao olhar para a janela Tarefas do Visual Studio, é bastante óbvio que existem cerca de 300 tarefas na frente dela. Apenas vários deles estão em execução, mas bloqueados. Se você adicionar um Console.Writeline na função DoOneThing, verá que o agendador de tarefas inicia várias delas quase ao mesmo tempo, mas diminui para cerca de uma por segundo. Então, isso significa que você precisa esperar cerca de 300 segundos antes que a tarefa de resolver o dns comece a ser executada. É por isso que excede o tempo limite de 30 segundos.

Agora, aqui vem uma solução rápida se você não estiver fazendo coisas malucas:
Task.Factory.StartNew(DoOneThing, TaskCreationOptions.LongRunning);

Isso forçará o ThreadPoolScheduler a iniciar um thread imediatamente em vez de esperar um segundo antes de criar um novo.

No entanto, isso não funcionará se você estiver fazendo coisas realmente loucas como eu. Vamos alterar o loop for de 300 para 30000, mesmo essa solução também pode falhar. A razão é que ele cria muitos tópicos. Isso consome recursos e tempo. E pode começar a dar início ao processo de GC. Tudo junto, pode não ser capaz de terminar de criar todos esses threads antes que o tempo se esgote.

A maneira perfeita é parar de criar muitas tarefas e usar o agendador padrão para agendá-las. Você pode tentar criar um item de trabalho e colocá-lo em um ConcurrentQueue e, em seguida, criar vários threads como os trabalhadores para consumir os itens.

No entanto, se você não quiser alterar muito a estrutura original, tente da seguinte maneira:

Crie um ThrottledTaskScheduler derivado de TaskScheduler.
  1. Este ThrottledTaskScheduler aceita um TaskScheduler como o subjacente que executará a tarefa real.
  2. Despeje as tarefas no agendador subjacente, mas se exceder o limite, coloque-as em uma fila.
  3. Se alguma tarefa for concluída, verifique a fila e tente despejá-la no agendador subjacente dentro do limite.
  4. Use o código a seguir para iniciar todas essas novas tarefas malucas:

·
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());

Você pode usar System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler como referência. É um pouco mais complicado do que o que precisamos. É para algum outro propósito. Portanto, não se preocupe com as partes que vão e voltam com a função dentro da classe ConcurrentExclusiveSchedulerPair. No entanto, você não pode usá-lo diretamente, pois ele não passa o TaskCreationOptions.LongRunning ao criar a tarefa de encapsulamento.

Funciona para mim. Boa sorte!

P.S.:A razão para ter muitas tarefas na versão do timer provavelmente está dentro do TaskScheduler.TryExecuteTaskInline. Se estiver na thread principal onde o ThreadPool é criado, ele poderá executar algumas das tarefas sem colocá-las na fila.