MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

System.TimeoutException:Nach 30000 ms ist ein Timeout aufgetreten, nachdem ein Server mit CompositeServerSelector ausgewählt wurde

Dies ist ein sehr kniffliges Problem im Zusammenhang mit der Aufgabenbibliothek. Kurz gesagt, es werden zu viele Aufgaben erstellt und geplant, sodass eine der Aufgaben, auf die der MongoDB-Treiber wartet, nicht abgeschlossen werden kann. Ich habe sehr lange gebraucht, um zu erkennen, dass es kein Deadlock ist, obwohl es so aussieht.

Hier ist der zu reproduzierende Schritt:

  1. Laden Sie den Quellcode des CSharp-Treibers von MongoDB herunter .
  2. Öffnen Sie diese Projektmappe und erstellen Sie darin ein Konsolenprojekt, das auf das Treiberprojekt verweist.
  3. Erstellen Sie in der Main-Funktion einen System.Threading.Timer, der TestTask rechtzeitig aufruft. Stellen Sie den Timer so ein, dass er einmal sofort startet. Fügen Sie am Ende ein Console.Read() hinzu.
  4. Verwenden Sie in TestTask eine for-Schleife, um 300 Aufgaben zu erstellen, indem Sie Task.Factory.StartNew(DoOneThing) aufrufen. Fügen Sie all diese Aufgaben zu einer Liste hinzu und verwenden Sie Task.WaitAll, um zu warten, bis alle erledigt sind.
  5. Erstellen Sie in der DoOneThing-Funktion einen MongoClient und führen Sie eine einfache Abfrage durch.
  6. Führen Sie es jetzt aus.

Dies schlägt an derselben Stelle fehl, die Sie erwähnt haben:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)

Wenn Sie einige Unterbrechungspunkte setzen, wissen Sie, dass der WaitForDescriptionChangedHelper eine Timeout-Aufgabe erstellt hat. Es wartet dann auf den Abschluss einer der Tasks DescriptionUpdate oder Timeout. Das DescriptionUpdate findet jedoch nie statt, aber warum?

Zurück zu meinem Beispiel gibt es einen interessanten Teil:Ich habe einen Timer gestartet. Wenn Sie die TestTask direkt aufrufen, wird sie problemlos ausgeführt. Wenn Sie sie mit dem Aufgabenfenster von Visual Studio vergleichen, werden Sie feststellen, dass die Timer-Version viel mehr Aufgaben erstellt als die Nicht-Timer-Version. Lassen Sie mich diesen Teil etwas später erklären. Es gibt noch einen weiteren wichtigen Unterschied. Sie müssen Debug-Zeilen in Cluster.cs hinzufügen :

    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]);
        }
    }

Wenn Sie diese Zeilen hinzufügen, werden Sie auch feststellen, dass die Nicht-Timer-Version zweimal aktualisiert wird, die Timer-Version jedoch nur einmal. Und der zweite kommt von „MonitorServerAsync“ in ServerMonitor.cs. Es stellte sich heraus, dass MontiorServerAsync in der Timer-Version ausgeführt wurde, aber nachdem es den ganzen Weg durch ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync und TcpStreamFactory.CreateStreamAsync gegangen war, erreichte es schließlich TcpStreamFactory.ResolveEndPointsAsync. Das Schlimme passiert hier:Dns.GetHostAddressesAsync . Dieser wird nie hingerichtet. Wenn Sie den Code leicht ändern und ihn umwandeln in:

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

Sie können die Aufgaben-ID finden. Wenn Sie in das Aufgabenfenster von Visual Studio schauen, ist es ziemlich offensichtlich, dass es ungefähr 300 Aufgaben davor gibt. Nur einige von ihnen werden ausgeführt, aber blockiert. Wenn Sie eine Console.Writeline in der DoOneThing-Funktion hinzufügen, werden Sie feststellen, dass der Aufgabenplaner mehrere davon fast gleichzeitig startet, sich dann aber auf etwa eine pro Sekunde verlangsamt. Das bedeutet, dass Sie etwa 300 Sekunden warten müssen, bevor die Aufgabe zum Auflösen des DNS ausgeführt wird. Deshalb wird das Timeout von 30 Sekunden überschritten.

Hier kommt jetzt eine schnelle Lösung, wenn Sie keine verrückten Dinge tun:

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

Dies zwingt den ThreadPoolScheduler, einen Thread sofort zu starten, anstatt eine Sekunde zu warten, bevor er einen neuen erstellt.

Dies wird jedoch nicht funktionieren, wenn Sie wirklich verrückte Dinge wie ich tun. Ändern wir die for-Schleife von 300 auf 30000, auch diese Lösung könnte fehlschlagen. Der Grund ist, dass es zu viele Threads erstellt. Dies ist ressourcen- und zeitaufwändig. Und es könnte den GC-Prozess in Gang setzen. Alles in allem ist es möglicherweise nicht möglich, alle diese Threads zu erstellen, bevor die Zeit abläuft.

Der perfekte Weg ist, nicht mehr viele Aufgaben zu erstellen und den Standardplaner zu verwenden, um sie zu planen. Sie können versuchen, ein Arbeitselement zu erstellen und es in eine ConcurrentQueue einzufügen und dann mehrere Threads als Worker zu erstellen, um die Elemente zu verbrauchen.

Wenn Sie jedoch die ursprüngliche Struktur nicht zu sehr verändern möchten, können Sie es auf folgende Weise versuchen:

Erstellen Sie einen von TaskScheduler abgeleiteten ThrottledTaskScheduler.

  1. Dieser ThrottledTaskScheduler akzeptiert einen TaskScheduler als zugrunde liegenden TaskScheduler, der die eigentliche Aufgabe ausführt.
  2. Legen Sie die Aufgaben an den zugrunde liegenden Planer ab, aber wenn das Limit überschritten wird, stellen Sie sie stattdessen in eine Warteschlange.
  3. Wenn eine der Aufgaben abgeschlossen ist, überprüfen Sie die Warteschlange und versuchen Sie, sie innerhalb des Limits in den zugrunde liegenden Scheduler zu werfen.
  4. Verwenden Sie den folgenden Code, um all diese verrückten neuen Aufgaben zu starten:

·

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

Sie können System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler als Referenz verwenden. Es ist etwas komplizierter als das, was wir brauchen. Es ist für einen anderen Zweck. Machen Sie sich also keine Sorgen um die Teile, die mit der Funktion innerhalb der ConcurrentExclusiveSchedulerPair-Klasse hin und her gehen. Sie können es jedoch nicht direkt verwenden, da es TaskCreationOptions.LongRunning nicht übergibt, wenn es die Wrapping-Aufgabe erstellt.

Für mich geht das. Viel Glück!

P.S.:Der Grund für die vielen Tasks in der Timer-Version liegt wahrscheinlich in TaskScheduler.TryExecuteTaskInline. Wenn es sich im Haupt-Thread befindet, in dem der ThreadPool erstellt wird, kann es einige der Aufgaben ausführen, ohne sie in die Warteschlange zu stellen.