Shopware 6.7.6.2: Mengenfeld zeigt falschen Bestand – „Cache aktualisieren“ ohne Wirkung

Hallo zusammen,

ich habe meinen Shop auf Shopware 6.7.6.2 aktualisiert (von Version 6.6.10.9) und stehe vor einem massiven Problem bei der Bestandsanzeige von Abverkauf-Artikeln.

Die Situation: Ein Artikel ist als „Abverkauf“ markiert, Bestand ist 3 Stück. Ein Kunde kauft 1 Stück, der reale Bestand sinkt korrekt auf 2.

Das Problem: Im Frontend bleibt im Mengenfeld (Dropdown/Zahlenauswahl) weiterhin die 3 anwählbar. Das passiert browserübergreifend und auch im Inkognito-Modus. Erst im Warenkorb erhält der Kunde die Fehlermeldung, dass nur noch 2 Stück verfügbar sind. Das führt zu einer extrem schlechten User Experience.

Das Kuriose: Selbst wenn ich unter Einstellungen > Caches & Indizes manuell auf „Cache aktualisieren“ klicke, ändert sich im Frontend nichts. Die „3“ bleibt im Mengenfeld stehen. Der Klick auf den Button scheint den HTTP-Cache oder die gerenderte Produktseite für das Mengenfeld überhaupt nicht zu tangieren.

Technische Details:

  • Messenger/Worker: Die Tabelle messenger_messages ist leer, alle Hintergrundaufgaben werden sofort abgearbeitet.
  • Shopware.yaml: Hier steht aktuell cache: invalidation: http_cache: []. Mir ist bewusst, dass dies die automatische Invalidierung beeinflusst, aber es erklärt nicht, warum selbst der manuelle Button im Admin keinerlei Auswirkung auf die Bestandsanzeige hat.
  • Texte/Andere Änderungen: Wenn ich Texte ändere und den Cache leere, werden diese übernommen. Nur die verfügbare Menge im Auswahlfeld scheint „festgefroren“.

Hat jemand eine Lösung für dieses Verhalten? Es scheint, als hätte Shopware ab Version 6.7 das Caching des Mengenfeldes so aggressiv entkoppelt, dass man als Händler keine Kontrolle mehr über die Echtzeit-Anzeige hat.

Gibt es eine Konfiguration oder einen Workaround, um das Mengenfeld dazu zu zwingen, den tatsächlichen Lagerbestand (available_stock) anzuzeigen?

Vielen Dank für eure Unterstützung!

Ist die Verzögerung des Caches nicht nur ungefähr 2 Minuten in der Standard-Konfiguration? Ist diese Zeitspanne zu lang? Dann müsstest du entweder schauen, ob man den Cache für teilweise deaktivieren kann (selbst noch nicht mit dem „neuen“ Cache beschäftigt) oder für das Input Field den Max Wert manuell per XHR nachladen.

2 Minuten wäre überhaupt kein Problem - das Problem ist bei mir, dass das Mengen-Eingabefeld auch 3 Stunden später noch 3 Stück anwählbar machte, obwohl „Cache aktualisieren“ bereits gedrückt und Backend-Bestand auf 2 war. Auch Browser-Cache leeren oder anderen Browser verwenden brachte auch nach Stunden noch die falsche Stückzahl im Mengeneingabefeld bei Abverkauf-Artikeln. Das war früher bei 6.6.x.x nicht so.

Ich habe es gerade lokal getestet und der Bestand wurde innerhalb „einer Sekunde“ korrigiert.

SHOPWARE_HTTP_CACHE_ENABLED=1
OpCache
APCu

Moin @Andreas_147 ,

hast du denn mal geprüft, ob es wirklich ein Cache Problem ist oder ein Index Problem? Also hat sich der Wert in der Datenbank selbst korrekt verändert?
Die Message Queue wird nicht über Redis verwaltet? Weil dann wäre die Tabelle messenger_messages ohnehin immer leer, da es dort nicht mehr reingeschrieben wird.

Grüße
Matthias

Hallo zusammen,

ich hab mir gerade alles nochmal angesehen. Cache ist auf Enabled. Testbestellung gemacht - Artikelbestand vor Bestellung = 3. Nach Bestellung im Backend und in der Datenbank korrekt auf = 2. Dann 15 Minuten später erstmal „Cache aktualisieren“ und dann erneut mit anderem Browser die Seite besucht, das „+“ für die Menge konnte ich 3x klicken. Also hier Bestand wieder falsch.

Untersuchen von „form-control js-quantity-selector quantity-selector-group-input“ brachte auch folgendes:

input type=„number“ name=„lineItems[0194acd6d59b731887494a810d0590e3][quantity]“ class=„form-control js-quantity-selector quantity-selector-group-input“ min=„1“ max=„3“ step=„1“ value=„1“ aria-label=„Anzahl“

Also auch hier ist max=3 immernoch hinterlegt. Woran könnte das liegen?

Ich habt das Stock-Handling deaktiviert.

Wenn ich die Lagerbestandsverwaltung von Shopware deaktiviere, dann funktioniert das mit den Aberkaufs-Artikeln doch nicht mehr, oder? Denn dann verwendet er den Bestand beim Produkt ja nicht mehr?

Ich konnte das Problem genauer eingrenzen und auch in zwei öffentlich zugänglichen Demo-Shops 1:1 wie bei mir nachstellen.

Das Problem: Nach einer Bestellung wird der verringerte Lagerbestand beim Abverkaufs-Artikel für den Käufer (Session-Teilnehmer) korrekt angezeigt. Für anonyme Neubesucher wird jedoch weiterhin die veraltete Version der Produktdetailseite aus dem HTTP-Cache geladen. Das führt dazu, dass Kunden Mengen in den Warenkorb legen können, die physisch nicht mehr verfügbar sind. Die Bestandsprüfung greift erst verzögert im Warenkorb, statt direkt das Mengenfeld auf der Detailseite zu begrenzen (wie es ja noch bei Version 6.6.x.x. der Fall war…)

Reproduzierbare Testreihe: Ich habe dieses Verhalten in zwei unterschiedlichen Demo-Umgebungen unabhängig voneinander reproduziert:

Test-Ablauf:

  1. Artikel auf „Abverkauf“ setzen, Bestand auf 3 Stück.
  2. Bestellung von 2 Stück durchführen.
  3. Ergebnis Browser A (Käufer): Bestand ist aktuell (1 Stück wählbar) [= wenn man z.B. nach der Bestellung als Käufer gleich in den Shop zurückgeht und den gekauften Artikel erneut aufruft].
  4. Ergebnis Browser B (Inkognito/Neubesucher): Es werden weiterhin 3 Stück als wählbar angezeigt

Der Cache der Produktseite für Neubesucher wird durch den Kauf offensichtlich nicht automatisch invalidiert. Weder das Warten noch der Button „Indizes aktualisieren“ im Admin lösen das Problem. Erst ein Klick auf „Caches leeren“ im Admin-Bereich macht den korrekten Bestand für alle Besucher sichtbar.

In der .env habe ich bei TTL für den Cache den Wert mal auf 300 geändert → und siehe da, nach 5 Minuten war der Bestand auch für Neubesucher korrekt auf der Seite auswählbar.

Es scheint, als würde die Cache-Invalidierung beim Event checkout_order_placed in der Version 6.7 standardmäßig nicht (oder nicht mehr zuverlässig) greifen.

Hat jemand eine Idee, wie man diese automatische Invalidierung nach dem Kauf wieder erzwingt, ohne die Systemstabilität zu gefährden? Etwas in der shopware-yaml eintragen?

1 „Gefällt mir“

Nach 5 Minuten sollte der Cache eigentlich von selbst aktualisiert werden, siehe Release notes Shopware 6.7.0.0 | Shopware Documentation

Ich frage mal auf Discord nach und lege sonst mit der Beschreibung ein GitHub Issue an.

1 „Gefällt mir“

Danke, bin gespannt was herauskommt.

Langsam frustiere ich an dem Problem!

Ich habe in den letzten Tagen intensiv weiter experimentiert, um das Problem mit dem veralteten Lagerbestand für Neubesucher in den Griff zu bekommen. Hier ist mein aktueller Stand und die Dokumentation der Sackgassen, in die ich gelaufen bin.

Schritt 1: Die Konfiguration (z-shopware.yaml) Zuerst habe ich versucht, das Problem über die Standard-Konfiguration zu lösen. Meine z-shopware.yaml sieht aktuell so aus:

shopware:
    auto_update:
        # Disables the auto updater in the UI
        # enabled: false
    admin_worker:
        # The Admin worker should be disabled on production server.
        # enable_admin_worker: false
    cache:
        invalidation:
            http_cache: []
            delay_enabled: false

Das Ergebnis:

  • Wenn ich im Backend ein Produkt öffne, etwas ändere (z.B. „Test“ in ein Costum-Field schreibe oder den Preis um 1 ct erhöhe) und manuell speichere, ist die Änderung für alle sofort im Frontend sichtbar - auch ohne „Cache aktualisieren“ klicken zu müssen (klar, weil delay_enabled: false).
  • Aber: Wenn eine Bestellung den Bestand in der DB mindert, passiert für Neubesucher absolut nichts. Die Seite bleibt im Cache, bis die TTL abläuft.

Schritt 2: Versuchsreihe mit einem Custom-Plugin: Da die YAML-Einstellung allein nicht reichte, hat mir jemand ein Plugin gebaut, das auf CheckoutOrderPlacedEvent reagiert, um die Invalidation künstlich anzustoßen. Hier unsere Versuche von „sanft“ bis „Brechstange“:

  • Versuch 1 (Erfolglos): Wir haben den CacheInvalidator genutzt und gezielt die Tags product-{id} und product-detail-route-{id} abgeschossen.
  • Versuch 2 (Erfolglos): Wir haben per Plugin das product.repository genutzt, um das updatedAt-Feld zu setzen. Die Hoffnung: Shopware erkennt das EntityWrittenEvent und löscht den Cache wie beim Admin-Save.
    • Ergebnis: In der Datenbank wird zwar die Zeit aktualisiert, zu der das Produkt „geändert“ wurde (Kaufzeitpunkt), der Cache für Neubesucher ignoriert das jedoch komplett, Mengenfeld bleibt falsch
  • Versuch 3 (Deep Update / Erfolglos): Wir haben versucht, ein inhaltsrelevantes Feld zu triggern. Das Plugin hat via DAL das Feld markAsTopseller von 0 auf 1 gesetzt, gespeichert und sofort wieder auf 0 gesetzt.
    • Ergebnis: Obwohl das für Shopware wie eine echte Inhaltsänderung aussehen müsste (die im Admin ja funktioniert, wenn ich „Produkt herorheben“ markiere & dann speichern klicke), bleibt die Storefront für Neubesucher auf dem alten Stand.

Code zum letzten Versuch:

<?php declare(strict_types=1);

namespace StockHttpCacheInvalidation\Subscriber;

use Shopware\Core\Checkout\Order\Event\CheckoutOrderPlacedEvent;
use Shopware\Core\Framework\Adapter\Cache\CacheInvalidator;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class OrderSubscriber implements EventSubscriberInterface
{
    private CacheInvalidator $cacheInvalidator;
    private EntityRepository $productRepository;

    public function __construct(
        CacheInvalidator $cacheInvalidator,
        EntityRepository $productRepository
    ) {
        $this->cacheInvalidator = $cacheInvalidator;
        $this->productRepository = $productRepository;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            CheckoutOrderPlacedEvent::class => ['onCheckoutOrderPlaced', -100],
        ];
    }

    public function onCheckoutOrderPlaced(CheckoutOrderPlacedEvent $event): void
    {
        $order = $event->getOrder();
        if (!$order || !$order->getLineItems()) {
            return;
        }

        $context = Context::createDefaultContext();

        foreach ($order->getLineItems() as $lineItem) {
            if ($lineItem->getType() !== 'product') {
                continue;
            }

            $productId = $lineItem->getReferencedId() ?: $lineItem->getIdentifier();
            if (!$productId) {
                continue;
            }

            // 1. Aktuellen Status holen
            $criteria = new Criteria([$productId]);
            $product = $this->productRepository->search($criteria, $context)->first();

            if ($product) {
                $originalStatus = $product->getMarkAsTopseller();
                
                // 2. Erster Schreibvorgang: Wert umkehren (0 -> 1 oder 1 -> 0)
                // Das zwingt Shopware, den Cache für "Inhaltsänderung" vorzubereiten
                $this->productRepository->update([
                    [
                        'id' => $productId,
                        'markAsTopseller' => !$originalStatus,
                        'updatedAt' => new \DateTime(),
                    ]
                ], $context);

                // 3. Zweiter Schreibvorgang: Original-Wert wiederherstellen
                // Jetzt wird der Cache endgültig gelöscht und der Ur-Zustand ist wieder da.
                $this->productRepository->update([
                    [
                        'id' => $productId,
                        'markAsTopseller' => $originalStatus,
                    ]
                ], $context);
                
                // 4. Sicherheitshalber die Tags löschen
                $this->cacheInvalidator->invalidate([
                    'product-' . $productId,
                    'product-detail-route-' . $productId
                ]);
            }
        }
    }
}
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="StockHttpCacheInvalidation\Subscriber\OrderSubscriber">
            <argument type="service" id="Shopware\Core\Framework\Adapter\Cache\CacheInvalidator"/>
            <argument type="service" id="product.repository"/>
            <tag name="kernel.event_subscriber" priority="-100"/>
        </service>
    </services>
</container>

Das große Rätsel: Warum löst ein Klick auf „Speichern“ in der Administration die Invalidation für den Cache aus, während derselbe Vorgang über das o.g. Plugin nichts bewirkt?

Hat jemand eine Idee, welchen speziellen Befehl oder welches Event der Admin-Bereich nutzt, das uns im Frontend-Prozess fehlt?

Es kann doch nicht sein, das ich Kunden jetzt vor dem Warenkorb nicht mehr den korrekten Bestand meiner Abverkaufs-Artikeln im Mengenfeld wählen lassen kann, es sei denn, ich schalte die TTL vom Cache auf 0 oder sowas wie 120 oder ich aktualisiere das nach jedem Kauf manuell indem ich was am Produkt im Backend ändere - aber das ist doch keine Lösung.

Ich habe das eben mit 6.7.7.0 noch einmal getestet.

In Browser Tab 1 Bestellung aufgegeben.
In Browser Tab 2 (Private Browsing) verfügbare Menge sofort aktualisiert.

Das wird vermutlich ein individuelles/Server-Problem sein.

Ich kann dir gerne per privater Nachricht eine URL zum Testshop schicken, in dem es tut.

Weißt du, ob in 6.7.7.0 hier evtl. schon irgendwas geändert wurde? Meine Versuche mit den Demoshops von shop-studio und marcel-krippendorf waren leider ebenfalls so, wie bei mir im Shop. Und vorher nie ein Problem. Soll ich das evtl. mal auf Github posten?

Ich sehe hier nichts: shopware/RELEASE_INFO-6.7.md at v6.7.7.0 · shopware/shopware · GitHub

Du kannst dir hier schnell eine Demo-Version installieren und das unabhängig testen, bevor du auf GitHub etwas schreibst, was ich nicht reproduzieren kann:

https://hub.shopware.com/build

Ok, ich hab mir unter dem von dir genannten Link schnell einen Demoshop erstellt, ein Produkt mit Abverkauf und Menge auf drei angelegt.

  1. Firefox (147.0.2) privates Fenster geöffnet
  2. Frontend von dem Demoshop geöffnet
  3. Produkt 1 Stück in den Warenkorb und dann Checkout durchgeführt (also einmal bestellt)
  4. Neues Privates Fenster in Opera geöffnet (126.0.5750.59)
  5. Demoshop geöffnet
  6. Produkt angeklickt → Mengenfeld ist korrekt auf „2“ begrenzt - passt
  7. Firefox privates Fenster geschlossen
  8. Firefox neues privates Fenster, Demoshop öffnen, Artikel nochmal 1 Stück bestellt (Mengenfeld hier auch korrekt auf „2“ begrenzt)
  9. Rüber ins noch offene Opera-Fenster. F5 gedrückt. Mengenfeld zeigt weiterhin „2“ Stück zur Auswahl
  10. Im Backend vom Demo-Shop auf „Refresh Cache“ und dann zurück zum Opera-Browser und wieder F5 = Mengenfeld bleibt auf 2 Stück.
  11. Opera geschlossen. Neues privates Fenster im Opera geöffnet, Demoshop aufgerufen, Produkt geöffnet, Mengenfeld lässt „2“ auswählen, obwohl im Backend Bestand korrekt auf „1“
  12. Ganz neues Fenster im Edge geöffnet (war bisher zu), in-private-Fenster, Demoshop aufgerufen, Produkt angeklickt, Mengenfeld = 2. Nochmal Cache-Refresh und wieder F5. Menge bleibt falsch auf 2.
  13. Ins Produkt vom Demoshop, in der Beschreibung als Text einfach „2“ reingeschrieben, gespeichert, Alle Browser F5 und Mengenfeld ist korrekt auf 1.

Danke übrigens für den Link, das kannte ich bisher nicht :+1: Konnte aber da mein Problem auch 1:1 reproduzieren. Der Demo-Shop, welcher hier installiert wird, ist übrigens 6.7.3.1.

1 „Gefällt mir“

Ich habe das mit Safari und Chrome nachgestellt und kann es nicht rekonstruieren.

Lediglich an Punkt 9 war kurzzeitig der falsche Wert noch vorhanden. Das liegt aber daran, dass Shopware hier auf einen Cache zurückgreift.

Ich bin mir nicht sicher, aber ich gehe stark davon aus das Private Browsing (oder wie auch immer genannt) teils eine Art „Browser Instanz Session“ ist, die erst geleert/gelöscht wird, wenn nicht das Tab, sondern der komplette Browser geschlossen wird (nicht nur über X, sondern richtig beendet). Ansonsten wirst du ggf. wiederum Dateien aus dem Browser-Cache erhalten.

Wenn du der Meinung bist, dass es dennoch ein Fehler ist, dann erstelle gerne auf GitHub ein Issue.

Danke fürs probieren – warum das bei dir immer funktioniert, aber bei mir nicht, ist mir weiterhin ein Rätsel. Gestern habe ich zuerst mal das Update auf 6.7.7.1 durchgeführt.

Ich habe auf deine Anregung hin mit dem Browser-Cache als mögliche Fehlerquelle nochmal ein anderes Testszenario durchgeführt.

  1. Testbestellung am PC im Browser (Browserdaten natürlich vorher gelöscht), 1 Stück von Abverkaufs-Artikel
  2. Smartphone (Samsung Android, Browser Chrome) benutzt, die Shopseite mit dem Abverkaufs-Artikel geladen, den Mengen-Button betätigt = falsche maximal wählbare Menge. Anwählbar sind 3 Stück, obwohl nur 2 noch verfügbar (in der Datenbank und im Backend ist die Menge korrekt).
  3. iPad (iOS 26) benutzt, mit Safari die Shopseite mit dem Abverkaufs-Artikel geladen, den Mengen-Button betätigt = falsche maximalmenge. Anwählbar sind 3 Stück, obwohl nur 2 noch verfügbar.

Also Fehler im eigenen Shop immer noch da und nachstellbar. Dann das ganze nochmal mit dem o.g. Testshop durchgeführt (in diesem nichts geändert, Standardinstallation wie über das Portal ohne Plug-Ins, nur der eine von mir angelegte Abverkaufs-Artikel ist in diesem Shop).

Das Ergebnis war der gleiche Fehler = falsche Menge vom Mengenfeld wie bei meinem eigenen Shop, in allen drei Geräten (PC, Android-Smartphone, iPad).

Mir hat zwischenzeitlich jemand versucht zu helfen, der deutlich mehr davon versteht als ich. Was aufgefallen ist, ist, das in der „CacheInvalidationSubscriber.php“ zwar Tags für Kategorien (Listing), Varianten-Logik, Product Streams generiert werden, aber es gibt keine Logik, die auf Änderungen des Lagerbestands (product.stock oder product.available_stock) reagiert. Shopware invalidiert den Cache, wenn ich Namen oder die Beschreibung ändere, aber nicht, wenn ein Kunde etwas kauft und sich nur der Bestand ändert.

Auch das Problem, warum F12 bei der Netzwerkanalyse trotz sichtbarer Änderung einen fortlaufenden Wert für „Age“ anzeigt, konnte aus der CacheStore.php abgeleitet werden.

Mir wurde es so erklärt: In der Methode lookup gibt es einen Mechanismus namens Soft Purge. Wenn ein Cache-Eintrag durch eine Änderung (z. B. Textänderung) als „ungültig“ markiert wird, löscht Shopware die Datei nicht. Stattdessen erkennt Shopware beim nächsten Aufruf: „Oh, die Datei ist alt, aber ich habe soft_purge aktiv“. Shopware liefert die alte Datei aus dem Cache aus, schickt aber im Hintergrund eine Nachricht (RefreshHttpCacheMessage) an den Server, um die Datei im Hintergrund zu aktualisieren. Der Age-Header bezieht sich somit wohl auf das ursprüngliche Erstellungsdatum der Datei, auch wenn der Inhalt im Hintergrund bereits ausgetauscht wurde.

Shopware „flickt“ quasi nun den Cache, anstatt ihn bei einer solchen Änderung frisch beim Seitenaufruf komplett neu zu erstellen (wie früher).

In der „CacheStore.php“ gibt es dann wohl eine Methode „getMinInvalidation(array $tags)“. Shopware prüft hier Zeitstempel für Tags nach dem Muster: http_invalidation_{tag}_timestamp.

Das Problem hier scheint: Wenn ich einen Text im Produkt ändere, feuert Shopware ein Event, das diesen Zeitstempel für das Produkt (product-{id}) hochsetzt. Bei einer Bestandsänderung (durch eine Bestellung) feuert Shopware dieses Event standardmäßig nicht, um den Server vor Lastspitzen zu schützen. Da der Zeitstempel sich nicht ändert, denkt der Cache: „Alles super, der Bestand im Cache ist noch aktuell“.

Dann fanden wir noch die „HttpCacheKeyGenerator.php“. Hier wird der Cache-Key generiert. Shopware nutzt das wohl für die URL, einen globalen cacheHash und Cookies (Währung, etc.)

Leider ist aber der Lagerbestand kein Teil des Cache-Keys. Das bedeutet, egal ob der Bestand 5 oder 0 ist, der Key bleibt http-cache-xyz. Ohne einen expliziten „Invalidate“-Befehl (der beim Bestand fehlt), gibt es für das System keinen Grund, das Dokument neu zu generieren.

Da das Problem bei allen Versionen vor 6.7.x.x von Shopware nicht auftrat, ist das für mich ein Bug. Bei einem Abverkaufs-Artikel sollte über die Cache-Generierung im Hintergrund auch die variable {{ product.calculatedMaxPurchase }} neu in den Cache geschrieben werden, damit der Kunde dann den Button der buy-widget-form.html.twig nur in der richtigen Menge betätigen kann - oder die Seite halt wie bei Änderungen am Text im Backend neu für den Cache generiert werden.

Ich werde jetzt mal versuchen, das für Github für ein Issue zusammenzufassen.