Mondi su mondi, sistemi di sistemi.

Coda & Subversion

Thursday, March 25th, 2010

Uso Coda ormai da qualche tempo. È un buon programma per lo sviluppo dei siti, piacevole da usare e affidabile, non ho avuto bisogno di molto altro per diverso tempo.

I limiti più evidenti di Coda

Dopo qualche tempo, però, ho cominciato a sentire la mancanza di Emacs e così ho configurato Coda in modo da usarlo come editor esterno. A quel punto ho scoperto con fastidio che la lista dei file da caricare sul sito non veniva sempre aggiornata con le modifiche da Emacs, nonostante fos­sero correttamente individuati come file modificati rispetto alla repository nel VCS (Version Control Software; attualmente è Subversion), costringendomi ogni volta a marcarli a mano. No fun.

Inoltre non sempre abbiamo a che fare con un deployment diretto. Spesso dobbiamo prima caricare il sito su un server di staging e poi, verificato che tutto vada bene, pas­sarlo in produzione. Teoricamente potremmo “ripuntare” Coda fra i due server ma, in pratica, sarebbe un delirio.

Sfruttiamo Subversion!

Il modo ovvio di risolvere questi problemi è usare i punti di aggancio che Subversion mette a disposizione, ovvero, la pos­sibilità di eseguire delle operazioni arbitrarie in concomitanza di alcuni eventi:

  • post-commit
  • post-lock
  • post-revprop-change
  • post-unlock
  • pre-commit
  • pre-lock
  • pre-revprop-change
  • pre-unlock
  • start-commit

In questo caso, lo script agganciato sarà agganciato in post-commit ed effettuerà il deployment.

I dettagli importanti

Una cosa piuttosto semplice, insomma, anche se ci sono diverse cosine di cui tener conto. In primo luogo, è sempre pre­feribile disimpegnare Subversion il prima pos­sibile1 e quindi dobbiamo posporre le varie operazioni; inoltre, dato che lo script è condiviso per tutta la repository, dobbiamo avere l’accortezza di fare le azioni giuste per il progetto giusto; infine, visto che ogni progetto fa storia a sé, dobbiamo trovare un buona fles­sibilità2 per poter specificare le azioni che vogliamo, nel modo che riteniamo più opportuno per quel progetto.

Disimpegnare velocemente la repository

Ad ogni commit viene creato uno script di shell (a partire da un template descritto più avanti) chiamato con il numero di release. La creazione è praticamente istantanea.

Questo script va a finire in una specie di coda sul filesystem. Il fatto di usare una directory invece di un file singolo evita problemi di accesso contemporaneo, lasciando completa libertà nel suo contenuto e mi consente di sfruttare launchd per fare in modo che il proces­samento degli script nella coda venga lanciato solo al momento opportuno. Questo è il file di configurazione:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>it.refactor.deployment-queue</string>
<key>OnDemand</key>
<true/>
<key>Program</key>
<string>/usr/local/bin/projects-auto-deployment</string>
<key>QueueDirectories</key>
<array>
<string>/var/tmp/deployment-queue</string>
</array>
</dict>
</plist>

Il dettaglio importante è la direttiva QueueDirectories che, a differenza di WatchPaths, lancia il processo alla modifica del contenuto della directory, ma solo quando in pre­cedenza era vuota.

In alternativa avremmo potuto usare un qualche daemon. Non un gran problema ma mi sembrava un po’ meno robusto.

Progetti diversi, copioni diversi

Come dicevo prima, lo scopo di usare degli script di shell è quello di lasciare la mas­sima libertà.

Ogni voce della configurazione letta dallo script è simile a questa:

    <regexp> {
        env {
            REL=$rev
        }
        preflight {...}
        deploy {...}
        postflight {...}
    }

Le quattro voci: env, preflight, deploy e postflight raggruppano e ordinano i diversi frammenti che vanno a comporre lo script di deployment, che viene attivato in base all’eventuale match dell’espressione regolare.

L’unica variabile conosciuta è il numero della release, che è pas­sato da Subversion, il resto è tutto libero. Più generico di così!

Di solito, dopo aver definito le variabili ambientali in env, il preflight si occupa di fare il checkout dal VCS; il deploy fa quello che si può immaginare; il postflight si occupa di fare un po’ di pulizia ecc. Ribadisco però il fatto che quelle descritte prima sono solo sezioni: se per assurdo voglio mettere tutto nella sezione env lo posso fare.

Ma non è finita qui…

Tutto bene? Quasi. Così com’è pensato, il sistema non permette di fare alcune cose molto importanti.

Percorsi variabili

Se vogliamo usarlo anche per il deployment delle release “taggate”3, dobbiamo agganciare lo script al percorso dei tag. Tuttavia, dato che l’url completa cambia ad ogni nuovo tag — ad esempio avremo …/tags/REL1, …/tags/REL2 — come facciamo a sapere in post-commit quale prendere?

Per ora non ho trovato una soluzione generica. Mi sono affidato al fatto che il nome della versione in tags è uguale al numero pre­cedente della release di post-commit4. Ad esempio, se l’operazione è stata eseguita alla release 3753, cercheremo la directory r3752. Una volta ottenuta l’URL rientriamo nel caso standard.

Azioni specifiche per le singole release

Un’altra cosa abbastanza frequente è l’esecuzione di operazioni specifiche per una data versione. Esempio tipico: delle modifiche allo schema del database, che vanno fatte una volta sola e non devono essere riapplicate.

Per risolvere questo problema, all’interno del progetto, in una directory nota, creiamo uno script che verrà cercato dal sistema di deployment per essere eseguito5.

roll-up delle modifiche

Infine, nello scenario staging & deployment è desiderabile che una serie di operazioni fatte in staging pos­sano venire applicate in una volta sola in deployment.

Dato che lo script specifico per una data release è anch’esso sotto controllo di versione, abbiamo accesso a tutta la sua storia. Bisognerà andare a prendere quella porzione di modifiche intercorse fra l’ultimo deployment e quello attuale e appenderle ad un unico file, con un comando del genere, ad esempio:
svn log -r3778:3783 -q URL | egrep ^r | awk '{ print $1 }' | sort | while read r; do svn cat -$r URL; done

Come ottengo il range da pas­sare al comando appena visto? Partiamo da un scenario semplificato all’osso, supponendo di partire da una repository nuova di zecca.

Dalla r1 alla r10, lavoro solo nel trunk; quando creo un nuovo tag, in base alle convenzioni adottate prima, il suo nome sarà r10 e il suo numero di release sarà r11. Proseguendo, dalla r12 alla r20, lavoro sempre nel trunk; come prima, creando un nuovo tag, il nome sarà r20 e il suo numero di release sarà r21.

A questo punto, se cerco tutte le modifiche in tags, escludendo quella relativa alla creazione della directory tags, avrò r11 ed r21. Come prima, in base al numero di release pas­sato in post-commit, che al secondo tag sarà 20, posso ricavare il range da considerare, calcolabile come (release immediatamente precedente):(numero di release - 1), ovvero, r11:r19.

There’s more than one way to do it

Per chi non avesse voglia di reinventare la ruota, ci sono delle alternative. In particolare, una delle pos­sibilità che ho preso in considerazione è quella di installare il software di VCS direttamente sul server.

In effetti è una soluzione piuttosto comoda, dato che basta fare un svn up per aggiornare l’installazione e con VCS distribuiti le pos­sibilità sono ancora più ampie.

Ci sono però diversi punti dolenti. Come visto prima è raro che basti un semplice aggiornamento del codice; in secondo luogo non mi piace moltis­simo l’idea che si crei un’ulteriore dipendenza fra il sistema che pubblica gli aggiornamenti e quello che li riceve.

Tolta questa pos­sibilità, ci sono i software di SCM veri e propri, da Puppet a Capistrano, pas­sando per Fabric. Il primo propone un modello in pull, dove i cliente contattano il server per stabilire cosa c’è da fare; il secondo e il terzo sono più vicini ai sistemi in push, dove l’azione è iniziata dal server.

Infine, non dimentichiamoci di Hudson che, pur essendo un server di integrazione, può automatizzare praticamente qualsiasi cosa e quindi anche processi come questi.

Hudson è quello più adatto, essendo un vero e proprio server di continous integration. Anzi, non escludo di cominciare ad usarlo a breve, ma per ora mi bastano le due righe che ho scritto in Tcl.

Per quanto riguarda gli SCM, più vicini al mondo dei sysadmin, Puppet mi piace di più, anche perché ci ho giocato un paio di volte, mentre ho visto usare Capistrano in situazioni simili a quelle descritte.

Come si può notare, alcune delle parti più contorte sono dovute a Subversion. Dato che ho in progetto di pas­sare a Git, sono curioso di sapere se e come il procedimento si semplificherà.

  1. In post-commit Subversion ignora l’esito delle operazioni, dato che il commit è già stato fatto; tuttavia non mi è chiaro se lo esegua in asincrono
  2. avete mai sentito di un progetto che non deve essere fles­sibile?
  3. Per chi non lo sapesse, un tag è un’istantanea del codice sorgente ad un dato momento. In Subversion, è di prassi che le versioni ufficiali stiano in tags e le versioni di sviluppo che deviano da quella principale stiano in branches.
  4. l’uso dei numeri di release come nome del tag non mi risulta sia la cosa più consigliabile, lo so, tuttavia nelle applicazioni con rilasci continui la cosa non mi sembra così grave
  5. Qualcuno storcerà il naso nel vedere una commistione fra deployment e sviluppo, all’interno del sorgente. Non è sicuramente la soluzione più pulita anche se ha l’indubbio vantaggio di tenere sotto controllo di versione anche cose che spesso vengono lasciate un po’ a se stesse. Ad oggi credo che il bilancio costi/benefici sia positivo.

Semantic versioning in tcl

Tuesday, December 29th, 2009

Leggendo questo articolo che definisce le pratiche per quello che viene definito semantic versioning, mi è venuto in mente che tcl supporta direttamente a livello di linguaggio il controllo della versione dei package caricati.

Package require e i moduli in soccorso

L’uso è banale: basta specificare la versione che si vuole usare, con la pos­sibilità di essere restrittivi fino al punto di caricare una versione specifica e non una semplicemente compatibile.

La cosa mi è stata molto utile durante lo sviluppo di alcuni moduli. Come spesso succede, man mano che le idee diventano più chiare, le prime versioni sono quasi sempre da rifare; tuttavia era spesso impos­sibile modificare subito tutti gli script che utilizzavano il modulo rivisto per fargli caricare subito la nuova versione. È bastato quindi specificare la versione corretta nel codice che usava quel modulo.

Bisogna anche notare che le due versioni dei moduli pos­sono convivere nel file system tranquillamente l‘una accanto all’altra grazie all’infrastruttura dei moduli.

Non è comunque pos­sibile caricare due versioni diverse all’interno dello stesso interprete 1 ma visto che in Tcl è facilis­simo creare interpreti aggiuntivi, il problema potrebbe essere mitigabile:

% set slave1 [interp create]
interp0
% set slave2 [interp create]
interp1
% $slave1 eval {source a.tcl}
% $slave1 eval {source a2.tcl}
conflicting versions provided
for package "a": 1.0, then 1.2.0
# a.tcl e a2.tcl contengono due
# versioni dello stesso package
% $slave2 eval {source a2.tcl}

E gli altri?

Che io sappia, non mi risulta che né PerlPythonRuby pos­siedano questa caratteristica.

Qualcuno ha informazioni più pre­cise su questi o altri linguaggi?

  1. cosa invece pos­sibile in Erlang

StarKits: ultimi dettagli

Monday, December 21st, 2009

Nel chiudere questa panoramica sugli Starkit, riporto qualche informazione in più sugli aspetti che mi sembrano meritino un qualche approfondimento.

VFS: Virtual File System

Una delle caratteristiche più interes­santi può essere usata anche al di fuori degli Starkit ed è il Virtual File System. Ricorda un po’ il clas­sico approccio Unix, dove uno dei modi pre­feriti di rendere disponibili degli oggetti al sistema operativo è quello di rappresentarli come file system.

Allo stesso modo, con i VFS, è pos­sibile “montare” un sito ftp remoto, come un database Metakit, addirittura un namespace, come in questo esempio, dove leggiamo il contenuto della procedura unknown:


package require vfs::ns
vfs::ns::Mount :: ::
cd ::
set f [open "unknown" r]
set proc_definition [read $f]
close $f

Ci sono anche altri modi di ottenere lo stesso risultato, a volte anche più adatti, ma l’uso dei comandi di I/O può essere a volte il sistema giusto.

StarKits auto–aggiornanti

Gli Starkit (ma non gli Starpack) pos­sono essere modificati al runtime e in tranquillità, visto che Metakit è transazionale.

In questo modo diventa pos­sibile usare gli Starkit un po’ come con Java Web Start, dove lo Starkit distribuito si incaricherà esso stesso di scaricare e installare i componenti neces­sari direttamente al suo interno.

Gli StarPack

Saturday, December 12th, 2009

Adesso che abbiamo visto come sono costruiti gli StarKit, è semplice estendere il discorso agli StarPacks.

Premesse

Prima di tutto dobbiamo ricordare che uno StarKit può contenere delle componenti compilate. In virtù di questo fatto, pos­siamo costruire un StarKit che conterrà al suo interno l’interprete specifico per una data piattaforma. In pratica, la differenza fra uno StarKit e uno StarPack sta nel fatto che quest’ultimo usa un eseguibile compilato per avviarsi e include tutte le librerie normalmente incluse con TclKit.

Pregi e difetti

Le limitazioni più importanti di uno StarPack sono:

  1. dato che usiamo dei compilati, ogni StarPack sarà specifico per un dato sistema operativo;
  2. gli StarPack non pos­sono auto–modificarsi 1.

La contro­partita è un sistema di distribuzione completamente auto–contenuto, senza alcuna installazione aggiuntiva.

La creazione di uno StarPack è molto simile a quella di uno StarKit: l’unica differenza è che nel primo caso dovremo specificare il runtime da associare al prodotto finito.

È interes­sante notare che gli stessi TclKit sono creati come StarPack, StarPack che non hanno codice applicativo ma solo l’interprete e librerie neces­sarie 2

  1. parleremo più avanti di questa pos­sibilità per gli StarKit
  2. Credo sia anche questo il motivo per cui, durante la creazione di uno StarPack, non pos­siamo lo stesso TclKit sia per eseguire sdx che runtime da copiare nello StarPack. Probabilmente (non ho trovato informazioni certe) il processo di costruzione prenderà il TclKit, monterà il virtual file system e ci copierà l’applicazione.

Anatomia di uno StarKit

Thursday, December 10th, 2009

Una definizione

Uno StarKit è un criterio per impacchettare tutte le risorse neces­sarie ad un’applicazione in modo auto–contenuto, che non neces­sita di alcuna installazione e portabile.

In cosa differisce uno StarKit?

Non è un’idea nuova, pensiamo per esempio ai Jar nel mondo Java, che sono degli zip con un file – il MANIFEST.MF – che indica la classe per avviare l’applicazione. Tuttavia, un Jar non è stato pensato per essere completamente indipendente, tant’è vero che è pos­sibile indicare nel MANIFEST.MF dei para­metri per il clas­spath, anche se non funziona molto bene, anzi. Lo stesso può essere detto, grosso modo, anche per formati come war, ear e compagnia bella.

Se tralasciamo la questione dell’auto–contenimento, il tratto distintivo di uno StarKit è l’uso di un file system virtuale, che consente all’applicazione di comportarsi esattamente come se fosse lanciata da una versione “spacchettata”. All’avvio, questo file system viene infatti montato in modo che appaia dal punto di vista dell’applicazione come una normale directory.

Creare uno StarKit con sdx

Uno StarKit viene creato con… un altro StarKit, chiamato StarKit Developer eXtension (sdx), a cui pas­seremo la nostra applicazione da impacchettare. In ogni caso la struttura risultante sarà una cosa del genere:

  • myKit.vfs
    • main.tcl
    • lib
    • app-myKit
      • myKit.tcl
      • pkgIndex.tcl

La sequenza di avvio

Ovvero, tutta l’applicazione viene messa sotto lib, mentre in main.tcl viene inserito il codice (generato da sdx) neces­sario all’avvio dell’applicazione:

 package require starkit
 starkit::startup
 package require app-myKit

La prime due righe caricano il modulo dello starkit, inizializzando il file system virtuale e configurando la variabile auto_path; la terza riga causa il caricamento di myKit.tcl e l’avvio vero e proprio dell’applicazione.

La pros­sima volta parleremo degli StarPack.