Wawi 2.1.1: Artikelmaske hängt beim Speichern von Bildänderungen an Artikeln mit laufendem eBay-Angebot (Analyse + Workaround)

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

  • 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

  1. Artikel mit aktivem eBay-Angebot öffnen (ebay_item.status = 3)
  2. Reiter Bilder, ein Bild markieren, Löschen - die Zeile verschwindet erwartungsgemäß
  3. 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):

SitzungZustandRolle
96sleeping, open_transaction_count = 1der Speichervorgang, user_transaction offen seit 10:23:20.483
94suspended, angemeldet 10:23:20.813die 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:

ArtikeleBay-Verknüpfunglaufendes Angebot (status = 3)Änderungsmarke lag schon in der WarteschlangeVerhalten
Akeine ebay_item-Zeilenein-funktioniert
Bebay_item-Zeile vorhandennein-funktioniert
Cebay_item-Zeile vorhandenjaneinhängt
Debay_item-Zeile vorhandenjajahä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:

VorgangVerhalten
Bild hinzufügen, eBay-Häkchen im Kasten Aktivierung für Plattform weggelassenfunktioniert
Bild hinzufügen mit eBay-Häkchenhängt
bei einem bestehenden, eBay-aktiven Bild das eBay-Häkchen wegnehmen und speichernhängt
bestehendes, eBay-aktives Bild löschenhä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:

Ähnliche Themen