Premessa
Il primo agente
Il codice seguente illustra il primo agente che andremo a costruire. Esso non fa nulla di particolare (non si muove, non comunica, non crea nuovi agenti e via dicendo) ma si limita a mostrare nella finestra del place di creazione una frase diversa dal solito saluto al mondo (ciatata da Puck in A Midsummer Night's Dream, di William Shakespeare, Act 3, Scene 2). import SOMA.agent.*; /** * Just a try to build an agent and let it say a shakespearian quotation. * * @author Giulio Piancastelli * @version 1.0 - Sunday 10th February, 2002 */ public class TryAgent extends Agent { public void run() { agentSystem.getOut().println("Lord, what fools these mortals be\n"); } } // end TryAgentTutti gli agenti di SOMA devono estendere la classe astratta SOMA.agent.Agent. L'unico metodo astratto che questa classe presenta è il metodo run(), che andrà quindi implementato in ogni agente, e che sarà il metodo a partire dal quale l'agente verrà messo in esecuzione. La classe Agent contiene infatti un campo stringa che identifica il nome del metodo da cui far partire l'agente, ed esso viene settato automaticamente al valore "run" all'atto di creazione di un agente. /** * @serial * Metodo che verra' eseguito alla prossima attivazione dell'agente. */ public String start = "run";Che cosa fa il nostro agente? Usa un riferimento ad un oggetto di tipo SOMA.agent.AgentSystem mantenuto internamente alla classe Agent per recuperare lo stream di output dell'environment corrente e stampare su di esso la citazione shakespeariana che abbiamo prescelto. La classe AgentSystem funge da interfaccia tra l'agente ed il sistema sottostante: al momento della attivazione di un agente in un place, il riferimento ad un AgentSystem è l'unica possibilità che ha l'agente di interagire con l'infrastruttura ad agenti mobili. L'interazione tra agente e sistema tramite un oggetto di tipo AgentSystem avviene tipicamente attraverso i seguenti metodi: public PlaceID getPlaceID() public Environment getEnvironment(); public InputStream getIn() { return getEnvironment().in; } public PrintStream getOut() { return getEnvironment().out; } public PrintStream getErr() { return getEnvironment().err; } /** * Restituisce l'elenco degli identificatori dei place di questo dominio. */ public PlaceID[] getPlaces(); /** * Restituisce l'elenco degli identificatori dei domini, o * un array vuoto se non e' presente un Domain Name Service, perche' * non siamo in un default place. */ public PlaceID[] getDomains(); /** * Restituisce il numero di worker e quindi di agenti del place. */ public int agentsNumber()Tra essi, vediamo anche il metodo getOut() che abbiamo appena utilizzato. Esso ci restituisce un riferimento ad un oggetto di tipo java.io.PrintStream che useremo tramite il metodo println() per mostrare a video la nostra citazione. Dopo aver compilato l'agente, averlo copiato nella directory agents relativa alla installazione di Soma 4.0, ed aver aperto, tramite GUI, una finestra di place relativa ad un dominio di prova precedentemente costruito, possiamo lanciare il nostro agente TryAgent e vederne a video gli effetti sulla finestra di place.
Interagire con l'Environment: i Place e i Domini Oltre a fornire uno stream di output come abbiamo appena visto, la classe Environment mette a disposizione diversi metodi per interagire con le facilities disponibili su ogni place, come ad esempio il Domain Name System ed il Place Name System. Il DNS è una tabella che contiene il sottoalbero gerarchico dei domini relativo al Default Place su cui ci troviamo. Dato che i domini di SOMA sono inseriti in un albero gerarchico per permettere una elevata scalabilità del sistema, al momento della creazione di un Default Place occorre specificare qual è il suo Default Place genitore, a meno che non si stia creando quello che diventerà la radice dell'albero. Ogni Default Place ha quindi una tabella di DNS che contiene necessariamente il nome del suo genitore e dei suoi figli. Può inoltre contenere altri nomi che possono essere utili, ma non è richiesto che in ogni tabella siano memorizzati i nomi di tutti i Default Place. Il PNS è una tabella in cui sono contenuti tutti i nomi dei Place appartenenti ad un certo dominio, in quanto, per come SOMA è strutturato, tutti i Place che appartengono ad uno stesso dominio si devono conoscere l'un l'altro. Il proprietario di questa tabella è però il Default Place, che provvede a copiarla ai nuovi Place al momento della loro creazione. Vediamo quindi un esempio di un agente che interagisce con le tabelle di DNS e PNS gestite dall'environment. import SOMA.agent.*; import SOMA.naming.*; import SOMA.naming.domain.*; import java.util.*; /** * A try to interact with the Environment and the Domain Name Service. * * @author Giulio Piancastelli * @version 1.0 - Sunday 10th February, 2002 */ public class EnvAgent extends Agent { private Vector placesToVisit = new Vector(); public void run() { PlaceID home = agentSystem.getPlaceID(); if (home.isDomain()) { // Get children domains DomainNameService dns = agentSystem.getEnvironment().domainNameService; Vector children = dns.getChildrenDNS(); // a Vector of PlaceIDs for (int index = children.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) children.get(index); placesToVisit.add(place); } // Get places in this domain Vector places = new Vector(Arrays.asList(agentSystem.getPlaces())); places.remove(home); for (int index = places.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) places.get(index); placesToVisit.add(place); } } else placesToVisit.add(home); // Print results for (int index = placesToVisit.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) placesToVisit.get(index); agentSystem.getOut().println(place); } } } // end EnvAgentSalta subito agli occhi che la classe SOMA.naming.domain.DomainNameService, rappresentante il gestore del Domain Name System, è un membro pubblico della classe SOMA.Environment, e che tra i metodi di accesso alle informazioni in esso contenute spicca getChildrenDNS(). Esso restituisce un java.util.Vector contenente i PlaceID di tutti i figli del Default Place corrente, e nell'esempio viene difatti richiamato solo se il Place corrente è un Default Place e qunidi effettivamente possiede una tabella di DNS (il controllo è effettuato dal predicato isDomain() della classe SOMA.naming.PlaceID). Il contenuto della tabella del Place Name System è invece restituito dal metodo getPlaces() della classe AgentSystem, che in realtà non è altro che un wrapper per il metodo getPlacesArray() della classe SOMA.naming.place.PlaceNameService. Parallelamente, anche nella classe DomainNameService esiste un metodo getDomainsArray(), il quale restituisce tutti gli ID dei domini contenuti in una tabella di DNS, compresi dunque l'eventuale Default Place che svolge il ruolo di parent per il Default Place corrente, ed il Default Place corrente stesso.
Muovere un agente nel sistema I due agenti realizzati finora non facevano altro che interagire con il sistema in maniera locale al Place su cui erano stati creati. Vediamo ora com'è possibile spostare un agente da un Place all'altro. Il sistema SOMA mette a disposizione il metodo public void go(PlaceID destination, String method)appartenente alla classe Agent, come primitiva per realizzare il movimento di un agente attraverso il sistema. Il primo parametro rappresenta il Place verso cui l'agente vuole migrare, ed il secondo parametro è il nome del metodo pubblico che l'agente desidera eseguire una volta arrivato sul Place di destinazione. La spiegazione della particolare signature del metodo go() risiede nei limiti del linguaggio Java, che permette solamente la realizzazione di un particolare tipo di mobilità di codice chiamato mobilità debole. Al contrario di ciò che avviene nel modello forte di mobilità, in cui si realizza la migrazione del codice e del suo stato di esecuzione, il modello di mobilità debole prevede solo la migrazione del codice. La piattaforma Java non permette l'accesso allo stato di esecuzione dei thread, in particolar modo allo stack: non è possibile quindi catturare lo stato di esecuzione di un agente, nè tantomeno ripristinarlo. La cosa più simile alla mobilità forte che si può ottenere con un modello a mobilità debole è permettere alla applicazione di riprendere la propria esecuzione in un punto staticamente determinato, come ad esempio il punto di ingresso di un metodo appartenente alla classe dell'agente. L'applicazione decide quindi dove e quando migrare e, pur non potendo riprendere la propria esecuzione dal punto esatto in cui essa è stata interrotta, è quantomeno in grado di scegliere da che metodo ripartire, una volta ripristinata sul nodo destinazione della sua migrazione. Si noti che il trasferimento dell'agente da un place ad un altro, come conseguenza della invocazione del metodo go(), avviene in realtà tramite una clonazione (una copia) dell'agente stesso sul place di destinazione. L'esecuzione dell'agente originale, situato ancora sul place di partenza, non viene automaticamente bloccata nè dalla Java Virtual Machine, nè dal sistema SOMA: dopo la chiamata al metodo go(), l'agente originale continua la sua esecuzione. Avere nel sistema due agenti con lo stesso nome contemporaneamente in esecuzione può portare a problemi o inconvenienti, ed è perciò buona norma fare in modo che l'esecuzione dell'agente originale termini in modo naturale dopo l'invocazione del metodo go(), inserendo questa come ultima chiamata nel metodo che la contiene. Un esempio di un agente che si muove all'interno del dominio nel quale è stato generato può essere sintetizzato nel seguente codice. import SOMA.agent.*; import SOMA.naming.*; import SOMA.naming.domain.*; import java.util.*; /** * A try to move an agent around the system. * * @author Giulio Piancastelli * @version 1.0 - Monday 11th February, 2002 */ public class RegionTourAgent extends Agent { // Serializable members private Vector placesToVisit = new Vector(); public void run() { list(); // Move agent on the other domains or places just listed if (placesToVisit.size() > 0) { PlaceID placeToGo = (PlaceID) placesToVisit.get(0); placesToVisit.remove(placeToGo); try { go(placeToGo, "run"); } catch (CantGoException e) { e.printStackTrace(); } } else agentSystem.getOut().println("Tour finished!"); } /** Very similar to the run() method in EnvAgent */ public void list() { PlaceID home = agentSystem.getPlaceID(); if (home.isDomain()) { // Get children domains DomainNameService dns = agentSystem.getEnvironment().domainNameService; Vector children = dns.getChildrenDNS(); // a Vector of PlaceIDs for (int index = children.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) children.get(index); placesToVisit.add(place); } // Get places in this domain Vector places = new Vector(Arrays.asList(agentSystem.getPlaces())); places.remove(home); for (int index = places.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) places.get(index); placesToVisit.add(place); } } // Print results for (int index = placesToVisit.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) placesToVisit.get(index); agentSystem.getOut().println(place); } } } // end RegionTourAgentSi noti come l'invocazione del metodo go() svolga il ruolo di ultima chiamata all'interno del metodo run() in cui è contenuta, almeno per quel particolare flusso di esecuzione che entra all'interno della clausola if. In buona sostanza, nello scrivere il codice di ogni agente, si deve cercare di fare in modo che nessuna altra chiamata segua il metodo go(), cosicché l'esecuzione dell'agente possa terminare subito dopo aver effettuato la migrazione. Se il place di destinazione per la migrazione è caduto o non è più raggiungibile, il metodo go() genera una eccezione di classe SOMA.agent.CantGoException. Ecco il motivo per cui l'invocazione del metodo go() è racchiusa in un blocco try/catch, così da poter catturare l'eccezione generata e gestire la condizione verificatasi in maniera adeguata (in modo magari più evoluto rispetto alla semplice stampa della stack trace come accade nell'esempio). Esiste però un caso in cui una migrazione che non ha avuto successo non porta alla generazione della relativa CantGoException. Supponiamo infatti che il place di destinazione non sia conosciuto dal place in cui correntemente si trova l'agente, cioè che il nome del place di destinazione non sia contenuto nella sua tabella di PNS. In questo caso, la migrazione avviene verso il Default Place, dove si cercherà di risolvere l'associazione col nome logico controllando la tabella del Domain Name Service. Ma se neppure questo tentativo ha successo, la generazione di una CantGoException non potrà avvenire, a causa della perdita dello stato di esecuzione caratteristico di Java (lo stack) dovuta alla migrazione appena verificatasi. Per evitare errori causati da un agente che riprenda la sua esecuzione su un place diverso da quello in cui si aspettava di ritrovarsi, è consigliabile controllare che, a seguito di una richiesta di migrazione, l'agente si trovi effettivamente sul place che desiderava raggiungere. Ha quindi senso scrivere il seguente codice, qui riportato come esempio generico. protected PlaceID destination; public void run() { try { // save the destination for later checking destination = placeToGo; go(placeToGo, "startMethod"); } catch (CantGoException e) { // manage the exception } } public void startMethod() { // get the current place PlaceID currentPlace = agentSystem.getPlaceID(); if (currrentPlace.equals(destination)) { // I am where I am supposed to be } else { // I did not reach my destination! } }Nel caso il dominio sia definito in maniera particolare (ad esempio sia costituito solo da Defaul Place situati su diverse macchine e sia completamente noto ad ogni nodo) è possibile semplificare il codice dei propri agenti evitando questo ulteriore controllo.
Creare nuovi agenti Un agente non è un thread, in quanto Java non ne permetterebbe la serializzazione, bensì un oggetto passivo: chi si occupa materialmente del flusso di esecuzione di un agente è una entità chiamata worker. Nel momento in cui un agente viene creato o arriva su un place attraverso la rete, il sistema crea un worker a cui l'agente viene affidato: il worker invoca il metodo di lancio dell'agente, per poi attendere che l'agente termini la propria esecuzione. transient AgentWorker worker = null;Il worker è definito transient poichè, essendo in realtà un thread, non può migrare insieme all'agente. Piuttosto, quello che avviene è la creazione di un nuovo worker in ogni Place in cui l'agente arriva. Per creare un agente e metterlo in esecuzione è quindi innanzitutto necessario associargli un worker. Questa operazione si effettua attraverso il metodo createAgent() della classe SOMA.agent.mobility.AgentManager. /** * Creazione di un agente. * @param agentName Nome dell'agente. * @param argument Parametro di inizializzazione, vedi * {@link SOMA.agent.Agent#putArgument(Object obj)}. * @param isSystemAgent Se a true si forza l'utilizzo del classloader di sistema * @param traceable Se a true l'agente è traceable ed ha una mailbox. */ public AgentWorker createAgent(String agentName, Object argument, boolean isSystemAgent, boolean traceable) { AgentWorker worker = null; try { AgentID newID = newAgentID(); ClassLoader classLoader; if (isSystemAgent) // Lo stesso ClassLoader della classe attuale! classLoader = getClass().getClassLoader(); else classLoader = new AgentClassLoader(env, agentName, newID); Agent agent = (Agent) classLoader.loadClass(agentName).newInstance(); agent.setID(newID); agent.putArgument(argument); agent.setTraceable(traceable); worker = createWorker(agent); } catch( Exception e ) { e.printStackTrace(env.err); } return worker; }Il metodo cerca la classe dal nome agentName, la carica in memoria tramite il Class Loader (usando il caricatore di classi del sistema se l'argomento isSystemAgent è impostato a true), e ne effettua il cast a SOMA.agent.Agent. Dopodichè viene creato un nuovo thread worker a cui viene assegnato l'agente appena creato. Il worker non viene però fatto partire, bensì ne viene restituito un riferimento a chi ha invocato createAgent(). Se il riferimento è null, ciò significa che non è stato possibile creare l'agente. In caso la creazione abbia successo, per lanciare il worker (e quindi porre in esecuzione l'agente) è sufficiente invocarne il metodo start(). Il secondo argomento del metodo createAgent() rappresenta una struttura contenente parametri utili alla inizializzazione dell'agente. L'ultimo argomento indica la tracciabilità di un agente all'interno del sistema: se un agente possiede o meno una mailbox (discussa più avanti in questo documento) risulta rintracciabile o meno da parte del sistema SOMA. Un esempio della creazione di nuovi agenti può essere costituito da una coppia di classi: la prima rappresenta un agente con il ruolo di base, atto a creare altri agenti e a recuperare i place su cui farli migrare; la seconda rappresenta un agente che, creato dall'agente base, effettua un singolo spostamento. import SOMA.agent.*; import SOMA.agent.mobility.*; import SOMA.naming.*; import SOMA.naming.domain.*; import java.util.*; /** * This class represents the base agent, and it creates LurkerTryAgent(s) in order * to explore the first-level domains and places. Note that the base agent is fixed * (i.e. it does not move itself around the domains). * * @author Giulio Piancastelli * @version 1.0 - Wednesday 13th February, 2002 */ public class BaseTryAgent extends Agent { public void run() { Vector placesToVisit = new Vector(); PlaceID home = agentSystem.getPlaceID(); SOMA.Environment env = agentSystem.getEnvironment(); if (home.isDomain()) { // Get children domains Vector children = env.domainNameService.getChildrenDNS(); for (int index = children.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) children.get(index); // update the list of places to visit placesToVisit.add(place); } // Get places in this domain Vector places = new Vector(Arrays.asList(agentSystem.getPlaces())); places.remove(home); for (int index = places.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) places.get(index); // update the list of places to visit placesToVisit.add(place); } } for (int index = placesToVisit.size() - 1; index >= 0; index--) { PlaceID placeToGo = (PlaceID) placesToVisit.get(index); Vector v = new Vector(); v.add(home); v.add(placeToGo); AgentWorker lurker = env.agentManager.createAgent("LurkerTryAgent", v, false, true); if (lurker != null) try { lurker.start(); } catch (AgentWorker.AgentWorkerException awe) { awe.printStackTrace(); } } } } // end BaseTryAgentSi notino le ultime righe del metodo run(): in esse si crea un java.util.Vector che contiene alcuni parametri passati dall'agente base agli agenti lurker. Come abbiamo già detto, il secondo argomento del metodo createAgent() appena analizzato rappresenta utili informazioni di inizializzazione per gli agenti creati, passate ad essi sotto forma di java.lang.Object. In questo esempio, si passano ad un LurkerTryAgent il place su cui esso viene creato ed il place di destinazione su cui esso dovrà migrare. Una volta passati, come vengono recuperati questi parametri? Diamo una occhiata al codice della classe LurkerTryAgent. import SOMA.agent.*; import SOMA.naming.*; import SOMA.naming.domain.*; /** * Coupled with BaseTryAgent, this agent realizes a first try to create and * move new agents from an initial place. * * @author Giulio Piancastelli * @version 1.0 - Wednesday 13th February, 2002 */ public class LurkerTryAgent extends Agent { // Serializable fields private PlaceID myHome; // the place where the lurker was created private PlaceID myNextPlace; // the place where the lurker was said to go public void putArgument(Object obj) { // the object is a Vector java.util.Vector v = (java.util.Vector) obj; myHome = (PlaceID) v.get(0); myNextPlace = (PlaceID) v.get(1); } public void run() { try { go(myNextPlace, "greetings"); } catch (CantGoException cge) { cge.printStackTrace(); } } public void greetings() { // get the current place PlaceID currentPlace = agentSystem.getPlaceID(); if (currrentPlace.equals(myNextPlace)) agentSystem.getOut().println("I come from " + myHome + "- I was said to go to " + myNextPlace + " and I am now in " + currentPlace); else agentSystem.getOut().println("I did not reach my destination!"); } } // end LurkerTryAgentLe informazioni contenute nella struttura passata ad un agente al momento della sua creazione vengono trasmesse al metodo putArgument(). Esso è il primo metodo che il sistema chiama per ogni agente creato su un qualsiasi place, ma la sua implementazione nella classe astratta Agent è vuota. /** * Permette di definire lo stato iniziale dell'agente. * Questo metodo è vuoto nella classe Agent e deve essere * ridefinito dalle sottoclassi che implementano agenti, in * maniera analoga a {@link #run()}. * @param obj Un oggetto contenente informazioni di inizializzazione. * Ovviamente l'oggetto può anche contenere una struttura dati complessa. */ public void putArgument(Object obj) {}Questo significa che se il programmatore non ridefinisce il metodo con la propria implementazione, l'effetto della sua chiamata da parte del sistema sarà nullo. Ma nel caso si vogliano passare dei parametri di inizializzazione ad un agente, essi si potranno accedere tramite l'argomento del metodo putArgument(), che dovrà qunidi essere ridefinito in maniera appropriata, a seconda del tipo di struttura usata per contenere le informazioni, e a seconda del tipo di informazioni passate. Si noti poi che, al momento della chiamata di putArgument() nel metodo createAgent() della classe AgentManager, il membro agentSystem interno alla classe dell'agente ancora non esiste: esso viene infatti settato solo all'interno del metodo createWorker(), che il metodo createAgent() chiama solo dopo aver chiamato putArgument(). public AgentWorker createWorker(Agent agent) { AgentWorker worker = null; try { // Se non c'è il security manager, non effettuo neanche i controlli. if( System.getSecurityManager() == null || agent.getClass().getProtectionDomain().implies(new PlaceAccessPermission(env.placeID))) { agent.agentSystem = agentSystem; // ... } // ... } catch( Exception e ) { e.printStackTrace(env.err); } return worker; }Ciò implica che nè l'AgentSystem stesso, nè tutte le altre strutture a cui si accede attraverso l'AgentSystem (come ad esempio gli Shared Objects trattati nel capitolo seguente), possono essere utilizzate all'interno del metodo putArgument(). La funzione per la quale esso è stato pensato, cioè passare agli agenti appena creati una struttura contenente parametri di inizializzazione, è anche l'unica funzione utile che esso può svolgere.
Comunicazione e coordinamento: gli Shared Objects In termini generali, si possono distinguere due forme di comunicazione tra agenti:
SOMA mette dunque unicamente a disposizione una tabella hash di oggetti condivisi, riferita all'interno di AgentSystem con il nome di sharedObjects. In essa è possibile memorizzare delle coppie {chiave, valore} in cui la chiave è un identificatore intero, ed il valore un qualsiasi oggetto Java. Un agente che deposita una coppia in sharedObjects renderà visibile il valore a tutti gli agenti che ne conoscano la chiave corrispondente. Un possibile esempio dell'uso degli sharedObjects può essere strutturato secondo lo schema comprendente un agente base e più agenti lurker già visto in precedenza. In questo caso, l'agente base depositerà nella tabella hash informazioni che gli agenti lurker provvederanno a manipolare. Infine, sempre l'agente base si occuperà di visualizzare il risultato del lavoro degli agenti lurker. Il codice dell'agente base è il seguente. import SOMA.agent.*; import SOMA.agent.mobility.*; import SOMA.naming.*; import SOMA.naming.domain.*; import java.util.*; /** * An example of a base agent using shared objects to coordinate its work * with other lurker agents. * Note that the base agent does not die, but waits for the lurker agent(s) * to return, and must be terminated manually, since it uses the idle() method * to suspend itself. * * @author Giulio Piancastelli * @version 1.0 - Wednesday 13th February, 2002 */ public class SObjBaseAgent extends Agent { public void run() { Vector placesToVisit = new Vector(); PlaceID home = agentSystem.getPlaceID(); SOMA.Environment env = agentSystem.getEnvironment(); if (home.isDomain()) { // Get children domains Vector children = env.domainNameService.getChildrenDNS(); for (int index = children.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) children.get(index); // update the list of places to visit placesToVisit.add(place); } // Get places in this domain Vector places = new Vector(Arrays.asList(agentSystem.getPlaces())); places.remove(home); for (int index = places.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) places.get(index); // update the list of places to visit placesToVisit.add(place); } } // Initialize the shared objects relative to all children domains and places for (int index = placesToVisit.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) placesToVisit.get(index); agentSystem.sharedObjects.put(index, place); } // Create lurkers which will move on first level domains and places, access shared // objects and somehow manipulate them for (int index = placesToVisit.size() - 1; index >= 0; index--) { Vector v = new Vector(); v.add(home); v.add(new Integer(index)); AgentWorker lurker = env.agentManager.createAgent("SObjLurkerAgent", v, false, true); try { lurker.start(); } catch (AgentWorker.AgentWorkerException awe) { awe.printStackTrace(); } } idle("printSharedObjects"); // wait } public void printSharedObjects() { for (int index = agentSystem.sharedObjects.size() - 1; index >= 0; index--) agentSystem.getOut().println(agentSystem.sharedObjects.get(index).toString()); } } // end SObjBaseAgentSi noti che le modalità di accesso agli sharedObjects sono quasi identiche a quelle di una qualsiasi java.util.Hashtable, con la differenza che in questo caso gli indici sono numerici invece di poter essere un qualsiasi oggetto Java. La struttura degli sharedObjects deriva in realtà dalla classe SOMA.utility.IndexHashtable che estende direttamente java.util.Hashtable, pur apportando alla sua interfaccia le modifiche appena segnalate. L'agente base aspetta sul metodo idle() che gli agenti lurker abbiano terminato le loro manipolazioni, entrando quindi nel cosiddetto stato Idle. Lo stato Idle, pur essendo uno stato di attesa di messaggi, differisce molto, ad esempio, dallo stato in cui l'agente si trova quando si mette in attesa di messaggi nella sua mailbox con la relativa primitiva (si veda il capitolo seguente per i dettagli sull'uso delle mailbox). Infatti, mentre nel secondo caso l'agente è attivo ma bloccato, quando si trova nello stato Idle l'agente è disattivo. Per illustrare la differenza tra questi due stati, basti pensare che, attendendo messaggi nella propria mailbox, un agente potrebbe sbloccarsi autonomamente allo scadere di un certo timeout prefissato pur non avendo ricevuto messaggi, mentre è invece concettualmente impossibile, per un agente, uscire dallo stato Idle in maniera autonoma. Una volta nello stato Idle, un agente viene congelato e affidato al sistema, e dovrà essere il sistema stesso a risvegliarlo al momento opportuno. Non usando una mailbox nel nostro esempio, l'unico modo di far attendere l'agente base per un tempo indefinito consiste nel farlo entrare nello stato Idle, da cui però non potrà risvegliarsi da solo: sarà necessario l'intervento manuale dell'utente per riattivare l'agente base e fargli stampare a video gli sharedObjects manipolati dagli agenti lurker.
Il codice relativo alla classe degli agenti lurker è il seguente. Si noti che, per permettere al lettore di concentrarsi sull'argomento del presente capitolo, sono stati omessi dal codice i controlli che ogni agente mobile dovrebbe effettuare per verificare di essere arrivato nel posto giusto dopo aver eseguito una migrazione. import SOMA.agent.*; import SOMA.naming.*; import SOMA.naming.domain.*; import java.util.*; /** * An example of a lurker agent coordinating with a base agent thanks to * the use of shared objects. * * @author Giulio Piancastelli * @version 1.0 - Wednesday 13th February, 2002 */ public class SObjLurkerAgent extends Agent { private PlaceID myHome; // the PlaceID the agent come from // myIndex really indicates the particular shared object property of the lurker agent, // i.e. the one and only object a single lurker agent will modify private int myIndex; private String manipulation = ""; public void putArgument(Object obj) { Vector v = (Vector) obj; myHome = (PlaceID) v.get(0); Integer temp = (Integer) v.get(1); // the agent destination place's index in the shared objects table myIndex = temp.intValue(); } public void run() { try { go((PlaceID) agentSystem.sharedObjects.get(myIndex), "buildString"); } catch (CantGoException cge) { cge.printStackTrace(); } } /** * Builds the String which will substitute the destination PlaceID entry in the * shared objects table. */ public void buildString() { // Get children domains if (agentSystem.getPlaceID().isDomain()) { SOMA.Environment env = agentSystem.getEnvironment(); Vector children = env.domainNameService.getChildrenDNS(); for (int index = children.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) children.get(index); manipulation += place.toString(); } } // Get places in this domain Vector places = new Vector(Arrays.asList(agentSystem.getPlaces())); places.remove(agentSystem.getPlaceID()); for (int index = places.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) places.get(index); manipulation += " " + place.toString(); } try { go(myHome, "manipulate"); } catch (CantGoException cge) { cge.printStackTrace(); } } public void manipulate() { // Substitute the old object (a PlaceID) with the String previously built agentSystem.sharedObjects.remove(myIndex); agentSystem.sharedObjects.put(myIndex, manipulation); // Print all the shared objects except the one just modified agentSystem.getOut().println("---" + getID() + "---" + myIndex + "---\n"); for (int index = agentSystem.sharedObjects.size() - 1; index >= 0; index--) if (index != myIndex) agentSystem.getOut().println( agentSystem.sharedObjects.get(index).toString()); } } // end SObjLurkerAgentVa sottolineato come gli Shared Objects siano una facility pensata per consentire lo scambio di oggetti tra agenti che risiedono seppur momentaneamente sullo stesso place, ma che non si conoscono l'un l'altro. Nel caso si desideri usare gli Shared Objects per altri fini, ci si dovrebbe domandare se non esista una maniera concettualmente più corretta di concretizzare le proprie intenzioni. Per fare un esempio, sebbene sia possibile inserire nella tabella hash oggetti che rappresentino parametri di inizializzazione che un agente appena creato legga al momento della esecuzione del suo metodo run(), questo tipo di uso degli Shared Objects non rispetta lo spirito con il quale lo strumento è stato creato, ed il sistema SOMA, come abbiamo visto parlando del metodo createAgent(), offre al problema della inizializzazione una soluzione più efficace, elegante, e soprattutto adeguata.
Comunicazione e coordinamento: la Mailbox Per quel che riguarda le forme di interazione stretta, illustrata all'inizio del precedente capitolo, ogni agente si porta dietro una propria mailbox, una rappresentazione della astrazione casella di posta nella quale vengono memorizzati i messaggi che arrivano in modo asincrono spediti dagli altri agenti. La mailbox mette a disposizione i seguenti metodi per inserire e prelevare messaggi: public synchronized void storeMessage(Message message) public syncrhonized Message getMessage() public synchronized boolean isMessage()La creazione dei messaggi da inviare ad altri agenti si effettua tramite il costruttore della apposita classe: public Message(Object message, AgentID from, AgentID to)Ogni messaggio porta quindi con sè informazioni relative al mittente e al destinatario, ed il suo contenuto informativo è rappresentato da un qualsiasi oggetto Java. Si noti che, come già detto, per spedire un messaggio, un agente deve conoscere il nome (l'identificatore unico) dell'agente destinatario. Compito del sistema è rintracciare l'agente destinatario e consegnargli il messaggio. Una volta creato il messaggio, per spedirlo bisognerà dunque consegnarlo al sistema attraverso il place in cui l'agente si trova, tramite il metodo sendMessage() di AgentSystem. Si notino poi le caratteristiche della azione di spedizione di un messaggio: essa è trasparente alla locazione, in quanto si deve specificare solo il nome dell'agente destinatario, ma non il place in cui esso si trova; essa è inoltre semanticamente asincrona, nel senso che il sistema si occupa della consegna senza notificare l'agente del successo o di un eventuale fallimento della operazione, e quindi senza dare garanzie sul fatto che il messaggio arrivi a destinazione; ma è operativamente bloccante, nel senso che, sebbene la spedizione venga effettuata dal sistema in modo che l'agente non debba attendere e possa proseguire la sua esecuzione successiva alla chiamata di sendMessage(), l'agente dovrà comunque attendere il tempo necessario a copiare il messaggio in una apposita area di sistema. Infine, si noti come anche la ricezione di un messaggio sia una operazione bloccante: nel momento in cui l'agente chiede la lettura di un messaggio, nel caso di mailbox vuota esso rimarrà bloccato fino a quando non ne arriverà uno. Questo comportamento è testimoniato dal codice del metodo getMessage() della classe SOMA.agent.Mailbox riportato qui di seguito. /** * Restituisce il primo messaggio in mailbox. La chiamata è sospensiva * ma esiste la possibilità di verificare se la Mailbox è piena. */ public synchronized Message getMessage() { try { while (messages.size() == 0) wait(); } catch (Exception e) { e.printStackTrace(); } return (Message) messages.removeFirst(); }La possibilità, citata nel commento JavaDoc, di verificare se la mailbox è piena, viene messa a disposizione dal metodo isMessage(). Possiamo creare un piccolo esempio in cui più agenti si coordinano attraverso una maibox, sfruttando il codice già scritto per l'esempio precedente concernente l'uso degli Shared Objects. Mentre prima era necessario un intervento esterno per consentire all'agente base di risvegliarsi e leggere il contenuto della tabella hash modificata dagli agenti lurker, ora il coordinamento verrà realizzato attraverso la mailbox. L'agente base manterrà in una variabile il numero di agenti lurker che ha creato; dopodichè, una volta che un agente lurker avrà finito il proprio lavoro, spedirà un particolare messaggio all'agente base; dopo aver ricevuto un numero di messaggi di un certo tipo pari al numero di lurker creati, l'agente base provvederà a presentare a video il contenuto degli Shared Objects. Il codice dell'agente base è il seguente. import SOMA.agent.*; import SOMA.agent.mobility.*; import SOMA.naming.*; import SOMA.naming.domain.*; import java.util.*; /** * An example of a base agent using its mailbox to coordinate its work * with other lurker agents. * * @author Giulio Piancastelli * @version 1.0 - Wednesday 13th February, 2002 */ public class MailboxBaseAgent extends Agent { private int numAgents = 0; private int numMessage = 0; public void run() { Vector placesToVisit = new Vector(); PlaceID home = agentSystem.getPlaceID(); SOMA.Environment env = agentSystem.getEnvironment(); if (home.isDomain()) { // Get children domains Vector children = env.domainNameService.getChildrenDNS(); for (int index = children.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) children.get(index); // update the list of places to visit placesToVisit.add(place); } // Get places in this domain Vector places = new Vector(Arrays.asList(agentSystem.getPlaces())); places.remove(home); for (int index = places.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) places.get(index); // update the list of places to visit placesToVisit.add(place); } } // Initialize the shared objects relative to all children domains and places for (int index = placesToVisit.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) placesToVisit.get(index); agentSystem.sharedObjects.put(index, place); } // Create lurkers which will move on first level domains and places, access shared // objects and somehow manipulate them for (int index = placesToVisit.size() - 1; index >= 0; index--) { Vector v = new Vector(); v.add(home); v.add(new Integer(index)); v.add(getID()); // the base agent ID AgentWorker lurker = env.agentManager.createAgent("MailboxLurkerAgent", v, false, true); try { lurker.start(); } catch (AgentWorker.AgentWorkerException awe) { awe.printStackTrace(); } numAgents++; } // if the agent is not traceable, there is no mailbox if (mailbox != null) mailbox.mailListener = new Mailbox.MailListener() { public void run() { Message mex = mailbox.getMessage(); agentSystem.getOut().println( "Message received by " + mex.from); if (mex.message.toString().equals("lurker_agent_done")) numMessage++; agentSystem.getOut().println( "" + numMessage + " message(s) arrived"); } }; agentSystem.getOut().println("" + getID() + ": I'm going idle..."); idle("idleCycle"); // wait } public void idleCycle() { if (numMessage != numAgents) idle("idleCycle"); else printSharedObjects(); } public void printSharedObjects() { for (int index = agentSystem.sharedObjects.size() - 1; index >= 0; index--) agentSystem.getOut().println(agentSystem.sharedObjects.get(index).toString()); } } // end MailboxBaseAgentL'interfaccia MailListener è definita all'interno della classe Mailbox e dichiara l'unico metodo run() che è stato implementato nel nostro esempio. In esso, l'agente richiede alla propria mailbox un messaggio, ed incrementa un contatore nel caso esso sia stato mandato da uno degli agenti lurker che sono stati precedentemente creati. Già da qui si può intuire il formato del messaggio: una semplice java.lang.String contenente una particolare sequenza di caratteri ("lurker_agent_done"). Va evidenziato l'uso del metodo idle() all'interno del metodo idleCycle(), richiamato la prima volta subito dopo aver costruito un listener per la mailbox, e poi più volte fino a quando tutti gli agenti lurker non hanno concluso il proprio lavoro e spedito un messaggio all'agente base. Si deve fare in modo che l'agente base non muoia subito dopo aver creato il MailListener e che rimanga sospeso nel sistema fino a quando tutti i messaggi spediti dai lurker non gli siano arrivati. Per ottenere questo comportamento è necessario che il sistema prenda in consegna l'agente e lo sospenda: esattamente ciò che accade con l'utilizzo del metodo idle(). Ma non si era forse sottolineata in precedenza la necessità di un intervento esterno al fine di risvegliare un agente messo nello stato Idle? L'uso della sospensione dell'agente sulla mailbox ci permette di non avere più bisogno di un intervento esterno operato da un utente; piuttosto, l'agente viene riavviato dal sistema non appena gli viene consegnato un messaggio. Il metodo sendMessage() della classe SOMA.agent.mobility.AgentManager, che è quello che alla fine si occupa della trasmissione vera e propria del messaggio, illustra molto bene questo caso particolare in una porzione del suo codice riportata qui di seguito. public synchronized void sendMessage(Message message, int attemptsCount) { AgentWorker destinationWorker = env.agentManager.agentWorkerStore.getWorker(message.to); if (destinationWorker != null && destinationWorker.getStatus() != AgentWorker.GONE) // Ho già trovato l'agente! { destinationWorker.agent.mailbox.storeMessage(message); // ATTENZIONE: Agenti in idle ==> riavviati. if (destinationWorker.getStatus() == AgentWorker.OFF || destinationWorker.getStatus() == AgentWorker.IDLE || destinationWorker.getStatus() == AgentWorker.STOPPED) { env.out.println("Waking up agent " + message.to); try { destinationWorker.start(); } catch (AgentWorker.AgentWorkerException e) { e.printStackTrace(); } } } // ... }Si nota infatti subito che, se l'agente destinatario di un messaggio è nello stato Idle, viene risvegliato, per poi potergli recapitare il messaggio e fare in modo che possa eventualmente reagire di conseguenza. Nel nostro esempio, fino a quando non sono arrivati un numero di messaggi pari al numero di lurker creati, l'agente base non potrà stampare i risultati del lavoro effettuato dai lurker, e quindi dovrà ripetutamente rientrare nello stato Idle fino a quando la condizione appena detta per il proseguimento della sua esecuzione non si sarà verificata. Il codice della classe relativa agli agenti lurker di questo esempio è il seguente. Questa volta i lurker non si limitano a manipolare un oggetto trovato nella tabella hash degli Shared Objects, ma costruiscono un vettore di 10.000 interi, in modo da perdere un po' di tempo e realizzare, su una singola macchina, una sequenza di arrivi cronologicamente simile a quella che si verifica in una rete locale. import SOMA.agent.*; import SOMA.naming.*; import SOMA.naming.domain.*; import java.util.*; /** * An example of a lurker agent coordinating with a base agent thanks to * the use of a mailbox. * * @author Giulio Piancastelli * @version 1.0 - Wednesday 13th February, 2002 */ public class MailboxLurkerAgent extends Agent { private PlaceID myHome; // the PlaceID the agent come from // myIndex really indicates the particular shared object property of the lurker agent, // i.e. the one and only object a single lurker agent will modify private int myIndex; private AgentID parent; private String manipulation = ""; public void putArgument(Object obj) { Vector v = (Vector) obj; myHome = (PlaceID) v.get(0); Integer temp = (Integer) v.get(1); // the agent destination place's index in the shared objects table myIndex = temp.intValue(); parent = (AgentID) v.get(2); } public void run() { try { go((PlaceID) agentSystem.sharedObjects.get(myIndex), "buildString"); } catch (CantGoException cge) { cge.printStackTrace(); } } /** * Builds the String which will substitute the destination PlaceID entry in the * shared objects table. */ public void buildString() { // Get children domains if (agentSystem.getPlaceID().isDomain()) { SOMA.Environment env = agentSystem.getEnvironment(); Vector children = env.domainNameService.getChildrenDNS(); for (int index = children.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) children.get(index); manipulation += place.toString(); } } // Get places in this domain Vector places = new Vector(Arrays.asList(agentSystem.getPlaces())); places.remove(agentSystem.getPlaceID()); for (int index = places.size() - 1; index >= 0; index--) { PlaceID place = (PlaceID) places.get(index); manipulation += " " + place.toString(); } // Build a huge vector - make them being late! Vector v = new Vector(); for (int i = 0; i < 10000; i++) v.add(new Integer(i)); try { go(myHome, "manipulate"); } catch (CantGoException cge) { cge.printStackTrace(); } } public void manipulate() { // Substitute the old object (a PlaceID) with the String previously built agentSystem.sharedObjects.remove(myIndex); agentSystem.sharedObjects.put(myIndex, manipulation); // the agent has done, and notifies it to its parent Message mex = new Message("lurker_agent_done", getID(), parent); agentSystem.sendMessage(mex); } } // end MailboxLurkerAgentSi noti che, ancora una volta, per permettere al lettore di concentrarsi sull'argomento del presente capitolo, sono stati omessi dal codice i controlli che ogni agente mobile dovrebbe effettuare per verificare di essere arrivato nel posto giusto dopo aver eseguito una migrazione.
Agenti con GUI personalizzate Gli strumenti trattati in [Ant99] non solo permettono l'accesso al sistema SOMA tramite una comoda interfaccia grafica, ma consentono anche di dotare i propri agenti di GUI attraverso le quali settare opzioni, originare comandi tramite pulsanti, e via dicendo. Un minimale agente che si presenti attraverso una finestra grafica appare come nella seguente figura.
Come si vede, la finestra con cui ogni agente si presenta contiene almeno: una area di testo su cui scrivere messaggi, in un certo modo simile all'area di testo presente nella finestra di ogni Place; un campo di testo da usare come se fosse una linea di comando, per impartire istruzioni a SOMA e all'agente; un pulsante Clear, per pulire l'area di testo contenuta nella finestra. Il codice di questo agente è riportato qui di seguito. import SOMA.agent.*; import SOMA.gui.*; import SOMA.utility.*; import java.awt.*; import javax.swing.*; /** * The minimal windowed agent - a mobile agent controlled by a window * built up using the SOMA GUI facilities. * * @author Giulio Piancastelli * @version 1.0 - Monday 18th February, 2002 */ public class WinSkeletonAgent extends Agent { /* Transient members */ // The agent's output window private transient OutputFrame2 window; // Wait on the semaphore before exiting - to synchronize the main thread (waiting) with // actions from the menu and buttons private transient WaitAndTimeout exitSemaphore; public void run() { agentSystem.getOut().println("Agent [" + getID() + "] running..."); buildWindow(); // create the agent's window exitSemaphore = new WaitAndTimeout(0, "<EXIT>", window.out); startMethod(); } public void startMethod() { agentSystem.getOut().println("Agent [" + getID() + "] executing startMethod..."); exitSemaphore.Wait(); // waiting here... // restarting with Exit, Go or Idle commands window.dispose(); agentSystem.getOut().println("Agent [" + getID() + "] exiting from main thread..."); } private void buildWindow() { window = new OutputFrame2("Agent [" + getID() + "]", WinSkeletonAgent.class.getName()); // what to do on exit window.onExitCommand = new OutputFrame2.Listener() { public void run() { // show this message in the agent window when exiting window.out.println("Agent [" + getID() + "] exiting..."); exitSemaphore.Done(); } }; window.pack(); window.setVisible(true); } } // end WinAgentSi noti innanzitutto che la finestra dell'agente è una istanza della classe SOMA.gui.OutputFrame2. Essa estende javax.swing.JFrame, e può dunque essere trattata come tale, il che significa ad esempio che ad essa si possono aggiungere menu, oltre ad effettuare tutte le manipolazioni che possono essere fatte su un normale JFrame. Inoltre, OutputFrame2 mette a disposizione del programmatore un pannello inizialmente vuoto in cui poter inserire i controlli di cui l'agente ha bisogno: tipicamente pulsanti appartenenti alla classe javax.swing.JButton, ma anche javax.swing.JComboBox e più in generale qualsiasi componente Swing si ritenga necessario. /** * Pannello vuoto, inserito prima del pannello coi bottoni di default. * Utile per inserirvi i propri bottoni. * Questo pannello viene CREATO E INSERITO NEL FRAME DA QUESTA CLASSE. * Dopo la costruzione lo si può riempire con ciò che si vuole, ma bisogna * farlo sempre PRIMA di chiamare il metodo setVisible() sulla finestra! */ public JPanel prePanBottoni;Il pannello verrà visualizzato accanto al pulsante Clear, come si può vedere nella seguente immagine, raffigurante un agente di esempio discusso in [Ant99] e recuperabile nella directory agents della distribuzione di SOMA.
Altri membri interessanti della classe OutputStream2 sono: public transient PrintStream out; public Listener onExitCommand; Il primo è lo stream di output collegato alla area di testo della finestra,
su cui si scrivono informazioni come sulla area di testo della finestra
di Place: nei nostri esempi, nel primo caso si è usato window.out.println(),
nel secondo caso si è usato agentSystem.getOut().println().
Il secondo è un listener al cui metodo run()
deve essere assegnato il lavoro da fare prima di chiudere la finestra
e di lasciare quindi che l'agente muoia.
|
||||||||||||
|
||||||||||||
|
||||||||||||