Caching Factory Design

9

Ich habe eine Fabrik class XFactory, die Objekte von erstellt class X. Instanzen von Xsind sehr groß, daher besteht der Hauptzweck der Factory darin, sie so transparent wie möglich für den Clientcode zwischenzuspeichern. Objekte von class Xsind unveränderlich, daher erscheint der folgende Code sinnvoll:

# module xfactory.py
import x
class XFactory:
  _registry = {}

  def get_x(self, arg1, arg2, use_cache = True):
    if use_cache:
      hash_id = hash((arg1, arg2))
      if hash_id in _registry:
        return _registry[hash_id]
    obj = x.X(arg1, arg2)
    _registry[hash_id] = obj
    return obj

# module x.py
class X:
  # ...

Ist es ein gutes Muster? (Ich weiß, dass es nicht das tatsächliche Fabrikmuster ist.) Gibt es etwas, das ich ändern sollte?

Jetzt stelle ich fest, dass ich manchmal XObjekte auf der Festplatte zwischenspeichern möchte . Ich werde es picklefür diesen Zweck verwenden und als Werte in _registryden Dateinamen der eingelegten Objekte speichern, anstatt auf die Objekte zu verweisen. Natürlich _registrymüsste selbst dauerhaft gespeichert werden (möglicherweise in einer eigenen Pickle-Datei, in einer Textdatei, in einer Datenbank oder einfach, indem Pickle-Dateien die darin enthaltenen Dateinamen zugewiesen werden hash_id).

Außer jetzt hängt die Gültigkeit des zwischengespeicherten Objekts nicht nur von den übergebenen Parametern ab get_x(), sondern auch von der Version des Codes, der diese Objekte erstellt hat.

Genau genommen kann sogar ein im Speicher zwischengespeichertes Objekt ungültig werden, wenn jemand Änderungen x.pyoder Abhängigkeiten vornimmt und es neu lädt, während das Programm ausgeführt wird. Bisher habe ich diese Gefahr ignoriert, da sie für meine Bewerbung unwahrscheinlich erscheint. Aber ich kann es sicherlich nicht ignorieren, wenn meine Objekte in einem dauerhaften Speicher zwischengespeichert werden.

Was kann ich machen? Ich glaube , ich könnte das machen hash_idrobusteres durch Hash eines Tupels Berechnung , die Argumente enthält arg1und arg2, sowie der Dateiname und Datum der letzten Änderung für x.pyund jedes Modul und Datendatei , dass es (rekursiv) abhängt. Um das Löschen von Cache-Dateien zu _registryerleichtern , die nie wieder nützlich sein werden, würde ich die unverhüllte Darstellung der Änderungsdaten für jeden Datensatz ergänzen .

Aber selbst diese Lösung ist nicht 100% sicher, da theoretisch jemand ein Modul dynamisch laden könnte, und ich würde es nicht wissen, wenn ich den Quellcode statisch analysiere. Wenn ich alles daran setze und davon ausgehe, dass jede Datei im Projekt eine Abhängigkeit ist, wird der Mechanismus immer noch unterbrochen, wenn ein Modul Daten von einer externen Website abruft usw.).

Darüber hinaus ist die Häufigkeit von Änderungen x.pyund deren Abhängigkeiten recht hoch, was zu einer starken Ungültigmachung des Caches führt.

Daher dachte ich, ich könnte genauso gut auf etwas Sicherheit verzichten und den Cache nur dann ungültig machen, wenn eine offensichtliche Nichtübereinstimmung vorliegt. Dies bedeutet, class Xdass eine Cache-Validierungskennung auf Klassenebene vorhanden ist, die geändert werden sollte, wenn der Entwickler glaubt, dass eine Änderung stattgefunden hat, die den Cache ungültig machen sollte. (Bei mehreren Entwicklern ist für jeden eine separate Ungültigkeits-ID erforderlich.) Diese ID wird zusammen mit arg1und gehasht arg2und wird Teil der in gespeicherten Hash-Schlüssel _registry.

Da Entwickler möglicherweise vergessen, die Validierungskennung zu aktualisieren, oder nicht feststellen, dass sie den vorhandenen Cache ungültig gemacht haben, ist es besser, einen weiteren Validierungsmechanismus hinzuzufügen: Sie class Xkönnen eine Methode verwenden, die alle bekannten "Merkmale" von zurückgibt X. Wenn es sich beispielsweise Xum eine Tabelle handelt, kann ich die Namen aller Spalten hinzufügen. Die Hash-Berechnung enthält auch die Merkmale.

Ich kann diesen Code schreiben, habe aber Angst, dass mir etwas Wichtiges fehlt. und ich frage mich auch, ob es vielleicht ein Framework oder Paket gibt, das all diese Dinge bereits kann. Idealerweise möchte ich In-Memory- und festplattenbasiertes Caching kombinieren.

BEARBEITEN:

Es scheint, dass meine Bedürfnisse durch ein Poolmuster gut bedient werden können. Bei weiteren Untersuchungen ist dies jedoch nicht der Fall. Ich dachte, ich würde die Unterschiede auflisten:

  1. Kann ein Objekt von mehreren Clients verwendet werden?

    • Pool: Nein, jedes Objekt muss ausgecheckt und dann eingecheckt werden, wenn es nicht mehr benötigt wird. Der genaue Mechanismus kann kompliziert sein.
    • XFactory: Ja. Objekte sind unveränderlich und können von unendlich vielen Clients gleichzeitig verwendet werden. Es ist nie erforderlich, eine zweite Kopie desselben Objekts zu erstellen.
  2. Muss die Poolgröße kontrolliert werden?

    • Pool: Oft ja. In diesem Fall kann die Strategie dafür recht kompliziert sein.
    • XFactory: Nein. Ein Objekt muss auf Anfrage an den Client geliefert werden. Wenn ein vorhandenes Objekt ungeeignet ist, muss ein neues erstellt werden.
  3. Sind alle Objekte frei austauschbar?

    • Pool: Ja, die Objekte sind normalerweise frei austauschbar (oder wenn nicht, ist es trivial zu überprüfen, welches Objekt der Client benötigt).
    • XFactory: Auf keinen Fall, und es ist sehr schwer herauszufinden, ob ein bestimmtes Objekt eine bestimmte Clientanforderung bearbeiten kann. Dies hängt davon ab, ob ein vorhandenes Objekt verfügbar ist, das mit (a) denselben Argumenten und (b) derselben Version des Quellcodes erstellt wurde. Teil (b) kann von XFactory nicht überprüft werden, daher wird der Client um Hilfe gebeten. Der Kunde erfüllt diese Verantwortung auf zwei Arten. Erstens kann der Client einen seiner mehreren festgelegten internen Versionszähler erhöhen (einen pro Entwickler). Dies kann zur Laufzeit nicht passieren. Nur ein Entwickler kann diese Zähler ändern, wenn er glaubt, dass die Änderung des Quellcodes vorhandene Objekte unbrauchbar macht. Zweitens gibt ein Client einige Invarianten zu den benötigten Objekten zurück, und XFactory überprüft, ob diese Invarianten nicht verletzt werden, bevor das Objekt an den Client gesendet wird. Wenn eine dieser Prüfungen fehlschlägt,
  4. Müssen die Auswirkungen auf die Leistung sorgfältig analysiert werden?

    • Pool: Ja, in einigen Fällen beeinträchtigt ein Pool tatsächlich die Leistung, wenn der Overhead der Objektverwaltung größer ist als der Overhead der Objekterstellung / -zerstörung.
    • XFactory: Nein. Die Berechnungskosten der betreffenden Objekte sind bekanntermaßen sehr hoch, und das Laden aus dem Speicher oder von der Festplatte ist zweifellos überlegen, als sie von Grund auf neu zu berechnen.
  5. Wann werden Gegenstände zerstört?

    • Pool: Wenn der Pool heruntergefahren wird. Möglicherweise werden Objekte auch zerstört, wenn Sie aufgefordert werden, Ressourcen (teilweise) freizugeben, oder wenn bestimmte Objekte längere Zeit nicht verwendet wurden.
    • XFactory: Immer wenn ein Objekt mit der Version des Quellcodes erstellt wurde, die nicht mehr aktuell ist, was entweder durch eine invariante Verletzung oder durch eine Nichtübereinstimmung des Zählers belegt wird. Das Auffinden und Zerstören solcher Objekte zum richtigen Zeitpunkt ist ziemlich kompliziert. Darüber hinaus kann eine zeitbasierte Ungültigmachung aller Objekte implementiert werden, um das akkumulierte Risiko der Verwendung ungültiger Objekte zu verringern. Da XFactory niemals sicher ist, dass es der alleinige Eigentümer eines Objekts ist, wird eine solche Ungültigmachung am besten durch einen zusätzlichen „Versionszähler“ in den Clientobjekten erreicht, der in regelmäßigen Abständen programmgesteuert inkrementiert wird und nicht von einem Entwickler.
  6. Welche besonderen Überlegungen gibt es für Multithread-Umgebungen?

    • Pool: Kollisionen beim Auschecken / Einchecken von Objekten müssen vermieden werden (Sie möchten kein Objekt an zwei Clients auschecken).
    • XFactory: Muss Kollisionen bei der Objekterstellung vermeiden (Sie möchten keine zwei Objekte basierend auf zwei identischen Anforderungen erstellen).
  7. Was ist zu tun, wenn der Client kein Objekt freigibt?

    • Pool: Möglicherweise möchten Sie das Objekt nach längerem Warten anderen zur Verfügung stellen.
    • XFactory: Nicht anwendbar. Clients benachrichtigen XFactory nicht darüber, wann sie mit dem Objekt fertig sind.
  8. Müssen Objekte geändert werden?

    • Pool: Muss möglicherweise auf den Standardzustand zurückgesetzt werden, bevor er wiederverwendet wird.
    • XFactory: Nein, die Objekte sind unveränderlich.
  9. Gibt es spezielle Überlegungen zur Persistenz von Objekten?

    • Pool: Normalerweise nicht. Bei einem Pool geht es darum, die Kosten für die Objekterstellung zu sparen, sodass alle Objekte im Speicher bleiben (das Lesen von der Festplatte würde den Zweck zunichte machen).
    • XFactory: Ja, bei XFactory geht es darum, die Kosten für die Durchführung komplexer Berechnungen zu sparen. Daher ist das Speichern vorberechneter Objekte auf der Festplatte sinnvoll. Infolgedessen muss sich XFactory mit den typischen Problemen der dauerhaften Speicherung befassen. Beispielsweise muss es bei der Initialisierung eine Verbindung zum dauerhaften Speicher herstellen, daraus die Metadaten abrufen, welche Objekte derzeit dort verfügbar sind, und bereit sein, sie auf Anfrage in den Speicher zu laden. Und das Objekt kann sich in einem von drei Zuständen befinden: "existiert nicht", "existiert auf der Festplatte", "existiert im Speicher". Während XFactory ausgeführt wird, kann sich der Status nur in eine Richtung ändern (in dieser Reihenfolge rechts).

Zusammenfassend ist die Komplexität des Pools in den Punkten 1, 2, 4, 6 und möglicherweise 5, 7, 8. Die XFactory-Komplexität ist in den Punkten 3, 6, 9 enthalten. Die einzige Überlappung ist Punkt 6 und es ist wirklich nicht der Kern Funktion von Pool oder XFactory, sondern eine Einschränkung des Designs, die allen Mustern gemeinsam ist, die in einer Multithread-Umgebung arbeiten müssen.

max
quelle
1
Dies ist definitiv keine Fabrik oder sogar in der Nähe. Bei Factory geht es um die Indirektion von Konstruktionen, mit denen ein konkreter Typ aus einer abstrakten Spezifikation erstellt werden kann. Dies ist ein Pool. Es ist an sich nicht schlecht, aber jetzt, da Sie wissen, dass es sich um einen Objektpool handelt, nach dem Sie suchen, würde ich vorschlagen, sich über bewährte Verfahren mit Pools zu informieren und nach Vorbehalten zu suchen, die die Menschen zu vermeiden gelernt haben und wie sie vermeiden können, die Probleme, die sie haben, erneut zu implementieren. d litt. Beginnen Sie hier: en.wikipedia.org/wiki/Object_pool_pattern
Jimmy Hoffa
1
Vielen Dank. Ich fand diese Lektüre sehr nützlich, aber was ich brauche, ist nicht ganz das Poolmuster. Ich habe meine Frage bearbeitet, um zu zeigen, warum.
max

Antworten:

4

Ihre Bedenken sind sehr berechtigt und sie sagen mir, dass Ihre ursprüngliche einfache Caching-Lösung schließlich Teil Ihrer Architektur wird, was natürlich eine neue Ebene von Problemen mit sich bringt, wie Sie selbst beschrieben haben.

Eine gute architektonische Lösung für das Caching besteht darin, Anmerkungen in Kombination mit IoC zu verwenden, um mehrere von Ihnen beschriebene Probleme zu lösen. Zum Beispiel:

  • Ermöglichen es Ihnen, den Lebenszyklus Ihrer zwischengespeicherten Objekte besser zu steuern
  • Ermöglichen es Ihnen, das Caching-Verhalten einfach durch Ändern der Anmerkungen zu ersetzen (anstatt die Implementierung zu ändern).
  • Sie können ganz einfach einen mehrschichtigen Cache konfigurieren, in dem Sie beispielsweise im Speicher und dann im Festplatten-Cache speichern können
  • Sie können den Schlüssel (Hash) für jede Methode in der Anmerkung selbst definieren

In meinen Projekten (Java oder C #) verwende ich Spring-Caching-Annotationen. Eine kurze Beschreibung finden Sie hier .

IoC ist ein Schlüsselkonzept in dieser Lösung, da Sie damit Ihr Caching-System beliebig konfigurieren können.

Um eine ähnliche Lösung in Python zu implementieren, müssen Sie herausfinden, wie Sie Anmerkungen verwenden und nach einem IoC-Container suchen, mit dem Sie Proxys erstellen können. So funktionieren die Annotationen, um alle Methodenaufrufe abzufangen und Ihnen diese spezielle Lösung für das Caching bereitzustellen.

Alex
quelle
Danke, ich habe bis jetzt noch nie von IoC gehört und es scheint sowohl interessant als auch relevant zu sein. Es scheint einige gute Beispiele für IoC in Python zu geben.
Max
@max IoC ist wahrscheinlich keine so große Sache. Um jedoch ein gutes Caching-Framework zu haben, müssen Sie einen Weg finden, Methodenaufrufe abzufangen (normalerweise mit Auto-Proxys) und dann mithilfe von Anmerkungen das gewünschte Caching-Verhalten zu implementieren. Viel Glück!
Alex
1

So wie ich das sehe, ist der Cache in Ordnung - X nicht.

IMHO-De-Serialisierung einzelner Instanzen sollte kein Problem des Cache sein. Es ist eine Aufgabe für die entsprechende Klasse. Das Hauptproblem hierbei ist, dass sich diese Klasse häufig ändert. Ich schlage vor, das Problem des Zwischenspeicherns von Instanzen und das Problem der De-Serialisierung des Objekts zu trennen. Letzteres muss verbessert werden, damit X auch ältere Formate de-serialisieren kann. Dies kann sehr schwierig und teuer sein. Wenn es zu teuer ist, müssen Sie sich fragen, ob Sie wirklich alte Versionen laden müssen, solange sich X häufig ändert.

Übrigens scheint eine Versionskennung obligatorisch. Ohne weitere Kenntnis der Struktur von XI können nur einige Vermutungen angestellt werden, aber die Struktur von X scheint logisch modular zu sein (z. B. haben Sie von Merkmalen gesprochen). Wenn ja, wäre es vielleicht hilfreich, diese Struktur explizit zu machen.

Scarfridge
quelle