Eine Datenbank auf der Grundlage des verteilten Versionskontrollsystems Git merkt sich mit Hilfe eines Perl-Skripts Bestellungen aus dem Internet und hilft so den Überblick zu wahren. Bei Wareneingang aktualisiert der Käufer seine Lagerbestände.
Wer wie ich oft und gern günstige Produkte im Internet bestellt, ist womöglich zuweilen unsicher, ob das Bestellte drei Tage später tatsächlich ankommt. Im Kaufrausch verliert der Schnäppchenjäger dann den Überblick. Was liegt also näher, als abgeschickte Bestellungen in eine Datenbank aufzunehmen und diese dann bei Wareneingang aufzufrischen? Die Datenbank sollte freilich immer dort verfügbar sein, wo der Kaufwütige seinem Bestellwahn erliegt.
Das aber kann überall sein: Im Büro, zu Hause oder vielleicht per Laptop aus einem billigen Motelzimmer (Abbildung 1). Dort gibt es womöglich gar keinen Netzanschluss und der Käufer, sollen ihn nicht seine Erwerbungen überrollen, muss die Datenbank zunächst lokal auf dem Laptop pflegen, um sie erst später via Internet zu aktualisieren.

Abbildung 1: Das zentrale Git-Repository auf einem gehosteten Server dient als Austauschpunkt zwischen den einzelnen lokalen Repositories zu Hause, in der Arbeit und unterwegs.
Für eine solche Datenbank bietet sich das verteilte Versionskontrollsystem Git [2] an. Von Kernel-Zampano Linus Torvalds ursprünglich eigenhändig in zwei Wochen zusammengeklopft, um das proprietäre Produkt Bitkeeper abzulösen, verwaltet Git heutzutage den Linux-Kernel und kann blitzschnell Tausende von Dateien patchen und mergen. Bei der hier anstehenden Aufgabe ist die Geschwindigkeit jedoch eher irrelevant. Praktisch ist nur, dass Git mit seinem eingebauten Replikationsmechanismus ohne große Klimmzüge mehrere verteilte Verzeichnisse synchronisieren kann.
Immer alles am Mann
Informationen über Internetkäufe und deren geschätzte Liefertermine liegen dabei in einem Cache auf der lokalen Festplatte. Das dafür verwendete Modul legt für jeden Zugriffsschlüssel eine eigene Datei an. Das entsprechende Verzeichnis versioniert Git in einem lokalen Repository, wie es für ein verteiltes Versionskontrollsystem typisch ist. Entwickler haben so auch ohne Internetzugang und ohne zentrale Repositories zu stören vollen Zugriff auf alle Funktionen. Sie können neue Versionen einchecken, alte hervorholen, parallele Entwicklungszweige (Branches) erzeugen oder mit anderen mischen und vieles mehr.
Um das lokale Repository mit anderen Rechnern zu synchronisieren, führt der lokale Benutzer einen Push auf das zentrale Repository aus, das auf einem gehosteten Rechner im Internet liegt. Letztlich ist jedoch kein Git-Repository wirklich zentral, es bleibt jedem selbst überlassen, mit welcher Instanz er Kontakt aufnimmt, um Patches oder neue Features herunterzuladen.
Sitzt der Kaufwütige später am Laptop, kann er sich aus der zentralen Instanz mit »git clone« einen Klon bauen, den er dann nicht nur Internet-los abfragen und mit neuen Daten füttern kann, sondern bei wieder bereitstehender Verbindung einfach mit der zentralen Instanz synchronisiert.
An den Haaren aus dem Sumpf
Wer diese Lösung implementieren will, der legt, wie die Abbildung 2 zeigt, auf dem Hosting-Server mit SSH-Zugang zuerst mit Hilfe von »git init« ein leeres Git-Repository an. Dafür erzeugt er schlicht ein neues Verzeichnis, springt hinein und führt dort »git init« aus. Nun sollte man meinen, dass der Client dieses Repository einfach klonen könnte, doch weit gefehlt: Aus schier unerfindlichen Gründen muss er zunächst durch einige brennende Reifen springen.
Wie Abbildung 3 zeigt, legt der Client dafür mit »git init« ebenfalls ein leeres Repository an. Zu Testzwecken packt er eine Datei »testfile« hinein, fügt sie erst mit »git add« ein und besiegelt dann den Vorgang mit einem »commit«. Der anschließend mit »remote add« definierte Remote-Branch zeigt unter dem Alias »origin« auf das zentrale Repository auf dem Server, das der Synchronisation verschiedener Clients dient.

Abbildung 3: Ein mit »git init« auf dem Client angelegtes leeres Repo erhält einen Remote-Branch, der auf das Server-Repo zeigt. Mittels »git push« füttert es dann das Remote-Repo mit den lokalen Änderungen.
Der Befehl »push origin master« synchronisiert dann den »master«-Branch des Clients mit dem gleichnamigen Branch des Servers. Bequem ist dabei, wenn der Server über den Public Key des Clients in der Datei »~/.ssh/authorized_keys« verfügt, sonst muss der Anwender bei jedem Zugriff übers Netz sein Passwort tippen. Möchte nun ein weiterer Client die Daten aus dem Server-Repository ziehen, klont er es lediglich (Abbildung 5). Einmal auf dem lokalen Rechner angekommen, ist es eine vollständige Kopie des Server-Repository samt der Möglichkeit, lokal eingecheckte Änderungen mit »git push« wieder zum Server hochzuspielen.

Abbildung 4: Weitere Clients können nun das Remote-Repository klonen und mit »git push« ihrerseits Änderungen auf der Server-Seite einspielen.

Abbildung 5: Das Kommando »git log« im Verzeichnis »~/data/shop« zeigt die letzten Transaktionen im lokalen Repository.
In Perl verpackt
Listing 1 zeigt das Perl-Skript, das die in Tabelle 1 aufgelisteten Befehle entgegennimmt und die entsprechenden Git-Kommandos absetzt. Es nutzt Sysadm::Install vom CPAN, um schnell zwischen mehreren Verzeichnissen hin und her zu springen (»cd« und »cdback«) und um verschiedene Git-Aufrufe von der Kommandozeile aus auszuführen.
Die Bestelldaten landen in einem Cache der Marke Cache::FileCache und die in Zeile 25 eingestellte Tiefe »0« bestimmt, dass jeder Eintrag in einer Datei direkt im Verzeichnis »~/data/shop« landet. Zeile 21 erzeugt mit »mkd« aus Sysadm::Install das Verzeichnis, falls es noch nicht existiert. Im Vergleich zur Perl-Funktion »mkdir()« führt es auch noch eine Fehlerprüfung durch und loggt, was es treibt, falls Log4perl aktiviert ist.
Auf Cache-Einträge greifen die Methoden »set()« und »get()« zu, die als Schlüssel den Produktnamen (etwa »I-Pod«) entgegennehmen und Einträge im Format der Funktion »record_new()« (Zeile 89) abspeichern und abrufen. Neben dem Produktnamen umfasst ein Datensatz auch noch zwei Datumsfelder des Typs »DateTime«. Das erste, das den Namen »bought« trägt, enthält das Kaufdatum, das der Einfachheit halber die Methode »today()« in Zeile 99 auf das aktuelle Datum setzt.
Wann der User einen Artikel erwartet, gibt er dem »buy«-Befehl mit, so legt
shop buy 'dell netbook' 30
fest, dass das bei Dell bestellte Netbook 30 Tage Lieferzeit hat. Zeile 101 in Listing 1 wandelt diese Tageszahl in ein Objekt der Klasse DateTime::Duration um, das sich anschließend durch etwas schwarze Operator-Magie einfach zu einem Date-Time-Objekt addieren lässt, um das voraussichtliche Lieferdatum zu errechnen. Beide Date-Time-Objekte erhalten noch einen Formatierer vom Typ DateTime::Format::Strptime, der mit »%F« festlegt, dass sich das Objekt in einem String-Kontext als Datum im Format »JJJJ-MM-DD« darstellt.
Das Modul legt die dadurch entstandene tief verschachtelte Datenstruktur Cache::FileCache ohne Murren in einer Datei ab, indem es sie vorher intern verflacht und beim Einlesen später wieder in Perl-Objekte zurückverwandelt. Ist die Cache-Datei im Workspace des lokalen Repository gelandet, schreibt ein folgendes »git commit«-Kommando sie endgültig fest.
Zum Löschen eines Eintrags nach Erhalt der Ware muss das Skript hinter die Kulissen des Cache blicken, denn um eine Datei aus einem Git-Repository zu tilgen, muss deren Name bekannt sein. Der Cache generiert aber für jeden Schlüssel einen 40 Byte langen Hashwert als Dateinamen (etwa »d549f860476c…«). Verlangt der User also mit dem Befehl »shop got I-Pod«, dass der Eintrag unter dem Schlüssel »I-Pod« aus dem Cache zu löschen ist, linst die ab Zeile 79 definierte Funktion »path_to_key()« hinter die Kulissen der Cache-Abstraktion und holt den zugehörigen Pfadnamen hervor. Die Zeile 47 setzt jetzt darauf ein »git rm« an, das die Datei im lokalen Workspace und den Eintrag im lokalen Repository löscht. Das darauf folgende »commit« zementiert die Aktion.
Auf den Befehl »list« hin ruft die Funktion »record_list()« ab Zeile 115 die Methode »get_keys()« der Cache-Implementierung auf und bekommt alle im Cache existierenden Schlüssel als Liste zurück. Die wiederum übergibt sie der Methode »get()«, die einen Cache-Eintrag zu einem Schlüssel von der Platte holt.
Die »git«-Kommandos laufen alle über die ab Zeile 148 definierte Funktion »cmd_run()«, die intern »tap« aus dem Modul Sysadm::Install benutzt, das Programmzeilen ausführt sowie »STDERR« und »STDOUT« abfängt und dem Aufrufer als Rückgabeparameter aushändigt.
|
Tabelle 1: Befehle |
|
|---|---|
|
Befehl |
Erklärung |
|
shop init |
Auf dem ersten Client das Repository erzeugen |
|
shop clone |
Auf weiteren Clients lokalen Klon des Server-Repo |
|
shop push |
Lokale Änderungen zum Server hochspielen |
|
shop pull |
Lokales Repo auf neuesten Server-Stand bringen |
|
shop list |
Aktuelle Bestellungen auflisten |
|
shop buy Item Days |
Bestellung für Item, erwartet in Days Tagen |
|
shop got Item |
Löschen der Bestellung für Item nach Eintreffen |
Alles mitprotokolliert
Das lokale Repository speichert die Vorgänge minutiös ab und könnte alle vergangenen Zustände umstandslos wiederherstellen. Wer sich zum Beispiel dafür interessiert, welche Aktionen im Repository ausgeführt wurden, kann – wie in der Abbildung 5 zu erkennen – im Verzeichnis »~/data/shop« einfach mit dem Kommando »git log« das Log des Repository abfragen.
Konflikte lösen
Speichern zwei Clients unabhängig voneinander das gleiche Produkt in ihre lokalen Repositories ein, kommt es zum Konflikt, sobald der zweite versucht, seine Änderungen mit »shop push« dem zentralen Server mitzuteilen. Abbildung 6 zeigt Gits unschöne Fehlermeldung, wenn der zweite Client »shop push« ausführt. Eigentlich sollte »git« bei einem darauf folgenden »shop pull« feststellen, dass die Änderung auf dem Remote-Branch und die lokale Datei identisch sind, aber da es sich bei der um eine von Cache::FileCache erzeugte Binärdatei handelt, traut Git sich nicht und beschwert sich lieber lautstark.

Abbildung 6: Zwei Clients – Client 1 und Client 2 – geben unabhängig voneinander das gleiche Produkt ein. Der zentrale Server meldet daraufhin einen Konflikt.
Git befindet sich nun im Merge-Zustand und wartet beharrlich darauf, dass der User das Problem löst. Im vorliegenden Fall löscht er das Produkt erst lokal (»shop got«) und legt es anschließend neu an (»shop buy«). Ein folgender »push« geht durch – und damit ist auch das Server-Repository wieder happy. Hätte der Client das Produkt übrigens nur gelöscht und nicht wieder angelegt und sofort den »push«-Befehl ausgeführt, wäre die bestellte Kettensäge erst aus dem Server-Repository verschwunden und nach einem »shop pull« des ersten Clients auch aus dessen lokalem Repository.
Installation
Das Skript benötigt die CPAN-Module Sysadm::Install, Cache::FileCache, DateTime und DateTime::Format::Strptime, die sich am besten mit einer CPAN-Shell einrichten lassen. Bevor es losgeht, installiert der Anwender dann erst das Server-seitige leere Repository von Hand mit »git«. Auf dem gehosteten Server sind demnach keine Perl-Module erforderlich, nur das auf Linux-Systemen normalerweise vorhandene Programm »git« muss installiert sein.
Abbildung 8 zeigt, wie der erste Client sein lokales Repository initialisiert, ein paar Käufe mit »shop buy …« einträgt, mit »shop list« die lokale Datenbank abfragt und dann mit »shop push« die Daten zum Server überträgt. Alle »shop«-Kommandos geben in bester Unix-Tradition nichts aus, falls sie erfolgreich ablaufen.

Abbildung 8: Der erste Client legt das lokale Repository an, füllt es mit Daten und frischt dann das bis dato leere Server-Repository auf.
Weitere Klienten
Der zweite Client legt dann nach Abbildung 9 seinen lokalen Klon des Server-Repository mit »shop clone« an. Auch er tätigt einige Käufe und markiert schließlich die Lieferung des bei Amazon bestellten I-Pod Nano als erfolgreich abgeschlossen. Und mit »shop push« spielt auch er anschließend die lokalen Änderungen wieder zum Server hoch.

Abbildung 9: Der zweite Client holt die Daten vom Server-Repository, fügt neue hinzu und spielt das Ergebnis wieder zum Server zurück.
Ähnliches gilt für alle weiteren Clients: Auch sie klonen zuerst das Server-Repository, fuhrwerken dann erst einmal lokal im Repository herum, spielen neue Daten mit »push« zum Server zurück und erhalten schlussendlich die neuesten Updates der anderen Clients über einen »pull« vom Server. Der Bestellreigen setzt sich ungestört fort und dank Git fällt es sofort auf, falls mal eine Bestellung nicht am Cubicle eintrifft. Von einer Vermüllung des Arbeitsplatzes, wie sie in Abbildung 7 zu sehen ist, rät die Personalleitung allerdings ab.
Hat sich übrigens jemand gewundert, was das für merkwürdige Objekte vor gelbem Hintergrund rechts oben im Cubicle in Abbildung 7 sein sollen? Nun, es sind falsche Schnurrbärte, denn das ist die Ausrüstung für “Fake Mustache Friday” [3]. Zugegeben, schon ein etwas verschrobener Humor! (jcb)
|
Listing 1: |
|---|
001 #!/usr/local/bin/perl -w
002 use strict;
003 use Sysadm::Install qw(:all);
004 use Cache::FileCache;
005 use DateTime;
006 use DateTime::Format::Strptime;
007 use File::Basename;
008
009 my ($H) = glob "~";
010 my $data_dir = "data";
011 my $repo_name = "shop";
012 my $repo_dir = "$H/$data_dir/$repo_name";
013
014 my $repo_url =
015 'mschilli@box.goofhost.com:repos/shop.git';
016
017 my($action) = shift;
018 die "usage: $0 buy|got|list ..." unless
019 defined $action;
020
021 mkd $repo_dir unless -d $repo_dir;
022
023 my $CACHE = Cache::FileCache->new({
024 cache_root => "$H/$data_dir",
025 cache_depth => 0,
026 namespace => $repo_name,
027 });
028
029 if($action eq "buy") {
030 my($item, $days) = @ARGV;
031 die "usage: $0 buy item days" if
032 !defined $days or
033 $days =~ /D/;
034
035 my $rec = record_new($item, $days);
036 if($CACHE->get($item) ) {
037 die "$item already exists.";
038 }
039 $CACHE->set($item, $rec);
040 git_commit("Added item $item");
041
042 } elsif( $action eq "got" ) {
043 my($key) = @ARGV;
044 die "usage: $0 got item" unless
045 defined $key;
046 my $path = path_to_key( $key );
047 git_cmd("git", "rm", "-f",
048 basename($path));
049 git_cmd("git", "commit", "-a",
050 "-m$key deleted");
051
052
053 } elsif( $action eq "list" ) {
054 record_list();
055
056 } elsif( $action eq "push" ) {
057 git_cmd("git", "push",
058 "origin", "master");
059
060 } elsif( $action eq "pull" ) {
061 git_cmd("git", "pull",
062 "origin", "master");
063
064 } elsif( $action eq "clone" ) {
065 cd "$H/$data_dir";
066 rmdir $repo_name;
067 cmd_run("git", "clone", $repo_url);
068 cdback;
069
070 } elsif( $action eq "init" ) {
071 git_cmd("git", "init");
072 git_cmd("git", "remote", "add",
073 "origin", $repo_url);
074 } else {
075 die "Unknown action '$action";
076 }
077
078 ###########################################
079 sub path_to_key {
080 ###########################################
081 my($key) = @_;
082
083 return
084 $CACHE->_get_backend()->_path_to_key(
085 $repo_name, $key );
086 }
087
088 ###########################################
089 sub record_new {
090 ###########################################
091 my($item, $days) = @_;
092
093 my $df =
094 DateTime::Format::Strptime->new(
095 pattern => "%F",
096 time_zone => "local",
097 );
098
099 my $now = DateTime->today();
100 my $exp = $now +
101 DateTime::Duration->new(
102 days => $days);
103
104 $now->set_formatter($df);
105 $exp->set_formatter($df);
106
107 return {
108 item => $item,
109 bought => $now,
110 expected => $exp,
111 };
112 }
113
114 ###########################################
115 sub record_list {
116 ###########################################
117
118 for my $key ( $CACHE->get_keys() ) {
119 my $r = $CACHE->get( $key );
120 print "$r->{item} ",
121 "bought:$r->{bought} ",
122 "exp:$r->{expected} ",
123 "n";
124 }
125 }
126
127 ###########################################
128 sub git_commit {
129 ###########################################
130 my($msg) = @_;
131
132 cd $repo_dir;
133 cmd_run("git", "add", ".");
134 cmd_run("git", "commit", "-a",
135 "-m$msg");
136 cdback;
137 }
138
139 ###########################################
140 sub git_cmd {
141 ###########################################
142 cd $repo_dir;
143 cmd_run(@_);
144 cdback;
145 }
146
147 ###########################################
148 sub cmd_run {
149 ###########################################
150 my($stdout, $stderr, $rc) = tap @_;
151 if($rc != 0) {
152 die $stderr;
153 }
154 }
|
|
Infos |
|---|
|
[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2009/06/Perl] [2] Travis Swicegood, “Pragmatic Version Control Using Git”: The Pragmatic Programmers LLC, 2008 [3] Fake Mustache Friday: [http://www.flickr.com/photos/sesen/2093159891] |
|
Der Autor |
|---|
|
|








