Gecachte Dateien im Server-Dateisystem des jadice web toolkit

Problemstellung

Im Dateisystem des jadice web toolkit Servers sammeln sich häufig temporäre Dateien an. Diese sollen so früh und umfassend wie möglich aufgeräumt werden.

Die Dateien haben in der Regel folgenden Ursprung:

  • Das Laden von Dokumenten im jadice web toolkit erfolgt in kundenspezifischen DocumentDataProvidern. Oft werden dabei Dateien aus einem Archiv ins Filesystem des JWT Servers heruntergeladen und von dort für die Anzeige im Browser eingelesen. Diese Dateien sollen nach der Bearbeitung eines Dokuments aus dem Filesystem des Servers gelöscht werden. Das Ende der Bearbeitung ist allerdings serverseitig nicht zuverlässig erkennbar. Beispielsweise erhält der Server beim Schließen des Browsertabs keine Benachrichtigung darüber, dass ein Dokument nicht mehr benötigt wird.
  • Gecachte InputStreams der jadice document platform: Vom Browser wird die aktuell angezeigte Seite sowie benachbarte Seiten angefordert. Falls der Benutzer einen Dokumentbereich erst zu einem späteren Zeitpunkt betrachtet, muss die aus dem Archiv heruntergeladene Datei zu diesem späteren Zeitpunkt erneut gelesen werden. Daher wird der zugehörige InputStream serverseitig gecacht und nicht geschlossen. Typischerweise wird man hier FileCacheInputStreams verwenden, da ein Zwischenhalten der Dokumentdaten im Hauptspeicher diesen schnell ausschöpfen würde. Erfolgt über eine konfigurierbare Zeitspanne (im Standard 60 Minuten) kein Zugriff mehr auf ein Dokument, werden alle zugehörigen Elemente aus dem Cache entfernt. 

Intelligente Speicherverwaltung mit dem BufferManager 

Eine Möglichkeit, die einzelnen Dateien frühzeitig freizugeben bzw. überflüssig zu machen, stellt die Verwendung des BufferManagers dar. Damit werden Dokumente nach dem initialen Einlesen intelligent im Hauptspeicher und einer einzigen, vom BufferManager verwalteten temporären Datei gepuffert.

Der Buffer Manager stellt die folgenden Konfigurationsmöglichkeiten zur Verfügung:

  • Die Größe des Puffers im Hauptspeicher

Das Verhalten bei der Pufferung sieht folgendermaßen aus:

  • Falls es einen freien Bereich im Hauptspeicherpuffer gibt, wird dieser verwendet.
  • Falls der Hauptspeicherbereich keinen freien Platz besitzt, wird ein freier Bereich in der Pufferdatei verwendet.
  • Falls weder in Hauptspeicher noch in der Pufferdatei ein freier Bereich existiert, wird die Pufferdatei vergrößert.
  • Die Pufferdatei wird nicht verkleinert.
  • Durch das Wiederbeschreiben freigegebener Bereiche ist Größe der Pufferdatei beschränkt (abhängig vom Nutzungsverhalten).
  • Beim Neustart des Servers wird eine bestehende Pufferdatei gelöscht und eine neue Pufferdatei mit der Größe 0 angelegt.

Wichtig ist das Zusammenspiel des BufferManagers mit dem serverseitigen Cache im jadice web toolkit. Sobald im Cache keine Einträge mehr auf ein Dokument vorhanden sind (im Standard 60 Minuten nach dem letzten Zugriff), gibt der Buffer Manager den zugehörigen Pufferbereich zum Überschreiben frei.

Heruntergeladene Archivdateien können also direkt nach dem Einlesen gelöscht werden, sobald ihre Daten in der Obhut des BufferManager liegen.

Näheres zur Arbeitsweise des BufferManager siehe Dokumentation für Entwickler.

Step-by-step guide

Hinzufügen der Maven Dependency

<dependency>
	<groupId>com.levigo.jadice.documentplatform.core</groupId>
	<artifactId>jadice-buffer-manager</artifactId>
</dependency>

Aktivierung des Buffer Managers 

Allein das Hinzufügen der Maven Dependency führt bereits zur Aktivierung des BufferManagers mit vorkonfigurierten Werten. Sollen abweichende Einstellungen verwendet werden, können diese entweder über die Jadice.properties oder programmatisch gesetzt werden.

  • Konfigurativ über die Jadice.properties:

    bufferManager.enabled=true
    # bufferManager.bufferSize=65536
    # bufferManager.fileStoreStrategy=AUTO
    # bufferManager.fileStoreMode=PLAIN
    # bufferManager.bufferStoreFile=C:\\User\\Temp
    # The default is 80 buffers, i.e. ~5MB of memory buffers.
    # We increase this value since we want to be on the safe side for larger/multiple streams.
    # (2000 buffers = 125 MB, which may still be too low)
    bufferManager.memoryBuffers=2000
  • Alternativ: Programmatisch beim Starten des Servers (z.B. im ContextListener)

    ...
    import com.levigo.jadice.bm.BufferManagerConfigurer;
    import com.levigo.jadice.bm.internal.DefaultBufferManager;
    ...
     
    private void configureBufferManager() {
      try {
        // the following configuration sample is usually NOT REQUIRED, since an appropriate strategy is automatically detected
        // BufferManagerConfigurer.configure().memoryBuffers(2000).fileStoreStrategy(BufferManagerConfigurer.FileStoreStrategy.ASYNCHRONOUS_FILE_CHANNEL).install(); 
        BufferManagerConfigurer.configure().install();   // <-- mimimal configuration 
        DefaultBufferManager.setEnabled(true);           // <-- important! 
     } catch (JadiceException e) {
        e.printStackTrace();
     }
    }

Nutzung des Buffer Managers im DocumentDataProvider

Im DocumentDataProvider muss darauf geachtet werden, die InputsStreams komplett zu lesen. Hierzu kann man ganz einfach die IOUtils nutzen:

...
 
import com.levigo.jadice.document.io.IOUtils;
import com.levigo.jadice.document.io.SeekableInputStream;
 
...

@Override
public void read(Reader reader, MyArchiveSource source) throws JadiceException, IOException {
 
 // here the file download from the archive takes place; file is placed in the file system
 ...
  
 if (!new File(source.getPath()).exists())
    throw new IOException("Can't find Resource on Filesystem: " + source.getPath());

 try(FileInputStream fis = new FileInputStream(source.getPath())) {
    SeekableInputStream sis = IOUtils.wrap(fis);  // <-- important!
    IOUtils.drainSynchronously(sis);              // <-- important!
    reader.read(sis);
 } finally {
    // delete the file in the filesystem (only required when using a FileInputStream!)
    new File(source.getPath()).delete();
 }
}

@Override
public void recover(Reader reader, MyArchiveHandle handle) throws RecoverFailedException, JadiceException {
 // same as in read()...
 ...
}

Konfiguration der maximalen Verweildauer von Einträgen im serverseitigen Cache

Dies konfigurieren wir in der Datei Jadice.properties:

...

# Defines whether the MaximumAgeExpiryStrategy should be used or not. This
# expiry is only affected by the properties jadice.viewer.cache.maxAge and
# jadice.viewer.cache.maxAge.timeUnit. All other properties wont influence this
# strategy. This strategy expires all entries which are older than the defined
# age. Age means the time passed since their last cache access.
jadice.viewer.cache.maxAgeExpiryEnabled=true

# This sets the max age of cache entries. All cache entries which are older
# than the defined age are expired. The unit of the age is configured through
# jadice.viewer.cache.maxAge.timeUnit
jadice.viewer.cache.maxAge=60

# The TimeUnit for the max age of the cache entries. The string must match an
# enum value of java.util.concurrent.TimeUnit.
jadice.viewer.cache.maxAge.timeUnit=MINUTES

...