Eine auf dem verteilten Versionskontrollsystem git basierende Datenbank merkt sich, ob Internet-Bestellungen auch wirklich beim Empfänger eintreffen.
Wer wie ich gerne günstige Produkte im Internet bestellt, fragt sich manchmal vielleicht, ob das, was da bestellt wurde, drei Tage später auch wirklich ankommt. Im Kaufrausch verliert so mancher Schnäppchenjäger gern den Überblick über das, was da in einem stetigen Bestellstrom bei verschiedenen Anbietern geordert wurde.
Abbildung 1: Kartons von Internetbestellungen stapeln sich im Cubicle des Autors |
Was liegt also näher, als abgeschickte Bestellungen in eine Datenbank aufzunehmen, und diese dann bei Wareneingang wiederum aufzufrischen? Die Datenbank sollte freilich auch dort verfügbar sein, wo der Kaufwütige seinem Bestellwahn nachgeht.
Ob im Büro, zu Hause, oder vielleicht auch auf einem Laptop in einem billigen Motelzimmer. Dort kann es allerdings sein, dass kein Internet zur Verfügung steht und die Datenbank zunächst lokal auf dem Laptop gepflegt wird, um sie später, wieder zurück in heimatlichen Gefielden, über die dort vorhandene Internetverbindung auf den neuesten Stand zu bringen.
Als Speichermedium bietet sich hierzu das verteilte Versionskontrollsystem
git
([2]) an. Von Kernel-Zampano Linus Torvalds ursprünglich eigenhändig
in einer Woche 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 heute anstehenden Aufgabe ist die Geschwindigkeit jedoch
eher irrelevant, praktisch ist nur, dass git mit seinem eingebauten
Replikationsmechanismus ohne große Klimmzüge
verschiedene verteilte Dateibäume synchronisieren kann.
Abbildung 2: Das zentrale Git-Repository auf dem Hosting-Service dient als Austauschpunkt zwischen den einzelnen lokalen Repositories zuhause, in der Arbeit und unterwegs. |
Informationen über getätigte Internetkäufe und deren geschätzte
Lieferungsdaten liegen in einem Cache auf der lokalen Festplatte. Das
hierzu verwendete Modul legt für jeden Zugriffsschlüssel eine eigene
Datei in einem Dateibaum an. Diesen versioniert git
in einem
lokalen Repository. Dies ist typisch für ein verteiltes
Versionierungssystem wie git. Entwickler haben auch ohne
Internetzugang und ohne zentrale Repositories zu stören vollen Zugriff
auf sämtliche Funktionen. Sie können neue Versionen einchecken, alte
hervorholen, parallele Entwicklungszweige (Branches) erzeugen, oder
mit anderen mischen und vieles mehr.
Um das lokale Repo mit anderen Rechnern zu synchronisieren, führt der lokale Benutzer einen ``push'' zu einem ``zentralen'' Repo aus, das auf einem Hostingservice liegt. Letztendlich ist jedoch kein git-Repo wirklich zentral, es bleibt jedem selbst überlassen, mit welcher Instanz man Kontakt aufnimmt und Patches einfließen lässt oder für den Download von neuen Features auf die lokale Instanz anzapft.
Sitzt der Kaufwütige dann am Laptop, kann er sich aus der zentralen Instanz mit ``git clone'' erstmal einen Klon bauen, den er später nicht nur internetlos abfragen und mit neuen Daten füttern kann, sondern bei wieder bereitstehender Verbindung einfach mit der zentralen Instanz synchronisiert.
Zunächst wird, wie Abbildung 3 zeigt, auf dem Hostingserver mit ssh-Zugang
mit git init
ein leeres git-Repo angelegt. Hierzu
erzeugt man schlicht ein neues Verzeichnis an,
springt hinein und führt git init
aus. Nun sollte man meinen,
dass man dieses auf dem Client einfach klonen könnte, aber aus
unerfindlichen Gründen muss der Client durch einige brennende Reifen
springen, um das zentrale Repo mit den ersten Daten zu füllen.
Abbildung 3: Per Hand wird serverseitig ein leeres Repo namens "buy.git" angelegt. |
Wie in Abbildung 4 gezeigt, legt der Client mit git init
ebenfalls ein
leeres Repo an. Zu Testzwecken packt er eine Datei testfile
hinein
und fügt sie erst mit git add
ins Repo 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'' Repo auf dem Hosting-server, das der Synchronisation
verschiedener Clients dient. 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.
Abbildung 4: 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. |
Möchte nun ein weiterer Client die Daten aus dem Server-Repo ziehen,
klont er es lediglich, wie Abbildung 5 zeigt. Einmal auf dem lokalen Rechner,
ist es eine vollständige Kopie des Server-Repo samt der Möglichkeit,
lokal eingecheckte Änderungen mit git push
wieder zum Server hochzuspielen.
Abbildung 5: Weitere Clients können nun das Remote-Repo klonen und anschließend mit "git push" ihrerseits Änderungen auf der Serverseite einspielen. |
Listing shop
zeigt das Perlskript, das die im Kasten ``Befehle für shop''
aufgelisteten Befehle entgegen nimmt und die entsprechenden git-Kommandos
absetzt. Es nutzt Sysadm::Install vom CPAN, um schnell zwischen
verschiedenen Verzeichnissen hin- und herzuspringen (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 Null 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 die Fehlerprüfung durch und loggt, was es treibt, falls
Log4perl aktiviert ist.
shop init: Auf dem ersten Client das Repository erzeugen shop clone: Auf weiteren Clients lokalen Klon des Server-Repo initialisieren 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
Auf Cache-Einträge greifen die Methoden set()
und get()
zu, die
als Schlüssel den Produktnamen (z.B. "iPod"
) entgegennehmen und Einträge
im in der Funktion record_new()
(Zeile 89)
definierten Format abspeichern und abrufen. Neben dem Produktnamen besteht
ein Datensatz auch noch aus zwei Datumsfeldern des Typs DateTime
.
Das erste, das den Namen bought
trägt, enthält das Kaufdatum, das
der Einfachheit halber in Zeile 99 mit der Methode today()
auf das aktuelle Datum gesetzt wird.
Wann der User einen Artikel per Post erwartet, gibt er bei einem buy
-Befehl
mit an, so legt zum Beispiel
shop buy 'dell netbook' 30
fest, dass das bei Dell bestellte Netbook 30 Tage Lieferzeit hat. Zeile
101 in Listing shop
wandelt diese Tageszahl in ein Objekt der Klasse
DateTime::Duration um, die man anschließend durch etwas schwarze
Operator-Magie einfach zu einem DateTime-Objekt addieren kann, um das
voraussichtliche Lieferdatum zu errechnen.
Beide DateTime-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.
Die so entstandene tief verschachtelte Datenstruktur legt
Cache::FileCache ohne Murren in einer Datei ab, indem es sie vorher
intern ausflacht und beim Einlesen später
wieder in Perl-Objekte zurückverwandelt.
Ist die Cache-Datei im Workspace des lokalen Repos gelandet, schreibt
ein folgendes git commit
sie im lokalen Repo fest.
Zum Löschen eines Eintrags nach Erhalt der Ware muss das Skript hinter
die Kulissen des Caches blicken, denn um eine Datei aus einem Git-Repo
zu tilgen, muss deren Name bekannt sein. Der Cache generiert aber für
jeden Schlüssel aus einem 40 Byte langen Hashwert bestehenden Dateinamen
(z.B. ``d549f860476c...''). Verlangt der User also mit dem Befehl
shop got iPod
, dass der Eintrag unter dem Schlüssel ``iPod'' 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
über den Backend der Implementierung den zugehörigen Pfadnamen hervor.
Zeile 47 setzt darauf ein git rm
an, das nicht nur die Datei im
lokalen Workspace löscht, sondern auch den Eintrag im lokalen Repo.
Der 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 in einer Liste zurück. Diese wiederum
übergibt sie der Methode get()
, die einen Cache-Eintrag zu einem
Schlüssel von der Platte holt.
Die git
-Kommandos laufen allesamt über die ab Zeile 148 definierte
Funktion cmd_run
, die intern tap
aus dem Modul Sysadm::Install
aufruft, das wiederum Programmzeilen ausführt, STDERR und STDOUT abfängt
und dem Aufrufer sauber als Rückgabeparameter aushändigt.
Das lokale Repository speichert die Vorgänge minutiös ab und könnte alle
vergangenen Zustände umstandslos wieder herstellen. Wer sich zum Beispiel
dafür interessiert, welche Aktionen im Repo ausgeführt wurden, kann, wie
in Abbildung 6 gezeigt, im
Verzeichnis ~/data/shop
einfach mit dem Kommando git log
das Log
des Repos abfragen.
Abbildung 6: Das Kommando 'git log' im Verzeichnis ~/data/shop zeigt die letzten Transaktionen im lokalen Repo. |
Speichern zwei Clients unabhängig voneinander das gleiche Produkt in ihre
lokalen Repos ein, kommt es zum Konflikt, sobald der zweite versucht, seine
Änderungen mit shop push
dem zentralen Server mitzuteilen.
Abbildung 7 zeigt gits unschöne Fehlermeldung, wenn der zweite Client
shop push
ausführt. Eigentlich sollte git
bei einem
darauffolgenden shop pull
feststellen, dass die Änderung auf dem
Remote-Branch und die lokale Datei identisch sind, aber da es sich
um eine von Cache::FileCache erzeugte Binärdatei handelt, traut es
sich nicht und beschwert sich lautstark. git
befindet sich nun
im Merge-Zustand und wartet beharrlich darauf, dass der User das Problem
löst. Im vorliegenden Fall löscht dieser das Produkt erst lokal (shop got
)
und legt es anschließend neu an (shop buy
). Ein anschließender
push
geht durch und damit ist auch das Server-Repo wieder happy.
Hätte der Client das Produkt übrigens nur gelöscht und nicht wieder angelegt,
und sofort den push
ausgeführt, wäre die bestellte Kettensäge erst
aus dem Server-Repo verschwunden, und nach einem shop pull
des ersten
Clients auch aus dessen lokalem Repo.
Abbildung 7: Zwei Clients geben unabhängig voneinander das gleiche Produkt ein und der zentrale Server meldet einen Konflikt. |
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 }
Das Skript benötigt die CPAN-Module Sysadm::Install, Cache::FileCache, DateTime und DateTime::Format::Strptime, die am besten mit einer CPAN-Shell installiert werden.
Bevor es losgeht, wird dann erst das
server-seitige leere Repository von Hand mit
git
installiert. Auf dem Hosting-Server sind demnach keine Perl-Module
erforderlich, nur das auf Linux-Systemen normalerweise vorhandene
git
-Programm 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. |
Abbildung 9: Der zweite Client holt die Daten vom Server-Repository, fügt neue hinzu und spielt das Ergebnis wieder zum Server zurück. |
Der zweite Client legt dann nach Abbildung 9 seinen lokalen Klon des
Server-Repositories mit shop clone
an. Auch er tätigt Käufe und markiert
die Lieferung des bei Amazon bestellten iPod Nano als erfolgreich
abgeschlossen. Und mit shop push
spielt auch er anschließend die lokalen Änderungen wieder zum Server hoch.
Ähnliches gilt für alle weiteren Clients, auch sie klonen zuerst
das Server-Repository, fuhrwerken dann zunächst lokal im Repo herum,
spielen neue Daten mit push
zum Server zurück und erhalten die neuesten
Updates der anderen Clients über einen pull
vom Server. Der Bestellreigen
setzt sich ungestört fort, und dank git fällt es jetzt sofort auf, falls
mal eine Bestellung nicht am Cubicle eintrifft, weil die Lieferung
verloren ging. Keine schlaflosen Nächte mehr, alles unter Kontrolle!
Hat sich übrigens jemand gewundert, was die falschen Schnurrbärte rechts oben im Cubicle in Abbildung 1 sollen? Nun, das ist die Ausrüstung für ``Fake Mustache Friday'' ([3]). Zugegeben, verschrobener Humor!
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2009/06/Perl
``Pragmatic Version Control Using Git'', Travis Swicegood, The Pragmatic Bookshelf``, 2008
``Fake Mustache Friday'', http://www.flickr.com/photos/sesen/2093159891/
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |