Realizzazione di un motore d`esecuzione per un linguaggio di
Transcript
Realizzazione di un motore d`esecuzione per un linguaggio di
Realizzazione di un motore d’esecuzione per un linguaggio di orchestrazione di servizi Blite ovvero BPEL in versione lite Paolo Panconi 15 aprile 2009 “A mio padre, a Valeria, presto mia moglie e soprattutto a mia madre” Indice 1 Introduzione 11 2 Linguaggi per l’orchestrazione 2.1 BPEL: uno standard per l’orchestrazione di Servizi Web 2.2 Blite, un approccio formale a BPEL . . . . . . . . . . . 2.3 Una grammatica per un compilatore . . . . . . . . . . . 2.4 Alcune osservazioni sulla semantica della correlazione . 3 Blite-se 3.1 Progetto di un motore per l’orchestrazione 3.2 Specifica dell’Engine . . . . . . . . . . . 3.3 Un modello per le attività . . . . . . . . . 3.4 Esecuzione e parallelismo . . . . . . . . . 3.5 Comunicazione ed eventi . . . . . . . . . 3.6 Contesto, FaultHandler e Compensazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 19 27 40 50 . . . . . . 55 55 60 62 67 72 81 4 Blide 91 4.1 Un IDE per Blite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 4.2 Un esempio d’uso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 5 Conclusioni e sviluppi 119 5.1 Osservazioni conclusive . . . . . . . . . . . . . . . . . . . . . . . . . . 119 5.2 Sviluppi futuri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 6 INDICE Elenco delle figure 2.1 Comunicazione asincrona con Blite . . . . . . . . . . . . . . . . . . . 30 2.2 Un parser con JJTree e JavaCC . . . . . . . . . . . . . . . . . . . . . . 45 2.3 Codice Blite, esempio delimitatori di blocco . . . . . . . . . . . . . . . 48 2.4 Codice Blite, esempio ready-to-run instance . . . . . . . . . . . . . . . 50 3.1 I moduli di Blite-se . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.2 Blite-se: Engine, ProcessManager e ProcessInstace . . . . . . . . . . . 60 3.3 Blite-se: modello statico e modello dinamico . . . . . . . . . . . . . . 63 3.4 Blite-se: Gerarchia delle ActivityComponent . . . . . . . . . . . . . . 66 3.5 Diagramma di classe FlowExecutor, FlowOwner e ThreadPool . . . . . 73 3.6 Comunicazione One-Way . . . . . . . . . . . . . . . . . . . . . . . . . 79 3.7 Propagazione di un’eccezione . . . . . . . . . . . . . . . . . . . . . . 83 3.8 ExecutionContext Class Diagram . . . . . . . . . . . . . . . . . . . . . 84 4.1 Blide Schermata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 4.2 Il pannello “Favorites” . . . . . . . . . . . . . . . . . . . . . . . . . . 94 4.3 Menu Contestuale su di un File Blite . . . . . . . . . . . . . . . . . . . 96 4.4 Blide, l’editor per Blite . . . . . . . . . . . . . . . . . . . . . . . . . . 99 4.5 Blide, feedback alla compilazione . . . . . . . . . . . . . . . . . . . . 99 4.6 Blide, feedback alla compilazione . . . . . . . . . . . . . . . . . . . . 100 4.7 Blide: rappresentazione grafica di istanze . . . . . . . . . . . . . . . . 103 4.8 Blide: programma Blite . . . . . . . . . . . . . . . . . . . . . . . . . . 104 4.9 Codice Blite, il Servizio Spedizioni . . . . . . . . . . . . . . . . . . . . 108 4.10 Codice Blite, Clienti del Servizio Spedizioni . . . . . . . . . . . . . . . 111 4.11 Codice Blite, interfaccia Store Service . . . . . . . . . . . . . . . . . . 114 4.12 Codice Blite, interfaccia Billing Service . . . . . . . . . . . . . . . . . 114 4.13 Blide, simulazione 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 8 ELENCO DELLE FIGURE 4.14 Blide, simulazione 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 4.15 Blide, simulazione 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 4.16 Blide, simulazione 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . 118 Elenco delle tabelle 2.1 2.2 2.3 2.4 2.5 2.6 La sintassi di Blite . . . . . . . . . . . . . . . . . . Congruenza Strutturale per le attività e i deployment Semantica operazionale per le attività . . . . . . . . Regole di riduzione per deployment . . . . . . . . . Definizione predicato match di ricezione . . . . . . . Delimitatori di blocco per le attività strutturate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 33 34 38 39 47 3.1 3.2 3.3 Metodi principali dell’interfaccia ActivityComponent . . . . . . . . . . Factory per creare le opportune sottoclassi che implementano le attività Interfaccia ExecutionContext . . . . . . . . . . . . . . . . . . . . . . . 64 67 90 4.1 4.2 4.3 Icone associate ai file Blite con estensione .blt . . . . . . . . . . . . . 93 Azioni che possono essere eseguite su un file Blite . . . . . . . . . . . 95 I vari tipi di nodi nell’albero Local Engines . . . . . . . . . . . . . . . 101 10 ELENCO DELLE TABELLE Capitolo 1 Introduzione Nell’era di Internet e della globalizzazione dell’informazione le necessità di coloro che utilizzano il software per svolgere le loro attività primarie sono cambiate drasticamente. Per le grandi organizzazioni (siano queste aziende private, pubbliche amministrazioni o altro) risulta indispensabile avere un modello di attività dinamico e aperto che possa permettere loro di modificare rapidamente le politiche e le strategie, e di dialogare in maniera sicura ed efficiente con il maggior numero di partner e clienti. È quindi ovvio che la risposta del mondo del software e della tecnologia non potesse che muoversi in tal direzione. Il primo passo è stato quello di creare, tramite processi condivisi, gli standard pubblici che permettessero di ideare una “lingua franca” tramite cui scambiare l’informazione. La creazione dello standard XML [8] ha permesso di descrivere i dati tramite una rappresentazione testuale che autoesplicita lo schema di encoding utilizzato per rappresentare i caratteri in sequenze di byte. In questo modo si è realizzata la completa indipendenza dei dati dalle specifiche architetture hardware/software e l’informazione è diventata finalmente portabile attraverso le diverse piattaforme. Insieme alla necessità di rappresentare i dati a basso livello vi è sempre quella di poterli strutturare in tipi e forme sempre più complessi. DTD [9] prima, e XML Schema [10] poi, in maniera definitiva, hanno permesso di risolvere brillantemente tale problematica nell’ambito di XML stesso. A questo punto avendo modo di rappresentare l’informazione in maniera totalmente portabile e tipata ci si è rivolti ad individuare un modello di comunicazione che permettesse lo scambio di tale informazione. È stata individuata l’astrazione di “servizio”, inteso semplicemente come un’interfaccia formalmente specificata ad una risorsa puramente stateless. Il concetto di “risorsa” è da intendersi in senso generale, come un 12 Introduzione documento statico, ma anche come unità elaborativa capace di restituire dinamicamente dei dati in base al valore di parametri ricevuti in input. La definizione della specifica formale dei servizi non poteva avvenire che nell’ambito di XML e XML Schema e l’introduzione di WSDL [11], linguaggio direttamente basato su questi standard, ha permesso la realizzazione di interfacce in cui si definiscono operazioni i cui parametri di input ed output sono formalmente tipati e la loro descrizione astrae completamente dalle tecnologie utilizzate per implementarle. Mentre WSDL rappresenta le interfacce, SOAP [13] e WSDL Binding [11] definiscono il modo in cui l’informazione che transita per tali interfacce viene strutturata e “imbustata” in messaggi. A questo punto non rimane altro che determinare il mezzo con cui inviare tali messaggi. E la scelta più ovvia non poteva che essere quella di utilizzare i mezzi più diffusi e collaudati, ovvero HTTP [14] [15] e i protocolli di Internet. Servizi basati sull’utilizzo combinato di XML, XML Schema, WSDL, SOAP e HTTP sono in generale individuati col termine “Servizi Web”, anche se spesso con tale termine si tende a definire una classe più ampia di Servizi non necessariamente basati su tutte le precedenti tecnologie. Avere delle specifiche con un cosı̀ alto livello di descrizione formale, fa sı̀ che sia possibile realizzare strumenti per la creazione automatica di ampie parti di codice in vari linguaggi di programmazione. In pratica, semplicemente dalla definizione di un dato servizio è possibile creare parte dell’implementazione di esso o parte dei client. Questa è stata una della caratteristiche che hanno contribuito maggiormente alla diffusione dei Servizi Web ed a determinarne il successo. Disporre di un sistema di comunicazione cosı̀ promettente ha fatto sı̀ che si cominciasse ad utilizzarlo per definire un nuovo paradigma di programmazione e di sviluppo di applicazioni, in cui al concetto di servizio si dovesse inevitabilmente affiancare il concetto di stato (tutti sanno che per realizzare applicazioni poco più che banali è indispensabile mantenere uno stato). Tale paradigma è noto con il nome di SOC (Service Oriented Computing) [17] [16]. In pratica, SOC racchiude tutte quelle attività tese alla definizione di un modello di sviluppo software basato sull’assemblaggio di componenti che possono essere specificati tramite i formalismi dei servizi ma che possono eventualmente mantenere anche uno stato. WS-Addressing [18], per esempio, è una tecnologia che contribuisce alla definizione del concetto di stato all’interno degli standard dei Web Service. In SOC, oltre al concetto fondamentale di servizio, confluiscono diverse metodologie per la realizzazione delle funzionalità più complesse tramite la composizione di 13 funzionalità più semplici offerte dai servizi componenti. Per esempio, un approccio che sta avendo un discreto successo è quello identificato con il termine Business Process Oriented Approach, secondo cui ogni attività aziendale1 deve avere un preciso obiettivo (per esempio, mettere a disposizione un prodotto o un servizio eventualmente anche solo per altre attività) e un ciclo di vita formale, secondo cui, ad un evento di inizio attività (istanziazione del processo), deve corrispondere un’evoluzione e un esito sempre individuabile come positivo o negativo. Ogni attività è quindi un processo composto da una serie di passi, visti esternamente come unità atomiche, ma che in realtà possono racchiudere a loro volta sottoprocessi (composizionalità), che vengono svolti secondo regole di evoluzione precise (workflow) che ne stabiliscono le dipendenze logiche e temporali. Le sottoattività utilizzate da un processo possono spaziare nell’intero dominio aziendale ed essere svolte sotto la responsabilità di diverse figure dell’organigramma societario. I tempi di evoluzione delle attività possono essere in generale fortemente variabili e dipendenti da innumerevoli fattori. In tal senso i processi, pur trovandosi spesso a dover garantire stringenti requisiti temporali, devono fare i conti con sottoattività particolarmente riluttanti a rispettare specifici vincoli temporali. Nell’ambito di SOC, tale metodologia ha trovato attuazione utilizzando i Servizi Web come tecnologia, sia per la realizzazione delle interfacce delle attività, sia per la rappresentazione e la comunicazione dei dati, e sviluppando un linguaggio come BPEL [3] per la codifica dei workflow dei processi. Tale linguaggio permette di utilizzare i Servizi Web per accedere alle sottoattività e, a sua volta, espone il processo stesso come un Servizio Web, permettendo di realizzare la composizionalità e uniformando il modo in cui ogni risorsa è vista esternamente. Come si può intuire, un linguaggio come BPEL risulta uno strumento molto complesso, ricco di costrutti sia per la gestione dei flussi interni che per la realizzazione della comunicazione. In pratica riguardo a BPEL risulta difficile sia realizzarne implementazioni a partire dalle specifiche del linguaggio, sia utilizzare queste ultime per sviluppare applicazioni. Attualmente BPEL è specificato da documenti, scritti in linguaggio naturale, prodotto di un processo di standardizzazione supervisionato da Oasis [5]. L’assenza di una semantica formale può far sı̀ che si creino dei fraintendimenti e che ci possano essere interpretazioni diverse del medesimo concetto, facendo sı̀ che le implementazioni infine risultino incompatibili. Per esempio, possono risultare di difficile interpretazione la 1 Il termine aziendale deve essere inteso in senso generale di organizzazione o di entità in cui più soggetti collaborano per il raggiungimento di un fine comune secondo regole e strategie condivise. 14 Introduzione relazione fra le Multiple Start Activity e il meccanismo di gestione dei conflitti nell’attribuzione dei messaggi alle diverse istanze concorrenti di un processo, o alcuni aspetti legati alle problematiche di terminazione delle attività e di compensazione. Il processo di realizzazione di applicazioni BPEL risulta difficile e fortemente soggetto ad errori anche per la presenza di caratteristiche complesse quali: parallelismo, concorrenza, terminazione forzata di attività, correlazione e compensazione, quest’ultima utilizzata per la realizzazione di “Long-Running Transaction”, e forse ancora non compresa in tutti i suoi aspetti e potenzialità dagli utilizzatori. In tal senso un processo di analisi e di formalizzazione di tali costrutti è certamente utile, sia per chi si trova ad utilizzarli, sia per coloro che ne realizzano l’implementazione a partire da specifiche in linguaggio naturale, in modo tale da renderne più chiaro il significato ed eventualmente semplificarne gli aspetti più delicati. Un approccio, per cercare di affrontare queste problematiche, è stato quello di utilizzare i metodi formali e la teoria delle “algebre di processo” per la definizione di una semantica formale e per la realizzazione successiva di una piattaforma per la verifica e la dimostrazione di proprietà di applicazioni basate su BPEL. Con tale scopo è stato ideato Blite [1], una variante semplificata di BPEL, che riproduce alcune delle sue funzionalità più caratteristiche, cercando d’altra parte di semplificare alcuni aspetti ritenuti marginali nella realizzazione del modello Process Oriented. In definitiva in Blite è stato selezionato un core di BPEL, per cui fosse abbastanza agevole definire una semantica formale e con cui, tramite “encoding”, fosse possibile sostanzialmente riproporre tutte le funzionalità del linguaggio. Il lavoro svolto in questa tesi è da considerarsi a supporto di tale attività, come un’ulteriore verifica e confronto del processo di astrazione teorica e formale con gli aspetti più concreti e tecnologici, legati all’implementazione di un linguaggio per l’orchestrazione distribuita di servizi. Infatti l’obiettivo primario è stato quello di realizzare un’implementazione di Blite che potesse essere il più vicino possibile, per caratteristiche ed applicabilità, ai motori di esecuzione di BPEL attualmente disponibili. Se da un lato Blite vuole essere un’analisi di BPEL, fatta tramite un processo di astrazione e formalizzazione, il lavoro qui esposto vuole essere a sua volta un’analisi di tale processo, nel senso che ne vuole verificare la compatibilità e l’attinenza con le problematiche più concrete e tecnologiche presenti nell’ambito dell’orchestrazione di servizi. Di fatto il lavoro svolto rappresenta un test critico di come i costrutti formali, utilizzati per fornire la semantica del linguaggio, possano essere realmente implementati in scenari di software di produzione e che quindi continuino a mantenere un legame a doppio filo con il 15 mondo delle tecnologie, da cui hanno tratto iniziale ispirazione. Oltre che nella direzione sopra esposta, la nostra attività di verifica di Blite si è mossa verso la creazione di strumenti che possano permettere direttamente di scrivere e simulare in maniera rapida processi definiti con esso. Ci è sembrato che un modo molto utile per verificare la funzionalità del linguaggio creato, fosse quello di poter scrivere ed eseguire programmi e di avere una rappresentazione, il più espressiva possibile, dell’avvenuta esecuzione e di come si sia attuata la comunicazione tra i processi. In pratica sono stati realizzati due progetti software basati su tecnologia Java [21]: Blite-se e Blide. Blite-se (Blite Service Engine) realizza l’insieme delle funzionalità necessarie alla esecuzione di processi definiti tramite Blite, o meglio, tramite una sintassi leggermente modificata. In particolare fornisce un compilatore (realizzato con la tecnologia JavaCC e JJTree [22]) e un modello statico per la rappresentazione dei programmi. Il compilatore, come risultato dell’analisi sintattica, produce un modello ad oggetti rappresentante il programma stesso. Tale modello, di seguito riferito come Modello Statico, è in pratica l’albero sintattico i cui nodi sono oggetti che espongono funzionalità specifiche, in base alla categoria sintattica rappresentata. In Blite-se è realizzato anche il motore (Engine) per l’esecuzione dei processi secondo la semantica formale definita in [1]. Tale componente software è stata progettata in riferimento a specifiche che tenessero il più possibile conto delle problematiche reali degli ambiti produttivi (efficienza, scalabilità, requisiti di memoria, ecc). È stato ideato inoltre, un modello di esecuzione basato sul pattern Composite [29], secondo cui ogni attività è rappresentabile come un componente che apporta il proprio contributo all’esecuzione dell’istanza di processo. A partire dal modello statico, a runtime viene costruito passo passo un Modello Dinamico, l’albero dei componenti/attività, e l’esecuzione procede tramite l’invocazione ricorsiva del metodo doActivity() su tali camponenti. In sintesi, il nostro modello di esecuzione può essere definito Activity Centric, nel senso che le varie attività saranno responsabili nel realizzare la loro esecuzione, ma anche nel collaborare, per far sı̀ che i flussi globali dei processi evolvano secondo quanto specificato dalla semantica. L’Engine è stato realizzato astraendo dagli aspetti più concreti e tecnologici, quali la comunicazione e il deployment. L’Engine, per comunicare con le porte dei servizi esterni, utilizza un’interfaccia (EngineChannel), che definisce un modello di comunicazione totalmente generico, utilizzabile per realizzare molteplici protocolli per lo scambio di messaggi, dal più semplice “fire and forget” ai più complessi, che necessitano un 16 Introduzione mantenimento dello stato. Le implementazioni di tale interfaccia verranno fornite dal componente software Environment, che realizza un contenitore per l’Engine nell’ambito di una precisa tecnologia di comunicazione. Attualmente è stato creato un Local Environment capace di eseguire localmente più Engine e simulare la rete e la comunicazione remota, ma sarebbe interessante realizzare Environment capaci di supportare direttamente gli standard tipici della tecnologia dei Servizi Web, come WSDL, SOAP e HTTP, e poter quindi far dialogare i nostri programmi Blite con ogni servizio disponibile in Internet. Altre possibilità potrebbero essere quelle di avere Environment capaci di supportare lo standard JBI [24] e poter integrare il nostro Engine con ESB (Enterprise service bus) che supportano tale tecnologia; o Environment integrabili con framework per la comunicazione remota come IMC [30]. Blide (Blite Integrated Development Environment) costituisce il secondo progetto software realizzato. Esso implementa un vero e proprio IDE che permette di scrivere rapidamente e testare, tramite l’esecuzione di simulazioni, i programmi Blite. Oltre alle consuete funzionalità di gestione ed editing dei file, compilazione ed esecuzione integrata dei programmi, Blide fornisce un formalismo e una tecnologia per la rappresentazione grafica dell’esecuzione delle istanze e la comunicazione fra esse. In pratica le varie istanze vengono monitorate durante la loro esecuzione e vengono memorizzate le informazioni necessarie a darne una rappresentazione grafica. Alcune parti dell’interfaccia grafica mostrano all’utente le varie istanze eseguite e in esecuzione, cosicché l’utente può scegliere fra le istanze andando a comporre rappresentazioni della loro interazione. Blide è stato realizzato tramite il progetto NetBeans Platform [27], un framework studiato per facilitare lo sviluppo di applicazioni Java con interfaccia grafica. È stato scelto tale progetto per la sua completezza e per il modello architetturale offerto. NetBeans Platform permette allo sviluppatore di realizzare le proprie applicazioni componendo diversi moduli, ciascuno dei quali offre una funzionalità specifica. La modularizzazione molto fine e la gestione formale delle dipendenze permettono di costruire applicazioni riuscendo a selezionare in modo molto mirato solamente i moduli realmente necessari e quindi a mantenere limitate le dimensioni complessive dell’applicazione. In questo modo è stato possibile realizzare un modello di distribuzione dell’applicazione basato su Java WebStart [26], tramite il quale gli utenti possono eseguire, istallare e aggiornare in maniera trasparente l’applicazione da una pagina Web, 17 semplicemente selezionando un link. Dalla pagina http://code.google.com/p/blite-se/ è possibile istallare e mettere in esecuzione la versione più recente di Blite sulla propria macchina, per poi effettuare eventualmente anche esecuzioni offline. Tutto il progetto software realizzato per questa tesi è disponibile online alla pagina http://code.google.com/p/blite-se/, da cui è possibile ottenere le distribuzioni binarie, la documentazione e i sorgenti. Questi ultimi sono gestiti con la tecnologia SVN [35], messa a disposizione da Google tramite il progetto Google Code [36] e sono rilasciati sotto la licenza GNU General Public License v3 [37]. Dopo aver presentato una panoramica delle motivazioni e del lavoro da noi svolto andiamo a illustrare i contenuti di questa tesi. Nel Capitolo 2 viene introdotto il linguaggio BPEL per l’orchestrazione di servizi, partendo da una presentazione ad alto livello dei suoi costrutti caratteristici; successivamente viene presentato Blite, fornendone sintassi e semantica operazionale. L’implementazione del compilatore per Blite necessita di una grammatica formale, e nella Sezione 2.3 vengono presentati i procedimenti che ci hanno portato ad ottenerla, a partire dalla sintassi astratta definita in Sezione 2.2. Il capitolo si chiude con una sezione in cui vengono commentati alcuni aspetti della semantica rispetto alle problematiche d’implementazione. Nel Capitolo 3 viene presentato il progetto software Blite-se. Dopo aver fornito una panoramica del progetto e dei principali moduli che lo compongono si presenta nel dettaglio l’architettura del motore di esecuzione, mettendo in evidenza il modello per le attività, la gestione del parallelismo e della comunicazione, l’implementazione dei contesti, con la gestione delle eccezioni e della compensazione. Il Capitolo 4 fornisce una panoramica delle funzionalità offerte da Blide e dalla sua interfaccia utente e si conclude con la presentazione di un semplice caso d’uso. Il Capitolo 5 presenta un riepilogo critico del lavoro svolto e si delinea i possibili sviluppi ulteriori. 18 Introduzione Capitolo 2 Linguaggi per l’orchestrazione Questo capitolo offre una panoramica ad alto livello di BPEL, il linguaggio che è diventato lo standard per l’orchestrazione dei Servizi Web. Successivamente viene introdotto Blite, una variante semplificata di BPEL, che ne riproduce alcune delle funzionalità più caratteristiche al fine di definire una semantica formale. Tale semantica è presentata e commentata in dettaglio nella Sezione 2.2. A partire dalla sintassi astratta di Blite si ricava una grammatica in notazione EBNF utilizzata successivamente per lo sviluppo di un compilatore; tale procedimento è presentato nella Sezione 2.3. Il capitolo si chiude con una disamina di alcuni aspetti della semantica rispetto a problematiche legate all’implementazione. 2.1 BPEL: uno standard per l’orchestrazione di Servizi Web Abbiamo detto come SOA e i Servizi Web siano una delle risposte più recenti alla necessità di integrare funzionalità applicative eterogenee e di fornire un modello di sviluppo software che sia il più efficace possibile rispetto alla natura dinamica dei domini applicativi tipici delle grandi realtà aziendali. Alla base della metodologia SOA vi è l’approccio “bottom-up”, secondo cui è possibile comporre le funzionalità di base offerte dalle diverse applicazioni aziendali per crearne di più complesse e articolate. Gli standard e i linguaggi dei Servizi Web forniscono il supporto tecnologico e formale per realizzare tale integrazione che nella maggior parte degli scenari reali prevede di dover far comunicare tecnologie e formalismi del tutto eterogenei e incompatibili. Una delle possibilità più interessanti è quella di poter 20 Linguaggi per l’orchestrazione utilizzare il patrimonio storico (“legacy”) del software aziendale tramite standard moderni e aperti, e poter fare dialogare le applicazioni di ultima generazione con quelle più “mature”, che spesso mantengono un alto valore aziendale ma si basano su tecnologie e metodologie proprietarie ormai difficilmente manutenibili. Predisporre i componenti e renderli disponibili secondo il paradigma dell’ Architettura Orientata ai Servizi certo non realizza tutte le necessità di un complesso sistema aziendale, il passo successivo non può essere che quello di comporre le singole funzionalità di base per realizzare i flussi operativi che implementano le reali politiche e strategie di business. Le attività aziendali quindi possono essere rappresentate come processi (“Business Process”) che raggruppano le singole funzionalità secondo precise regole aziendali (“Business Rules”) e tramite primitive di aggregazione che attuano dipendenze temporali e logiche fra i diversi componenti pubblicati come servizi (“Service Orchestration”). Se si può pensare che le singole funzionalità siano i mattoni del business e che abbiano una certa robustezza temporale, in termini di specifica e implementazione, i processi al contrario possono essere fortemente dinamici e mutevoli, per poter facilmente adeguarsi alle necessità sempre nuove che si presentano nelle attività di un’azienda. Si capisce quindi come nasca la necessità di un formalismo specifico per la definizione e realizzazione di tali processi. In generale si vorrebbe poter disporre di un linguaggio semplice e flessibile che possa essere utilizzato a diversi livelli aziendali, compreso e adoperato dalle diverse figure professionali che partecipano alla definizione e attuazione dei processi stessi. Si vorrebbe disporre non solo di un linguaggio di programmazione utile ai tecnici del software ma anche di un formalismo utilizzabile dai manager e dagli esperti dei domini applicativi, che possa realizzare una piattaforma comune per la collaborazione fra le diverse aree disciplinari. È proprio come risposta a tale necessità che si propone BPEL, un linguaggio basato su XML per la definizione di processi aziendali realizzati come composizione di funzionalità esposte da Servizi Web. BPEL storicamente nasce dalla fusione di due tecnologie sviluppate indipendentemente all’inizio degli anni 2000: WSFL (Web Service Flow Language) di IBM [6] e XLANG di Microsoft [7]. La prima versione del linguaggio (BPEL4WS “Business Process Execution Language for Web Services Version 1.0” [2]) risale al 31 Luglio 2002 e fu prodotta dal lavoro congiunto di grandi aziende come IBM, BEA, SAP, Siebel e Microsoft. Dall’Aprile del 2003 il lavoro di stesura della versione successiva (1.1 [3]) è stato affidato alla supervisione di Oasis [5], società nata con il compito di realizzare standard aperti e condivisi dalla comunità 2.1 BPEL: uno standard per l’orchestrazione di Servizi Web 21 internazionale; al 5 Maggio 2003 è datato il rilascio di questa versione nella stesura ufficiale. Oasis è anche la curatrice della versione 2.0 dello standard (WS-BPEL “Web Services Business Process Execution Language Version 2.0” [4]) che ha visto il primo rilascio ufficiale in data 11 Aprile 2007. BPEL, come molti linguaggi legati alla tecnologia dei servizi web, ha una sintassi basata su XML. Tramite tale sintassi è possibile descrivere un business process definendo una logica di orchestrazione che determina le elaborazioni interne e le interazioni con i partner del processo, le quali sono, in entrambi i casi prodotte dall’esecuzione di attività definite sintatticamente in forma di tag XML. In BPEL possono essere date definizioni di processi eseguibili, le quali comprendono tutti i dettagli relativi ai meccanismi interni del processo e descrivono in maniera completa l’orchestrazione dei servizi web partner; processi di questo tipo possono essere eseguiti da un motore di esecuzione BPEL. BPEL permette anche di definire processi astratti, che costituiscono una descrizione del flusso di informazioni tra i partner ed il processo e non specificano i dettagli delle elaborazioni interne al processo; un processo di questo tipo non può essere eseguito, poiché mancano i dettagli relativi all’orchestrazione e descrive piuttosto una coreografia di messaggi. Noi siamo interessati alla descrizione completa dell’orchestrazione di un insieme di servizi web, pertanto prenderemo in considerazione solo i processi eseguibili. La definizione di un processo BPEL include la definizione delle relazioni tra il processo ed i suoi partner, rappresentate dai cosiddetti partner link. Tra le definizioni WSDL utilizzate dal processo devono essere presenti le definizioni di uno o più partner link type, espresse tramite uno o più elementi della forma: <partnerLinkType name="ncname"> <role name="ncname"> <portType name="qname"/> </role> <role name="ncname">? <portType name="qname"/> </role> </partnerLinkType> Ad ogni partner link type viene associato un nome e due ruoli, ognuno dei quali rappresenta una delle due estremità del link ed indica la specifica interfaccia (port type) 22 Linguaggi per l’orchestrazione WSDL che deve essere implementata dall’entità posta a tale estremità (il processo o uno dei suoi partner). Nel caso in cui uno dei due ruoli usufruisce delle operazioni dell’altro senza che ad esso sia richiesto di fornire specifici servizi, viene indicato un solo elemento <role> (quello relativo all’estremità a cui sono richieste funzionalità specifiche). Sottolineiamo che la definizione dei partner link type deve essere contenuta in un documento WSDL. Di fatto i partner link type a due ruoli introducono la novità rispetto a WSDL e servono ad estendere il concetto di contratto ad una comunicazione asincrona in cui il richiedente del servizio può essere non noto in fase di sviluppo del provider. La definizione del processo BPEL invece contiene le definizioni dei partner link veri e propri, espresse da uno o più elementi <partnerLink> annidati nell’elemento <partnerLinks>: <partnerLinks> <partnerLink name="ncname" partnerLinkType="qname" myRole="ncname"? partnerRole="ncname"?>+ </partnerLink> </partnerLinks> Ciascun partner link viene definito indicando il nome del partner link type e indicando quali sono il ruolo del processo (tramite l’attributo myRole) ed il ruolo del partner (tramite l’attributo partnerRole). Una caratteristica importante di BPEL è la possibilità di utilizzare delle strutture dati particolari, dette endpoint reference, che rappresentano l’indirizzo fisico di un partner del processo. Gli endpoint reference possono essere copiati da una variabile ad un’altra oppure possono essere inviati all’interno dei messaggi trasmessi tra processo e partner; ciò permette di determinare gli indirizzi fisici degli stessi partner in modo dinamico, durante il corso dell’esecuzione del processo. In questo modo un processo BPEL non deve per forza conoscere fin dall’inizio i suoi partner effettivi e come precedentemente fatto notare, la combinazione endpoint reference con i partner link type bidirezionali permette di realizzare l’indipendenza dell’implementazione e della locazione di una parte della comunicazione asincrono pur lasciando la possibilità di un controllo statico dei tipi. La possibilità di modificare le connessioni tra il processo ed i propri partner in modo dinamico caratterizza in modo importante il linguaggio BPEL e rimanda al passaggio dei nomi (name-passing) attuato nelle algebre di processo come il π-calculus. 2.1 BPEL: uno standard per l’orchestrazione di Servizi Web 23 Un processo BPEL può essere eseguito su un dato motore di esecuzione, che ha naturalmente anche la funzione di server, cioè la funzione di accogliere le richieste che provengono dai client che invocano le operazioni dell’interfaccia WSDL del processo. Uno stesso processo può essere istanziato più volte, in modo che ogni istanza provveda a soddisfare una specifica richiesta pervenuta e che più richieste possano essere processate contemporaneamente; solo certe attività interne del processo possono determinare la creazione di una nuova istanza e tipicamente queste sono attività per la ricezione di un messaggio. Affinché l’esecuzione di ogni istanza avvenga in modo corretto può essere necessario inserire nei messaggi scambiati tra processo e partner delle informazioni di correlazione, che permettono di associare ciascun messaggio all’istanza corretta a cui è destinato o da cui proviene. Tali informazioni di correlazione sono definite attraverso il concetto di proprietà e vengono associate ai dati contenuti nei messaggi mediante il meccanismo dell’aliasing delle proprietà. <property name="ncname" type="qname"/> <propertyAlias propertyName="qname" messageType="qname" part="ncname" query="queryString"/> Una proprietà è definita tramite un elemento <property> associando un nome ad un tipo di dati. Ad esempio la proprietà CodiceFiscale definisce un’informazione di tipo stringa (che corrisponde al codice fiscale di un certo utente): <property name="CodiceFiscale" type="xsd:string"/> Tale proprietà deve essere individuata all’interno dei messaggi scambiati tra processo e partner. Ciò avviene in virtù della funzione di corrispondenza definita attraverso gli elementi propertyAlias. Con il codice di esempio che segue, si dichiara che la proprietà CodiceFiscale sarà contenuta nei messaggi con formato richiestaConteggioFiscale ed esattamente nella parte cfUtente: <propertyAlias propertyName="CodiceFiscale" messageType="richiestaConteggioFiscale" part="cfUtente"/> </bpws:propertyAlias> 24 Linguaggi per l’orchestrazione Tipiche informazioni di correlazione possono essere il numero di un ordine, il codice di un cliente, l’identificativo di una transazione, etc. Quando le informazioni contenute nei messaggi inviati o ricevuti dal processo devono essere confrontate con le informazioni di correlazione possedute dal processo, le attività tramite cui tali messaggi sono inviati o ricevuti devono indicare uno o più correlation set, che sono insiemi di proprietà tra di loro attinenti. I correlation set che verranno usati dal processo devono essere dichiarati in un elemento <correlationSets> con la seguente sintassi: <correlationSets>? <correlationSet name="ncname" properties="qname-list"/>+ </correlationSets> dove qname-list è una lista di nomi di proprietà separati da uno spazio. Di seguito è mostrata la struttura XML di una definizione di processo BPEL <process name="pName" ...> <partnerLinks>? ... </partnerLinks> <partners>? ... </partners> <variables>? ... </variables> <correlationSets>? ... </correlationSets> <faultHandlers>? ... </faultHandlers> <compensationHandler>? ... </compensationHandler> <eventHandlers>? ... </eventHandlers> 2.1 BPEL: uno standard per l’orchestrazione di Servizi Web 25 activity <!-- attivita’ principale del processo --> </process> L’attività principale del processo indicata activity è una delle attività elementari o strutturate di BPEL e definisce il comportamento del processo. Le attività BPEL sono i blocchi con i quali si definisce la logica di orchestrazione del processo e si dividono in elementari e strutturate. Le attività elementari sono elencate di seguito, accompagnate da una breve descrizione degli effetti della loro esecuzione: <receive> il processo rimane in attesa della ricezione di un messaggio corrispondente all’invocazione, da parte di un partner, di una delle operazioni contenute in una delle interfacce WSDL del processo stesso; <reply> il processo invia un messaggio ad un partner in risposta all’invocazione -da parte dello stesso partner- di un’operazione di tipo request-response del processo; <invoke> il processo invia un messaggio per invocare un’operazione di tipo request-response oppure one-way contenuta nell’interfaccia WSDL di un partner (nel caso di operazione request-response, dopo avere inviato il messaggio di richiesta, il processo attende la ricezione di una risposta orrelata, che può anche essere un messaggio di errore); <assign> il processo aggiorna il contenuto di una o più variabili, attraverso uno o più assegnamenti elementari che vengono eseguiti in modo atomico (cioè tutti o nessuno); <wait> il processo attende senza fare nulla fino allo scadere di un timeout o fino a che non si raggiunge un certo istante di tempo (detto deadline); <empty> l’esecuzione di questa attività non ha alcun effetto (spesso viene usata per sincronizzare attività concorrenti o per ignorare un errore); <throw> l’esecuzione di questa attività provoca il sollevamento di un’eccezione, cioè notifica al resto del processo una situazione anormale o di errore; <compensate> vengono annullati gli effetti di un’attività precedentemente completata con successo, cioè senza che essa abbia sollevato eccezioni; <terminate> l’esecuzione del processo viene interrotta in maniera immediata. Sottolineiamo che un processo BPEL implementa un’operazione WSDL di tipo request-response tramite una coppia di attività receive e reply, mentre a un’operazione di tipo one-way viene implementata attraverso una singola attività receive. Un’operazione contenuta nell’interfaccia di un partner viene invocata dal processo mediante l’attività invoke. Di seguito descriviamo le attività strutturate: 26 Linguaggi per l’orchestrazione <sequence> le attività annidate tra i tag <sequence> e </sequence> vengono eseguite una dopo l’altra nell’ordine in cui compaiono tra i due tag; <switch> l’attività <switch> definisce un insieme di attività alternative, associate a condizioni in base alle quali viene selezionata ed eseguita una sola di tali attività; <while> l’attività annidata nel costrutto while viene eseguita ripetutamente fino a che una data condizione di controllo è verificata; <pick> si attende fino a che non viene ricevuto uno specifico messaggio, corrispondente all’invocazione di una delle operazioni WSDL del processo; <flow> le attività annidate tra i tag <flow> e </flow> vengono eseguite in modo concorrente; <scope> un’attività <scope> corrisponde ad un’unità logica di elaborazione e definisce un contesto (in inglese scope) per l’attività principale annidata nel tag <scope>, cioè indica quali sono le variabili locali, i gestori e delle eccezioni, l’attività di compensazione ed i gestori degli eventi dell’attività principale. BPEL utilizza un meccanismo di gestione degli errori chiamato Long-Running (Business) Transaction (LRTs), che permette di controllare in modo flessibile l’esecuzione di transazioni long-running. Quando si ha a che fare con questo tipo di transazioni è necessario poter definire delle procedure di compensazione, cioè delle attività che tentano di annullare gli effetti delle transazioni completate con successo. Un’attività di compensazione può essere invocata solo dopo che la transazione -per la o quale è stata definitaha completato con successo la sua esecuzione e se -in seguito- si verifica qualche situazione eccezionale che richieda l’annullamento degli effetti della suddetta transazione. Nel caso delle transazioni sul web non è solitamente possibile eseguire delle operazioni di rollback totale come nel caso delle transazioni tradizionali su una base di dati (in quel caso il rollback riporta la base di dati esattamente allo stato precedente l’esecuzione della transazione); per le transazioni su web invece si programmano delle procedure di rollback ad hoc, in base alla specifica business logic, che cercano per quanto possibile di annullare gli effetti della transazione per la quale sono state definite. L’elemento <faultHandlers> annidato in un’attività scope (o nello stesso elemento radice <process>) definisce un insieme di gestori delle eccezioni o fault handler, che sono attività che vengono eseguite se l’attività principale dello scope (o del processo) genera un’eccezione. Infatti, se viene sollevata un’eccezione, l’esecuzione dell’attività principale si interrompe e viene selezionato uno dei fault handler in base al tipo dell’eccezione. L’elemento <compensationHandler> annidato in un’attività scope (o nello stesso 2.2 Blite, un approccio formale a BPEL 27 elemento radice <process>) definisce invece l’attività di compensazione dello scope (o del processo stesso). Tale attività può essere invocata dopo il completamento dello scope attraverso l’esecuzione di un’attività compensate, che può essere contenuta in un fault handler o nel compensation handler dello scope in cui è annidato lo scope da compensare. Nonostante BPEL sia ampiamente documentato da specifiche e standard ufficiali e sia disponibile in diverse implementazioni, le problematiche relative al suo uso non mancano e alcune di esse possono essere attribuite alla mancanza di una semantica formale. Il processo di realizzazione di applicazioni BPEL risulta difficile e fortemente soggetto ad errori anche per la presenza di costrutti complessi come: il parallelismo, la concorrenza, la terminazione forzata di attività, la correlazione e la compensazione, quest’ultima utilizzata per la realizzazione di “Long-Running Transaction” e forse ancora non compresa in tutti i suoi aspetti e potenzialità dagli utilizzatori. In tal senso si è pensato che un processo di analisi e di formalizzazione di tali costrutti potesse essere utile sia per chi si trova ad utilizzarli sia per coloro che ne definiscono le specifiche in linguaggio naturale, in modo tale da renderne più chiaro ed eventualmente semplificarne gli aspetti più delicati. Un approccio, per cercare di affrontare queste problematiche, è stato quello di utilizzare la teoria dei metodi formali e delle algebre di processo per la definizione di una semantica formale e per la realizzazione successiva di una piattaforma per la verifica e la dimostrazione di proprietà di applicazioni basate su BPEL. Con tale scopo è stato creato Blite, una variante semplificata di BPEL, che riproduce alcune delle sue funzionalità più caratteristiche cercando d’altra parte di semplificarne alcuni aspetti ritenuti marginali nella realizzazione del modello Process Oriented. Di seguito andremo a descrivere la sintassi e la semantica originali di Blite e le versioni leggermente modificate di cui è stata realizzata l’implementazione. 2.2 Blite, un approccio formale a BPEL Come già detto Blite è un linguaggio che può essere visto come una semplificazione di BPEL, nel senso che ne ripropone solo alcuni aspetti essenziali come: partner link, process e activity termination, message correlation, fault handler e compesation handler, e ne tralascia altri come timeout, eventi, termination handler e flow graph. 28 Linguaggi per l’orchestrazione Anche il modello di invocazione dei servizi è stato semplificato. In BPEL difatti sono possibili sia invocazioni one-way che request-response. Le prime sono invocazioni asincrone, in cui il client dopo aver invocato può continuare la sua elaborazione senza dover attendere alcuna risposta, le seconde invece realizzano invocazioni sincrone, in cui l’operazione di richiesta blocca il client fino al sopraggiungere del risultato. In Blite è stato di fatto scelto di supportare solamente la comunicazione asincrona, in quanto il comportamento sincrono può essere sempre riprodotto tramite l’opportuna sequenzializzazione di operazioni asincrone. In generale in BPEL il meccanismo di instradamento dei messaggi alle opportune istanze di processo è realizzato tramite la Correlazione (Message Correlation) che discrimina rispetto ai valori applicativi contenuti in determinate parti del corpo stesso dei messaggi. In Blite viene rappresentata tale metodologia e si tralasciano tecniche alternative, come il WS-Addressing, che guida l’indirizzamento in base al valore di metainfomazioni contenute negli header1 . In definitiva è stato selezionato un core di BPEL per cui fosse abbastanza agevole definire una semantica formale e con cui, tramite “encoding”, fosse possibile riproporre tutte le funzionalità del linguaggio. In [1] vengono presentate in dettaglio le regole per la realizzazione dell’encoding, tramite Blite, dei costrutti BPEL che non sono stati inclusi in tale core. La sintassi di Blite è data in Tabella 2.1. La categoria sintattica Servizio (Service) rappresenta sia la definizione di processo [r • a f ], che le istanze nella loro esecuzione a runtime con uno specifico stato della memoria, µ ⊢ a; quest’ultima forma non è utilizzabile direttamente dal programmatore, ma serve per poter introdurre una rappresentazione della fase di esecuzione indispensabile per definire la semantica operazionale. Una definizione di servizio è semplicemente uno Scope (o Contesto) in cui è definita una Start Activity r e un Fault Handler a f . Difatti s’impone che le attività iniziali di un processo siano un sotto-insieme di tutte quelle possibili e che in pratica la prima attività operativa (o Basic Activity) sia una ricezione. Le attività sono divise in due categorie principali: le Basic Activity e le Structured activities. Le prime sono le attività primitive, cioè individuano le operazioni di base compiute da un’istanza di processo, le seconde sono una composizione strutturale di quest’ultime. Di fatto le attività di base sono costituite dall’invocazione asincrona inv ℓ i o x̄ di un’operazione remota o su un partner link 1 WS-Addressing, sebbene non faccia direttamente parte delle specifiche del linguaggio BPEL, viene utilizzato in numerose implementazioni come tecnica aggiuntiva alla Message Correlation. 2.2 Blite, un approccio formale a BPEL Basic activities b ::= inv ℓ i o x̄ | rcv ℓ r o x̄ | x := e | empty | throw | exit Structured activities a ::= b | if(e){a1 }{a2 } | while(e) {a} P r | a1 ; a2 | j∈J rcv ℓ j o j x̄ j ; a j | a1 | a2 | [a • a f ⋆ ac ] P r Start activities r ::= rcv ℓ r o x̄ | j∈J rcv ℓ j o j x̄ j ; a j | r ; a | r1 | r2 | [r • a f ⋆ ac ] 29 invoke, receive, assign empty, throw, exit basic, conditional, iteration sequence, pick (with | J | > 1) parallel, scope receive, pick sequence, parallel, scope Services s ::= [r • a f ] | µ ⊢ a | µ ⊢ a , s definition, instance, multiset Deployments d ::= {s}c | d1 k d2 deployment, composition Tabella 2.1: La sintassi di Blite ℓ i con parametri attuali x̄, dall’attesa dell’invocazione rcv ℓ r o x̄ dell’operazione locale o tramite il partner link ℓ r con parametri formali x̄, dall’assegnazione della valutazione dell’espressione e alla variabile x, dalla attività vuota empty, dalla sollevazione di una eccezione throw e dall’operazione di terminazione d’istanza exit. Le attività strutturate invece sono costituite dalla scelta condizionale if(·){·}{·}, dalla iterazione while(e) {a}, dalla composizione sequenziale di sottoattività a1 ; a2 , dalla P scelta esterna su un set non vuoto di possibili porte j∈J rcv ℓ jr o j x̄ j ; a j 2 , dalla composizione parallela di attività a1 | a2 e per finire dal costrutto di scope o contesto [a • a f ⋆ ac ], dove ad un’attività pricipale detta Contest Activity a viene associato un Fault Handler a f e un Compensation Handler ac . La sintassi afferma che le operazioni di comunicazione sono definite sui partner link i ℓ per l’invocazione e ℓ r per la ricezione. Un’ulteriore imposizione viene fatta sulla struttura sintattica di tali oggetti, richiedendo che essi siano tuple di uno o al massimo due elementi, con la seguente ulteriore restrizione: h u , p i hp, ui r con p staticamente noto e u evetualemente variabile ℓ = ℓi = hpi hui Di fatto i partner link possono essere monodirezionali h·i o bidirezionali hs1, s2i. In quest’ultimo caso sottintendono una comunicazione asincrona richiesta-risposta secon2 La scelta può essere anche espressa tramite l’operatore binario · + ·. 30 Linguaggi per l’orchestrazione x := "s1" rcv <"s1", z> op1 x ... inv <x, "s2"> op1 value ... inv <z> op2 k ... rcv <"s2"> op2 y Figura 2.1: Realizzazione della comunicazione asincrona richiesta-risposta con i construtti della sintassi di Blite do cui ad un’invocazione sul servizio s1 quest’ultimo risponderà in maniera asincrona con una risposta sul servizio s2. Di fatto s’impone anche che i nomi dei servizi su cui si eseguono le ricezioni siano staticamente noti3 . Un esempio di comunicazione asincrona con i costrutti definiti da Blite è rappresentato in Figura 2.1, dove è esplicitato il fatto che i nomi dei servizi s1 e s2 oggetto delle invocazioni sono determinati a runtime. La composizione distribuita di diversi processi viene definita dalla categoria sintattica Deployments. Un termine d1 k d2 rappresenta la contemporanea esecuzione di tutte le istanze ottenute dalle definizioni presenti in d1 e in d2 . Un Deployment {s}c è formato dalla definizione di processo s con tutte le istanze locali a cui è stato associato il Correlation Set c. Tale insieme individua fra tutte le variabili presenti nella definizione del processo quelle che dovranno essere considerate per valutare la correlazione di un messaggio ad una particolare istanza. Vedremo come la semantica di Blite definisca tale processo di attribuzione di un messaggio a una istanza e come tale semantica sia stata implementata del nostro engine di esecuzione. In generale si impone la restrizione che un insieme di deployments sia ben formato, nel senso che i nomi dei partner link utilizzati per le ricezioni non siano condivisi fra diversi deployment. In questo modo ogni definizione di processo avrà i propri nomi di servizio univoci e dato un nome sarà sempre possibile individuare un deployment specifico. In seguito, si farà uso della notazione ¯·, per denotare tuple di valori o variabili, x̄ 3 Quest’ultima imposizione rispecchia il fatto che staticamente sono noti i contratti e fissate le locazioni su cui essi sono disponibili; a runtime le varie istanze si possono scambiare quest’ultima informazione, ma il modello non prevede che possano nascere ne nuovi contratti ne nuove locazioni dove questi siano implementati. 2.2 Blite, un approccio formale a BPEL 31 può essere considerata l’abbreviazione di hx1 , . . . , xh i (con h ≥ 0). L’ulteriore notazione ˜· indicherà tuple speciali di uno o al massimo due elementi (p̃ starà per hp1 , p2 i o hp1 i). Inoltre l’operatore · : · permetterà di ottenere la tupla hp, u, x1 , . . . , xh i come concatenazione di hp, ui e hx1 , . . . , xh i scrivendo hp, ui : hx1 , . . . , xh i. Per concludere si osservi come la sintassi esposta debba ancora essere considerata “astratta”, in quanto non definisce tutti gli aspetti necessari all’imlementazione. In particolar modo non definisce i tipi dei valori attribuibili alle variabili, né la sintassi delle espressioni supportate e nemmeno esplicita la precedenza degli operatori di sequenzializzazione, parallelismo e scelta esterna4 . Di fatto l’implementazione da noi realizzata fa riferimento ad una sintassi concreta che definisce in maniera formale anche questi aspetti. In particolare vedremo come i semplici operatori binari siano trasformati in costrutti con delimitatori di blocco. In questo modo risulta più semplice l’implementazione del parser e anche la precedenza di un attività rispetto l’altra risulta esplicitata nella codifica stessa dei blocchi. Cosı̀ il codice risulta più leggibile e più simile nella struttura sia ai tradizionali linguaggi di programmazione che ad un linguaggio come BPEL basato su XML . Presentiamo ora la semantica formale in termini operazionali. Le relazioni semantiche sono definite su termini che includono ulteriori simboli e costrutti, rispetto a quelli definiti dalla sintassi di Tabella 2.1, allo scopo di rappresentare alcuni aspetti dinamici dell’esecuzione. In particolare vengono introdotti: • Protected activity, LaM, utilizzato per sostituire contesti falliti con i rispettivi compesation handler da eseguire in maniera protetta. La semantica chiarirà il significato di esecuzione protetta. • Unsuccessful termination, stop, utilizzato per uniformare il comportamento delle attività exit e throw. • Message ≪ p̃ : o : v̄ ≫, utilizzato per rappresentare i messaggi prodotti dalle attività invoke. • Scope dalla forma [a′ • a f ⋆ ac △ ad ], utilizzato per rappresentare l’evoluzione del 4 In merito alla precedenza degli operatori, il documento originale in cui è descritto Blite, afferma che l’operatore di sequenza ·; · ha precedenza sull’operatore di parallelismo ·|·, e che quest’ultimo ha precedenza sull’operatore di scelta · + · 32 Linguaggi per l’orchestrazione contesto [a • a f ⋆ ac ], in cui il completamento di sottocontesti definiti in a hanno prodotto l’istallazione di compensation handler rappresentati con ad . Inoltre si introduce il concetto di attività short-lived con l’intento d’individuare un sott’insieme fra tutti i tipi di attività presenti nel linguaggio. Vedremo che la semantica attribuirà a tali attività la proprietà di essere immuni alla terminazione (in un certo senso queste attività dovranno essere considerate atomiche rispetto al processo di terminazione). Tale insieme è costituito dalla seguenti attività Attività short-lived : {empty, exit, throw, stop, ≪ p̃ : o : v̄ ≫} e di seguito una generica attività short-lived sarà indicata con il simbolo sh. La semantica dei termini Blite è definita tramite una congruenza strutturale, e tramite due relazioni di transizione, una che descrive l’evoluzione delle istanze in termini di operazioni interne e una che descrive l’evoluzione del sistema di deployment attraverso le comunicazioni. La Congruenza Strutturale identifica come equivalenti termini sintatticamente diversi ma che intuitivamente rappresentano il medesimo comportamento. Essa è definita come la più grande congruenza indotta dalle equazioni rappresentate in Tabella 2.2, dove peraltro non sono riportate le leggi che esplicitano le ovvie proprietà di commutatività e associatività per gli operatori binari. Dalla congruenza strutturale deduciamo che: l’attività empty agisce come l’elemento identità sia per l’operatore di sequenza che per l’operatore di parallelismo. La composizione parallela di più attività stop è equivalente ad un’unica stop, mentre la stessa stop premessa nella sequenzializzazione disabilita le attività successive. L’operatore di protezione L·M risulta idempotente e le attività short-lived sono da considerarsi implicitamente protette, in questo modo è espressa la loro immunità alla terminazione. I messaggi possono essere estratti dall’operatore di protezione. All’inizio dell’esecuzione di uno scope il compensation handler istallato è da considerarsi uguale all’attività empty. Importante è notare come la produzione di messaggi non blocchi le attività successive, né il completamento di scope a meno che nello scope stesso non sia attivato un throw (quest’ultima possibilità è verificata tramite il predicato · ⇓throw che verrà formalizzato più avanti). Quest’ultime equazioni concorrono alla definizione di un modello di comunicazione puramente asincrono per i processi Blite. Le equazioni rimanenti risultano particolarmente ovvie, in quanto estendono la congruenza strutturale all’applicazione dei costrutti di scope, deployment e composizione di deployment. 2.2 Blite, un approccio formale a BPEL a | empty ≡ a 33 empty ; a ≡ a ; empty ≡ a LLaMM ≡ LaM LshM ≡ sh stop | stop ≡ stop L≪ p̃ : o : v̄ ≫ | aM ≡ ≪ p̃ : o : v̄ ≫ | LaM [a • a f ⋆ ac ] ≡ [a • a f ⋆ ac △ empty] (≪ p̃ : o : v̄ ≫ | a1 ) ; a2 ≡ ≪ p̃ : o : v̄ ≫ | (a1 ; a2 ) [≪ p̃ : o : v̄ ≫ | a • a f ⋆ ac △ ad ] ≡ ≪ p̃ : o : v̄ ≫ | [a • a f ⋆ ac △ ad ] ′ a≡a af ≡ stop ; a ≡ stop a′f ac ≡ a′c ad ≡ if ¬a ⇓throw a′d [a • a f ⋆ ac △ ad ] ≡ [a′ • a′f ⋆ a′c △ a′d ] r ≡ r′ a f ≡ a′f {[r • a f ] , s}c ≡ {s , [r′ • a′f ]}c d1 k d2 ≡ d2 k d1 a ≡ a′ {µ ⊢ a , s}c ≡ {s , µ ⊢ a′ }c (d1 k d2 ) k d3 ≡ d1 k (d2 k d3 ) {µ ⊢ stop , s}c ≡ {s}c {µ ⊢ empty , s}c ≡ {s}c {µ ⊢ empty}c k d ≡ d {µ ⊢ stop}c k d ≡ d Tabella 2.2: Congruenza Strutturale per le attività e i deployment Per concludere si osservi che istanze del tipo µ ⊢ empty e µ ⊢ stop risultano terminate e possono essere eliminate. Equivalentemente deployment contenenti solamente istanze terminate sono da considerarsi terminate e possono a loro volta essere eliminate. L’evoluzione delle attività di un’istanza di processo è descritta dalla relazione di α transizione etichettata −−_, le cui regole sono presentate in Tabella 2.3, dove le etichette o azioni α sono generate dalla seguente grammatica: α ::= τ | x ← v | ! p̃ : o : v̄ | ? ℓ r : o : x̄ | i | | (a) in cui i nuovi simboli introdotti devono essere interpretati come le seguenti azioni: • τ indica la produzione di un messaggio o operazioni interne come la valutazione di test nella scelta condizionata e nell’iterazione o l’istallazione/attivazione di compensation handler. • x ← v indica l’assegnamento del valore v alla variabile x. • ! p̃ : o : v̄ e ? ℓ r : o : x̄ indicano rispettivamente l’esecuzione di un’invocazione 34 Linguaggi per l’orchestrazione ? ℓ r :o:x̄ τ µ ⊢ inv ℓ i o x̄ −_ ≪ µ(ℓ i ) : o : µ(x̄) ≫ (inv) rcv ℓ r o x̄ −−−−−−_ empty (rec) x←µ(e) µ ⊢ x := e −−−−−−_ empty (asg) throw −_ stop (thr) α exit −−_ stop (term) ≪ p̃ : o : v̄ ≫ −−−−−_ empty (msg) α µ ⊢ a1 −−_ a′1 α µ ⊢ a1 ; a2 −−_ a′1 ; a2 ( a1 if µ(e) = tt a= a2 if µ(e) = ff τ µ ⊢ if(e){a1 }{a2 } −_ a α µ ⊢ a1 −−_ a′1 µ ⊢ a −−_ a′ ! p̃:o:v̄ i P (seq) α µ ⊢ LaM −−_ La′ M (prot) ? ℓhr :oh :x̄h j∈J rcv ℓ jr o j x̄ j ; a j −−−−−−−−_ ah (h ∈ J) (pick) a′ = (if) ( a ; while(e) {a} if µ(e) = tt empty if µ(e) = ff (while) τ µ ⊢ while(e) {a} −_ a′ α < {i, } ¬(a2 ⇓throw ∨ a2 ⇓exit ) α µ ⊢ a1 | a2 −−_ a′1 | a2 (a c ) [empty • a f ⋆ ac △ ad ] −−−_ empty (done1 ) α µ ⊢ a −−_ a′ α (par1 ) a1 −−_ a′1 α ∈ {i, } α a1 | a2 −−_ a′1 | end(a2 ) (par2 ) τ [stop • a f ⋆ ac △ ad ] −_ Lad ; a f M (done2 ) α < {, (a′′ )} α µ ⊢ [a • a f ⋆ ac △ ad ] −−_ [a′ • a f ⋆ ac △ ad ] (exec) (a′′ ) a −−−−_ a′ τ [a • a f ⋆ ac △ ad ] −_ [a′ • a f ⋆ ac △ a′′ ; ad ] (done3 ) a −_ a′ τ [a • a f ⋆ ac △ ad ] −_ [a′ • a f ⋆ ac △ ad ] (fault) Tabella 2.3: Semantica operazionale per le attività. sull’operazione o, con il partner link valorizzato a p̃ e parametri attuali v̄, e la ricezione sull’operazione o, partner link ℓ r e parametri formali x̄. • i indica la richiesta di una terminazione forzata di un’istanza di processo. • indica la generazione di un’eccezione. • (a) indica il completamento con successo di uno scope che potrà essere eventualmente compensato tramite l’attività a. α La relazione −−_ è definita con l’ausilio della funzione di stato µ che associa un 2.2 Blite, un approccio formale a BPEL 35 valore ad ogni variabile dell’istanza a cui è associata (vi è uno stato distinto e unico per ogni istanza di processo, mentre non vi è il concetto di stato locale associato ai contesti). In particolare µ(x) restituisce il valore della variabile x nello stato µ e µ(e) restituisce il risultato della valutazione dell’espressione e nello stato µ. La funzione stato µ può essere aggiornata con µ′ , scrivendo µ ◦ µ′ , in modo da avere µ′ (x) se x ∈ dom(µ′ ) µ ◦ µ′ (x) = µ(x) altrimenti Bisogna osservare che in alcune regole di Tabella 2.3 tale funzione non è esplicitata, α α per esempio per semplicità si è scritto a −−_ a′ invece di µ ⊢ a −−_ a′ . Alcune funzioni e predicati ausiliari, come già precedentemente osservato, sono stati introdotti a supporto della definizione semantica. In particolare i predicati a ⇓exit and a ⇓throw valutano la capacità di a di eseguire exit o throw, rispettivamente. Essi sono definiti per induzione sulla struttura sintattica delle attività e di fatto valgono sempre falso tranne che per i casi seguenti in cui si ha: exit ⇓exit throw ⇓throw a1 ⇓exit a1 ⇓throw a1 ; a2 ⇓exit a1 ; a2 ⇓throw a ⇓exit a ⇓exit a ⇓exit [a • a f ⋆ ac △ ad ] ⇓exit LaM ⇓exit LaM ⇓throw a1 ⇓exit ∨ a2 ⇓exit a1 ⇓throw ∨ a2 ⇓throw a1 | a2 ⇓exit a1 | a2 ⇓throw Per rappresentare la semantica della terminazione viene introdotta la funzione end(·), che applicata ad un’attività, la restituisce in modo da lasciare solamente le attività short-lived e quelle protette. Anche tale funzione è definita sulla struttura sintattica delle attività e si ha che ∀a end(a) = stop tranne i seguenti casi: end(sh) = sh end(LaM) = LaM end([a • a f ⋆ ac △ ad ]) = [end(a) • a f ⋆ ac △ ad ] end(a1 ; a2 ) = end(a1 ) end(a′ | a′′ ) = end(a′ ) | end(a′′ ) in cui a1 non può essere congruente a empty o a ≪ p̃ : o : v̄ ≫ o alla composizione parallela di essi. Commentiamo ora le regole di Tabella 2.3. 36 Linguaggi per l’orchestrazione (inv) (asg) Affermano che le attività di invocazione e di assegnamento possono procede- re solamente se i loro argomenti sono espressioni chiuse (cioè senza varibili non inizializzate). Inoltre (inv) insieme alla successiva (msg) e alla congruenza strutturale definisce la semantica asincrona per la comunicazione. Si osservi infatti il seguente comportamento: τ inv ℓ i o x̄ ; a −_ ≪ µ(ℓ i ) : o : µ(x̄) ≫ ; a ≡ (≪ µ(ℓ i ) : o : µ(x̄) ≫ | empty) ; a ≡ ≪ µ(ℓ i ) : o : µ(x̄) ≫ | (empty ; a) ≡ ≪ µ(ℓ i ) : o : µ(x̄) ≫ | a (rec) Un’operazione di ricezione offre la possibilità di invocazione di un’operazione locale su un fissato partner link. Da osservare che a differenza dell’invocazione, la ricezione blocca le attività sequenzialmente successive. (thr) (term) Rappresentano rispettivamente la produzione di un’eccezione e di una richiesta di terminazione per l’istanza corrente. (msg) Un messaggio può essere consumato indipendentemente dalla sua generazione. (prot) Il costrutto LaM permette la normale esecuzione di a al suo interno. Poiché end(LaM) = LaM, con l’operatore L·M si ottiene la protezione di a dalle richieste esterne di terminazione. (seq) (pick) (if) (while) Esprimono una semantica standard rispetto ai tradizionali linguag- gi di programmazione e alle più comuni algebre di processo che prevedono l’operatore di scelta esterna. (par1 ) (par2 ) Esprimono la semantica caratteristica di BPEL riguardo alla terminazione nella composizione parallela con attività fallite. Di fatto esse affermano che l’esecuzione di attività parallele evolve come atteso se nessuna di esse ha sollevato un’eccezione o richiesto una terminazione. Viceversa, se questo accade, tutti i rami della composizione parallela vengono terminati. Si ricordi anche che per definizione end(a′ | a′′ ) = end(a′ ) | end(a′′ ). (done1 ) (done3 ) Esprimono la caratteristica di BPEL secondo cui contesti completati con successo istallano nei rispettivi contesti padre i loro compensation handler per un’eventuale successiva compensazione. Il contesto padre colleziona in sequenza i compensation halder di contesti figli completati (done3 ). Questa semantica, differentemente dalle ultime specifiche informali di BPEL, prevede che i compensation handler siano mantenuti attraverso un solo grado di parentela nella gerarchia dei 2.2 Blite, un approccio formale a BPEL 37 contesti5 . Difatti un contesto completato istalla nel suo padre solo il suo compensation handler e non quelli istallati dai suoi figli. Una semantica alternativa poteva essere: (ad ;ac ) [empty • a f ⋆ ac △ ad ] −−−−−_ empty in cui si mantengono anche i compensation handler precedentemente istallati. (exec) Se non si producono eccezioni l’esecuzione interna ad uno scope procede come atteso. Da osservare che questa regola è da applicare anche nel caso in cui a i richieda una terminazione (a −−_ a′ − _ . . . stop); qui a differenza di (fault) l’azione i viene propagata anche al di fuori del contesto per produrre la terminazione di tutta l’istanza e successivamente tramite l’applicazione di (done2 ) vengono eseguiti i compensation handler istallati e il fault hadler del contesto corrente. (done2 ) In uno scope in cui la contest activity giunge a stop a causa di un’eccezione o di una richiesta di terminazione, attraverso una transizione interna si attiva l’esecuzione protetta dei compensation handler istallati e del fault handler. Importante è osservare come l’esecuzione dell’ambiente protetto avvenga nello stato attuale dell’istanza. (fault) La sollevazione di un’eccezione in a non viene propagata al di fuori dello sco- pe, ma gestita tramite una transizione interna. La successiva evoluzione di a −_ a′ − _ . . . stop in cui vengono terminati gli eventuali rami paralleli, porterà all’applicazione di (done2 ). Abbiamo definito il comportamento delle attività interne delle istanze di processo, vediamo ora come avviene la creazione di tali istanze e la comunicazione in sistemi composti da più deployment. Il comportamento di questi ultimi è definito dalla relazione di riduzione ≻− _, le cui regole sono presentate in Tabella 2.4. La problematica fondamentale è quella di individuare, fra le varie istanze di un deployment, quella predisposta alla ricezione di un messaggio, o capire se il sopraggiungere di un messaggio debba causare la creazione di una nuova istanza. 5 Un punto abbastanza discusso, nelle prime specifiche di BPEL è stato il meccanismo di realizzazione della compensazione. Il modello proposto dalla semantica di Blite non vuole essere una semplificazione, ma riprodurre la linea di pensiero secondo cui la compensazione di un contesto esegue ogni azione necessaria ad annulare tutti gli effetti del contesto stesso, comprese quindi le azioni svolte dai sotto-contesti. 38 Linguaggi per l’orchestrazione ! t2 ? t1 a2 −−−_ a′2 a1 −−−_ a′1 match(c1 , µ1 , t1 , t2 ) = µ′1 c1 ,|µ′1| ¬ ( µ1 ⊢ a1 , s1 ⇓t2 ) (com) _ {µ1 ◦ µ′1 ⊢ a′1 , s1 }c1 k {µ2 ⊢ a′2 , s2 }c2 {µ1 ⊢ a1 , s1 }c1 k {µ2 ⊢ a2 , s2 }c2 ≻− ? t1 [r • a f ⋆ empty] −−−_ a1 ! t2 a2 −−−_ a′2 c ,|µ1| match(c1 , ∅, t1 , t2 ) = µ1 ¬ (s1 ⇓t21 ) (new) {[r • a f ] , s1 }c1 k {µ2 ⊢ a2 , s2 }c2 ≻− _ {µ1 ⊢ a1 , [r • a f ] , s1 }c1 k {µ2 ⊢ a′2 , s2 }c2 x←v µ ⊢ a −−−−_ a′ {µ ⊢ a , s}c ≻− _ {µ ◦ µ′ ⊢ a′ , s}c α µ ⊢ a −−_ a′ d1 ≻− _ d′1 match(c, µ, x, v) = µ′ d1 k d2 ≻− _ d′1 k d2 d ≡ d1 α < {? t1 , ! t2 , x ← v} {µ ⊢ a , s}c ≻− _ {µ ⊢ a′ , s}c (var) (enab) d1 ≻− _ d2 d ≻− _ d′ (part) d2 ≡ d′ (cong) Tabella 2.4: Regole di riduzione per i deployment (si consideri t1 = ℓ r : o : x̄ e t2 = p̃ : o : v̄). Nella definizione originale di Blite è stato scelto un approccio puramente semantico, nel senso che né la sintassi né la struttura dei messaggi prevedono la presenza di informazioni aggiuntive che possono essere utilizzate per guidare tali scelte. Di fatto si è scelto di definire un ordinamento fra tutte le istanze di processo che concorrono alla ricezione di un determinato messaggio e di dare la possibilità di consumare il messaggio solo alle prime istanze dell’ordinamento. Talvolta tale ordinamento può individuare più di un’istanza destinataria del messaggio, in questo caso la semantica non specifica ulteriormente la scelta e consente un comportamento non deterministico. In maniera informale l’ordinamento si basa sul seguente criterio: “i messaggi che giungono alle porte di una definizione di processo possono, tramite i parametri formali, aggiungere più o meno informazione nello stato di una istanza che riceve. Un messaggio costituito dalla tupla hv1 , . . . , vn i, tramite i parametri formali hx1 , . . . , xn i ha grado di definizione k (k ≤ n), se il messaggio definisce o ridefinisce con nuovi valori esattamente k variabili formali e lascia immutato lo stato delle restanti n−k. Un messaggio può essere consumato solamente da una fra le istanze con grado di definizione minimo”. Insieme a questa regola si impone il vincolo generale che l’assegnamento ad una variabile inclusa nel correlation set può essere fatto una sola volta, cioè non è possibile assegnare un valore diverso ad una variabile del correlation set già precedentemente inizializzata. 2.2 Blite, un approccio formale a BPEL | match(c, µ, ℓ r : o : x̄, p̃ : o : v̄) |< n µ ⊢ rcv ℓ r o x̄ ; a ⇓p̃c,n :o:v̄ µ ⊢ a1 ⇓p̃c,n :o:v̄ 39 ∃ h ∈ J . | match(c, µ, ℓhr : oh : x̄h , p̃ : o : v̄) |< n P µ ⊢ j∈J rcv ℓ jr o j x̄ j ; a j ⇓p̃c,n :o:v̄ µ ⊢ a1 ⇓p̃c,n ∨ µ ⊢ a2 ⇓p̃c,n :o:v̄ :o:v̄ µ ⊢ a1 ; a2 ⇓p̃c,n :o:v̄ µ ⊢ a1 | a2 ⇓p̃c,n :o:v̄ µ ⊢ a ⇓p̃c,n :o:v̄ µ ⊢ a ⇓p̃c,n :o:v̄ µ ⊢ a ⇓p̃c,n ∨ s ⇓p̃c,n :o:v̄ :o:v̄ µ ⊢ LaM ⇓p̃c,n :o:v̄ µ ⊢ [a • a f ⋆ ac △ ad ] ⇓p̃c,n :o:v̄ µ ⊢ a , s ⇓p̃c,n :o:v̄ Tabella 2.5: Definizione del predicato s ⇓p̃c,n:o:v̄ . Se ne conclude che non è possibile leggere un messaggio che assegnerebbe un valore diverso ad una variabile di correlazione già precedentemente inizializzata. Questi ragionamenti vengono formalizzati nella seguente funzione match(c, µ, x̄, v̄), che prende un correlation set c, una funzione di stato µ, una tupla di variabili x e una tupla di valori v e restituisce una funzione di stato eventualmente anche vuota su un sotto-insieme delle variabili di x̄: {x 7→ v} if x < c ∨ (x ∈ c ∧ x < dom(µ)) match(c, µ, x, v) = ∅ if x ∈ c ∧ {x 7→ v} ∈ µ match(c, µ, hi, hi) = ∅ match(c, µ, x1 , v1 ) = µ′ match(c, µ, x̄2 , v̄2 ) = µ′′ match(c, µ, (x1 , x̄2 ), (v1 , v̄2 )) = µ′ ◦ µ′′ Si osservi come match(c, µ, x̄, v̄) non sia definita nel caso in cui le tuple x̄ e v̄ abbiano diversa lunghezza e quando si abbia per un qualche i che xi ∈ c e {xi 7→ v′ } ∈ µ con v′ , vi . Quest’ultimo fatto impone quanto precedentemente detto che un’istanza non possa ricevere messaggi i cui valori assegnati alle variabili attuali non siano compatibili con lo stato del correlation set. Con la funzione match(·, ·, ·, ·) è possibile definire il predicato s ⇓p̃c,n:o:v̄ , che valuta se un’istanza di processo s con correlation set c ha un’attività di ricezione attiva che possa 40 Linguaggi per l’orchestrazione ricevere il messaggio p̃ : o : v̄ con un grado di definizione minore di n. In Tabella 2.5 tale predicato viene definito induttivamente sulla struttura sintattica di s A questo punto il comportamento descritto dalle regole di Tabella 2.4 risulta abbastanza intuitivo e può essere sintetizzato nel seguente modo: (com) Afferma che la comunicazione fra due istanze di processo può avvenire solo se si realizza il matching, in rispetto dello stato di correlazione, dei valori del messaggio con le variabili di ricezione. Ovviamente ci deve essere anche uguaglianza sintattica fra i partner link e il nome dell’operazione. Tale regola afferma anche che fra tutte le istanze con un’attività di ricezione attiva conforme con i partner link e l’operazione del messaggio in arrivo, se ne debba scegliere una con grado di definizione minimo. (new) Descrive la creazione di una nuova istanza di processo a partire dalla definizione [r • a f ] a seguito dell’arrivo di un messaggio t2 = p̃ : o : v̄ che risulti in matching con una delle start activity della definizione stessa. La nuova istanza verrà creata solo se non c’è già un’istanza capace di ricevere il messaggio con un grado di definizione minore della start activity. (var) Afferma quanto già precedentemente detto che l’assegnamento di un valore v ad una variabile x del correlation set su uno stato µ può avvenire solo se x < dom(µ) o se {x 7→ v} ∈ µ. (part) (pass) (cong) Tali regole risultano particolarmente ovvie ed esprimono rispettiva- mente l’evoluzione parallela di un sistema di deployment, l’esecuzione indipendente delle operazioni interne in ciascuna istanza, l’uguaglianza delle riduzioni di deployment strutturalmente congruenti. 2.3 Una grammatica per un compilatore Il primo passo per lo sviluppo di un motore di esecuzione per Blite è la realizzazione di un parser per l’analisi di programmi ottenibili dalla sintassi presentata in Tabella 2.1. Un parser è il software predisposto alla realizzazione dell’Analisi Sintattica, cioè del procedimento che riconosce la struttura del programma sorgente e costruisce l’Albero Sintattico (Abstract Sintatic Tree - AST) associato. Come già osservato la sintassi esposta, pur specificando in modo formale i termini del linguaggio, non si presta ad essere 2.3 Una grammatica per un compilatore 41 implementata direttamente, in quanto per esempio presenta regole del tipo a ::= . . . a;a | a|a ... che la rendono ambigua, per cui ad una frase del linguaggio può corrispondere più di un albero sintattico. Risulta chiaro quindi che il primo obiettivo è stato quello di ottenere una grammatica direttamente implementabile dal cui linguaggio sia possibile ottenere i programmi Blite. Di seguito chiameremo termini Blite gli elementi del linguaggio derivato dalla sintassi di Tabella 2.1, mentre chiameremo programmi Blite le frasi del linguaggio realmente implementato. Cenni di teroria dei linguaggi Dalla teoria sappiamo che le grammatiche per generare linguaggi analizzabili con parser deterministici, cioè che non fanno uso di tecniche di backtraking, sono raggruppabili in sotto classi specifiche fra tutte le possibili grammatiche context free. In particolar modo risultano fondamentali per la realizzazione di analizzatori sintattici le seguenti classi di grammatiche: • LL(k): Per cui risulta possibile realizzare parser top-down deterministici che utilizzano al più k simboli non ancora riconosciuti della frase di input. • LR(k): Per cui risulta possibile realizzare parser bottom-up deterministici che utilizzano al più k simboli non ancora riconosciuti della frase di input. I parser top-down costruiscono l’albero sintattico dalla radice (simbolo iniziale della grammatica) alle foglie (simboli terminali della frase di input) secondo la derivazione canonica sinistra, mentre quelli bottom-up realizzano l’albero sintattico dalle foglie alla radice seguendo la riduzione destra. La derivazione è quel processo che a partire dal simbolo iniziale della grammatica S arriva alla frase di input applicando ad ogni passo una sostituzione di un simbolo non terminale X con una parte destra Y presente in una regola della grammatica del tipo X ::= Y. Una derivazione può essere scritta come S ⇒ · · · ⇒ abcX . . . ⇒ abcY . . . · · · ⇒ abcdeZ . . . ⇒ · · · ⇒ abcde f . . . mn in cui l’ultima stringa è formata solo da terminali. Con il simbolo ⇒∗ si intendono zero o più passi successivi e con ⇒+ uno o più passi successivi. I singoli passi della 42 Linguaggi per l’orchestrazione derivazione legano forme di frasi ammesse dalla grammatica, dove una forma di frase non è altro che una stringa s ∈ V ∗ , con V insieme dei terminali e non terminali della grammatica. In un’analisi top-down si realizza la derivazione canonica sinistra, cioè si impone la sostituzione del simbolo non terminale che compare più a sinistra nella forma di frase corrente. Quindi quest’ultima avrà sempre una forma del tipo aXw con a ∈ VT ∗ (VT insieme dei simboli terminali del linguaggio), X simbolo non terminale corrente e w ∈ V ∗ . La stringa a costituirà un prefisso della frase di input e indicherà la parte di essa già riconosciuta. Analogamente alla derivazione canonica sinistra si definisce la derivazione canonica destra che sceglie di sostituire il simbolo non terminale che compare più a destra nella forma di frase corrente. L’analisi bottom-up realizza la riduzione destra che è la sequenza inversa della derivazione canonica destra. In generale con le grammatiche LR si riesce a rappresentare un numero maggiore di linguaggi rispetto alle grammatiche LL, cioè l’insieme dei linguaggi generati con grammatiche LL è contenuto nell’insieme dei linguaggi generati con grammatiche LR; con quest’ultime però risulta molto più complessa la realizzazione del parser, in quanto è necessario implementare algoritmi e strutture dati più raffinati. Inoltre le grammatiche LL hanno caratteristiche che le rendono facilmente individuabili e da una generica grammatica, come quella di Tabella 2.1, si può ottenere un grammatica LL applicando i seguenti passi:6 • Eliminazione della ricorsione sinistra diretta o indiretta. Una grammatica presenta ricorsione sinistra quando ammette derivazioni del tipo X ⇒+ Xu, dove X ∈ V N e u ∈ V + (la ricorsione diretta si ha banalmente quando sono presenti regole del tipo X ::= Xu), in tal caso l’analizzatore top-down può entrare in un ciclo infinito. Esiste un algoritmo per eliminare tale problema (si veda [19]), ma in generale può essere evitato semplicemente aggiungendo nuovi non terminali e riorganizzando le regole. Per esempio la seguente grammatica: E ::= E + T | E − T | T | − T T ::= T ∗ F | F F ::= (E) | i 6 Per una trattazione formale riguardo al riconoscimento di grammatiche LL(k) si rimanda ai testi specifici della teoria del linguaggi formali come [19] e [20]. 2.3 Una grammatica per un compilatore 43 che presenta ricorsione sinistra può essere trasformata nella grammatica: E E′ T T′ F ::= ::= ::= ::= ::= T E′ | − T E′ +T E ′ | − T E ′ | ǫ FT ′ ∗FT ′ | ǫ (E) | i che ne è priva. • Fattorizzazione sinistra. Questo procedimento serve ad eliminare un eventuale prefisso comune a due parti destre di regole associate allo stesso simbolo non terminale, cosa che in generale porta non determinismo nel processo di derivazione canonica sinistra. Se, ad esempio, una regola della grammatica è del tipo: A ::= yv | yw con y ∈ V + , v ∈ V ∗ e w ∈ V + e le stringhe v, w non hanno prefissi comuni, l’analizzatore top-down, quando espande il simbolo non terminale A non può, in generale, scegliere con certezza la parte destra di A da usare, guardando un solo simbolo (o anche più di uno ma in numero determinato) in avanti nella frase di input. Avrebbe conferma della scelta fatta, soltanto dopo aver riconosciuto una sottostringa derivabile da y e, in caso di scelta errata, sarebbe costretto ad effettuare backtracking. Per eliminare quest’incertezza, è necessario fattorizzare a sinistra tali parti destre, cioè sostituire la regola con le due A := yA′ e A′ := v | w. Questa tecnica si può estendere al caso in cui vi siano anche più di due parti destre associate allo stesso simbolo non terminale aventi un prefisso comune. La grammatica per Blite Nel realizzare la nostra implementazione si è scelto di produrre una grammatica LL(k) per rappresentare i programmi Blite. Per la precisione una grammatica LL(1), cioè una grammatica il cui parser è in grado di riconoscere le frasi del linguaggio senza fare lookahead, ma utilizzando unicamente il primo token ancora non riconosciuto nella frase di input. Nel momento in cui ci si trovi a dover realizzare un parser le strade da poter percorrere sono fondamentalmente due: la prima è quella di munirsi di un buon background teorico ed implementare, partendo da zero, tutto il software necessario; la seconda invece prevede l’utilizzo di un tool per la creazione automatica di compilatori che, a partire da una definizione formale della grammatica, generi tutto o parte del codice necessario. 44 Linguaggi per l’orchestrazione Grazie all’ampia disponibilità di tool (anche open source)7 , all’ottimo livello di affidabilità e all’alto numero di funzionalità offerto da questi, si è optato per la seconda strada. In particolare si è scelto di utilizzare JavaCC (Java Compiler Compiler) [22]. Questo è uno dei più diffusi generatori di parser in linguaggio Java e permette di realizzare in maniera produttiva ed efficiente compilatori per un gran numero di tipologie di linguaggi di programmazione. JavaCC di fatto realizza parser top-down a discesa ricorsiva a partire da grammatiche LL(k) definite in un formalismo specifico in cui una notazione simile a EBNF è arricchita con istruzioni semantiche espresse direttamente in linguaggio Java. In realtà JavaCC non si limita alle grammatiche LL(k), ma in pratica gestisce qualsiasi grammatica che non presenti ricorsione sinistra. Infatti dispone di un supporto estremamente potente per eseguire lookahead anche sintattico e semantico8 . Questo fa sı̀ che in pratica il parco dei linguaggi implementabili con JavaCC sia del tutto uguale a quello realizzabile con tool come Cup o SableCC che supportano grammatiche LALR. Un punto a sfavore, rispetto ad altri tool, può essere invece che JavaCC di per sè non offre nessun supporto alla creazione degli alberi sintattici. Di fatto è generato un semplice parser top-down a discesa ricorsiva in cui l’utente può inserire qualsiasi tipo di azione tramite codice Java, e tramite questa possibilità si lascia all’utente la completa responsabilità di creare un eventuale albero sintattico. Si capisce bene che questa scelta, sebbene permetta di realizzare parser molto efficienti nel caso di linguaggi molto semplici, risulta penalizzante qualora si voglia realizzare alberi sintattici per linguaggi con un certo grado di complessità strutturale. Per superare questa limitazione, all’interno del medesimo progetto, è stato sviluppato JJTree, che realizza un preprocessore per i file letti da JavaCC. In pratica JJTree supporta un’estensione del linguaggio di specifica di JavaCC, in cui è possibile inserire, direttamente nella grammatica e in formalismo dichiarativo, informazioni per guidare la 7 Rimanendo solamente nell’abito della piattaforma Java, si contano numerosi software per la generazione automatica di compilatori, fra i più diffusi certamente troviamo: ANTLR, SableCC, CUP (Java Yacc/Bision), JavaCC, Coco/R, JLex/JFlex. Per una lista più ampia si faccia riferimento alla pagina online http://java-source.net/open-source/parser-generators. 8 Il lookahead sintattico prevede non di considerare in avanti un numero fisso di token, ma di fare in avanti una vera e propria sotto analisi sintattica. In base alla categoria sintattica riconosciuta “in avanti” è possibile fare la scelta adeguata nell’espansione del non terminale corrente nell’analisi principale. Il lookahead semantico prevede invece che il programmatore possa definire delle azioni semantiche che accedendo arbitrariamente ai simboli in avanti possono determinare la scelta opportuna da fare nel processo di analisi. Per un approfondimento su tali tecniche si faccia riferimento a https://javacc.dev.java.net/doc/lookahead.html 2.3 Una grammatica per un compilatore JJTree Grammar Specification (*.jjt) JJTree Compiler JavaCC Grammar Specification 45 JavaCC Compiler (*.jj) Java Code for top−down parser with AST managing actions (*.java) Figura 2.2: Generazione in due passi di un parser Java con JJTree e JavaCC. creazione dell’albero sintattico. L’output prodotto da JJTree non sarà altro che la grammatica per JavaCC arricchita delle istruzioni Java per la gestione automatica dell’albero sintattico. In Figura 2.2 è rappresentato l’uso congiunto di JJTree e JavaCC per la produzione del codice Java che realizza il parser con la gestione automatica dell’albero sintattico. A questo punto non ci rimane che mostrare come dalla sintassi di Tabella 2.1 sia stata prodotta una grammatica EBNF che potesse essere direttamente codificata nel linguaggio di specifica supportato da JJTree. Abbiamo già detto come, nonostante l’elevato supporto al lookahead fornito da JavaCC, si sia scelto di mantenere la grammatica nella classe LL(1), questo non tanto per motivi di efficienza, che per altro potrebbero essere sempre rilevanti, ma per mantenere il più semplice possibile la definizione stessa della grammatica che, per la ricchezza del linguaggio offerto da JJTree, tende a complicarsi notevolmente. Inoltre ci è sembrato che i vincoli imposti dalla classe LL(1) non appesantissero la sintassi, ma che anzi contribuissero a chiarirne alcuni aspetti, come per esempio la distinzione fra la definizione di processo e le istanze pronte per essere eseguite (Ready-to-Run Instance). Come illustrato i passi da compiere sono quelli di eliminare la ricorsione sinistra e i prefissi comuni nelle diverse parti destre associate al medesimo non terminale. Di fatto tali criticità sono presenti nelle regole EBNF di tipo: a ::= a ; a | a | a | b ( + b )∗ d ::= {s}c | d k d In generale per risolvere il problema si può utilizzare l’approccio usato per le espres- 46 Linguaggi per l’orchestrazione sioni aritmetiche e introdurre i simboli terminali delle parentesi e nuovi simboli non terminali e tramite questi riscrivere le regole nel seguente modo: a ::= sq sq ::= par par ::= pk pk ::= ba ba ::= b d ::= dep ( ( ( | ; par )+ | pk )+ + ba )∗ (a) ( k dep )+ dep ::= {s}c Si può banalmente vedere che tale grammatica è di classe LL(1) e quindi non ambigua9 , infatti essa esplicita strutturalmente la precedenza dell’operatore ; su | e la precedenza di quest’ultimo sull’operatore di +. Il costo che si è dovuto pagare, oltre ad un aumento delle regole e dei simboli non terminali, è la perdita di chiarezza e simmetria nella grammatica che si traduce in una complicazione nella gestione dell’albero sintattico in quanto dovranno essere implementate in maniera specifica le azioni associate a ciascuna regola. Tale approccio è stato utilizzato solo per la categoria sintattica dei deployment d ma non per quella delle attività strutturate a. Per queste ultime si è preferito un approccio che aggiungendo dei nuovi simboli terminali (usati come delimitatori di blocco) permettesse di ottenere un grammatica LL(1) mantenendo una certa simmetria nella definizione e facilitando la gestione dell’albero sintattico. Quello che abbiamo fatto è stato di introdurre per ogni operatore una coppia di simboli terminali che delimitasse la rispettiva attività strutturale, di fatto vengono introdotte regole del seguente tipo: Activity ::= BasicActivity | S equenceActivity | S cope | FlowActivity | PickActivity | ConditionalActivity | IterationActivity ... S equenceActivity ::= "seq" Activity (";" (Activity)? ) ∗ "qes" FlowActivity ::= "flw" Activity ("|" Activity ) + "wlf" PickActivity ::= "pck" ReceiveActivity ("+" ReceiveActivity ) + "kcp" 9 Si può dimostrare che le grammatiche LL(1) non sono ambigue, cioè essere in LL(1) è condizione sufficiente a garantire la non ambiguità di una grammatica. 2.3 Una grammatica per un compilatore Sintassi Blite Sequenza a1 ; a2 . . . ; an Parallelismo a1 | a2 . . . | an Scelta Esterna rcv 1 + rcv 2 . . . + rcv 47 n Sintassi Implementata seq a1 ; a2 . . . ; an qes f lw a1 | a2 . . . | an wl f pck rcv 1 + rcv 2 . . . + rcv n kcp Tabella 2.6: Utilizzo dei delimitatori di blocco per le attività strutturate con operatori binari. In ogni modo la motivazione che ci ha spinto maggiormente a propendere per l’utilizzo dei delimitatori è stata quella di avere una sintassi il più chiara possibile e che non lasciasse a regole implicite la determinazione delle precedenze fra gli operatori. In questo modo il codice stesso espliciterà tutte le relazioni di sequenzialità fra le diverse attività strutturate e l’utilizzatore non sarà obbligato a ricordare le regole di priorità fra i vari costrutti. In Figura 2.3 è rappresentato, nella sintassi da noi implementata, il programma corrispondente al termine seguente: {(rcv h′′ auction′′ i seller hpid, selleri | rcv h′′ auction′′ i buyer hpid, buyeri) ; inv hselleri ok hpid, buyeri ; inv hbuyeri ok hpid, selleri}(pid) A scapito di una maggiore compattezza si è guadagnato in chiarezza. Inoltre le tecnologie attuali offrono un alto grado di assistenza all’editing del codice, per cui sarà facilmente implementabile un editor che possa supportare l’utente nella scrittura dei costrutti previsti dalla nostra grammatica. Un altro aspetto da considerare nella sintassi di Tabella 2.1 è quello legato alla definizione dei termini Services, tramite la regola: (Services) s ::= [r • a f ] | µ ⊢ a | µ ⊢ a , s definition, instance, multiset La parte destra instance ha la funzionalità di rappresentare le istanze in fase di esecuzione con uno stato di memoria (µ ⊢ a), ma anche di dare la possibilità di definire, e quindi di rendere immediatamente disponibili nei deployment, istanze di processo che sono già pronte per andare in esecuzione, senza dover ricevere messaggi di attivazione. Tali definizioni sono qui identificate con l’espressione Ready-to-Run Instance. 48 Linguaggi per l’orchestrazione { [ seq flw rcv <"auction"> seller(pid, seller) | rcv <"auction"> buyer(pid, buyer) wlf; inv <seller> ok(pid, buyer); inv <buyer> ok(pid, seller) qes ] } (pid) Figura 2.3: Esempio di programma Blite con la sintassi dei delimitatori di blocco per le attività strutturate. Nel linguaggio da noi implementato si è voluto mantenere quest’ultima possibilità in quanto ci sembra molto utile, nell’ottica di voler fornire uno strumento per la prototipizzazione, avere un meccanismo integrato nel formalismo, che possa permettere di avviare i processi10 . I tal senso si vuole dare la possibilità all’utente di poter definire dei deployment del tipo: {a1 , . . . , an , [r • a f ]}c in cui è presente la definizione di processo [r • a f ] e n ready-to-run instance. Per non appesantire ulteriormente la sintassi, il linguaggio da noi implementato non prevede un formalismo specifico per la definizione della funzione di stato µ nelle ready to run instance, anche perché lo stato iniziale di tali istanze può essere definito semplicemente con attività di assegnamento. Di fatto quindi la distinzione fra le ready-to-run instance e la definizione di processo sta semplicemente nel fatto che quest’ultima deve obbligatoriamente avere la struttura [r • a f ], mentre le prime possono essere generiche attività strutturate. Nel caso quindi queste siano dei contesti, devono sempre esplicitare il compensation handler ([r • a f ⋆ ac ]) per essere distinguibili dalle definizioni. 10 Si fa osservare che in BPEL tale possibilità non è presente. Il formalismo di BPEL prevede solamente la definizione di processi che interagiscono con il mondo esterno esclusivamente attraverso le porte dei servizi web. L’unico modo di avviare le istanze di processo è quello di inviare un messaggio ad una start activity tramite l’uso di una tecnologia alternativa. Per esempio un’applicazione Java può fare un invocazione ad una porta di una start activity e istanziare il processo BPEL 2.3 Una grammatica per un compilatore 49 Si capisce come limitandosi a tale sintassi non sia possibile prescindere dall’eseguire lookahead sintattico per distinguere le definizioni dalle ready-to-run instance, in quanto in generale tali termini possono avere prefissi comuni del tipo ′′ [a • a′′f . Anche in questo caso si è preferito introdurre un nuovo terminale che preposto alle ready-to-run instance le rendesse immediatamente riconoscibili, ottenendo quindi una grammatica LL(1) e aumentando la leggibilità dei programmi. In pratica si è introdotto il simbolo :: e nella grammatica si sono definite le seguenti regole per la definizione della categoria sintattica Service: S ervice ::= S erviceDe f | (S erviceInstance ("," S ervice)? ) S erviceInstance ::= "::" Activity S erviceDe f ::= "[" S tartActivity ("fh:" Activity)? "]" Usando un prefisso per distinguere le ready-to-run instance dalle definizioni di processo, cade l’obbligatorietà di avere sempre un compensation handler nelle scope activity generiche, per cui queste ultime potranno essere definite tramite la seguente regola in cui risultano opzionali sia la definizione del compensation che del fault handler: S cope ::= "[" Activity ("fh:" Activity )? ("ch:" Activity )? "]" I simboli tipografici • e ⋆ sono stati sostituiti rispettivamente dalle scritture fh: e ch: e si assume che un contesto che non definisca gli handler, implicitamente preveda l’attività throw come fault handler e l’attività empty come compensation handler. In pratica si assume che: [a] = [a • throw ⋆ empty] In Figura 2.4 è riportato un frammento di codice Blite in cui è definito un deployment con una definizione di processo e due istanze pronte per essere eseguite. Con i simboli terminali introdotti tutte le attività strutturate risultano avere un prefisso caratterizzante per cui, nelle regole di Tabella 2.1 che definiscono l’attività condizionata e l’iterazione: (Structured activities) a ::= . . . if(e){a1 }{a2 } | while(e) {a} . . . possono essere eliminati i delimitatori di blocco {·} e definire le seguenti regole: ConditionalActivity ::= "if" "(" Expression ")" Activity Activity IterationActivity ::= "while" "(" Expression ")" Activity 50 Linguaggi per l’orchestrazione { :: seq inv <"s1"> start("john"); inv <"s1"> corre("john"); qes, :: seq inv <"s1"> start("bill"); inv <"s1"> corre("bill"); qes, [ seq rcv <"s1"> start(y); x := y; rcv <"s1"> corre(x) qes ] } (x) Figura 2.4: Esempio di programma Blite con un deployment in cui è presente una definizione di processo e due istanze ready-to-run. Si deve osservare che dopo la scelta interna devono essere sempre esplicitate (eventualmente con empty) entrambe le attività che realizzano le alternative. In appendice è riportato il lessico e in notazione EBNF la grammatica completa che definisce il linguaggio da noi implementato. In essa sono specificati i tipi supportati (attualmente stringhe, numeri -interi e decimali- e booleani) e la sintassi delle espressioni logiche e aritmetiche. 2.4 Alcune osservazioni sulla semantica della correlazione La semantica presentata in Sezione 2.2 descrive il comportamento dei programmi Blite in modo molto elegante e sintetico. L’obiettivo del lavoro di tesi è stato quello di creare un motore capace di eseguire i programmi producendo un comportamento equivalente a quello specificato, nel rispetto di specifiche tecniche che tenessero conto il più possibile di problematiche di efficienza, scalabilità e requisiti di memoria. 2.4 Alcune osservazioni sulla semantica della correlazione 51 Come vedremo nel capitolo seguente alla base dell’architettura software realizzata, ci sono due componenti fondamentali: • Un oggetto software chiamato ProcessManager, che, data una precisa definizione di processo, ha la responsabilità di creare le istanze e di metterle in esecuzione. Tale oggetto ha inoltre la responsabilità di gestire i messaggi indirizzati alle porte della propria definizione, memorizzandoli in opportune strutture dati. • Un insieme di thread, ThreadPool, che eseguono concorrentemente le varie istanze di processo e i flussi paralleli che si generano in esse. Gli aspetti più critici nella realizzazione del comportamento specificato sono quelli legati all’attribuzione dei messaggi alle diverse istanze e alla decisione se l’arrivo di un messaggio produca o meno la creazione di una nuova istanza (regole (com) e (new) di Tabella 2.4). In riferimento alla prima problematica, la semantica definisce un ordinamento fra tutte le istanze che contemporaneamente attivano una ricezione su una determinata porta11 e, in base a tale ordinamento, stabilisce l’attribuzione del messaggio. Di fatto in un’architettura che usa una tecnologia di multithreading per realizzare la non sequenzialità specificata dalla semantica di Blite, non ha molto senso parlare di attività di ricezione contemporaneamente attive su una porta. In pratica, quando il flusso di esecuzione di un’istanza arriva ad eseguire una ricezione su una determinata porta, si deve poter decidere se un messaggio è destinabile a tale istanza solamente in base allo stato attuale dell’istanza stessa. Semplificando, per valutare se un messaggio può essere attribuito ad un’istanza che esegue l’operazione rcv hpi o x̄, si può attuare la seguente procedura: m := null for each v in { messages on <p,o>} if corr(x, v) m := v break iteration if m is null start waiting on <p,o> else 11 Con il termine porta si intende la coppia hservice-name, operation-namei, essa identifica univocamente una funzionalità nell’interfaccia associata ad una definizione di processo. 52 Linguaggi per l’orchestrazione consume m continue execution in cui “messages on <p,o>” identifica tutti i messaggi ricevuti sulla porta <p,o> e la funzione corr(x, v), che restituisce un valore booleano, valuta se il messaggio v può essere correlato all’istanza in base ai parametri attuali x, allo stato di memoria dell’istanza e al correlation set definito dal programmatore. In pratica, tale funzione può essere ricavata, in riferimento alla funzione match(c, µ, x̄, v̄) definita in Sezione 2.2, nel seguente modo: f alse se x ∈ c ∧ x ∈ dom(µ) ∈ c ∧ v , µ(x) corr(c, µ, x, v) = true altrimenti corr(c, µ, hi, hi) = true corr(c, µ, x1 , v1 ) = b′ corr(c, µ, x̄2 , v̄2 ) = b′′ corr(c, µ, (x1 , x̄2 ), (v1 , v̄2 )) = b′ ∧ b′′ dove µ rappresenta lo stato di memoria associato all’istanza e c il correlation set associato alla definizione di processo da cui l’istanza è ricavata. Questa strategia risolve il problema della correlazione dei messaggi con le istanze di processo, producendo un comportamento equivalente a quello specificato dalla semantica formale di Blite. Si fa notare infatti che la semantica non risolve più conflitti rispetto all’implementazione realizzata, in quanto la regola (pass) porta ad attivare in modo non deterministico le attività di ricezione delle diverse istanze. I concetti di grado di definizione e di ordinamento delle istanze rispetto alla possibilità di consumare messaggi in arrivo, introdotti dalla semantica di Blite, risultano invece determinanti per discriminare quando si deve creare una nuova istanza, e in particolare per realizzare le multiple start activity. Le multiple start activity sono attività di inizio istanza del tipo: rcv ℓ1r o1 x¯1 ; a1 | rcv ℓ2r o2 x¯2 ; a2 in cui la start activity è costituita dalla composizione parallela di più ricezioni. La specifica BPEL afferma, anche se in maniera non molto esplicita12 , che nel caso delle multiple start activity, la correlazione di un messaggio ad un’istanza abbia precedenza sulla creazione di nuove istanze. 12 Per esempio, nella specifica di BEPEL4WS 1.1 [3] il comportamento associato alle multiple start activity era addirittura descritto semplicemente con un esempio (sezione 1.6). 2.4 Alcune osservazioni sulla semantica della correlazione 53 Questo comportamento è espresso alla perfezione dalla semantica di Blite, che determina in maniera univoca il comportamento del seguente termine: {[(rcv hp1 i o hxi | rcv hp2 i o hx, zi) ; a] }{x} k {{y 7→ v} ⊢ inv hp1 i o hyi} k {{y1 7→ v, y2 7→ v′ } ⊢ inv hp2 i o hy1 , y2 i} _ ... . . . ≻− {[(rcv hp1 i o hxi | rcv hp2 i o hx, zi) ; a] , {x 7→ v} ⊢ [rcv hp2 i o hx, zi) ; a]}{x} k {{y1 7→ v, y2 7→ v′ } ⊢ inv hp2 i o hy1 , y2 i} . . . ≻− _ ... {[(rcv hp1 i o hxi | rcv hp2 i o hx, zi) ; a] , {x 7→ v, z 7→ v′ } ⊢ [a]}{x} in cui dalla definizione viene creata un’unica istanza di processo. Nella implementazione realizzata il ProcessManager, nel momento in cui arriva un nuovo messaggio, deve poter decidere se creare una nuova istanza o meno in base alle informazioni di cui dispone in quel momento. Si potrebbe pensare di risolvere il problema a livello sintattico rendendo distinguibili le porte di ricezione su cui vengono create le istanze (create port). In pratica si potrebbe dire che un messaggio su una porta utilizzata in una start activity conduce sempre alla creazione di una nuova istanza, lasciando al programmatore la responsabilità di non riutilizzare tali porte per effettuare la correlazione (in quanto tale correlazione non si realizzerebbe mai). È ovvio che questa tecnica non permetta di realizzare le multiple start activity, in cui una porta è contemporaneamente di creazione e di possibile correlazione, e in generale produca comportamenti non conformi alla semantica specificata. Invece, per ottenere tali comportamenti, compreso il caso delle multiple start activity, si deve fare in modo che il ProcessManager, all’arrivo di un messaggio ≪ p : o : v̄ ≫, attui la seguente procedura: On a new message v: put v in { messages on <p,o> } for each ’rcv <p> o x’ waiting on <p,o> if cor(x, v) goto continue 54 Linguaggi per l’orchestrazione if <p,o> is a create port create a new instance i start i wait until all start rcv of i are activated continue: manage next message In pratica viene creata una nuova istanza solo se il messaggio è indirizzato a una porta di creazione e non c’è alcuna attività di ricezione (fra tutte le varie istanze) in attesa su tale porta in grado correlare col messaggio. Inoltre si fa notare come la procedura di gestione dei nuovi messaggi, dopo aver creato una nuova istanza e averla messa in esecuzione, attenda che tutte le sue start activity si siano attivate, cioè si siano registrate in attesa sulle rispettive porte o abbiano consumato qualche messaggio. Solo dopo questo la procedura tornerà a gestire altri messaggi in arrivo. Cosı̀ si realizza l’atomicità descritta dalla regola (new), in cui in un solo passo si crea una nuova istanza e si attivano le attività di ricezione. In questo modo si implementa la semantica delle multiple start activity secondo cui la correlazione ha priorità sulla creazione di istanze. Queste osservazioni risulteranno più chiare dopo aver letto il Capitolo 4 in cui verrà presentata l’architettura e il modello di esecuzione dell’Engine. In questa fase si è illustrato come il processo di interpretazione della semantica risulti particolarmente delicato e come, in presenza di un’architettura multithreading, siano necessari algoritmi e strutture dati non banalmente deducibili dalla semantica stessa. Inoltre è chiaro come risultino indispensabili meccanismi di sincronizzazione fra i vari thread eseguiti concorrentemente. Capitolo 3 Blite-se Come già precedentemente detto il software realizzato può essere distinto in due progetti a se stanti: Blite-se e Blide. In questo capitolo si descrive il primo; sono presentati ad alto livello i diversi moduli con le loro funzionalità e in particolare per l’Engine, il modulo predisposto all’esecuzione dei programmi Blite, si descrive l’architettura in maniera dettagliata. Di questa si cerca di mettere in luce il modello di esecuzione ideato, basato sul pattern Composite [29], secondo cui ogni attività è rappresentabile come un componente che apporta il proprio contributo all’esecuzione dell’istanza di processo. 3.1 Progetto di un motore per l’orchestrazione Blite-se (Blite Service Engine) è il progetto principale e implementa il linguaggio presentato nel capitolo precedente. Realizzare un linguaggio per l’orchestrazione di servizi è un’attività complessa, che spazia in molteplici aree dello sviluppo software. Per questo motivo è fondamentale organizzare il progetto in moduli distinti che realizzino le diverse funzionalità in maniera indipendente e che interagiscano fra loro con interfacce ben definite, cercando di ridurre il più possibile le dipendenze dalle varie implementazioni. Abbiamo individuato tre moduli principali con cui realizzare le funzionalità necessarie: 1. Blite-se Definition Model: questo modulo realizza tutti quegli aspetti legati alla definizione statica dei programmi Blite. In pratica qui è definita la grammatica EBNF, secondo il formalismo di JJTree, da cui è stato ricavato l’analizzatore sintattico. In questo modulo è realizzato anche il modello statico per i costrutti del linguaggio. Di fatto sono state implementate in maniera opportuna le classi i cui 56 Blite-se Blite-se Defnition Model Blite Static Def. Model Blite Parser Blite-se Engine Bilte Dynamic Model Factory Blite Runtime Dynamic Model Blite Execution Model Blite Execution Machine Blite-se Enviroment EngineChannel Implementations ... Blite-se Jbi Env. Blite-se IMC Env. Blite-se Local Env. Figura 3.1: I moduli software del progetto Blite-se e le loro dipendenze. oggetti andranno a creare i nodi dell’albero sintattico. Tali classi sono tutte discendenti della classe BltDefBaseNode che fornisce un’interfaccia comune per accedere ai rispettivi nodi padre e figli, inoltre fornisce il metodo: / ∗ ∗ Accept the v i s i t o r . ∗ ∗ / public Object j jt A c c ep t ( B l i t e P a r s e r V i s i t o r v i s i t o r , Object data ) ; che permette la visita dell’albero secondo il pattern Visitor [29]. Oltre alle varie classi che implementano i nodi dell’albero sintattico ovviamente è reso disponibile il parser tramite la classe BliteParser. Questa dispone di due metodi statici1 : 1 In JavaCC è stata usata l’opzione STATIC = “true”, in questo modo il parser top-down a discesa ricorsiva è generato esclusivamente tramite metodi statici. L’efficienza risulta aumentata ma ovviamente, vi può essere un unico compilatore per virtual machine e successive compilazioni devono prevedere una di fase di reinizializzazione. 3.1 Progetto di un motore per l’orchestrazione 57 s t a t i c public void i n i t ( j a v a . io . InputStream stream ) s t a t i c p u b l i c B l t D e f B a s e N o d e p a r s e ( ) throws P a r s e E x c e p t i o n che hanno le funzionalità rispettivamente di inizializzare il parser su una risorsa di input e di eseguire l’analisi sintattica su di questa. Il valore restituito dal metodo parse() è ovviamente un oggetto conforme al tipo BltDefBaseNode e nel caso specifico l’oggetto istanza della classe BLTDEFCompilationUnit che costituisce la radice dell’albero rappresentante la struttura del programma Blite. Da osservare l’eccezione ParseException eventualmente sollevata dal metodo parse(); con essa è realizzata la gestione degli errori lessicali e sintattici incontrati durante una compilazione. Dai suoi metodi è possibile risalire a tutte le informazioni utili eventualmente per correggere l’errore, come: il token corrente, i token attesi, la riga e la colonna a cui si è arrestata la compilazione. Questo modulo non dipende dagli altri, ma da questi sarà utilizzato. 2. Blite-se Environment: questo modulo ha lo scopo di realizzare l’ambiente contenitore per l’Engine, facendo sı̀ che nella realizzazione di quest’ultimo si possa astrarre dagli aspetti più tecnologici come le problematiche di comunicazione o di deployment. Di fatto si vuole che il motore di esecuzione possa essere utilizzato in scenari diversi e questo modulo realizza le astrazioni necessarie per rendere indipendente l’engine dalle diverse tecnologie di comunicazione. Per esempio una possibilità molto interessante potrebbe essere quella di integrare l’engine Blite con un ESB (Enterprise Service Bus) basato sul protocollo JBI, in questo caso basterebbe realizzare un sottomodulo con le funzionalità specifiche per dialogare con il bus. Un’altra ancora potrebbe essere quella di realizzare un environment capace di supportare direttamente gli standard tipici della tecnologia Web Services, come WSDL, SOAP e HTTP, e poter quindi far dialogare i nostri programmi Blite con ogni servizio di Internet. Attualmente è stato implementato un environment (Local Environment) capace di eseguire localmente più Engine e simulare la rete e la comunicazione remota. Tale environment è stato creato per realizzare i test di verifica dell’implementazione dell’Engine2 , e per allestire l’ambiente di esecuzione di Blide, strumento nato per fare prototipi e simulazioni di processi. A livello di Engine è definita e utilizzata la seguente interfaccia EngineChannel: 2 Nel progetto Local Environment è stata realizzata una Test Suite basata su JUnit in cui vengono verificate le attività e i costrutti del linguaggio con test eseguiti nell’ambiente locale. 58 Blite-se /∗ ∗ ∗ This i n t e r f a c e r e p r e s e n t s the communication channel between ∗ t h e E n g i n e and t h e E n v i r o n m e n t . ∗ ∗ @author p a n k s ∗/ public i n t e r f a c e EngineChannel { /∗ ∗ ∗ T h i s method i n i t i a l i z e s a c o m m u n i c a t i o n e x c h a n g e f r o m ∗ the invoking process to the requested endpoint ∗ ∗ @param s e r v i c e I d ∗ @param o p e r a t i o n ∗ @param m e s s a g e C o n t a i n e r ∗ @param i n s t a n c e t h e P r o c e s s I n s t a n c e i n i t i a t i n g t h e e x c h a n g e . ∗ ∗ @return O b j e c t m e s s a g e E x c h a n g e I d ∗ the i d e n t i f i c a t i o n key f o r communication p r o t o c o l s t a t e . ∗ ∗/ public Object createExchange ( S e r v i c e I d e n t i f i e r serviceId , String operation , ProcessInstance instance ); /∗ ∗ ∗ Send a m e s s a g e c o n t a i n e r i n t o t h e c r e a t e d e x c h a n g e . ∗ This a c r e a t i o n post s t e p in the communication . ∗ ∗ @param m e s s a g e C o n t a i n e r , t h e c o n t e i n e r f o r a p p l i c a t i o n m e s s a g e e ∗ p o s s i b l e metadata . ∗ @param m e s s a g e E x c h a n g e I d t h e i d e n t i f i c a t i o n k e y f o r c o m m u n i c a t i o n ∗ p r o t o c o l exchange . ∗/ public void sendIntoExchange ( Object messageExchangeId , MessageContainer messageContainer ) ; /∗ ∗ ∗ Closes the exchange . ∗ ∗ @param m e s s a g e E x c h a n g e I d ∗/ public void closeExchange ( Object messageExchangeId ) ; } Tramite questa, la parti del motore di esecuzione interessate alla comunicazione, potranno inizializzare, sviluppare e concludere le comunicazioni con l’Environment; quest’ultimo invece utilizzerà l’interfaccia dell’Engine stesso per instradare le richieste, che dall’esterno arriveranno verso le porte dei programmi Blite. Da osservare come tale interfaccia definisca un modello di comunicazione total- 3.1 Progetto di un motore per l’orchestrazione 59 mente generico, che può essere utilizzato per realizzare molteplici protocolli per lo scambio di messaggi, dal più semplice “fire and forget” ai più complessi, che necessitano un mantenimento dello stato. Alla base di questo ci sono l’astrazione MessageContainer, che permette di raggruppare i messaggi applicativi con eventuali metainformazioni e il messageExchangeId, che costituisce un identificativo univoco per la sessione corrente di comunicazione. Le varie tipologie di Environment implementeranno in maniera opportuna tale interfaccia in modo da supportare la tecnologia di comunicazione desiderata. L’enviroment, dopo aver creato le istanze della classe Engine, imposterà in esse l’oggetto che realizza l’opportuna implementazione di EngineChannel. Il modulo dipende da Blite-se Definition Model per svolgere le funzionalità di compilazione e deploy e ovviamente da Blite-se Engine per la realizzazione delle esecuzioni. 3. Blite-se Engine: questo modulo contiene l’implementazione vera e propria del motore di esecuzione per i programmi Blite. Esso potrà essere eseguito all’interno di un opportuno Environment e, nel caso in cui quest’ultimo supporti la comunicazione remota, essere istallato su un nodo di rete per rendere disponibili i processi Blite ai client. Nei paragrafi successivi del capitolo verrà illustrata nel dettaglio l’architettura di questa parte di software. Questo modulo utilizza il Blitese Definition Model per accedere al modello statico rappresentante la definizione dei programmi Blite. In Figura 3.1 è illustrato un diagramma (senza un formalismo specifico) che rappresenta i vari moduli e le loro dipendenze. 60 Blite-se 3.2 Specifica dell’Engine In uno scenario reale, in cui i processi sono distribuiti, avremo un Engine per locazione o nodo di rete, e su ognuno di questi componenti sarà possibile istallare o rimuovere definizioni di processi Blite. In pratica, un engine gestirà un insieme di definizioni, creando a partire da queste istanze di processi e utilizzerà l’Environment per interagire con gli altri Engine. Dall’Environment stesso l’Engine riceverà notifiche riguardo l’accadere di eventi, quali l’arrivo di messaggi indirizzati alle porte delle sue definizioni. Dal punto di vista logico relazionale abbiamo già individuato le macro entità e relazioni rappresentate in Figura 3.2. Each Network Location has one Engine Engine +has +manages ProcessManager 0_* ProcessInstance 0_* +uses 1_1 Blite Definition (AST) ... ... Figura 3.2: Un Engine, ha n definizioni di processo e per ognuna di esse un ProcessManager. Questo gestisce le ProcessInstace istanziandole dalla definizione. Per ogni definizione istallata sull’Engine sarà presente un oggetto della classe ProcessManager che avrà il compito di gestire, nel loro ciclo di vita, le istanze di processo derivate dalla definizione. Prima di entrare nel dettaglio delle scelte architetturali ricapitoliamo quali sono le caratteristiche peculiari di un sistema che deve gestire programmi per l’orchestrazione di servizi, in modo che sia più facile da una parte comprendere e dall’altra giustificare le scelte fatte. A nostro vantaggio abbiamo che: 3.2 Specifica dell’Engine 61 • Un Engine contiene in generale un numero contenuto di definizioni, per cui non ci interessa la scalabilità rispetto alla quantità di definizioni istallate su singolo engine. Tale scalabilità al contrario può essere ottenuta aggiungendo altri engine e installando definizioni su engine diversi. • Una definizione (o programma) Blite avendo principalmente funzionalità di integrazione avrà una lunghezza generalmente limitata. • Poiché le operazioni fondamentali di un programma di questo genere sono invocazioni remote, le durate delle esecuzioni hanno ordini di grandezza minimi dettati dai tempi caratteristici della rete. Per questo motivo non risulta determinante l’efficienza di esecuzione delle operazioni interne di un processo. Il nostro engine non necessiterà di una particolare ottimizzazione rispetto all’efficienza di esecuzione interna. Al contrario, risultano particolarmente critici i seguenti aspetti: • Per ciascuna definizione potrà essere richiesta la creazione di numerose istanze. La scalabilità rispetto al numero delle richieste remote e quindi di istanze di processo risulta essere un prerequisito fondamentale. • Se da un lato abbiamo detto che l’efficienza di esecuzione non è un aspetto particolarmente critico, dall’altro però ogni attività interna necessita di un elevato grado di controllo e tracciabilità. Ogni attività deve potere essere eventualmente terminata o abortita. Poiché in generale ogni istanza potrebbe avere un’immagine persistente o perlomeno essere soggetta ad un’attività di monitoring, l’Engine necessiterà di un grado di controllo a livello di singola attività Blite. Tenendo conto di queste iniziali considerazioni sono state fatte alcune scelte basilari di organizzazione del progetto e l’architettura software è stata basata sulle seguenti specifiche fondamentali: 1. La compilazione di una definizione Blite (che eventualmente in un ambiente distribuito può essere fatta in fase di deploy) produce un modello statico della definizione stessa, che può essere implementato con una struttura ad oggetti. Tale struttura può essere mantenuta in memoria presso l’Engine ed esplorata a runtime per ricavare il flusso e la logica di esecuzione. Sempre in fase di deploy l’Engine può ricavare tutte le informazioni per popolare le strutture dati in cui sono memorizzati i binding fra i nomi delle porte e le definizioni; anche tali strutture dati possono essere mantenute in memoria. 62 Blite-se 2. Le richieste che giungono all’Engine non devono produrre un aumento delle risorse complessive mantenute dall’Engine. Ogni istanza di processo nel suo svolgersi deve, man mano che procede, rilasciare le risorse di memoria acquisite. Anche il numero complessivo dei thread deve essere limitato superiormente (generalmente dell’ordine dell’unità). La realizzazione del parallelismo di attività deve essere attuata tramite il pattern “Resources Pool”: ogni Engine deve disporre di un pool di thread con cui eseguire in parallelo le attività secondo le definizioni Blite. 3. Il modello di esecuzione deve essere Activity Centric. L’engine deve trattare ogni attività secondo un’astrazione generica che possa permettere di fattorizzare i comportamenti comuni e mantenere semplice e pulita l’implementazione della semantica di esecuzione del linguaggio. 3.3 Un modello per le attività A questo punto, dopo aver esposto a grandi linee quelle che devono essere le caratteristiche fondamentali di un Engine, entriamo nel dettaglio del progetto dell’architettura. Nella realizzazione di questa abbiamo scelto di utilizzare il formalismo degli oggetti e delle classi secondo il consueto paradigma “Object Oriented”. Inoltre abbiamo preso come fonte di ispirazione il pattern Composite [29] cercandone una trasposizione nella problematica dell’esecuzione di un programma Blite. In particolare l’astrazione di componente è stata applicata all’entità “attività”. Come i componenti contribuiscono alla realizzazione di un documento o di un’interfaccia utente, le singole attività contribuiscono allo svolgersi dell’esecuzione di un processo Blite. Inoltre la tipica struttura gerarchica presente staticamente negli elementi sintattici di una definizione può essere naturalmente riprodotta a runtime tra i singoli passi di esecuzione, andato a completare l’analogia con le strutture gerarchiche ad albero, tipiche dei tradizionali domini di applicazione del Composite Pattern. L’entità fondamentale del nostro dominio applicativo è stata quindi individuata nella ActivityComponent, trasposizione a runtime dell’elemento sintattico Activity definito dalla grammatica di Blite. Ogni ActivityComponent è rappresentabile tramite interfaccia del Listato 3.1 e descritta in Tabella 3.1. package i t . u n i f i . d s i . b l i t e s e . e n g i n e . r u n t i m e ; import i t . u n i f i . d s i . b l i t e s e . p a r s e r . B l t D e f B a s e N o d e ; /∗ ∗ 3.3 Un modello per le attività 63 ProcessInstance FlowExecutor Node currentActivity ... ActCp ActCp Node ActCp Node ... Node ... Runtime Dynamic Execution Tree Static Definition Tree (AST) Figura 3.3: Il modello statico è utilizzato a runtime per costruire il modello dinamico delle ActivityComponent. ∗ The b a s e u n i t o f r u n t i m e e x e c u t i o n o f a R u n t i m e P r o c e s s I n s t a n c e . ∗ The method < t t > d o A c t i v i t y < / t t > i t t h e k e y o f t h e e x e c u t i o n model . ∗ ∗ @author p a n k s ∗/ public i nt erf a ce ActivityComponent { public boolean d o A c t i v i t y ( ) ; public ActivityComponent getParentComponent ( ) ; public BltDefBaseNode getBltDefNode ( ) ; public ExecutionContext getContext ( ) ; } Listing 3.1: Interfaccia base del modello di escuzione dell’Engine Blite Lo scenario che si va a delineare è quindi quello di due strutture gerarchiche associate: una costituita dall’AST (Abstract Syntax Tree) ricavato dal parsing del codice Blite e che come si detto è mantenuta nella sua interezza; l’altra costituita dall’albero dinamico delle ActivityComponent che realizzano l’esecuzione a runtime (Figura 3.3). Quest’ultima struttura, una per ogni istanza, non è però costruita in un unico momento 64 Blite-se ActivityComponent boolean doActivity() Costituisce il metodo centrale per lo svolgersi dell’esecuzione del programma. L’invocazione di tale metodo su un oggetto attività fa sı̀ che essa possa essere eseguita. Il valore booleano restituito sarà il discriminante del fatto che il flusso di esecuzione corrente dovrà o meno interrompersi. Ogni attività, oltre che eseguire se stessa, sarà quindi anche responsabile nel guidare il flusso nel passo successivo. Utilizzando la gerarchia ad essa nota imposterà la nuova attività corrente da eseguire (l’attività padre o figlio) e restituirà il valore true. Al contrario, potrà interrompere il flusso corrente restituendo false. ActivityComponent Tale metodo restituisce l’elemento padre dell’attività corrente. In questo modo si realizza la struttura gerarchica fra i vari componenti dell’esecuzione. getParentComponent() BltDefBaseNode getBltDefNode() Ogni attività componente dell’esecuzione è strettamente associata ad un elemento sintattico del programma. Con questo metodo ogni oggetto attività restituisce il nodo che la definisce nell’albero sintattico ricavato dal parsing del codice Blite. Tabella 3.1: Metodi principali dell’interfaccia ActivityComponent in fase di inizializzazione del processo, ma al contrario è istanziata man mano che l’esecuzione procede. Come già accennato le attività stesse avranno il compito di creare i loro successori e di metterli in esecuzione. Inoltre gli oggetti attività già eseguiti dovranno essere rilasciati il prima possibile in modo da poter essere collezionati dal Garbage Collector e rilasciare le risorse di memoria. Per ogni tipologia di attività prevista dalla grammatica di Blite esiste una sottoclasse specifica che implementa l’interfaccia ActivityComponent e che realizza in maniera opportuna, in rispetto della semantica, il metodo boolean doActivity(). Per ottimizzare il disegno e fattorizzare il codice comune è stata ovviamente introdotta una classe astratta ActivityComponentBase, da cui ogni altra implementazione di ActivityComponent 3.3 Un modello per le attività 65 eredita le funzionalità comuni di base. Anche la classe ProcessInstance, che modella con i suoi oggetti le varie istanze di processo nell’engine, implementa l’interfaccia ActivityComponent uniformando la struttura gerarchica di esecuzione (anche le attività che avranno come padre oggetti ProcessInstance potranno interagire con questi tramite l’interfaccia ActivityComponent). Le varie istanze di ActivityComponent del tipo specializzato verranno create tramite una classe Factory ActivityComponentFactory, Listato 3.2, che espone il metodo ActivityComponent makeRuntimeActivity(·). In Tabella 3.3 è descritta la segnatura di tale metodo. /∗ ∗ ∗ Factory c l ass to create d i f f e r e n t ActivityComponent implemetation Objects . ∗ @author p a n k s ∗/ public class ActivityComponentFactory { p r i v a t e s t a t i c f i n a l A c t i v i t y C o m p o n e n t F a c t o r y SINGLETON = new A c t i v i t y C o m p o n e n t F a c t o r y ( ) ; private ActivityComponentFactory ( ) { } /∗ ∗ ∗ gets singleton instance ∗ @return A c t i v i t y C o m p o n e n t F a c t o r y ∗/ public s t a t i c ActivityComponentFactory getInstance ( ) { r e t u r n SINGLETON ; } public ActivityComponent makeRuntimeActivity ( BltDefBaseNode bltDefNode , ExecutionContext context , ActivityComponent parentComponent , FlowExecutor executor ) { ... } } Listing 3.2: ActivityComponentFactory, permette di creare oggetti ActivityComponent In Figura 3.4 viene riportato un diagramma di classe abbastanza dettagliato per le entità ActivityComponent. 66 Blite-se Figura 3.4: Diagramma di classe per la gerarchia delle ActivityComponent 3.4 Esecuzione e parallelismo 67 ActivityComponentFactory ActivityComponent makeRuntimeActivity( BltDefBaseNode bltDefNode, ExecutionContext context, ActivityComponent parentComponent, FlowExecutor executor) Permette di ottenere istanze opportune di oggetti ActivityComponent. Il parametro bltDefNode individua l’elemento sintattico che definisce l’attività specifica, in pratica il nodo nell’AST. Il parametro parentComponent è l’attività padre nella gerarchia di esecuzione, mentre gli altri due parametri individuano rispettivamente il contesto di esecuzione e l’esecutore del flusso in cui l’attività verrà creata. Tali entità verranno descritte nelle sezioni successive Tabella 3.2: Factory per creare le opportune sottoclassi che implementano le attività 3.4 Esecuzione e parallelismo Abbiamo visto che i componenti base per l’esecuzione sono oggetti delle varie sottoclassi che implementano l’interfaccia ActivityComponent e che l’esecuzione ha atto tramite l’invocazione del metodo doActivity() su tali oggetti. A questo punto però dobbiamo domandarci da chi e in che modo tale metodo è invocato. Nel rispondere a questa domanda dobbiamo tenere conto che in un Engine verranno eseguite contemporaneamente molteplici istanze di processo e che inoltre il formalismo stesso del linguaggio dà la possibilità di richiedere l’esecuzione concorrente di diverse attività (costrutto flow). Quindi su ciascun Engine dovranno essere disponibili più thread per poter realizzare un livello di parallelismo sufficiente. Si capisce bene che istanziare un nuovo oggetto Thread per ogni nuovo flusso logico presente sull’engine non sia un approccio assolutamente vantaggioso. Come già accennato infatti tale politica non sarebbe per nulla scalabile rispetto al numero delle richieste remote gestite dell’engine; inoltre, poiché ogni istanza di processo tendenzialmente trascorrerà la gran parte del tempo in attesa di comunicazioni remote3 , ci 3 Come già osservato i tempi di comunicazione remota sono di vari ordini di grandezza maggiori rispetto a quelli delle operazioni interne per cui ogni istanza nel suo ciclo di vita si troverà ad occupare 68 Blite-se troveremo con un gran numero di thread in stato di attesa, con uno spreco di risorse del tutto ingiustificabile. Più banalmente gestire direttamente oggetti Thread è una pratica alquanto sconsigliabile 4 , in quanto può portare ad errori di programmazione o a Memory Leaks nel caso in cui il ciclo di vita di tali oggetti non sia sempre ben gestito dal programmatore. Per questi motivi si è scelto di utilizzare la tecnica del Pooling per gestire un insieme di Thread a livello di Engine. Con tale tecnica si isola la gestione della tecnologia di multitasking e si possono applicare politiche anche molto raffinate capaci di adattare la quantità di risorse utilizzate al carico di lavoro da svolgere. Nella implementazione attuale dell’Engine si è fatto uso dei thread pool forniti dalla classe java.util.concurrent.Executors presente nella piattaforma standard Java 5. La scelta che quindi è stata fatta per realizzare il parallelismo è la seguente: ogni flusso logico attivo presente nelle varie istanze di processo sarà associato all’entità FlowExecutor (tale entità è già comparsa in alcuni diagrammi precedentemente illustrati in questo capitolo, per cui il lettore avrà già intuito la sua funzionalità). Tali oggetti presentano l’interfaccia descritta dal Listato 3.3, essa permette da un lato di impostare l’attività corrente che dovrà essere eseguita da uno dei thread del pool, dall’altro di eseguire effettivamente tale attività. package i t . u n i f i . d s i . b l i t e s e . e n g i n e . r u n t i m e ; /∗ ∗ ∗ T h e s e o b j e c t s a r e one t o one r e l a t e d t o a c t i v e d r u n n i n g f l o w on t h e e n g i n e . ∗ I t ’ s p o s s i b l e t o s e t t o c u r r e n t A c t i v i t y and t o e x e c u t e i t w i t h t h e mathod ∗ < t t > e x e c u t e C u r r e n t A c t i v i t y ( ) < / t t >. ∗ ∗ @author p a n k s ∗/ public i n t e r f a c e FlowExecutor { void s e t C u r r e n t A c t i v i t y ( ActivityComponent activityComponent ) ; ActivityComponent g e t C u r r e n t A c t i v i t y ( ) ; FlowOwner getOwner ( ) ; void e x e c u t e C u r r e n t A c t i v i t y ( ) ; } Listing 3.3: I FlowExecutors saranno gli oggetti che realizzeranno i flussi di esecuzione realmente la CPU per tempi quasi trascurabili rispetto ai tempi di attesa di eventi remoti. 4 Questo è ancor più vero dalla versione 5 in poi di Java, in cui sono stati introdotti i pacchetti java.util.concurrent.* che mettono a disposizione un framework ad alto livello per la concorrenza che ottimizza e astrae l’uso delle API a più basso livello. 3.4 Esecuzione e parallelismo 69 parallela all’interno dell’Engine A questo punto, quando ci sarà bisogno di creare un nuovo flusso di esecuzione per l’ActivityComponent act, si dovrà, creare un nuovo FlowExecutor, impostarvi act come attività corrente e renderlo disponibile ad un thread per l’esecuzione. Quest’ultimo passaggio verrà realizzato tramite l’interfaccia dell’Engine che dispone del seguente metodo per notificare gli executor pronti per essere eseguiti e metterli a disposizione del pool di thread: /∗ ∗ ∗ Add a r e a d y t o r u n e x e c u t o r t o q u e u e where t h e w o r k i n g t h r e a d s g e t ∗ the job to execute . ∗ ∗ @param e x e c u t o r ∗/ public void queueFlowExecutor ( FlowExecutor e x e c u t o r ) ; Inoltre, quando un flusso giungerà a conclusione (l’istanza di processo termina o una esecuzione parallela definita in una flow Activity si conclude), si dovrà registrare tale evento e produrre eventualmente altri effetti. Per far sı̀ che si realizzi questa necessità si è introdotto il concetto di FlowOwner, Listato 3.4. Ogni attività che nel suo eseguirsi si troverà a creare nuovi flussi di esecuzione, e quindi oggetti FlowExecutor, dovrà implementare l’interfaccia FlowOwner e impostare se stessa nel FlowExecutor da essa creato. In questo modo quando il flusso logico terminerà, il FlowOwner potrà ricevere notifica di tale accadimento tramite l’invocazione del metodo flowCompleted(). package i t . u n i f i . d s i . b l i t e s e . e n g i n e . r u n t i m e ; /∗ ∗ ∗ T h i s i n t e r f a c e r e p r e s e n t s a owner o f a l o g i c a l p a r a l l e l f l o w e x e c u t i o n ∗ ∗ @author p a n k s ∗/ p u b l i c i n t e r f a c e FlowOwner { void flowCompleted ( ) ; } Listing 3.4: Interfaccia FlowOwner, gli oggetti che creeranno i flussi di esecuzione dovranno implementare tale interfaccia. A questo punto è lecito domandarsi quando si dovrà considerare terminato un flusso di esecuzione. In generale si applica il seguente ragionamento. Le varie ActivityCompo- 70 Blite-se nent, legate in una struttura gerarchica che riflette la definizione statica del programma, termineranno la loro esecuzione mettendo come attività corrente nel loro FlowExecutor la propria attività padre (parentComponent). In accordo con questa osservazione risulta quindi corretto affermare che un flusso di esecuzione può essere considerato terminato dal FlowExecutor quando questo si troverà ad eseguire come attività corrente proprio il suo FlowOwner. In questo caso su di esso il FlowExecutor non dovrà invocare il metodo doActivity(), ma il metodo flowCompleted() e fatto questo, dovrà terminare di eseguire il flusso. Il codice del Listato 3.5 spiega meglio di mille parole la logica alla base di tutto il modello di esecuzione dell’Engine; in poche righe è sintetizzato il cuore dell’architettura. package i t . u n i f i . d s i . b l i t e s e . e n g i n e . r u n t i m e . imp ; ... /∗ ∗ ∗ @author p a n k s ∗/ p u b l i c c l a s s F l o w E x e c u t o r I m p implements F l o w E x e c u t o r { private ActivityComponent c u r r e n t A c t i v i t y ; p r i v a t e FlowOwner flowOwner ; ... /∗ ∗ ∗ T h i s ’ s t h e c o r e o f b l i t e e n g i n e e x e c u t i o n model . ∗ ∗ The c u r r e n t e a c t i v i t y i s e x e c u t e d u n t i l i t ’ s n o t t h e ∗ flowOwner . ∗/ public void e x e c u t e C u r r e n t A c t i v i t y ( ) { w h i l e ( ! ( c u r r e n t A c t i v i t y . e q u a l s ( flowOwner ) ) ) { boolean i s N e w C u r r e n t A c t i v i t y S e t = c u r r e n t A c t i v i t y . d o A c t i v i t y ( ) ; i f (! isNewCurrentActivitySet ) { return ; / / the flow i s suspended } } flowOwner . f l o w C o m p l e t e d ( ) ; / / t h e f l o w has f i n i s h e d } ... } Listing 3.5: Classe che implementa il FlowExecutor. Il metodo executeCurrentActivi- 3.4 Esecuzione e parallelismo 71 ty() è il cuore del modello d’esecuzione realizzato dall’Engine. Esso esegue l’attività corrente fintanto che essa non sia uguale al FlowOwner. In generale possiamo quindi dedurre le seguenti affermazioni che ci possono aiutare a definire alcune proprietà invarianti. • Un FlowExecutor non invocherà mai il metodo doActivity del suo FlowOwner. Viceversa, di questo potrà invocare il metodo flowCompleted(). • Su un oggetto ActivityComponent che implementa anche l’interfaccia FlowOwner, il metodo doActivity() verrà invocato dal FlowExecutor dell’attività padre. • Su di un oggetto ProcessInstance, che è il FlowOwner del flusso principale dell’istanza e l’unica attività senza padre, il metodo doActivity() non verrà mai invocato da nessun FlowExecutor. • Il metodo doActivity() di una ProcessInstance verrà invocato dall’Engine stesso in fase di creazione dell’istanza. Oltre a creare, eseguire e terminare flussi sarà anche necessario sospenderne alcuni già in esecuzione, si veda per esempio il caso dell’attività di ricezione che deve arrestare il flusso corrente in attesa di un evento remoto. In questo caso, l’attività, utilizzando l’interfaccia dell’Engine e in particolare il seguente metodo: . . . <E n g i n e i n t e r f a c e > . . . /∗ ∗ ∗ Put t h e e x e c u t o r i n t h e w a i t i n g queue f o r t h e incoming e v e n t . ∗ ∗ @param e x e c u t o r ∗ @param e v e n t K e y ∗/ p u b l i c v o i d a d d F l o w W a i t i n g E v e n t ( F l o w E x e c u t o r e x e c u t o r , InComingEventKey e v e n t K e y ) ; potrà mettere in attesa il suo FlowExecutor su un evento identificato dalla chiave eventKey, che essa stessa aveva provveduto precedentemente a ricavare5 . Fatto questo, l’attività ritornerà dal proprio metodo doActivity con il valore false, in questo modo il FlowExecutor terminerà l’esecuzione del Flow corrente. Nel momento in cui il 5 Si usa qui il termine ricavare e non generare in maniera voluta. Le chiavi di evento non sono identificate da oggetti in memoria, ma hanno una loro entità indipendente dagli oggetti che possono essere creati per rappresentarle. Per un esempio le chiavi utilizzate per la ricezione saranno costituite dalla coppia hservice-name, operation-namei, tale coppia verrà indicata come portId. 72 Blite-se messaggio sarà recapitato, l’Engine potrà individuare il FlowExecutor tramite la chiave eventKey e rimetterlo in esecuzione. Poiché l’attività corrente di quest’ultimo sarà rimasta l’attività di ricezione, essa potrà riprendere l’esecuzione, consumando il messaggio e permettendo al suo flusso di esecuzione di procedere. Nella sezione successiva verrà illustrato nel dettaglio come si realizza la comunicazione e la notifica degli eventi. In Figura 3.5 è rappresentato un diagramma di classe che raffigura, fra le altre entità, il FlowExecutor e FlowOwner e le principali relazioni che le coinvolgono. 3.5 Comunicazione ed eventi Da quanto abbiamo già esposto risulta chiaro che l’esecuzione di un processo è caratterizzata dallo svolgersi di attività interne e l’accadere di eventi esterni di cui le attività possono essere in attesa. In generale si può considerare che gli eventi sono generati a livello di Environment, e possono essere prodotti dall’arrivo di nuovi messaggi verso le porte locali o dalla notifica degli ack/nok delle invocazioni locali verso porte remote. Attualmente le funzionalità espresse da Blite non individuano eventi diversi da queste due tipologie. Nell’Engine però si è preferito realizzare un meccanismo generico di notifica di eventi che soddisfacesse i requisiti attuali del linguaggio ma che non escludesse eventuali possibilità di estensione verso altre caratteristiche tipiche di BPEL (comunicazione Request-Response, EventActivity, ecc). Per l’implementazione dello schema di comunicazione prescelto sono richieste le seguenti funzionalità: 1. A livello di attività o di ProcessManager stesso sarà necessario ricavare una chiave univoca per uno specifico evento. I tipi di evento e le regole per generare di volta in volta le chiavi verranno esposti di seguito. Per il momento può bastare aver chiaro che una chiave individua in maniera univoca una particolare tipologia di evento e, all’interno di questa, un evento specifico o un insieme di eventi gemelli. 2. In un qualsiasi momento al ProcessManager potranno essere notificati eventi. In tal caso esso dovrà provvedere a ricavarsi la chiave e a memorizzare in una mappa associativa l’evento con la chiave, per poterlo poi rendere disponibile alle attività interessate. Il ProcessManager dovrà anche provvedere a “risvegliare” tutti i flussi di esecuzione che eventualmente si sono messi in attesa di quell’evento specifico. 3. In un qualsiasi momento, e anche più volte nel suo ciclo di vita, un’attività potrà interrogare l’Engine (o meglio il ProcessManager responsabile della sua istanza) 3.5 Comunicazione ed eventi Figura 3.5: Diagramma di classe FlowExecutor, FlowOwner e ThreadPool 73 74 Blite-se chiedendogli se un evento associato ad una particolare chiave sia avvenuto. In caso affermativo l’attività potrà consumare l’evento. 4. Un’attività potrà avere la necessità di mettersi in attesa di un particolare evento non ancora avvenuto; il tal caso dovrà sospendere il suo flusso di esecuzione e notificare questo al ProcessManager con il riferimento alla chiave dell’evento d’interesse. In particolare, si può vedere come queste funzionalità di base possono essere utilizzate nel caso dell’attività ReceiveActivity e come l’attività stessa collabora con il ProcessManager affinché si realizzi la ricezione di messaggi. La ReceiveActivity nel suo metodo doActivity() compierà i seguenti passi: 1. Ricava la chiave d’evento eventKey. In questo caso particolare la chiave sarà di tipo RequestInComingEventKey e la sua unicità sarà costituita dal portId (ovvero la coppia hservice-name, operation-namei) della porta su cui si sta eseguendo l’operazione di ricezione. Ovviamente la ReceiveActivity potrà ricavare il portId dal nodo della definizione sintattica a lei associata. 2. Richiede al ProcessManager il set degli eventi associati all’eventKey. Se non c’è alcun evento associato, mette in attesa il FlowExecutor sull’evento eventKey, e termina restituendo false; al contrario, si procede con il passo successivo. 3. Analizza il set degli eventi (che in questo caso possono essere identificati con tutti i messaggi indirizzati alla porta in questione non ancora consumati) per vedere se ce ne possa essere uno indirizzabile all’istanza della ReceiveActivity in questione. Questo controllo è fatto in base alle regole di correlazione, in pratica vengono implementate la procedura e la funzione corr(x, v) presentate nella Sezione 2.4. Se un tale messaggio non viene identificato, la ReceiveActivity mette in attesa il FlowExecutor sull’evento eventKey e termina restituendo false, al contrario procede con il passo successivo. 4. In questo caso è stato individuato un messaggio inviato alla porta e all’istanza in questione. Tale messaggio viene consumato aggiornando lo stato delle variabili coinvolte nella ricezione, viene impostata la parentComponet come attività corrente del FlowExecutor e il metodo termina restituendo true. 3.5 Comunicazione ed eventi 75 D’altro canto un oggetto ProcessManager nel metodo manageRequest( ServiceIdentifier service, String operation, MessageContainer messageContainer)6 eseguirà le seguenti operazioni, in accordo con la procedura presentata nella Sezione 2.4: 1. Verifica che la terna hservice-name, operation-name, n.parti messaggioi identifichi effettivamente una porta valida per la definizione di processo in questione. Se non è cosı̀, notifica una situazione di errore all’Environment, il quale provvederà ad inviare un NOK al processo invocante. 2. Ricava l’eventKey di tipo RequestInComingEventKey associata alla porta e con questa provvede a memorizzare il messaggio in arrivo in una struttura dati associativa. 3. Se la porta in questione individua una start activity e non vi è nessuna attività di ricezione (fra tutte le varie istanze) in attesa su tale porta che possa correlare con il messaggio, crea una nuova istanza di processo e la mette in esecuzione; alternativamente, se la porta non è associata ad una start activity risveglia tutti i FlowExecutor in attesa di eventi individuati dalla eventKey. 4. Se è stata creata una nuova istanza, il ProcessManager, prima di gestire un nuovo messaggio, attende che tutte le start ReceiveActivity di questa si siano attivate. Già pensando che le due procedure precedenti saranno eseguite concorrentemente, si capisce che un punto particolarmente critico del meccanismo di notifica/consumo di eventi sta nel fatto che l’elevato grado di parallelismo presente possa portare ad una perdita eventi (ovvero mancata consegna di messaggi). Quest’ultima è di fatto un’ipotesi inammissibile e che deve essere assolutamente evitata. Di fatto essa si manifesterebbe allorché le precedenti attività parallele fossero sequenzializzate, per esempio nel seguente modo: • La ReceiveActivity arriva ad eseguire metà del passo 2, cioè ricerca eventi non trovandoli ma non arriva a registrare il FlowExecutor come in attesa dell’evento. • Il controllo passa al ProcessManager a cui viene effettivamente notificato l’evento di interesse dell’attività. Esso però non trovando nessun FlowExecutor in attesa registra l’evento e termina. 6 Bisogna notare che l’invocazione di tale metodo sarà scatenata lato Environment, e che la sua esecuzione avverrà a carico di un Thread allocato a livello stesso di Environment, logicamente del tutto indipendente dai thread del pool dell’Engine. 76 Blite-se • Il controllo passa di nuovo alla ReceiveActivity che registra il suo FlowExecutor come in attesa. Essendo però l’evento già accaduto nessuno a questo punto sarà in grado di notificarlo all’attività, di fatto si ha una mancata consegna del messaggio. Per evitare questi scenari alquanto deprecabili sono attuabili due strategie. Una potrebbe essere quella di dotare l’Engine di una procedura temporizzata che ogni dato intervallo di tempo analizza la mappa degli eventi accaduti e risveglia gli eventuali FlowExecutor registrati in attesa di essi. In questo modo il ProcessManager sarebbe sollevato da quest’ultimo compito e la possibilità di avere la sequenzializzazione sopra descritta non sarebbe più un problema. L’altra strategia potrebbe essere quella di eseguire alcuni passi delle procedure in mutua esclusione, di fatto realizzare due sezioni critiche su un medesimo monitor; la prima, per la ReceiveActivity dovrebbe comprendere i passi 2 e 3, mentre quella per il ProcessManager dovrebbe raggruppare i passi 2 e 4, o nel caso della creazione di una nuova istanza, i passi 2 e 3. Per semplicità di realizzazione, e poiché ad una analisi poco più attenta si capisce che di un certo grado di sincronizzazione non si può fare a meno7 , si è scelto di implementare la seconda strategia. Di fatto si è scelto la possibilità di realizzare una sincronizzazione a livello di definizione di processo. Il ProcessManager espone il seguente metodo: /∗ ∗ ∗ Provides a lock at Process d e f i n i t i o n Level ∗ ∗ @return O b j e c t . T h i s o b j e c t i s u s e d t o g e t a l o c k a t p r o c e s s d e f i n i t i o n l e v e l . ∗/ public Object getDefinitionProcessLevelLock ( ) ; che restituisce un oggetto utilizzabile per sincronizzare un’istanza di processo con il ProcessManager, ma anche le diverse istanze fra di loro. Tale necessità è evidente in quanto ci saranno strutture dati (ad esempio la mappa che mantiene gli eventi/messaggi per le porte) che saranno condivise fra le diverse istanze di una definizione di processo. In un certo modo si può pensare che le diverse istanze siano in competizione (“race”) su tali eventi nel limite di quelle che sono le regole di correlazione stabilite dal linguaggio. Nel momento in cui è necessario accedere alle strutture dati condivise, una sezione critica può essere creata nel seguente modo: M e s s a g e C o n t a i n e r consumedMes = n u l l ; InComingEventKey i c e k = InComingEventKeyFactory . createRequestInComingEventKey ( p o r t I d ) ; 7 Se non altro l’accesso alle strutture dati deve essere sincronizzato fra i vari thread. 3.5 Comunicazione ed eventi 77 s y n c h r o n i z e d ( manager . g e t D e f i n i t i o n P r o c e s s L e v e l L o c k ( ) ) { L i s t < M e s s a g e C o n t a i n e r > mcs = manager . p r o v i d e E v e n t s ( i c e k ) ; f o r ( M e s s a g e C o n t a i n e r mc : mcs ) { i f ( c o r r ( mc , f o r m a l P a r a m s ) ) { consumedMes = mc ; mcs . remove ( mc ) ; break ; } } i f ( consumedMes == n u l l ) manager . g e t E n g i n e ( ) . a d d F l o w W a i t i n g E v e n t ( g e t E x e c u t o r ( ) , i c e k ) ; } Il codice precedente è esattamente l’implementazione dei passi 2 e 3 della procedura, precedentemente descritta, realizzata dalla ReceiveActivity per consumare i messaggi in arrivo. Abbiamo detto che che il ProcessManager, nel momento in cui crea un nuova istanza di processo, deve attendere che tutte le RecieveActivty di questa si siano attivate prima di gestire un nuovo messaggio. Tale attesa viene implementata con la seguente strategia: il metodo manageRequest(·), che è definito synchronized cosı̀ che sia eseguito da un solo thread alla volta, si conclude con il seguente codice: i f ( n e w I n s t a n c e != n u l l ) { synchronized ( newInstance ) { newInstance . a c t i v e t e ( ) ; try { newInstance . wait ( ) ; } c a t c h ( I n t e r r u p t e d E x c e p t i o n ex ) { \ l d o t s } } } in questo modo il processo di gestione dei messaggi in arrivo è bloccato. La nuova istanza newInstance riceverà notifica delle sue start activity dell’avvenuta attivazione e, nel momento in cui tutte saranno attivate, sbloccherà il thread in attesa, e quindi tutto il processo di gestione dei messaggi in arrivo alla definizione di processo, tramite la seguente operazione: synchronized ( t h i s ) { this . notify (); } Tramite questo accorgimento, come osservato in Sezione 2.4, si realizza la semantica 78 Blite-se delle multiple start activity. Lo schema di comunicazione di Blite risulta semplificato rispetto al modello proposto da BPEL. In Blite sono possibili solamente invocazioni asincrone, in cui un’istanza invoca un’operazione remota con degli input senza attendere nessuna risposta; il risultato dell’elaborazione sarà reso disponibile all’invocante tramite una successiva ricezione sfruttando il meccanismo della correlazione. È stato scelto di realizzare tale schema di comunicazione applicativa tramite lo schema più semplice di comunicazione infrastrutturale, ovvero lo schema One-Way (o se si preferisce In-Only secondo la terminologia delle “WSDL 2.0 Extensions” [12]). Tale schema può essere rappresentato nell’ambito della nostra architettura come in Figura 3.6. In pratica un’istanza consumer invocherà tramite una request una porta presso un Engine remoto, provider del servizio richiesto. Questo, dopo aver verificato la disponibilità della porta richiesta e la conformità formale del messaggio in arrivo rispetto alla stessa, invierà un acknowledgment status-done al richiedente, il quale potrà continuare la sua esecuzione. Al contrario, se si sarà verificato un qualche problema, un segnale status-error verrà recapitato al consumer; questo potrà esser fatto dall’Engine remoto ma anche dall’Environment locale stesso qualora si arrivasse allo scadere di un certo timeout senza aver ricevuto alcuno status di risposta (caso quest’ultimo in cui il provider risulta non raggiungibile). Da notare come tale schema di comunicazione realizzi un modello asincrono di cooperazione dei servizi secondo quanto specificato dalla semantica Blite. Il consumo e l’elaborazione del messaggio da parte di un’istanza provider sono eseguiti in maniera del tutto indipendente, in termini di dipendenze temporali, e quindi anche eventuali errori nella elaborazioni della richiesta non verranno comunicate al consumer all’interno di questa comunicazione. Tale modello ci è sembrato il più adatto per poter realizzare i comportamenti seguenti: • Un’istanza di processo può invocare operazioni di altri servizi in maniera asincrona potendo continuare a svolgere altre attività mentre l’elaborazione remota è in atto. • Se un’istanza invoca un servizio che non esiste, o che è momentaneamente indisponibile o con modalità non conformi, l’istanza deve ricevere notifica di tale accadimento. Di fatto questo è sembrato il giusto compromesso fra una comunicazione sincrona 3.5 Comunicazione ed eventi 79 Provider Engine request Consumer Instance Port status-done Provider Instance Consumer Engine Figura 3.6: Il modello di comunicazione One-Way realizzato dai Blite Engine. (che del resto può sempre essere riprodotta) e una comunicazione puramente “fire-andforget” come specificato nella semantica originale di Blite, in cui le istanze procedono in maniera totalmente svincolata senza poter fare nessuna ipotesi sull’esito della comunicazione. Inoltre questo modello di comunicazione è quello che meglio si adatta al binding su protocolli di trasporto tipo HTTP, in cui ad una richiesta c’è sempre una risposta del server che chiude la connessione. In HTTP un’invocazione remota secondo lo schema One-Way può essere implementata come segue: • il client esegue una HTTP GET o HTTP POST • il server risponde con 202 Accepted, nel caso il messaggio possa essere accettato, o con un error code della serie 400 o 500 (es. 400 Bad Request), nel caso in cui ci sia un qualche problema con la richiesta ricevuta. Per concludere questa sezione facciamo vedere come lo schema di comunicazione One-Way può essere facilmente realizzato con il modello ad eventi previsto dalla nostra architettura. Per far questo riportiamo i passi logici eseguiti dal componente InvokeActivity: 1. Si costruisce il messaggio e l’oggetto MessageConteniner, utilizzato per inviare il messaggio stesso nell’EngineChanel (si veda la Sezione 3.1). Si crea un identificativo del Service Provider tramite il seguente metodo messo a disposizione dall’interfaccia del ProcessManager: 80 Blite-se /∗ ∗ ∗ Resolve PartnerLink at rintime . ∗ ∗ @param p a r t n e r s m D e f S t a t i c d e f i n i t i o n o f t h e P a r t n e r L i n k ∗ @param v a r i a b l e S c o p e R u n t i m e v a r i a b l e s c o p e ∗ ∗ @return S e r v i c e I d e n t i f i e r t h e r u n t i m e p a r t n e r l i n k . ∗/ p u b l i c S e r v i c e I d e n t i f i e r r e s o v l e P a r t e r L i n k ( BLTDEFInvPartners p a r t n e r s D e f , VariableScope variableScope ) ; 2. Tramite il seguente metodo: InComingEventKey i n v o k e ( S e r v i c e I d e n t i f i e r s e r v i c e I d , S t r i n g o p e r a t i o n , MessageContainer messageContainer ) ; fornito dall’interfaccia del ProcessManager, si avvia la comunicazione. Si osservi che il metodo restituisce un oggetto eventKey rappresentante come sempre una chiave di tipo InComingEventKey. Nel caso particolare sarà un oggetto della classe StatusInComingEventKey che individuerà l’arrivo dello status associato alla richiesta. Questo tipo di chiavi verranno generate a livello di Environment negli strati di più basso livello e le caratteristiche di unicità dipenderanno fortemente dalla tecnologia di comunicazione utilizzata. Per esempio nell’ambito della tecnologia JBI [24] tali chiavi potranno coincidere con i MessageaExchange, mentre in ambito puramente TCP potranno essere ricavate dagli identificativi di connessione. In ogni modo a livello di Engine è possibile astrarre (e lo si deve fare) completamente dalla loro natura e utilizzarle semplicemente come identificatori univoci di eventi. 3. In sezione critica sul monitor DefinitionProcessLevelLock, si prova a consumare l’evento associato all’eventKey. Se questo è effettivamente già disponibile si valuta lo status di ritorno. Se si è avuto uno status-done si termina l’attività mettendo come attività corrente del FlowExecutor il parentComponet e si restituisce true, se invece si è avuto status-error si avvia la procedura di errore. Se l’evento associato all’eventKey non fosse ancora disponibile si registra il FlowExecutor come in attesa dello stesso e si termina il metodo doAtivity() restituendo false. 4. Quando l’evento atteso sarà disponibile il FlowExecutor verrà risvegliato e l’invokeActivity potrà essere di nuovo messa in esecuzione. Questa, disponendo già di 3.6 Contesto, FaultHandler e Compensazione 81 eventKey, potrà rendersi conto di aver già eseguito l’invocazione e potrà semplicemente consumare l’evento atteso e procedere nell’analisi dello status come descritto sopra. Si osservi che in questo caso non sarà necessario operare in sezione critica. 3.6 Contesto, FaultHandler e Compensazione Il contesto è senz’altro quello uno dei costrutti più caratteristici di BPEL. Tale costrutto è stato riprodotto con le opportune semplificazioni anche in Blite e nella versione qui implementata. Semplificando e rimanendo nell’ambito del linguaggio implementato, un contesto è un raggruppamento logico di attività (di fatto realizzato da quella che verrà indicata con il nome ContextActivity), a cui possono essere associati un FaultHandler e un CompensationHandler8 . I due handler non sono altro che due attività, la FaultHandlerActivity e la CompensationHandlerActivity che hanno funzionalità in un certo senso complementari. • La FaultHandlerActivity, è un’attività che deve essere eseguita nel caso in cui sia sollevato un Fault non gestito durante l’esecuzione della ContestActivity. Di fatto si può pensare che la ContestActivity non sia stata completata a causa del verificarsi di una situazione di errore e che a livello del contesto di definizione della ContestActivity stessa si possa gestire l’errore tramite l’esecuzione della FaultHandlerActivity. Quindi la FaultHandlerActivity entra in gioco nel momento in cui la ContestActivity non completa. • Al contrario, CompensationHandlerActivity entra in gioco solo nel caso in cui ContestActivity completi la propria esecuzione con successo, cioè senza che si siano verificati errori non gestiti. Di fatto la CompensationHandlerActivity può essere vista come una serie di operazioni capaci di annullare ciò che è stato fatto dalla ContestActivity nel suo complesso. Supponiamo di avere un contesto c2, a sua volta contenuto in un contesto c1 (cioè c2 è una sotto attività di a1, ContestActivity di c1), e che c2 definisca la ContestActivity a2 e la CompensationHandlerActivity ch1. Nel caso in cui a2 completi con successo la sua esecuzione, 8 Nel caso in cui il programmatore non codifichi direttamente gli Handler, sono previste due versioni di default. Il DefaultFaultHandler prevede semplicemente la ThrowActivity, in questo modo si lascia la gestione dell’eccezione al contesto di livello superiore, mentre il DefaultCompensationHandler prevede la semplice EmptyActivity. 82 Blite-se ch2 verrà resa nota a c1 che la potrà utilizzare per annullare gli effetti di a2 qualora si verifichi qualche successivo errore che impedisca a a1 di completare con successo. Da quanto detto si capisce che l’esecuzione degli handler di un contesto c è strettamente legata al verificarsi di situazioni di errori: la FaultHandlerActivity è lanciata qualora si verifichi un’eccezione nell’esecuzione della ContestActivity di c stesso, la CompensationHandlerActivity è lanciata qualora completato c si verifichi un errore nell’esecuzione di una qualche attività “sorella” di c 9 . Il verificarsi di un’eccezione, oltre a mettere in esecuzione le eventuali CompensationHandlerActivity e FaultHandlerActivity associate, ha un altro effetto: la terminazione di tutti i flussi “fratelli” o discendenti dei fratelli del flusso in cui si genera il fault. Per capire gli effetti prodotti dal verificarsi di un’eccezione si faccia riferimento alla Figura 3.7 dove è rappresentato il seguente scenario a runtime: • Il contesto base C0 definisce il FaultHandler fh0. • L’esecuzione della ContestActivity di C0 produce tre flussi di esecuzione parallela f1, f2 e f3. • Nel flusso f1 viene eseguito in contesto C1 che termina, installando in C0 il CompensationHandler ch1. • Nel flusso f2 vine creato un nuovo contesto C2. FlowActivity avvia altri due flussi paralleli f4 e f5. All’interno del quale un • Nell’esecuzione del flusso f3 viene generato un fault che dovrà essere gestito a livello del cotesto C0. • Il fault produce la terminazione dei flussi f1 e f2 e a cascata dei flussi f4 e f5. • Il fault produce la creazione di un contesto protetto in cui saranno eseguite in sequenza la CompensationHandlerActivity ch1 e la FaultHandlerActivity fh0. Come si può vedere dalla Figura 3.7 la notifica e gli effetti di un fault si propagano nelle due direzioni opposte nella gerarchia a runtime delle attività. Dall’attività che ha 9 Nell’ambito della sintassi e semantica di Blite l’espressione “attività sorella” ha il seguente significato: “attività che è eseguita nel medesimo contesto immediatamente contenente il contesto c”. 3.6 Contesto, FaultHandler e Compensazione 83 C0: ch1; fh0 f1 f2 f3 C2 C1 f4 f5 FAULT!! ch1 terminate terminate terminate Figura 3.7: Un’eccezione sollevata in un flusso di esecuzione produce la terminazione di tutti i flussi paralleli e la conseguente messa in esecuzione di Compensation Handler istallati e FaultHandler definiti nel Contesto padre dei flussi. scatenato il fault, risalendo i padri, raggiunge il primo contesto e da questo riscende verso le attività figlie per interrompere i vari flussi paralleli. Questo andamento di salita e discesa dell’informazione, legata al verificarsi di un’eccezione, è la caratteristica peculiare della semantica di BPEL e quindi di Blite e, sulla base di questa caratteristica, sono stati disegnati le componenti e le interfacce software dell’Engine predisposte alla gestione dei contesti e delle eccezioni. L’entità principale designata per la realizzazione dei meccanismi esposti è stata individuata nel ExecutionContext, la cui interfaccia, citata più volte nelle sezioni precedenti, è presentata nel Listato 3.6. In particolare si è visto che un oggetto conforme a tale interfaccia era presente nella segnatura del metodo makeRuntimeActivity() della classe ActivityComponentFactory. Difatti ogni oggetto ActivityComponent nascerà all’interno di un contesto di esecuzione ExecutionContext. Le classi concrete che implementano l’interfaccia ExecutionContext sono tre, come si vede dalla Figura 3.8: la ProcessInstanceImp, la ScopeActivity e la ProtectedScope. Ogni istanza di processo infatti costituirà il contesto base per un’esecuzione e all’interno di esso le varie attività ScopeActivity andranno a realizzare dei sottocontesti. Un discorso a parte invece merita il tipo ProtectedScope, che permette di realizzare speciali contesti, “protetti”, in cui. come si è visto, dovranno essere eseguiti i Fault e Compensation Handler. Ancora una volta 84 Blite-se Figura 3.8: ABaseContext e le sue sottoclassi forniscono diverse implementazioni dell’interfaccia ExecutionContext per fattorizzare i comportamenti comuni si è definita una classe astratta ABaseContext in cui è codificata gran parte della specifica dell’interfaccia ExecutionContext. In pratica le varie attività che implementano l’Interfaccia ExecutionContext realizzano una sotto-gerarchia all’interno della gerarchia principale delle ActivityComponent. Infatti, a parte la ProcessInstance che costituisce la radice dell’albero, ogni ExecutionContext avrà un parentContext ed eventualmente un insieme di contesti figli. package i t . u n i f i . d s i . b l i t e s e . e n g i n e . r u n t i m e ; ... /∗ ∗ ∗ T h i s c l a s s r e p r e s e n t s an a c t i v i t y e x e c u t i o n c o n t e x t . 3.6 Contesto, FaultHandler e Compensazione 85 ∗ ∗ T h i s c o u l d be t h e P r o c e s s I n s t a n c e i t s e l f b u t a l s o o t h e r ∗ s t r u c t u r e d a c t i v i t i e s l i k e scope a c t i v i t y . ∗ ∗ At runtime , E x e c u t i o n C o n t e x t s are i n a t r e e ∗ ∗ @author p a n k s ∗/ public i n t e r f a c e ExecutionContext extends VariableScope { ProcessInstance getProcessInstance ( ) ; boolean m a t c h C o r r e l a t i o n ( S t r i n g v a r i a b l e , Object value ) ; ExecutionContext getParentContext ( ) ; void r e g i s t e r I n n e r C o n t e x t ( ExecutionContext c h i l d ) ; void n o t i f y F a u l t ( F a u l t f a u l t ) ; public boolean i s I n A F a u l t e d B r a n c h ( ) ; void r e g i s t e r F l o w ( FlowExecutor flow ) ; void resumeWaitingFlows ( ) ; public ContextState g e t S t a t e ( ) ; public void s e t S a t e ( C o n t e x t S t a t e s t a t e ) ; p u b l i c v o i d a d d C o m p l e t e d S c o p e ( AScope s c o p e ) ; } Listing 3.6: Interfaccia ExecutionContext. In Tabella 3.3 analizziamo l’interfaccia ExecutionContext nei suoi metodi principali, mentre di seguito vediamo come questi possono essere utilizzati per realizzare la semantica specificata. Come abbiamo già osservato si è scelto un modello di esecuzione Activity Centric, nel senso che le varie attività saranno totalmente responsabili nel realizzare la loro esecuzione, ma anche nel collaborare per far sı̀ che i flussi globali dei processi evolvano secondo quanto specificato dalla semantica. Nell’ambito della terminazione questo concetto si esemplifica nel fatto che ogni attività (con la sola esclusione delle attività short-lived) ogni qualvolta venga eseguita, dovrà per prima cosa verificare se si stia trovando nella discendenza di un contesto fallito (questo potrà essere verificato invocando il metodo isInAFaultedBranch() del proprio ExecutionContext). In caso di risposta positiva l’attività dovrà collaborare alla terminazione del proprio flusso di 86 Blite-se esecuzione, cioè dovrà impostare il proprio parentComponent come attività corrente del FlowExecutor e concludere immediatamente il metodo doActivity() restituendo il valore true. In questo modo, con un processo a catena, il flusso corrente raggiungerà il FlowOwner e potrà cosı̀ terminare. Di fatto la ricerca di un fault a ritroso nella gerarchia dei contesti può essere semplicemente fatta implementando il metodo isInAFaultedBranch() secondo il seguente algoritmo ricorsivo10 : 1. Se lo stato corrente del contesto è uguale a FAULTED (cioè al contesto è stato notificato direttamente un fault) si restituisce true. Altrimenti si procede al passo successivo. 2. Se il contesto non ha un contesto padre si restituisce false. Altrimenti si procede al passo successivo. 3. Ricorsivamente si restituisce il valore restituito dal metodo isInAFaultedBranch() invocato sul contesto padre. È anche interessante osservare qual è la procedura alla base dell’implementazione del metodo notifyFault(Fault fault)11 : 1. Si imposta lo stato interno al valore FAULTED. 2. Si invoca sul contesto corrente il metodo resumeWaitingFlows(). Tale metodo ha un comportamento ricorsivo: • Su ogni contesto figlio del contesto corrente si invoca ricorsivamente il metodo resumeWaitingFlows() stesso. • Si risveglia il ogni flusso di esecuzione registrato nel contesto corrente. Tale operazione viene fatta in sezione critica sul consueto semaforo disponibile a livello di definizione di processo: 10 Quella qui presentata è l’implementazione fornita dalla classe ABaseContext ereditata anche per i contesti ProcessInstanceImp e ScopeActivity. Come si vedrà, il contesto ProtectedScope invece, fornisce una riscrittura di questa implementazione per garantire la protezione dell’esecuzione. 11 Anche questa implementazione è fornita dalla classe ABaseContext ed è conforme alla semantica dei contesti ProcessInstanceImp e ScopeActivity. Per il contesto ProtectedScope come si vedrà è necessario una piccola modifica. 3.6 Contesto, FaultHandler e Compensazione 87 s y n c h r o n i z e d ( manager . g e t D e f i n i t i o n P r o c e s s L e v e l L o c k ( ) ) { engine . resumeWaitingFlow ( flow ) ; } Le due implementazioni saranno valide sia per i contesti di tipo ProcessInsatnceImp che per i contesti definiti dalle ScopeActivity. In più, il metodo doActivity() di quest’ultima classe realizzerà un algoritmo del tipo seguente: • Se isInAFaultedBranch() è true: – Se lo stato corrente è FAULTED, cioè il contesto ha ricevuto direttamente notifica di un’eccezione, si devono avviare gli Handler: ∗ Si crea una SequenceActivity con la sequenza dei CompensationHandler istallati e con il FaultHandler definito. ∗ Si crea un ProtectedScope in cui si imposta come ContestActivity la SequenceActivity appena creata. ∗ Si imposta come attività corrente del FlowExecutor il ProtectedScope e si restituisce il valore true. – Se lo stato corrente non è FAULTED, cioè un’eccezione è stata notificata in un qualche contesto padre: ∗ Si imposta lo stato a TERMINATED. ∗ Si crea un ProtectedScope in cui si imposta come ContestActivity la sequenceActivity dei CompesationHandler installati. ∗ Si imposta come attività corrente sul FlowExecutor la parentComponent e si restituisce dal metodo con true . • Altrimenti, cioè se isInAFaultedBranch() è false: – Se lo stato corrente è STARTED, cioè uguale allo stato iniziale all’attività, il contesto deve iniziare l’esecuzione, per cui: ∗ Si imposta lo stato a RUNNING. ∗ Si mette come attività corrente del FlowExecutor la ContestActivity e si ritorna con true. – Se lo stato corrente è RUNNING allora la ContestActivity ha completato correttamente la sua esecuzione: 88 Blite-se ∗ Si imposta lo stato a COMPLETED. ∗ Si registra il CompensationHandler nel contesto padre. ∗ Si imposta come attività corrente sul FlowExecutor la parentComponent e si restituisce dal metodo con true. Concludiamo questa sezione analizzando il contesto ProtectedScope. Come già detto tale contesto è utilizzato per la messa in esecuzione dei CompesationHandler e dei FaultHandler di un contesto oggetto di un fault. La semantica di Blite caratterizza tali contesti, differenziandoli da quelli definiti dalle ScopeActivity, solo per il fatto che questi sono immuni alla terminazione o, se si vuole parlare in termini della semantica Blite, la funzione end(·) agisce come la funzione identità per questi contesti: end( LaM ) = LaM Questa caratterizzazione è implementata nel nostro modello andando a riscrivere il metodo isInAFaultedBranch() nella classe ProtectedScope in modo tale da evitare la chiamata ricorsiva sui contesti padre. Di fatto l’implementazione in questo caso seguirà il seguente semplice schema: • Se lo stato corrente del contesto è uguale a FAULTED (cioè sul contesto è stato notificato direttamente un fault) si restituisce true. • Altrimenti, si restituisce false. In questo modo l’esecuzione che avviene in un ExecutionContext di tipo ProtectedScope è protetta dai fallimenti che possono avvenire in altri flussi paralleli. Un’altra caratteristica della semantica di Blite è che il ProtectedScope protegge l’attività interna dai fallimenti esterni, ma non fa il viceversa, cioè i fallimenti che avvengono all’interno del ProtectedScope si propagano verso il contesto in cui il ProtectedScope è eseguito. Nel momento in cui il ProtectedScope viene creato, in fase di gestione dell’eccezione, il contesto fallito imposta il proprio contesto padre come contesto padre del ProtectedScope stesso; a questo punto la “semi trasparenza” definita dalla semantica di Blite per il ProtectedScope, si realizza nel momento in cui il metodo notifyFault(Fault fault) viene riscritto secondo la seguente logica: • Si imposta lo stato a al valore FAULTED, 3.6 Contesto, FaultHandler e Compensazione 89 • Si invoca lo stesso metodo notifyFault(fault) su parentContext @Override public void n o t i f y F a u l t ( F a u l t f a u l t ) { s e t S a t e ( C o n t e x t S t a t e . FAULTED ) ; getParentContext ( ) . notifyFault ( fault ); } Si può facilmente capire che con questa implementazione il ProtectedScope è del tutto trasparente per le eccezioni che avvengono al suo interno, le quali si propagano direttamente sul contesto padre dove eventualmente possono essere anche gestite. 90 Blite-se ExecutionContext ExecutionContext getParentContext() Restituisce il contesto padre del contesto corrente void registerInnerContext( Aggiunge un contesto figlio al contesto corrente ExecutionContext child) void notifyFault(Fault fault) Con questo metodo è possibile sollevare eccezioni (faults). Le attività che necessiteranno di lanciare eccezioni (per esempio la ThrowActivty) potranno invocare tale metodo sul loro ExecutionContext e terminare il loro metodo doActivity rimettendo in esecuzione il loro parentComponent. L’ExecutionContext potrà aggiornare il proprio stato per riflettere la situazione di errore. Con questo metodo ha inizio la propagazione del fault. boolean isInAFaultedBranch() Questo metodo sarà utilizzato da ogni attività per vedere se nella loro gerarchia di contesti ne sia presente uno su cui sia stata notificata un’eccezione. La gerarchia in questo caso sarà ispezionata dal contesto padre verso i predecessori. In tal caso, le attività dovranno collaborare per attuare la terminazione del proprio flusso, cioè dovranno terminare mettendo in esecuzione il proprio parentComponent. void registerFlow(FlowExecutor flow) Poiché il processo di terminazione deve coinvolgere tutti i flussi, anche quelli che eventualmente sono in attesa di eventi, il contesto, una volta notificato del fault, dovrà risvegliare tutti i flussi creati sotto lui per far sı̀ che questi possano terminare. Ovviamente, per poter far questo, il contesto dovrà conoscere i flussi che sono stati creati sotto di lui. Con tale metodo di fatto si realizza proprio questo, si metterà a conoscenza il contesto sotto cui si sta operando di ogni nuovo flusso creato. void addCompletedScope( Con questo metodo i contesti completati con successo potranno registrare nel proprio contesto padre il loro CompensationHandler per un’eventuale esecuzione futura. AScope scope) Tabella 3.3: Interfaccia ExecutionContext Capitolo 4 Blide Questo capitolo presenta Blide (Blite Integrated Development Environment) uno strumento che racchiude un ambiente di esecuzione locale e fornisce un’interfaccia grafica che permette di svolgere in maniera integrata tutte le operazioni legate allo sviluppo di programmi Blite. In questo capitolo vengono descritte le caratteristiche e le funzionalità dell’interfaccia e successivamente viene presentato un caso d’uso ispirato ad un esempio presentato nella specifica di BPEL 1.1 [2]. 4.1 Un IDE per Blite Per verificare se uno strumento è funzionale la cosa migliore da fare è provarlo. In tal senso per verificare se il linguaggio e il motore di esecuzione realizzati risultino veramente utilizzabili è stato sviluppato un vero e proprio IDE che permetta di scrivere rapidamente e testare, tramite l’esecuzione di simulazioni, i programmi Blite. Questo strumento prevede la possibilità di gestire i file con le definizioni dei processi, editarli, compilarli e metterli in esecuzione. E’ presente anche la funzionalità per visualizzare, tramite una rappresentazione grafica, l’esecuzione delle istanze di processo. Blide è stato realizzato tramite il progetto NetBeans Platform [27], un framework studiato per facilitare lo sviluppo di applicazioni Java con interfaccia grafica. E’ stato scelto tale progetto per la sua completezza e per il modello architetturale offerto. NetBeans Platform permette allo sviluppatore di realizzare le proprie applicazioni componendo diversi moduli, ciascuno dei quali offre una funzionalità specifica. La modularizzazione molto fine e la gestione formale delle dipendenze permettono di costruire applicazioni riuscendo a selezionare in modo molto mirato solamente i moduli realmen- 92 Blide Figura 4.1: Come si presenta Blide al primo avvio. te necessari e quindi a mantenere limitate le dimensioni complessive dell’applicazione. Per una presentazione approfondita di NetBeans Platform si consulti [34]. In Figura 4.1 è presentata l’interfaccia di Blide al primo avvio. Oltre alle consuete barre dei menù e dei pulsanti posizionati in alto, l’applicazione presenta quattro aree distinte: 1. Un pannello identificato dall’etichetta “Favorites” che permette di accedere al filesystem e organizzare i file “preferiti” in raggruppamenti logici. 2. Un pannello identificato con l’etichetta “Engines” in cui è possibile accedere ai motori di esecuzione attualmente disponibili, alle definizioni di processo e alle istanze create da queste. 4.1 Un IDE per Blite 93 File Blite che deve essere compilato. Il file non è mai stato compilato o ha subito modifiche dopo l’ultima compilazione. File Blite che presenta errori di compilazione. compilazione eseguita sul file ha riportato errori. L’ultima File Blite compilato con successo. Delle definizioni presenti nel file sono disponibili i modelli statici. Tabella 4.1: Icone associate ai file Blite con estensione .blt 3. Un’area centrale in cui verrano visualizzati gli editor con i file sorgenti e le rappresentazioni grafiche delle istanze eseguite. 4. Un pannello in basso in cui è possibile visualizzare l’output di sistema, come per esempio l’esito della compilazione dei programmi Blite. Di seguito andiamo ad analizzare più nel dettaglio queste aree e altre funzionalità dell’interfaccia di Blide. Favorites - Gestione dei Sorgenti Blite Abbiamo detto che quest’area permette di accedere al filesystem, in realtà non offre un’unica visione dell’albero come i consueti file manager, ma permette in pratica di visualizzare direttamente più sotto-alberi del filesystem. Di fatto l’utente può selezionare un direcotry e con la funzione “Add to Favorites. . . ” aggiungere tale directory, con tutto il suo sotto albero, come una nuova radice che compare direttamente nel pannello. In questo modo l’utente si può creare una specie di Bookmarks alle directory a cui accede maggiormente e in cui probabilmente ha salvato i file di lavoro. Da notare che tali configurazioni vengono salvate automaticamente dall’applicazione e al successivo riavvio l’utente si troverà mantenute le scorciatoie alle directory preferite. Al primo avvio l’applicazione mostrerà come unica risorsa favorita la directory “home” dell’utente. All’interno degli alberi di Favorites i file con estensione .blt vengono riconosciuti come file sorgenti Blite e vengono visualizzati con icone speciali. Inoltre l’applicazione decora tali icone in base al fatto che i file necessitino compilazione, siano stati compilati 94 Blide Figura 4.2: Una direcory con alcuni sorgenti Blite. con successo o presentino degli errori che ne abbiano compromesso la compilazione. In Figura 4.2 è rappresentato il pannello in cui è stato aggiunto nei favoriti la directory data, questa contiene una serie di file sorgenti Blite che vengono visualizzati con le opportune icone. In Tabella 4.1 sono rappresentate le varie icone associate ai file Blite con il riferimento alla loro semantica. Su ogni nodo rappresentante un file Blite possono essere eseguite diverse azioni. In particolare tali azioni, come è consuetudine nelle applicazioni con interfaccia grafica, possono essere eseguite in modi diversi: dai menù a discesa della “Menu Bar”, che si trova nella parte alta della finestra, dai menù contestuali che si aprono premendo il tasto destro del mouse su un oggetto selezionato e dai bottoni che si trovano nello spazio subito sotto la menù bar detto “Tool Bar”. In Figura 4.3 è rappresentato il menù contestuale che viene visualizzato premendo il tasto destro su un file Blite, che nel caso specifico deve essere ancora compilato, per cui risulta abilitata l’azione “Compile”, 4.1 Un IDE per Blite 95 Compilazione del codice Blite. Istallazione dei Deployment contenuti nel file sugli Engine. Rimozione dei Deployment contenuti nel file dagli Engine. Tabella 4.2: Azioni che possono essere eseguite su un file Blite. mentre le azioni “Re/Deploy” e “Undeploy” sono ovviamente disabilitate, in quanto non è ancora disponibile un modello di definizione da poter istallare. In generale tutta l’interfaccia è stata realizzata secondo il paradigma Context-Sensitive Interface, per cui le sue varie parti sono sensibili allo stato degli oggetti che si trovano a gestire. Oltre che nel menù contestuale che si apre sui file Blite, le operazioni che risultano più frequentemente eseguite dall’utente, “Save”, “Compile”, “Deploy” e “Undeploy”, sono state rese disponibili tramiti pulsanti, facilmente raggiungibili, posizionati nella Tool Bar: Si osservi come anche questa parte dell’interfaccia risulti sensibile al contesto, cioè in questo caso al file attualmente selezionato o visualizzato nell’editor, e come siano attivati solamente i pulsanti, le cui azioni siano attualmente applicabili. In Tabella 4.2 sono ricapitolate le azioni eseguibili su file Blite con le icone presentate nell’interfaccia. 96 Blide Figura 4.3: Facendo click con il tasto destro su un nodo rappresentante un file Blite, si apre un menù contestuale che visualizza le azioni che l’utente può compiere sul file stesso. Da osservare come siano abilitate solo le azioni applicabili, di fatto viene realizzato il paradigma Context-Sensitive Interface, secondo cui ogni componente dell’interfaccia è sensibile allo stato degli oggetti contestuali. Editing e compilazione del codice Blite Facendo doppio click, o usando l’azione contestuale “Open”, su un nodo degli alberi di Favorites rappresentante un file Blite, si apre un pannello di editing nell’opportuna sezione dell’interfaccia grafica. Per creare un nuovo file invece si devono eseguire i seguenti passi: 1. Nel pannello Favorites, selezionare la directory in cui si desidera creare il nuovo file. 2. Premere il tasto destro e visualizzare il menù contestuale. 3. Su di questo posizionarsi sulla voce “New” in modo da visualizzare il sottomenù associato: 4.1 Un IDE per Blite 97 4. A questo punto le possibilità sono due: (a) Creare un file vuoto, selezionando la voce di menù “Empty File”. In questo caso si aprirà una finestra in cui l’utente potrà digitare il nome del nuovo file comprensivo dell’estensione .blt. (b) Utilizzare un file di esempio da cui iniziare a sviluppare il proprio programma Blite scegliendo la voce “All Templates . . . ”. In questo caso tramite un “Wizard” l’utente potrà scegliere fra una serie di file che possono costituire un punto di partenza (Template) per scrivere il proprio codice. Abbiamo mostrato come aprire un sorgente Blite all’interno di un editor. Blide fornisce un editor specializzato per scrivere programmi Blite, che mette a disposizione le funzionalità specifiche di: • Syntax Highlight: Tramite la grammatica formale, presentata nel Capitolo 2, è stato possibile realizzare una tecnologia capace di riconoscere la sintassi Blite ed evidenziare in maniera opportuna le parole riservate e i costrutti specifici del linguaggio. Tale tecnologia si basa sul progetto “Generic Languages Framework (GLF)” [28], che mette a disposizione un modulo per NetBeans Platform con cui è possibile sviluppare il supporto ad un linguaggio di programmazione all’interno delle proprie applicazioni. • Autocompletamento: L’editor dei file Blite assistono l’utente nella scrittura del codice andando a completare automaticamente alcune parti del codice. In particolare l’editor inserisce automaticamente i delimitatori di chiusura dei blocchi. Per esempio, all’utente basterà digitare la stringa seq e il sistema inserirà automaticamente la chiusura inserendo il corrispettivo qes. Inoltre anche le operazioni di invocazione e ricezione sono completate automaticamente, digitando per esempio 98 Blide semplicemente la stringa rcv l’utente si troverà a disposizione l’intera struttura dell’operazione di ricezione come: rcv<"on_me"> operation(x), e in questa potrà modificare le parti necessarie. Abbiamo visto nel Capitolo 2, come la sintassi originale di Blite sia stata arrichita con alcune parole riservate e come tutte le attività strutturate siano state dotate di delimitatori di blocco. Con il supporto dell’editor tale appesantimento sintattico non verrà avvertito dal programmatore. In Figura 4.4 è raffigurato un editor con il sorgente Blite in cui sono evidenziate tramite colori e font specifici le parole riservate e i costrutti del linguaggio. La compilazione del file visualizzato nell’editor attualmente attivo, come detto, può essere eseguita con l’opportuno pulsante nella Tool Bar. All’utente verrà notificato l’esito della compilazione nell’area in basso dell’interfaccia grafica identificata con l’etichetta “Output”. In caso di errore tale area riporterà il messaggio opportuno e l’utente potrà direttamente fare click su di esso per posizionare automaticamente il cursore dell’editor in corrispondenza della linea e della colonna in cui è presente l’errore. In Figura 4.5 è rappresentata questa situazione. Engines - Deploy ed esecuzione dei processi Blide è dotato di un ambiente Local Environment per l’esecuzione dei processi Blite. Come già detto il Local Environment permette di eseguire localmente (in una Java Virtual Machine) più Engine e di gestire la comunicazione fra questi simulando la rete. Quando un file sorgente di Blite è compilato, è abilitata per questo l’azione di “Deploy”; a questo punto se l’utente la esegue i Deployment (si ricorda che nella sintassi i deployment sono individuati dai delimitatori { . . . }) definiti nel file vengono istallati su opportuni Engine. Attualmente, per velocizzare il processo di sviluppo dell’utente, l’associazione Engine-Deployment è fatta in maniera automatica dal tool secondo il seguente schema: • Ogni Deployment ha il proprio Engine di esecuzione. • L’Engine di un Deployment viene identificato nel seguente modo: (“nome file che definisce il deployment” : “numero d’ordine del dep. nel file”) Per cui, se abbiamo che un deployment è il terzo definito nel file source.blt esso verrà istallato in un engine identificato con l’etichetta: source.blt:3. 4.1 Un IDE per Blite Figura 4.4: L’editor di Blide per con il “Syntax Highlight” per il codice Blite. Figura 4.5: Feedback interattivo per gli errori di compilazione. 99 100 Blide Figura 4.6: Rappresentazione del Local Environment con gli Engine, in questi le definizioni di processo istallate. Per ogni definizione sono visualizzate le istanze eseguite o in esecuzione. Nell’interfaccia, il pannello identificato con l’etichetta “Engines” visualizza un albero, la cui radice è contrassegnata come “Local Engines”. I nodi figli della radice rappresentano gli Engine attualmente presenti nel Local Environment identificati con la convenzione sopra esposta; espandendo i loro figli troviamo le definizioni di processo, e per ognuna di essa, a livello inferiore troviamo le istanze. In Figura 4.6 è presentato l’albero dei Local Engines. Come si può osservare fra le definizioni istallate negli Engine, si può distinguere le cosiddette Ready-to-Run Instances, da queste l’utente può avviare direttamente delle istanze tramite l’operazione contestuale “Run”, o tramite l’opportuno pulsante posizionato nella Tool Bar. 4.1 Un IDE per Blite 101 Engine. Definizione di Processo. Ready-to-Run Instance. Istanza in esecuzione. Istanza completata. Instanza terminata con errori . Tabella 4.3: I vari tipi di nodi nell’albero Local Engines. In tabella sono riepilogati (con le rispettive Icone) i vari tipi di nodi che si trovano nei livelli dell’albero dei Local Engines. Monitor - Visualizzazione dell’istanze Abbiamo visto come l’utente possa scrivere i propri programmi Blite, istallarli ed eseguirli all’interno di un Local Environment. L’esecuzione viene avviata facendo partire una definizione di tipo Ready-to-Run Instance, da questa verrà creata immediatamente un’istanza che probabilmente, invocando le porte di start activity di altre definizioni di processo, provocherà la creazione di altre istanze. Per esempio si consideri il seguente caso: 102 Blide ⇒ in cui si hanno due Engine: faulthandler.blt:1 con la definizione Definition 1 e faulthandler.blt:2 con la Ready-to-Run Instance Definition 1. Quest’ultima può essere messa in esecuzione direttamente dall’utente facendo click con il tasto destro e selezionando dal menù contestuale l’azione “Run”. A questo punto come si vede dalla figura a destra vengono generate due istanze; la Ready to Run Instance ha provocato la creazione di un’istanza della definizione di processo istallata su faulthandler.blt:1. Già dalla rappresentazione delle istanze nell’albero Local Engines si ha un’informazione sull’esito della loro esecuzione. Come precedentemente detto le icone riportano graficamente il fatto che un’istanza abbia concluso con esito positivo o meno la propria elaborazione. All’utente viene data inoltre la possibilità di visualizzare il flusso completo di esecuzione di un’istanza. Facendo doppio click sul nodo dell’istanza, si apre, nella zona dedicata agli editor, un pannello che propone una rappresentazione grafica dell’esecuzione della stessa. Tale pannello viene di seguito identificato con il termine Monitor View. Disponendo già di un Monitor View aperto, l’utente vi può aggiungere altre istanze trascinandocele sopra, secondo il paradigma Drag-and-Drop. In questo modo è possibile visualizzare in un’unica rappresentazione grafica l’interazione di più istanze di processo che nel loro ciclo di vita si sono scambiate messaggi. In Figura 4.7 è rappresentato il Monitor View con la rappresentazione dell’esecuzione di due istanze generate dal codice di Figura 4.8. Si noti come la Ready-to-Run Instance tramite la prima invocazione produca la creazione dell’istanza derivante dalla definizione di processo, quest’ultima, con una successiva invocazione, risponde con un messaggio di correlazione. Si osservi inoltre come l’istanza di destra concluda con successo la sua esecuzione, in quanto l’eccezione sollevata è gestita in un Fault Handler (è il Fault Handler stesso che esegue l’invocazione di risposta). Quella di sinistra invece fallisce poiché l’eccezione sollevata non è gestita da alcun Fault Handler e la stessa produce la terminazione del ramo parallelo che era fermo in attesa di ricevere un messaggio. 4.1 Un IDE per Blite 103 Figura 4.7: Rappresentazione grafica dell’esecuzione delle istanze e dalla loro comunicazione. 104 Blide Figura 4.8: Programma Blite associato alla rappresentazione grafica di Figura 4.7. A prima vista potrebbe sorprendere la presenza della seconda Throw (racchiusa dal rettangolo tratteggiato di rosso) nell’istanza fallita, in quanto non sembrerebbe prevista dal codice. In realtà essa non è altro che l’attività Throw prevista dal Default 4.1 Un IDE per Blite 105 Fault Handler (si veda la Sezione 2.3). C’è sembrato che la scelta migliore per rendere la rappresentazione grafica più chiara possibile fosse quella di rappresentare esplicitamente i Default Fault Handler, in quanto condizionano fortemente il flusso di esecuzione, e di nascondere al contrario i Default Compensation Handler (Empty Activity), in quanto del tutto ininfluenti. Concludiamo questa sezione dando una panoramica dei costrutti grafici utilizzati per rappresentare i flussi di esecuzione: Le istanze di processo sono rappresentate tramite scatole (ombreggiate) che contengono le attvità. Le scatole presentano alle estremità due cerchi, di cui quello in basso, tramite la sua colorazione indica lo stato della richiesta: grigio, in esecuzione; verde, completata con successo; rosso, fallita. Le attività di base (Basic Activity), sono rappresentate come dei quadrati con all’interno un’opportuna icona, la cui grafica rimanda alla semantica della attività. L’attività di sequenza è rappresentata con una freccia verticale, che unisce dall’alto verso il basso le attività sequenzializzate. Le attività che cronologicamente sono eseguite prima si trovano più in alto. L’attività di composizione parallela è rappresentata come una barra orizzontale sotto di cui, da sinistra a destra, vengono rappresentati i flussi di esecuzione. 106 Blide I contesti sono rappresentati come rettangoli tratteggiati, al cui interno viene svolta la Context Activity. Se il contesto attiva un Protected Scope, questo viene rappresentato a partire dell’angolo in alto a destra come un rettangolo con il bordo tratteggiato di colore rosso. La scelta esterna è rappresentata tramite la disposizione orizzontale delle attività di ricezione unite dal simbolo +. La valutazione della scelta condizionata è rappresentata dall’icona a sinistra. Nel caso in cui la condizione di test è valutata a false, l’icona ha contorno rosso (come nell’immagine riportata); viceversa, se valuta a true, ha contorno verde. Le operazioni eseguite in un ciclo d’iterazione sono precedute dall’icona rappresentata a sinistra. 4.2 Un esempio d’uso In questa sezione presentiamo un esempio d’uso degli strumenti realizzati. L’esempio considerato è stato introdotto nel documento di specifca BPEL 1.1 [3] (sezione 16.1) e ripreso in [1]. L’esempio, pur risultando essere una semplificazione di uno scenario reale, permette di sperimentare un buon numero delle caratteristiche del linguaggio, come i partner link dinamici, i correlation set, i fault handler e i compensation handler. Si realizza un servizio di spedizioni (Shipping Service) che gestisce gli ordini di clienti. Gli ordini sono composti da un numero di articoli e il servizio mette a disposizione due tipi di spedizioni: una in cui tutti gli articoli dell’ordine sono inviati insieme, 4.2 Un esempio d’uso 107 e l’altra, in cui è possibile spedire in parti gruppi di articoli, in base alla disponibilità degli stessi in un ipotetico magazzino. Il servizio di spedizione da noi realizzato interagisce con altri due servizi di backend, un servizio di gestione del magazzino (Store Service) e un servizio di addebito dei pagamenti (Billing Service). I due servizi sono realizzati con interfacce stateless, la cui implementazione è esclusivamente rivolta a fornire un supporto per un’esecuzione verosimile del processo realizzato dal servizio spedizioni e non vuole essere in nessun modo una soluzione alle problematiche presenti in scenari reali. Il servizio spedizioni è realizzato con la definizione di processo Blite di Figura 4.9. Il Deployment definisce un correlation set con la variabile id che rappresenta un identificativo univoco per gli ordini. Tale identificativo è utilizzato per creare la correlazione dei messaggi che arrivano dai servizi di backend con l’istanza appropriata del processo spedizioni. Anche i processi client del servizio spedizioni utilizzano tale valore per correlare le diverse istanze con i messaggi scambiati. La definizione di processo presenta: rcv<"ship", cust> req(id, c, items) come attività di creazione delle istanze, in cui la ricezione è fatta tramite un partner link bidirezionale, in cui la locazione del richiedente è resa dinamica tramite l’identificativo cust. I valori ricevuti rappresentano rispettivamente: id, l’identificativo dell’ordine (già introdotto e utilizzato per correlare la comunicazione); c, un valore booleano che discrimina fra le due modalità di invio degli articoli; items, un intero che rappresenta il numero degli articoli nell’ordine. Se c ha il valore true il processo prova ad inviare tutti gli articoli in una sola spedizione. Tramite l’invocazione: inv<"bend", "ship"> packall(id, items) richiede al servizio di magazzino l’imballaggio di tutti gli articoli e tramite: rcv<"ship"> packallcb(id, ok) attende la risposta di questo. In questo caso il partner link è pure bidirezionale, ma le due parti sono staticamente note: "bend" identifica il servizio di magazzino e "ship" il servizio spedizione. Il valore di ok discrimina se la spedizione sia possibile o meno. Se questo ha valore true, il processo sottomette il conto e invia la nota di spedizione 108 Blide {[ seq rcv<"ship", cust> req(id, c, items); if (c) //invio completo seq inv<"bend", "ship"> packall(id, items); rcv<"ship"> packallcb(id, ok); if (ok) seq inv<"bill"> bill(id, items); inv<cust> notice(id, items); qes inv<cust> err(id, "sorry") qes //else − invio in parti [ seq shiped := 0; [ //accredito pagamento inv<"bill"> bill(id, items) ch: //compensazione accredito seq noshiped := items − shiped; inv<"bill"> revoke(id, noshiped); qes ]; while (shiped < items) seq inv<"bend", "ship"> pack(id, shiped, items); rcv<"ship"> packcb(id, count); if (count > 0) seq inv<cust> notice(id, count); shiped := shiped + count; qes //else − articoli non disponibili throw; qes qes fh: inv<cust> err(id, "sorry") ] qes ]}(id) Figura 4.9: Il codice Blite che realizza lo Shipping Service. 4.2 Un esempio d’uso 109 tramite le invocazioni: inv<"bill"> bill(id, items); inv<cust> notice(id, items) in cui è utilizzato il valore attuale di cust per identificare il client; altrimenti, se il valore di ok è false, si comunica un errore con: inv<cust> err(id, "sorry") Quando c ha il valore false il processo può inviare gli articoli in maniera differita. Per prima cosa il processo inizializza uno scope (o contesto) in cui viene svolta l’attività principale ed è definito un fault handler per mezzo del quale, in caso di eccezione, si comunica l’errore al client. La politica del servizio è quella di attribuire subito tutto il costo della spedizione, per cui si esegue il sotto scope seguente, in cui vi è l’interazione con il servizio di backend per la gestione dei pagamenti: [ //accredito pagamento inv<"bill"> bill(id, items) ch: //compensazione accredito seq noshiped := items - shiped; inv<"bill"> revoke(id, noshiped); qes ] Se l’attribuzione del pagamento avviene con successo tramite l’invocazione: inv<"bill"> bill(id, items) viene istallato un compensation handler che, tramite la seguente sequenza di azioni, annulla il pagamento degli articoli che non sono stati effettivamente inviati: seq noshiped := items - shiped; inv<"bill"> revoke(id, noshiped); qes 110 Blide Si deve osservare che, nel momento in cui il compensation handler verrà eseguito, la variabile shiped sarà effettivamente valorizzata al numero degli articoli inviati. Questo in accordo con la semantica di Blite per cui lo stato della memoria dell’istanza è unico e condiviso da tutte le attività, comprese quelle eseguite nei vari fault e compesation handler. Terminato quest’ultimo scope il processo inizia un’iterazione, in cui ad ogni passo interroga il servizio di magazzino, richiedendo l’invio degli articoli attualmente disponibili. Le due attività seguenti eseguono in maniera asincrona l’interazione: inv<"bend", "ship"> pack(id, shiped, items); rcv<"ship"> packcb(id, count); La prima invocazione invia i seguenti valori al servizio di backend: id, identificativo dell’ordine, shiped, numero degli articoli attualmente inviati, items, numero complessivo degli articoli. La successiva ricezione, utilizzando ancora il valore dell’id per attuare la correlazione, valorizza nella variabile count il numero degli articoli inviati in questo passo. Se il valore di count è maggiore di zero, il processo invia al client una nota di spedizione con tale valore ed aggiorna la variabile shiped con il numero di tutti gli articoli attualmente inviati. Al contrario, se count ha valore minore o uguale a zero, il processo solleva un’eccezione e l’esecuzione esce forzatamente dal ciclo iterativo. L’eccezione viene gestita nello scope che contiene l’iterazione. In questo, come precedentemente è stato fatto osservare, verrà eseguito il compensation handler del sottoscope completato, revocando il pagamento degli articoli non inviati. Poiché lo scope che gestisce l’eccezione definisce un faulthandler, questo verrà eseguito subito dopo il compensation handler, comunicando cosı̀ al client l’errore. Se le politiche e le disponibilità del magazzino sono tali che, anche in più passi differiti, il valore di shiped diventi uguale a quello di items, l’iterazione termina e il processo si conclude avendo eseguito l’invio di tutti gli ordini. Si deve osservare che il processo di invio articoli termina positivamente anche quando è sollevata l’eccezione, in quanto questa è gestita internamente. In Figura 4.10 è presentato il codice Blite in cui è definita la composizione di due Deployment, ciascuno dei quali definisce una Ready-to-Run-Instance che realizza un processo client per lo Shipping Service. Il primo dei due richiede l’invio completo dell’ordine, il secondo l’invio anche differito. Di seguito tali processi verrano identificati rispettivamente come: All e Dif. 4.2 Un esempio d’uso 111 { // richiesta invio completo − (All) :: seq id := 123; c := true; items := 5; inv<"ship", "cust−all"> req(id, c, items); pck rcv<"cust−all"> notice(id, items); empty; + //tale invio non e’ disponibile rcv<"cust−all"> err(id, err); exit; //si fallisce l’istanza kcp qes } (id) || { // richiesta invio anche in parti − (Dif) :: seq id := 15; c := false; items := 20; inv<"ship", "cust−dif"> req(id, c, items); shiped := 0; while (shiped < items) pck rcv<"cust−dif"> notice(id, count); shiped := shiped + count; + rcv<"cust−dif"> err(id, err); exit; //si fallisce l’istanza kcp qes } (id) Figura 4.10: Il codice Blite che definisce i client del Servizio Spedizioni. Il codice è abbastanza semplice ed intuibile. In entrambi i casi i processi eseguono l’inizializzazione preliminare delle variabili utilizzate, in particolare: alla variabile id è assegnato il valore identificativo dell’ordine, tale variabile è anche utilizzata per realizzare la correlazione, alla variabile c viene assegnato un valore booleano per discriminare la tipologia di invio articoli richiesta, alla variabile items viene assegnato il numero di articoli che compongono l’ordine. Fatto questo entrambi i processi eseguono un’invocazione del servizio di spedizioni, producendone rispettivamente la creazione di un’istanza. L’invocazione comunica an- 112 Blide che la locazione dei servizi client andando a valorizzare dinamicamente il partner link con i rispettivi identificativi. Il client All esegue: inv<"ship", "cust-all"> req(id, c, items) e il client Dif: inv<"ship", "cust-dif"> req(id, c, items) A questo punto i comportamenti dei due client differiscono in base alla tipologia di invio articoli richiesta. In particolare All espone allo Shipping Service semplicemente la scelta fra le due operazioni notice ed err, tramite la seguente Pick Activity: pck rcv<"cust-all"> notice(id, items); empty; + //tale invio non e’ disponibile rcv<"cust-all"> err(id, err); exit; //si fallisce l’istanza kcp Se viene attivata la prima opzione l’istanza di processo termina con successo; mentre nel caso della seconda viene eseguita l’attività exit, che produce la terminazione forzata e il fallimento dell’istanza. L’istanza client Diff invece esegue la Pick Activity all’interno del seguente ciclo while: while (shiped < items) pck rcv<"cust-dif"> notice(id, count); shiped := shiped + count; + rcv<"cust-dif"> err(id, err); exit; //si fallisce l’istanza kcp in cui viene accumulato nella variabile shiped il numero degli articoli attualmente inviati. L’iterazione termina quando tale valore diviene uguale a quello di items e l’i- 4.2 Un esempio d’uso 113 stanza completa positivamente l’esecuzione. Nel caso in cui venga attivata dal servizio spedizioni l’opzione associata all’operazione err, l’attività exit produce l’uscita immediata dall’iterazione e il fallimento dell’istanza. Per completezza in Figura 4.11 e in Figura 4.12 vengono riportati anche i programmi Blite che definiscono rispettivamente lo Store Service e il Billing Service. Facciamo osservare ancora che tale codice ha semplicemente la funzione di definire delle interfacce verosimili utilizzabili dal servizio spedizioni; l’implementazione invece è di volta in volta adeguata in base alle esigenze delle simulazioni realizzate. Di seguito vengono mostrate alcune simulazioni effettuate tramite Blide. In Figura 4.13 è rappresentato lo scenario, in cui un’istanza client richede l’invio di tutti gli articoli allo Shipping Service. Poichè la disponibiltà di magazzino è tale da soddifare la richiesta, il servizio di spedizione risponde invocando l’operazione notice del servizio client. Si attua la correlazione del messaggio con l’istanza e questa termina con successo la propria esecuzione. In Figura 4.14 ad una richiesta di invio dell’ordine completo, il servizio di spedizione risponde invocando l’operazione err, questo produce la terminazione forzata e il fallimento dell’istanza client. In Figura 4.15 è rappresentata la comunicazione fra un client che supporta l’invio differito degli articoli e il servizio di spedizione. Come si può osservare le esecuzioni presentano la ripetizione delle attività contenute all’interno del ciclo while. In particolare il servizio spedizioni interagisce due volte con il magazzino e con il client, facendo sı̀ che quest’ultimo porti a termine positivamente la propria esecuzione. In Figura 4.16 sono illustrati ancora i processi associati al tentativo di spedizione differita. In questo caso però al servizio spedizioni, nel secondo ciclo dell’iterazione, viene comunicata l’indisponibilità degli articoli. Si genera cosı̀ un’eccezione che avvia il Proteced Scope, in cui viene prima compensata l’attività di accredito pagamenti e poi comunicato l’errore al client. Quest’ultimo processo, ricevuto tale messaggio di errore, termina forzatamente la propria esecuzione. 114 Blide { // Store Service [ pck // invio completo dell’ordine rcv<"bend", "ship"> packall(id, items); seq inv<"ship"> packallcb(id, true); qes; + // invio anche differito dell’ordine rcv<"bend", "ship"> pack(id, shiped, items); seq if (shiped > 0) inv<"ship"> packcb(id, 0) seq packed := items / 2; inv<"ship"> packcb(id, packed) qes qes; kcp ] } Figura 4.11: Il codice Blite che definisce il Servizio di gestione del magazzino. { // Billing Service [ pck rcv<"bill"> bill(id, items); empty; + rcv<"bill"> revoke(id, items); empty; kcp ] } Figura 4.12: Il codice Blite che definisce il Servizio di gestione pagamenti. 4.2 Un esempio d’uso 115 Figura 4.13: La spedizione completa dell’ordine è possibile, l’istanza client All termina con successo la propria esecuzione 116 Blide Figura 4.14: La spedizione completa dell’ordine non è possibile, l’istanza client All fallisce. 4.2 Un esempio d’uso Figura 4.15: Spedizione differita degli articoli di un ordine. 117 118 Blide Figura 4.16: Il processo di spedizione differita fallisce, il compensation e il fault handler sono eseguiti in un Proteced Scope (rettangolo tratteggiato di rosso). Capitolo 5 Conclusioni e sviluppi 5.1 Osservazioni conclusive Realizzare un’implementazione del linguaggio ci ha permesso di analizzare approfonditamente la semantica formale definita per Blite e di capire quali aspetti di essa risultassero particolarmente critici in fase di implementazione e, più in generale, ci ha permesso di fare alcune riflessioni su BPEL e sul significato di alcune sue funzionalità. Il lavoro svolto in questa tesi non è da considerarsi esaustivo, ma vorrebbe essere solamente l’inizio di un procedimento iterativo, il cui fine dovrebbe essere quello di ottenere uno strumento funzionale per l’orchestrazione di servizi, con una semantica rigorosa da cui sia possibile ricavare implementazioni coerenti. In particolare abbiamo capito che un linguaggio per l’orchestrazione di servizi, come BPEL, risulta essere uno strumento molto complesso e per questo riuscirne a sintetizzare gli aspetti cruciali in una semantica formale è veramente un’attività delicata. Contemporaneamente, sviluppare un’ implementazione di tale semantica, in rispetto dei requisiti che ci possono essere in sistemi di produzione, non è da meno complicato. Per questo può essere utile che le varie attività trovino sostegno reciproco. Come l’implementazione è guidata dalla semantica, cosı̀ può essere utile che l’esperienza ricavata dal processo di sviluppo ritorni nella fase di specifica formale per apportare eventualmente revisioni e migliorie, e cosı̀ iterativamente fino al raggiungimento di un accettabile grado di funzionalità. Un punto molto critico dell’implementazione della semantica formale è stato quello in cui essa definisce la correlazione dei messaggi con le diverse istanze di processo e 120 Conclusioni e sviluppi risolve il problema delle multiple start activity. In particolare la semantica di Blite specifica tale comportamento in maniera molto brillante e con un formalismo estremamente sintetico ed efficace, ma che mal si presta a guidare lo sviluppo del software. In un’implementazione che usa il multithreading per realizzare il parallelismo, non ha senso parlare di più istanze di processo che eseguono contemporaneamente la ricezione sulla medesima porta, per cui diventa inutile valutare una priorità nell’attribuzione dei messaggi alle diverse istanze. Di fatto un’istanza di processo, nel momento in cui si trova nella condizione di poter consumare un messaggio, può stabilire se questo è ad essa correlato semplicemente valutando la funzione booleana corr(c, µ, x, v) introdotta nella Sezione 2.4. Se dal punto di vista dell’attribuzione dei messaggi alle istanze il problema può considerarsi risolto, rimane il fatto di distinguere se un messaggio in arrivo debba produrre o meno una nuova istanza. Il ProcessManager deve di fatto prendere questa decisione al momento della ricezione in base alle informazioni di cui dispone in quel momento. In generale si potrebbe pensare di risolvere il problema semplicemente a livello sintattico1 , rendendo distinguibili le porte di ricezione su cui vengono create le istanze (create port). È ovvio che questa tecnica non permette di realizzare le multiple start activity, in cui una porta è contemporaneamente di creazione e di possibile correlazione. Per realizzare le multiple start activity è necessario implementare un meccanismo di notifica da parte delle start activity di ricezione nei confronti delle istanze di processo e di quest’ultime nei confronti del ProcessManager. Questo flusso d’informazione e l’utilizzo opportuno dei metodi per la sincronizzazione dei thread (si veda la Sezione 3.5 e in particolare il codice presentato a pagina 76) permette di realizzare il comportamento specificato dalla semantica di Blite, in particolare la correlazione e le multiple start activity. È chiaro che la semantica attuale di Blite specifica alla perfezione il comportamento voluto, ma crea un notevole scarto tra la sfera teorica e quella tecnologica che rende difficile capire quanto l’implementazione realizzi del tutto tale comportamento. Inoltre diversi processi di sviluppo possono attuare strategie di implementazione tra loro completamente diversi, aumentando di fatto la probabilità di difformità. In tal senso speriamo che questa esperienza e queste osservazioni aiutino ad affiancare alla semantica attuale una versione, in cui i formalismi e le strategie di specifica siano più in linea con le necessità di sviluppo che abbiamo incontrato e che le soluzioni ideate possano in qualche modo essere d’ispirazione. 1 La soluzione può sembrare ingenua, ma per esempio è ciò che di fatto fa Oracle Process Manager. 5.2 Sviluppi futuri 5.2 121 Sviluppi futuri Per ciò che concerne lo sviluppo software, le attività da svolgere sono essenzialmente due: • Sviluppare diversi Environment per l’Engine di Blite per realizzare diverse strategie di comunicazione. • Migliorare il formalismo e la tecnologia grafica usata per la rappresentazione delle istanze in Blide. Per quanto riguarda il primo punto, come già più volte detto, le possibilità sono molteplici. Un primo passo molto semplice potrebbe essere quello di creare un Environment che supporti la comunicazione remota fra programmi Blite. In questo caso si potrebbe utilizzare una tecnologia nativa per implementare la comunicazione. Per esempio, utilizzando le socket e la Java Object Serialization si potrebbe di fatto scambiare in rete gli oggetti attualmente utilizzati dal Local Environment e avere quindi la possibilità di distribuire i Deployment Blite su diversi nodi di rete. Ovviamente per poter fare questo bisogna che ai nomi dei servizi sia associato un indirizzo di rete. Questa associazione può essere fatta direttamente nel file di definizione di Blite, in pratica la sintassi stessa delle invoke potrebbe esplicitare l’indirizzo dell’host a cui è diretto il messaggio. inv <"hostserver.dom/serviceName"> operation (params) Alternativamente, per non appesantire troppo la struttura del codice (specialmente nel caso di partner link bidirezionali), potrebbe essere prevista una sezione a parte in cui si va a legare un nome simbolico utilizzato nelle invoke con un indirizzo fisico. Sicuramente molto più interessante sarebbe poter far dialogare i nostri programmi Blite con altri tipi di servizi definiti con linguaggi e tecnologie diverse, primi fra tutti i Web Service. Per far questo la cosa fondamentale è creare un meccanismo che relazioni il codice Blite alle definizioni WSDL. In pratica serve, da una parte ricavare una definizione dell’interfaccia del processo Blite e dall’altra associare le operazioni d’invocazione ad alcune definizioni di servizi preesistenti. Si potrebbe pensare di realizzare un tool blite2WSDL che, preso in input un file .blt, esegua le operazioni necessarie ad impostare un legame fra il programma e le definizioni WSDL. 122 Conclusioni e sviluppi Il processo di creazione dell’interfaccia può essere fatto, per esempio, combinando un’attività utente con un processo automatico. Tramite un’analisi del codice Blite si potrebbe pensare di creare uno scheletro per la definizione WSDL, che l’utente può successivamente raffinare, andando per esempio ad esplicitare: i tipi XSD delle parti dei messaggi, alcuni riferimenti dei namespace o alcuni dettagli del binding WSDL/SOAP. In questo modo non ci sarebbe ovviamente nessun controllo statico che i messaggi ricevuti siano usati, all’interno del codice Blite, in maniera conforme con il tipo esplicitato nel contratto, ma questo concorda con la gestione attuale, in cui a runtime si attua una conversione implicita dei tipi o, se in ultima analisi nessuna conversione è applicabile, si genera un errore. L’associazione delle operazioni di output con le definizioni WSDL dei servizi partner può essere fatta inserendo alcune meta-annotazioni nel codice Blite, associando, per esempio i nomi dei servizi alla coppia hservice:name/port:namei di una definizione WSDL che deve essere disponibile a runtime. Il tool blite2WSDL nella sua esecuzione potrebbe anche fare alcuni controlli sulla conformità della struttura sintattica fra l’operazione Blite di invocazione e la definizione WSDL associata. Una volta che si dispone dei file WSDL e delle associazioni fra questi e le operazioni di comunicazione di Blite, si può pensare di generare un tool (blite2Java) in grado di produrre il codice Java, che a runtime andrà ad eseguire l’effettiva integrazione fra il framework per Web Service utilizzato (ad esempio uno fra JAX-WS [31], Axis2 [32], CXF [33], ecc) e l’interfaccia EngineChannel. Il nostro tool dovrà essere utilizzato in sinergia con il tool del framework scelto che produce il codice Java a partire dalle definizioni WSDL e dovrà attuare un’integrazione di tale codice. Probabilmente in fase di invocazione è anche possibile evitare la generazione statica del codice e utilizzare le API che framework come JAX-WS o WSIF mettono a disposizione per l’invocazione dinamica di Web Service. Un approccio totalmente dinamico in fase di ricezione risulta anche possibile limitandosi ad una versione specifica del protocollo SOAP. Probabilmente risulta più facile realizzare un Environment capace di dialogare con un Enterprise Service Bus basato su JBI [24], in quanto tale standard è stato ideato appositamente per l’integrazione di Service Engine e quindi esistono, oltre ad una serie di API appositamente studiate, una buona documentazione e diversi esempi su come realizzare tale l’integrazione Ad esempio il progetto Open nESB [25] può essere un valido punto di partenza per iniziare a sviluppare componenti software basati sullo 5.2 Sviluppi futuri 123 standard JBI. Per quanto riguarda Blide l’attività potrebbe concentrarsi sull’apportare migliorie al formalismo grafico utilizzato per rappresentare l’esecuzione delle istanze e a potenziarne l’implementazione, nel senso di fornire un maggiore grado di interattività. Si potrebbe dare all’utente la possibilità di spostare le varie istanze nel Monitor View al fine di ottenere una rappresentazione ottimale o di posizionare il mouse sulle diverse attività e ricavare da queste informazioni sullo stato dell’esecuzione. 124 Conclusioni e sviluppi Appendice Grammatica EBNF per Blite CompilationUnit := Deployments <EOF> Deployments := Deployment ( "||" Deployment )* Deployment := "{" Service "}" ( CorrelationSet )? CorrelationSet := "(" <IDENTIFIER> ( "," <IDENTIFIER> )* ")" Service := ServiceDef | ( ServiceInstance ( "," Service )? ) ServiceInstance := "::" Activity ServiceDef := ServiceScope ServiceScope := "[" StartActivity ( <FH> Activity )? "]" Scope := "[" Activity ( <FH> Activity )? ( <CH> Activity )? "]" StartScopeActivity := "[" StartActivity ( "fh:" Activity )? ( "ch:" Activity )? "]" StartActivity := ReceiveActivity | StartSequenceActivity | 126 Conclusioni e sviluppi StartScopeActivity | StartFlowActivity | StartPickActivity Activity := BasicActivity | SequenceActivity | Scope | PickActivity | FlowActivity | ConditionalActivity | IterationActivity BasicActivity := EmptyActivity | ReceiveActivity | InvokeActivity | ExitActivity | AssignActivity | ThrowActivity StartSequenceActivity := SequenceActivity := StartFlowActivity := FlowActivity := "seq" Activity ( ";" ( Activity )? )* "qes" "flw" StartActivity ( "|" StartActivity )+ "wlf" "flw" Activity ( "|" Activity )+ "wlf" StartPickActivity := PickActivity := "seq" StartActivity ( ";" ( Activity )? )* "qes" "pck" ReceiveActivity ";" Activity ";" ( "+" ReceiveActivity ";" Activity ";" )+ "kcp" "pck" ReceiveActivity ";" Activity ";" ( "+" ReceiveActivity ";" Activity ";" )+ "kcp" EmptyActivity := "empty" 5.2 Sviluppi futuri 127 ReceiveActivity := "rcv" RecPartners OperationId "(" RecParams ")" InvokeActivity := "inv" InvPartners OperationId "(" InvParams ")" ExitActivity := "exit" ThrowActivity := "throw" RecPartners := "<" PartnerLitId ( "," PartnerId )? ">" InvPartners := "<" PartnerId ( "," PartnerLitId )? ">" PartnerId := PartnerLitId | BoundId PartnerLitId := <STRING_LITERAL> OperationId := <IDENTIFIER> RecParams := BoundId ( "," BoundId )* InvParams := InvParam ( "," InvParam )* InvParam := Expression AssignActivity := BoundId ":=" Expression VarId := <IDENTIFIER> BoundId := <IDENTIFIER> ConditionalActivity := "if" "(" Expression ")" Activity Activity IterationActivity := "while" "(" Expression ")" Activity 128 Conclusioni e sviluppi Expression := ConditionalOrExpression ConditionalOrExpression := ConditionalAndExpression ( "or" ConditionalAndExpression )* ConditionalAndExpression := EqualityExpression ( "and" EqualityExpression )* EqualityExpression := RelationalExpression ( "==" RelationalExpression | "!=" RelationalExpression )* RelationalExpression := AdditiveExpression ( "<" AdditiveExpression | ">" AdditiveExpression | "<=" AdditiveExpression | ">=" AdditiveExpression )* AdditiveExpression := MultiplicativeExpression ( "+" MultiplicativeExpression | "-" MultiplicativeExpression )* MultiplicativeExpression := UnaryExpression ( "*" UnaryExpression | "/" UnaryExpression )* UnaryExpression := "!" UnaryExpression | BaseExpression BaseExpression := VarId | Literal | "(" Expression ")" Literal := <STRING_LITERAL> | <NUMBER_LITERAL> | BooleanLiteral 5.2 Sviluppi futuri BooleanLiteral := <TRUELITERAL> 129 | <FALSELITERAL> /* LITERALS */ < TRUELITERAL: "true" > < FALSELITERAL: "false" > < STRING_LITERAL: "\"" ( (˜["\"","\\","\n","\r"]) | ("\\" ( ["n","t","b","r","f","\\","’","\""] | ["0"-"7"] ( ["0"-"7"] )? | ["0"-"3"] ["0"-"7"] ["0"-"7"] ) ) )* "\"" > < NUMBER_LITERAL: (["0"-"9"])+ ( "." )? (["0"-"9"])* (<EXPONENT>)? (["f","F","d","D"])? | "." (["0"-"9"])+ (<EXPONENT>)? (["f","F","d","D"])? | (["0"-"9"])+ <EXPONENT> (["f","F","d","D"])? | (["0"-"9"])+ (<EXPONENT>)? ["f","F","d","D"] > < #EXPONENT: ["e","E"] (["+","-"])? (["0"-"9"])+ > /* IDENTIFIERS */ < IDENTIFIER: <LETTER> (<PART_LETTER>)* > 130 < #LETTER: [ "$", "A"-"Z", "_", "a"-"z" ]> < #PART_LETTER: [ "\u0000"-"\u0008", "\u000e"-"\u001b", "$", "0"-"9", "A"-"Z", "_", "a"-"z", ]> Conclusioni e sviluppi Riferimenti [1] Alessandro Lapadula, Rosario Pugliese, Francesco Tiezzi. “A formal account of WS-BPEL”. Proc. 10th international conference on Coordination Models and Languages (COORDINATION’08). Springer “Lecture Notes in Computer Science” 5052. 2008. [2] IBM, Bea, Microsoft. “Business Process Execution Language for Web Services, Version 1.0” - July 31, 2002. (http://download.boulder.ibm.com/ibmdl/pub/software/dw/specs/ws-bpel/ws-bpel1.pdf) [3] IBM, Bea, Microsoft, SAP, Siebel System. “Business Process Execution Language for Web Services Version 1.1” - May 5, 2003. (http://download.boulder.ibm.com/ibmdl/pub/software/dw/specs/ws-bpel/ws-bpel.pdf) [4] OASIS “Web Services Business Process Execution Language Version 2.0” OASIS Standard - April 11, 2007. (http://docs.oasis-open.org/wsbpel/2.0/OS/wsbpel-v2.0-OS.html) [5] OASIS (Organization for the Advancement of Structured Information Standards). Oasis Web Site: http://www.oasis-open.org/ [6] IBM. Web Services Flow Language (WSFL). 2001. (http://xml.coverpages.org/wsfl.html) [7] Microsoft. XLANG - XML Business Process Language. 2005 (http://xml.coverpages.org/xlang.html) [8] W3C Recommendation. “Extensible Markup Language (XML) 1.1 (Second Edition)” August 16, 2006. (http://www.w3.org/TR/2006/REC-xml11-20060816) 132 RIFERIMENTI [9] W3C Recommendation. “Extensible Markup Language (XML) 1.0 (Fifth Edition)” November 26, 2008. (http://www.w3.org/TR/REC-xml/) [10] W3C Recommendation. “XML Schema Part 0: Primer Second Edition” October 28, 2004. (http://www.w3.org/XML/Schema) [11] W3C, Official WSDL Web Site. “Services Description Working Group”. (http://www.w3.org/2002/ws/desc/) [12] W3C Working Draft. “Web Services Description Language (WSDL) Version 2.0 Part 2: Predefined Extensions” August 3, 2004. (http://www.w3.org/TR/2004/WD-wsdl20-extensions-20040803/) [13] W3C Recommendation. “SOAP Version 1.2 Part 1: Messaging Framework (Second Edition)” April 27, 2007. (http://www.w3.org/TR/2007/REC-soap12-part1-20070427/) [14] W3C Architecture Domain. “HTTP - Hypertext Transfer Protocol” Specifications, Drafts, Papers and Reports. 1999-2008 (http://www.w3.org/Protocols/) [15] Tim Berners-Lee. “HyperText Transfer Protocol Design Issues” CERN Geneva, 1991. (http://www.w3.org/Protocols/DesignIssues.html) [16] Tim Berners-Lee. “Web Services, Program Integration across Application and Organization boundaries” W3C Web Services Workshop, 2002-2003. (http://www.w3.org/DesignIssues/WebServices.html) [17] S. C. Cheung, Hui Lei, Michael R. Lyu. “Service Oriented Computing and Applications ” Springer London ISSN 1863-2386, February 22, 2007. (http://www.springerlink.com/content/1863-2386) [18] W3C Recommendation. “Web Services Addressing 1.0 - Core” May 9, 2006. (http://www.w3.org/TR/2006/REC-ws-addr-core-20060509/) [19] Bruno Giorgio “Linguaggi formali e compilatori” UTET Università, ISBN 8877502045. 1992. RIFERIMENTI 133 [20] Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullman “Compilers: Principles, Techniques, and Tools. Second edition.” ISBN 0-321-48681-1. 2006. [21] Java Official Web Site: http://java.sun.com/ [22] Java Compiler Compiler - The Java Parser Generator. Web Site: https://javacc.dev.java.net/. [23] JJTree Reference Documentation. Web Site: https://javacc.dev.java.net/doc/JJTree.html [24] Java Comunity Process. “Java Business Integration (JBI) 1.0 - Final Release” JSR-000208 - August 17, 2005. (http://jcp.org/aboutJava/communityprocess/final/jsr208/index.html) [25] OpenEsb - Open Source JBI Enterprise Service Bus. Web Site: https://open-esb.dev.java.net/ [26] Java Web Start Documentation. Web Site: http://java.sun.com/javase/6/docs/technotes/guides/javaws/developersguide/contents.html. [27] NetBeans Platform. Web Site: http://platform.netbeans.org/. [28] Generic Languages Framework (GLF). Web Site: http://languages.netbeans.org/. [29] Gamma Erich, Richard Helm, Ralph Johnson, John Vlissides “Design Patterns: Elements of Reusable Object-Oriented Software”, Addison-Wesley. ISBN 0-20163361-2. [30] Lorenzo Bettini, Rocco De Nicola, Daniele Falassi, Michele Loreti “Implementing a distributed mobile calculus using the IMC framework” ENTCS. Elsevier. 2006. (http://imc-fi.sourceforge.net/) [31] Java Comunity Process. “Java API for XML-Based Web Services 2.0” JSR-000224 - October 7, 2005. (http://jcp.org/aboutJava/communityprocess/pfd/jsr224/index.html) [32] Apache Axis2 Web Services Engine. Web Site: http://ws.apache.org/axis2/. 134 RIFERIMENTI [33] Apache CXF: An Open Source Service Framework. Web Site: http://cxf.apache.org/. [34] Geertjan Wielenga, Jaroslav Tulach and Tim Boudreau “Rich Client Programming: Plugging into the NetBeans Platform ” 2007 Sun Press. ISBN 0132354802. [35] Subversion: An Open Source Version Control System. Web Site: http://subversion.tigris.org/. [36] Google Code. Web Site: http://code.google.com/. [37] GNU General Public License Version 3, June 29, 2007. (http://www.gnu.org/licenses/gpl-3.0.html)