Il web è sempre più veloce, o per lo meno deve dare questa percezione a chi lo usa: creare interfacce responsive al pari di quelle desktop è la sfida vinta col massiccio uso di chiamate Ajax che introducono i concetti di “render parziale” della pagina e “chiamate asincrone” rispetto al caricamento dell’intera pagina. Il concetto di asincrono lato server invece non è mai stato nuovo: riuscire ad inviare una richiesta che verrà soddisfatta in un secondo momento permette di distribuire il carico computazionale sul server nel tempo e soprattutto generare risposte veloci verso il client anche quando si richiedono operazioni onerose. La piattaforma Java Enterprise fornisce da tempo soluzioni architetturali o meno che permettono di raggiungere questo scopo. Dalla versione EJB 3.1 poi, è stato introdotto il concetto di EJB Asincrono che semplifica notevolmente le cose.
Chiamate asincrone e code JMS
Fino a ieri, chiamate asincrone al server su Java EE era sinonimo di Message Driven Bean (MDB). Con una chiamata sincrona al server, viene creato e inviato un messaggio ad una coda, dopodiché il controllo torna al client. Su questa coda sono in ascolto uno o più listener (gli MDB appunto) che prelevano il messaggio dalla coda e ne soddisfano le richieste. Abbiamo già discusso ampiamente di questa architettura in diversi post: abbiamo visto , come si configura, come si usa e come è possibile recuperare i risultati asincroni da un’altra coda tramite il .
E’ evidente che non è banale mettere in piedi un’architettura di questo tipo e se si ha bisogno di poche chiamate asincrone è proprio uno spreco di risorse e tempo.
Chiamate asincrone e EJB Timer
Qualcuno si chiederà: ma non basta staccare un semplice thread durante una chiamata e far proseguire il lavoro a lui? La risposta può essere banale, ma la spiegazione no. Diciamo che è meglio evitare di gestire manualmente thread all’interno di un Application Server per non andare incontro ad una morte lenta, ma inesorabile.
Invece che staccare un thread manualmente, possiamo farlo fare ad un EJB Timer Single Action, per esempio, schedulato a scadere al momento della della sua inizializzazione!! Con un semplice trucco, si riesce così a creare una chiamata asincrona eseguita in transazione con pochissimo sforzo : dopo l’inizializzazione del timer, il controllo verrà restituito subito al client e al timeout (ovvero subito) verrà eseguita l’operazione in modo asincrono su un altro thread. Se si tratta di una azione “fire and forget” (cioè che non necessità di feedback), questa soluzione è più che sufficiente, ma se è necessario recuperare il risultato dell’operazione o sapere se questa è andata a buon fine, è necessario inventarsi qualche strada.
Una soluzione potrebbe essere sfruttare la cache distribuita presente in molti Application Server (abbiamo già visto per esempio) per memorizzare il risultato restituito dal task asincrono. Con questa soluzione, anche in un ambiente clusterizzato, siamo sicuri che il risultato sia disponibile e univoco per tutti i nodi.
@Stateless
public class AsyncTimerEJB implements AsyncTimerEJBLocal {
private static final Logger logger =
Logger.getLogger(AsyncTimerEJB.class);
@EJB
private TimerResultCacheEJB cacheEJB;
@Resource
private TimerService service;
@Override
public void prepareAsyncAction(String id) {
logger.info("Request thread " + Thread.currentThread().getId());
this.service.createTimer(0, id);
}
@Timeout
public void doAsyncAction(Timer timer) {
try {
logger.info("Async thread " + Thread.currentThread().getId());
TimeUnit.SECONDS.sleep(5);
this.cacheEJB.getAsyncResult().put((String) timer.getInfo(), true);
} catch (Throwable t) {
this.cacheEJB.getAsyncResult().put((String) timer.getInfo(), false);
t.printStackTrace();
}
}
@Override
public Boolean getResult(String id) {
Boolean result = null;
if (this.cacheEJB.getAsyncResult().containsKey(id)) {
result = this.cacheEJB.getAsyncResult().get(id);
this.cacheEJB.getAsyncResult().remove(id);
}
return result;
}
}
-
TimerResultCacheEJB
è un EJB che gestisce la cache distribuita e la presenta ad un client (cioè l’EJB che la usa) come una mappa. Sarà responsabilità del client inserire e rimuovere l’entry dalla cache. Si sarebbe potuto usare allora una semplice HashMap Java? Meglio di no: la cache infatti permette di sfruttare i servizi di aggiornamento, manutenzione e invalidazione che il server opera su di essa. Se un client quindi si dimenticasse di rimuovere un elemento dalla cache, col tempo verrebbe invalidato dal server. -
prepareAsyncAction(String id)
imposta il timer per scadere immediatamente e riceve un parametro, per esempio il nome utente loggato, che farà da chiave alla mappa e che permetterà di recuperare il risultato. -
doAsyncAction(Timer timer)
esegue l’operazione asincrona e inserisce l’esito di esecuzione positiva (true
) nella cache se non si sono verificate eccezioni. Se lasciassimo passare le eccezioni dall’EJB, la transazione verrebbe marcata da “rollbackare” e il server tenterebbe di rieseguire l’operazione almeno una volta, come da specifica. Nel mondo reale il numero dei tentativi è indefinito e dipende dall’Application Server in uso: è sempre bene quindi catturare le eccezioni. -
getResult(String id)
restituisce il risultato dell’operazione, rimuovendolo prima dalla cache, se viene trovato.
I nuovi EJB Asincroni
Finché lavoriamo con Java EE 5, una delle soluzioni mostrate è pressoché obbligatoria. Fortunatamente Java EE 6 ha reso più naturale e strutturata quest’esigenza non così recondita dopotutto: EJB 3.1 infatti introduce la nuova annotazione @Asyncronous
che permette di rendere asincrona la chiamata ad un intero EJB o solo ad un suo metodo. La firma di un metodo asincrono ha un solo vincolo:
- se l’azione è di tipo “fire-and-forget“, il tipo ritornato sarà
void
; - se l’azione è di tipo “retrieve-result-later“, il tipo ritornato sarà
Future
.
@Stateless
public class AsyncEJB implements AsyncEJBLocal {
@Override
@Asynchronous
public Future doLongProcessingAction() {
long startMillis = System.currentTimeMillis();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
long stopMillis = System.currentTimeMillis();
return new AsyncResult("Elapsed time in millis: " + (stopMillis - startMillis));
}
}
-
doLongProcessingAction()
eseguirà l’operazione in modo asincrono rispetto alla chiamata: il client si troverà un riferimento all’oggettoFuture
che, una volta conclusa l’operazione, indicherà il tempo che ha impiegato in una stringa. - l’oggetto
Future
è un wrapper del risultato. I suoi metodi più usati sono:-
cancel(boolean mayInterruptIfRunning)
: cancella l’esecuzione asincrona; -
isDone()
: controlla che il processo sia terminato e il risultato salvato (è vera anche nel caso in cui sia stato cancellato); -
get(long timeout, TimeUnit unit)
: preleva il risultato o attende per timeout unit di tempo; -
get()
: preleva il risultato o attende finché non è pronto (cioè finchéisDone()
è vera).
-
Conclusioni
EJB 3.1 permette finalmente di creare processi asincroni rispetto all’elaborazione principale in modo estremamente semplice, soprattutto per quanto riguarda la gestione del risultato dell’operazione. Come abbiamo visto nelle prime due soluzioni, le azioni del tipo “retrieve-result-later” sono sempre quelle più difficili da gestire e richiedono il supporto dei servizi dell’Application Server, come le code o la cache, per poter recuperare i risultati. Con i metodi @Asyncronous
, il server sfrutta appieno la potente concurrent API di Java, liberando lo sviluppatore dal problema della gestione dei thread e classi callable, nascondendo il tutto dietro una semplice chiamata EJB. Probabilmente infatti, il codice nascosto dal proxy EJB sarà qualcosa che somiglia a:
...
ExecutorService threadExecutor = Executors.newSingleThreadExecutor();
Future futureResult = threadExecutor.submit(new Callable() {
@Override
public String call() throws Exception {
return asyncEJB.doLongProcessingAction();
}
});
...
dove cioè la chiamata al metodo EJB sarà wrappata ed eseguita come task futuro.
Pingback: ()