• Non ci sono risultati.

Di seguito illustriamo alcune soluzioni e scelte tecniche di particolare rilievo che sono state implementate nella realizzazione della libreria.

- Ogni nodo Rappresentante che partecipa al processo di

deployment è in grado di ricevere messaggi dagli altri nodi secondo le modalità indicate nel file di topologia della rete; in tale file sono indicati l’id univoco all’interno della rete, il protocollo di comunicazione da utilizzare cosi come anche i parametri per avviare un eventuale server (es. indirizzo per il bind e porta di ascolto). Ogni

Rappresentante è un nodo permanente della griglia costantemente in grado di ricevere, elaborare messaggi e rispondere agli altri nodi (Committenti e Rappresentanti) della griglia.

- Il Committente è un nodo non permanente della griglia, non ha un id univoco di rete e resta agganciato alla griglia per la sola durata della sessione di deployment.

La libreria consente e gestisce correttamente l’esecuzione in contemporanea di più Committenti i quali possono effettuare deployment in parallelo.

- Ogni nodo (Committente o Rappresentante) ha una sua cache suddivisa in due partizioni logiche e fisiche: Cache Memory e Cache Disk. Ad ognuna viene assegnato un limite di utilizzo; la Cache Disk viene utilizzata solo se la Cache Memory è piena; nel caso in cui anche la Cache Disk si riempia viene lanciata un’eccezione.

Un apposito metodo si occupa di effettuare l’ottimizzazione della cache (spostando blocchi tra le due partizioni) a intervalli regolari di tempo quando la macchina è idle o non effettua operazioni cpu- intensive.

La cache rende disponibili le classiche operazioni di get, put e test per lavorare con i blocchi, ma fornisce anche ulteriori metodi per lavorare (leggere / scrivere) direttamente con blocchi su stream e per effettuare delle get condizionali con timeout di una serie di chiavi (es. metodo waitForAny).

La cache prevede inoltre dei meccanismi di locking per una più efficiente gestione delle fasi del deployment.

- Ogni nodo elabora i file (per la creazione dei blocchi o per la loro ricostruzione) con un pool di Thread per migliorare le performance di tali operazioni. La gestione di tali thread avviene però

consentono l’esecuzione in parallelo di un numero eccessivo (in base alle capacità della macchina) di Thread che penalizzerebbero le prestazioni.

- Il modello delle comunicazioni prevede l’utilizzo di una sola primitiva di base, la primitiva Send. Tale primitiva, implementata dal metodo Send della classe Nodo, consente di inviare dei messaggi di qualunque tipo a qualsiasi nodo e di ricevere una risposta. Tale primitiva accetta come parametri solo l’id del nodo destinatario e il messaggio da inviare e ritorna un messaggio di risposta. Questa primitiva ad alto livello è di tipo logico e si occupa di richiamare l’apposita primitiva di basso livello implementata per contattare il particolare tipo di nodo a cui viene inviato il messaggio. Tale modello consente l’utilizzo di più protocolli in contemporanea e consente di dissociare la parte logica dell’applicazione da quella fisica per l’invio e ricezione di dati in rete. L’aggiunta di nuovi protocolli di comunicazione alla libreria richiede la scrittura di poche righe di codice che si preoccupano semplicemente di creare gli stream di lettura/scrittura dai canali di comunicazione.

- Per agevolare la gestione delle comunicazioni è stata realizzata la classe AsyncSendManager che si occupa di gestire in modo asincrono più comunicazioni in parallelo. Tale classe implementa anche delle primitive di broadcast e consente di testare la terminazione di tutte o solo alcune Send in esecuzione. AsyncSendManager consente anche di limitare il numero massimo di comunicazioni in esecuzione contemporaneamente con degli appositi meccanismi asincroni di wait/signal. Ciò al fine di non superare le risorse fisiche della macchina e per ottimizzare le fasi di deployment gestendo e ripartendo meglio la banda disponibile.

- Due ottimizzazioni che hanno prodotto ottimi risultati sono state la possibilità di creare blocchi compressi a partire da stream non compressi di dati e in modo complementare creare file decompressi a partire da una serie di blocchi compressi, tutto ciò senza creare file temporanei di appoggio. Ciò è avvenuto estendendo le classi InputStream e OutputStream rispettivamente con BlockInputStream e BlockOutputStream.

Evitare la creazione di un file temporaneo d’appoggio ha consentito di eliminare la creazione di pesanti file di swapping della memoria e di risparmiare delle letture/scritture aggiuntive da disco (entrambe operazioni molto costose in termini di tempo e risorse).

BlockInputStream simula uno stream continuo a partire da una serie di blocchi in cache, BlockOutputStream in modo complementare crea dei blocchi da mettere in cache a partire da uno stream. Entrambe le classi sono state fondamentali nei processi di compressione e decompressione che operano su stream per garantire delle buone performance.

- Una classe molto importante è PoliticaInvio dove vengono eseguite: la procedura di previsione della probabilità di Hit in Cache, le fasi di lookup e l’euristica per il calcolo dello schema di distribuzione. Tale classe si occupa poi anche di creare i messaggi di NewJob per l’avvio del deployment. In tale classe sono state implementate molte ottimizzazioni per migliorare le performance in particolar modo dell’euristica. Ad esempio l’array delle celle della matrice è mantenuto ordinato in tempo logaritmico (anziché NlogN) tra ogni step dell’euristica; sono utilizzati molti hash per abbattere i costi di ricerca di oggetti e varie altre tecniche descritte in seguito. - Un’ottimizzazione che ha consentito di sfruttare molto più efficacemente la rete è quella di aver inserito nei messaggi di NewJob

del messaggio e prima dei blocchi inviati dal committente (che occupano la parte preponderante del messaggio). Ciò consente ai nodi che iniziano a ricevere il messaggio di tipo MsgRequest_NewJob di poter avviare immediatamente, tramite l’AsyncSendManager, le richieste dei blocchi mancanti agli altri nodi. In tal modo mentre si continua la lettura dei blocchi inviati dal committente nel messaggio di NewJob è possibile contemporaneamente in modo asincrono sfruttare il massimo numero possibile di link in rete ricevendo blocchi dagli altri nodi. Questi vengono ricevuti man mano che si rendono disponibili, eventualmente non nell’ordine della richiesta, grazie anche alla primitiva waitForAny di lettura dei blocchi dalla Cache.

- Le classi comparatorMolteplicita e comparatorPriorita

consentono di ottimizzare ulteriormente l’invio dei dati da parte del Committente.

comparatorMolteplicita consente di inviare prima i blocchi che devono essere ridistribuiti a più rappresentanti che quindi possono far partire prima in parallelo su più link un maggior numero di richieste pendenti in attesa.

comparatorPriorita consente di ordinare i messaggi da inviare da parte del Committente secondo un priorità che favorisce i messaggi che contengono più blocchi ad alta molteplicità (richiesti da più nodi).