[.NET Core 2.2] Parallel.ForEach mit MySQL Insert ergibt doppelte Einträge

  • C#

Es gibt 10 Antworten in diesem Thema. Der letzte Beitrag () ist von xChRoNiKx.

    [.NET Core 2.2] Parallel.ForEach mit MySQL Insert ergibt doppelte Einträge

    Heyho,

    ich habe da mal wieder ein kleines Problemchen das ich nicht lösen kann.

    Ich habe eine List<int> mit verschiedenen (keine doppelten) IDs.
    Diese gehe ich per Parallel.ForEach durch. Nun wird darin dann mit dieser ID ein WebRequest gemacht
    und zurück kommt JSON und das funktioniert auch tadellos.

    Nun schreibe ich dann einen Datensatz in meine MySQL Datenbank nur da kommen völlig Random manche Einträge 2,3 oder auch 4 mal
    an obwohl das absolut nicht sein kann. Ich habe Haltepunkte gesetzt und es wird jedes mal die richtige ID übergeben nur kommt dann was völlig anderes in
    der Datenbank an. Habt ihr ne Idee? Ich such da jetzt schon locker 4 Stunden den Fehler aber egal was ich versuche es will nicht. Es kommt halt auch kein Fehler
    sondern es ist einfach so das Random ein paar Einträge doppelt sind und dafür dann sogar andere Fehlen.
    Falls ihr noch was braucht sagt bescheid.

    Hier mal ein wenig Code:
    Spoiler anzeigen

    C#-Quellcode

    1. public static WebHelper wHelp = new WebHelper();
    2. public static dbHelper dataBase = new dbHelper();
    3. static void Main(string[] args)
    4. {
    5. Settings.LoadSettings();
    6. string json = null;
    7. bool isSub = false;
    8. int x = 0;
    9. //Auskommentiert um erstmal nur mit dem gleichen JSON zu testen.
    10. //WebHelperResult newVideosResult = new WebHelperResult();
    11. //newVideosResult = wHelp.makeRequestAsync(Settings.newestVideosURL).Result;
    12. //if (newVideosResult.status == WebHelperResult.StatusTyp.SERVERERROR) Environment.Exit(0);
    13. //json = ExtractJson(result.content);
    14. json = File.ReadAllText("json.json");
    15. List<int> videos = Parse.ParseNewestVideoJsonToList(json);
    16. List<int> onlyNewVideos = new List<int>();
    17. foreach (int videoId in videos)
    18. {
    19. bool exists = dataBase.CheckVideoExists(videoId);
    20. if (exists) continue;
    21. onlyNewVideos.Add(videoId);
    22. Console.WriteLine(exists.ToString() + " / " + videoId.ToString());
    23. }
    24. Console.WriteLine("Vorsortierung fertig: {0} neue Videos, Enter drücken um Weiter zu machen.", onlyNewVideos.Count.ToString());
    25. Console.ReadLine();
    26. Parallel.ForEach(onlyNewVideos, (videoId) =>
    27. {
    28. x += 1;
    29. VideoInfo vDetail = new VideoInfo();
    30. WebHelperResult result = new WebHelperResult();
    31. result = wHelp.makeRequestAsync(Settings.baseVideoURL + videoId.ToString()).Result;
    32. if (result.status == WebHelperResult.StatusTyp.SERVERERROR || result.status == WebHelperResult.StatusTyp.VIDEOERROR) return;
    33. json = ExtractJson(result.content);
    34. vDetail = Parse.ParseSingleVideoJsonToVideoInfo(json);
    35. isSub = dataBase.CheckUserWhitelisted(vDetail.videoUploader);
    36. dataBase.addVideoToDatabase(vDetail, whitelisted: isSub);
    37. if (isSub) downloadVideo(vDetail, true);
    38. });
    39. Console.WriteLine("Fertig");
    40. Console.ReadLine();
    41. }


    Und hier die addVideoToDatabase Methode:
    Spoiler anzeigen

    C#-Quellcode

    1. public void addVideoToDatabase(VideoInfo vDetails, bool whitelisted = false, bool download = false)
    2. {
    3. int subStatus = 0;
    4. int downloadStatus = 0;
    5. if (whitelisted) subStatus = 1;
    6. if (download) downloadStatus = 1;
    7. string myInsertQuery = "INSERT INTO " + Settings.mysql_video_table_name + " (`videoId`, `videoUrl`, `videoUploader`, `videoUploadDate`, `videoTitle`, `videoDuration`, `videoCategories`, `watchedStatus`, `subscribedStatus`, `DownloadStatus`) "
    8. + " VALUES (@videoId, @videoUrl, @videoUploader, FROM_UNIXTIME(@videoUploadDate), @videoTitle, @videoDuration, @videoCategories, @watchedStatus, @subscribedStatus, @DownloadStatus)";
    9. using (MySqlConnection c = new MySqlConnection(Settings.connectionString))
    10. {
    11. c.Open();
    12. using (MySqlCommand myCommand = new MySqlCommand(myInsertQuery, c))
    13. {
    14. myCommand.Prepare();
    15. myCommand.Parameters.AddWithValue("@videoId", vDetails.videoId);
    16. myCommand.Parameters.AddWithValue("@videoUrl", vDetails.videoUrl);
    17. myCommand.Parameters.AddWithValue("@videoUploader", vDetails.videoUploader);
    18. myCommand.Parameters.AddWithValue("@videoUploadDate", vDetails.videoCreationTimestamp);
    19. myCommand.Parameters.AddWithValue("@videoTitle", vDetails.videoTitle);
    20. myCommand.Parameters.AddWithValue("@videoDuration", vDetails.videoDuration);
    21. myCommand.Parameters.AddWithValue("@videoCategories", vDetails.videoCategories);
    22. myCommand.Parameters.AddWithValue("@watchedStatus", 0);
    23. myCommand.Parameters.AddWithValue("@subscribedStatus", subStatus);
    24. myCommand.Parameters.AddWithValue("@DownloadStatus", downloadStatus);
    25. myCommand.ExecuteNonQuery();
    26. //Console.WriteLine("addVideo| ID: {0}", vDetails.videoId.ToString());
    27. }
    28. }
    29. }
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen
    Aus der ​Parse.ParseNewestVideoJsonToList(json); Methode kommen auch keine doppelten Einträge zurück? Du überprüfst zwar anscheint mit ​dataBase.CheckVideoExists(videoId);, ob die Einträge bereits in der Datenbank sind aber nicht ob innerhalb der Liste doppelte vorkommen. Oder ist das nicht möglich?
    Heyho,

    danke für die Antwort. Nein da können keine Doppelten IDs kommen. Es sind immer verschiedene (Unique) IDs.
    Es sind genau 60 nicht doppelte IDs die da rauskommen. Am Ende sind dann 60 Einträge in der Datenbank aber 2-3 Einträge doppelt oder dreifach
    und es fehlen dann natürlich andere IDs. Ich würde ja verstehen wenn es dann 70 Einträge sind wo alle meine 60 Unique IDs drin sind nur halt manche doppelt.

    Aber sobald eine doppelt ist fehlt dafür eine andere. Ziemlich doofes Problem.
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen
    Würde hier nochmal einen Nachtrag machen, ich habe nun nach Zeile 45 mal eine List<string> genommen und dort
    einfach mal die videoId von der ForEach und die videoId nach dem Parsen rein geschrieben.
    log.Add(videoId.ToString() + " // " + vDetail.videoId.ToString());
    raus kam (links die ID die sein soll / rechts die die zurück kommt):
    Spoiler anzeigen

    Quellcode

    1. 11238588 // 11238658
    2. 11238658 // 11238658
    3. 11242137 // 11242137
    4. 11238603 // 11238603
    5. 11238529 // 11238529
    6. 11168676 // 11168676
    7. 11238610 // 11238610
    8. 11238632 // 11238632
    9. 11238546 // 11238546
    10. 11238570 // 11238570
    11. 11238504 // 11238504
    12. 11238585 // 11238585
    13. 11238542 // 11238542
    14. 11238619 // 11238619
    15. 11238553 // 11238553
    16. 11238581 // 11238581
    17. 11238541 // 11238541
    18. 11238628 // 11238628
    19. 11238657 // 11238657
    20. 11238609 // 11238609
    21. 11238664 // 11238664
    22. 11238601 // 11238601
    23. 11238574 // 11238574
    24. 11238569 // 11238569
    25. 11238656 // 11238656
    26. 11238536 // 11238536
    27. 11168414 // 11168414
    28. 11238620 // 11238620
    29. 11167808 // 11167808
    30. 11238626 // 11238626
    31. 11238519 // 11238519
    32. 11168337 // 11168337
    33. 11238598 // 11238598
    34. 11238655 // 11238655
    35. 11238633 // 11238633
    36. 11238669 // 11238669
    37. 11238568 // 11238568
    38. 11238572 // 11238572
    39. 11167278 // 11167278
    40. 11238517 // 11238517
    41. 11238618 // 11238618
    42. 11238625 // 11238625
    43. 11238531 // 11238531
    44. 11238552 // 11238552
    45. 11238567 // 11238567
    46. 11238590 // 11238590
    47. 11167587 // 11167587
    48. 11238511 // 11238511
    49. 11098356 // 11098356
    50. 11238616 // 11238616
    51. 11238565 // 11238565
    52. 11238589 // 11238589
    53. 11167628 // 11167628
    54. 11242047 // 11242047
    55. 11238506 // 11238506
    56. 11238502 // 11238502
    57. 11238613 // 11238613
    58. 10070718 // 10070718
    59. 11238612 // 11238612
    60. 11242088 // 11242088


    Wie man sieht ist direkt bei der ersten nun falsch. Die linke erste ID existiert nicht in der Table dafür natürlich die 2. ID gleich 2mal.
    Evtl liegt das an meinem WebRequest? Der sieht so aus:
    Spoiler anzeigen

    C#-Quellcode

    1. class WebHelperResult
    2. {
    3. public string content = null;
    4. public StatusTyp status;
    5. public enum StatusTyp
    6. {
    7. SUCCESS,
    8. SERVERERROR,
    9. VIDEOERROR
    10. }
    11. }
    12. class WebHelper
    13. {
    14. private HttpClient client;
    15. private HttpClientHandler handler = new HttpClientHandler();
    16. private WebHelperResult result;
    17. public WebHelper()
    18. {
    19. handler.AllowAutoRedirect = true;
    20. client = new HttpClient(handler);
    21. client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36");
    22. client.DefaultRequestHeaders.Accept.ParseAdd("*/*");
    23. }
    24. public async Task<WebHelperResult> makeRequestAsync(string url)
    25. {
    26. result = new WebHelperResult();
    27. HttpResponseMessage response = await client.GetAsync(url);
    28. if (response.IsSuccessStatusCode)
    29. {
    30. result.status = WebHelperResult.StatusTyp.SUCCESS;
    31. result.content = await response.Content.ReadAsStringAsync();
    32. return result;
    33. } else {
    34. int statusCode = (int)response.StatusCode;
    35. if (statusCode >= 500 && statusCode <= 599)
    36. {
    37. result.status = WebHelperResult.StatusTyp.SERVERERROR;
    38. result.content = null;
    39. return result;
    40. }
    41. result.status = WebHelperResult.StatusTyp.VIDEOERROR;
    42. result.content = null;
    43. return result;
    44. }
    45. }
    46. }


    Kommt evtl. der httpClient nicht mit na Parallel.ForEach klar? Das er da irgendwie was vertauscht oder so?
    Ich weiß echt nicht mehr weiter.
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen

    C#-Quellcode

    1. json = ExtractJson(result.content);
    2. vDetail = Parse.ParseSingleVideoJsonToVideoInfo(json);
    3. isSub = dataBase.CheckUserWhitelisted(vDetail.videoUploader);


    Du führst das parallel aus, aber überschreibst immer wieder die selbe Variablen (json und isSub). Wenn jetzt ein Task den anderen zwischendrin unterbricht, dann hast du ein anderes Ergebnis in den Variablen bevor du dataBase.addVideoToDatabase(vDetail, whitelisted: isSub); aufrufst. Leg diese einfach in der ForEach neu an.
    Mir scheint da eine doppelt gemoppelte Nebenläufigkeit implementiert:
    Das Parallel.For erzeugt ja bereits viele Tasks.
    Dann sollte imo im einzelnen Task nicht noch mal mit Async herumgefuchtelt werden, weil paralleller wirds nimmer.

    also Vermutungsweise könnte diese evtl. überflüssige Komplexität ja zum Problem beitragen.

    Bluespide schrieb:

    Leg diese einfach in der ForEach neu an.


    Könnte sein. Ich habe dies mal geändert und lasse nun Tests laufen. Werde ich dann heute Abend bescheid geben.

    @ErfinderDesRades jou leider. Das Problem ist wenn ich den httpClient ohne await aufrufe und stattdessen mit .Result am Ende dann wartet er nicht bis der Request vorbei ist.
    Also muss ich die Methode als async deklarieren und mit await arbeiten. Es sei denn du kennst einen anderen weg. Der wär mir natürlich dann lieber :)

    EDIT// @Bluespide leider hilft das nicht. Es kommen weiterhin duplicates in die Datenbank. Evtl. liegt es ja daran was der @ErfinderDesRades da sagt,
    nur weiß ich nicht wie ich das ändern solle weil sonst nicht gewartet wird bis der Request durch ist.
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „xChRoNiKx“ ()

    vlt. idt httpClient kein gutes Instrument - das ist ja eine ziemlich fette Komponente.
    Ich nehme normalerweise lieber HttpWebRequest, vor allem, wenn ich mehrere Anfragen gleichzeitig losschicken will.
    ich denk mir das so: Parallel.For erzeugt viele Requests, jeder in seim Task. Der einzelne Request ruft synchron ab, und blockiert also für die Dauer der Abfrage seien Task.
    Der Paralell.For-Mechanismus kümmert sich dabei darum, dass so lange gewartet wird, bis alle Request durchgelaufen sind.

    (dass er nur bei Await wartet, da versteh ich nicht, was du meinst. Ein Teil des Codes wartet ja bei Await grade nicht. aber bei paralell.For ist grade erwünscht, dass der einzelne Thread blockiert, bis der Job getan ist. Daher ist Async/Await innerhalb des einzelnen Threads zu vermeiden.
    Aber vmtl. weisst du das selbst, und ich kapier einfach noch nicht das eigliche Problem)

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

    @ErfinderDesRades habe das ganze nun auf nen HttpWebRequest umgebaut, und nun funktioniert es ohne Duplikate.

    Ich glaube da bin ich mal wieder gegen meine Wand im Kopf gefahren was das async/await gedöns angeht.

    Das eigentliche Problem waren die doppelten Einträge in der Datenbank. Diese sind nun nach der Umstellung auf HttpWebRequest weg.
    Also zumindest nach ca. 200.000 Test Einträgen war keiner doppelt. Werde das ganze noch über nacht laufen lassen aber das sollte es denke ich gewesen sein.

    Einer der Gründe warum ich den httpClient gerne nutze ist weil dort das Handling der HTTPStatusCodes super einfach ist.
    Während ich bei einem HttpWebRequest diese per Try-Catch fangen und behandeln muss, es sei denn da gibt es auch wieder eine andere lösung.
    Meine sieht da so aus:

    Spoiler anzeigen

    C#-Quellcode

    1. public WebHelperResult makeRequest(string url)
    2. {
    3. WebHelperResult result = new WebHelperResult();
    4. try
    5. {
    6. HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
    7. request.UserAgent = userAgent;
    8. using (WebResponse response = request.GetResponse())
    9. {
    10. using (StreamReader responseStream = new StreamReader(response.GetResponseStream()))
    11. {
    12. result.status = WebHelperResult.StatusTyp.SUCCESS;
    13. result.content = responseStream.ReadToEnd();
    14. return result;
    15. }
    16. }
    17. }
    18. catch (WebException wex)
    19. {
    20. if (wex.Response != null)
    21. {
    22. HttpWebResponse resp = (HttpWebResponse)wex.Response;
    23. int statusCode = (int)resp.StatusCode;
    24. if (statusCode >= 500 && statusCode <= 599)
    25. {
    26. result.status = WebHelperResult.StatusTyp.SERVERERROR;
    27. result.content = null;
    28. return result;
    29. }
    30. else if (statusCode >= 400 && statusCode <= 499)
    31. {
    32. result.status = WebHelperResult.StatusTyp.VIDEOERROR;
    33. result.content = null;
    34. return result;
    35. }
    36. }
    37. throw;
    38. } catch (Exception ex)
    39. {
    40. throw;
    41. }
    42. }



    (ich lasse das Thema mal noch als "unerledigt" da ich dann erst morgen nach den über nacht tests definitiv sagen kann das sie weg sind)
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen
    ich finds eigentümlich, dass du mit deim Catch aus dem Using rausspringst.
    Ich würd den Catch da machen, wo die Response noch direkt zugreifbar ist.
    Dann brauchts auch keinen Cast, und keinen Test, ob wex.Response null ist (ich bezweifel eh, dass das jemals auftreten kann).
    Und einen Catch-Zweig, in dem nur throw steht kann man sich doch sowieso sparen, odr?
    Und result.Content ist doch bereits null, wieso das an zwei stellen nochma extra zuweisen?
    Und return result taucht sogar an 3 stellen auf, sodasses ganzngar unausweichlich ist (mit Ausnahme von ReThrowing).
    Ganz unausweichliche Zeilen kann und sollte man aber immer so plazieren (versuchen), dass sie nur einmal auftauchen.

    Also imo viel Refactoring-Potential, und am Ende findestes vlt. ebenso schnuckelig wies vorher mittm httpClient war.

    Ach - und Benamung: responseStream ist kein Stream, also darf er auf keinen Fall so heissen.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

    ErfinderDesRades schrieb:

    Ich würd den Catch da machen, wo die Response noch direkt zugreifbar ist.


    Ok, das habe ich versucht. Allerdings wenn ich den try-catch innerhalb des Usings mache kann ich zwar zugreifen aber er fängt die Exception nicht mehr die von request.GetResponse() ausgelöst wird.
    Habe das jetzt ohne using gemacht, kann dann zwar auf response zugreifen allerdings ist das natürlich null weil, wie ich denke, die Exception kommt haut er das alles in die WebException wex.


    Und dort muss ich den WebResponse zu HttpWebResponse casten damit ich an den StatusCode komme.

    Alle anderen Punkte habe ich glaube ich erfolgreich bearbeitet(hat schon mal gut 10 zeilen gebracht). Duplikate sind in der Nacht nicht aufgetaucht also ist das eigentlich Problem gelöst.

    Spoiler anzeigen

    C#-Quellcode

    1. public WebHelperResult makeRequest2(string url)
    2. {
    3. WebHelperResult result = new WebHelperResult();
    4. HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
    5. WebResponse response = null;
    6. request.UserAgent = userAgent;
    7. try
    8. {
    9. response = request.GetResponse();
    10. }
    11. catch (WebException wex)
    12. {
    13. HttpWebResponse wresp = (HttpWebResponse)wex.Response;
    14. int statusCode = (int)wresp.StatusCode;
    15. if (statusCode >= 500 && statusCode <= 599)
    16. {
    17. result.status = WebHelperResult.StatusTyp.SERVERERROR;
    18. }
    19. else if (statusCode >= 400 && statusCode <= 499)
    20. {
    21. result.status = WebHelperResult.StatusTyp.VIDEOERROR;
    22. }
    23. return result;
    24. }
    25. using (StreamReader responseReader = new StreamReader(response.GetResponseStream()))
    26. {
    27. result.status = WebHelperResult.StatusTyp.SUCCESS;
    28. result.content = responseReader.ReadToEnd();
    29. }
    30. return result;
    31. }
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen