/ couchdb

Lokaler NPM-Registry Mirror mit regelmäßiger Synchronisation

Im Rahmen der Entwicklung eines unserer Frontends verwenden wir node.js. Node.js erlaubt Paketmanagement mit Hilfe des Node Package Managers (NPM), der ähnlich wie Maven eine global verfügbare Registry verwendet.

Einrichten einer lokalen NPM-Registry

Die NPM-Registry wird auf einem nicht immer stabil erreichbarem Host bereitgestellt. Um die Latenz beim Abruf der aktuellen Paketinformationen zu vermeiden und um Downtimes möglichst gut kompensieren zu können, kann man einen Mirror der NPM-Registry im lokalen Netz bereitstellen.

Für einen NPM-Registry Mirror benötigt man lediglich eine CouchDB und ausreichend Festplattenspeicher. Da die NPM-Registry selbst komplett innerhalb einer CouchDB läuft, werden die Replikation und Synchronisation durch die von der CouchDB bereitgestellten Features stark vereinfacht. Das initiale Einrichten und die Verwendung eines Mirrors wird sehr ausführlich in einem Eintrag des clock blog beschrieben, darüber hinaus gibt es schon Lösungen, das Setup weitgehend zu automatisieren. Die genannten Blog-Einträge sollen hier nicht wiederholt werden, unsere Anforderung war eine möglichst bequeme regelmäßige Synchronisation per Cron-Job.

Synchronisation mit der offiziellen NPM-Registry

Nach dem initialen Setup hat man die Option, eine fortlaufende Synchronisation einzurichten oder in regelmäßigen Abständen die neuesten Updates zu erhalten. Um das Netzwerk nicht durchgehend zu belasten und da uns tagesaktuelle Node-Module ausreichen, haben wir uns für die zweite Option entschieden und nach einer einfachen Möglichkeit gesucht, dies per Job zu automatisieren. Die CouchDB unterstützt uns dabei ab Version 1.1.0 mit einer dedizierten Replication Datenbank inklusive einer persistenten Beschreibung von Quell- und Ziel-Datenbank. Details dazu befinden sich im CouchDB Wiki bzw. dem dort referenzierten Gist. Im Folgenden wird für unseren Anwendungsfall beschrieben, wie die Replicator Datenbank konfiguriert wird und wie man per Job die Synchronisation starten kann.

In der Replicator Datenbank legt man folgendes Dokument an:

{  
  "_id": "npm_registry_replication",  
  "source": "http://isaacs.iriscouch.com/registry/",  
  "target": "npm_registry",  
  "owner": "adminuser",  
  "user_ctx": {  
    "name": "adminuser",  
    "roles": ["_admin"]  
  }  
}

Inhaltlich definiert dieses Dokument, dass vom offiziellen NPM-Mirror gelesen und in die lokale Datenbank namens „npm_registry“ geschrieben werden soll. Falls es einen in der CouchDB definierten Administrator gibt, ist dieser wie im Beispiel ebenfalls zu berücksichtigen. Die Replikation läuft dann im Kontext des Administrators.

Regelmäßiges Triggern der Synchronisation

Nachdem man dieses Dokument angelegt hat, sollte die Replikation auch schon automatisch gestartet sein. Der Replicator schreibt in das von uns angelegte Dokument und dokumentiert den aktuellen Stand der Synchronisation. So kann man erkennen, ob die Synchronisation noch läuft, ein Fehler aufgetreten oder ob sie erfolgreich abgeschlossen ist. Dazu wird, falls noch nicht vorhanden, ein Attribut _replication_state ergänzt, das folgende Werte annehmen kann:

  • triggered
  • completed
  • error

Dieses Attribut kann auch dazu genutzt werden, eine noch nicht laufende Synchronisation zu starten. Dazu kann man durch ein Update des Dokuments den Wert _replication_state="triggered" eintragen.

Die CouchDB pflegt innerhalb des von uns angelegten Dokuments ein Attribut _rev, das bei Update-Request für dieses Dokument immer übergeben werden muss. So stellt die CouchDB sicher, dass man auf der aktuellsten Version eines Dokuments Änderungen vornimmt. Wenn nun neuer _replication_state gesetzt werden soll, muss vorher die aktuelle Revision des Dokuments ermittelt und später mit dem Update-Request mitgeschickt werden. Das Parsen und Ändern eines JSON Dokuments ist innerhalb eines Cron-Jobs unbequem, daher haben wir ein anderes CouchDB-Feature genutzt: die CouchDB bietet sogenannte Document Update Handler, die auch ein In-Place Update eines Dokuments ermöglichen, ohne dieses vollständig per POST/PUT übertragen zu müssen.

Update Handler werden im Design Dokument einer CouchDB Datenbank gepflegt. Die Design Dokumente nehmen eine Sonderrolle ein, denn sie dienen einer optionalen Validierung, enthalten Applikationscode und andere Aspekte, die nicht direkt in den Daten einer Datenbank liegen können.

Wir haben im Design Dokument der Replication Datenbank unter /_replicator/_design/_replicator ein neues Attribut updates mit folgendem Inhalt eingefügt:

{  
  ..  
  "updates": {  
    "trigger-replication": "function(doc, req) {  
      if (doc._replication_state == "completed")  
        doc._replication_state = "triggered";  
      return [doc, toJSON(doc)];  
    }"  
  }  
  ..  
}

Diese Funktion setzt innerhalb des per Parameters übergebenen Dokuments den _replication_state auf „triggered“, falls aktuell keine Replikation läuft und die letzte Replikation fehlerfrei beendet wurde.

Achtung: Wir haben für das Ergänzen der „trigger-replication“ Funktion das CouchDB Admintool Futon verwendet, mit dessen Hilfe man Dokumente bequem editieren kann. In der unter „Fields“ möglichen Ansicht eines Dokuments wird das oben gezeigte Snippet leider als String eingefügt, was später bei Verwendung des Update-Handlers zu folgender Fehlermeldung führt: <br></br> {"error":"not_found","reason":"missing updates function trigger-replication on design doc _design/_replicator"}<br></br>
Empfehlenswert ist daher, entweder die „Source“-Ansicht in Futon auszuwählen oder direkt per Terminal Änderungen am Design Dokument vorzunehmen.

Um nun die Update-Funktion innerhalb eines täglichen Cron-Jobs zu verwenden reicht es unter Ubuntu, folgendes Script unter /etc/cron.daily als ausführbare Datei zu speichern:

#!/bin/bash  
HOST="http://user:password@couchdb.example.com:5984"  
 DESIGN_DOCUMENT_URL="${HOST}/_replicator/_design/_replicator"  
UPDATE_HANDLER="_update/trigger-replication/npm_registry_replication"

curl -X PUT "${DESIGN_DOCUMENT_URL}/${UPDATE_HANDLER}"

Fazit

Das Setup eines NPM-Mirrors funktionierte ohne Probleme. Wenn man nicht auf eine ständige Replikation setzen möchte, bietet die CouchDB mit den Update Handlern eine bequeme Methode nur bei Bedarf eine Synchronisierung zu starten. Falls die per Terminal/curl aufzurufende URL zu komplex ist, lässt sich ebenfalls mit CouchDB Features ein Rewrite Handler konfigurieren.