Dele
Aktives Mitglied
- 9. März 2011
- 39
- 9
Kurzfassung
Wird an einem Artikel mit laufendem eBay-Angebot eine Bildänderung gespeichert, die für die Plattform eBay aktiviert ist, hängt die Artikelmaske dauerhaft. Ursache ist eine Selbstblockade über zwei Datenbankverbindungen: der Speichervorgang schreibt innerhalb seiner offenen Transaktion eine Zeile in ebay_geaenderte_laufende_angebote und ruft anschließend - noch in derselben Transaktion - EbayGrundpreisAktualisieren auf. Diese Methode schreibt über die alte Datenbankschicht (jtlDatabase.DB.executeNonQuery) auf einer zweiten, eigens geöffneten Verbindung genau dieselbe Zeile und wartet dort auf die Sperre der ersten. Die erste kann nicht festschreiben, weil ihr Thread synchron in der zweiten steckt.
Beim Beenden über den Task-Manager werden die Artikeldaten sauber zurückgerollt, es gehen keine Daten verloren. Vollständig ist die Rückrollung aber nicht: Wurde ein Bild hinzugefügt, bleibt eine halbfertige Zeile in tBild zurück, die später den Bildimport der Ameise mit einer NullReferenceException abstürzen lässt - dazu unten ein eigener Abschnitt.
Umgebung
Reproduktion
Ab hier reagiert die Maske nicht mehr; auch „Abbrechen" und das Fenster-X bleiben wirkungslos. Nur der Task-Manager beendet den Vorgang. Bei Artikeln ohne laufendes eBay-Angebot tritt das Problem nicht auf.
Die Aufrufliste im hängenden Prozess
Gemessen mit dotnet-stack report -p <PID>, zwei Proben im Abstand von etwa einer Minute, identisch:
Der Aufruf ist synchron und ohne CommandTimeout. Der Oberflächen-Thread steht derweil in Form.ShowDialog -> WaitMessage; das Grau der Maske ist die Folge, nicht die Ursache.
Die Wartekette auf dem Server
Aus sys.dm_exec_sessions, sys.dm_exec_requests und sys.dm_tran_locks, während der Hänger bestand. Alle vier Sitzungen gehören zu demselben host_process_id (der hängenden Wawi):
Und die Gegenseite, aus sys.dm_tran_locks:
Sitzung 94 wurde 0,33 Sekunden nach dem Beginn der Transaktion von Sitzung 96 angemeldet - die zweite Verbindung wird also mitten im Speichervorgang aufgebaut.
Warum es nie von selbst endet
Sitzung 96 steht auf sleeping: sie wartet nicht auf eine Sperre, sondern auf den Client. Für den SQL-Server ist das kein Zyklus, also greift die Deadlock-Erkennung nicht und es wird kein Opfer gekürt. Da ExecuteNonQuery kein Zeitlimit gesetzt bekommt, wartet der Aufruf unbegrenzt. Die Sperre hängt am Client, nicht am Server: Sobald der Prozess beendet wird, ist sie sofort weg.
Was in der Datenbank passiert
Während des Hängers, mit NOLOCK gelesen (ungesicherter Zwischenstand):
Nach dem Beenden über den Task-Manager:
Es gehen also keine Daten verloren.
Nebenwirkung: halbfertige Zeilen in tBild, die später die Ameise zum Absturz bringen
Die Rückrollung ist allerdings nicht vollständig. Wird beim Hängen ein Bild hinzugefügt, bleibt dessen Zeile in tBild bestehen: Sie wird offenbar vor der Transaktion geschrieben, die sie anschließend verknüpfen und mit einem Blob-Identifier versehen würde. Nach dem Abschießen bleibt zurück:
Diese Zeile ist nicht nur Ballast, sie ist ein Stolperstein. Wird dasselbe Bild später über die Ameise importiert, prüft diese per FindByHash, ob es bereits vorhanden ist, findet die Zeile und bricht beim Deserialisieren ab:
tBild.iBlobIdentifier ist im Schema als uniqueidentifier NULL angelegt, der Domänen-Layer kann den leeren Wert aber nicht lesen. Das ist unabhängig vom Hänger ein eigener Fehler: Die Ameise verträgt einen Datenzustand nicht, den die Wawi selbst erzeugt. Solange die Zeile existiert, scheitert jeder Ameise-Import dieses Bildes - an welchem Artikel auch immer.
Abgrenzung: wann es auftritt und wann nicht
An vier Artikeln desselben Mandanten geprüft, jeweils Bild hinzufügen und löschen:
Daraus folgt zweierlei: Es genügt nicht, dass überhaupt eine eBay-Verknüpfung besteht - erst ein laufendes Angebot löst es aus. Und es liegt nicht am INSERT der Änderungsmarke: Steht die Zeile bereits in der Warteschlange, hängt es genauso, dann eben an zwei konkurrierenden UPDATEs.
Entscheidend ist die Plattformaktivierung des Bildes. Am selben Artikel (Fall D) geprüft:
Ohne Zeile mit kPlattform = 30 (eBay) in tArtikelbildPlattform wird die Änderungsmarke nicht gesetzt und der zweite Schreibweg läuft gar nicht erst an. Die Bedingung lautet also genau: laufendes Angebot + Bildänderung, die eBay betrifft - und das Entfernen der eBay-Aktivierung ist selbst eine solche Änderung.
Die Aufrufliste und die Sperrkette oben stammen von Fall C. Die übrigen drei Fälle sind am Verhalten der Oberfläche geprüft.
Vorschlag
Drei Punkte, die aus unserer Sicht zusammengehören:
1. EbayGrundpreisAktualisieren sollte innerhalb von SaveArtikeldetailsAggregateAsync dieselbe Verbindung und Transaktion verwenden wie der übrige Speichervorgang - oder erst nach dem Festschreiben laufen. Zusätzlich wäre ein CommandTimeout an diesem synchronen ExecuteNonQuery sinnvoll: dann bräche der Vorgang mit einer Fehlermeldung ab, statt die Oberfläche dauerhaft stillzulegen.
2. Die tBild-Zeile sollte in derselben Transaktion entstehen wie ihre Verknüpfung, damit beim Abbruch nichts zurückbleibt.
3. Unabhängig davon sollte der Domänen-Layer iBlobIdentifier = NULL lesen können - die Spalte ist als nullable deklariert, und ein einziger solcher Datensatz legt den Bildimport der Ameise lahm.
Workaround bis dahin
Die Wawi über den Task-Manager beenden - es wird sauber zurückgerollt.
Ein Ausweichweg besteht nur beim Hinzufügen: Lässt man im Kasten Aktivierung für Plattform das Häkchen des eBay-Verkaufskanals weg, läuft das Speichern durch. Das Bild geht dann allerdings auch nicht an eBay.
Die Ameise ist nicht betroffen. Derselbe Artikel (laufendes Angebot), dasselbe Bild, dieselbe eBay-Aktivierung - als Bildimport über die Ameise eingespielt, läuft es durch. Dabei entstehen sowohl die Zeile mit kPlattform = 30 in tArtikelbildPlattform als auch die Änderungsmarke in ebay_geaenderte_laufende_angebote, und die tBild-Zeile bekommt einen iBlobIdentifier. Der Fehler steckt also nicht in der eBay-Nachführung selbst, sondern im Speicherpfad der Artikelmaske. Für das Hinzufügen und Ersetzen von Bildern ist die Ameise damit der Weg, der zurzeit funktioniert.
An bestehenden, eBay-aktiven Bildern führt über die Maske kein Weg vorbei. Auch der naheliegende Umweg, zuerst nur das eBay-Häkchen zu entfernen und danach zu löschen, hängt bereits im ersten Schritt. Solange ein Angebot läuft, sind die vorhandenen eBay-Bilder eines Artikels in 2.1.1 also gar nicht zu ändern.
Nachtrag: Wir haben den Fall zusätzlich als Supportticket eingereicht - Ticket#202609203600119 (JTL-Wawi 2.1.1, Komponente Artikel), mit denselben Belegen: Aufrufliste, Wartekette und Sperrenauszug.
Wird an einem Artikel mit laufendem eBay-Angebot eine Bildänderung gespeichert, die für die Plattform eBay aktiviert ist, hängt die Artikelmaske dauerhaft. Ursache ist eine Selbstblockade über zwei Datenbankverbindungen: der Speichervorgang schreibt innerhalb seiner offenen Transaktion eine Zeile in ebay_geaenderte_laufende_angebote und ruft anschließend - noch in derselben Transaktion - EbayGrundpreisAktualisieren auf. Diese Methode schreibt über die alte Datenbankschicht (jtlDatabase.DB.executeNonQuery) auf einer zweiten, eigens geöffneten Verbindung genau dieselbe Zeile und wartet dort auf die Sperre der ersten. Die erste kann nicht festschreiben, weil ihr Thread synchron in der zweiten steckt.
Beim Beenden über den Task-Manager werden die Artikeldaten sauber zurückgerollt, es gehen keine Daten verloren. Vollständig ist die Rückrollung aber nicht: Wurde ein Bild hinzugefügt, bleibt eine halbfertige Zeile in tBild zurück, die später den Bildimport der Ameise mit einer NullReferenceException abstürzen lässt - dazu unten ein eigener Abschnitt.
Umgebung
- JTL-Wawi 2.1.1.0 (JTL-SharpWawi.exe, .NET 10)
- MS SQL Server, Version laut SSMS 17.0, Datenbank eazybusiness
- Einzelplatz, Client und Server im selben Netz
Reproduktion
- Artikel mit aktivem eBay-Angebot öffnen (ebay_item.status = 3)
- Reiter Bilder, ein Bild markieren, Löschen - die Zeile verschwindet erwartungsgemäß
- Speichern
Ab hier reagiert die Maske nicht mehr; auch „Abbrechen" und das Fenster-X bleiben wirkungslos. Nur der Task-Manager beendet den Vorgang. Bei Artikeln ohne laufendes eBay-Angebot tritt das Problem nicht auf.
Die Aufrufliste im hängenden Prozess
Gemessen mit dotnet-stack report -p <PID>, zwei Proben im Abstand von etwa einer Minute, identisch:
Code:
SaveArtikeldetailsAggregateAsync
-> EbayGrundpreisAktualisieren
-> jtlEbay_geaenderte_laufende_angebote.TrackChangesForEbayItem
-> jtlDatabase.DB.executeNonQuery
-> JTL.Database.DbCommandHelpers.ExecuteNonQuery
-> Microsoft.Data.SqlClient.SqlCommand.ExecuteNonQuery()
-> TdsParserStateObject.ReadSniSyncOverAsync()
Der Aufruf ist synchron und ohne CommandTimeout. Der Oberflächen-Thread steht derweil in Form.ShowDialog -> WaitMessage; das Grau der Maske ist die Folge, nicht die Ursache.
Die Wartekette auf dem Server
Aus sys.dm_exec_sessions, sys.dm_exec_requests und sys.dm_tran_locks, während der Hänger bestand. Alle vier Sitzungen gehören zu demselben host_process_id (der hängenden Wawi):
| Sitzung | Zustand | Rolle |
| 96 | sleeping, open_transaction_count = 1 | der Speichervorgang, user_transaction offen seit 10:23:20.483 |
| 94 | suspended, angemeldet 10:23:20.813 | die zweite Verbindung der alten DB-Schicht |
Code:
session_id 94 -> blocking_session_id 96
command UPDATE
wait_type LCK_M_X
wait_time 327 Sekunden
wait_resource KEY: 5:72057594061455360 (3f3ef1bb1e5f)
Anweisung (@changeGroup int, @kEbayItem_0 int)
UPDATE dbo.ebay_geaenderte_laufende_angebote SET ...
Und die Gegenseite, aus sys.dm_tran_locks:
Code:
94 WAIT KEY X ebay_geaenderte_laufende_angebote (3f3ef1bb1e5f)
96 GRANT KEY X ebay_geaenderte_laufende_angebote (3f3ef1bb1e5f) <- derselbe Schlüssel
96 GRANT KEY X tArtikelbildPlattform (mehrere Zeilen)
96 GRANT KEY X tArtikelkosten
Sitzung 94 wurde 0,33 Sekunden nach dem Beginn der Transaktion von Sitzung 96 angemeldet - die zweite Verbindung wird also mitten im Speichervorgang aufgebaut.
Warum es nie von selbst endet
Sitzung 96 steht auf sleeping: sie wartet nicht auf eine Sperre, sondern auf den Client. Für den SQL-Server ist das kein Zyklus, also greift die Deadlock-Erkennung nicht und es wird kein Opfer gekürt. Da ExecuteNonQuery kein Zeitlimit gesetzt bekommt, wartet der Aufruf unbegrenzt. Die Sperre hängt am Client, nicht am Server: Sobald der Prozess beendet wird, ist sie sofort weg.
Was in der Datenbank passiert
Während des Hängers, mit NOLOCK gelesen (ungesicherter Zwischenstand):
- die 9 tArtikelbildPlattform-Zeilen des gelöschten Bildes sind entfernt, nicht festgeschrieben
- in ebay_geaenderte_laufende_angebote steht eine zusätzliche, nicht festgeschriebene Zeile für das laufende Angebot des Artikels, mit nChanges = 128
Nach dem Beenden über den Task-Manager:
- tArtikelbildPlattform wieder vollständig (9 Zeilen je Bild)
- die Zeile in ebay_geaenderte_laufende_angebote ist verschwunden
- die Bilddaten in tBild sind unverändert
Es gehen also keine Daten verloren.
Nebenwirkung: halbfertige Zeilen in tBild, die später die Ameise zum Absturz bringen
Die Rückrollung ist allerdings nicht vollständig. Wird beim Hängen ein Bild hinzugefügt, bleibt dessen Zeile in tBild bestehen: Sie wird offenbar vor der Transaktion geschrieben, die sie anschließend verknüpfen und mit einem Blob-Identifier versehen würde. Nach dem Abschießen bleibt zurück:
- eine tBild-Zeile ohne jede Verknüpfung in tArtikelbildPlattform
- mit iBlobIdentifier = NULL - in unserem Bestand die einzige von 1665 Zeilen
Diese Zeile ist nicht nur Ballast, sie ist ein Stolperstein. Wird dasselbe Bild später über die Ameise importiert, prüft diese per FindByHash, ob es bereits vorhanden ist, findet die Zeile und bricht beim Deserialisieren ab:
Code:
Unbehandelte Ausnahme vom Typ System.NullReferenceException
in Void Setter_iBlobIdentifier(System.Object, System.Object)
at JTL.Domain.Base.Serialization.SerializationHandlerVirtualPropertiesObject.DeserializeInternal[T](...)
at jtlDatabase.classes.jtlDBClasses.jtlBildList.FindByHash(String cHash)
at jtlDatabase.classes.Models.Bilder.Bild..ctor(Byte[] bilddaten, String source)
at ameise.importer.Importer_Artikel_BilderPlattformen.PrüfeBildDaten(Int32 kArtikel, ArtikelbildList lArtikelBild)
CommandText = SELECT * FROM [tBild] WHERE [cHash] = @cHash
tBild.iBlobIdentifier ist im Schema als uniqueidentifier NULL angelegt, der Domänen-Layer kann den leeren Wert aber nicht lesen. Das ist unabhängig vom Hänger ein eigener Fehler: Die Ameise verträgt einen Datenzustand nicht, den die Wawi selbst erzeugt. Solange die Zeile existiert, scheitert jeder Ameise-Import dieses Bildes - an welchem Artikel auch immer.
Abgrenzung: wann es auftritt und wann nicht
An vier Artikeln desselben Mandanten geprüft, jeweils Bild hinzufügen und löschen:
| Artikel | eBay-Verknüpfung | laufendes Angebot (status = 3) | Änderungsmarke lag schon in der Warteschlange | Verhalten |
| A | keine ebay_item-Zeile | nein | - | funktioniert |
| B | ebay_item-Zeile vorhanden | nein | - | funktioniert |
| C | ebay_item-Zeile vorhanden | ja | nein | hängt |
| D | ebay_item-Zeile vorhanden | ja | ja | hängt |
Daraus folgt zweierlei: Es genügt nicht, dass überhaupt eine eBay-Verknüpfung besteht - erst ein laufendes Angebot löst es aus. Und es liegt nicht am INSERT der Änderungsmarke: Steht die Zeile bereits in der Warteschlange, hängt es genauso, dann eben an zwei konkurrierenden UPDATEs.
Entscheidend ist die Plattformaktivierung des Bildes. Am selben Artikel (Fall D) geprüft:
| Vorgang | Verhalten |
| Bild hinzufügen, eBay-Häkchen im Kasten Aktivierung für Plattform weggelassen | funktioniert |
| Bild hinzufügen mit eBay-Häkchen | hängt |
| bei einem bestehenden, eBay-aktiven Bild das eBay-Häkchen wegnehmen und speichern | hängt |
| bestehendes, eBay-aktives Bild löschen | hängt |
Ohne Zeile mit kPlattform = 30 (eBay) in tArtikelbildPlattform wird die Änderungsmarke nicht gesetzt und der zweite Schreibweg läuft gar nicht erst an. Die Bedingung lautet also genau: laufendes Angebot + Bildänderung, die eBay betrifft - und das Entfernen der eBay-Aktivierung ist selbst eine solche Änderung.
Die Aufrufliste und die Sperrkette oben stammen von Fall C. Die übrigen drei Fälle sind am Verhalten der Oberfläche geprüft.
Vorschlag
Drei Punkte, die aus unserer Sicht zusammengehören:
1. EbayGrundpreisAktualisieren sollte innerhalb von SaveArtikeldetailsAggregateAsync dieselbe Verbindung und Transaktion verwenden wie der übrige Speichervorgang - oder erst nach dem Festschreiben laufen. Zusätzlich wäre ein CommandTimeout an diesem synchronen ExecuteNonQuery sinnvoll: dann bräche der Vorgang mit einer Fehlermeldung ab, statt die Oberfläche dauerhaft stillzulegen.
2. Die tBild-Zeile sollte in derselben Transaktion entstehen wie ihre Verknüpfung, damit beim Abbruch nichts zurückbleibt.
3. Unabhängig davon sollte der Domänen-Layer iBlobIdentifier = NULL lesen können - die Spalte ist als nullable deklariert, und ein einziger solcher Datensatz legt den Bildimport der Ameise lahm.
Workaround bis dahin
Die Wawi über den Task-Manager beenden - es wird sauber zurückgerollt.
Ein Ausweichweg besteht nur beim Hinzufügen: Lässt man im Kasten Aktivierung für Plattform das Häkchen des eBay-Verkaufskanals weg, läuft das Speichern durch. Das Bild geht dann allerdings auch nicht an eBay.
Die Ameise ist nicht betroffen. Derselbe Artikel (laufendes Angebot), dasselbe Bild, dieselbe eBay-Aktivierung - als Bildimport über die Ameise eingespielt, läuft es durch. Dabei entstehen sowohl die Zeile mit kPlattform = 30 in tArtikelbildPlattform als auch die Änderungsmarke in ebay_geaenderte_laufende_angebote, und die tBild-Zeile bekommt einen iBlobIdentifier. Der Fehler steckt also nicht in der eBay-Nachführung selbst, sondern im Speicherpfad der Artikelmaske. Für das Hinzufügen und Ersetzen von Bildern ist die Ameise damit der Weg, der zurzeit funktioniert.
An bestehenden, eBay-aktiven Bildern führt über die Maske kein Weg vorbei. Auch der naheliegende Umweg, zuerst nur das eBay-Häkchen zu entfernen und danach zu löschen, hängt bereits im ersten Schritt. Solange ein Angebot läuft, sind die vorhandenen eBay-Bilder eines Artikels in 2.1.1 also gar nicht zu ändern.
Nachtrag: Wir haben den Fall zusätzlich als Supportticket eingereicht - Ticket#202609203600119 (JTL-Wawi 2.1.1, Komponente Artikel), mit denselben Belegen: Aufrufliste, Wartekette und Sperrenauszug.
Zuletzt bearbeitet: