☰

Seite 12

5717,57 Stunden

5717,57 Stunden habe ich seit dem 16. Januar 2023, 8:58 Uhr, in meinem Tracking-Tool aufgezeichnet. Also war es mal an der Zeit, das Tool in ein öffentliches Repo zu verfrachten.

Es hat sich neben Stift und Papier als das verlässlichste Zeiterfassungswerkzeug herausgestellt. Und warum? Weil die Erfassung möglichst schmerzfrei ist. Ich kann über die Weboberfläche oder via Rasycast oder Shell mit einer Zeile ein Tracking aufnehmen. Und wenn ich den Text dann noch mit einer Youtrack-ID beginne, kann ich die erfasste Zeit als Workitem bei Youtrack über die API verbuchen. Die Erfassung der Länge erfolgt über die Differenz zum vorigen Tracking. Das heißt, ich beginne meinen Arbeitstag mit einem Tracking, das den Arbeitsbeginn festlegt, und alle anderen Trackings bauen darauf auf.

Primel

Rosa Primelblüte mit gekerbten Blütenblättern und weißem Zentrum vor unscharfem blauem Hintergrund.
Primel

Wenn du den Hintergrund als türkisblau wahrnimmst, hast du eine evtl. Farbschwäche.

Oder jemand hat bei Color Efex an der Kurbel gedreht.

MariaDBs impliziter Rollback frisst Doctrine-Savepoints

Eine Symfony-Message-Queue-Failed-Queue wächst über Tage auf tausende Einträge. Sämtliche Fehlermeldungen lauten SQLSTATE[42000] 1305: SAVEPOINT DOCTRINE_n does not exist oder There is no active transaction. Man sucht also nach einem Savepoint-Bug, nach kaputtem Transaction-Handling, nach einem Doctrine-Problem. Alles falsch: Der eigentliche Fehler ist ein InnoDB-Deadlock, und der steht in keiner einzigen Fehlermeldung.

  1. Doctrine DBAL bildet verschachtelte Transaktionen über Savepoints ab (SAVEPOINT DOCTRINE_2, DOCTRINE_3, ...). Das betrifft auch Systeme, die das Doctrine ORM gar nicht einsetzen: Shopware etwa ersetzt mit seinem eigenen DAL nur die ORM-Schicht, geschrieben wird trotzdem über die Doctrine\DBAL\Connection. Ein Repository-Write innerhalb einer äußeren Transaktion läuft dort im Savepoint.
  2. Erwischt ein Deadlock eines der Statements, rollt MariaDB/MySQL nicht nur das Statement, sondern die komplette Transaktion implizit zurück, und zwar inklusive aller Savepoints. Der Client erfährt davon nur durch den Deadlock-Error des einen Statements; die Verbindung selbst steht danach ohne Transaktion da.
  3. Doctrine weiß davon nichts und räumt regulär auf: ROLLBACK TO SAVEPOINT DOCTRINE_n. Den Savepoint gibt es nicht mehr, der Server antwortet mit 1305. Diese neue Exception ersetzt bzw. überlagert die ursprüngliche DeadlockException. Je nach Verschachtelungstiefe schlägt stattdessen später das äußere COMMIT/ROLLBACK mit There is no active transaction fehl.
  4. Im Log und in der Failed Queue landet damit nur das Aufräum-Symptom, nie die Ursache. Bei tiefer Verschachtelung kappt der Exception-Handler die Previous-Kette auch noch („Over 9 levels deep, aborting normalization“ bei Monolog), der Deadlock ist endgültig unsichtbar.

Die eigentliche Pointe ist die Folgewirkung: Retry-Mechanismen, die auf Doctrine\DBAL\Exception\RetryableException matchen, greifen nicht mehr, denn die Exception, die oben ankommt, ist eine schnöde DriverException, die nicht retrybar aussieht. Der Deadlock, der klassische Fall für „einfach nochmal versuchen“, wird durch sein eigenes Symptom vor der Retry-Logik versteckt. Shopware hat dafür in RetryableTransaction inzwischen einen Workaround, der per Regex auf SAVEPOINT ... does not exist matcht (siehe auch Doctrine-Issue dbal#6651), aber die Variante There is no active transaction rutscht auch daran vorbei.

Fix-Muster: Retry eine Ebene höher ansetzen als die Transaktion, die gesamte Exception-Kette (getPrevious) auf Deadlock-Signaturen prüfen statt nur auf Exception-Typen, und vor dem Retry die Connection hart zurücksetzen. Bei DBAL 4 reicht $connection->close(): Das setzt auch den internen Transaction-Nesting-Level zurück, reconnected wird lazy beim nächsten Query.

Debugging-Heuristik: tausende Savepoint-Fehler oder „no active transaction“ in einem Zeitfenster mit parallelen Schreibprozessen (Importe, Indexer, mehrere Queue-Worker) sind fast nie ein Transaction-Handling-Bug, sondern fast immer ein maskierter Deadlock. Die Ursache habe ich in SHOW ENGINE INNODB STATUS (LATEST DETECTED DEADLOCK) gefunden, nicht im Stacktrace.

Mein Nachmittag.

Wenn die monatliche Rasur der Grünanlage wieder ansteht, um das junge Ökosystem auf der Wiese zu zerstören, hilft mir kein Antihistamin der Welt. Ich würde mir ja die Augen auskratzen, aber vor lauter Niesen komme ich nicht dazu.

That is actually a funny idea.

Der Flow, der da ist und trotzdem nichts tut

Shopware-6-Flows, die per SQL-Migration angelegt werden (auch von Core-Migrationen!), sind zunächst funktionslos: Die payload-Spalte ist NULL, und der FlowLoader überspringt solche Flows kommentarlos. Kompiliert wird das Payload erst asynchron vom Flow-Indexer über die Message-Queue.

Auf Systemen ohne laufenden Worker (typisch: lokale Dev-Umgebungen) feuert ein Event, der Flow ist im Admin sichtbar und aktiv, es gibt null Fehlermeldungen, und trotzdem passiert nichts.

Übertragbares Muster: Migration erzeugt Daten, die erst durch asynchrone Indexierung funktional werden; der Silent-Skip macht die Diagnose fies (Debugging-Weg: log_entry-Tabelle bestätigt Event, flow.payload IS NULL verrät die Ursache).

Movie Tracker als Standalone Projekt

Ich habe mal den Movie Tracker aus der Blogsoftware herausgepfriemelt und als eigenständiges Projekt auf GitHub bereitgestellt. Eigentlich sind es nur vier Views:

Login

Movie Tracker Login
Movie Tracker Login

Der Single-User-Login wird über die .env konfiguriert.

AUTH_USERNAME=mk
AUTH_PASSWORD_HASH='<password_hash>'

Der Passwort-Hash kann mit einem mitgelieferten PHP-Skript erzeugt werden:

$ php bin/create-password-hash.php mein-passwort
In die .env übernehmen (inklusive Anführungszeichen):
AUTH_PASSWORD_HASH='$2y$12$eKomaqWXRQNHJa0pdrw9huODslOQMADsLwGCfYRqrJu76NlO2Zuxu'

Movies Übersicht

Movie Tracker Movie List
Movie Tracker Movie List

Im Listview werden die angeschauten Filme und Serien in chronologischer Reihenfolge angezeigt. Ein Klick auf einen Eintrag öffnet den Editview.


Hinzufügen/Bearbeiten

Movie Tracker Movie Edit
Movie Tracker Movie Edit

Beim Anlegen eines neuen Elements reicht es, den Titel einzugeben und auf „Von TMDB laden“ zu klicken. Soweit der Film gefunden wird, werden Darsteller, Originaltitel, Jahr und URL automatisch ergänzt.


Cast

Movie Tracker Cast
Movie Tracker Cast

Die Darstellerliste enthält den Cast, der mit einem Film gespeichert wurde.

Der schnellste Weg, um die Darsteller mit einer URL auszustatten, geht über ein kleines Skript, das die URL in der TMDb abfragt.

Aber die URLs können auch von Hand bearbeitet werden und man kann eine andere als den automatisch ergänzten Person-Link von TMDb nehmen.

$ php bin/update-movie-cast-urls.php
[1 von 11] Brad Pitt: https://www.themoviedb.org/person/287
[2 von 11] Charlotte Gainsbourg: https://www.themoviedb.org/person/4273
[3 von 11] Jessie T. Usher: https://www.themoviedb.org/person/198847
[4 von 11] LaKeith Stanfield: https://www.themoviedb.org/person/1200864
[5 von 11] Maika Monroe: https://www.themoviedb.org/person/1094091
[6 von 11] Mark Wahlberg: https://www.themoviedb.org/person/13240
[7 von 11] Rosa Salazar: https://www.themoviedb.org/person/973667
[8 von 11] Thomas Jane: https://www.themoviedb.org/person/11155
[9 von 11] Travis Tope: https://www.themoviedb.org/person/1278725
[10 von 11] William Fichtner: https://www.themoviedb.org/person/886
[11 von 11] 杨颖: https://www.themoviedb.org/person/150899
Fertig. 11 von 11 aktualisiert.

Nun bin ich doch wieder bei Disney+ gelandet, nur um mir passend zum 4. Juli mal wieder Independence Day anzuschauen.
Aber im Laufe des Jahres werden ja auch noch Grogu und der Mandalorianer vorbeischauen.

Tillandsia Usneoides

Nahaufnahme der fädigen, korkenzieherartig gewundenen Triebe von Tillandsia usneoides mit silbrig-grauer Behaarung vor unscharfem grünem Hintergrund.
Tillandsia Usneoides

Makroaufnahme von Tillandsia usneoides, dem Louisianamoos. Die dünnen, korkenzieherartig gewundenen Triebe mit dem silbrig-grauen Schimmer sind typisch für diese wurzellose Hängepflanze.

Der graue Flaum sind die Saugschuppen (Trichome), über die die Pflanze Wasser und Nährstoffe direkt aus der Luft aufnimmt. Wurzeln braucht sie ja praktisch keine. Die feine Behaarung ist an den schärfsten Trieben gut zu erkennen.

Das Foto habe ich in einem der Gewächshäuser im Botanischen Garten der RUB aufgenommen. Louisianamoos, wie der Name schon vermuten lässt, hängt in unserer Region kaum einfach so herum.

Die Flickr-App wurde überarbeitet

Mein kleines iPad ist mit einer neuen Version der Flickr-App beseelt worden. Und auch wenn die App auf den ersten Blick recht fesch aussieht, ist das kein Grund zur Freude, wie sich zeigte, als ich auf die Idee kam, das Pad in den Landscape-Modus zu drehen. Auf dem iPhone ist der Landscape-Modus unterbunden, auf dem iPad nicht. Vermutlich, um für Split View, Slide Over und Stage Manager Flexibilität zu bieten.

Beim Landscape-Modus hätte man sich echt mehr Mühe geben müssen. Bei einer App zum Ansehen von Fotos könnte man damit rechnen, dass sie auch mal auf einem iPad benutzt wird. Es ist ja nicht jeder durch Instagram geprägt und steht drauf, sich thumbnailgroße Bildchen anzuschauen. Eine offensichtliche und üble Regression.

Gestern habe ich Infinite geschaut. Witzige Idee. Aber die Story kam dann leider etwas kurz, aber dafür wurde wieder unglaublich viel rumgewahlbergt.

Es wird mal wieder Zeit für ein Foto aus Venedig.

Ein mit Kisten voller Obst und Gemüse beladenes Boot liegt unter einer weißen Plane an einem venezianischen Kanal, daneben mehrere vertäute Motorboote und alte Wohnhäuser.
Bootsverkauf

Ein kleines Boot als Verkaufsfläche. Es ist sogar auf OpenStreetMap als Obst- und Gemüsehändler eingetragen. Vielleicht habe ich den Händler auch nicht zum ersten Mal fotografiert.

Ich mag diese Ecke von Venedig. Der Stadtteil Castello ist weit genug vom Piazzale Roma entfernt, sodass die Tagestouristen sich bis hierher etwas verlaufen haben.

Rechts sieht man die breite Via Giuseppe Garibaldi. Eine richtig gediegene Einkaufsstraße mit zwei kleinen Supermärkten, Apotheken und natürlich einigen Kneipen. Wenn ich mein Hotel nicht gerade auf der anderen Seite der Stadt habe, mag ich es, hier einzukaufen.

Wie lebensnah ;)

Soll ich nun schockiert oder amüsiert sein, wenn ich sehe, wie weltfremd Teile des Reformpakets nun sind.

Attest vom ersten Krankheitstag an. Klingt auf den ersten Blick nicht sehr arbeitgeberorientiert. OK, ich bin noch nie für nur einen Tag krankgeschrieben worden. Bei regelmäßigen Migräneanfällen kommt es schon mal vor, dass ich nur einen Tag ausfalle. Aber wenn ich dann gequält wegen Migräne das Wartezimmer mit anderen Patienten verstopfe, die vergleichbare Leiden haben, kann ich mir dann zumindest sicher sein, dass ich den Rest der Woche mit beruhigtem Gewissen und einem Attest genesen kann.

Wie bitte kann das für einen noch niedrigeren Krankenstand oder eine Entlastung von Praxen taugen.

Naja, erst mal einen Tee… 

Force mailpit (DDEV Hooks)

Falls ich mal eine natürlich DSGVO-konform anonymisierte Shopware‑DB in ein DDEV‑Projekt importieren und sicher gehen möchte, dass ausgehende Mails in Mailpit landen und nicht in die freie Welt entlassen werden, habe ich mir zwei Hooks angelegt.

.ddev/config.yaml

hooks:
  post-start:
    - exec-host: .ddev/mail/force-mailpit.sh
  post-import-db:
    - exec-host: .ddev/mail/force-mailpit.sh

Würde das folgende Skript beim Start und nach dem Import-DB-Kommando aufrufen.

.ddev/mail/force-mailpit.sh

set -euo pipefail
ddev mysql db <<'SQL'
UPDATE system_config
SET configuration_value = '{"_value":""}'
WHERE configuration_key = 'core.mailerSettings.emailAgent';
SQL

Man kann überhaupt die witzigsten und praktischsten Sachen mit Hooks machen.