sql >> Databasteknik >  >> NoSQL >> MongoDB

System.TimeoutException:En timeout inträffade efter 30 000 ms val av en server med CompositeServerSelector

Detta är ett mycket knepigt problem relaterat till Task Library. Kort sagt, det finns för många uppgifter skapade och schemalagda så att en av uppgifterna MongoDB:s förare väntar på inte kommer att kunna slutföras. Jag tog väldigt lång tid innan jag insåg att det inte är ett dödläge även om det ser ut som det är det.

Här är steget för att reproducera:

  1. Ladda ner källkoden för MongoDB:s CSharp-drivrutin .
  2. Öppna den lösningen och skapa ett konsolprojekt inuti och hänvisa till drivrutinsprojektet.
  3. I huvudfunktionen skapar du en System.Threading.Timer som anropar TestTask i tid. Ställ in timern så att den startar omedelbart en gång. I slutet lägger du till en Console.Read().
  4. I TestTask, använd en for-loop för att skapa 300 uppgifter genom att anropa Task.Factory.StartNew(DoOneThing). Lägg till alla dessa uppgifter i en lista och använd Task.WaitAll för att vänta på att alla är klara.
  5. I DoOneThing-funktionen skapar du en MongoClient och gör en enkel fråga.
  6. Kör det nu.

Detta kommer att misslyckas på samma plats som du nämnde:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)

Om du lägger in några brytpunkter kommer du att veta att WaitForDescriptionChangedHelper skapade en timeout-uppgift. Den väntar sedan på att någon av uppgiften DescriptionUpdate eller timeout-uppgiften ska slutföras. Beskrivningsuppdateringen sker dock aldrig, men varför?

Nu, tillbaka till mitt exempel, finns det en intressant del:jag startade en timer. Om du ringer TestTask direkt kommer den att köras utan problem. Genom att jämföra dem med Visual Studios Tasks-fönster kommer du att märka att timerversionen kommer att skapa mycket fler uppgifter än icke-timerversionen. Låt mig förklara den här delen lite senare. Det finns en annan viktig skillnad. Du måste lägga till felsökningsrader i 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]);
        }
    }

Genom att lägga till dessa rader kommer du också att få reda på att versionen utan timer kommer att uppdateras två gånger men att timerversionen bara uppdateras en gång. Och den andra kommer från "MonitorServerAsync" i ServerMonitor.cs. Det visade sig att MontiorServerAsync exekverades i timerversionen, men efter att den kommit hela vägen genom ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync och TcpStreamFactory.CreateStreamAsyncpStream nådde den äntligen TcReamEnFactory. Det dåliga händer här:Dns.GetHostAddressesAsync . Den här blir aldrig avrättad. Om du ändrar koden något och omvandlar den till:

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

Du kommer att kunna hitta uppgifts-id:t. Genom att titta in i Visual Studios Tasks-fönster är det ganska uppenbart att det finns runt 300 uppgifter framför det. Endast flera av dem körs men blockeras. Om du lägger till en Console.Writeline i DoOneThing-funktionen kommer du att se att aktivitetsschemaläggaren startar flera av dem nästan samtidigt men sedan saktar det ner till runt en per sekund. Så det betyder att du måste vänta i cirka 300 sekunder innan uppgiften att lösa dns börjar köras. Det är därför den överskrider tidsgränsen på 30 sekunder.

Nu kommer här en snabb lösning om du inte gör galna saker:

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

Detta tvingar ThreadPoolScheduler att starta en tråd omedelbart istället för att vänta en sekund innan du skapar en ny.

Detta kommer dock inte att fungera om du gör riktigt galna saker som jag. Låt oss ändra for-slingan från 300 till 30000, även denna lösning kan också misslyckas. Anledningen är att det skapar för många trådar. Detta är resurskrävande och tidskrävande. Och det kan börja sätta igång GC-processen. Sammantaget kanske det inte går att skapa alla dessa trådar innan tiden rinner ut.

Det perfekta sättet är att sluta skapa massor av uppgifter och använda standardschemaläggaren för att schemalägga dem. Du kan prova att skapa arbetsobjekt och lägga det i en ConcurrentQueue och sedan skapa flera trådar som arbetarna för att konsumera objekten.

Men om du inte vill ändra den ursprungliga strukturen för mycket kan du prova på följande sätt:

Skapa en ThrottledTaskScheduler härledd från TaskScheduler.

  1. Denna ThrottledTaskScheduler accepterar en TaskScheduler som den underliggande som kommer att köra den faktiska uppgiften.
  2. Dumpa uppgifterna till den underliggande schemaläggaren, men om den överskrider gränsen, placera den i en kö istället.
  3. Om någon av uppgifterna har slutförts, kontrollera kön och försök dumpa dem till den underliggande schemaläggaren inom gränsen.
  4. Använd följande kod för att starta alla de där galna nya uppgifterna:

·

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

Du kan ta System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler som referens. Det är lite mer komplicerat än det vi behöver. Det är för något annat syfte. Så oroa dig inte för de delar som går fram och tillbaka med funktionen i ConcurrentExclusiveSchedulerPair-klassen. Du kan dock inte använda den direkt eftersom den inte klarar TaskCreationOptions.LongRunning när den skapar omslutningsuppgiften.

Det funkar för mig. Lycka till!

P.S.:Anledningen till att ha många uppgifter i timerversionen ligger förmodligen i TaskScheduler.TryExecuteTaskInline. Om det finns i huvudtråden där ThreadPool skapas, kommer den att kunna utföra några av uppgifterna utan att lägga dem i kön.




  1. kan inte komma åt req.users egenskaper

  2. Förstå WriteConcern i MongoDB C#

  3. Hur hånar man mongodb för python unittests?

  4. Sammanfoga och formatera array av objekt i Python