SimpleCacheStore im Zusammenspiel mit einer Serveranfrage

SimpleCacheStore: Eigenständiges Storage Framework für Offlinebetrieb

09. Feb. 2017 // Moritz Kanzler //

Als Teil meines Studiums arbeite ich an einer Social Networking App, welche naturgemäß dauerhaft auf Daten meiner Server angewiesen ist. Im Zuge der Weiterentwicklung gefiel es mir aber nicht, dass bei einem Netzwerkausfall oder sehr langsamen Verbindungen die App überhaupt nicht zu gebrauchen ist.

Teile der App benötigen ständigen Internetzugriff um „live“ Änderungen anzuzeigen. Ein anderer Teil allerdings, dazu gehören Freundeslisten,  Nachrichtenverläufe und ähnliches sollten aber auch Offline benutzbar sein. Dazu überlegte ich mir eine grundsätzliche Lösung, welche nahtlos in die jetzige Infrastruktur eingreifen sollte: ein persistenter Datenspeicher, welcher einmal vom Server geladene Objekte abspeichert um diese wieder zu verwenden.

Darüber hinaus sollte er auch noch weitere Anforderungen mitbringen welche im Folgenden aufgelistet werden.

Anforderungen

  • persistenter Datenspeicher für Objektstrukturen innerhalb der App
  • Erreichbarkeit der Objekte über einen eindeutigen Schlüssel
  • Zwischenspeichern der eingelesenen Objekte in einem Cache um schnellere Antworten zu ermöglichen
  • Einbettung dieses Systems in ein eigenes Framework zur Nutzung in verschiedenen Szenarien
  • Abrufen von Gruppen von Objekten über sekundäre Schlüssel

Ziel sollte also sein, eine Art Key-Value Storage zu bauen, welcher Objekte wiederverwendbar auf dem Gerät ablegt, um Offline auf diese zuzugreifen und zusätzlich auch schnelle Antwortzeiten während der Benutzung der App ermöglicht.

Warum? Bei langsamen Internetverbindungen eröffnet das die Möglichkeit, schon vorhandene Daten aus dem Cache vorzuladen, bis die (eventuell) aktuelleren Daten vom Server vorhanden sind. Somit hat der Nutzer auch bei langsamen Verbindungen das Gefühl einer gut reagierenden App.

Szenario

Um das Szenario in welchem dieses Storage System eingesetzt wird, charakterisiere ich hier noch einmal ein paar Ausgangspunkte, welche bei der Implementierung der Lösung vorausgesetzt werden sollten:

  1. Daten vom Server werden immer als die aktuellsten Informationen angesehen (auch wenn sie es nicht sind). Deshalb sollte auch bei vorhandenem Objekt im Storage immer eine Serveranfrage gestartet werden, welche das Objekt bei erhalt im Speicher überschreibt.
  2. Der Entwickler ist dafür verantwortlich, die gespeicherte Datenmenge auf dem Gerät im Auge zu behalten. Dabei muss klar sein, dass die gespeicherten Daten vorhanden sein können, aber nicht zwingend müssen. Außerdem sollte er sich grundsätzlich überlegen welche Daten gespeichert bleiben sollen und wann er unnötig gewordene Datenbestände bereinigt.

SimpleCacheStore

Als Lösung für die obigen Anforderungen ist das Framework SimpleCacheStore entstanden. Das Projekt ist auf GitHub zu finden. Im weiteren Verlauf beschreibe ich die Entwicklung und die Ideen hinter SimpleCacheStore. Jeder ist herzlich dazu eingeladen es zu testen oder auch produktiv einzusetzen.

Ich beschreibe den Aufbau von SimpleCacheStore hier mit dem Hintergrund der eingesetzen App. Diese lädt bisher alle Daten von einem Server. Durch den Einsatz des Storages sollen im Normalbetrieb bereits geladen Daten präsentiert werden, bis die Serverdaten vollständig geladen werden, um eine flüssigere GUI zu ermöglichen. Außerdem soll der Storage auch als Ersatz dienen, falls keine Internetverbindung besteht. So werden zwar unter Umständen alte Daten gezeigt, aber die App ist weiterhin benutzbar (eventual consistency).

Ablauf

Wie schon oben beschrieben versucht die App grundsätzlich die Daten von einem Server zu bevorzugen. Wird ein Datensatz angefragt, hier mit getUser(x), werden Anfragen sowohl an den Server als auch an SimpleCacheStore (im weiteren SCS abgekürzt) gestellt. Dabei können verschiedene Szenarien eintreten:

SCSServerSzenario
xxWird zunächst ein SCS Objekt geliefert, verarbeitet die App dieses. Sobald auch die Daten des Servers angekommen sind, wird eine erneute Verarbeitung angestoßen
xDer Server ist nicht erreichbar, ein entsprechendes Objekt in SCS aber vorhanden. Darauf hin wird das SCS Objekt verarbeitet. Keine Internetverbindung
xDie Anfrage an SCS ergibt, dass das Objekt nicht im Storage vorhanden ist. Der Server liefert dieses aber. Dann werden zunächst die Serverdaten verarbeitet und zusätzlich die gewonnen Daten als Objekt in SCS abgelegt. SCS Cache/Storage miss
Die App erkennt, das die gesuchten Daten schlicht nicht vorhanden sind, und sollte entsprechende Fehlermeldungen ausgeben.
Sonderfall: Der Server antwortet, gibt aber zurück, dass die gesuchten Daten nicht mehr vorhanden sind. In diesem Fall muss das entsprechende Objekt aus dem Cache gelöscht werden und die App sollte mit einer Fehlermeldung reagieren.

 

Der Ablauf einer Abfrage ist in der unteren Abbildung noch einmal grafisch dargestellt. Wichtig ist zu erkennen, dass SimpleCacheStore zwar die Speicherung, Löschung und das Suchen nach Objekten unterstützt, die Reaktion z.B. anhand der obigen Tabelle vom Entwickler selbst implementiert werden muss. Vorteil daran ist, dass SCS auch in anderen Szenarien Einsatz finden kann. So zum Beispiel als schnelles Overlay für Offline Anwendungen, welche normalerweise direkt auf CoreData zugreifen, oder in Verbindung mit weiteren Abfragemechanismen.

 

Verarbeitung einer getUser(x)-Anfrage an einen klassischen Server mit zusätzlichem Nutzen von SimpleCacheStore

Konzept & Komponenten

SimpleCacheStore setzt sich aus drei größeren Teilen zusammen.
Von außen instanzierbar ist der SCManager welcher das Speichern, Löschen und Holen von Objekten ermöglicht.
Darunter liegend werden die Daten persistent in CoreData abgelegt sowie in NSCache zwischengespeichert. Das ermöglicht eine schnellere Antwortzeit auf Anfragen und stellt einen Vorteil zu einer direkten Implementierung von CoreData in Projekten dar.

Der SCManager verwaltet das Zusammenspiel aus CoreData und NSCache innerhalb des Systems.

Objekte in CoreData

Die persistente Datenspeicherung in SimpleCacheStore basiert auf CoreData. Dort werden alle zu speichernden Objekte mit einem eindeutige Identifier (String), einem Erstellungsfeld (NSDate) und einem freien Binary-Feld abgelegt. Damit SCS Objekte in dieses Binary-Feld legen kann, müssen die Objekte das Interface NSCoding unterstützen. Zur genauen Implementierung gibt es später ein Beispiel. Außerdem ist darauf zu achten die Objekte beim Auslesen aus SCS wieder in den richtigen Objekttyp zu typecasten. Noch einmal die Aufführung des CoreData Objektes zur Speicherung der Werte:

FeldnameDatentypFunktion
identifierStringEindeutiger Schlüssel zum referenzieren von Objekten. Key
dataDataObjektdaten die gespeichert werden. Value
createdDateErstellungsdatum zur Verwaltung.
labelStringSekundäre Index für Rangequeries
lastUpdateDateZuletzt aktualisiert
requestedInt64Anzahl der Aufrufe des Objektes aus Cache und CoreData

Objekte im Cache

Während der Laufzeit einer SimpleCacheStore-Instanz wird zu den CoreData Objekten auch noch ein Cache für schnellere Antwortzeiten betrieben. Für den Cache wird die Klasse NSCache benutzt, welche eine Speicherung von beliebigen Typen über einen eindeutigen Schlüssel ermöglicht. Zusätzlich kann NSCache ein Limit-Parameter übergeben werden. Durch diese Objektobergrenze kann gesteuert werden, wie viele Daten im Arbeitsspeicher während eines Prozesses liegen sollen. Diesen Mechanismus nutzt auch SCS. Wichtig ist für die Performance hierbei vorallem die richtige Wahl der Obergrenze, dazu aber später mehr.

Zusammenspiel von Cache und CoreData

Abgesehen vom Startprozess, schreibt SCS Objekte in drei Fällen in den Cache:

  1. Ein Objekt wird angefragt und befindet sich zwar in CoreData, aber nicht im Cache. Dann wird dieses Objekt aus CoreData geliefert und zusätzlich im Cache abgelegt.
  2. Ein Objekt wird zu SCS hinzugefügt oder geändert. Dann wird im gleichen Zug auch das Objekt im Cache aktualisiert bzw. hinzugefügt.
  3. Ein Objekt wird aus SCS gelöscht. Dann wird es entsprechend auch aus dem Cache genommen.

Mit diesem Verfahren kann sichergestellt werden, dass immer die aktuellsten Objektdaten im Cache vorhanden sind. Zusätzlich wird der Cache auch darauf hin optimiert möglichst häufig gefragte Objekte im Cache vorzuhalten.
Wie zu erahnen ist, wird bei jeder Anfrage immer zunächst im Cache nach dem Objekt gesucht und danach erst CoreData bemüht um eine schnellere Ausführung zu ermöglichen, dazu zwei Beispiele für get und save. Die grüne Linie zeigt immer den ersten Schritt an, die blaue den Zweiten. Zu erkennen ist, dass Änderungen immer zunächst in den CoreData Speicher geschrieben werden, sodass dieser immer den aktuellsten Stand des Objektes enthält. Danach erst wird der Cache enstprechend angepasst.

SimpleCacheStore: CoreData und NSCache Verhalten bei einer get AnfrageSimpleCacheStore: CoreData und NSCache Verhalten bei einer get Anfrage

SCManager

Damit der Entwickler sich nicht um die Aufteilung der Objekte in Cache und CoreData kümmern muss, instanziert er lediglich die Klasse SCManager zum Betrieb von SimpleCacheStore. Dieser bietet Methoden zum Speichern, Anfordern und Löschen von Objekten. Die Arbeit auf Cache und CoreData erfolgt intern. Somit kennen sich der Cache- und CoreData-Handler nicht, sondern werden auch innerhalb von SCS vom SCManager bedient.

Der SCManager lässt alle Methoden welche in Core Data schreiben in einer eigenen Concurrency Queue ablaufen. Dazu werden die Abfragen in private MOCs gemacht und bei Beendigung zusammen geführt. Somit gibt es auch bei nebenläufigen Operationen keine Dead Locks. Die save und delete Methode läuft synchron ab, das System wartet also auf die Durchführung und lässt keine anderen Operationen zu. Die get Methoden, egal ob für alle Datensätze, Datensätze nach Bereichen bzw. Labeln oder Einzelabfragen laufen jedoch asynchron ab, da gerade Bereichsabfragen länger dauern können. Die Ergebnisse werden in einem Callback zurückgegeben.

Eine umfassende Dokumentation über die API Methoden des SCManagers finden sich in der Dokumentation auf Github.

Sekundäre Indizes und Bereichsabfragen

In einem praktischen Szenario reicht es oft nicht einfache Objekte einfach in den Speicher zu legen und diese abrufen zu können. Oftmals ist es nötig auch Gruppen von Objekten definieren zu können. Im Bezug auf die von mir geschriebene Social Networking App war das besonders im Bezug auf Personengruppen wie Freunde nötig. Natürlich könnte einfach eine weitere Datenstruktur mit den verweisenden Indizes auf die eindeutigen Schlüssel der Objekte im Speicher gesichert werden, das ist aber nur eine Notlösung und verbraucht zusätzlich Ressourcen.

Um dem entgegen zu wirken gibt es im SimpleCacheStore zusätzlich für jedes gespeicherte Objekt einen weiteren Index der gesetzt werden kann, das ist aber optional. Dieses sogenannte Label kann dann für spätere Bereichsabfragen in einer Art der get Methode genutzt werden um eine Gruppe von Objekten zurückzuerhalten. Bisher ist nur ein zusätzlicher Index pro Objekt möglich. In Zukunft soll das aber über eine 1:n Beziehung auf beliebig viele sekundäre Schlüssel erweitert werden können.