Erfahrungsbericht - CherryPy und Cheetah

Cheetah Vorlagen von CherryPy automatisch ausliefern lassen

Vorlagen zwischenspeichern und nur bei Bedarf neu kompilieren lassen

Cheetah hat eine Möglichkeit eingebaut, Werte, die die Vorlage schon einmal errechnet oder von einer Python-Funktion erhalten hat, im Speicher zu halten. Bei einem nochmaligen Aufruf der Seite müssen diese Berechnungen nicht noch einmal ausgeführt werden. Im oben gezeigten Beispiel wird die Vorlage jedes mal neu aus der TMPL-Datei erstellt und alle Funktionen werden neu ausgeführt. Damit wird jedes Caching von Cheetah schon im Ansatz abgewürgt. Um das zu verhindern, muss das geladene Template in einer Liste oder in einem Dictionary im Speicher gehalten werden. Die Schwierigkeit dabei ist allerdings, die Anzahl der zwischengespeicherten Vorlagen einzuschränken und auch nach einer gewissen Zeit automatisch wieder aus dem Container zu löschen.

Da auf diese Liste von mehreren Threads aus zugegriffen werden kann, muss der Zugriff auf diesen Container threadsicher sein. Das ist nicht mit wenigen Zeilen Code zu machen. Deshalb erscheint das nächste Beispiel ein wenig groß. Dafür hat es aber die oben schon erwähnten Vorteile. Es sind zwar mehr als 200 Codezeilen, aber ich möchte dir das Programm nicht vorenthalten, da es die Arbeit mit CherryPy und Cheetah erheblich erleichtern kann.

#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# cptest.py

import os
import cherrypy
from Cheetah.Template import Template
import time
import threading

APPDIR = os.path.dirname(os.path.abspath(__file__))
INI_FILENAME = os.path.join(APPDIR, "cptest.ini")


class CheetahTemplateContainer(dict):

    def __init__(
        self,
        max_items_in_container = 1000,
        max_minutes_in_container = 120,
        vacuum_interval_minutes = 2.9,
        max_lock_tries = 300
    ):
        """
        TemplateContainer initialisieren und Einstellungen übernehmen

        :param max_items_in_container: Gibt an, wie viele Vorlagen maximal im
            Container zwischengespeichert bleiben. Ist diese Höchstgrenze
            überschritten, dann werden die ältesten Vorlagen aus dem Container
            entfernt
        :param max_minutes_in_container: Gibt an, wie lange eine Vorlage maximal
            im Container bleiben darf. Ist diese Zeit überschritten, dann wird
            die Vorlage aus dem Container entfernt.
        :param vacuum_interval_minutes: Gibt an, in welchem Intervall die
            Vacuum-Funktion aufgerufen werden soll. Diese Vacuum-Funktion ist
            dafür zuständig, die Vorlagen wieder aus dem Container zu entfernen.
        :param max_lock_tries: Beim Anfordern einer Vorlage, wird versucht einen
            Lock zu acquirieren, damit sich Threads nicht in die Quere kommen.
            Damit es nicht zu einem kompletten Blockieren der Auslieferung kommen
            kann, wird beim Acquirieren des Locks nicht gewartet, bis der Lock
            akzeptiert wurde. Es wird <max_lock_tries> mal versucht, einen Lock
            zu bekommen. Zwischen den Versuchen wird jeweils 0.1 Sekunde gewartet.
            Konnte nach <max_lock_tries> Versuchen kein Lock acquiriert werden,
            dann wird cherrypy.TimeoutError() ausgelöst. Standard: 300 = ca. 30 sec.
        """

        dict.__init__(self)

        self.max_items_in_container = max_items_in_container
        self.max_minutes_in_container = max_minutes_in_container
        self.vacuum_interval_minutes = vacuum_interval_minutes
        self.max_lock_tries = max_lock_tries

        self._filenames_list = [] # Speichert die Ladereihenfolge
        self._lock = threading.Lock()

        #Vacuum-Timer starten
        self._last_vacuum_time = time.mktime(time.gmtime())
        self._vacuum_timer = threading.Timer(5, self._autovacuum)
        self._vacuum_timer.start()


    def _autovacuum(self):
        """
        Löscht Vorlagen aus den Listen und aus dem Dictionary falls diese
        älter als die angegebene Zeitspanne MAX_MINUTES_IN_CONTAINER ist.
        Es werden auch die ältesten Vorlagen aus den Listen und aus dem
        Dictionary gelöscht falls die Anzahl MAX_ITEMS_IN_CONTAINER
        überschritten wurde.

        Achtung!
        Diese Funktion wird alle 5 sec. ausgeführt. Wenn die eingestellte Zeit
        für das Vacuum aber noch nicht gekommen ist, dann wird die Ausführung
        abgebrochen.
        """

        try:
            # Prüfen ob die Zeit für das Vacuum gekommen ist
            if (
                self._last_vacuum_time + (self.vacuum_interval_minutes * 60.0) >
                time.mktime(time.gmtime())
            ):
                return

            # Wenn die Engine nicht mehr läuft, dann Vacuum stoppen
            if not cherrypy.engine.state:
                return

            # Ab jetzt wird mit einem Lock gesperrt
            self._lock.acquire(True)
            try:
                cherrypy.log.error("Autovacuum BEGIN")

                # Wenn MAX_ITEMS_IN_CONTAINER überschritten wurde, dann werden
                # die ältesten Vorlagen aus dem Dict und aus der Liste gelöscht.
                files_count = len(self._filenames_list)
                if files_count > self.max_items_in_container:
                    for i in range(files_count - self.max_items_in_container):
                        filename = self._filenames_list.pop()
                        dict.__delitem__(self, filename)
                        cherrypy.log.error("Autovacuum released the file: '%s'" % repr(filename))

                # Wenn MAX_MINUTES_IN_CONTAINER überschritten wurde, dann wird die
                # Vorlage aus dem Dict und aus der Liste gelöscht.
                now = time.mktime(time.gmtime())
                for filename in self.keys():
                    template = dict.__getitem__(self, filename)
                    if (template._loadtime + (self.max_minutes_in_container * 60.0)) < now:
                        try:
                            self._filenames_list.remove(filename)
                        except ValueError:
                            pass
                        dict.__delitem__(self, filename)
                        cherrypy.log.error("Autovacuum released the file: '%s'" % repr(filename))

                cherrypy.log.error("Autovacuum END")
            finally:
                self._lock.release()
                self._last_vacuum_time = time.mktime(time.gmtime())
                # Lock aufgehoben und Vacuumzeit aktualisiert
        finally:
            if cherrypy.engine.state:
                self._vacuum_timer = threading.Timer(5, self._autovacuum)
                self._vacuum_timer.start()
            else:
                self._vacuum_timer.cancel()
                cherrypy.log.error("Autovacuum stopped")


    def load_templatefile(self, filename):
        """
        Vorlage laden und mit den Attributen _mtime und _ctime versehen.
        Die Vorlage wird dabei in die Dateinamenliste und in das Dict eingetragen.

        :return: Gibt die geladene Vorlagegeninstanz zurück
        """

        template = Template(file = filename)
        mtime = os.path.getmtime(filename)

        # Interne MTime und CTime
        template._mtime = mtime
        template._loadtime = time.mktime(time.gmtime())

        # MTime-String und CTime-String für die Verwendung im HTML-Kopf
        template.mtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime))
        template.ctime = time.strftime(
            "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getctime(filename))
        )

        self._filenames_list.append(filename)
        dict.__setitem__(self, filename, template)

        return template


    def __getitem__(self, filename):
        """
        Wird aufgerufen, wenn ein Template aus dem Dict abgerufen wird.

        :param filename: Vollständiger Pfad zur Vorlagendatei
        """

        # Versucht einen nicht blockierenden Lock zu bekommen
        for i in xrange(self.max_lock_tries):
            if self._lock.acquire(False):
                break
            time.sleep(0.1)
        else:
            raise cherrypy.TimeoutError()
        try:
            if filename in self:
                template = dict.__getitem__(self, filename)
                # Prüfen ob sich das Template im Dateisystem geändert hat
                if template._mtime == os.path.getmtime(filename):
                    return template
                else:
                    # Alten Dateinamen aus Liste entfernen, neu laden und
                    # zurück geben
                    try:
                        self._filenames_list.remove(filename)
                    except ValueError:
                        pass
                    return self.load_templatefile(filename)
            else:
                # Template laden, in dict eintragen und zurück geben
                return self.load_templatefile(filename)
        finally:
            self._lock.release()


    def __setitem__(self, *args, **kwargs):
        """
        Das direkte Zuweisen von Einträgen ist nicht erlaubt
        """

        raise RuntimeError("Direct asigning not allowed!")


class Root(object):

    def __init__(self, max_lock_tries = 300):
        """
        Root initialisieren

        :param max_lock_tries: Vorlagen, die in der "default"-Methode automatisch
            abgearbeitet werden, sollen nicht gleichzeitig von mehreren Threads
            abgearbeitet werden. Um die Threads die Vorlagen hintereinander
            abarbeiten zu lassen, wird ein Lock acquiriert.
            Damit es nicht zu einem kompletten Blockieren der Auslieferung kommen
            kann, wird beim Acquirieren des Locks nicht gewartet, bis der Lock
            akzeptiert wurde. Es wird <max_lock_tries> mal versucht, einen Lock
            zu bekommen. Zwischen den Versuchen wird jeweils 0.1 Sekunde gewartet.
            Konnte nach <max_lock_tries> Versuchen kein Lock acquiriert werden,
            dann wird cherrypy.TimeoutError() ausgelöst. Standard: 300 = ca. 30 sec.
        """

        # In diesem Container werden alle Cheetah-Vorlagen verwaltet, die
        # automatisch geladen wurden.
        self.templatecontainer = CheetahTemplateContainer()

        # Vorlagen sollten *hintereinander* befüllt werden. Das wird mit dieser
        # Sperre realisiert.
        self.max_lock_tries = max_lock_tries
        self._template_lock = threading.Lock()


    def default(self, *args, **kwargs):
        """
        Standard-Handler für Objekte, die keiner anderen Handler-Funktion
        zugewiesen werden konnten.

        TMPL-Dateien werden hier auch ohne zugewiesener Funktion abgearbeitet
        und ausgeliefert.
        """

        filename = os.path.join(APPDIR, *args)

        if os.path.isdir(filename):
            # Handelt es sich um die index.tmpl?
            filename = os.path.join(filename, "index.tmpl")
            if os.path.isfile(filename):
                # Fehlenden Slash anhängen
                request = cherrypy.request
                path_info = request.path_info
                if not path_info.endswith('/'):
                    new_url = cherrypy.url(path_info + '/', request.query_string)
                    raise cherrypy.HTTPRedirect(new_url)
            else:
                raise cherrypy.NotFound()
        else:
            # Benannte Vorlage
            if filename.lower().endswith(".tmpl"):
                if not os.path.isfile(filename):
                    raise cherrypy.NotFound()
            else:
                raise cherrypy.NotFound()

        # Versucht einen nicht blockierenden Lock zu bekommen
        for i in xrange(self.max_lock_tries):
            if self._template_lock.acquire(False):
                break
            time.sleep(0.1)
        else:
            raise cherrypy.TimeoutError()
        try:
            # Vorlage holen
            template = self.templatecontainer[filename]

            # Request und Response zur Vorlage hinzu fügen
            template.request = cherrypy.request
            template.response = cherrypy.response

            # Vorlage rendern und zurück geben
            return str(template)
        finally:
            # Sperre aufheben
            self._template_lock.release()

    default.exposed = True


def main():
    cherrypy.quickstart(Root(), config = INI_FILENAME)

if __name__ == "__main__":
    main()

Der Code ist zwar gut kommentiert, aber er ist nicht einfach zu verstehen. Die Klasse "CheetahTemplateContainer" stellt ein verändertes Dictionary dar. In diesem Dictionary werden die Vorlagen verwaltet. Sie werden darin geladen und mit dem Ladezeitpunkt gekennzeichnet. Das ist alles notwendig, damit die Funktion "_autovacuum" alle paar Minuten durch die geladenen Vorlagen fegen kann um die ältesten Vorlagen wieder aus dem Container raus zu schmeißen.

Näher möchte ich nicht auf das Beispiel eingehen. Es ist Threading im Spiel und ist für Anfänger evt. nicht so leicht zu verstehen. Aber keine Sorge, das kommt mit der Zeit. ;-)