MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

System.TimeoutException:si è verificato un timeout dopo 30000 ms selezionando un server utilizzando CompositeServerSelector

Questo è un problema molto complicato relativo alla Libreria attività. In breve, ci sono troppe attività create e pianificate in modo che una delle attività che il driver di MongoDB sta aspettando non possa essere completata. Ci ho messo molto tempo per capire che non è un punto morto anche se sembra che lo sia.

Ecco il passaggio per riprodurre:

  1. Scarica il codice sorgente del driver CSharp di MongoDB .
  2. Apri quella soluzione e crea un progetto console all'interno e facendo riferimento al progetto del driver.
  3. Nella funzione Main, crea un System.Threading.Timer che chiamerà TestTask in tempo. Impostare il timer in modo che si avvii immediatamente una volta. Alla fine, aggiungi un Console.Read().
  4. In TestTask, usa un ciclo for per creare 300 attività chiamando Task.Factory.StartNew(DoOneThing). Aggiungi tutte queste attività a un elenco e usa Task.WaitAll per attendere che tutte siano terminate.
  5. Nella funzione DoOneThing, crea un MongoClient ed esegui alcune semplici query.
  6. Ora eseguilo.

Questo non riuscirà nello stesso posto che hai menzionato:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)

Se inserisci alcuni punti di interruzione, saprai che WaitForDescriptionChangedHelper ha creato un'attività di timeout. Attende quindi il completamento di una qualsiasi delle attività DescriptionUpdate o dell'attività di timeout. Tuttavia, il DescriptionUpdate non avviene mai, ma perché?

Ora, tornando al mio esempio, c'è una parte interessante:ho avviato un timer. Se chiami direttamente TestTask, verrà eseguito senza alcun problema. Confrontandoli con la finestra Attività di Visual Studio, noterai che la versione timer creerà molte più attività rispetto alla versione senza timer. Lascia che ti spieghi questa parte un po' più tardi. C'è un'altra importante differenza. Devi aggiungere righe di debug in 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]);
        }
    }

Aggiungendo queste righe, scoprirai anche che la versione senza timer si aggiornerà due volte ma la versione con timer si aggiornerà solo una volta. E il secondo viene da "MonitorServerAsync" in ServerMonitor.cs. Si è scoperto che, nella versione timer, MontiorServerAsync è stato eseguito, ma dopo essere passato attraverso ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync e TcpStreamFactory.CreateStreamAsync, ha finalmente raggiunto TcpStreamFactory.ResolveEndPointsAsync. La cosa brutta accade qui:Dns.GetHostAddressesAsync . Questo non viene mai giustiziato. Se modifichi leggermente il codice e lo trasformi 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();

Sarai in grado di trovare l'ID attività. Esaminando la finestra Attività di Visual Studio, è abbastanza ovvio che ci sono circa 300 attività davanti ad essa. Solo molti di essi sono in esecuzione ma bloccati. Se aggiungi un Console.Writeline nella funzione DoOneThing, vedrai che l'utilità di pianificazione delle attività ne avvia diversi quasi contemporaneamente, ma poi rallenta a circa uno al secondo. Quindi, questo significa che devi attendere circa 300 secondi prima che l'attività di risoluzione del DNS inizi a essere eseguita. Ecco perché supera il timeout di 30 secondi.

Ora, ecco una rapida soluzione se non stai facendo cose pazze:

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

Ciò forzerà ThreadPoolScheduler ad avviare immediatamente un thread invece di attendere un secondo prima di crearne uno nuovo.

Tuttavia, questo non funzionerà se stai facendo cose davvero pazze come me. Cambiamo il ciclo for da 300 a 30000, anche questa soluzione potrebbe fallire. Il motivo è che crea troppi thread. Questo richiede risorse e tempo. E potrebbe iniziare a dare il via al processo GC. Nel complesso, potrebbe non essere in grado di completare la creazione di tutti quei thread prima che il tempo scada.

Il modo perfetto è smettere di creare molte attività e utilizzare l'utilità di pianificazione predefinita per pianificarle. Puoi provare a creare un elemento di lavoro e inserirlo in una ConcurrentQueue e quindi creare diversi thread come lavoratori per consumare gli elementi.

Tuttavia, se non vuoi modificare troppo la struttura originale, puoi provare nel modo seguente:

Crea un ThrottledTaskScheduler derivato da TaskScheduler.

  1. Questo ThrottledTaskScheduler accetta un TaskScheduler come quello sottostante che eseguirà l'attività effettiva.
  2. Esegui il dump delle attività nello scheduler sottostante, ma se supera il limite, mettilo invece in una coda.
  3. Se un'attività è terminata, controlla la coda e prova a scaricarla nello scheduler sottostante entro il limite.
  4. Utilizza il seguente codice per iniziare tutte quelle nuove folli attività:

·

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

Puoi prendere System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler come riferimento. È un po' più complicato di quello di cui abbiamo bisogno. È per qualche altro scopo. Quindi, non preoccuparti di quelle parti che vanno avanti e indietro con la funzione all'interno della classe ConcurrentExclusiveSchedulerPair. Tuttavia, non puoi usarlo direttamente perché non passa TaskCreationOptions.LongRunning durante la creazione dell'attività di wrapping.

Per me funziona. Buona fortuna!

P.S.:il motivo per avere molte attività nella versione timer è probabilmente all'interno di TaskScheduler.TryExecuteTaskInline. Se si trova nel thread principale in cui viene creato ThreadPool, sarà in grado di eseguire alcune attività senza metterle in coda.