Errors in Exceptions umwandeln (2)

30 Nov 2008 In: PHP, Webworking
PHP Bugs: #45895

PHP Bugs: #45895

Da war ich wohl in meinem letzten Artikel “Errors in Exceptions umwandeln” etwas voreilig. Durch einen Workaround, den ich ausversehen noch bei mir eingebaut hatte, wirkte es auf mich so, als wäre der Backtrace der ErrorException richtig. Dem ist aber leider nicht so. Nach wie vor beinhaltet der erste Eintrag die Zeile, in der die ErrorException geworfen wird.

Und wie ich in den Kommentaren im PHP-Manual gelesen habe (und auch selber nachvollziehen konnte), gibt es auch noch einen PHP-5.2-Bug in der ErrorException, welche die Argumente im Backtrace durcheinanderwürfelt. Und dieser kann leider erst mit PHP 5.3 behoben werden. Na, klasse.

Bevor ihr jetzt den Workaround aus den Manual-Comments einbaut, bei dem ihr auch noch ständig die PHP-Version überprüfen müsst, empfehle ich, die ExceptionError-Klasse selbst durch Erweiterung der Exception-Klasse zu bauen. Sollte recht schnell gehen, beinhaltet den Fehler nicht, und man könnte automatisch den ersten Eintrag aus dem Backtrace herauswerfen, um sich um keinen Sonderfall im Exception-Handler kümmern zu müssen.


  1. PHP Bugs: #45895: Exception in set_error_handler() messes up backtrace args

Verwandte Artikel:

Clientside Cache Control

27 Nov 2008 In: PHP, Webworking
Artikel auf techblog.tillate.com

Artikel auf techblog.tillate.com

Auf techblog.tilllate.com gab es vor kurzer Zeit einen recht interessanten Ansatz zur Cache-Kontrolle durch den User. Dieses ist eine deutsche Erklärung dazu, die noch einen kleinen Schritt weiter geht.

Jeder Webworker hat wohl schon mal Caching in seine Applikationen eingebaut. Spätestens, wenn der Server fast am Krepieren ist und der Kunde Sturm klingelt, weil seine Website nicht mehr zu erreichen ist, ist es soweit. Die mit Abstand häufigste Caching-Form ist dabei wohl das serverseitige Cachen, bei dem die Website-Administratoren dann selbst entscheiden, wie oft sich ein Cache erneuert. Leider sorgt das bei den Besuchern gern mal für Unmut, wenn z.B. der User in dem seiteninternen Messaging-System nicht mehr sieht, ob er schon eine neue Nachricht bekommen hat.

Was tun in einem solchen Fall? Man möchte die User ja nicht vergraulen. Andererseits darf der Server auch nicht in die Knie gehen.

Die Lösung: den User selbst dafür sorgen lassen, dass sich der Server-Cache zur richtigen Zeit erneuert.

Auf die Tastenkürzel kommt es an

Die meisten wissen, dass man z.B. im Firefox per Strg+Shift+R dafür sorgen kann, dass der Browser-eigene Cache übergangen wird. Weniger bekannt ist, dass in diesem Fall auch zusätzliche Request Header an den Server geschickt werden. Anschauen kann man dieses sehr gut, wenn man die verschiedenen Reload-Varianten mal auf requestheaders.com ausprobiert.

Beim Check sieht man, dass entweder der Header cache-control (HTTP/1.1) oder pragma (HTTP/1.0) mitgeschickt wird. Beide entweder mit dem Wert no-cache oder max-age=0.

Wie in dem Original-Bericht habe ich mal überprüft, bei welchen Browsern verwertbare Request Header mitgeschickt werden, wobei ich ein paar Tastenkombinationen mehr unter die Lupe genommen habe. Lustigerweise kommen bei mir zum Originalartikel abweichende Ergebnisse heraus:

Browser URL-Enter F5 Strg+F5 Strg+R Strg+Shift+R
Firefox 3.0.4 O O X O X
IE 7 O O X O -
IE 6 O O O O -
Safari 3.1.2 (Windows) O X - X -
Opera 9.51 O X - X -
Chrome X X X X -

X: cache-control oder pragma wurden mitgeschickt
O: keine zusätzlicher Header wurde mitgeschickt
-: Die Tastenkombination ist in dem Browser nicht möglich

Fazit:

Bei jedem Browser kommt unter bestimmten Bedingungen ein gültiger Wert heraus. Man könnte es also dem User überlassen, ob ein serverseitiger Cache invalide wird. Da die Befehle ja auch nur gesendet werden, wenn ein User explizit einen Reload anordnet, während er die Seite eh schon auf dem Schirm hat, kann das auch als forcierter und gewollter Reload betrachtet werden. Der User will also schauen, ob es eine neue Variante gibt.

Sehr viel mehr Traffic dürfte für den Server im Normalfall nicht herauskommen, weil die User normalerweise nur von Seite zu Seite springen und daher ziemlich selten die benötigten Header absenden.

Ich werde das wohl in Kürze einbauen, weil es auch für mich als Entwickler eine Erleichterung ist, den Cache während der Entwicklung einfach per Tastendruck zu umgehen.


  1. Der Originalartikel: http://techblog.tilllate.com/2008/11/14/clientside-cache-control/
  2. RFC2616: HTTP/1.1: Header Field Definitions
  3. http://www.requestheaders.com

Verwandte Artikel:

Errors in Exceptions umwandeln

25 Nov 2008 In: PHP, Webworking
PHP: ErrorException

PHP: ErrorException

Exceptions sind eine feine Sache. Sie kommen jedem faulen Programmierer entgegen, der keine Lust hat, jeden möglichen Fehlerfall einzeln abzuhandeln. Dumm nur, dass PHP-Funktionen aufgrund von Rückwarts-Kompatibilität immer noch Fehler statt Exceptions werfen.

Einer eher bekanntere Methode ist, einen Errorhandler zu schreiben, der nur die Aufgabe hat, eine Exception zu werfen. Das Problem: Der Backtrace, den man ja mit $e->getTrace() erhält, beginnt dummerweise mit der Zeile, in der die Exception geworfen wurde, und nicht mit der ders eigentliche Fehlers. Deshalb kann man dummerweise nicht den gleichen Top-Level-Exception-Handler verwenden, den man für Standard-Exceptions verwendet.

Eher unbekannt ist, dass es in PHP schon eine erweitere Exception namens ErrorException gibt. Mit folgendem simplen Code ist es problemlos möglich, Fehler auf Exceptions zu mappen:

function exception_error_handler($errno, $errstr, $errfile, $errline ) {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
set_error_handler("exception_error_handler");

Wer das ganze jetzt ein wenig aufmöbeln will, kann in diesem error handler jetzt noch den error_reporting Level abfragen, damit nur Fehler Exceptions werfen, die das auch wirklich sollen.


  1. ErrorException im PHP Manual
  2. Exceptions im PHP Manual
  3. error_reporting() im PHP Manual

Verwandte Artikel:

Üble Wakeboard Slams

8 Nov 2008 In: Wakeboarding

Nachdem ich mir letztens das geniale Wakeboard Video “Impact” von Christian Grüner zugelegt habe, habe ich jetzt auch im Internet das Gegenstück gefunden, welches perfekt zeigt, wie es beim Wakeboarden auch zugehen kann. Doh! Da fällt einem wieder ein, wozu man eigentlich die Prallschutz-Weste braucht.

Verwandte Artikel:

Speicherverbrauch und große Arrays in PHP

10 Okt 2008 In: PHP, Webworking

Heute hatte ich das Problem, dass ich einem Kunden etwa 10.000 Datensätze aus einer Online-MySQL-Datenbank als Download zur Verfügung stellen sollte. Dieser Download sollte alle Datensätze live ausgeben, natürlich als Excel-Datei.

Wie immer habe ich dafür unser firmeneigenes MVC-Framework verwendet. Im Controller alle Datensätze geholt, in ein Array geschrieben und ans View übergeben, damit dieses daraus eine HTML-Tabelle generiert, welche bekanntlich problemlos in Excel importiert werden kann.

Allerdings schmierte PHP regelmäßig bei etwa 8.000 Datensätzen ab: Speicher voll. 40MB standen auf dem Hosting-Paket pro Script zur Verfügung, 50MB wollte PHP belegen.

Schritt 1:
Überall den Speicherverbrauch mit memory_get_usage() gemessen. Klar, das Array aus der Datenbank ist groß, etwa 18 MB. Aber diese Daten brauche ich nun mal. Also in der View-Komponente geschaut, was sich da machen lässt.

Die View-Komponente erstellt erst die HTML-Tabelle und baut dann am Ende einfach die html-Tags für eine gültige Seite drumherum, also

$output = '<html><head><title>Excel</title></head><body>'.$content.'</body></html>';

Aha, in dieser Zeile schmiert PHP ab. Begründung:
Bug #44069 Huge memory usage with concatenation using . instead of .=

Also umgebaut auf:

$output = '<html><head><title>Excel</title></head><body>';
$output .= $content;
$output .= '</body></html>';

Speicherverbrauch kleiner, aber Problem noch nicht gelöst.

Schritt 2:
Das Framework verwendet standardmäßig gzip-Kompression, um die Daten auszuliefern. Die Kompression dieser Masse an Daten kostet Speicher. Also die Kompression ausgeschaltet. Resultat: Speicherverbrauch kleiner.

Eine Zeit lang gehts gut. Dann werden die Kunden-Daten mehr und der Download schmiert schon wieder ab. OK, also weiter schauen.

Schritt 3:
Eine Auffälligkeit im Controller:

echo memory_get_usage(); // ergibt 18 MB

// überflüssigen Key löschen, den der Kunde nicht in seiner Tabelle braucht
foreach ($data as $key=>$row) {
unset($data[$key]['id']);
}

echo memory_get_usage(); // ergibt 23 MB

Was ist denn nu los? Ich lösche Daten aus dem Array, gebe den Speicher frei, aber der Speicherverbrauch steigt? OK, dafür habe ich immer noch keine Erklärung. Also, die MySQL-Query so umgebaut, dass ich das id-Feld gar nicht erst holen muss. Schleife gelöscht. Speicherverbrauch bleibt bei 18 MB. Schonmal besser, aber das reicht nicht.

Schritt 4:
Nach langem Suchen zu Speicherproblemen bei großen Arrays bin ich letztendlich zu der Lösung gekommen: PHP braucht einfach viel Speicher, um große Arrays zu verwalten. Bei 10.000 Datensätzen mit etwa 15 Feldern muss PHP 150.000 Variablen verwalten. Ups, sorry, PHP.

Also hab ich, nachdem ich alle Datensätze geholt habe, jeden Datensatz als String gespeichert:

echo memory_get_usage(); // ergibt 18 MB

// Datensätze als String speichern
foreach ($data as $key=>$row) {
$output[$key] = implode('|', $row);
}

echo memory_get_usage(); // ergibt 2 MB

Bestens. Hab dann ein neues View geschrieben, dass dementsprechend jeden Datensatz durchgeht, ein explode() durchführt, um die Felder wieder einzeln zu bekommen, und schon liegt mein Speicherverbrauch am Ende bei 2 MB.

Fazit:

  1. Aufpassen bei sehr großen Arrays. Der Speicherverbrauch steigt enorm an.
  2. Das MVC-Pattern hat eine Schwachstelle. Man kann nicht sauber die Ergebnisse einer MySQL-Query streamen (damit hätte ich das Problem ja auch nicht gehabt), weil ja immer der Controller seine Daten an das View übergeben muss. Das wird irgendwann immer zu einem Speicherproblem führen.
    Für Lösungen oder Hinweise dazu bin ich immer dankbar.

Verwandte Artikel: