|
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 TryAgent
Tutti 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 EnvAgent
Salta 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 RegionTourAgent
Si 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 BaseTryAgent
Si 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 LurkerTryAgent
Le 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 SObjBaseAgent
Si 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 SObjLurkerAgent
Va 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 MailboxBaseAgent
L'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 MailboxLurkerAgent
Si 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 WinAgent
Si 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.
|
||||||||||||
|
||||||||||||
|
|
||||||||||||