Serendipity-Entwicklungswoche, Tag 3: Cache und XMLRPC
Monday, 11. February 2019
Heute wird es etwas kürzer als gestern, jedoch konnte ich die Entwicklungswoche fortführen.
XMLRPC und PHP 7.2
Bernd hatte schon eine Weile den deprecated Konstruktor von XMLRPC gemeldet.
Ich bin mir nicht 100% sicher warum ich die Fehlermeldung nicht gesehen habe. Meine Vermutung: Die Library wird nur eingebunden, wenn ein Pingback gesendet oder empfangen wird, also sieht man die Meldung nur als Fehlermeldung wenn man einen Pingback verschickt. Und das passiert nur wenn der Trackback scheiterte, was nur selten vorkommt.
Wie auch immer, ich habe den Konstruktor jetzt einfach wie von modernen PHP-Versionen gefordert auf __construct
umgestellt.
Cache
Danke für die Kommentare zum Artikel gestern. Der Cache des Entryproperties-Plugins ist nun standardmäßig aus und folgt damit der üblichen, von euch definierten Praxis. Dafür wird der interne Cache bei der Installation nun aktiviert.
Das VGWORT-Plugin ist auf meinem Server nun in der aktuellen Version, und trotzdem wird anders als im Testblog die Zeichenanzahl in der Eintragsliste nicht angezeigt. Stellt sich raus, dass ich dafür einen neuen Event backend_view_entry eingeführt hatte der schlicht noch nicht in der hier laufenden Serendipity-Version 2.1.4 drin ist. Am Ende der Woche werde ich diesen Blog aktualisieren.
Serendipity-Entwicklungswoche, Tag 2: Fertige Mediendatenbank, Trackbacks und etwas VGWORT
Sunday, 10. February 2019
Nach der Vorarbeit gestern war deutlich, dass Serendipitys Alpha im Grunde gut funktioniert. Also widmete ich meine Zeit am zweiten Tag der Entwicklungswoche größtenteils der Fertigstellung meiner Überarbeitung.
Die überarbeitete Mediendatenbank
Was meine ich mit "Überarbeitung der Mediendatenbank" überhaupt, da hat sich doch gar nichts geändert? Wenn dieser Eindruck entsteht ist die Überarbeitung gelungen. Ich habe tatsächlich nur Interna geändert, den Code neugeschrieben und organisiert, insbesondere wenn Bilder umbenannt oder in andere Ordner geschoben werden.
Anlass war das Plugin für responsive Bilder. Dafür musste ich in den Code der Mediendatenbank (=ML, für Media Library) hinabsteigen und die Thumbnailerstellung anpassen. Denn das Plugin braucht ja jetzt mehrere Thumbnails, nicht mehr nur eines, um von denen immer das für die Fenstergröße passende auszuwählen. Diese Thumbnails müssen dann auch mit verschoben und umbenannt werden, wenn gleiches mit dem Originalbild passiert. Ohne Serendipity und die Arbeit meiner Vor- und Mitentwickler schlechtreden zu wollen: ich war ein bisschen entsetzt was ich dabei im Code sah.
Beispielsweise war das Herzstück bei Umbenennungen vorher, hier zu sehen, die Funktion serendipity_moveMediaDirectory
. Wohlgemerkt auch dann, wenn eine einzelne Datei und kein Ordner verschoben wird. Was gemeint war steuerte der Parameter $type
, der entweder 'dir'
, 'file'
oder 'filedir'
erwartete. Ist klar. Die Funktion war dann auch noch 403 Zeilen lang und daher absolut nicht einfach zu beherrschen.
Um also die responsiven Bilder unterstützen zu können musste ich das neu organisieren, mindestens entstand der Wunsch diese Stelle im Kern zu verbessern. Ich splittete die Funktionalität in mehrere Teilfunktionen: serendipity_renameDir
, serendipity_renameFile
, serendipity_updateImageInDatabase
, serendipity_updateImageInEntries
, ..., ich denk die Idee wird klar. Diese Funktionen sind jeweils wesentlich kürzer als der alte Blob und sie können in anderen Codestellen aufgerufen werden, wenn eben genau ihre Funktionalität gebraucht wird. Ich würde das gerne anschaulicher zeigen, aber die functions_images.inc.php ist leider zu lang für githistory.xyz.
Ich wurde aber nicht fertig: Erst fehlte die Zugangskontrolle über die ACLs, die ich nie nutze, nicht kannte und deren Fehlen auch nicht bemerkte. Garvin besserte hier nach. Dann vergaß ich schlicht, dass ja nicht nur der Pfad zum Bild umgebogen werden muss, sondern auch der Link. Das reparierte ich vor einer Weile. Was aber bis heute noch fehlte: Wenn ein eingefügtes Bild nicht zu sich, sondern sonstwohin linkt, soll der manuelle Link bei Umbenennung des Bildes ja erhalten bleiben. Genau diese Funktion baute ich heute ein.
Solch ein tiefgreifendes Refactoring ohne bestehende Unit-Tests ist riskant und erfordert sehr viel Aufmerksamkeit. Mittlerweile scheint der neue Code sehr stabil zu funktionieren. Er ermöglicht die neue Funktionalität, macht wohl nichts kaputt, und er wird wesentlich einfacher zu erweitern sein. Aber genau hier ist die Instabilität im Master, der Grund warum wir nicht einfach eine neue Beta oder stabile Serendipity-Version veröffentlichen konnten. Ab heute ginge das hoffentlich.
Trackbacks funktionieren(?)
In meinem Blog beobachte ich das schon etwas länger, aber jetzt las ich es indirekt auch von Thomas und einem Kommentator bei ihm: Irgendwas stimmt nicht mit den Trackbacks. Verschickt Serendipity für einen Artikel mehrere Trackbacks an einen anderen Blog – oder auch an andere Artikel im eigenen Blog – dann kommt nur einer davon durch.
Ich vermutete den Fehler im Kern, testete das in meinem Entwicklungsblog – und alles funktionierte. Ich hatte einen neuen Artikel geschrieben, der zu zwei älteren Artikeln verlinkt, und unter beide wurde ein Trackback gesetzt. Das ist also höchstwahrscheinlich kein Fehler im Kern, sondern ich vermute das Problem beim Server oder einem Plugin. So oder so, auch wenn es sich lohnen würde hier weiter zu forschen, wenn das in einer jungfräulichen Installation funktioniert blockiert es das Release nicht.
VGWORT-Plugin geht erstmal wieder
Im Testblog wurde nach der Pluginstallation sofort eine Fehlermeldung geworfen. Mit einer kleinen Änderung konnte ich die vermeiden und der Zeichenzähler funktionierte danach. Ich bin gespannt, ob das morgen nach dem Pluginupdate auch hier im Blog geht, gehe jetzt aber erstmal von aus.
Eine Frage in die Runde: Benutzt jeder von euch das Plugin Erweiterte Eigenschaften von Artikeln (entryproperties), mit dem dort enthaltenen Cache?
Beim Entwickeln wurde nach dem Umbenennen von Bildern dieser Cache nicht invalidiert, ich dachte erst der Code der ML sei kaputt, aber das lag rein am Cache dieses Plugins. Ich überlege ob man ihn deaktivieren sollte und stattdessen den im Kern integrierten Cache aktiviert. Der ist sowieso mächtiger, und es sollte einfacher sein ihn wenn nötig zu invalidieren. Meinungen?
Serendipity-Entwicklungswoche, Tag 1: Installation, PHP 7.2 und Responsive Images
Saturday, 9. February 2019
Das passierte bisher am ersten Tag der Entwicklungswoche:
Installation
Die Installation lief problemlos. Ich habe mein Dev-Installation auf uberspace komplett gelöscht, nochmal kontrolliert dass PHP 7.2 aktiv ist, und dann den aktuellen Git-Master heruntergeladen und neu installiert. Nach interner Versionierung ist das die Serendipity 2.2.1-alpha2.
Weil SQLite eigentlich immer funktioniert habe ich die Installation nur mit MySQL getestet. Es fielen mir keine Besonderheiten auf, ich landete direkt im Blog. Der von hannob gemeldete Installationsfehler ist also wirklich repariert.
PHP 7.2
Im Backend und auch sonst sind mir keinerlei Probleme bezüglich PHP 7.2 angezeigt worden. Ich habe Artikel geschrieben und bearbeitet, die Medienlibrary genutzt, Plugins installiert und das Autologin-Cookie aktiviert. Ich bezweifel nicht, dass es mit PHP 7.2 noch Probleme geben kann, aber die Kernfunktionalität scheint zu stehen. Vielleicht stolpere ich noch über Fehler, ansonsten freue ich mich über konkrete Hinweise. Vielleicht verschluckt uberspace auch ein paar Fehlermeldungen, die anderswo angezeigt werden?
Responsive Images
Ansonsten habe ich mich mit dem Plugin für die responsiven Bilder beschäftigt, dessen Idee aus dem devcamp in Essen stammt. Bei der Installation war mir aufgefallen, dass das Plugin nicht automatisch aktiviert wurde. Es fehlte in der Liste der Standardplugins, dort habe ich es jetzt hinzugefügt. Es wäre zu schade drum, wenn neue Nutzer diese Funktion nicht zu sehen bekommen, und es schien mir bereits sehr gut zu funktionieren.
Es war für dieses Plugin aber noch ein Bug gemeldet: Bernd hatte entdeckt, dass die srcsets
in Feeds nicht umgeschrieben werden. Im Kern ist das ja ganz bewusst so gehalten, dass die URL zu Bildern relative URLs sind, also /pfad/zum/Bild ohne vorangestelltes http://domain. Das vereinfacht Blogumzüge auf eine neue Domain. Aber wenn ein Feedreader einen solchen Link sieht kann er den ja nur schwer auflösen. Auch wenn es die besseren versuchen werden, denken vielleicht auch die nicht an das relativ neue srcset-Attribut. Die nötige Logik zum Umschreiben der ungewöhnlichen Kommaliste passte nicht ganz in das Schema des bisher genutzten regulären Ausdrucks für href
und src
, daher habe ich den Code zu preg_replace_callback
umgewandelt und in das Plugin eingebaut.
Stolperfalle war $eventData['feed_body']
. Das muss nämlich statt $eventData['body']
manipuliert werden, da der genutzte Event frontend_display:rss-2.0:per_entry
so spät in serendipity_printEntries_rss
aufgerufen wird. Der Code dort arbeitet nämlich erst auf body
, weist das aber gegen Ende feed_body
zu, und die nachher den Feed ausgebende rss.php schaut nur noch auf feed_body
. Das zu verstehen kostete fast so viel Zeit die Konzipierung des gesamten preg_replace_callback
Abschnitts, samt regulärem Ausdruck und Callback-Funktion.
Nächste Woche ist bei mir Serendipity-Entwicklungswoche
Wednesday, 6. February 2019
Über lange Zeit konstant an einem Projekt zu arbeiten ist schwer. Es gibt ja auch immer anderes zu tun: Andere Softwareprojekte, Spiele, Dinge außerhalb des PCs. Irgendwann gerät das konstante Projekt dann in den Hintergrund. Im Grunde ist das okay. Bei Serendipity aber habe ich etwas angefangen und nicht zuende gebracht: Die Überarbeitung der Interna der Medienlibrary. Die ist fast fertig, aber eben noch nicht ganz. Wenn so etwas dann im Hinterkopf herumspukt empfinde ich das als Stress.
Bei s9y kommt die PHP-Entwicklung dazu. Da ständig neue Versionen herauskommen und die Kompatibilität gebrochen wird können wir uns nicht zurücklehnen und einfach eine lange Weile nichts machen. Sondern bis zum Supportende muss eine neue s9y-Version draußen sein, sonst kann man es auch gleich bleiben lassen. Das ist bei PHP 7.1 der Dezember 2019. Das ist machbar, aber schwieriger wenn Instabilitäten im Git-Master lauern und wenn die Issues sich nicht von selbst lösen. So wie das der Überarbeitung der Medienlibrary.
Daher will ich dieses Wochenende und die nächste Arbeitswoche kontinuierlich etwas Zeit in Serendipity stecken. Ich verspreche mir davon, einen guten Überblick über den Zustand der Alpha im Master zu gewinnen und sie so weit zu bringen, dass zumindest von meiner Seite aus eine Beta veröffentlicht werden kann. Und da mehrere Tage etwas Zeit zu opfern soll dafür sorgen, dass nichts aufgeschoben wird und dann in meinem Hinterkopf bleibt. Was ich im Blick habe:
- Durchtesten, dass die Installation sowohl mit SQLite als auch mit MySQL funktioniert.
- Wenn ein per ML hochgeladenes Bild auf eine andere URL zeigt, bleibt diese beim Umbenennen des Bildes nicht erhalten
- Kompatibilität mit PHP 7.2 sollte komplett sein, aber da will ich nochmal draufschauen.
- Update der bundled-libs.
- Mein Plugin für die VG WORT hat mal Zusatzinformationen in der Eintragsliste angezeigt, jetzt nicht mehr.
- Wenn ich mehrere Einträge in meinem Blog verlinke kommt nur der erste Trackback an.
- Das srcset der responsiven Bilder wird in Feeds nicht von relativen zu absoluten URLs umgeschrieben.
Wenn die Zeit es zulässt will ich dann jeden Tag auch etwas über die Entwicklung schreiben. Selbst wenn das nur ein kurzes "Die Installation lief bei mir einwandfrei" ist. Einfach, damit das Wissen über den Stand nicht untergeht.
Jeder kann helfen: Installiere den aktuellen Git-Master von s9y in einer Testumgebung und melde mir oder repariere direkt Fehler. Zusammen macht das alles viel mehr Spaß.
Erster Eindruck von Ruby/Roda und Frust bei der Authentifizierung
Wednesday, 23. January 2019
Das ist noch keine Gesamtbewertung von Roda, einem an Sinatra angelehnten Framework zum Bauen von Webanwendungen. Sowas braucht mehr Zeit, aber ich will schonmal anfangen zu dokumentieren. Kommend von Sinatra wollte ich für ein neues Softwareprojekt eine ähnliche Serverkomponente nutzen, aber auch etwas neues lernen. Daher Roda, und weil für die spätere Bewertung diese Ersteindrücke oft schon vergessen sind, starte ich mit dem Aufschreiben diesmal ziemlich direkt.
Roda hat mir erst sehr gut gefallen. Es ist ganz klar an Sinatra orientiert, sodass ich schnell starten konnte. Um GET /home.html
zu bedienen ist nur wenig und vertraut aussehender Code nötig:
require "roda" class App < Roda route do |r| r.on "/home.html" do r.get do "Hi" end end end end
Das ist ein bisschen expliziter als gewohnt, mit dem expliziten Iterieren über route
. Das passt, weniger unsichtbare Magie zu haben soll einer der Vorteile sein.
Ich habe schnell verstanden, dass viele der Grundfunktionen in Roda nicht direkt enthalten sind, sie aber per mitgeliefertem Plugin aktiviert werden können. Das meint wirklich Grundfunktionen. Selbst das Ausliefern von Dateien im Verzeichnis public/ oder die Templateunterstützung ist nicht direkt an. Da aber alles nötige direkt anschaltbar ist wirkt das positiv. Als ein hilfreiches Werkzeug, dessen hilfreiche Funktionen man bei Bedarf aktiviert und bewusst entdeckt, anstatt nach ein paar Monaten der Nutzung über eine alternative integrierte Lösung zu stolpern. Das vermeidet das Gefühl, als spiele man mit einer mächtigen Maschine mit unbekannten Grundkonzepten.
Wo steckt also der erwähnt Frust?
Die fehlende Auth-Story
Meine Anwendung ist die letzten Tage fleißig gewachsen. Der Serverpart ist noch minimal, ich habe viel ins Frontend gepackt und dafür vue.js eingesetzt, aber dazu vielleicht später mehr. Jetzt wäre der Zeitpunkt, Daten auf dem Server zu speichern. Das bedeutet auch: Eine Nutzerverwaltung einzuführen und diese Nutzer zu authentifizieren. Sie müssen sich registrieren und einloggen können.
Nun war das auch bei Sinatra damals etwas verwirrend. Da war aber das Problem nur, die vorhandenen simplen Beispiele auszuweiten, zu verstehen wie man Nutzer und Passwörter in der Datenbank ablegt und später abgleicht. Wobei ich dann über browserid stolperte, dessen Nachfolger Portier ich heute bevorzugt nutze, was das Problem komplett umgeht.
Bei Roda dagegen stehe ich derzeit vor einer Wand vor Unverständnis. Gegen die Wand habe ich gestern ein paar Stunden mit dem Kopf geschlagen, langsam wackelt sie, ein bisschen Licht scheint durch und ich habe eine vage Ahnung, was meine Optionen sind. Dafür tut mein Kopf weh.
Insgesamt stolperte ich über fünf Möglichkeiten:
- Es gibt rodauth. Das ist ein großes, allumfassendes Authentifizerungssytem, das auch ohne Roda nutzbar wäre. Aber ich finde keine klare Dokumentation. Die Github-Projektseite listet dagegen all die Datenbanksysteme, die ich nutzen könnte (wovon ich keines nehmen will, denn keines ist sqlite) und all die Nutzer und Tabellen, die ich manuell anlegen soll. Vermittelt nicht den Eindruck, als ob das die gesuchte simple Lösung sei.
- Dann gibt es roda-auth. Schreibt direkt:
Also höchstens als Nachschlagequelle geeignet.This is a first stab at integrating Roda and Warden. It is by no means ready for real use.
- Das nächste gem roda-http-auth macht einen Fehler, der gleich wiederkehren wird: Es erklärt nicht was es macht. Das kann irgendwie authentifizieren, mit verschiedenen Methoden – aber es erklärt nicht wie genau. Bedeutet HTTP-Auth, dass der Browser dieses unmögliche eingebaute Login-Fenster öffnen soll? Was können die angeblich unterstützten anderen Schemata? Ist da irgendwas dabei, was tatsächlich direkt nutzbar ist? Ich weiß es nicht, die Seite erklärt es nicht und man findet auch sonst nichts.
- Es gibt ein Beispiel im Wiki: Example: Adding authentication. Das benutzt warden und sequel_secure_password. Ich wollte es erst verwerfen, weil ich sequel nicht einsetzen wollte. Las dann doch nach und stellte fest, dass sequel_secure_password wieder nicht erklärt wie es funktioniert. Irgendwie wird da eine per Magie herbeigezauberte User-Tabelle benutzt, um Passwörter zu authentifizieren. Damit kann ich nichts anfangen.
- Ich gelang zur Erkenntnis: Dir fehlen da Grundlagen, die in dieser Community voraussetzt werden. Wahrscheinlich, wie warden die Auth-Geschichte erschlagen kann. Also schaute ich dort nach, ging durch das gut lesbare Wiki – das wieder genau dann aufhört, wenn die Authentifizierung beginnt. Da sollen dann irgendwelche Strategien reingepackt werden, deren Funktionsweise nicht erklärt wird. Ich fand dann nur Beispiele, die sich durch ihren Hashtyp unterscheiden – und veraltet sind. Wtf.
Tatsächlich fand ich noch eine weitere Art Option: warden-browserid. Ich könnte wahrscheinlich dieses Gem nehmen und in ein Portier-Gem umwandeln, wie ich es für Sinatra auch gemacht habe. Der Vorteil wäre, dass ein solches Modul für eine Rack-Middleware Portier für alle Ruby/Rack-Frameworks einfacher nutzbar machen würde – für mich und andere. Der Nachteil ist, dass ich mich immer noch mit warden und dessen Integration in Roda auseinandersetzen müsste, Zeit verlöre; Das alles könnte ich mir sparen wenn ich statt Roda einfach wieder zu Sinatra wechsle. Da hadere ich noch mit mir.
Roda wirkt nett und als ob es viele Anwendungsfelder abdeckt. Es gibt auch Dokumentation. Aber sie reicht nicht aus. Leider finden sich zu wenige Anleitungen online um diesen Nachteil auszugleichen. Ich vermute, dass es sogar schlicht zu wenige Leute gibt, die Authentifizierung mit Roda umgesetzt haben und außerhalb der rodauth-Lösung des Hauptentwicklers gar nötige Softwarelösungen fehlen. Vielleicht ist auch alles schon da und es fehlen nur die kompletten Erklärungen, wie man sie in seiner Anwendung nutzen kann. So oder so, ich zweifel sehr, ob Roda für mein gerade anlaufendes Projekt die richtige Lösung ist. Man soll nicht zuviele Technologien auf einmal wechseln und wenn Roda doch in der Praxis ganz neue Probleme aufwirft, dann reicht diesmal vue.js im Frontend einzusetzen vielleicht als Neuheit.
PS: Ich stolpere gerade über diese Erklärung, die zwar auch Sequel voraussetzt, aber verständlich ist. Das könnte ich entweder mit Sequel übernehmen oder ohne nachbauen. Schwere Geburt.
Farbige Klammern für Syntax-Highlighting
Sunday, 13. January 2019
Schaut euch das an:
You NEED multi colorized brackets in your IDE, it will change your life! pic.twitter.com/ehqonnSyy8
— Souvir 💡 (@Souvir) January 10, 2019
Zusammengehörige Klammern immer farbig zu markieren ist eine tolle Idee. Die Funktion gibt es als Plugin für VSCode und atom, und natürlich auch für vim und emacs. Für Geany habe ich leider kein entsprechendes Plugin gefunden. Kennt da jemand eines, das diese Funktion nachrüstet?
Wie man mit AMQP Progammbestandteile unter Ruby/Sinatra auslagern kann
Thursday, 13. September 2018
Bevor ich wie beschrieben die problematische Datenbankabfrage beim PC-Hardwareempfehler pc-kombo entdeckte hatte ich ja schon einige andere Verbesserungen probiert. Die anspruchsvollste war das Auslagern der Preisaktualisierung auf einen zweiten Server.
Ausgangssituation
Der Hardwareempfehler ist eine Ruby/Rails-Anwendung. Der reguläre Teil davon kümmert sich um das Berechnen der Empfehlungen, und natürlich um das Bauen des HTMLs der Webseite. Eine normale Web-App. Etwas ungewöhnlich ist der Threadpool. Er wird beim Start der Webanwendung erstellt.
In diesem Threadpool lief der Code, der regelmäßig die APIs der eingebundenen Shops abfragt um die Preise zu aktualisieren. Das ist superwichtig für die Software, denn nur mit aktuellen Preisen können die besten PC-Builds für das Wunschbudget zusammengestellt werden.
Aber hier lag auch ein Problem: Dieses Preisaktualisieren ist keine leichtgewichtige Operation. Denn sie beinhaltet das Herunterladen, Entpacken und Durchsuchen richtig großer XML- und CSV-Dateien. Das ist besonders problematisch in einer Sprache wie Ruby, in der trotz der Threads wegen des GIL kein echter Parallelismus möglich ist. Und der Threadpool läuft ja im gleichen Prozess wie der Servercode. Das war also eine durchaus wahrscheinliche Ursache für das gelegentliche Langsamsein der Anwendung.
Diese Aufgabe sollte also ausgelagert werden.
Die Lösung: Microservices und AMQP
Anstatt die Preisaktualisierung nur in einen anderen Prozess zu verfrachten wollte ich sie auf einen eigenen Server packen. Dort sollte ein Daemon laufen, der die Preise aktualisiert und die neuen Preise zum Hauptserver sendet. So ist sichergestellt, dass der Hauptserver nur die minimale Last hat, die fertig aktualisierten Preise anzuwenden. Doch das war vor allem aufgrund von SQLite einfacher gesagt als getan. Denn das ist die genutzte und inzwischen auch schwer auswechselbare Datenbank. Der zweite Server kann also keine Verbindung zum Datenbankserver aufbauen und direkt die neuen Preise eintragen, denn es gibt beim dateibasierenden SQLite keinen Datenbankserver.
Meine erste Idee war das Replizieren der Datenbank. Es gibt da mit rqlite eine interessant aussehende Lösung, mit der man eine oder auch mehrere SQLite-Datenbanken auf mehrere Server replizieren kann. Und Änderungen werden synchronisiert. Dann hätte der zweite Server die Datenbank aktualisiert und die Änderungen wären automatisch aktualisiert worden. Doch fehlt rqlite ausgerechnet ein Ruby-Client.
So landete ich als zweites bei Bedrock. Auch diese Software repliziert SQLite-Datenbanken. Und es umgeht ziemlich genial das Client-Problem, indem es den MySQL-Client für seine Zwecke umfunktioniert, und den hat wohl jede Sprache. Doch leider war Bedrock nicht zum Laufen zu kriegen. Es hängt von gcc-6 ab, was schon alleine ein schlechtes Zeichen ist. Und dann lief es einmal kompiliert trotzdem nicht. Zudem ist unklar, ob Bedrock mit bestehenden SQLite-Datenbanken initialisiert werden kann, oder ob die Daten nachträglich eingepflegt werden müssten. Dann hilft es auch nicht, dass es wie ein ziemlich sympathisches Projekt wirkt.
Also zurück zum Kernproblem: Im Grunde sollen nur Informationen von einem Server zum anderen transportiert werden. Dafür gibt es auch andere Lösungen. So ist Sinatra wunderbar geeignet eine REST-API zu betreiben, sodass dann der zweite Server die neuen Preise POSTen könnte. Doch läuft man dann in das typische Problem beim Umbau eines Monolith in eine Microservice-Architekturen: Was tun, wenn die Ziel-API mal down ist? Wie kann auf Fehler und Netzwerkprobleme reagiert werden?
Nun sind Microservices schon fast wieder aus der Mode und es gibt für so etwas natürlich fertige Lösung. Die meines Wissens beste: Setze eine Queue zwischen die Server, welche Daten Zwischenspeichern kann. Und genau das ist AMQP mit einem Broker wie RabbitMQ.
Neue Architektur
Wir landen also hier:
Au dem zweiten Server werden die Preise aktualisiert. Um nicht unnötig Daten zu senden werden diese auch lokal gespeichert, nur Änderungen werden an den Hauptserver gemeldet. Diese Änderungen werden an den auf dem gleichen Server laufenden RabbitMQ-Broker gepusht, über AMQP. Der Hauptserver macht ebenfalls eine AMQP-Verbindung zu diesem Broker auf. Und erhält dann über diese Verbindung über eine Queue die Preisaktualisierungen zugespielt.
Wir haben immer noch den Threadpool, aber der wird nur noch zum regelmäßigen Löschen des Datenbank-Cache genutzt und aktiviert so die Nutzung der neuen Preise.
Das tolle daran ist das Zwischenspeichern auf dem Broker. Ist pc-kombo oder die Verbindung zum Hauptserver down gehen keine Preisaktualisierungen verloren. Sie werden gesichert und dann später und geordnet gesendet, sobald die Verbindung wieder da ist. Das hat sich nun schon mehrfach als sehr praktisch erwiesen.
Der Code dafür ist relativ simpel. Genutzt wird der eine bekannte AMQP-Client für Ruby, bunny. Auf dem zweiten Server senden wir die Aktualisierung:
conn = Bunny.new("amqp://user:password@localhost:5672") conn.start ch = conn.create_channel @q = ch.queue('offers', :exclusive => false) @x = ch.default_exchange # Notify master of new offer or offer to be deleted, but only if it is necessary # For this to work offers.db has to be in sync on the two servers def notifyMaster(offer) changes = updateDB(offer) if changes @x.publish(offer.to_msgpack, routing_key: @q.name) end end
Zusätzlich und nicht zu sehen ist der Threadpool, der wieder genutzt wird um regelmäßig die Preise des Hardwaresortiments zu aktualisieren, woraufhin jedes mal notifyMaster
aufgerufen wird. Sinatra läuft hier aber nicht.
Auf dem Hauptserver bauen wir in Sinatras configure
ebenfalls eine Verbindung zum Broker auf, nur dass wir nichts senden, sondern empfangen:
configure do if settings.production? conn = Bunny.new("amqp://user:password@server2-ip:5672") conn.logger.level = Logger::INFO conn.start ch = conn.create_channel q = ch.queue("offers") q.subscribe do |delivery_info, metadata, payload| offer = MessagePack.unpack(payload) Database.instance.updateOffer(offer) end end end
MessagePack hat übrigens die Eigenheit, Hashes mit Symbolen in Hashes mit Strings zu verwandeln. Aus einem gesendeten offer[:abc]
wird auf dem Hauptserver offer["abc"]
. Darauf musste ich erstmal kommen. Ansonsten erschien es mit aber absolut logisch, für diesen Datentransport ein platzsparenderes binäres Format statt JSON zu nutzen, was wohl sonst meine übliche Wahl gewesen wäre. Ich hatte es zwischendurch auch mit protobuf probiert, kam mit der Logik dessen Ruby-Clients aber gar nicht zurecht.
Insgesamt funktioniert die neue Architektur. Und auch wenn die Preisaktualisierung schließlich nicht die Hauptursache des Performanceproblems war, hilft diese Auslagerung doch sicher dabei, die Performance nicht doch gelegentlich einbrechen zu lassen. Und auf dem zweiten Server kann ich nun besseres Monitoring einbauen, sodass ich mir der laufenden Preisaktualisierung sicher sein kann. Den ziemlich intransparenten Threadpool einzusehen war vorher nämlich schwierig.
Fazit: Nett, aber nicht ganz so ohne
Ich finde, das ist ein schönes Beispiel für den Einbau eines Microservices in einen bestehenden Monolith, bzw. die Auslagerung eines kritischen Programmbestandteils auf einen zweiten Servers. Es war allerdings auch eine größere Operation. Viele Probleme waren zu lösen: Welcher Code muss auf den zweiten Server dupliziert werden, um dort überhaupt Preise aktualisieren zu können? Ich wollte ja nicht alles neu schreiben. Letzten Endes liegt dort jetzt der gesamte Programmcode, nur wird er von einem neuen Ruby-Skript anders genutzt als vorher. Ähnliches Problem: Welche Daten müssen dupliziert werden? So war mir schnell klar, dass Benchmarkdaten nicht, dafür aber alle Hardwaredaten gebraucht werden. Dass es aber auch sinnvoll ist, die vorhandenen Preisdaten zu spiegeln um nur Änderungen zu senden (was das Datenaufkommen und damit die Last auf dem Hauptserver reduziert) wurde mir erst etwas später klar.
Von RabbitMQ habe ich einen gemischten Eindruck. Die Software funktioniert sehr solide, das Dashboard ist hervorragend gemacht. Doch war die Konfiguration gar nicht so einfach. RabbitMQ unterstützt verschiedene Arten von Queues, und mir war nicht ganz klar welche die passendste ist. Letzten Endes passte wohl der Standard. Hauptproblem der Software ist meinem Eindruck nach die Dokumentation: Sie erklärt zwar haarklein alles, schau dir nur die Installationsanleitung an! Aber sie erklärte nie genau das, was ich wissen wollte. Sie ist zu abstrakt und deckte nicht meine Fragen und Probleme ab, wie das richtige Nutzersetup um das Dashboard für einen Remotezugriff zu aktivieren, oder wie Letsencrypt-Zertifikate dieses absichern können.
Auch bunny entpuppte sich als Herausforderung. Der Client hat eine gute API und scheint angemessen ressourcenschonend zu funktionieren. Aber gelegentlich ist mir auf dem Hauptserver die Verbindung abgerissen und konnte nicht automatisch wiederhergestellt werden. Und ich finde es archaisch, zur Besprechung solcher Bugs auf Mailinglisten verwiesen zu werden. Dort konnte mir dann auch nur geraten werden, die automatische Verbindungsreparatur zu deaktivieren und sie manuell neu herzustellen, was keine tolle Lösung ist. Allerdings scheint die Verbindung stabiler zu funktionieren, seit das kernauslastende Performanceproblem gelöst ist. Vielleicht triggerte die Serverlast eine Race-Condition im Code von bunny. Das wäre natürlich ein schwer zu findender Bug.
Insgesamt war es eine lehrreiche Aktion, die glücklicherweise auch noch funktioniert hat.
Wenn ein fehlender Index den Server lahmlegt
Monday, 10. September 2018
In letzter Zeit hatte ich bei meinem PC-Hardwareempfehler pc-kombo mit Performanceproblemen zu kämpfen. Auf meinem Rechner lief die Anwendung lokal gut. Aber wenn mehrere Besucher auf dem Server waren brach desöfteren die Performance ein. Und zwar aller Seiten, die irgendwie auf die SQLite-Datenbank zugriffen oder kompliziertere Berechnungen durchführten.
Bis hierhin hatte ich schon einige Stellen der Serveranwendung optimiert. Die Preisaktualisierung – für die große XML-Dateien geparst werden müssen, was die Serverperformance beeinträchtigen konnte – wurde auf einen zweiten Server ausgelagert, das war die größte Aktion. Geschickter und ebenfalls mit großem Effekt: Ein Cache verhindert das unnötige Neuberechnen des besten PCs für einen bestimmten Preispunkt, solange sich die Komponentenpreise nicht geändert haben. Übersetzungen werden nun genauso im Arbeitsspeicher zwischengespeichert, was die Customize-Funktion beschleunigt. Die Berechnung der Durchschnittsgröße eines Gehäuses wird nur noch durchgeführt, wenn die Detailseite eines Gehäuses mit dem Größenvisualisierer angezeigt wird, und so weiter. All das half, doch es war nicht die Ursache des Grundproblems. Dafür wurde das nun sichtbar.
Schuld an den Performanceeinbrüchen war wohl eine einzelne Datenbankabfrage.
Der Debugweg
Nach all den vorherigen Verbesserungen stach eine Seite als noch besonders langsam heraus: Die Detailansicht einer Komponente, z.B. die des Prozessors i7-8700K. Auf dem lokalen Rechner mit Testdaten war diese Seite allerdings schnell. Ich entschloss mich, trotzdem erstmal dort zu debuggen, da der langsamste Abschnitt auf meinem Rechner ja auf dem Server noch problematischer sein und die längere Verzögerung verursachen könnte.
Zum Performance-Debuggen benutze ich ruby-prof
mit ruby-prof-flamegraph
. Über die Gemseite finden sich gute Erklärungen. Bei mir sieht der gekürzte Code so aus:
get '/:country/product/:type/:ean' do |country, type, ean| if settings.development? require 'ruby-prof' require 'ruby-prof-flamegraph' begin RubyProf.start rescue RuntimeError => re RubyProf.stop RubyProf.start end end html = doStuff() if settings.development? result = RubyProf.stop printer = RubyProf::FlameGraphPrinter.new(result) File.open("profiling/profile_data", 'w+') { |file| printer.print(file) } end html end
Die Datei profiling/profile_data kann ich dann mit flamegraph.pl
visualisieren:
flamegraph.pl --countname=ms --width=1920 < profile_data > product.svg
Und das sah so aus:
Mit 73% ging der größte Teil der Rechenzeit also in Hardware#priceHistory
verloren. Auf meinem Rechner war das weniger als eine Sekunde. Aber auf dem Server hatte das Laden der Seite vorher – unter Last – ~8 Sekunden gedauert. Dort würde diese Funktion wohl ebenfalls den größten Teil der Rechenzeit ausmachen, vermutete ich.
Was macht priceHistory
? In Grunde führt es diesen SQL-Query aus:
SELECT * FROM pmdb.priceHistory WHERE ean = ? AND (vendor = ? OR vendor = ?) ORDER BY date ASC
Ein ziemlich simples Select. Doch der Query-Plan sah nicht gut aus:
EXPLAIN QUERY PLAN SELECT * FROM priceHistory WHERE ean = "05032037108652" AND (vendor = "vendor1" OR vendor = "vendor2") ORDER BY date ASC; 0|0|0|SCAN TABLE priceHistory 0|0|0|USE TEMP B-TREE FOR ORDER BY
SCAN TABLE priceHistory ist der problematische Teil hier. Diese Erklärung bedeutet, dass zum Erfüllen dieser Abfrage die gesamte Tabelle gelesen werden muss. Aber wie groß kann eine solche Tabelle mit etwas Preishistorie schon sein?
du -sh /home/pc-kombo/www.pc-kombo.de/productMeta.db 823M /home/pc-kombo/www.pc-kombo.de/productMeta.db
Upps. Nicht alles davon sind die Preisdaten. Aber der größte Teil.
Um also die Detailseite anzuzeigen musste der Server jedes mal eine 800MB große Tabelle auslesen. Das führte dazu, dass ein Prozessorkern für ein paar Sekunden komplett ausgelastet war (was in Sprachen wie Ruby ohne echten Parallelismus besonders problematisch ist), und auch die Festplatte wurde dadurch natürlich komplett in Beschlag genommen. Kein Wunder, dass dann auch die anderen Seitenaufrufe langsam wurden.
Hier hilft normalerweise ein Index:
CREATE INDEX priceHistory_ean_vendor ON priceHistory(ean, vendor);
Und tatsächlich:
EXPLAIN QUERY PLAN SELECT * FROM priceHistory WHERE ean = "05032037108652" AND (vendor = "vendor1" OR vendor = "vendor2") ORDER BY date ASC; 0|0|0|EXECUTE LIST SUBQUERY 1 0|0|0|USE TEMP B-TREE FOR ORDER BY
Der Index wird direkt genutzt. Und so viel bringt das in Praxis:
Alleine durch diesen einen Index ist die Preishistorie nun nicht mehr der größte Zeitfaktor in der Berechnung, sondern mit 16% einer von mehreren. Und htop
bestätigt das, denn bei dem Seitenaufruf schießt die Prozessorlast des einen Kerns nun nicht mehr auf 100%, sondern bleibt irgendwo unter 10%. Dementsprechend werden auch die anderen Seiten nicht mehr ausgebremst.
Die eigentliche Ursache
Es ist fast immer problematisch, wenn ein Provisorium länger genutzt wird als geplant. Genau das ist hier passiert. Als ich die Preishistorie entwickelte wollte ich die Preise nur für wenige Wochen in der Datenbank speichern. Die eigentliche Lösung sollte eine Zeitseriendatenbank wie rrdtool sein. Solche Datenbanken können die Datenmenge begrenzen, in dem für längere zurückliegende Zeiträume Datenpunkte entfernt werden. Statt den Preis einer Grafikkarte von vor zehn Jahren alle 5 Minuten parat zu haben, speichert rrdtool dann eben nur den Durchschnittspreis einer zehn Jahre zurückliegenden Kalenderwoche. Und bestimmt wäre bei der richtigen Lösung dann auch der Index gesetzt gewesen.
Stattdessen blieb das Provisorium bestehen, bis die Datenmenge so sehr anstieg, dass der fehlende Index – dessen Fehlen beim Entwickeln mit den wenigen Datenpunkten ja noch kein Problem war – den Server ausbremsen konnte.
Ruby und mehrere Kerne
Sunday, 18. March 2018
Im Laufe der Zeit habe ich einige Performance-Optimierungen an meinen Ruby/Sinatra-Anwendungen vorgenommen. Insbesondere bei pc-kombo, denn dort würde ohne tiefere Optimierungen die Antwortzeiten eher bei Minuten als bei Sekunden liegen. Der wichtigste Baustein dort ist die Optimierung der Datenbankabfragen und Caching via memoist.
Im Hinterkopf hatte ich auch immer die Idee, dass die moderne Hardware mit ihren vielen Kernen gut genutzt werden sollte. Das macht Ruby ja leider nicht automatisch. Ich hatte mit dem parallel-gem gespielt, und ich benutze für Hintergrundaufgaben einen Threadpool. Aber wie effektiv kann das sein, wenn die reguläre MRI-Ruby-Implementierung mit einem globalen Lock (GIL) arbeitet und daher paralleles Ausführen von Code praktisch nicht geht? Selbst wenn der Linux-Scheduler Threads über mehrere Kerne verteilen kann, mehrere Prozesse dafür nicht nötig sind? Reicht das Warten auf IO und alles andere, während dessen kein Ruby-Code ausgeführt wird, um mit MRI trotz GIL die Prozessorkerne auszunutzen?
Um das durch Vergleich zu beantworten habe ich mir andere Ruby-Implementierungen angeschaut. Die beiden bekannten: jRuby und Rubinius. Beide sollten das MRI-Problem beseitigen und damit neue Performaneoptimierungen ermöglichen. Spoiler: Beide funktionierten für mich nicht, bis zum Benchmarken kam ich gar nicht.
jRuby
jRuby ist Ruby auf der JVM, der virtuellen Maschine die ursprünglich für Java genutzt wurde und inzwischen einige andere Sprachen trägt. Der Vorteil davon soll sein, dass jRuby-Programme von den Optimierungen der JVM profitieren, und das schließt Parallelismus mit ein. jRuby war der Anleitung folgend erstaunlich einfach installiert: Java installieren, jRuby herunterladen, entpacken und ausführen. Auch rvm unterstützt jRuby, als ich beim Ausführen dann nicht durchaute wie ich den PATH
und gems verwalten soll löschte ich die manuelle Installation wieder und installierte es damit.
Es scheiterte dann für mich an sqlite3. Das sqlite3-gem nutzt C-Code, und jRuby kann keine C-Module ausführen. Stattdessen würde man wohl jdbc laden, also das machen wie im Java-Land. Dafür müsste ich meinen Datenbankcode umschreiben, und für ein Experiment ungewissen Ausgangs war das keine Option. Das ist wohl weniger problematisch, wenn man activerecord benutzt, dann stellt man das einfach auf den jdbc-Adapter um.
Rubinius
Rubinius hatte ich mir vor einiger Zeit schonmal angeschaut, damals war es nicht mal installierbar. Inzwischen hat das Projekt wohl etwas Zeit darein investiert, tatsächlich Nutzer zu erreichen, die Installation war ein simples rvm install rbx-3
. Aber Rubinius ist trotzdem noch nicht praxistauglich genug.
Erst hing es sich an meiner Benchmark-Klasse auf. Ich hatte mir den Issue-Tracker angeschaut und fand die Interaktionen dort nett genug, um diesen für mich unerklärlichen Bug zu melden. Dort bekam ich dann auch wirklich gute Hilfe, Benchmark wird derzeit durch die Standard-Lib belegt, ich müsste es in ein Modul packen. Gesagt, getan, nächster Fehler. Nun war es das http-accept-gem, mit dem pc-kombo die Lokalisierung umsetzt, das an einer unvollständigen StringScanner-Implementierung starb. Auch diesen Fehler meldete ich.
Ich bin erfreut über die gute Antwortzeit des Projektes, aber es ist für mich unmöglich einzuschätzen, ob Rubinius auch nur annähernd kompatibel genug ist, eine echte Sinatra-Webanwendung mit ihren weiteren gems auszuführen. Auch absolut unklar ist, ob Rubinius überhaupt einen Performancevorteil bringen würde - neben der GIL-losigkeit kamen viel der berichteten positiven Performanceerfahrungen wohl vom JIT-Interpreter, der aber inzwischen herausgenommen wurde. Könnte immer noch Parallelismus ermöglichen und damit meinen Anwendungen nützen, aber klar ist das nicht.
Alternative: Der MRI-Weg
Außer auf Verbesserungen bei Rubinius zu hoffen sehe ich für mich drei Alternativen:
- Ich könnte einfach nichts machen. Seit der letzten Performance-Tuning-Runde ist die Seite wieder schnell. Durch Debugging hatte ich bemerkt, dass das Problem bei den Datenbankabfragen lag, nicht am Prozessor. Da waren blöde Sachen dabei, wie für jede Hardwarekomponente beim Seitenaufbau nochmal in der Datenbank nach Bildern zu suchen. Solange die Seite keinen Besucheransturm erlebt kann das also erstmal so bleiben. Auch wenn es generell doof ist, die Fähigkeiten moderner Prozessoren mit meiner bevorzugten Sprache nicht gut nutzen zu können.
- Ich könnte auf Ruby 3 hoffen. Großes Ziel der Entwicklung sind Performanceverbesserungen, 3x3 war das Stichwort. Bessere Unterstützung von Parallelimus/Concurrency durch neue Operatoren und Konzepte ist auch dabei, selbst die Beseitigung des GILs wurde ins Auge gefasst. Das könnte ich verfolgen und frühzeitig versuchen einzubauen.
- Wenn jetzt etwas geschehen soll könnte ich auch die Architektur der Anwendung ändern. Derzeit ist das ein Monolith, wobei an ein paar Kernstellen ein Threadpool benutzt wird. Ich könnte es aufsplitten: Eine Anwendung zum Aktualisieren der Preise, eine andere zum Zeichnen der Weboberfläche, eine dritte zum Erstellen der Hardwarezusammenstellungen. Herausforderung wäre das Austauschen der Informationen, aber auch dafür gibt es schon Lösungen.
Theoretisch könnte man natürlich auch die Sprache wechseln, aber dafür ist pc-kombo viel zu groß. Trotzdem etwas für den Hinterkopf für Leser, die diesen Artikel lesen um für eine noch zu bauende Anwendung den Einfluss von Ruby auf die Performance einzuschätzen. Wichtig ist dann die Erkenntnis: Sollen mehrere Prozessorkerne genutzt werden, muss das genau geplant werden - Ruby macht das nicht für dich.
Rubys Twitter-Gem
Thursday, 22. February 2018
Twitter in Pipes einzubauen erschien mir unheimlich schwer, aber ich hatte es völlig überkompliziert. Mein Problem war der Startpunkt: https://developer.twitter.com/en/docs. Die offizielle Twitter-Dokumentation ist verwirrend. Es gibt für verschiedene Aufgaben verschiedene APIs, und als ich dort anfangs reinschaute gab es nochmal andere APIs, bei denen dann die Warnung der baldigen Abschaltung dranstand, die Alternative gab es noch nicht. Es erschien mir unmöglich, da etwas zukunftssicheres zu entwickeln.
Ich hätte mir direkt sferik/twitter anschauen sollen. Denn wie so viele gute gems abstrahiert er diese Entscheidungen völlig. Er verlangt nur nach den API-Keys und Tokens (warum auch immer Twitter beide Systeme hat) und funktioniert danach einfach als Code:
client.search("ruby")
sucht nach Tweets mit ruby,
client.user_timeline("onliandone")
zeigt Tweets aus der Timeline des angegebenen Nutzers. Rate-Limiting fehlt, aber dafür habe ich mit throttle-queue schon eine Weile eine saubere Lösung im Arsenal.
Sollte nun Twitter wirklich wieder die APIs ändern kann ich davon ausgehen, dass das gem alle Probleme lösen wird. Es wird viel benutzt und immer noch entwickelt, auch wenn die Github-Issues sich nicht toll lesen. Und sollte ich mich da irren, kann das gem immer noch durch eine manuelle Lösung ersetzt werden. Das Ruby-Universum ist für sowas schon toll, die spezielle Lösung in der Sprache ist wie so oft der bessere Startpunkt für die Problemlösung gewesen.
WebGL zum Größenvergleich von PC-Gehäusen
Friday, 15. September 2017
Ein neues Feature meines Hardwareempfehlers sind Übersichtsseiten für die einzelnen Komponenten. Jede Seite soll so nützlich wie möglich sein: So stehen dort neben den aktuellen Preisen die ganzen Spezifikationen, Bilder, Links zu Reviews und zur Herstellerseite, und bei Prozessoren und Grafikkarten auch die Benchmarkbewertung.
Mein Lieblingsfeature aber ist inspiriert von einem Reddit-Thread. Dort hatte ein Nutzer eine Reihe von Gehäusen mit CAD gerendert, und es war wirklich aufschlussreich die Dimensionen der Gehäuse im Vergleich zu sehen. Sowas wollte ich für die Übersichtsseiten der Gehäuse haben. Mit WebGL und three.js konnte ich es nun bauen.
Das ist das Ergebnis, als Beispiel die Übersichtsseite samt Größendarstellung des NZXT Source 340:
Was ich schon hatte waren die Dimensionen der Gehäuse: Höhe, Breite, Tiefe. Es geht hier nicht darum, detailgetreue Nachbildungen zu schaffen, sondern abstrakt die Größe darzustellen. Dafür reichen diese drei Werte, denn mit ihnen kann man einen Quader zeichnen. Diesen Quader des aktuellen Gehäuses stelle ich dann neben den Quader nach den Durchschnittswerten aller Mid-Tower in der Datenbank, und auf der anderen Seite den Quader nach den Durchschnittswerten aller Mini-Gehäuse.
Three.js erwies sich dabei als hervorragendes Hilfsmittel. Es ermöglicht das Programmieren von WebGL auf einer hohen Abstraktionsebene, dadurch wird es ziemlich einfach. Dazu gibt es für three.js einige Beispiele und eine hilfreiche Dokumentation.
Der gekürzte Code:
var scene = new THREE.Scene(); scene.background = new THREE.Color(0xE5E5E5); scene.fog = new THREE.FogExp2( 0xcccccc, 0.02 ); var camera = new THREE.PerspectiveCamera( 60, 400/300, 0.1, 1000 ); var renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( 400, 300 ); document.querySelector('#visualize_size').insertBefore(renderer.domElement, document.querySelector('#case_explanation')); // current case var geometry = new THREE.BoxGeometry(WIDTH, HEIGHT, DEPTH ); var materialCurrent = new THREE.MeshPhongMaterial( { color: 0x156289, emissive: 0x072534, side: THREE.DoubleSide, flatShading: true } ) var cube = new THREE.Mesh( geometry, materialCurrent ); scene.add( cube ); var pointLight = new THREE.PointLight( 0xE6E6FA, 1, 100 ); pointLight.position.set( 10, 10, 10 ); scene.add( pointLight ); var light = new THREE.AmbientLight( 0x222222 ); scene.add( light ); camera.position.set(0.5, 0.5, 2) camera.lookAt(0, 0, 0) controls = new THREE.OrbitControls( camera, renderer.domElement ) var animate = function () { requestAnimationFrame( animate ); renderer.render(scene, camera); }; animate();
Zuerst erstellt dieser Code die Umgebung: Es gibt eine Szene, eine Camera, einen Renderer. Dann wird ein Objekt erstellt, ein Cube (=Quader), mit den Daten des Gehäuses. Das Objekt wird in die Szene gesetzt, dann noch Licht hinzugefügt, die Kamera positioniert und mit den OrbitControls steuerbar gemacht. Der Nutzer kann die Kamera später in alle Richtungen bewegen. Schließlich wird das alles gerendert.
Die Lichter waren knifflig für mich, und da meine Lösung dort wahrscheinlich nicht optimal ist habe ich das auch weitestgehend ausgelassen. Die Farben der Lichter beeinflussen ja die Farbe der Gehäuse, und man will gleichzeitig alle Seiten beleuchten, sie aber alle leicht unterschiedlich haben, sodass die Kanten sichtbar sind. Die Szene braucht dafür noch ein paar Lichter mehr, um die Gehäuse von der anderen Seite zu beleuchten.
In meinem echten Code kommen noch die zwei anderen Gehäuse dazu, um einen Größenvergleich zu haben, und ein Rasternetz unten zur Orientierung.
Ein Problem blieb: Mit den drei Gehäusen nebeneinander hatte man immer noch nicht wirklich eine Idee, wie groß sie in Wirklichkeit sind. Wer sich vorstellen kann, wie groß ein durchschnittliches PC-Gehäuse ist, für den funktionierte das. Doch selbst ich fand das schwierig. Deswegen baute ich eine Banane ein, die rechts neben dem aktuellen Gehäuse steht. Ist zwar ein halber Witz, macht es aber doch einfacher.
Ich bin ziemlich glücklich über das Ergebnis. Besonders die Größen von ITX-Gehäusen zu sehen finde ich interessant. Der Kolink Satellite z.B. ist deutlich kleiner als der Thermaltake Core V1, aber Fractals Node 202 hat nochmal einen ganz anderen Ansatz.
Mich hat es auch gefreut WebGL nutzen zu können. Bisher hatte ich mich immer gefragt, warum man solch eine Technik im Browser haben muss. Hier aber war es eindeutig praktisch, und viel einfacher als ich erwartet hatte.
CSS: Mit dem neuen Grid-Layout eine DL als Tabelle formatieren
Tuesday, 30. May 2017
Definition Lists (DLs) bzw. description lists (siehe Matthias Kommentar) sind ein kaum genutztes HTML-Element, das ich aber ganz gerne mag. Ihre semantische Aufgabe ist es, Definitionen zu einem Schlagwort aufzulisten. Spinnt man das etwas weiter macht es das zum einzigen geeigneten Element, wenn man Eigenschaften einer Sache auflisten will. Zum Beispiel die Eigenschaften eines Prozessors, wie Prozessortakt und die Anzahl der Kerne.
DLs sind ein <dl>
Element, in dem dann mindestens ein <dt>
ist - das zu definierende Ding - dem ein oder mehrere <dd>
folgen - die Definitionen.
Im Browser werden DLs so normalerweise angezeigt:
DT DD DD ...
Die <dd>
stehen also rechts eingerückt (Firefox löst das mit margin-left: 40px
) unter dem Schlagwort. Hier nochmal als Fiddle:
Das ist nicht immer ideal. Diese Art der Darstellung wirkt schlicht unfertig und sie nimmt viel vertikalen Platz weg. Entsprechend gibt es seit Jahren ein paar wenige (denn wie erwähnt, das Element wird selten benutzt) Artikel, wie man DL anders darstellen kann.
Eine hübsche Lösung bietet sich mit CSSs relativ neuem grid
-Layout an. Eine sinnvolle Darstellung für DLs ist eine tabellarische: Links die Schlagworte, rechts die Definitionen. Das war bisher aber nicht ganz einfach, wenn man weiterhin die mehreren <dd>
unter einem <dt>
unterstützen will, und gleichzeitig float
zu vermeiden wünscht.
Mit grid
ist es nun ganz einfach:
dl { display: grid; grid-template-columns: 50% 50%; } dt { grid-column-start: 1; } .specs dd { grid-column-start: 2; margin-left: 0; padding-left: 1em; }
Das sieht dann so aus:
Da fehlt dann nur noch etwas übliches Tabellenstyling für ein gutes Ergebnis.
Übrigens: Support für grid
ist mit den letzten Browserversionen in alle Mainstream-Browser gerutscht, wobei IE wie Edge noch ein Prefix benötigt und nicht die aktuelle Version der Spezifikation unterstützt.
RSS in Ruby Lesen und Erstellen
Monday, 22. May 2017
Für mein derzeitiges Softwareprojekt arbeite ich mit RSS- und Atom-Feeds. Dabei entdeckte ich, dass Ruby in der Standardbibliothek ein Modul zum Arbeiten mit diesen Formaten hat.
Es gibt einen Parser zum Lesen (Beispielcode aus der Dokumentation):
require 'rss' require 'open-uri' url = 'http://www.ruby-lang.org/en/feeds/news.rss' open(url) do |rss| feed = RSS::Parser.parse(rss) puts "Title: #{feed.channel.title}" feed.items.each do |item| puts "Item: #{item.title}" end end
Und eine Klasse zum Erstellen:
require "rss" rss = RSS::Maker.make("rss2.0") do |maker| maker.channel.updated = Time.now.to_s maker.channel.title = 'Testing the RSS 2.0 maker' maker.channel.link = 'http://www.ruby-lang.org/en/feeds/news.rss' maker.channel.description = 'An important description' maker.items.new_item do |newItem| newItem.title = 'Item title' newItem.updated = Time.now.to_s newItem.link = 'https://www.onli-blogging.de/1641/Space-Hulk-Ascension.html' newItem.content_encoded = 'A mediocre game' newItem.guid.content = 'https://www.onli-blogging.de/1641/Space-Hulk-Ascension.html' end end puts rss
Allerdings ist dieses Modul nicht unproblematisch. Abgesehen von der Readme gibt es nahezu keine Dokumentation. Auch Google findet kaum Diskussionen über den Code, das macht das Arbeiten damit schwierig. Ein Beispiel, in das ich heute gerannt bin, steckt im obigen Beispielcode: In bestimmten Situationen gibt der RSS-Bauer einfach nichts aus.
Deswegen ist es eine wichtige maker.channel.description
. Ist diese nämlich nil
oder leer (""
), scheitert die Erstellung ohne Fehlermeldung und es gibt keine Ausgabe.
Eine weitere Stolperstelle steckt in der guid
. Normalerweise würde man die ja einfach über newItem.guid = 'die guid'
setzen. Weil dieser Setter aber nicht implementiert ist, muss stattdessen newItem.guid.content
aufgerufen werden.
Den Parser nutze ich gar nicht erst direkt, weil ich eine Normalisierung von Atom und RSS brauche. Das kleine Gem feedparser kann das, nutzt ihn allerdings intern. Ansonsten hätte ich auf feedjira gesetzt, das normalisiert wohl auch, wirbt aber nicht explizit damit, was mich zögern lässt.
Zum Erstellen hätte ich aber keinen anderen Weg, außer das wie bei ursprung manuell über ein Template zu erledigen.
Trotzdem: Sehr schön, so etwas in der Standardbibliothek zu haben. Jetzt müsste es nur noch dokumentiert sein.
Mehrere SQLite-Datenbanken kombinieren
Tuesday, 14. March 2017
Das tolle an SQLite ist, dass es eine Datenbank in einer Datei ist. Das hat viele Vorteile: Einfache Installation, einfache Konfiguration, einfache Backups. Und die Performance ist trotzdem nicht schlecht. Es hat aber auch einen Nachteil: Wie soll man Daten synchronisieren?
Also, ein Beispiel: Eine Webapp auf dem Produktivsystem aktualisiert die Datenbank mit neuen Produktpreisen, und hat dafür eine eigene Tabelle offers
. Gleichzeitig wird auf dem Entwicklungssystem die Tabelle products
angepasst, also welche Produkte die Webapp überhaupt kennt. Jetzt sollen gleichzeitig die Preise aktuell gehalten und irgendwie die neue Produkt-Tabelle ins Produktivsystem geschoben werden.
In einem solchen Szenario kann man die Datenbank aufteilen. Die aktuellen Preise bleiben in einer Datei namens offers.db, die Produkte landen in products.db. SQLite kann die dann mit ATTACH
zusammenfügen.
$ sqlite3 products.db SQLite version 3.17.0 2017-02-13 16:02:40 Enter ".help" for usage hints. sqlite> ATTACH DATABASE 'offers.db' as odb;
Danach können die Tabellen in offers.db ganz normal in SQL-Abfragen benutzt werden. Bei Kollisionen hilft das Namenspräfix. Sogar neue Tabellen können damit zielgerichtet in dieser Datei erstellt werden:
CREATE TABLE odb.sales(link TEXT, price REAL);
Mit dieser Funktion kann man wunderbar die Daten in permanente und regelmäßig aktualisierte aufteilen, sodass das Überschreiben der ersten nicht die Aktualisierungen der anderen verlorengehen lässt.
Simple i18n with Ruby/Sinatra
Friday, 20. January 2017
Für den Hardwareempfehler gibt es natürlich Übersetzungen, schon weil er nicht nur den deutschen Markt unterstützt. Worauf ich damals aber nicht kam war, die Sprache je nach der Browsersprache zu wählen. Der Browser sendet die als Header mit, Accept-Language. Den müsste man nur auslesen, und das geht mit einem passenden Gem ziemlich einfach. Ich glaube, beides war mir damals nicht richtig klar.
In Sinatras configure-Block werden die Übersetzungen initialisiert. Diese liegen unter locales/. Im before-Block wird vor jedem Seitenaufruf die passende Sprache gewählt. Das Gem i18n verwaltet die Übersetzungen, http-accept liest den Accept-Language-Header aus. Leider fehlt da eine Hilfsfunktion, um die regionsübergreifende Sprache zu bekommen (z.B. englisch, egal ob der Header nun en_GB oder en_US sendet). Aber das erledigt dann die Regexpression bei der Case-Abfrage.
configure do I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks) I18n.load_path = Dir[File.join(settings.root, 'locales', '*.yml')] I18n.backend.load_translations I18n.default_locale = :"de" I18n.exception_handler = lambda do |exception, locale, key, options| case exception when I18n::MissingTranslation return key.to_s end end end before do if request.env['HTTP_ACCEPT_LANGUAGE'] languages = HTTP::Accept::Languages.parse(request.env['HTTP_ACCEPT_LANGUAGE']) languages.each do |language| case language.locale when /en[_]*/ I18n.locale = "us" return when /de[_]*/ I18n.locale = "de" return when /fr[_]*/ I18n.locale = "fr" return when /es[_]*/ I18n.locale = "es" return end end end I18n.locale = "us" end
Wenn dann die Seite nach dem Neuladen die Sprache ändert, nur weil in den Browsereinstellungen eine neue gewählt wurde, wirkt das schon ein bisschen wie cooles Voodoo.