MokaByte 87 - Luglio/Agosto 2004 
Creiamo il bitTempo!

di
Claudio
Levantini

Immaginate la mia faccia quando Nadia mi dice: "..e l'applicazione deve, naturalmente, notificare giornalmente agli utenti che non l'ahnno ancora fatto,di compilare il feed-back...". E' stato lì che ho scoperto che a Tomcat mancava una dimensione fondamentale: il bitTempo.

Cos'è il tempo?
Sant'Agostino rispondeva così a questa domanda: "Se non me lo chiedono lo so!". Ed è proprio dietro a quel "naturalmente" di Nadia che si nasconde la genesi del problema, la risposta del padre della chiesa e fors'anche una sua possibile soluzione.

Nadia vuole che naturalmente, un'applicazione web, allo scoccare di un nuovo giorno notifichi ai suoi utenti un qualcosa,e quel naturalmente ha un doppio significato:

  • la richiesta funzionale dell'invio di una notifica è una conseguenza necessaria che viene fuori quasi da sè come risultato naturale di come l'applicazione è stata pensata;
  • il "tempo" dell'applicazione è naturalmente molto simile al tempo del mondo reale, ed è questa similitudine che rende scontato il fatto che, così come nel tempo reale ogni tanto succede qualcosa, anche nel tempo della nostra applicazione, ogni tanto deve succedere qualcosa: la spedizione di una notifica.

Tutto accade così naturalmente nel tempo che il tempo stesso è un dato quasi scontato, inutile da sottolineare o un'entità poco problematica. Eppure adesso abbiamo due problemi:

  • scoprire che cos'è il tempo;
  • creare un "tempo dell'applicazione" che sia il più possibile simile al tempo reale per far succedere qualcosa nel suo flusso.

 

Da Aristotele ad Einstein
Se vogliamo costruire il "bitTempo" o tempo di un'applicazione come una struttura il più possibile simile al tempo naturale allora risulta abbastanza chiaro il perchè sia importante rispondere alla domanda su cosa sia il tempo reale. Per simulare al meglio qualcosa,dobbiamo farci un'idea dell'originale.

Poiché il mosso si muove da un punto verso un altro punto, e ogni grandezza è continua, il movimento segue alla grandezza. Infatti, poiché la grandezza è continua, è continuo anche il movimento; e per il fatto che lo è il movimento, è continuo anche il tempo, giacché la quantità del tempo trascorso è proporzionata a quella del movimento. [...]Pertanto, quando noi percepiamo l’istante come unità e non già come un prima e un poi nel movimento e neppure come quell’entità che sia la fine del prima e il principio del poi, allora non ci sembra che alcun tempo abbia compiuto il suo corso, in quanto non vi è neppure movimento. Quando, invece, percepiamo il prima e il poi, allora diciamo che il tempo c’è. Questo, in realtà, è il tempo: il numero del movimento secondo il prima e il poi.
(Aristotele, Fisica, D,10 e G,11, in Opere, Roma-Bari, Laterza, 1973)

Come possiamo leggere qui sopra, Aristotele da una definizione del tempo per noi molto interessante. Per il filosofo greco infatti il tempo non esisterebbe se non ci fosse una qualsiasi sorta di movimento o cambiamento.

Credo che questa definizione si adatti benissimo al "tempo di un'applicazione" di tipo informatico. Pensate infatti ad un qualsiasi programma che rimanga in attesa che l'utente faccia qualcosa. Se l'utente non fa nulla, l'applicazione rimane sempre nel medesimo stato, dal suo punto di vista quindi, nulla è cambiato; è come se fosse congelata in quello stato che solo l'intervento umano può cambiare e quando ciò accade, ecco che improvvisamente il cambiamento di stato dell'applicazione implica una differenza e quindi in un qualche senso, un prima ed un poi.

La definizione aristotelica si adatta molto bene come tempo di una applicazione, ma essa non fa altro che mettere in luce in maniera ancora più evidente il problema che dobbiamo risolvere: a noi serve costruire un bitTempo che scorra e che faccia succedere qualche cosa anche se non cambia nulla nello stato dell'applicazione.

 

Newton


Fin qui è stato indicato in quale senso da intendere, nel seguito, parole non comunemente note. Non definisco, invece, tempo, spazio, luogo e moto, in quanto notissimi a tutti. Va notato tuttavia, come comunemente non si concepiscano queste quantità che in relazione a cose sensibili. Di qui nascono i vari pregiudizi, per eliminare i quali conviene distinguere le medesime cose in assolute e relative, vere e apparenti, matematiche e volgari. I. Il tempo assoluto, vero, matematico, in sé e per sua natura senza relazione ad alcunché di esterno, scorre uniformemente, e con altro nome è chiamato durata; quello relativo, apparente e volgare, è una misura (accurata oppure approssimativa) sensibile ed esterna della durata per mezzo del moto, che comunemente viene impiegata al posto del vero tempo: tali sono l’ora, il giorno, il mese, l’anno.
Newton, Princípi matematici della filosofia naturale, Scolio

 

Newton, come S. Agostino, non si avventura in una definizione di tempo, giustificandosi con il fatto che la nozione di tempo è comune a tutti, tanto naturale come conoscenza da non richiedere nessun tipo di specificazione se non il chiarimento che la nozione comune di tempo è un po' come se fosse la brutta copia del tempo assoluto che scorre indipendentemente da tutto, regolarissimo e per sempre. Ciò che è più o meno regolare è la nostra misurazione del tempo, che può avvenire in relazione a qualche cambiamento, e può essere più o meno perfettibile.
Rispetto alla definizione aristotelica, qui il cambiamento o il movimento non è messo in relazione direttamente con il tempo, ma con la possibilità di misurarlo in qualche modo, più o meno imperfettamente. Il tempo è invece qualcosa che esiste da sè, perfetto nel suo scorrere, quasi fosse un sottofondo necessario attraverso il quale le cose accadono.
E' proprio il tempo newtoniano, quello che stiamo cercando per risolvere il nostro problema. Qualcosa che scorra indipendentemente dalle azioni che compie l'utente. Un contenitore dentro al quale far succedere qualcosa ogni qual volta il tempo scorre.

 

Einstein

Citiamo la concezione relativistica del tempo per amor di bellezza, ben sapendo che il tempo di Einstein è tanto affascinantte quanto lontano dal nostro comune sentire. Nulla è più distante dalla concezione relativistica del tempo del "naturalmente" di Nadia!

Il fulmine ha colpito le rotaie di una linea ferroviaria in due punti A e B, molto lontani l'uno dall'altro. Affermo che i due colpi di fulmine hanno avuto luogo simultaneamente. Ove ponessi al lettore la domanda se tale mia affermazione è sensata, la risposta sarebbe un reciso "sì". Se ora però gli chiedessi di spiegarini in maniera più precisa il senso di quell' affermazione, egli, dopo un po' di riflessione, troverebbe che la risposta a tale domanda non è così facile come sembra a prima vista. [...] Ci imbattiamo nella stessa difficoltà in tutti gli enunciati fisici ove entra in gioco il concetto di "simultaneità". Questo concetto non esiste per il fisico fino a quando egli non ha la possibilità di scoprire nel caso concreto se esso risulti fondato oppure no. Abbiamo perciò bisogno di una definizione di simultaneità capace di fornirci il metodo per mezzo del quale decidere sperimentalmente, nel caso attuale, se entrambi i colpi di fulmine hanno avuto luogo simultaneamente o no. [...] Dopo una certa riflessione, il lettore farà la seguente proposta, per verificare la simultaneità. Con una misurazione effettuata lungo le rotaie, verrà calcolato l'intervallo che collega i punti A e B, e verrà messo un osservatore nel punto di mezzo M dell'intervallo AB. Quest'osservatore verrà fornito di un dispositivo (per esempio due specchi inclinati a 90 gradi) che gli permetta di osservare visualmente i due punti A e B contemporaneamente. Se l'osservatore percepisce i due bagliori del fulmine nel medesimo istante, essi saranno allora simultanei [...]. Supponiamo che un treno molto lungo viaggi sulle rotaie con la velocità costante v e nella direzione indicata dalla figura 1. Le persone che viaggiano su questo treno useranno vantaggiosamente il treno come corpo rigido di riferimento (sistema di coordinate); esse considerano tutti gli eventi in riferimento al treno. Ogni evento, poi, che ha luogo lungo la linea ferroviaria ha pure luogo in un determinato punto del treno. Anche la definizione di simultaneità può venir data rispetto al treno nello stesso preciso modo in cui venne data rispetto alla banchina. Ora però si presenta, come conseguenza naturale, la seguente domanda: due eventi (per esempio i due colpi di fulmine A e B che sono simultanei rispetto alla banchina ferroviaria saranno tali anche rispetto al treno? […]Allorché diciamo che i colpi di fulmine A e B sono simultanei rispetto alla banchina intendiamo: i raggi di luce provenienti dai punti A e B dove cade il fulmine si incontrano l'uno con l'altro nel punto medio M dell'intervallo AB della banchina. Ma gli eventi A e B corrispondono anche alle posizioni A e B sul treno. Sia M' il punto medio dell'intervallo AB sul treno in moto. Proprio quando si verificano i bagliori del fulmine, questo punto M' coincide naturalmente con il punto M, ma esso si muove verso la destra del disegno con la velocità v del treno. Se un osservatore seduto in treno nella posizione M' non possedesse questa velocità, allora egli rimarrebbe permanentemente in M e i raggi di luce emessi dai bagliori del fulmine in A e B lo raggiungerebbero simultaneamente, vale a dire si incontrerebbero proprio dove egli è situato. Tuttavia nella realtà (considerata; con riferimento alla banchina ferroviaria), egli si muove rapidamente verso il raggio di luce che proviene da B, mentre corre avanti al raggio di luce che proviene da A. Pertanto l'osservatore vedrà il raggio di luce emesso da B prima di vedere quello emesso da A. Gli osservatori che assumono il treno come loro punto di riferimento debbono perciò giungere alla conclusione che il lampo di luce B ha avuto luogo prima del lampo di luce A. Perveniamo così al seguente importante risultato: gli eventi che sono simultanei rispetto alla banchina non sono simultanei rispetto al treno e viceversa (relatività della simultaneità); ogni corpo di riferimento (sistema di coordinate) ha il suo proprio tempo particolare: un'attribuzione di tempo è fornita di significato solo quando ci venga detto a quale corpo di riferimento tale attribuzione si riferisce.
Da A. Einstein, Relatività: esposizione divulgativa (1917), tr. it. in A. Einstein, Opere scelte, Boringhieri, Torino 1988.

In buona sostanza, se con Galileo nasce il concetto di "moto relativo", concetto che ci è abbastanza familiare, basta guidare la macchina, infatti, per rendersi conto che un oggetto si muove rispetto ad un altro se e solo se prendiamo come riferimento delle misurazioni un preciso sistema inerziale (una delle due macchine oppure l'autogrill sull'autostrada); con Einstein anche il tempo subisce la stessa sorte e diventa relativo ad un sistema inerziale di riferimento, nel senso che due eventi accadono simultaneamente (nello stesso tempo) in dipendenza da un sistema di riferimento.
Una delle conseguenze più straordinarie dell'universo einsteniano non è tanto la relatività del concetto di "simultaneo", ma l'insensatezza del concetto di "istantaneo"!

 

Il bitTempo come la simulazione di un orologio
Se il nostro bitTempo deve essere la minima simulazione possibile del nostro tempo naturale che abbiamo identificato con il tempo newtoniano, ecco che salta subito all'occhio che sarà indispensabile utilizzare uno strumento della misurazione del tempo reale per simularrlo informaticamente. I più perspicaci dei miei lettori avranno già intuito che per questa simulazione andremo ad utilizzare il cloccker di sistema di un Personal Computer!
Il motore temporale

Immaginiamo che il nostro motore temporale sia un thread a se stante che, per similitudine al tempo eterno, cicli all'infinito svolgendo le seguenti azioni:

  • la prima volta fuori dal ciclo legge il clock di sistema e ne memorizza il valore; dentro al ciclo, rimane dormiente per un tot di tempo;
  • rilegge il clock di sistema e lo confronta con l'ultimo memorizzato. Se è trascorso un lasso di tempo sufficiente, avvisa il mondo esterno che è passato del tempo;
  • Memorizza l'ultima lettura del clock di sistema per il prossimo confronto.

Oggettivamente parlando (nel senso della programmazione orientata agli oggetti), avvisare qualcuno di qualcosa significa spedire un messaggio a tutti quelli che sono in Ascolto, ed infatti il nostro thread, oltre ad essere un thread è anche un oggetto "osservabile".
Il pattern Observable/Observers
Credo che nessun modello di risoluzione eguagli la semplicità e la potenza di questo. In pratica è qualcosa di molto simile alla trasmissione televisiva: c'è una sorgente che trasmette, e chi vuole essere un ricevitore deve dotarsi di un'antenna. Tutte le volte che la fonte lo ritiene necessario, spedisce alla lista dei suoi "ricevitori" il messaggio, da lì in poi, sono affari dei ricevitori.
Fare tutto questo in java è semplicissimo, basta far derivare la classe "fonte" dalla classe Observable, mentre i ricevitori devono implementare l'interfaccia Observer.
La classe Observable mantiene al suo interno una lista di "ascoltatori" con i relativi metodi di aggiunta o rimozione di un ascoltatore, ma cosa ancora più importante, da la possibilità di inviare a chi è in ascolto un determinato messaggio.
La classe Cloccker
Ecco l'incarnazione informatica del nostro motore temporale, diamogli un'ochiata un po' più da vicino:

public class Cloccker extends Observable implements Runnable {
[...]
/** E' il thread vero e proprio che viene fatto partire nel metodo run */
private Thread dThread=null;

Come anticipato deriva da Observable ed essendo già "occupata" l'unica derivazione possibile, non ci rimane altra strada se non quella di implementare l'interfaccia runnable ed istanziare al'interno della classe il thread vero e proprio.

La sala macchine del nostro thread è il metodo run() che merita un'attenta analisi:

public void run()
{
dCalendar=new GregorianCalendar();
while(isRunning())
{
try
{
dThread.sleep(58000);
GregorianCalendar aCalendar=new GregorianCalendar();
if(aCalendar.get(Calendar.YEAR)>dCalendar.get(Calendar.YEAR))
{
ChangeTimeEvent vEvent=new ChangeTimeEvent(ChangeTimeEvent.CH_YEAR+
   ChangeTimeEvent.CH_MONTH+
   ChangeTimeEvent.CH_DAY+
    ChangeTimeEvent.CH_HOUR+ChangeTimeEvent.CH_MINUTE);
setChanged();
notifyObservers(vEvent);
} // end if
else if(aCalendar.get(Calendar.MONTH)>dCalendar.get(Calendar.MONTH))
{
ChangeTimeEvent vEvent=new ChangeTimeEvent(ChangeTimeEvent.CH_MONTH+
                       ChangeTimeEvent.CH_DAY+
                       ChangeTimeEvent.CH_HOUR+ChangeTimeEvent.CH_MINUTE);
setChanged();
notifyObservers(vEvent);
} // end if
else if(aCalendar.get(Calendar.DAY_OF_MONTH)>dCalendar.get(Calendar.DAY_OF_MONTH))
{
ChangeTimeEvent vEvent=new ChangeTimeEvent(ChangeTimeEvent.CH_DAY+
                              ChangeTimeEvent.CH_HOUR+
                              ChangeTimeEvent.CH_MINUTE);
setChanged();
notifyObservers(vEvent);
} // end if
else if(aCalendar.get(Calendar.HOUR)>dCalendar.get(Calendar.HOUR))
{
ChangeTimeEvent vEvent=new ChangeTimeEvent(ChangeTimeEvent.CH_HOUR+ChangeTimeEvent.CH_MINUTE);
setChanged();
notifyObservers(vEvent);
} // end if
else if(aCalendar.get(Calendar.MINUTE)>dCalendar.get(Calendar.MINUTE))
{
ChangeTimeEvent vEvent=new ChangeTimeEvent(ChangeTimeEvent.CH_MINUTE);
setChanged();
notifyObservers(vEvent);
} // end if
dCalendar=aCalendar;
} // end try
catch(InterruptedException anEx)
{
System.out.println(anEx);
} // end catch
} // end while
dCalendar=null;
} // end method

Il funzionamento di questo metodo è molto semplice, in un ciclo infinito viene letta l'ultima data di sistema che viene confrontata con la data letta in precedenza. L'anno delle due date viene confrontato e, se diverso, vuol dire che nel "frattempo" e cambiato l'anno, il mese, il giorno, l'ora ed il minuto. Viene costruito un ogetto evento che memorizza tutti questi cambiamenti e l'evento viene spedito a tutti gli oggetti che sono in ascolto. Nel caso non fosse cambiato l'anno si passa al controllo sul cambiamento del mese e così via, in cascata, fino al minuto.

Il "frattempo" è rapresentato dall'istruzione sleep() che riceve come parametro i millisecondi durante i quali il thread deve rimaneere a riposo. Per il nostro oggettino questo valore è molto importante perchè rappresenta la risoluzione temporale minima del nostro motore. Con un valore di 58.000 millisecondi, noi diamo alla nostra macchina del tempo una risoluzione "al minuto". In pratica gli eventi temporali più fitti che siamo in grado di discriminare stanno ad una distanza l'uno dall'altro di un minuto.

L'istruzione sleep è immersa in un catch che intercetta il fatto che mentre il thread è a riposo, può essere interrotto dall'istruzione interrupt. In effetti è proprio quello che succede nel metodo stop del Cloccker che riportiamo qui sotto:

public void stop()
{
if(isRunning())
{
setRunning(false);
if(!dThread.interrupted())
dThread.interrupt();
dCalendar=null;
dThread=null;
} // end if
} // end method

Il simmetrico del metodo stop è il metodo start che aziona il cloccker dal quale partono gli eventi temporali.

public void start()
{
if(!isRunning())
{
dThread=new Thread(this);
setRunning(true);
dCalendar=new GregorianCalendar();
dThread.start();
} // end if
} // end method

In questo metodo vediamo l'implementazione del meccanismo per la costruzione di un thread attraverso l'utilizzo dell'interfaccia runnable. In pratica, non potendo derivare la classe cloccker dalla classe Thread, implementiamo l'interfaccia che ci obbliga alla definizione di un metodo run. La classe Cloccker ha al suo interno, come dato membro, un'istanza della classe Thread. Al momento della creazione vera e propria dell'oggetto Thread, gli viene passato il riferimento all'oggeto Cloccker stesso. Quando viene eseguito lo start dell'oggetto Thread, lo stesso chiamerà il metodo run dell'oggetto Cloccker.

 

Dalla parte del consumatore
Fin qui abbiamo analizzato il "produttore" degli eventi temporali, che con una costanza e pazienza infinita, allo scattare di ogni minuto genera l'evento temporale appropriato. Ma leggi economiche ben precise dicono che dove c'è un produttore c'è anche un consumatore. Nel nostro caso, una serie di classi che, ricevuto l'evento temporale, in qualche modo lo "consumano".

 

Dispaccio dell'evento temporale
La classe che, dato un evento temporale in arrivo dal cloccker, decide o meno se scatenare uno o più task, è la classe EventDispatcher. I suoi compiti fondamentali sono:

  • Mantenere al suo interno una lista di task da eseguire;
  • filtrare in qualche modo gli eventi temporali in arrivo e se è il caso, eseguire uno ad uno la lista dei task.

Ecco la definizione della classe:

public abstract class EventDispatcher extends Observable{

La classe è astratta perchè il filtro sugli eventi viene fatto attraverso varie specializzazioni della classe stessa. Deriva da observable per permettere ad un Logger di tenere traccia dei task che vengono eseguiti scrivendo il tutto su un file.

public void put(TaskBase aTask){
dMap.put(aTask.getName(),aTask);
if(aTask.isActive() && !isActive())
switchOn();
} // end method

Il metodo put inserisce un nuovo task nella lista. Ogni task deve avere un nome univoco che è la chiave di inserimento nella collezione di oggetti.

Il metodo run, attraverso un'iterazione sui task, li esegue uno ad uno:

public void run(ChangeTimeEvent anEvent)
{
if(isActive())
{
Iterator i=dMap.values().iterator();
TaskBase vTask=null;
while(i.hasNext())
{
vTask=(TaskBase)i.next();
vTask.run(anEvent);
log("Partitto l'evento "+vTask.getName());
} // end while
} // end if
} // end method

Le specializzazioni di EventDispatcher

Abbiamo detto che il filtro sugli eventi generati dal cloccker avviene attraverso la specializzazione della classe EvendtDispacher. Ecco una lista dei dispacciatori specializzati:

  • MinuteAlarmDispatcher - raccoglie tutti i task che devono partire ogni minuto;
  • HourAlarmDispatcher - raccoglie tutti i task che devono partire ogni ora;
  • DayAlarmDispatcher - raccoglie tutti i task che devono partire ogni giorno;
  • MonthAlarmDispatcher - raccoglie tutti i task che devono partire ogni mese;
  • YearAlarmDispatcher - raccoglie tutti i task che devono partire ogni anno.

E fin qui esiste una perfetta corrispondenza tra gli eventi che scatena il cloccker e le specializzazioni della classe EventDispatcher. A titolo di esempio vediamo l'implementazione della classe DayAlarmDispatcher:

public class DayAlarmDispatcher extends EventDispatcher
{
/**
* Costruttore della classe.
*
* @since Version 1.0
*/
public DayAlarmDispatcher(boolean anActive)
{
super(EDT_DAY,anActive);
} // end constructor
public DayAlarmDispatcher(Logger aLog)
{
super(EDT_DAY,false,aLog);
} // end constructor
/**
* Costruttore di default della classe.
*
* @since Version 1.0
*/
public DayAlarmDispatcher()
{
this(false);
} // end constructor
} // end class

L'implementazione delle altre classi specializzate è del tutto analoga a quela riportata qui sopra.

E' possibile implementare EventDispatcher ancora più specializzati, dove il parallelismo con gli eventi temporali generati dal cloccker si perde:

  • BeginWEAlarmDispatcher - raccoglie i task che devono partire all'inizio del Week-End;
  • DateTimeAlarmDispatcher - Raccoglie i task che devono partire ad una determinata ora di un giorno preciso;
  • EndWEAlarmDispatcher - raccoglie tutti i task che partono alla fine del fine settimana;

Anche l'implementazione di questi dispacciatori è del tutto analoga a quella presentata in precedenza. Fa eccezzione la classe DateTimeAlarmDispatcher dove il metodo run è stato riscritto:

public void run(ChangeTimeEvent anEvent)
{
if(isActive())
{
Iterator i=dMap.values().iterator();
TaskBase vTask=null;
while(i.hasNext())
{
vTask=(TaskBase)i.next();
if(vTask.isRunnable(anEvent))
{
vTask.run(anEvent);
log("Partitto l'evento "+vTask.getName());
} // end if
} // end while
} // end if
} // end method

Rispetto al metodo run dela classe EventDispatcher visto prima, l'unica differenza è data dal fatto che il task viene fatto partire solo se il test:

if(vTask.isRunnable(anEvent))

da esito positivo. Il metodo isRunnable infatti confronta se il giorno e l'ora che il Cloccker ha generato corrispondono al giorno e l'ora di schedulazione del task. Se vi è corrispondenza il task viene attivato.

Tutti questi dispatcher sono raggruppatti in una classe, la DispatcherGroup, che estende l'interfaccia Observer. E' questa classe l'ascoltatore diretto degli eventi generati dal Cloccker. A seconda del tipo di evento, la DispatcherGroup notifica al dispatcher corretto l'evento generato. Vediamo in dettaglio il metodo che fa tutto questo :

public void update(Observable anObj, Object anEvent){
EventDispatcher vDispatcher=null;
if(((ChangeTimeEvent)anEvent).isMinuteChanged()){
vDispatcher=(MinuteAlarmDispatcher)(dGroup.get(
                  new Integer(EventDispatcher.EDT_MINUTE)));
DateTimeAlarmDispatcher vDispatcher2=(DateTimeAlarmDispatcher)(dGroup.get(new Integer(EventDispatcher.EDT_DATETIME)));
vDispatcher2.run((ChangeTimeEvent)anEvent);
vDispatcher.run((ChangeTimeEvent)anEvent);
} // end if MinuteEvent
if(((ChangeTimeEvent)anEvent).isHourChanged()){
vDispatcher=(HourAlarmDispatcher)(dGroup.get(new Integer(EventDispatcher.EDT_HOUR)));
vDispatcher.run((ChangeTimeEvent)anEvent);
} // end if HourEvent
if(((ChangeTimeEvent)anEvent).isDayChanged())
{
vDispatcher=(DayAlarmDispatcher)(dGroup.get(new Integer(EventDispatcher.EDT_DAY)));
vDispatcher.run((ChangeTimeEvent)anEvent);
String vWeekDay=((ChangeTimeEvent)anEvent).getWeekDay();
if(vWeekDay.equals("Sa"))
{
BeginWEAlarmDispatcher vDispatcher2=(BeginWEAlarmDispatcher)(dGroup.get(new Integer(EventDispatcher.EDT_BEGINWE)));
vDispatcher2.run((ChangeTimeEvent)anEvent);
} // end if beginWeekEnd
else if(vWeekDay.equals("Mo"))
{
EndWEAlarmDispatcher vDispatcher2=(EndWEAlarmDispatcher)(dGroup.get(new Integer(EventDispatcher.EDT_ENDWE)));
vDispatcher2.run((ChangeTimeEvenanEvent);
} // end if End week end
} // end if DayEvent
if(((ChangeTimeEvent)anEvent).isMonthChanged())
{
vDispatcher=(MonthAlarmDispatcher)(dGroup.get(new Integer(EventDispatcher.EDT_MONTH)));
vDispatcher.run((ChangeTimeEvent)anEvent);
} // end if MonthEvent
if(((ChangeTimeEvent)anEvent).isYearChanged())
{
vDispatcher=(YearAlarmDispatcher)(dGroup.get(new Integer(EventDispatcher.EDT_YEAR)));
vDispatcher.run((ChangeTimeEvent)anEvent);
} // end if YearEvent
} // end method

La soluzione scelta sembra non essere molto elegante. Molto più bella sarebbe stata un'iterazione su tutti i dispatcher per la chiamata del metodo run, il polimorfismo avrebbe risolto tuto, ed il metodo run, al suo interno avrebbe testato la gestione o meno dell'evento. Abbiamo però scelto questo tipo di soluzione, un po' meno OOP per minimizzare i test da eseguire. Con la soluzione proposta il dispatcher è già sicuro che l'evento che gli arriva è di sua competenza. Ammetto che sono stato molto combattuto fra le due implementazioni e chi scegliesse per il "più bello" piuttosto che per il "più efficente" avrebbe la mia approvazione, anche perchè l'aggiunta di un nuovo dispatcher, con la soluzione bella, non implica alcuna modifica a questo metodo. Nell'implementazione attuale un nuovo dispatcher implica il dover mettere mano a questo metodo.

 

Il lavoro sporco
Fino a questo punto abbiamo parlato di task sottointendendo degli oggetti che eseguono il lavoro vero e proprio. Una volta che il cloccker ha generato l'evento temporale e questo è stato dispacciato al corretto dispatcher, questo attiva tutti i task che l'evento deve attivare.
Un task è una classe con le seguenti caratteristiche:

  • possiede un nome univoco;
  • ha una data di attivazione;
  • ha un thread che viene lanciato ogni volta che il task è attivato.

Tutti i task che si vogliono creare devono derivare dalla classe TaskBase che ha nel suo metodo run il fulcro principale del suo lavoro:

public void run(ChangeTimeEvent anEvent)
{
if(isActive())
{
preRun();
if(dThread!=null)
dThread.start();
else
System.out.println("Thread null in task "+getName());
} // end if
} // end method

Una volta controllato che il task sia attivo, viene chiamato il metodo preRun. Normalmente questo metodo istanzia dinamicamente il thread da lanciare. Nella costruzione di un nuovo task questo metodo può essere riscritto per effettuare una new statica del thread da eseguire. Una volta creato, il thread viene lanciato ed il gioco è fatto.

Riportiamo il metodo preRun di default per l'istanziazione dinamica di un thread:

protected void preRun()
{
if(dDynamicClassName!=null)
{
try
{
dThread=(Thread)(Class.forName(dDynamicClassName).newInstance());
} // end try
catch(ClassNotFoundException anEx)
{
dThread=null;
System.out.println(anEx);
} // end catch
catch(InstantiationException anEx)
{
dThread=null;
System.out.println(anEx);
} // end catch
catch(IllegalAccessException anEx)
{
dThread=null;
System.out.println(anEx);
} // end catch
} // end if
else
dThread=null;
} // end method

In seguito vedremo come specificare il nome della classe che deve essere creata. Di seguito riportiamo invece la semplicissima implementazione di un metodo preRun che istanzia staticamente il thread da eseguire:

protected void preRun(){
dThread=new ThreadCiaoMinuto();
} // end method

La classe TaskBase definisce due metodi astratti, che quindi devono essere implementati nei task derivati:

public abstract boolean isRunnable(ChangeTimeEvent anEvent);
public abstract int getDispatcherType();

Il primo metodo è quello che esegue il match tra il tempo generato dal cloccker ed il tempo di partenza del task. Se coincidono deve ritornare true. L'implementazione più significativa la troviamo nella classe TaskDateTime che è una specializzazione di TaskBase per l'esecuzione di tutti quei task che devono partire ad un'ora e in un giorno preciso:

public boolean isRunnable(ChangeTimeEvent anEvent)
{
String vTemp="";
// Year
String vTemp2=getYear();
if(vTemp2.equals("****"))
vTemp2=anEvent.getYear();
vTemp+=vTemp2;
vTemp+="-";
// Month
vTemp2=getMonth();
if(vTemp2.equals("**"))
vTemp2=anEvent.getMonth();
vTemp+=vTemp2;
vTemp+="-";
// Day
vTemp2=getDay();
if(vTemp2.equals("**"))
vTemp2=anEvent.getDay();
vTemp+=vTemp2;
vTemp+=" ";
// Hour
vTemp2=getHour();
if(vTemp2.equals("**"))
vTemp2=anEvent.getHour();
vTemp+=vTemp2;
vTemp+=":";
// Minute
vTemp2=getMinute();
if(vTemp2.equals("**"))
vTemp2=anEvent.getMinute();
vTemp+=vTemp2;
vTemp+=" ";
// Day of week
vTemp2=getWeekDay();
if(vTemp2.equals("**"))
vTemp2=anEvent.getWeekDay();
vTemp+=vTemp2;
return vTemp.equals(anEvent.getDateTime());
} // end method

Il meccanismo, nonostante le apparenze è piuttosto semplice ed abbastanza potente da permettere una buona schedulazione dei task. Un task di tipo DateTime ha una data ed un'ora di partenza. Il metodo costruisce una stringa temporanea formata dall'anno, il mese, il giorno, l'ora ed i minuti della partenza del task. Ogni qual volta al posto del dato effettivo vengono trovati degli asterischi, questi vengono sostituiti con il relativo valore dell'evento temporale generato dal cloccker. Alla fine la stringa temporanea e la stringa temporale del cloccker sono confrontate. Se risultano uguali il task può partire.

Questo meccanismo permette di definire in maniera abbastanza intuitiva e potente task con cadenza ciclica. Supponiamo di voler settare un task che venga attivato ogni giorno alle 13:55. Basterà scrivere come tempo di attivazione nell'apposito file xml la seguente data:

****-**-** 13:55 **

Il primo gruppo di 4 asterischi rappresenta l'anno. Il fatto che siano asterischi significa, praticamente, che qualsiasi anno va bene. Analogo discorso vale per il mese (successivi due asterischi) e il giorno (i due asterischi ancora successivi). Segue poi la specificazione dell'ora e del minuto di partenza del task. Gli ultimi due asterischi rappresentano il giorno della settimana.

Riportiamo altri esempi di schedulazione con relativo commento per chiarire ulteriormente il meccanismo:

****-**-** 15:22 Sa

(Schedula un task che parte ogni sabato alle 15 e 22 minuti)

2004-**-** **:10 **

(Schedula un task che parte tutti i giorni, alle 10 di ogni ora, ma solo per l'anno 2004)

Naturalmente se viene messa la data e l'ora completa, senza alcun campo asteriscato, il task verrà eseguito una sola volta alla data ed all'ora specifica.

Dopo questa lunga digressione sulla schedulazione dei task in un file di configurazione in xml di cui parleremo più avanti, torniamo ad analizzare il secondo metodo astratto della classe TaskBase:

public abstract int getDispatcherType();

Questo metodo specifica il tipo di task. E' importante perchè in base a questo valore, il task viene asseggnato al dispatcher appropriato. In pratica esiste una relazione uno ad uno tra i tipi di dispatcher ed i tipi di task. A conferma di cio, possiamo guardare l'implementazione del metodo nella classe TaskDay:

public int getDispatcherType() {return EventDispatcher.EDT_DAY;}

Esso ritorna la costante definita nella classe dalla quale derivano tutti i dispacciatori di eventi.

 

Il file tasks.xml
I task che devono essere eseguiti vengono specificati in un file di configurazione in formato xml che ha la seguente struttura:

<TaskLisst>
  <Task>
    <UniqueName>TestMinuto</UniqueName>
    <DateTime>Minute</DateTime>
    <ClassName>org.illeva.bittime.task.CiaoMinuto</ClassName>
  </Task>
  <Task>
    <UniqueName>TestOra</UniqueName>
    <DateTime>Hour</DateTime>
    <ClassName>org.illeva.bittime.task.CiaoOra</ClassName>
  </Task>
</TaskLisst>

Il tag TaskLisst è una collezione di Task e ciascun task è formato dai seguenti tag:

  • UniqueName - E' un nome univoco all'interno della lista;
  • DateTime - E' il tipo di task. Può assumere i seguenti valori:
    • Minute - scatta ogni minuto
    • Hour - scatta ogni ora;
    • Day - scatta ogni giorno;
    • Month - scatta ogni mese;
    • Year - scatta ogni anno;
    • 2004-**-** 12:15 ** - scatta nella data impostata;
  • ClassName - E' il nome della classe che rappresenta il task;
  • ThreadClassName - E' il nome del thread da caricare dinamicamente;
  • WorkDir - E' una directory di lavoro che viene passata al task.

Gli ultimi due parametri sono facoltativi e possono essere omessi.

 

Il TaskManager

La classe TaskManager svolge due compiti fondamentali:

  • A partire da un file xml crea dinamicamente i task da eseguire;
  • Raccoglie e fa interagire tra loro tutti i componenti visti fino ad ora.

Questa classe è, di fatto, il punto di entrata dell'applicazione. Chi vuole usare lo scheduler deve istanziare un oggetto di questa classe passandogli nel costruttore il file xml che contiene la lista dei task da eseguire.

TaskManager dTask=new TaskManager(new File("tasks.xml"));

Nel package gui si trova una semplicissima interfaccia grafica per eseguire e fermare il TaskManager utilizzando i metodi di start e stop che altro non fanno se non richiamare i rispettivi metodi del cloccker.


public void start()
{
dClock=new Cloccker();
addObservers();
dClock.start();
dLog.log("Taskmanager partito.");
} // end method
public void stop()
{
if(dClock!=null)
{
dClock.stop();
dClock=null;
dLog.log("Taskmanager stopato.");
} // end if
} // end method

 

A parte il richiamo all'aggiormamento del file di log, va sottolineata nel metodo start() l'esecuzione del metodo addObservers() che aggiunge come osservatore del cloccker il gruppo dei dispacciatori. Questi due oggetti sono dati membro della classe TaskManager:

protected void addObservers(){
dClock.addObserver(dDispatcher);
} // end method

Con questo nostro approccio all'analisi dei sorgenti un po' bottom-up siamo arrivati alla classe TaskManager che è la sintesi di tutte le nostre fatiche. Approfittiamo di questo fatto per riassumere, grazie ad un class-diagram tutto quello che abbiamo visto fino ad ora:

 

 

L'integrazione con Tomcat
Ultimo scoglio da superare è l'integrazione del nostro TaskManager con un'applicazione di Tomcat. Come un maldestro scrittore di gialli anticipo subito l'assassino che come al solito è il maggiordomo, nel nostro caso il ServletContextListener.

La bellezza e potenza di Tomcat non finisce mai di stupirmi, figuratevi i miei salti di gioia quando ho scoperto che è possibile costruire un listener in grado di intercettare due eventi fondamentali:

  • la nascita di un contesto applicativo;
  • la distruzione di un contesto applicativo.

A questo punto basta creare un TaskManager quando nasce il contesto applicativo che ci interessa e distruggere lo stesso TaskManager quando il contesto dell'applicazione viene distrutto.

Primo passo per riuscire a fare tutto questo è la costruzione di una classe che implementi l'interfaccia ServletContextListener. Questo ci costringe all'implementazione di due metodi:

public void contextInitialized(ServletContextEvent event)
public void contextDestroyed(ServletContextEvent event)

che, vuole il buon caso, sono proprio i due metodi che partiranno alla nascita ed alla morte del nostro contesto dell'aplicazione di Tomcat.

Vediamo in ordine logico l'implementazione che abbiamo fatto del primo metodo, scatenato da Tomcat sul nascere della nostra applicazione web. Anticipo che dei due è anche il metodo più complesso:

public void contextInitialized(ServletContextEvent event)
{
ServletContext vContext=event.getServletContext();
String vTaskFileNameXml=vContext.getInitParameter("TaskFileNameXml");
System.out.println("*** Illeva ***"+vTaskFileNameXml);
TaskManager vTask=new TaskManager(new File(vTaskFileNameXml));
vContext.setAttribute(ARCIBALD,vTask);
vTask.start();
} // end method

Operazione preliminare e fondamentale è quella di recuperare il ServletContext. Da questo possiamo leggere un parametro di configurazione che è il file xml dove sono presenti i task da eseguire. Subito dopo creiamo il TaskManager e lo inseriamo nel ServletContext assegnandogli un nome. Ora non resta che far partire il TaskManager!

Parametri del contesto

Nell'istanziare una classe del TaskManager va indicato all'oggetto dove trovare il file xml contenente la lista dei task da eseguire. Utilizzando il TaskManager con Tomcat, una delle soluzioni più semplici per passare questo file all'ogetto è quello di definire un parametro a livello del contesto applicativo che stiamo utilizzando.

Fare questo è molto semplice, basta definire nel file "web.xml" dell'applicazione il nome ed il valore del parametro:

<context-param>
<param-name>TaskFileNameXml</param-name>
<param-value>D:/jwsdp-1.3/webapps/risorse/WEB-INF/tasks.xml</param-value>
</context-param>

A questo punto risulta banalmente chiaro il motivo dell'istruzione:

String vTaskFileNameXml=vContext.getInitParameter("TaskFileNameXml");

presente nel nostro ContextListeners. Essa va a recuperare proprio il valore del parametro che indica al TaskManager dove trovare il file xml.

Ultimo passo affinchè il nostro listener possa diventare effettivamente operativo è quello di informare Tomcat di utilizzare il ServletContext da noi costruito. Ancora una volta basterà inserire poche righe nel file "web.xml":

<listener>
<listener-class>listeners.Context</listener-class>
</listener>

Ma non pensate sia finita qui, il più bello deve ancora venire...

 

Controllare il TaskManager via Web!!!
Nell'ambito del nostro ContextListener, essere riusciti ad inserire il TaskManager come oggetto del ServletContext con l'istruzione:

vContext.setAttribute(ARCIBALD,vTask);

ci permette di andare a recuperarlo in una pagina JSP oppure all'interno di una servlet per poterlo gestire anche a livello dell'interfaccia web dell'applicazione.

A titolo puramente indicativo, riportiamo il sorgente di una pagina JSP che visualizza lo stato del TaskManager:

 

<%@ page language="java" import="org.illeva.bittime.agent.TaskManager" %>
<jsp:useBean id="Maggiordomo" scope="application" class="org.illeva.bittime.agent.TaskManager" />
<HTML>
<HEAD>
<TITLE>Test Maggiordomo</TITLE>
</HEAD>
<BODY>
<CENTER> Il TaskManager e'
<%
if(Maggiordomo.isRunning()) out.print("attivo");
else out.print("disattivato");
%>
</CENTER>
</BODY>
</HTML>

Da sottolineare lo scope dello useBean e l'ID che deve essere uguale alla stringa che abbiamo utilizzato per inserire il TaskManager nel servletContext.
Naturalmente potete sbizzarrirvi sulla gestione del TaskManager; si possono, per esempio, attivare o disattivare i task schedulati.

 

Conclusione
Mai fidarsi di una donna che ti chiede qualcosa utilizzando l'avverbio "naturalmente". Chi l'avrebbe mai detto che il pensare e costruire un task manager ci avrebbe coinvolto in una simile maratona. Ma alla fine ce l'abbiamo fatta! Ripercorriamo brevemente la lunga strada effettuata:

  • abbiamo costruito un thread che con risoluzione temporale di un minuto lancia degli eventi che chi è in ascolto può intercettare;
  • abbiamo costruito un gruppo di dispacciatori di questi eventi, raggruppati in un ascoltatore;
  • abbiamo costruito un task manager capace di istanziare dinamicamente dei task da lanciare nel momento esatto in cui l'evento deve partire;
  • last but not least, abbiamo costruito il tutto pensando ad una buona integrazione con Tomcat e ci siamo riusciti grazie alla ottima architettura di questo servlet engine.

Termino con le solite raccomandazioni riguardanti l'affidabilità dell'applicativo. Per quanto sia perfettamente funzionante il prodotto deve essere considerato di carattere prettamente didattico.


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it