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
listante come unità e non già come un
prima e un poi nel movimento e neppure come quellentità
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 lora, il giorno, il mese, lanno.
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.
|