Università degli Studi di Padova Dipartimento di Matematica Corso di informatica Tesi di Laurea Triennale Applicazioni Web Offline studio delle caratteristiche di applicazioni web offline Candidato: Riccardo Cucco - 593945 Relatore: Dott.sa Ombretta Gaggi Azienda ospitante: Zucchetti SPA Tutor aziendale: Dott. Stefano Boldrin A.A. 2011/2012 Scopo del presente documento è quello di illustrare le attività svolte dal laureando Riccardo Cucco durante lo stage obbligatorio atto al conseguimento della laurea triennale in informatica. Le attività documentate sono divise in capitoli corrispondenti ai vari aspetti affrontati ovvero studio delle funzionalità messe a disposizione, progettazione di prototipi, considerazioni sull’utilizzo di dati locali e loro sincronizzazione con la controparte remota. Le attività di raccolta dati e documentazione sono state costantemente presenti durante lo svolgimento dello stage. Indice 1 Introduzione 1.1 L’evoluzione del web, da siti ad applicazioni 1.2 Obiettivi dello stage . . . . . . . . . . . . . 1.3 Azienda ospitante . . . . . . . . . . . . . . . 1.3.1 Informazioni generali . . . . . . . . . 1.3.2 Gruppo Zucchetti . . . . . . . . . . 2 Funzionalità e standard coinvolti 2.1 Le funzionalità offline . . . . . . . . . . . . 2.1.1 Cache . . . . . . . . . . . . . . . . . 2.1.2 Application Cache . . . . . . . . . . 2.2 La persistenza dei dati . . . . . . . . . . . . 2.2.1 SessionStorage . . . . . . . . . . . . 2.2.2 LocalStorage . . . . . . . . . . . . . 2.2.3 WebSQL . . . . . . . . . . . . . . . 2.2.4 IndexedDB . . . . . . . . . . . . . . 2.3 Funzionalità d’appoggio . . . . . . . . . . . 2.3.1 Chiamate asincrone . . . . . . . . . 2.3.2 JSON . . . . . . . . . . . . . . . . . 2.3.3 Programmazione ad eventi e callback 2.4 Maturità degli standard e limitazioni . . . . 2.4.1 Standard per manifest . . . . . . . . 2.4.2 Standard per WebSQL . . . . . . . . 2.4.3 Standard per IndexedDB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Applicazioni web offline 3.1 Perchè un’applicazione offline? . . . . . . . . . . . . . . . 3.1.1 Un esempio di applicazione esistente: Google Docs 3.2 Tipologie di applicazioni offline . . . . . . . . . . . . . . . 3.2.1 Applicazioni isolate . . . . . . . . . . . . . . . . . . 3.2.2 Applicazioni in sola lettura . . . . . . . . . . . . . 3.2.3 Applicazioni complesse . . . . . . . . . . . . . . . . 3.3 Progettazione delle applicazioni . . . . . . . . . . . . . . . 3.3.1 Interfacce distinte o integrazione trasparente . . . 3.3.2 Spostamento della componente di presentazione . . 3.3.3 Considerazioni sulle caratteristiche delle reti mobili 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 10 10 11 11 . . . . . . . . . . . . . . . . 12 12 12 13 14 14 14 14 15 15 15 16 16 16 17 17 17 . . . . . . . . . . 18 18 19 22 22 23 23 24 24 25 26 3.4 Il manifest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 Funzionamento del file manifest . . . . . . . . . . . . . . . . . . . . . 3.4.2 Stati dell’applicationCache e relativi eventi . . . . . . . . . . . . . . 3.4.3 Problematiche relative allo stato updateReady e inconsistenza dell’applicazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.4 Interferenza con la cache standard . . . . . . . . . . . . . . . . . . . 3.4.5 Problematiche relative al caching delle pagine e all’asincronia degli eventi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.6 Svuotamento dell’application cache . . . . . . . . . . . . . . . . . . . 3.4.7 Effetto BlankPage . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.8 Gestire l’effetto BlankPage . . . . . . . . . . . . . . . . . . . . . . . 3.4.9 Differenza tra assenza di connessione e tolleranza ai guasti . . . . . . 3.4.10 Implementare il rilevamento della connessione . . . . . . . . . . . . . Considerazioni conclusive . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 34 34 35 36 38 40 4 Persistenza dei dati 4.1 Sistemi di persistenza non strutturati . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Cookies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.2 Sessioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.3 LocalStorage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Sistemi di persistenza strutturati . . . . . . . . . . . . . . . . . . . . . . . . 4.2.1 Database remoti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.2 WebSQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.3 IndexedDB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Studio dei database locali . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.1 Contesto d’utilizzo di un database locale . . . . . . . . . . . . . . . . 4.3.2 Differenze tra IndexedDB e WebSQL . . . . . . . . . . . . . . . . . . 4.3.3 IndexedDB nel dettaglio . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.4 La scelta di supportare solamente IndexedDB: motivazioni e critiche 4.3.5 Scegliere il proprio database offline . . . . . . . . . . . . . . . . . . . 4.3.6 Variazioni al concetto di sicurezza del dato nel database locale . . . 4.4 Confronto con i database remoti . . . . . . . . . . . . . . . . . . . . . . . . 4.4.1 Relazionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.2 Non relazionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.3 Teorema di CAP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 41 41 42 42 43 43 44 45 45 45 46 48 49 50 50 51 51 52 52 5 Sincronizzazione dei dati 5.1 Sincronizzazione fisica di database . . . . . . . . . . 5.1.1 Ipotesi iniziale: interpretazione in ottica fisica 5.1.2 Problematiche riscontrate lato client . . . . . 5.1.3 Problematiche riscontrate lato server . . . . . 5.1.4 Partizionamento dei dati . . . . . . . . . . . . 5.1.5 Rivalidazione delle azioni eseguite client side 54 55 55 55 56 57 58 3.5 4 . . . (file) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 28 30 31 32 5.2 5.3 5.4 Sincronizzazione a livello applicativo . . . . . . . . . . . . . 5.2.1 Sincronizzazione trasparente . . . . . . . . . . . . . . 5.2.2 Sincronizzazione incrementale mediante copie delta . Strumenti di sincronizzazione . . . . . . . . . . . . . . . . . 5.3.1 Utilizzo di marcature di sincronizzazione . . . . . . . 5.3.2 DBDigest . . . . . . . . . . . . . . . . . . . . . . . . 5.3.3 Marcatori di operazioni incrementali legati a MVCC 5.3.4 Problematiche legate al Vacuum . . . . . . . . . . . 5.3.5 Aggiunta di timestamp ai fini della sincronizzazione 5.3.6 Inserire uno strato logico nel driver del database . . Sincronizzare le eliminazioni . . . . . . . . . . . . . . . . . . 5.4.1 Eliminazioni logiche . . . . . . . . . . . . . . . . . . 5.4.2 Tabelle cestino . . . . . . . . . . . . . . . . . . . . . 5.4.3 View di facciata . . . . . . . . . . . . . . . . . . . . 5.4.4 Log delle eliminazioni . . . . . . . . . . . . . . . . . 6 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 59 59 60 60 61 62 63 65 65 65 66 67 67 68 69 5 Elenco delle figure 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 logo logo logo logo logo logo logo logo logo Google Docs. . . . . Facebook. . . . . . . From Dust. . . . . . Symfony. . . . . . . . Google Web Toolkit. Sproutcore. . . . . . Impress.js. . . . . . . JavaScriptMVC. . . . Zucchetti SPA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 8 8 8 9 9 9 11 3.1 3.2 3.3 una schermata di Google Docs offline . . . . . . . . . . . . . . . . . . . . . . logo Zucchetti SPA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . logo Zucchetti SPA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 20 21 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Introduzione Il presente documento descrive l’attività di stage svolta dal laureando Riccardo Cucco presso l’azienda Zucchetti SPA. Il progetto di stage consiste nello studio delle funzionalità a supporto delle applicazioni web offline: in particolare l’obbiettivo primario di questo progetto è stato quello di valutare la maturità delle funzionalità offerte dai browser nell’attuale implementazione degli standard (ancora in via di sviluppo). Lo studio delle funzionalità è stato svolto in un ottica non prettamente teorica, ma applicativa, e quindi è stata dedicata una gran parte del tempo alla sperimentazione e allo sviluppo di prototipi che potessero verificare la corretta implementazione e l’effettiva applicabilità delle specifiche analizzate. 1.1 L’evoluzione del web, da siti ad applicazioni Il mondo del web per come lo conosciamo ora è un ambiente estremamente dinamico e ricco di risorse. Negli ultimi anni alcuni grandi progetti hanno trasformato la concezione statica del web in una versione molto più interattiva e funzionale, entrando nel quotidiano con vere e proprie applicazioni quali GMail, Google Docs, Facebook, Twitter e simili. Tutte queste realtà indicano come il web sia diventato e venga percepito come una reale piattaforma di sviluppo al pari dell’ambiente desktop. La ricchezza delle applicazioni sviluppate raggiunge livelli estremamente alti, specie considerando la rapidità con cui siamo passati da meccanismi di interazione basilari a navigatori, giochi di ruolo e via dicendo. Figura 1.1: logo Google Docs. Google Docs è un’applicazione che è nata in supporto al progetto GMail. Google Docs viene utilizzata in tutto il mondo come suite di strumenti d’ufficio. Offre la possibilità di utilizzare in maniera collaborativa i propri documenti. Questa applicazione è stata una delle prime a concretizzare la visione del Cloud Computing orientata all’ottica di produzione da ufficio e a fornirla gratuitamente su vasta scala. Facebook è un’applicazione web che ha guadagnato rapidamente una vastissima fascia d’utenza. Il suo successo deriva sicuramente anche dal livello di aggiornamento tecnico costante. 7 Figura 1.2: logo Facebook. Figura 1.3: logo From Dust. From Dust è un gioco 3D con un particolare sistema di interazione distribuito dalla Ubisoft, inizialmente per Xbox360, Playstation3, Windows e Mac. Recentemente è stata curata una sua trasposizione in applicazione web per Chrome (seppur sfruttando del codice nativo). Questo gioco è vincitore di molti premi, e trasformato in applicazione web ha potuto raggiungere un panorama di utenza ancora più vasto, tra cui l’intera fascia di mercato rappresentata dagli utenti Linux. Lo sviluppo di queste applicazioni si deve innanzitutto ad un potenziamento dei motori Javascript integrati nei browser, senza i quali saremmo ancora completamente schiavi dei tempi di trasmissione e dell’aggiornamento della pagina. Per ottenere un alta usabilità ed efficacia dei programmi sviluppati sono fiorite una serie di librerie in Javascript per la codifica client-side, in grado di migliorare l’interazione avanzata con l’utente. Per quanto riguarda la programmazione server-side, la maggior parte dei linguaggi ha sviluppato dei moduli per la programmazione web based, ed alcune compagnie hanno rilasciato interi framework in grado di rendere più veloce e disciplinato il processo di costruzione di un’applicazione web. Symfony è un framework completo per lo sviluppo web in PHP. È stato sviluppato da una web agency francese nel corso di lunghi anni di lavoro, condensando in un unico progetto tutte le conoscenze acquisite con l’esperienza. Attualmente il framework è uno dei più completi ed ha una comunità di supporto molto attiva. Data la sua stabilità è stato scelto come base per la prossima versione del noto CMS Drupal. Figura 1.5: logo Google Toolkit. Figura 1.4: logo Symfony. Google Web Toolkit è un framework basato su Java che permette di collegare a questo poliedrico linguaggio un interfaccia ed un modello di interazione prettamente web. Con GWT è possibile creare la propria applicazione Java e vederla poi eseguire nativamente su di un browser, senza curarsi di gestire chiamate asincrone, callback in JavaWeb Script, stile degli elementi e composizione della pagina. Con la diffusione di tutte queste risorse per lo sviluppo la concezione di web piatto è stata sostituita da un nuovo ambiente web coinvolgente, multidevice oltre che multipiattaforma. Le applicazioni sopracitate hanno aperto la strada verso una vera e propria rivoluzione: le 8 chiamate asincrone, framework capaci di creare dinamicamente la pagina client side disegnando gli elementi su un canvas, effetti grafici sempre più stupefacenti, modellazione 3D, database integrati nel browser e copia permanente in cache di intere applicazioni. Sproutcore è un framework Javascript utilizzato da Apple per il suo progetto ICloud. Lavorare con Sproutcore significa utilizzare un sistema di sviluppo ben definito e disciplinato, ma garantisce una resa visiva ed un efficacia veramente invidiabili. Figura 1.7: logo Impress.js. Figura 1.6: logo Sproutcore. Impress.js è una libreria JavaScript che permette di codificare in maniera rapida e semplice un sistema di presentazione con slide disposte all’interno di una vasta area piana (o tridimensionale), simulando il concetto alla base del sistema di presentazione Prezi. L’utilizzo del canvas e della visualizzazione tridimensionale è stato curato molto bene, garantendo effetti di transizione fluidi e armoniosi. JavaScriptMVC appartiene alla categoria dei framework strutturali, utilizzati per garantire alle applicazioni da sviluppare una corretta ripartizione del codice, secondo i pattern dell’ingegneria del software. Vi sono molte altre Figura 1.8: logo librerie simili, ognuna con un diverso pensiero riguardo alJavaScriptMVC. l’organizzazione delle componenti dell’applicazione: Backbone.js (pattern Model View Collection), Spine.js (Model Controller). In un solo punto la differenza tra applicazione web based e desktop è insormontabile: la necessità di una connessione. La disponibilità di applicazioni offline è un punto chiave, che crea un divario notevole tra applicazioni web e desktop. 9 1.2 Obiettivi dello stage L’obbiettivo dello stage è uno studio di maturità delle tecniche di programmazione, delle funzionalità e degli strumenti necessari a colmare il divario descritto nel paragrafo precedente. La necessità di una connessione continua comporta alcune difficoltà, specialmente quando l’applicazione deve essere usata in un contesto particolare, come quello della mobilità, che quindi verrà spesso utilizzato come ambiente di riferimento. I punti di principale interesse nello studio delle applicazioni web offline sono quindi: • lo studio delle differenti forme di applicazioni offline • l’utilizzo delle funzioni di caching HTML5 per sopperire alla mancanza di connessione • l’utilizzo di dati locali in vari formati di archiviazione e • i modelli di sincronizzazione possibili tra dati locali e remoti in un ottica multidevice e multiutente Per lo studio delle funzionalità sono stati utilizzati vari riferimenti: come prima fonte sono stati scelti gli standard del W3C, integrati dalle specifiche tecniche dei browser più fedeli agli standard. Utilizzando i primi come fonti teoriche e le seconde come fonti più pratiche è stato possibile in varie situazioni poter comprendere il concetto generale e il reale obbiettivo di una certa funzionalità prima di calarsi nella sua attuale implementazione, a volte solo parziale e condizionata o limitata da altre necessità tecniche o progettuali. 1.3 Azienda ospitante Il progetto di stage, comprendente le attività di analisi, progettazione, programmazione e documentazione, è stata svolta presso l’azienda Zucchetti SPA con sede a Padova. La Zucchetti SPA, sede dello stage, fa parte del gruppo Zucchetti. La scelta di tale azienda è stata guidata dall’interesse dimostrato nello studio delle nuove soluzioni messe a disposizione dai moderni standard per il web. L’azienda sviluppa da anni applicazioni web per la gestione aziendale, ed ha quindi affrontato una vasta gamma di problemi derivanti dall’uso avanzato delle funzionalità web in sostituzione ai normali programmi desktop. La sede di Padova, scelta per lo stage, si occupa totalmente di ricerca e sviluppo di nuove soluzioni, ed è quindi un ambiente molto stimolante, in cui vari progetti vengono portati avanti in parallelo. 10 1.3.1 Informazioni generali Nome: Zucchetti SPA Sede del Tirocinio: Via Giovanni Cittadella 7 CAP: 35137 Città: Padova Provincia: PD Telefono: 049659831 Fax: 049659831 Partita IVA: 05006900962 Mail: [email protected] 1.3.2 Gruppo Zucchetti Il gruppo Zucchetti si occupa principalmente di fornire soluzioni software, hardware e servizi innovativi ad aziende di ogni dimensione e settore, professionisti, associazioni di categoria, CAF e alla pubblica amministrazione. Il gruppo è all’attivo da oltre 30 anni e fornisce prodotti e servizi utilizzati da oltre 10.000 aziende italiane. Nella figura 1.9 è illustrato il logo del gruppo Zucchetti. Figura 1.9: logo Zucchetti SPA 11 2 Funzionalità e standard coinvolti L’intera esperienza di stage può essere considerata uno studio avanzato di alcune funzionalità. Il principio di fondo che ha guidato questo studio non si basa solamente sul modo in cui applicare gli standard, non è una sorta di guida allo sviluppo. L’idea principale è quella di poter formare prima di tutto un concetto di applicazione web offline, che differisce radicalmente dall’applicazione web online. Per riuscire ad acquisire la visione generale dell’ambiente offline è però necessario capire preventivamente su quali principi si basa. I punti esposti successivamente sono un anticipazione di ciò che verrà esposto nella tesi, ma sono utili per formare una rapida visione d’insieme, necessaria per poter poi capire meglio la connessione dei vari elementi. 2.1 Le funzionalità offline Studiare le funzionalità offline significa cercare di comprendere i linguaggi, le tecniche di programmazione e di progettazione, gli standard e gli strumenti messi a disposizione per risolvere il problema dell’assenza di connessione. Il principio fondante è mantenere una copia delle pagine necessarie all’applicazione in memoria, in modo da poterle presentare senza doverle scaricare nuovamente alla prossima richiesta. Un’applicazione di questo tipo richiede quindi una prima connessione (definita come connessione di setup) in cui i dati vengono recuperati ed immagazzinati. Per realizzare questo comportamento vi sono due principali soluzioni, che differiscono pesantemente per quanto riguarda sia il setup che la gestione delle pagine memorizzate. 2.1.1 Cache Il principio di funzionamento della cache è il più basilare: ogni file viene marcato con un istruzione nell’intestazione. Questa istruzione indica il periodo per il quale il file deve essere considerato attendibile e quindi riutilizzato senza essere aggiornato. Tale istruzione non dev’essere necessariamente inserita nell’intestazione di ogni file, utilizzando htaccess è possibile associare una specifica politica di caching ai file contenuti all’interno di una cartella, o corrispondenti ad un particolare schema. I problemi principali di questo approccio sono due: l’irreversibilità dell’istruzione e la necessità della scansione. Per irreversibilità dell’istruzione si intende l’impossibilità di forzare un aggiornamento del file finché l’istruzione di caching non esaurisce il suo effetto. In sostanza un elemento marcato come definitivo, con un periodo di caching illimitato, non verrà mai aggiornato, e quindi 12 successive modifiche alla sua politica di cache non verranno mai interpretate. Questa caratteristica comporta quindi un isolamento di alcune sezioni dell’applicazione, che seppur inizialmente voluto potrebbe creare successivamente dei gravi problemi di manutenibilità. Il secondo problema riguarda la necessità di effettuare una scansione manuale completa di tutti i file che compongono l’applicazione, dal momento che le istruzioni di caching vengono interpretate solamente se il file viene effettivamente utilizzato almeno una volta in presenza di connessione. Per memorizzare un’applicazione e poterla utilizzare in modalità offline, l’utente dovrebbe quindi utilizzare almeno una volta ogni sua funzionalità online, esplorando ogni sezione, comprese azioni proibite, errori e simili. Le tecniche per gestire in maniera intelligente gli aggiornamenti della cache sfruttano gli appigli più impensati, cercando di modificare il nome del file, la data di creazione e simili. Purtroppo questi espedienti non hanno un funzionamento uniforme sui vari browser (essendo appunto principalmente espedienti), i quali al di là delle istruzioni di caching fornite applicano delle politiche automatiche difficilmente gestibili dallo sviluppatore. 2.1.2 Application Cache L’application cache è un evoluzione del concetto di cache definito al punto precedente. Le istruzioni di cache non vengono distribuite file per file, ma mantenute in un singolo file chiamato manifest. Nel manifest di un’applicazione web sono elencate tutte le risorse che lo compongono, e per ogni elemento è specificato se debba essere mantenuto in memoria ed utilizzato senza essere richiesto al server, o se debba essere costantemente richiesto. Utilizzando questo approccio si isolano immediatamente le due aree dell’applicazione: quella offline e quella online. L’application cache differisce soprattutto nella gestione dei due punti analizzati precedentemente nella sezione della cache: nonostante le politiche di caching previste è possibile aggiornare senza difficoltà la struttura e la versione dei file inseriti, e non è richiesta una scansione completa dell’applicazione per il setup. Quando il file manifest viene interpretato per la prima volta il browser legge l’elenco delle risorse elencate, ed avvia lo scaricamento di tutti gli elementi inseriti nella sezione offline. Da questo momento in poi tutti i file inseriti nella sezione offline non verranno più richiesti al server. Scaricando in un singolo istante tutti i file dell’applicazione può venire a crearsi qualche effetto indesiderato (trattato successivamente nella tesi come effetto BlankPage ) ma si risolve il problema della scansione dell’applicazione. Ogni volta che viene caricata una pagina presente nel file manifest viene interrogato automaticamente il file manifest remoto, per verificarne la versione. Se il file remoto non è reperibile (funzionamento offline) oppure è identico a quello locale, vengono utilizzati tutti i file precedentemente scaricati. Se invece il file manifest remoto risulta differente dal file manifest locale esso viene riscaricato e reinterpretato con il conseguente aggiornamento di tutte le risorse in esso contenute (anche quelle non aggiornate). Questa politica risolve il problema delle variazioni nella politica di caching e dell’aggiornamento dei file, seppur introducendo alcuni effetti indesiderati che verranno analizzati successivamente. 13 2.2 La persistenza dei dati Per ottenere una funzionalità completa in assenza di connessione non è sufficiente analizzare solamente il problema del reperimento delle risorse; è necessario occuparsi anche della strutturazione dei dati, del loro recupero e dell’utilizzo. La persistenza di dati tra una pagina e l’altra è stato uno dei primi fattori chiave nello sviluppo dei siti web. Una versione di web stateless, cioè senza memoria di stato tra una pagina e la successiva, non avrebbe permesso la creazione di procedure di login, di carrelli della spesa e quant’altro. I sistemi di memorizzazione più utilizzati sono stati inizialmente i cookies (piccoli file testuali generati dal browser client side ed altamente vulnerabili), poi le sessioni (un meccanismo server side in grado di abbinare un set di dati ad una connessione attiva tra client e server), ed ovviamente i database, in grado di memorizzare dati strutturati come in qualsiasi altro contesto applicativo. La necessità di limitare le iterazioni con il server ha spinto gli sviluppatori ad immaginare un sistema alternativo di memorizzazione tra le pagine che non coinvolgesse direttamente il server se non in caso di reale bisogno. Distinguiamo quindi i dati locali da quelli remoti, in base sia al loro dominio d’applicazione che alla loro effettiva localizzazione. 2.2.1 SessionStorage SessionStorage è stata una delle prime soluzioni al problema sviluppate. In sostanza offre un sistema di persistenza basato su coppie chiave-valore in cui entrambi i campi sono rappresentati da stringhe. Questo sistema basilare di archiviazione permette quindi di archiviare rapidamente valori, istruzioni ed anche oggetti se opportunamente serializzati. La persistenza si SessionStorage è decisamente limitata, infatti ogni dato memorizzato al suo interno viene eliminato all’uscita dal dominio dell’applicazione o alla chiusura del browser. 2.2.2 LocalStorage Se SessionStorage può essere vista come un area prettamente temporanea, LocalStorage offre invece gli stessi servizi con un livello di persistenza concreto. Tutti i dati memorizzati per il dominio dell’applicazione vengono mantenuti anche all’uscita dal dominio stesso o alla chiusura dell’applicazione, richiedendo quindi all’utente di cancellarli nel caso si rendessero non più necessari. 2.2.3 WebSQL Per lavorare con dati strutturati utilizzando le soluzioni descritte finora sarebbe necessario codificare i dati all’interno di oggetti, serializzarli ed archiviarli, perdendo quindi la possibilità di eseguire su di essi ricerche specifiche in base alla struttura o alle caratteristiche del dato stesso. WebSQL fornisce un interfaccia di accesso ad un vero e proprio database contenuto all’interno del browser (attualmente un implementazione di SQLite). Il sistema di interrogazione 14 prevede l’utilizzo del linguaggio SQL, largamente diffuso ed adottato nella quasi totalità degli ambiti di programmazione. La versione di SQLite utilizzata in WebSQL è in realtà affetta da alcune limitazioni, ma rimane comunque uno strumento decisamente potente. L’idea di poter sincronizzare i dati tra SQLite e un server remoto è stata lo spunto iniziale per questo stage. 2.2.4 IndexedDB A seguito delle pesanti critiche a WebSQL è stato sviluppato parallelamente un progetto che sembra aver preso il sopravvento: IndexedDB. Questo DBMS si rifà a database non relazionali quali MongoDB, offrendo al tipico programmatore Javascript un ambiente estremamente familiare, sia per le definizioni che per le interrogazioni. I dati da memorizzare vengono codificati nella sintassi JSON (JavaScript Object Notation), che ne definisce attributi e metodi proprio come i normali oggetti nei linguaggi di programmazione orientati ad oggetti. IndexedDB è in grado di memorizzare questi oggetti in collezioni, senza che essi rispondono ad uno schema strutturale preciso. Il fattore chiave è la presenza di un indice in un formato stabilito per la collection, che viene utilizzato per identificare univocamente l’elemento ed effettuare le ricerche. Per i campi più comuni è poi possibile definire indici secondari che verranno sfruttati per eseguire partizionamenti del database per velocizzare le ricerche. Contrariamente alla soluzione con WebSQL i dati non vengono serializzati, ma memorizzati nel formato di definizione, rendendo possibile l’utilizzo di ogni attributo come chiave di ricerca senza eseguire deserializzazioni ripetitive. 2.3 Funzionalità d’appoggio Per concludere la panoramica delle funzionalità utilizzate è conveniente descrivere alcune caratteristiche del linguaggio JavaScript necessarie per far dialogare le componenti descritte tra di loro. 2.3.1 Chiamate asincrone Le chiamate asincrone rendono possibile un dialogo con il server indipendente dalla normale procedura di aggiornamento della pagina. Possono essere eseguite richieste al server, è possibile inviare dati di moduli come se l’utente li avesse compilati personalmente, ed ottenere la risposta che il server avrebbe fornito per presentarla poi all’utente. Alla base delle chiamate asincrone vi è un oggetto HttpRequest, presente in varie forme su tutti i browser. Questo oggetto compone delle chiamate incapsulando i dati forniti nel formato corretto, pronti per essere inoltrati ad un indirizzo specificato nella richiesta. Nel contesto in analisi le chiamate asincrone sostituiscono la maggior parte della navigazione tra le pagine che compongono le applicazioni, questo per aumentare il senso di consistenza dell’interfaccia (nessuna pagina bianca, ne caricamenti evidenti che possano ricondurre ad un esperienza web standard) e rende possibile l’utilizzo di politiche di instradamento logico per le risorse o la generazione di risorse in realtà virtuali. 15 2.3.2 JSON La JavaScript Object Notation è un formato di definizione di dati largamente diffuso ed utilizzato. La sua semplicità lo rende adatto a rappresentare efficacemente la maggior parte dei costrutti JavaScript, mantenendone intatta la chiarezza e la semplicità di utilizzo. Ogni oggetto JSON è considerabile come un array associativo, in cui ogni funzione o attributo è identificato dal suo nome che ne è la chiave di ricerca. Ogni oggetto JSON può essere serializzato rapidamente con una funzione spesso implementata nativamente dai browser, ed in maniera altrettanto rapida è possibile ritornare dalla rappresentazione testuale a quella di oggetto. 2.3.3 Programmazione ad eventi e callback Nel momento in cui si utilizza JavaScript per scopi avanzati, quali l’interrogazione dei database o l’utilizzo di chiamate asincrone, ci si trova a dover ragionare in maniera non lineare, prevedendo un flusso di codice da seguire progressivamente, al contrario, bisogna fornire continuamente funzioni di callback che vengono eseguite nel momento in cui determinati eventi si verificano. In sostanza è possibile provocare la chiamata ad una funzione, e fornire come argomenti le funzioni da richiamare per la gestione dei risultati nei diversi casi. Questo ragionamento a volte risulta difficile da comprendere fino in fondo e porta a dover dare al proprio codice una forma molto meno comprensibile. Non è più possibile quindi determinare in un particolare punto del proprio programma quali operazioni siano state eseguite e quali no, in quanto ogni chiamata può dare il via ad una reazione a catena parallela in grado di determinare ulteriori eventi e modifiche ai dati in comune. In questa situazione caotica a volte è necessario cercare di progettare in anticipo alcuni checkpoint in cui le chiamate secondarie convergono e il flusso principale possa proseguire verificando preventivamente alcune condizioni. 2.4 Maturità degli standard e limitazioni Alcuni degli standard relativi alle componenti presentati sono abbastanza maturi da avere un’implementazione coerente tra i browser attuali, mentre vi sono altri casi in cui lo standard non è ancora pienamente definito, e l’implementazione dei principali browser è determinata, oltre che dalle necessità tecniche, da una differente interpretazione dei concetti di fondo, o da un assenza di chiarezza su alcune specifiche. Gli standard W3C per HTML5 sono stati scritti con uno scopo più grande rispetto alla semplice definizione di interfacce comuni. Prima di HTML5 l’approccio era più descrittivo: si definiva un componente, quali funzionalità doveva offrire e perché. Gli standard attuali invece fanno un passo avanti, dando oltre che alla definizione, alcune importanti linee guida sul funzionamento, che vengono poi utilizzate come base per un implementazione comune a tutti i browser. Questo approccio è molto importante, perché limitando leggermente la libertà degli sviluppatori dei browser, rende gli stessi più conformi agli standard, e scongiura in certi ambiti esplosioni della nota guerra tra browser. 16 2.4.1 Standard per manifest Lo standard per la componente manifest [1] è ad uno stadio di definizione quasi conclusivo. Fa parte della definizione dello standard HTML5. Le funzionalità elencate sono ben descritte, ed attualmente le implementazioni nei browser principali non differiscono sensibilmente. La chiarezza nella descrizione della componente deriva sicuramente dalla semplicità del concetto alla base, che la rende una funzionalità solida su cui poter costruire senza troppi pensieri la propria applicazione web. Il file manifest è riconosciuto sia dai browser webkit (Chrome, Safari e Opera per l’ambiente desktop, i browser di Android, iPhone e Nokia per l’ambiente mobile) che da Firefox. Rimane invece inutilizzabile su Internet Explorer, eliminandolo quindi dalla lista dei potenziali utilizzatori di applicazioni web offline, salvo poi effettuare una sostituzione con il motore webkit mediante il plugin Chrome Frame. 2.4.2 Standard per WebSQL La stesura dello standard per WebSQL [2] è stata abbandonata in quanto il progetto è stato sostituito ufficialmente da IndexedDB. Compagnie come Google e Apple continuano comunque ad utilizzare largamente WebSQL, rendendone di fatto improbabile l’abbandono completo. Google Docs e molte altre applicazioni si appoggiano in maniera decisa sul database integrato, alcune proprio per fornire funzionalità offline complete. I browser non webkit possono utilizzare tramite quale espediente la stessa interfaccia di WebSQL: Firefox utilizza internamente SQLite per la gestione dei preferiti, ed è possibile raggiungere questa componente e reindirizzarvi le chiamate destinate originariamente a WebSQL. 2.4.3 Standard per IndexedDB Lo standard per IndexedDB [3] è attualmente in fase di sviluppo, sebbene i concetti chiave siano già chiari e difficilmente potranno subire modifiche. Anche in questo caso non si è inventato ex novo un DBMS ma si sono applicati i concetti esplorati dai database non relazionali e i sistemi di interrogazione sviluppati per MongoDB. IndexedDB è implementato oltre che su Firefox (Mozilla è stato uno dei principali sostenitori per l’esclusione di WebSQL), su i browser webkit e su Internet Explorer (il quale però non può attualmente essere preso in considerazione a causa dell’assenza del supporto per il file manifest) 17 3 Applicazioni web offline 3.1 Perchè un’applicazione offline? Nella parte introduttiva è stato spiegato l’obbiettivo di questa tesi: dimostrare come le funzionalità offline possano appianare completamente il divario tra le applicazioni web e desktop. Non si è però discusso degli altri scenari d’applicazione di tali funzionalità. Alcuni effetti secondari della programmazione in ottica offline valgono decisamente la pena di essere considerati ai fini di un progetto efficiente, offline o non. Il primo passo nella strutturazione di un’applicazione con supporto offline è la divisione tra le componenti stabili, cioè quelle che di norma non vengono mai variate tranne in casi di radicali modifiche all’applicazione, componenti variabili, che cioè vengono poste periodicamente ad aggiornamento, e componenti temporanee come i dati stessi. Escludendo i dati, che verranno trattati ampiamente più avanti, rimane isolata la struttura portante dell’applicazione, quella principalmente interessata dai vantaggi dell’application caching. La suddivisione definita può essere individuata all’interno di qualsiasi applicazione web o perfino sito, garantendo la possibilità di estendere queste considerazioni alla quasi totalità del panorama web. Le applicazioni web senza politiche di caching specifiche prevedono che l’utente ottenga una nuova versione dell’applicazione ogni volta che esso vi si collega, garantendo quindi la freschezza dell’aggiornamento. Con la diffusione delle connessioni ad alta velocità lo scaricamento ripetuto delle pagine potrebbe essere chiaramente accettabile, ma la crescente diffusione di dispositivi mobile in grado di accedere al web implica delle considerazioni molto importanti a riguardo. La navigazione mobile generalmente non può fare affidamento su una grande disponibilità di banda, e in molti casi oltre alla scarsa velocità di trasferimento vengono imposti dai provider dei limiti alla quantità di dati da scaricare. Integrando quindi la visione iniziale con lo scenario mobile si capisce come la leggerezza di un’applicazione, e quindi il suo utilizzo intelligente delle risorse e delle connessioni, sia un valore di chiara importanza. Organizzare una politica di caching efficace, permette all’utente di non dover mai riscaricare componenti applicativi che già possiede, riducendo i tempi d’attesa, e abbattendo i costi di trasferimento. In una visione ideale l’applicazione locale scambia solamente i dati con la controparte remota, arrivando quindi al massimo dell’efficienza. A causa della natura implicitamente dinamica dell’ambiente web è difficile ipotizzare applicazioni che non siano costantemente in aggiornamento, basti pensare agli sforzi costanti 18 per ottenere una resa perfetta sui vari browser. Un’applicazione web, a differenza di un comune sito, potrebbe avere la necessità ulteriore di eseguire degli avanzamenti di versione, con conseguente modifica delle funzioni di base o del formato dei dati. In quest’ottica un’applicazione offline soffre della mancanza d’immediatezza, ma con i dovuti accorgimenti è possibile progettare un vero e proprio sistema di aggiornamento della propria applicazione. 3.1.1 Un esempio di applicazione esistente: Google Docs Google Docs è stato progettato come suite di applicazioni d’ufficio basata su web. La possibilità di avere i propri file ed il proprio editor in qualsiasi browser lo rende un prodotto eccezionale, ma senza un adeguata componente offline non potrebbe ambire alla sostituzione completa di altre suite d’ufficio. L’approccio utilizzato da Google Docs è completo, infatti permette all’utente sia la visione dei propri file in modalità di sola lettura, che la possibilità di modificarli ed applicare le modifiche al file principale una volta ripristinata la connessione. Nelle figure 3.1, 3.2 e 3.3 è possibile notare come Google Docs in versione offline sfrutti i database locali per memorizzare i documenti. Figura 3.1: una schermata di Google Docs offline 19 Nella figura 3.1 si può vedere come il database locale sia stato popolato dalle entità necessarie a modellare sia i documenti che l’applicazione. In particolare la tabella Documents contiene i riferimenti a tutti i documenti memorizzati. In questo caso la freccia azzurra a metà immagine indica il record relativo al documento attualmente aperto, come si può notare dal titolo del documento e dall’attributo title del record. Figura 3.2: logo Zucchetti SPA Nella figura 3.2 viene ispezionata la sezione del database che contiene del corpo del documento. Le frecce sono posizionate in modo da evidenziarne il titolo. Nella figura 3.3 viene dimostrato come l’aggiornamento della versione online provochi automaticamente una modifica della versione locale. Questa schermata si differenzia dalla prima poiché Google Docs non sta lavorando in modalità offline. Nonostante questo le modifiche vengono apportate sia al documento in lavorazione online che alla copia offline, effettuando quindi una sincronizzazione trasparente che non coinvolge l’utente. Applicando le modifiche 20 Figura 3.3: logo Zucchetti SPA alla versione locale anche quando c’è connessione con il server si garantisce la possibilità di poter troncare in qualsiasi momento il collegamento senza perdere la possibilità di utilizzare l’applicazione. Quest’ultimo punto rappresenta un ulteriore vantaggio di una applicazione offline ben progettata: la tolleranza ai guasti. 21 3.2 Tipologie di applicazioni offline Le applicazioni web che offrono funzionalità offline possono essere classificate in tre categorie principali: applicazioni offline isolate, applicazioni di sola lettura, ed applicazioni complesse. Le tre categorie hanno scopi differenti e pertanto sono basati su diverse concezioni, prima tra tutte il ruolo del dato all’interno dell’applicazione. Nell’applicazione isolata i dati generati localmente (se ve ne sono) vengono utilizzati solamente a livello locale, senza alcuna condivisione con il server. Nell’applicazione offline di sola lettura i dati sono generati solamente online, e vengono replicati localmente in modo da poter essere letti in ogni momento. Non è però concessa all’utente la possibilità di modificare tali dati se non utilizzando la versione online. Nell’applicazione complessa i dati vengono creati sia localmente in modalità offline che remotamente in modalità online. In quest’ultima variante le strategie di sincronizzazione e gestione dei conflitti (specialmente con dati condivisi) è di primaria importanza. 3.2.1 Applicazioni isolate Le applicazioni offline isolate sono forse le più semplici da comprendere poiché l’applicazione nasce per un utilizzo espressamente offline. Se viene generato qualche contenuto rimane ad uso esclusivo dell’applicazione locale e non viene sincronizzato con il server. Un esempio concreto di un’applicazione di questo tipo potrebbe essere una guida interattiva per un museo o una galleria, composta da i dati relativi alle opere, alle sale e ai percorsi tematici, che caricata su dispositivi mobile può fornire assistenza offline ai dispositivi dei clienti. Alla reception potrebbe essere disponibile un collegamento con l’applicazione remota, che una volta visitata replica i propri contenuti all’interno del dispositivo. Una volta replicata l’applicazione ogni visitatore potrebbe visitare il museo senza necessariamente essere collegato al server principale. Una funzionalità simile potrebbe non essere necessaria in una struttura moderna e ben informatizzata, ma probabilmente si adatterebbe molto bene a ville storiche, giardini e grandi parchi. In un’applicazione simile non è previsto che gli utenti manipolino i dati effettuandovi modifiche, né che i dati vengano aggiornati continuamente. Gli utenti potrebbero generare dei nuovi dati (ad esempio salvandosi percorsi preferiti o appuntandosi le opere da rivedere alla prossima visita o da consigliare agli amici), ma non è prevista la sincronizzazione di questi dati con quelli contenuti nel server. Ragionando più in un ottica di sviluppo potremmo dire che i dati memorizzati localmente, cosı̀ come il loro schema di rappresentazione all’interno del database locale, potrebbero essere una copia speculare di quelli inseriti nel server. Un database server potrebbe addirittura non essere presente, dal momento che non vi sarebbe bisogno di sincronizzazione. I dati necessari all’utente potrebbero essere inviati al dispositivo contestualmente alle pagine che compongono l’applicazione e generati quindi localmente. 22 3.2.2 Applicazioni in sola lettura Le applicazioni offline di sola lettura non sono in realtà applicazioni complete, ma componenti di compatibilità inserite all’interno di un’applicazione web più grande. I contenuti e le funzionalità dell’applicazione sono accessibili solamente online, ma mantenendo una copia dello stato dell’applicazione nella memoria locale è possibile visualizzarne i contenuti senza modificarli. Un esempio di questo tipo di sistema potrebbe essere un blog reader: mano a mano che si naviga nei vari blog vengono archiviate localmente tutte le informazioni contenute nei post, e nel momento in cui viene a mancare la connessione è possibile fornire una versione di “sola lettura” dei blog visitati. In questo caso si possono eseguire delle valutazioni sui dati da immagazzinare e sulla struttura dello schema. Un’applicazione gestionale lato server struttura i dati secondo le proprie logiche, valutando le relazioni tra i dati e suddividendoli in modo da mantenere integrità e consistenza (ad esempio rispettando i vincoli delle forme normali nello schema del database). La progettazione del database nel server segue una logica funzionale, considerando quindi principalmente le componenti, le relazioni tra le stesse ed i moduli che devono manipolarle. Nel momento in cui si utilizza l’applicazione i dati vengono manipolati, combinati e presentati attraverso una selezione e una proiezione. I dati ottenuti non sono più strutturati secondo una logica di relazione, ma secondo una logica di presentazione appunto. Qui si possono introdurre alcune considerazioni sulla tipologia e la struttura dei dati da memorizzare in modalità offline: se l’obbiettivo della componente offline è solamente quello di visualizzare i dati archiviati, potrebbe non aver senso mantenere localmente una copia esatta della struttura dei dati remoti, ma piuttosto una struttura progettata appunto nell’ottica della presentazione. Se i dati lato server sono strutturati finemente, e poi per essere presentati vengono proiettati in una vista, potrebbe aver senso memorizzare localmente i dati nel formato elaborato piuttosto che nel formato di partenza. 3.2.3 Applicazioni complesse Nelle applicazioni offline complesse non vi è un grande sbilanciamento tra componente locale e remota, poiché entrambe le componenti vengono sviluppate per poter collaborare attivamente. In un’applicazione offline complessa buona parte delle funzionalità della versione online sono replicate nella versione offline (ovviamente in caso queste abbiano un senso). I dati online vengono replicati localmente, e le modifiche in modalità offline vengono applicate ai dati remoti avviando specifiche politiche di sincronizzazione. Le politiche di sincronizzazione utilizzate da questo tipo di applicazioni devono necessariamente prevedere un sistema di risoluzione di conflitti, anche se non vi è condivisione di dati tra più utenti. Questo deriva dal fatto che nel momento in cui si replicano i dati remoti su di un dispositivo, questo rappresenta un unità di memorizzazione a sé stante, con il risultato che lo stesso utente, utilizzando più dispositivi potrebbe trovarsi a generare delle situazioni 23 di conflitto con i dati che ha utilizzato. Risulta chiaro quindi che qualsiasi applicazione offline complessa si può trovare in diversi livelli di condivisione che possono richiedere diversi sistemi di gestione dei conflitti: un dato potrebbe essere non condiviso, condiviso tra vari dispositivi dello stesso utente e infine condiviso tra più dispositivi di più utenti. Un esempio di un’applicazione del genere potrebbe essere una rubrica telefonica aziendale o un taccuino per le note condiviso. Ogni utente può accedere all’applicazione in modalità offline, creare del contenuto sia online che offline, ed avviare la sincronizzazione del dispositivo in momenti differenti, esponendo quindi il sistema al rischio di conflitti di scrittura e aggiornamento. La progettazione di un’applicazione di questa tipologia deve necessariamente prevedere una struttura dati locale molto simile alla struttura dati remota, ma contenente solamente una sezione dei dati liberamente accessibili all’utente. Valutando le problematiche di sincronizzazione si rende necessaria una progettazione particolare delle operazioni di manipolazione dei dati, che dovranno cercare di mantenere il più aggiornato possibile lo stato locale e quello remoto in modo da ridurre il numero di potenziali conflitti. 3.3 Progettazione delle applicazioni 3.3.1 Interfacce distinte o integrazione trasparente Nella progettazione dell’applicazione è necessario studiare le interazioni che l’utente può avere con la stessa in un contesto offline. Realizzare una componente offline dedicata, con una propria interfaccia e meccanismi di presentazione e controllo differenti, può presentare dei grandi vantaggi in termini di chiarezza, ma implica una duplicazione del codice che ne aumenta la complessità di manutenzione. L’applicazione presentata al paragrafo 3.1.1, Google Docs, utilizza due differenti interfacce per la versione online ed offline. Nella versione offline manca tutta la parte di interfaccia dedicata alla modifica del documento. Troviamo invece un impostazione dell’ambiente più ordinato, adatto appunto alla lettura. La dicitura solo visualizzazione in alto nella pagina fuga ogni dubbio. Un altra applicazione Google molto famosa è GMail. La versione offline di GMail non solo presenta un interfaccia differente, ma decisamente progettata per un utilizzo mobile. L’intera struttura della pagina tende a migliorare l’esperienza di utilizzo su dispositivi quali tablet e smartphone, dimostrando quindi che le funzionalità offline devono essere applicate principalmente all’ambiente mobile. Presentare una doppia interfaccia permette di mantenere i due ambienti di utilizzo (online e offline) nettamente distinti, esponendo funzionalità ed interfacce personalizzate per entrambi, con la possibilità di condividere comunque le strutture di controllo basilari. L’utilizzo di un approccio a doppia interfaccia migliora la chiarezza per l’utente, riducendo al minimo la possibilità di confondere operazioni offline e online. La realizzazione di una seconda interfaccia, dove possibile, potrebbe inoltre ridurre la necessità di modificare pesantemente 24 il codice preesistente nel caso una ristrutturazione completa fosse troppo complicata. L’integrazione delle funzionalità offline in maniera trasparente all’interno di una struttura preesistente può presentare alcune problematiche, specialmente se la componente di presentazione è posizionata completamente nel server. Come componente di presentazione si intendono i meccanismi di composizione delle pagine e le componenti che si occupano dell’inserimento dei contenuti nelle stesse. Spostare la componente di presentazione dal server al client presenta dei vantaggi per quanto riguarda la compatibilità tra offline e online, ed aumenta la separazione funzionale del codice, rendendo necessaria l’applicazione di design pattern che regolino il comportamento tra le componenti. 3.3.2 Spostamento della componente di presentazione Per comprendere il concetto dello spostamento della componente di presentazione da server a client è necessario comprendere come vengono generalmente progettate le applicazioni web, e in quali punti si inseriscono i vari linguaggi in base alle loro funzioni. Generalmente i dati di partenza provengono da un database posizionato nel server che fornisce l’applicazione, dal quale vengono prelevati tramite istruzioni SQL che li proiettano in una visione tabellare singola in base alle necessità. I dati passano quindi dalla forma strutturale relazionale ad una forma composta più semplice che ricorda una tabella. A questo punto il linguaggio server side che ha richiesto i dati li compone in un formato visivo, generalmente popolando un template precostruito. Il passaggio in cui i dati vengono inseriti all’interno dell’interfaccia rappresenta la componente di presentazione del sistema. Spesso nel template vengono incluse anche alcune logiche di controllo basilari in JavaScript, finalizzate alla manipolazione ridotta dei dati o al miglioramento delle funzionalità di interazione lato client. A questo punto la pagina che è stata composta viene inviata al client che la riceve e la visualizza mediante l’uso di un browser. Se la composizione dei dati all’interno della pagina viene realizzata lato server è difficile inserire correttamente e in maniera trasparente un meccanismo di recupero e di inserimento dei dati locali, poiché la pagina proveniente dal server è già completa. Inoltre se la pagina dovesse essere memorizzata localmente per funzionare in offline dovrebbe essere il quanto più possibile generica: una sorta di interfaccia vuota con meccanismi di controllo dati, che dovrebbe prelevare i dati dal server o dal client in base alla presenza o assenza di connessione. Spostare la componente di presentazione verso il client significa quindi prevedere la composizione delle pagine direttamente nel client, costruendo quindi lato server una pagina abbastanza scarna, con dei meccanismi di composizione lato client molto più avanzati. Un sistema progettato in questo modo permetterebbe il passaggio da online ad offline senza interruzioni, garantendo quindi la tolleranza ai guasti della rete. Il ruolo dell’applicazione lato server cambierebbe lievemente, espandendo i proprio compiti in base alle nuove necessità: verificare la bontà delle operazioni eseguite dal client in base ai limiti imposti, verificare 25 l’integrità dei dati che devono essere sincronizzati, e in generale controllare il flusso di informazioni tra l’applicazione remota e la controparte locale. Ragionando in ottica di dispositivi mobile vi sono inoltre altri vantaggi in un approccio simile: trasferendo solamente i dati variati ed i comandi, si ottiene una riduzione del traffico tra il client ed il server, e probabilmente una riduzione del carico del server stesso, dal momento che buona parte delle operazioni di lettura potrebbero essere realizzate in modalità offline, sgravando quindi il server dal costo di gestione delle richieste, del recupero dei dati e della generazione delle pagine da inviare al client. 3.3.3 Considerazioni sulle caratteristiche delle reti mobili Come detto precedentemente, progettare in un ottica online/offline richiede la definizione di procedure di sincronizzazione ben definite. Ragionando in termini di dispositivi mobile bisogna considerare alcune caratteristiche della trasmissioni su reti non cablate, che generalmente hanno tempi di latenza superiori alle connessioni su cavo. Questa caratteristica comporta alcune considerazioni importanti per la progettazione delle comunicazioni. Dal momento che nell’ambiente mobile i tempi di connessione sono più alti, il peso dei dati da trasferire passerebbe in secondo piano, con la conseguenza che anche con un contenuto molto ridotto, il costo di trasferimento in termini di latenza potrebbe essere non indifferente. Questa osservazione pone quindi in secondo piano la necessità di creare risposte estremamente compatte, e rende invece più efficiente compattare insieme le richieste (comandi o dati da inviare) per accumulare contenuto e ridurre i tempi medi di trasferimento. Al fine di ottimizzare i trasferimenti è importante sfruttare al massimo ogni risposta del server creando una sorta di piggybacking. Il piggybacking è una tecnica utilizzata nella progettazione dei pacchetti a basso livello su reti generiche. Questa tecnica consiste nell’aggiungere informazioni aggiuntive ai pacchetti trasmessi tra un dispositivo e l’altro, generalmente pacchetti necessari a regolare la connessione tra le parti o a verificare l’integrità del messaggio. Cercare di utilizzare il piggybacking nel contesto mobile significa sfruttare i trasferimenti richiesti dalle due applicazioni per trasferire dati aggiuntivi necessari non tanto all’utente ma alle due applicazioni in sé. Un esempio di funzionamento potrebbe essere questo: l’applicazione locale esegue delle operazioni di modifica ad alcuni dati, e li mantiene in un area temporanea per la sincronizzazione. Nel momento in cui si rende necessaria una comunicazione con il server, magari per recuperare un file allegato o una qualsiasi risorsa, si inoltra insieme alla richiesta del contenuto dei comandi di sincronizzazione ed un marcatore che indichi lo stato locale. Il server, ricevendo la richiesta, interpreterebbe anche i comandi aggiuntivi e verificherebbe lo stato dell’applicazione locale grazie al marcatore. Una volta eseguite queste operazioni il server comporrebbe i dati da inviare richiesti da una pagina in un pacchetto da trasferire, a cui allegherebbe l’esito dei comandi richiesti. In questo semplice schema abbiamo compattato in una richiesta ed una risposta: • l’iniziale richiesta della pagina al server 26 • la risposta del server che fornisce la pagina • l’invio da parte del client dei comandi da eseguire • la risposta da parte del server riguardo l’esecuzione dei comandi richiesti • la richiesta o l’inoltro da parte del client del marcatore di sincronizzazione • la risposta o la valutazione e risposta da parte del server con il proprio marcatore In base alle necessità è poi possibile includere buona parte dei dati in chiamate asincrone di questo tipo, ottimizzando quindi i tempi di apertura delle connessioni e diminuendo il costo medio del trasferimento dei dati. 3.4 Il manifest Come introdotto nel paragrafo 3.1, per progettare un’applicazione offline è necessario distinguere il progetto in due parti: una parte statica che verrà memorizzata localmente nell’application cache ed una parte dinamica che verrà richiesta al server ogni volta in cui viene utilizzata. È importante eseguire un ragionamento appena si opera questa distinzione: mentre la parte statica può essere sottoposta ad una sorta di versionamento dell’applicazione condizionato dalla versione del file manifest, la componente dinamica non può essere versionata in modo analogo. Questa considerazione indica che nel momento in cui vengono modificate sostanzialmente alcune componenti dinamiche dell’applicazione lo stato globale della stessa potrebbe trovarsi in una condizione di incoerenza. Dividere l’applicazione comporta alcune problematiche nel momento in cui il programma viene modificato, ed in una buona parte di applicazioni web le modifiche sono all’ordine del giorno. Modificare la parte online è semplice: nel momento in cui il client si connette troverà la nuova versione dell’applicazione e la parte locale dialogherà con essa. Ciò rappresenta un problema in caso l’applicazione locale non abbia eseguito lo stesso aggiornamento di versione della parte remota e tenti di dialogarvi secondo un formato non più utilizzato. Questa incoerenza tra le due applicazioni deve essere gestita in fase di progettazione, predisponendo appositi meccanismi di avanzamento di versione. Il meccanismo del file manifest prevede già un meccanismo di aggiornamento, che però può risultare ingestibile in alcuni casi. Ogni volta che viene rilevata una variazione al file manifest tutta la parte statica dell’applicazione viene eliminata e riscaricata. Questo comportamento risulta drastico in alcuni casi e potrebbe rappresentare un problema, specialmente se l’aggiornamento avviene tra una schermata e l’altra dell’applicazione o se a causa dell’aggiornamento della stessa i dati archiviati in memoria si vengano a trovare in una situazione di inconsistenza. I problemi principali in questo sistema di aggiornamento sono due: l’impossibilità di definire un unico punto di aggiornamento e l’impossibilità di rifiutare un aggiornamento e mantenere la versione corrente dell’applicazione. Nei capitoli successivi verranno proposti alcuni stratagemmi utili a evitare di incappare in uno dei problemi elencati. 27 3.4.1 Funzionamento del file manifest Il file manifest è un semplice file testuale contenente l’elenco delle risorse utilizzate dall’applicazione. Questo file è suddiviso in tre sezioni: cache, network e fallback. Nella sezione cache vengono elencate le risorse che devono essere memorizzate localmente (in un area di memoria chiamata application cache), nell’area network vengono elencate tutte le risorse che devono essere richieste al server ad ogni utilizzo, e nell’area fallback sono elencate una serie di risorse da utilizzare in mutua esclusione in base alla presenza o meno della connessione. In sostanza ogni elemento della versione fallback specifica quale elemento offline utilizzare in caso venga richiesto un file remoto in assenza di connessione. Le due aree cache e network rappresentano quindi la parte statica e dinamica dell’applicazione, ed istruiscono il meccanismo di memorizzazione su quali risorse richiedere e mantenere localmente. Nel momento in cui viene visitata per la prima volta una pagina contenente il file manifest, il browser reagisce lanciando una serie di eventi che avviano dei processi di sincronizzazione tra la versione in application cache e quella fornita dal server. Una volta che le risorse cache sono state completamente scaricate e memorizzate l’applicazione procede con il suo normale funzionamento, anche in assenza di connessione. Ogni volta che viene utilizzata una risorsa elencata in un file manifest il browser cerca di recuperare nuovamente il file manifest dell’applicazione dal server. Se questo file non è disponibile (com’è normale che sia in assenza di connessione) l’applicazione prosegue con il suo normale funzionamento. In caso invece sia presente una connessione il file manifest viene scaricato nuovamente, e viene confrontato con la copia locale. Se i due file risultano identici l’application cache non viene aggiornata, ma se vi è una qualsiasi differenza tra i due file vengono richieste nuovamente tutte le risorse elencate, e l’application cache relativa a tale file manifest viene cancellata. In sostanza i passaggi del processo di caching controllato attraverso il manifest sono i seguenti: Primo caso: nessun elemento in cache • riconoscimento di un file manifest nella pagina • (evento checking) controllo di differenze tra il file manifest locale ed il file manifest remoto • (evento downloading) viene avviato un download massiccio dell’applicazione • (evento progress) vengono scaricati i file necessari e memorizzati nell’application cache • (evento cached) terminato il download dei file viene lanciato l’evento cached, che informa il browser che da quel punto in poi dovranno essere utilizzate le risorse locali Secondo caso: nessuna variazione nel manifest o manifest irraggiungibile • utilizzo di una risorsa memorizzata nell’application cache 28 • (evento checking) controllo di differenze tra il file manifest locale ed il file manifest remoto • (evento noupdate) al browser viene confermata la versione memorizzata nell’application cache Terzo caso: applicazione in application cache e variazione nel manifest • utilizzo di una risorsa memorizzata nell’application cache • (evento checking) controllo di differenze tra il file manifest locale ed il file manifest remoto • (evento downloading) verificata una differenza nel manifest viene avviato un download massiccio dell’applicazione • (evento progress) vengono scaricati i file necessari e memorizzati nell’application cache • (evento updateReady) una volta scaricati tutti i file che compongono l’applicazione notifico la disponibilità ad un aggiornamento di versione, da questo momento in poi si utilizzerà la nuova application cache Si è parlato più volte di file manifest referenziato da una pagina, per essere più specifici il file manifest è un attributo del tag <HTML> che apre la pagina. Il contenuto dell’attributo è necessariamente un indirizzo URL con mimetype text/cache-manifest e estensione .appcache, quindi il server deve essere istruito per fornire questo file con l’opportuno mimetype. Vi è una precisazione importante da fare riguardo al primo punto della scaletta descritta. Come spiegato precedentemente la proceduta di caching dell’applicazione non viene eseguita solamente in caso venga richiamata una pagina con l’attributo manifest all’interno del tag <HTML>, ma anche in caso venga utilizzata una qualsiasi pagina contenuta in application cache ed elencata all’interno di un file manifest. Un esempio potrebbe essere questo: un’applicazione composta da un set di pagine contiene l’attributo manifest solamente nella pagina di ingresso. Al primo avvio dell’applicazione viene scaricato tutto il set di pagine. Ci si aspetterebbe che il file manifest venga verificato solamente quando si visita la pagina principale, dal momento che non è referenziato nelle altre pagine. Il file manifest viene invece ricontrollato ogni volta che una pagina richiesta viene fornita dall’application cache, rendendo quindi impossibile ottenere un singolo punto di controllo del file manifest. Il concetto del singolo punto di controllo può essere vista come il problema del punto d’aggiornamento, infatti se questo comportamento non venisse regolato con qualche stratagemma risulterebbe impossibile forzare l’applicazione ad aggiornarsi in un punto ben definito, adibito al cambiamento di versione e lontano dai passaggi critici. 29 3.4.2 Stati dell’applicationCache e relativi eventi L’oggetto window.applicationCache racchiude in sé lo stato e le funzioni di controllo dell’application cache. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 DOMApplicationCache oncached : null onchecking : null ondownloading : null onerror : null onnoupdate : null onobsolete : null onprogress : null onupdateready : null status : 1 __proto__ : DOMApplicationCache CHECKING : 2 DOWNLOADING : 3 IDLE : 1 OBSOLETE : 5 UNCACHED : 0 UPDATEREADY : 4 addEventListener : function addEventListener () { [ native code ] } constructor : function DOMApplicationCache () { [ native code ] } dispatchEvent : function dispatchEvent () { [ native code ] } removeEventListener : function removeEventListener () { [ native code ] } swapCache : function swapCache () { [ native code ] } update : function update () { [ native code ] } __proto__ : Object Listing 3.1: Struttura dell’oggetto window.applicationCache Gli elementi dalla linea 2 alla 9 sono gli eventi che vengono scatenati come discusso precedentemente, alla linea 10 abbiamo lo stato dell’application cache che può assumere i valori da 0 a 4 come specificato nelle linee dalla 12 alla 17. Nel momento in cui viene riconosciuta una variazione nel file manifest avviene, come precedentemente descritto, un download automatico della nuova versione dell’applicazione. In questo momento tutte le nuove risorse appena scaricata compongono un application cache più aggiornata, mentre la pagina continua a vivere con le risorse indicate nella application cache della versione precedente. In questo caso l’application cache è pronta per l’avanzamento di versione e si pone nello stato di UPDATEREADY. Intercettando questo cambiamento di stato è possibile invocare il comando swapCache() che forzando il cambiamento di application cache aggiorna la pagina corrente alla versione corretta. Una caratteristica che è importante sottolineare è l’impossibilità di gestire selettivamente il passaggio da una versione dell’applicazione ad una versione successiva. La possibilità fornita dall’oggetto che espone l’applicationCache è limitata alla decisione se passare istan- 30 taneamente alla nuova versione dell’applicazione o se attendere il prossimo ricaricamento o cambiamento di pagina. Non vi è attualmente alcun modo per interrompere il sistema di aggiornamento e riprenderlo in un secondo momento, magari dopo aver richiesto una conferma esplicita all’utente, e ciò può portare ai problemi di inconsistenza presentati in apertura al paragrafo. 3.4.3 Problematiche relative allo stato updateReady e inconsistenza dell’applicazione In caso l’applicazione sia in stato di updateReady, tutte le risorse a cui essa si riferisce sono prelevate dalla vecchia versione dell’application cache, a meno che non sia stato eseguito un comando di swapCache(). È importante fare questa precisazione, perché permette di stabilire lo stato di coerenza della pagina attualmente in esecuzione. È stato verificato durante l’esecuzione del progetto di stage il comportamento effettivo di questo meccanismo, ed esso non ha presentato differenze rispetto a quanto descritto. Il test è stato eseguito in questo modo: • Ci si è posizionati nella pagina principale, contenuta nell’application cache, • sono state apportare modifiche sia alla pagina in questione, sia a risorse esterne (in questo caso un file JavaScript in cui sono state aggiunte delle chiamate di debug), • È stata aggiornata la pagina, con conseguente download in background della nuova versione, • È stata verificata l’assenza di modifiche alla pagina principale e l’esecuzione dello script senza chiamate di debug, • È stato eseguito un refresh ed osservato l’aggiornamento della pagina e l’esecuzione dello script con le chiamate di debug. Questa verifica ci permette di fare un assunzione di coerenza sulla pagina in visione al momento dell’aggiornamento della cache. L’unico punto di incoerenza è rappresentato da chiamate asincrone ad altre pagine con trasferimento di dati. Immaginando che la pagina in questione sia alla versione X e pensi di dialogare con una pagina secondaria secondo un formato dati specificato e coerente nella versione X, nel momento in cui la pagina subisce un aggiornamento di application cache senza essere ricaricata, tenterebbe di dialogare con la pagina secondaria secondo il protocollo della versione X, ma la pagina secondaria avrebbe già eseguito l’aggiornamento di versione, magari sostituendo il protocollo di comunicazione della versione X con quello della versione X+1. Una versione più semplice del problema potrebbe prevedere nella pagina principale un link interno all’applicazione con dei parametri di tipo GET codificati tramite querystring, il cui schema viene cambiato dalla versione X alla X+1. Transitando dal link della index in versione X finiremmo per accedere al file di versione X+1, al cui interno magari uno script tenta di recuperare i parametri GET ipotizzando un diverso formato. 31 3.4.4 Interferenza con la cache standard L’utilizzo della memoria application cache, generata a partire dalle risorse elencate dal file manifest interagisce anche con il sistema di caching generico delle pagine. Purtroppo le risorse scaricate per essere inserite nell’application cache vengono memorizzate anche nella memoria cache del browser alla prima richiesta, generando una fastidiosa interferenza. Mentre il file manifest rappresenta un sistema chiaro di controllo della gestione delle pagine locali, la browser cache ragiona secondo logiche determinate dal produttore del browser, come presentato al paragrafo 2.1.1. L’interferenza tra i due sistemi rappresenta un problema quando avviene un avanzamento di versione (ipotizzando di nuovo un avanzamento dalla versione X alla versione X+1), poiché parte dei file richiesti dall’aggiornamento del manifest potrebbero essere recuperati dalla memoria cache locale e non dalla rete, producendo quindi uno stato di alta incoerenza tra le pagine dell’applicazione, che si troverebbero parte in una versione aggiornata del sistema e parte in un altra, senza nessuna possibilità di avanzare coerentemente alla versione più recente fino alla prossima modifica del file manifest. La divisione quindi non sarebbe più solamente tra parte statica e parte dinamica, ma anche tra parte statica soggetta ad aggiornamento controllato e parte statica non più aggiornabile. Per evitare questo comportamento riscontrato è necessario specificare delle politiche di caching esplicite nei singoli file, imponendo quindi che non vengano mai memorizzati nella cache del browser, ma solo nell’application cache. Comprendere questa particolarità può chiarire alcuni dubbi relativi a comportamenti incoerenti delle applicazioni offline. Un altra strategia potrebbe essere utilizzare i due sistemi in collaborazione, dividendo cioè l’applicazione in parte dinamica online, parte statica offline soggetta ad aggiornamenti e parte statica offline non soggetta ad aggiornamenti. Utilizzare un approccio simile può fornire un vantaggio relativo al tempo di aggiornamento, poiché purtroppo a qualsiasi modifica del file manifest corrisponde uno scaricamento di tutti i file dell’application cache, non solamente la sezione realmente modificata. In caso l’applicazione offline sia corposa, scaricarla completamente ad ogni aggiornamento potrebbe essere improponibile. Combinando la memoria cache con l’application cache sarebbe possibile recuperare i file soggetti a cambiamento dalla rete, ed i file che per loro natura non vengono modificati (magari i più pesanti quali immagini, video e quant’altro) dalla memoria cache, senza attendere tempi di scaricamento inopportuni. 32 3.4.5 Problematiche relative al caching delle pagine e all’asincronia degli eventi Durante il caching delle risorse elencate nel manifest vengono lanciati gli eventi descritti precedentemente. Teoricamente si potrebbero intercettare gli eventi per creare una barra di progressione che informi l’utente della percentuale di completamento del processo di caching. In realtà a causa della velocità di trasferimento e della gestione interna dei thread del browser gli eventi vengono scatenati nell’ordine corretto, ma solo una volta eseguite tutte le operazioni. Per rendersi conto di questo problema è stato utilizzato un programma con la funzione di filtro, in grado di rallentare la velocità di scaricamento a piacere, rendendo possibile uno studio accurato dei passaggi eseguiti dall’applicazione. Creando ad esempio una semplice serie di listener in questo modo è possibile intercettare gli eventi di progressione dell’application cache: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 applicationCache . addEventListener ( ’ cached ’ , cachingStop , false ) ; applicationCache . addEventListener ( ’ downloading ’ , cachingStart , false ) ; applicationCache . addEventListener ( ’ progress ’ , cachingProgress , false ) ; function cachingStart ( ev ) { console . log ( " caching iniziato " , ev ) ; } function cachingProgress ( ev ) { console . log ( " progresso " , ev ) ; } function cachingStop () { console . log ( " caching concluso " ) ; } Listing 3.2: esempio: cache listener L’evento progress ha due attributi che possono tornare utili per la progress bar: loaded che indica il numero progressivo della risorsa scaricata e total che indica ovviamente il totale delle risorse da scaricare. Avendo questi due valori è possibile fornire una progress bar non basata sul tempo d’esecuzione del download, ma sulla progressione delle risorse richieste. Purtroppo in ambiente mobile questi due attributi non sono attualmente disponibili, ma è comunque possibile eseguire un polling per verificare la completezza dello scaricamento. Vi è però un problema relativo al posizionamento della chiamata JavaScript che associa i listener agli eventi. Posizionando il codice JavaScript in fondo alla pagina tutte le chiamate di tipo console.log avvengono una volta terminati i download delle risorse, rendendo quindi inutile presentare una progressbar una volta completato il download. Posizionando lo script nel tag <HEAD> come prima istruzione otteniamo un miglioramento: una prima parte di risorse vengono scaricate (circa la metà secondo i test effettuati), do- 33 podiché vengono notificati gli eventi di checking, download e parte degli eventi progress. A questo punto può capitare nel caso migliore che ogni evento progress venga lanciato correttamente all’inizio di ogni download, oppure nel caso peggiore che tutti gli eventi progress vengano lanciati in blocco una volta terminati tutti i download, ottenendo una barra di progress che si completa in due singoli passaggi. Purtroppo il file manifest viene interpretato appena viene processato il relativo attributo nel tag <HTML>, e non esiste punto più vicino al tag <HTML> della prima linea del tag <HEAD>. In questo sistema risulta quindi impossibile intercettare l’evento download prima dell’effettivo avvio dello scaricamento. Nel paragrafo 3.4.8 viene proposto una soluzione ad un altro problema che fortunatamente risolve anche quanto appena descritto. 3.4.6 Svuotamento dell’application cache Nel momento in cui il browser rileva una nuova versione del file manifest, avvia le politiche descritte precedentemente per la gestione dell’application cache in cui è contenuta l’applicazione in esecuzione. Vi è però un caso particolare in cui l’application cache viene posta in stato obsolete, che sta ad indicare che l’application cache in utilizzo non è più necessaria e dev’essere quindi abbandonata e cancellata. Ciò avviene quando il file manifest viene rimosso dal server o viene reso inaccessibile. In questo caso l’applicazione interpreta questo cambiamento come un comando di rimozione dell’application cache che viene quindi pulita completamente. Questa soluzione può essere utile in caso ci si trovi nella condizione di dover sospendere tutte le funzionalità offline e ritornare ad una versione di applicazione perennemente connessa. 3.4.7 Effetto BlankPage È stato notato un comportamento insolito al primo ingresso in un’applicazione con manifest non ancora presente in memoria: un blocco del rendering della pagina (la composizione e visualizzazione della stessa), che rimane bianca fino al termine del download delle risorse elencate nel manifest. Essendo registrato nel file manifest l’intero elenco delle risorse necessarie all’applicazione è possibile ipotizzare che questo ritardo cresca proporzionalmente all’aumentare della dimensione dell’applicazione. Questo effetto non viene rilevato quando la pagina è già stata visitata almeno una volta, o quando essa è già stata memorizzata in cache o application cache e subisce un aggiornamento di manifest. Probabilmente ciò avviene perché al primo avvio non si ha ancora nessun dato relativo alla pagina da mostrare, ed il download delle risorse listate nel manifest assume la priorità assoluta, impedendo il download dei file relativi alla pagina. Aggiungendo questo comportamento all’asincronia delle chiamate descritta precedentemente si presenta uno scenario spesso inaccettabile per la maggior parte degli utenti: al primo 34 avvio dell’applicazione ci si presenta una schermata bianca immobile, e dopo aver atteso il tempo necessario a scaricare tutta l’applicazione viene disegnata parte della pagina e si viene trasportati istantaneamente in una pagina secondaria che ci informa della progressione (ultimata) del download dell’applicazione. Dal momento che spesso in ambiente web la prima impressione è quella decisiva non è il caso di presentare ad un utente un’applicazione che come prima azione sembra bloccare il browser del proprio dispositivo, mobile magari. La tolleranza degli utenti per un applicazione che fallisce al primo tentativo è estremamente bassa, e per certi contesti potrebbe rappresentare un’opzione inaccettabile. 3.4.8 Gestire l’effetto BlankPage Per evitare l’effetto BlankPage iniziale è possibile utilizzare uno stratagemma che è stato ideato dopo aver riscontrato questo fastidioso effetto. La pagina bianca viene presentata solamente quando si entra per la prima volta nella pagina iniziale dell’applicazione contenente il file manifest. Se la pagina non ha riferimenti interni al file manifest il suo tempo di caricamento e di rendering risulta normale, e quindi accettabile. Sfruttando le caratteristiche del file manifest, ed in particolare il fatto che ogni risorsa listata al suo interno provochi una verifica dello stesso quando viene utilizzata in seguito, possiamo predisporre un sistema che abbia un accesso rapido all’applicazione, presenti una barra di progressione ben temporizzata, e fornisca anche successivamente un punto di aggiornamento per il sistema. Una prima alternativa potrebbe essere rimuovere il file manifest dalla pagina di ingresso (forzando gli utenti ad accedere all’applicazione solamente attraverso tale pagina), valutare lo stato dell’oggetto window.applicationCache e se trovata vuota, richiedere una pagina secondaria contenente il manifest (definita pagina di setup), in cui avviare automaticamente i meccanismi di loading dell’application cache. In realtà questo sistema risolve il problema dell’effetto BlankPage nella pagina iniziale, ma lo sposta alla pagina di setup. Abbiamo quindi ottenuto una schermata iniziale che presenta l’applicazione seguita da una lunga schermata bianca. La soluzione risulta quindi ancora incompleta. Per migliorare la soluzione potrebbe essere utilizzato un iframe nascosto nella pagina di setup, creato dinamicamente via JavaScript, relativo ad una pagina bianca contenente solo il riferimento al manifest. L’iframe nascosto avrebbe un proprio oggetto window, il quale si occuperebbe di eseguire il caching senza bloccare l’esecuzione della pagina principale. Analizziamo in maniera più dettagliata il meccanismo: All’ingresso nella prima pagina dell’applicazione viene valutato lo stato dell’application cache. Se l’applicazione è già stata salvata nell’application cache quest’ultima si può trovare in due stati: IDLE che indica nessuna operazione in corso (quindi nessun aggiornamento eseguito) e UPDATEREADY che indica la possibilità di eseguire un avanzamento di versione dei file contenuti nell’application cache. 35 Se l’applicazione non è stata ancora salvata nell’application cache lo stato di quest’ultima sarà UNCHACHED. Rilevando questo stato è possibile spostarsi nella pagina di setup senza che siano ancora state avviate politiche di caching dell’applicazione. All’ingresso della pagina di setup (che presenterà una progress bar vuota e le altre voci utili all’inizializzazione dell’applicazione, come i passaggi che eseguirà l’applicazione per configurare il database) viene generato dinamicamente via JavaScript un iframe nascosto, avente come indirizzo un ipotetico file manifest_loader.html che contiene al suo interno solamente il riferimento al file manifest e gli script che legano gli eventi dell’application cache ai listener della finestra principale. In questo modo il download dell’applicazione viene avviato solamente quando viene generato l’iframe, e l’effetto blank page viene spostato in secondo piano, precisamente sull’iframe nascosto. In questo modo abbiamo evitato l’effetto BlankPage iniziale ed abbiamo inoltre ottenuto un evento di inizio download effettivamente precedente all’inizio del caching dell’applicazione, risolvendo quindi anche il problema dell’asincronia degli eventi descritta precedentemente. L’evento di avvio del download coincide con la creazione dell’iframe, e collegando tutti gli eventi di caching dell’iframe alla finestra principale possiamo visualizzare al suo interno la progressione delle risorse scaricate. 3.4.9 Differenza tra assenza di connessione e tolleranza ai guasti L’utilizzo di una sezione di fallback nel file manifest permette di creare alcune dinamiche di gestione basilari della connessione. Utilizzando correttamente le istruzioni di fallback è possibile specificare un diverso funzionamento per la versione offline e la versione online dell’applicazione. Una gestione orientata alle risorse però ha delle pesanti limitazioni, poiché si dovrebbe teoricamente duplicare l’intera sezione da fornire in modalità offline senza avere un effettivo potere decisionale a tempo d’esecuzione (dal punto di vista delle operazioni JavaScript). In parole povere un utilizzo avanzato della sezione fallback non permetterebbe una condivisione avanzata di codice tra la versione offline ed online, e renderebbe impossibile il passaggio istantaneo dall’una all’altra. Una gestione più avanzata della connessione passa attraverso l’oggetto window.navigator esposto dal browser. L’oggetto window.navigator, oltre ad offrire un attributo che indica se lo stato del browser e del sistema operativo sottostante è online o offline, viene notificato anche in caso avvengano una serie di eventi relativi alla cache. Gli eventi che è possibile intercettare vanno dall’aggiornamento della cache, al download dei file e alla rilevazione di errori. Per una descrizione degli eventi nel dettaglio é possibile consultare la specifica degli eventi di caching delle risorse nell’application cache[4]. 36 Un esempio di gestione avanzata della connessione può essere il seguente. Il codice riportato simula il funzionamento di un sistema di notifica in stile Twitter. Viene richiesto lo stato all’utente, che verrà poi inviato al server in caso l’applicazione sia in stato connesso, o verrà memorizzato localmente in caso vi sia assenza di connessione. Vengono poi legate delle funzioni alle variazioni di stato della connessione, realizzando quindi un sistema che rileva la possibilità di dialogo con il server ed agisce di conseguenza inviando gli stati rimasti in locale. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function w h at Is Y ou rC u rr e nt St a tu s () { var status = window . prompt ( " Qual ’e ’ il tuo stato attuale ? " ) ; if (! status ) return ; if ( navigator . onLine ) { sendToServer ( status ) ; } else { saveStatusLocally ( status ) ; } } function sendLocalStatus () { var status = readStatus () ; if ( status ) { sendToServer ( status ) ; window . localStorage . removeItem ( " status " ) ; } } window . addEventListener ( " load " , function () { if ( navigator . onLine ) { sendLocalStatus () ; } } , true ) ; window . addEventListener ( " online " , function () { sendLocalStatus () ; } , true ) ; window . addEventListener ( " offline " , function () { alert ( " Sei entrato in modalita ’ offline . I tuoi aggiornamenti di stato verranno inviati quando tornerai online . " ) ; } , true ) ; Listing 3.3: esempio di gestione della connessione Questo sistema sembrerebbe funzionare bene concettualmente, ma si appoggia allo stato logico della presenza o assenza di connessione fornito dal browser. Purtroppo in base ai test effettuati esso non sempre è concorde con il reale stato del sistema. Togliere il cavo di rete da un computer non provoca istantaneamente un aggiornamento dello stato logico. Analogamente in ambiente mobile un assenza di connessione determinata dal passaggio del dispositivo in modalità offline o aeroplano comporta i relativi aggiornamenti dello stato logico dell’applicazione, mentre un comune passaggio in galleria o perdita di 37 segnale non vengono rilevati, provocando quindi un completamento incoerente. Ci si può quindi trovare nel caso in cui un applicazione ben organizzata per funzionare in modalità offline si trovi poi a non sfruttare mai tale componente a causa di un errore si valutazione dello stato del dispositivo. Vi è un altro punto che vale decisamente la pena di sottolineare: la presenza di connessione del server. Si è finora valutata la presenza di connessione solamente tra il dispositivo e la rete, assumendo che ciò bastasse a garantire una comunicazione con la componente offline dell’applicazione. Questa visione è incompleta, poiché potrebbe venire a verificarsi la situazione in cui sia il server a non essere raggiungibile dalla rete, e non il dispositivo client. Con i moderni sistemi di backup si potrebbe tranquillamente affermare che con un minimo investimento sarebbe possibile mantenere un server online in ogni situazione, ma estendendo questa considerazione all’ambiente aziendale ci si rende conto di come l’impossibilità di raggiungere il server da qualsiasi postazione per ragioni di sicurezza sia in realtà una situazione piuttosto comune. L’ipotetica applicazione aziendale, progettata per funzionare nel browser del portatile del dipendente con supporto offline, potrebbe avere come scopo la generazione di grafici e report nel server , e la loro memorizzazione locale in sola lettura per poter essere presentati in altre sedi o altre aziende senza necessariamente aprire una connessione dal proprio portatile al server aziendale (anche per motivi di sicurezza). Accedendo all’applicazione ci si troverebbe nello stato problematico in cui il dispositivo può contare su una connessione alla rete, ma non riesce a raggiungere la controparte remota dell’applicazione. In questo caso la componente offline potrebbe essere completamente ignorata, rendendo di fatto impossibile la presentazione. Queste considerazioni rendono chiaramente necessaria una valutazione della connessione differente, non basata sugli stati logici del sistema, ma sull’effettiva presenza di un collegamento stabile tra applicazione client e controparte server. 3.4.10 Implementare il rilevamento della connessione Per realizzare delle vere e proprie funzionalità offline con tolleranza ai guasti è possibile muoversi in diversi modi, tra cui la sostituzione dell’oggetto window.navigator.onLine con un indicatore più “intelligente” e l’utilizzo di chiamate asincrone con relative funzioni di callback eseguite in base al contenuto ottenuto. In entrambi casi la sezione fallback del manifest viene ignorata, ed è quindi necessario utilizzarla solamente per scopi basilari e non per elaborate politiche di gestione della connessione. Uno scopo basilare adatto alle attuali politiche di utilizzo della sezione fallback potrebbe servire per diminuire la quantità di risorse necessarie per la versione offline. In caso si decida di implementare le versioni offline ed online di un applicazione nella stessa interfaccia, sarebbe possibile decidere di ridurre le risorse da memorizzare in application cache per mantenere libero più spazio possibile. Si potrebbero prevedere delle risorse alternative, più leggere, da utilizzare nell’applicazione offline. Le immagini potrebbero essere sostituite con risorse più leggere ed i video potrebbero a loro volta essere sostituiti con immagini. 38 La prima soluzione proposta al problema della reale presenza di connessione prevede la decisione delle risorse da utilizzare non più basandosi sull’oggetto window.navigator.onLine, ma su di un test pratico di connessione. Un iniziale chiamata a una delle risorse del server deve quindi occuparsi di testare effettivamente la presenza di un collegamento. L’inserimento di un test simile non è tutto sommato un operazione cosı̀ invasiva, ma comporta l’aggiunta delle chiamate di prova prima della richiesta di risorse al server. Questa tecnica può essere utilizzata per la richiesta di risorse in modalità sincrona come ad esempio i link: prima di spostarsi in una pagina memorizzata nel server e listata nella sezione network del file manifest è possibile testare l’effettiva connessione e successivamente ridirezionare la chiamata verso una risorsa memorizzata nell’application cache. Questo sistema in sostanza simula il funzionamento della sezione fallback, ma testando la connessione esplicitamente prima della richiesta di una risorsa. Bisogna però ricordare le considerazioni espresse nel paragrafo 3.3.3 riguardo al costo delle connessioni in ambiente mobile. Duplicare tutti i tentativi di connessione potrebbe essere un opzione non ammissibile. Nel caso il sistema di navigazione tra le pagine incorpori già la possibilità di utilizzare chiamate asincrone, ad esempio per l’invio dei dati di un modulo ad una pagina secondaria (come nel framework per sviluppo web mobile JQueryMobile), non è necessario testare in anticipo la connessione, ma semplicemente fornire un adeguata funzione di callback da attivare in caso di fallimento. Lo scenario più facile da immaginare è proprio l’invio di dati tramite una chiamata di tipo POST o GET ad una pagina server. Dovendo recuperare i parametri forniti per elaborarli lato server, si ipotizza che la pagina da contattare sia elencata nella sezione NETWORK del file manifest. In questo caso non è necessario testare in anticipo la connessione, è sufficiente tentare l’invio dei dati, ed in caso non sia possibile contattare la pagina necessaria, avviare una seconda chiamata asincrona verso una pagina in application cache che si occupi di gestire il salvataggio dei dati in una memoria temporanea. In questo modo si divide in tre fasi il processo descritto precedentemente. L’utente genera il contenuto utilizzando la componente locale offline, la quale tenta di inviarlo alla componente di memorizzazione online, ed in caso di fallimento ripiega comunicando i dati inviati ad una componente di memorizzazione offline sostitutiva. Ragionando in termini di sincronizzazione, in realtà coinvolgere sempre anche la componente di memorizzazione offline sostitutiva potrebbe rappresentare un vantaggio. Procedendo in questo modo sarebbe possibile minimizzare le differenze tra la versione dei dati remota e quella locale, ottimizzando quindi le politiche di sincronizzazione. 39 3.5 Considerazioni conclusive Le considerazioni espresse finora sono decisamente generiche, e devono essere contestualizzate al caso di utilizzo. Generalmente per quanto verificato durante lo stage, occorre valutare tutte le possibilità e combinare al meglio le alternative offerte per creare delle soluzioni personalizzate per il problema, valutandone le caratteristiche e gli obbiettivi. Per la progettazione di un’applicazione offline realmente funzionale non è possibile utilizzare un approccio classico ed introdurre una piccola variante pensando di guadagnare istantaneamente la libertà di utilizzo in qualsiasi ambiente. Realizzare un applicazione offline completa significa abbandonare completamente la progettazione web, e calarsi in un contesto di progettazione di applicazioni client-server, valutando costantemente tutte le limitazioni imposte dal browser, che diventa in sostanza un contenitore di applicazioni, una macchina virtuale in cui eseguire le proprie applicazioni. La progettazione di un’applicazione offline è più complessa di una normale progettazione di applicazioni web o desktop, perché prevede la combinazione delle due realtà, cercando di trarre i vantaggi da entrambe e minimizzando le problematiche. Dal momento che le applicazioni web hanno delle caratteristiche di portabilità decisamente convincenti, risolvere il problema dell’assenza di connessione permetterebbe di estendere il funzionamento di un’applicazione non solo a molteplici dispositivi, ma anche a molteplici scenari d’uso. È necessario però valutare ampiamente a che livello di dettaglio scendere con le funzionalità offline, poiché mano a mano che si amplia il set di funzioni supportate ci si espone sempre di più ad una possibile inconsistenza dei dati (tra più utenti, tra più dispositivi, e anche tra diverse versioni della stessa applicazione) ed in alcuni casi perfino dell’applicazione in sé (aggiornamenti parziali o corrotti). 40 4 Persistenza dei dati Come introdotto nel capitolo 2.2 esistono vari sistemi di memorizzazione che permettono la condivisione di dati tra le pagine web. Questi sistemi sono stati progettati in periodi differenti, ed hanno seguito lo sviluppo dell’ambiente web fornendo gli strumenti più adeguati ai problemi del momento. Possiamo dividere i sistemi di persistenza in due grandi categorie, i sistemi strutturati ed i sistemi non strutturati. Per sistema non strutturato si intende un sistema in grado di memorizzare solamente dati semplici, generalmente stringhe, memorizzate nella forma chiave-valore per consentirne la selezione in un secondo momento. Per sistema strutturato si intendono invece i sistemi in grado di memorizzare dati complessi, con campi dato in grado di memorizzare per ogni oggetto i relativi attributi. Sistemi di memorizzazione di questo tipo offrono spesso le funzionalità e le potenzialità di veri e propri database. 4.1 Sistemi di persistenza non strutturati I sistemi di persistenza non strutturati sono stati la prima soluzione fornita al problema dell’assenza di stato tra le pagine web. Si differenziano principalmente per la loro posizione (lato client o lato server) e per livello di sicurezza. 4.1.1 Cookies I cookies rappresentano la forma più basilare di persistenza. Sono costituiti da piccoli file testuali memorizzati nelle cartelle del browser, contenente un elenco di coppie chiave-valore che possono essere scritte e lette dal server tramite dei comandi inseriti nell’intestazione delle pagine web. Quando viene generato un cookie gli viene assegnato un tempo di vita, oltre il quale tale file non dev’essere più considerato attendibile, e viene quindi rimosso dal browser. Il fatto che il contenuto del cookie sia memorizzato in un file testuale rappresenta un problema non indifferente per la siscurezza del dato, poichè potrebbe essere alterato senza difficoltà sia dall’utente che da altri programmi. I cookies non sono quindi una forma di persistenza dati realmente utilizzabile per memorizzare dati importanti quali password, account e simili. Possono essere utilizzati per memorizzare impostazioni di poco conto, come le preferenze dell’utente per quanto riguarda lingua, contrasto e dimensione di carattere della pagina. In caso questi dati vengano cancellati o corrotti non rappresenterebbero una gran perdita per l’utente, che potrebbe reimpostarli in pochi click. 41 4.1.2 Sessioni Le sessioni sono in grado di memorizzare coppie di valori in una posizione differente: il server web. Questi dati sono quindi accessibili alla componente software remota, e possono essere utilizzati agevolmente per la composizione delle pagine web secondo specifiche impostazioni relative all’utente. Nel momento in cui il linguaggio lato server lo richiede, può essere creata una sessione per l’utente in grado di memorizzarne i dati. Tale sessione è unica per ogni utente, e rappresenta un sistema di sicurezza molto efficace, perché rende possibile l’implementazione di politiche di login, carrelli, liste dei desideri e simili. Se non esistessero le sessioni il server potrebbe essere ingannato facilmente, rendendo insicura ogni operazione via web che coinvolga meccanismi di riconoscimento dell’utente. Il sistema delle sessioni rappresenta attualmente l’unico sistema di memorizzazione lato server che non richieda l’utilizzo di un database. Questo sistema non è esente da vulnerabilità, poiché anche esso si basa in parte sui cookies, ma prendendo le giuste contromisure è possibile assicurarsi un certo margine di sicurezza. Ovviamente il meccanismo delle sessioni è adatto ad una memorizzazione temporanea (ristretta appunto alla sessione di navigazione), non certo ad una persistenza duratura o che coinvolga una quantità elevata di informazioni. Per tutte queste situazioni vengono utilizzati i database remoti residenti nel server in cui vengono ospitate le applicazioni o ad esse collegati. 4.1.3 LocalStorage LocalStorage è stato introdotto nell’ambito dello standard HTML5 e rappresenta un evoluzione del concetto di dato locale non strutturato. Questo sistema è accessibile solamente via JavaScript, e permette di memorizzare dati nel browser con una semplicità disarmante. In una chiamata è possibile leggere o scrivere nell’archivio, rendendo quindi molto agevole l’utilizzo di questo sistema all’interno di applicazioni web. La memorizzazione avviene all’interno di un database contenuto nel browser, in una sezione accessibile solamente da pagine del determinato dominio. Questa accortezza limita il rischio di intrusione da parte di script malevoli provenienti da altri domini, ma non assicura l’integrità dei dati, in quanto un utente con capacità avanzate è in grado di arrivare a modificare i dati archiviati in un paio di passaggi. LocalStorage risulta quindi perfetto per raccogliere i dati provenienti da moduli di inserimento distribuiti su più pagine, che verranno poi validati e inviati al server in un unico momento. LocalStorage può essere utilizzato anche per memorizzare temporaneamente password e altri dati sensibili, sapendo che un eventuale corruzione dei dati può essere imputata ad una manipolazione eseguita dall’utente. Questa assunzione può essere considerata valida solamente in caso l’applicazione sia progettata in modo da non consentire l’inclusione di codice JavaScript estraneo al suo interno. Se fosse possibile inserire codice JavaScript malevolo nelle pagine dell’applicazione, tale codice dannoso verrebbe eseguito all’interno del dominio dell’applicazione, ed avrebbe quindi libero accesso ai dati contenuti nel LocalStorage. 42 4.2 Sistemi di persistenza strutturati I sistemi di persistenza strutturati locali sono stati introdotti nell’ambito dello standard HTML5, e permettono allo sviluppatore la memorizzazione di oggetti complessi, contenenti vari attributi relazionati ad una chiave. Attualmente, come per i database remoti, esistono due grandi categorie: i database relazionali ed i database non relazionali o noSQL. A differenza di un sistema di persistenza non strutturato, che viene utilizzato principalmente come area più o meno temporanea per immagazzinare impostazioni, variabili e quant’altro, in un sistema di persistenza strutturato vengono mantenuti tutti i dati necessari ad un funzionamento più avanzato dell’applicazione. Riprendendo l’esempio dei moduli distribuiti su più pagine fatto per LocalStorage, possiamo immaginare che il riepilogo di tutti gli ordini eseguiti e convalidati dal server venga mantenuto in un database locale contenente una serie di record rappresentanti gli ordini, con campi quali l’importo, i prodotti scelti, la data di consegna e via dicendo. Mentre un sistema di memorizzazione non strutturato non fornisce la possibilità di eseguire ricerche, perché comprende solamente il rapporto tra chiave e valore, un sistema di memorizzazione strutturato consente di impostare specifici parametri di ricerca, rendendo quindi possibile l’estrazione di un determinato sottoinsieme di dati, accomunati da alcune caratteristiche. 4.2.1 Database remoti I database remoti sono presenti dagli albori del web. In un certo senso l’intero ambiente web nasce per fornire un accesso pubblico a dati contenuti in sistemi di memorizzazione. I database remoti si dividono in due tipologie: relazionali e non relazionali. I database relazionali si basano sul concetto di entità, e rappresentano i legami tra i dati inseriti come relazioni tra le entità definite. In un database relazionale viene definito inizialmente uno schema, o struttura, che determina per ogni entità le sue caratteristiche, il numero ed il tipo degli attributi, i vincoli che deve soddisfare per essere ritenuta integra e le relazioni con altre entità. Tutti i dati che vengono inseriti all’interno di un database di questo tipo devono rispecchiare la struttura definita per le varie entità, garantendo l’integrità generale del database. In un database non relazionale invece viene a perdere d’importanza la rigidità dello schema, che è si presente, ma in una versione molto più flessibile. I dati inseriti devono rispecchiare un sottoinsieme minimo di attributi, necessari al funzionamento basilare del sistema di archiviazione. Tali attributi sono principalmente indici necessari per rendere più efficienti le ricerche, e mantenere quindi la competitività con i tempi di accesso dei database relazionali. Un dato inserito in un database di questo tipo non viene considerato propriamente un record, in quanto non segue rigidamente lo schema dell’entità. Il concetto stesso di entità viene a mancare, in quanto introducendo la possibilità di variare il numero e tipo di attributi non è più possibile asserire fermamente che un dato oggetto sia un istanza di una certa entità. Viene quindi introdotto il concetto di collezione, inteso come un insieme di elementi con alcune caratteristiche in comune. Per comprendere meglio la differenza tra questi due concetti si veda un entità come uno 43 schema preciso, che definisce dei parametri per ogni oggetto che possono assumere differenti valori. Se un certo oggetto non possiede certi parametri o ne possiede altri non elencati nello schema, esso non è un istanza della tale entità. In una collezione invece viene definito un sottoinsieme minimo di caratteristiche comuni, necessarie al funzionamento ottimale del database. Se un oggetto soddisfa le proprietà di base esso può far parte della collezione, ma in realtà le caratteristiche salienti del tale oggetto potrebbero non essere contemplate nel sottoinsieme di parametri definito dalla collezione. I database non relazionali hanno conosciuto un periodo di grande interesse con la diffusione di social network e motori di ricerca. Devono il loro successo alla rapidità in cui forniscono l’informazione richiesta, e alla possibilità di ottimizzare la dimensione dell’archivio (grazie allo schema flessibile) e alla possibilità di distribuire i propri dati su più server gestendo agevolmente l’integrità dei dati. Nell’ambiente web entrambe le categorie sono ben rappresentate da soluzioni molto semplici da implementare. Generalmente i linguaggi di programmazione lato server utilizzano delle funzioni predisposte dai vari DBMS (Database Management System) per interfacciarsi con i database e impartire i comandi necessari all’utilizzo dei dati in essi contenuti. 4.2.2 WebSQL WebSQL è un progetto nato all’interno dello standard HTML5 con l’obbiettivo di fornire un database relazionale all’interno del browser. L’implementazione di questo sistema è molto semplice, dato che la maggior parte dei browser utilizzano al loro interno un database relazionale per memorizzare i dati necessari per le proprie funzionalità, quali la memorizzazione dei preferiti, delle password e altri dati utente e dei valori di autocompletamento dei moduli. Fornire un database relazionale si può quindi ridurre ad implementare delle funzioni che regolino l’accesso ad un area del database sottostante appositamente predisposta per il dominio dell’applicazione. L’implementazione attuale si basa sul noto database SQLite, presente in numerose applicazioni e sistemi integrati. Tale database è accessibile via JavaScript tramite opportune funzioni asincrone, che consentono di far eseguire codice SQL all’interno della propria porzione di database. SQLite è un database che offre buone funzionalità, essendo comunque molto semplice. Per ottenere rapidità di calcolo e semplicità risulta però inefficace nel gestire l’integrità referenziale dei dati, non permettendo l’impostazione di vincoli stringenti tra due o più entità. Risulta quindi un database dalle ottime capacità, se si tengono in considerazione i suoi limiti. 44 4.2.3 IndexedDB IndexedDB è la proposta non relazionale fornita in sostituzione a WebSQL. In questo caso non è stato sufficiente includere un database non relazionale all’interno del browser ed esporne le funzionalità, si è invece partiti dal basso, costruendo il DBMS in JavaScript, ottenendo quindi un sistema ottimizzato per il funzionamento all’interno del browser. Attualmente IndexedDB è l’unico database locale ufficialmente supportato dal W3C, in seguito a un lungo dibattito durato mesi che ha visto WebSQL perdere importanza fino al suo completo abbandono ufficiale. Essendo progettato appositamente per l’interazione con JavaScript, questo sistema di memorizzazione offre delle funzionalità di manipolazione molto semplici, e guadagna in velocità di esecuzione demandando allo sviluppatore il compito di mettere in relazione tra di loro gli elementi contenuti all’interno delle collection. 4.3 Studio dei database locali 4.3.1 Contesto d’utilizzo di un database locale Un applicazione web potrebbe trarre un grande vantaggio dall’utilizzo dei database locali. Mantenere a portata di funzione tutta una serie di dati relativi all’applicazione web potrebbe rendere più veloce il loro utilizzo, ed ottimizzare le richieste verso il server. Considerando poi le applicazioni offline, l’utilizzo di database locali diventa quasi un prerequisito, senza il quale non sarebbe possibile creare applicazioni non banali. Un database locale può fornire all’applicazione lo spazio per memorizzare i dati ad essa relativi, ma richiede una distinzione aggiuntiva tra i dati che possono essere memorizzati localmente e quelli che invece non possono essere memorizzati in posizioni differenti dal server, spesso per problemi di sicurezza. Prima di utilizzare un database locale per la memorizzazione di informazioni occorre comprendere appieno la variazione al concetto di affidabilità del dato memorizzato. Utenti con capacità avanzate possono modificare i dati memorizzati senza troppe difficoltà, rendendo quindi insicuro il loro utilizzo in alcuni contesti. Per certe tipologie di dati non è possibile accettare un livello di affidabilità cosı̀ basso. Un esempio per chiarire questo concetto può essere dato da un ipotetico applicativo aziendale, in cui in base al ruolo di un determinato utente esso ha accesso alla modifica dei dati aziendali. Un utente malintenzionato potrebbe modificare senza problemi il proprio ruolo, garantendosi l’accesso autorizzato ai dati aziendali, locali e remoti. Questo esempio rende evidente come, approcciandosi ai database locali, può essere necessario mettere in discussione le buone intenzioni dell’utente dell’applicazione. 45 4.3.2 Differenze tra IndexedDB e WebSQL I due database locali presentati offrono funzionalità molto differenti, che derivano da limiti e caratteristiche ben specifici. Per comprendere quale database utilizzare per il proprio progetto occorre comprendere la natura dei due DBMS, cercando di individuare quali siano le caratteristiche che più si addicono al progetto da sviluppare. Durante il dibattito tra i due sistemi di memorizzazione sono stati prodotti molti esempi di utilizzo comparato, reperibili facilmente in rete [5]. Tali esempi analizzano sia la creazione dei database, delle collezioni o entità, sia il loro utilizzo per quanto riguarda la selezione e la modifica dei dati. Di seguito viene presentato un esempio di utilizzo di un database WebSQL e IndexedDB. Lo scopo del confronto è mettere in mostra i passaggi necessari allo sviluppatore per interrogare il database ed ottenere i dati richiesti in un formato utile per essere utilizzati nell’applicazione. La query in esempio si pone l’obbiettivo di ottenere l’elenco dei bambini che hanno mangiato caramelle e la quantità. WebSQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var db = window . openDatabase ( " CandyDB " , " 1 " , " My candy store database " , 1024) ; db . readTransaction ( function ( tx ) { tx . executeSql ( " SELECT name , COUNT ( candySales . kidId ) " + " FROM kids " + " LEFT JOIN candySales " + " ON kids . id = candySales . kidId " + " GROUP BY kids . id ; " , function ( tx , results ) { var display = document . getElementById ( " purchaseList " ) ; var rows = results . rows ; for ( var index = 0; index < rows . length ; index ++) { var item = rows . item ( index ) ; display . textContent += " , " + item . name + " bought " + item . count + " pieces " ; } }) ; }) ; Listing 4.1: esempio di utilizzo WebSQL 46 IndexedDB 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 IndexedDB candyEaters = []; function displayCandyEaters ( event ) { var display = document . getElementById ( " purchaseList " ) ; for ( var i in candyEaters ) { display . textContent += " , " + candyEaters [ i ]. name + " bought " + candyEaters [ i ]. count + " pieces " ; } }; var request = window . indexedDB . open ( " CandyDB " , " My candy store database " ) ; request . onsuccess = function ( event ) { var db = event . result ; var transaction = db . transaction ([ " kids " , " candySales " ]) ; transaction . oncomplete = displayCandyEaters ; var kidCursor ; var saleCursor ; var salesLoaded = false ; var count ; var kidsStore = transaction . objectStore ( " kids " ) ; kidsStore . openCursor () . onsuccess = function ( event ) { kidCursor = event . result ; count = 0; attemptWalk () ; } var salesStore = transaction . objectStore ( " candySales " ) ; var kidIndex = salesStore . index ( " kidId " ) ; kidIndex . openObjectCursor () . onsuccess = function ( event ) { saleCursor = event . result ; salesLoaded = true ; attemptWalk () ; } function attemptWalk () { if (! kidCursor || ! salesLoaded ) return ; if ( saleCursor && kidCursor . value . id == saleCursor . kidId ) { count ++; saleCursor . continue () ; } else { candyEaters . push ({ name : kidCursor . value . name , count : count }) ; kidCursor . continue () ; } } } Listing 4.2: esempio di utilizzo IndexedDB 47 Esaminando i due listati proposti si può facilmente notare una certa complessità nel codice di IndexedDB. Mentre WebSQL permette di definire i parametri di ricerca e demanda al database la selezione dei dati realmente utili, con IndexedDB non è possibile effettuare questo tipo di operazione, e lo sviluppatore deve prendersi la responsabilità di effettuare di persona la selezione dei dati da utilizzare, esprimendo il tutto tramite chiamate JavaScript. Da questa considerazione possiamo comprendere per quali sviluppatori sono stati progettati i due DBMS: mentre uno sviluppatore lato server si potrebbe trovare a proprio agio con il linguaggio SQL, avendo invece difficoltà ad esprimere i singoli vincoli in JavaScript, uno sviluppatore lato client potrebbe trovarsi molto più agevolato nell’utilizzo del codice JavaScript piuttosto che nell’utilizzo del linguaggio SQL. Una differenza sostanziale che attualmente non viene considerata, ma che potrebbe diventare molto importante in futuro, riguarda la granularità di blocco dei due database. Sono già in via di sviluppo i WebWorker, entità molto simili ai thread della programmazione concorrente. Questi WebWorker offrirebbero la possibilità di avere più processi JavaScript in esecuzione per ogni pagina, portando dunque tutti i vantaggi della programmazione concorrente all’interno del browser. Lavorare in un contesto multithread all’interno delle pagine rende necessario limitare gli accessi concorrenti al database. Nel caso in cui diversi thread della pagina accedano concorrentemente all’archivio dati potrebbero essere utilizzati dei meccanismi di sincronizzazione, dal momento che i thread condividerebbero un area di memoria comune fornita dalla pagina iniziale. Nel caso in cui siano diverse pagine ad accedere concorrentemente al database, senza condividere memoria tra di loro, un blocco del database potrebbe rivelarsi necessario per evitare gravi problematiche di inconsistenza. WebSQL non fornisce una politica di blocco sufficientemente granulare, mentre uno degli obbiettivi di IndexedDB è quello di fornire meccanismi di blocco con una granularità più avanzata, in grado di bloccare solamente il record attualmente in uso, e garantire quindi l’utilizzo corretto dei dati. 4.3.3 IndexedDB nel dettaglio Dal momento che i database non relazionali sono meno conosciuti a livello tecnico rispetto ai normali database vale la pena addentrarsi maggiormente nella descrizione, aggiungendo alcuni dettagli utili a comprendere meglio gli esempi. Ogni database non relazionale è identificato da un nome, e contiene al suo interno uno o più Object Stores. Ogni Object Stores rappresenta una collezione di oggetti, e definisce almeno una proprietà chiave che ogni oggetto memorizzato deve contenere, implicitamente o esplicitamente. Ad esempio nel caso di un agenda telefonica ogni contatto dovrebbe avere un proprio identificativo, questo identificativo è la proprietà che accomuna e indicizza tutti gli oggetti memorizzati. Gli oggetti possono essere strutturati, ma non sono vincolati ad uno schema definito rigidamente. Ogni oggetto può presentare delle proprietà aggiuntive, e se queste sono frequenti tra gli oggetti della collezione è possibile includerli nella definizione della collezione. L’O- 48 bject Stores può utilizzare queste proprietà per definire degli indici in grado di consentire una ricerca ed un filtraggio più rapidi. Ritornando all’esempio dell’agenda è possibile immaginare che alcuni contatti abbiano informazioni aggiuntive che altri contatti non hanno, ad esempio indirizzo, email, eccetera. Utilizzando questi indici aggiuntivi è possibile selezionare dei sottoinsiemi della collezione, oppure esprimere all’interno delle ricerche valori particolari per questi indici, ottenendo dei risultati soddisfacenti in termini di tempi di calcolo. Ogni database IndexedDB è abbinato ad un controllo di versione. Il versionamento del database è lasciato allo sviluppatore, che si deve occupare via JavaScript di aggiornare il numero di versione quando vengono applicate modifiche. L’utilizzo di tale indice di versione permette il confronto di copie dello stesso database al fine di capire rapidamente quale sia la più recente. Ogni operazione, dall’apertura, alla creazione, all’interrogazione e alla modifica di dati è realizzata in maniera asincrona. In caso si verifichi un errore è possibile rilevare gli eventi di errore ed invocare un ripristino dell’ultimo stato consistente del database. La disponibilità di funzioni di controllo asincrone, combinata con l’espressività di JavaScript, consente allo sviluppatore di gestire completamente ogni evento relativo al database, che con un normale DBMS relazionale rimarrebbe invece racchiuso all’interno del sistema di gestione interno. Questa possibilità apre la strada ad un utilizzo più avanzato dei dati memorizzati, completamente integrato con il linguaggio di manipolazione dei dati stessi (JavaScript in questo caso). 4.3.4 La scelta di supportare solamente IndexedDB: motivazioni e critiche Attualmente alcune importanti realtà all’interno del W3C (principalmente Mozilla e Windows) hanno espresso pesanti critiche a WebSQL (proposto e realizzato inizialmente da Apple, adottato da Google per il suo browser Chrome ed il browser di Android), che hanno portato all’adozione di IndexedDB (proposto da Mozilla). Alcune delle principali obbiezioni all’utilizzo di WebSql: • Il linguaggio SQL non è realmente uno standard, vi sono differenze tra gli standard ufficiali, ed ogni implementazione attuale offre delle estensioni non standard per sopperire alle mancanze del linguaggio; • SQL è un linguaggio ad alto livello. Se un programmatore volesse semplicemente inserire e rimuovere dati in una tabella, non sarebbe necessario forzarlo ad utilizzare un linguaggio di programmazione complesso; • WebSQL si basa su SQLite, ma SQLite non implementa uno standard specifico, e sarebbe difficile creare una versione di SQL standard per il web, dato che non si è ancora riusciti a crearne una standard per gli altri ambienti della programmazione; 49 • SQL è un sistema completamente differente, non collegato ai linguaggi in cui viene normalmente integrato; • È facile per programmatori inesperti inserire difetti dal punto di vista della sicurezza nelle espressioni SQL; • È facile per programmatori inesperti sviluppare query oltremodo complesse, tali da rendere impossibile al sistema la risoluzione delle stesse in un lasso di tempo ragionevole; • Sebbene sia possibile scrivere SQL in grado di funzionare con DBMS quali Oracle, PostgreSQL e MySQL, è veramente difficile scrivere del codice per un utilizzo avanzato senza conoscere le caratteristiche ed i difetti dei tre sistemi. In queste critiche si può comprendere come la figura principale dello sviluppatore di applicazioni web venga intesa come evoluzione del normale sviluppatore web, e non come un evoluzione dello sviluppatore di applicazioni standard. Si assume in vari ambienti di trovarsi di fronte a sviluppatori le cui nozioni di programmazione siano piuttosto basilari, mentre le potenzialità delle tecnologie in via di sviluppo potrebbero portare verso l’ambiente web anche sviluppatori esperti, con un bagaglio di conoscenze non indifferente. È importante notare come WebSQL sia supportato sia dai dispositivi Android sia da quelli Apple, garantendosi quindi una posizione predominante nel settore mobile, ultimamente in rapida espansione. 4.3.5 Scegliere il proprio database offline Da un analisi iniziale ci si potrebbe chiedere perché non mantenere entrambe le possibilità, fornendo quindi agli sviluppatori le due alternative, ottenendo quindi il massimo della copertura dei casi d’applicazione. L’importanza di questa scelta secondo Mozilla e Microsoft è legata allo sviluppo futuro delle applicazioni. Pian piano si sta assistendo alla conversione quasi completa di una serie di applicazioni desktop in applicazioni web. Avvertendo la pressione di una passaggio cosı̀ radicale, i maggiori componenti del W3C vorrebbero innanzitutto avere le idee chiare sulle basi da utilizzare, per evitare di dover modificare i propri progetti in corso d’opera. Pretendere certezze dal mondo del web, nato e cresciuto evolvendosi per conto proprio, un ambiente in cui gli standard spesso hanno inseguito le implementazioni, è forse chiedere troppo. Questa pretesa però può far riflettere su come stia cambiando anche la prospettiva di utilizzo del web in un ottica più produttiva. Il potenziamento dei motori JavaScript, la diffusione di una miriade di librerie di terze parti altamente innovative, ed il successo di applicazioni web largamente diffuse stanno gradualmente modificando l’idea del web visto come strumento di diffusione di informazioni per elevarlo al ruolo di vero e proprio ambiente di lavoro, ricco di alternative ed in costante movimento. 4.3.6 Variazioni al concetto di sicurezza del dato nel database locale Come già esposto precedentemente, quando il DBMS è residente in un server si trova in un ambiente relativamente sicuro, in cui gli accessi sono controllati, ed è possibile vinco- 50 lare logicamente le operazioni da eseguire sui dati in base al ruolo dell’utente, sia a livello di database, sia a livello di linguaggio di manipolazione. Una sicurezza del genere viene completamente scardinata nel momento in cui l’utente ha accesso fisicamente ai dati memorizzati nel database. Al di là del rischio di violazione dei dati locali, vi è il problema dell’esposizione completa dello schema e dei dati del database all’utente. In caso il database locale fosse una porzione esatta del database remoto e non una vista adattata, verrebbero esposti i dettagli implementativi dello schema all’utente, il quale se malintenzionato potrebbe sfruttarli per forzare falle o alterare i dati presenti mantenendo comunque una parvenza di integrità. Un terzo punto di riflessione riguarda infine la possibilità di accesso a dati inadeguati per l’utente. Anche in caso l’utente non abbia intenzione di alterare la struttura di memorizzazione o i dati in sé, è possibile che abbia accesso a dati necessari per il funzionamento dell’applicazione, ma non adeguati all’utente. Esporre completamente i propri dati potrebbe rappresentare un serio problema in alcuni contesti applicativi. 4.4 Confronto con i database remoti Anche lato server stanno avvenendo alcuni cambiamenti importanti. Per anni l’utilizzo di database non relazionali è stato una scelta di nicchia, applicata solamente in casi specifici in cui fosse necessario gestire una gran mole di dati. Ultimamente sono state messe in discussione alcune caratteristiche dei database relazionali che ne vincolano alcune funzionalità, diventate improvvisamente molto importanti. I database non relazionali hanno suscitato un grande interesse e sono stati sviluppati una serie di DBMS non relazionali molto interessanti. Attualmente buona parte dei servizi web più importanti può essere ricollegata ad un database non relazionale: Google offre ai suoi utenti DataStore, Linkedin ha sviluppato il progetto Voldemort, Facebook si appoggia su Cassandra, un progetto nato internamente a Facebook e incubato poi dall’Apache Fundation, la quale ha all’attivo altri progetti riguardanti DBMS della stessa tipologia, come ad esempio CouchDB. 4.4.1 Relazionali Il modello relazionale è stato da sempre la variante più diffusa tra i DBMS. Si tratta di un modello logico di rappresentazione dei dati basato sull’algebra relazionale e sulla teoria degli insiemi ed è strutturato attorno al concetto di relazione (detta anche tabella). Una relazione è un insieme di attributi legati da vincoli di integrità. Questo concetto teorico di relazione viene a concretizzarsi nella definizione di una tabella in cui ogni campo ha dei vincoli precisi da rispettare. Oltre ai vincoli sui singoli attributi, vi sono poi vincoli che coinvolgono gruppi di attributi, anche appartenenti a entità differenti. Quando il database è consistente significa che tutti i suoi vincoli sono soddisfatti, e quindi esso si trova in uno stato di integrità referenziale. 51 4.4.2 Non relazionali I database non relazionali vengono utilizzati in applicazioni in cui i vincoli strutturali dei database relazionali non portano a vantaggi sensibili. Alcuni nomi già citati sono Google e Facebook, ma sono veramente tanti i progetti web importanti a poggiare su questo tipo di database per fornire i propri servizi. Contrariamente ai principali DBMS relazionali, abbastanza simili come funzionamento, differenziati principalmente da performances, costo e tipo di licenza, ogni DBMS non relazionale ha particolarità che ne esaltano alcuni aspetti, rendendolo adatto a particolari casi d’applicazione. Ci sono DBMS che fanno della scalabilità o della tolleranza ai guasti il punto chiave, altri che sono basati su linguaggi altamente concorrenti come Erlang e si rendono una scelta importante in ambienti multicore. Una terza categoria è composta dai database progettati per un alta resa in ambienti distribuiti. Tutte queste caratteristiche rendono la scelta del DBMS da utilizzare un passaggio impegnativo, da affrontare con un Architect Designer competente. Per un confronto più tecnico tra i principali DBMS non relazionali è utile analizzare alcuni degli studi già eseguiti da esperti del settore [6][7]. 4.4.3 Teorema di CAP Nel momento in cui ci si addentra nel mondo dei database non relazionali può capitare di perdere di vista alcuni importanti principi, e dare per buone quasi tutte le promesse che questi sistemi propongono in maniera cosı̀ attrattiva. Per poter valutare correttamente ogni sistema può essere utile avere come riferimento nell’analisi della caratteristiche il teorema di CAP. l teorema di CAP (Consistency, Availability, Partitions tollerance) è stato espresso nel 1998 da Eric A. Brewer. Esso sostiene che un sistema di dati condivisi è generalmente caratterizzato dalle tre proprietà che creano l’acronimo: consistenza, disponibilità e tolleranza al partizionamento di rete. L’affermazione principale di questo teorema è che si possono soddisfare contemporaneamente solamente due proprietà su tre. Realizzando due caratteristiche per volta otteniamo i seguenti sistemi: • consistenza e disponibilità: si tratta di sistemi localizzati che non risentono di partizionamento di rete, come un LDAP, un database centralizzato o un file system. La consistenza è garantita da transazioni two phase commit. • consistenza e tolleranza al partizionamento: sistemi distribuiti, con un elevato partizionamento di rete, in cui il valore della consistenza dei dati è di primaria importanza, a scapito della eventuale elevata latenza e bassa disponibilità (dovuta a politiche di locking pessimistiche). • disponibilità e tolleranza al partizionamento: la disponibilità è il valore principale per questo genere di sistemi. Per questo utilizzano protocolli più ottimistici e un approccio best-effort. Un esempio classico sono i DNS. 52 I grandi siti web dei nostri giorni sono caratterizzati da architetture altamente scalabili e profondamente distribuite, con politiche di prossimità geografica. Essi puntano chiaramente sulla garanzia di alte prestazioni ed estrema disponibilità dei dati. Si trovano quindi nella terza delle situazioni citate. Lo sforzo di garantire dati consistenti porta questi sistemi ad adottare approcci differenti per l’accesso e l’aggiornamento dei dati. Si passa dall’approccio transazionale ACID (Atomicity, Consistency, Isolation, Durability) che garantisce la consistenza, ad un approccio BASE (Basic Available, Soft-state, Eventual consistency) che mette in primo piano la disponibilità. 53 5 Sincronizzazione dei dati Un applicazione offline non isolata deve necessariamente fornire un sistema di sincronizzazione, senza il quale non sarebbe possibile mantenere coerenti tra loro la versione locale e quella remota dell’applicazione, dando all’utente la sensazione di lavorare sempre all’interno della stessa applicazione. La sincronizzazione è un argomento che non viene ampiamente trattato in maniera generale o teorica, questo perché ogni strategia di sincronizzazione efficace rappresenta un sistema unico, calibrato minuziosamente sul tipo di dati da memorizzare e sul tipo di interazioni che li definiscono o li manipolano. È importante capire che non esiste una strategia generica in grado di mantenere una perfetta aderenza alle caratteristiche del particolare problema. Esistono una serie di principi di consistenza, un insieme di tecniche e strumenti che possono essere utilizzati, ma tutto ciò va modellato in funzione del particolare caso di applicazione. Le applicazioni web presentano alcune caratteristiche importanti che come detto rappresentano i parametri sulla cui base possono essere prese alcune considerazioni. Prima di tutto è importante considerare che i database locali non hanno un canale di comunicazione diretto né tra di loro, né con la controparte remota. Questa considerazione elimina dall’insieme delle possibilità buona parte delle strategie di replicazione automatiche fornite dai DBMS. Un secondo fattore da tenere in considerazione è la facilità con cui l’utente ha accesso al database. Un terzo dettaglio da non sottovalutare parte da una considerazione ben più ampia: lo scenario d’uso di un applicazione offline. Un applicazione offline potrebbe essere eseguita agevolmente su di un dispositivo mobile, che a differenza di un computer fisso o di un portatile può essere perso o sottratto con estrema facilità. Da quest’ultima considerazione deriva la necessità di non memorizzare dati importanti se si ha motivo di credere che possano essere sottoposti ad un attacco fisico. I meccanismi che si occupano della sincronizzazione possono essere divisi in due categorie: modelli di sincronizzazione fisica o orientati ai dati, e modelli di sincronizzazione applicativa o orientati ai processi. La differenza tra queste due tipologie di meccanismi risiede nel diverso punto di inserimento all’interno dell’applicazione. Mentre un sistema di sincronizzazione orientato ai dati si occupa di eseguire periodicamente un confronto globale tra lo stato del database remoto e di quello locale, un sistema di sincronizzazione orientato ai processi segue passo passo il funzionamento dell’applicazione, e mantiene in un area riservata le istruzioni aggiuntive progettate per agevolare la sincronizzazione. 54 Indubbiamente un sistema orientato ai dati risulta essere il più semplice da implementare in un ambiente di programmazione standard, e in applicazioni complesse potrebbe rivelarsi anche molto più efficiente, dal momento che sgrava l’applicazione dal compito di mantener traccia di tutte le operazioni compiute. Vi sono tuttavia due grandi problemi in questo approccio: per prima cosa non vi è un canale dedicato che colleghi i due database, e ciò rappresenta un grave problema, in quanto a volte rende impossibile arrivare ad un analisi approfondita dei dati. In secondo luogo i dati mantenuti in memoria localmente sono solamente un sottoinsieme dei dati archiviati nella controparte remota, che potrebbe anche avere la necessità di ricombinarli insieme prima di sincronizzarli con il database locale. 5.1 Sincronizzazione fisica di database Nonostante la sincronizzazione fisica di due database non si sia dimostrata la scelta più adatta alla soluzione del problema della sincronizzazione tra database locali e remoti, vale la pena di studiarne le caratteristiche in relazione al problema in esame. In fase di definizione del progetto di stage era stata valutata come ipotesi la sincronizzazione a basso livello del database client e server. Studiando le caratteristiche dei due database sono stati messi in luce alcuni aspetti problematici che rendono attualmente improponibile una sincronizzazione effettiva mediante tecniche basilari, utilizzate diffusamente in altri ambiti quali la sincronizzazione di file tra più computer. 5.1.1 Ipotesi iniziale: interpretazione in ottica fisica (file) Inizialmente si era considerato il problema partendo da una situazione esistente già affrontata con successo in altri ambiti: la replicazione di file tra più computer. In questo tipo di sincronizzazione paritetica vi sono alcune ipotesi che difficilmente possono essere adattate al concetto client-server, il quale è espressamente gerarchico. Nella replicazione i due sistemi vengono sincronizzati costantemente in modo da rappresentare (in un momento in cui il sistema è in uno stato coerente) due versioni identiche dello stesso database. Nel progetto in esame invece la necessità è di un altro tipo: mantenere coerenti tra loro due versioni differenti di database che condividono secondo alcune specifiche politiche una serie di dati. 5.1.2 Problematiche riscontrate lato client Le problematiche riscontrate in un approccio di tipo replicativo sono prima di tutto tecnologiche e in secondo piano applicative. L’aspetto applicativo, sebbene sia di indubbia importanza, passa in secondo piano valutando le limitazioni tecnologiche imposte, principalmente relative all’impossibilità di accedere fisicamente ai dati memorizzati nel browser. Per meglio comprendere questo aspetto si può ragionare sulla differenza tra un ambiente di replicazione server side e un ambiente di sincronizzazione client-server: nel primo caso 55 abbiamo a che fare con due database noti (generalmente dello stesso tipo), con meccanismi di replicazione preesistenti, messi in comunicazione attraverso un canale stabile e controllato. Nel secondo caso abbiamo invece un sistema server su cui possono essere fatte delle assunzioni (probabilmente limitate), ed un sistema client residente in un browser, il quale espone solo parzialmente le caratteristiche del database. Il canale di comunicazione è composto dalle varie componenti dell’ambiente web: il browser, il linguaggio JavaScript, il protocollo HTML ed infine il linguaggio lato server. Nessuna di queste componenti può offrire un canale di comunicazione diretto tra i due database. Mentre nella replicazione lato server si hanno alcune libertà (ad esempio è possibile esaminare i log del database dall’interno dell’applicazione), le quali possono consentire la creazione di punti d’accesso per i meccanismi di sincronizzazione, nell’ambiente client-server non si ha accesso ai reali componenti del sistema, ma solamente alle interfacce esposte dagli strati che lo compongono. In un contesto di questo tipo una limitazione tecnologia impone un limite spesso invalicabile. 5.1.3 Problematiche riscontrate lato server I principali DBMS prevedono politiche di replicazione dedicate, utilizzabili quindi solamente tra database compatibili. Questi sistemi di replicazione possono lavorare a basso livello, eventualmente sincronizzando a basso livello i dati che contengono. Esistono da tempo algoritmi efficienti già impiegati per la sincronizzazione dei file che possono essere adattati a questo scopo. Nel momento in cui si cerca di sincronizzare due sistemi aventi specifiche differenti, bisogna retrocedere di un passo a causa delle differenti interfacce e protocolli, e spostarsi ad un livello più alto, tornando ad avere a che fare con la componente logica. Nel caso migliore è possibile elaborare il registro delle transazioni effettuate, estraendo le operazioni eseguite successivamente all’ultima data di sincronizzazione e replicandole nell’altro database. In questo modo si può ottenere una copia consistente senza dover esportare e importare completamente i dati memorizzati. In caso non sia fornita la possibilità di analizzare il registro delle transazioni si può inserire uno strato logico posizionato all’accesso del database, che registri le operazioni eseguite creando un log funzionale delle variazioni necessarie per la sincronizzazione. In caso non fosse possibile manipolare la logica dell’applicazione in questo modo si ricade nell’ultimo caso, in cui è necessaria un esportazione ed importazione massiccia dei dati. Tali operazioni rappresentano sempre un problema perché introducono pericolose anomalie all’interno dei sistemi, e devono essere eseguite in specifici momenti in cui il blocco del database non provoca danni. Tali sistemi di sincronizzazione sono conosciuti come BulkExport e BulkImport (letteralmente esportazione ed importazione massicci). Nell’ipotesi in esame ci troviamo circa nel secondo caso: non è possibile accedere ad un registro delle transazioni né client side né server side (anche se in questo ambiente potrebbe essere possibile aggirare l’ostacolo avendo sufficiente libertà di movimento nel server), non è consentito un accesso diretto al database, e gestire una copia massiccia dei dati da client 56 a server presenta una grave problematica dal punto di vista dell’efficienza, oltre a scontrarsi con il problema del partizionamento e della rivalidazione server side, discussi nei paragrafi 5.1.4 e 5.1.5. 5.1.4 Partizionamento dei dati Nel caso delle applicazioni web non è possibile mantenere localmente una copia esatta del database remoto, poiché ogni client ha accesso ad un set ristretto di dati. A causa di problematiche legate alla sicurezza non è poi ipotizzabile esporre all’utente dati non pertinenti con il suo ruolo specifico. Considerata questa premessa i dati contenuti nel server possono essere organizzati in tre macro categorie: dati privati dell’applicazione, dati pubblici dell’applicazione e dati utente. La prima categoria contiene i dati necessari al funzionamento dell’applicazione, ma normalmente non accessibili dall’utente che la utilizza. In un’applicazione che gestisce ordini e transazioni può essere un pericolo esporre chiavi software e altre informazioni che possano permettere una forzatura del sistema. Il concetto di base è chiaro: alcuni meccanismi dell’applicazione sono e devono rimanere fuori dalla portata di qualsiasi utente, che potrebbe altrimenti utilizzarli per scopi non previsti. Nella seconda categoria troviamo i dati a cui l’utente può accedere, ad esempio l’elenco dei clienti dell’azienda o l’elenco degli ordini in lavorazione. All’interno di questa categoria potremmo dividere ulteriormente le informazioni in base a politiche d’accesso mirate: potremmo magari esporre al responsabile di una sezione solamente i dati relativi ai propri dipendenti. Nella terza categoria possiamo posizionare i dati di proprietà dell’utente, ad esempio ordini creati personalmente (su cui si dovrebbe avere una maggiore possibilità di manipolazione). In quest’ultima sezione andrebbero inserite anche le dinamiche di controllo e i permessi dell’utente, che vincolano le operazioni che possono essere eseguite sui propri dati e su quelli della sezione pubblica condivisa. L’accesso alle tre categorie viene normalmente regolato da specifici meccanismi di autenticazione e validazione delle operazioni. Nella versione locale dell’applicazione non possiamo più fare affidamento su questa sicurezza, e le tre sezioni collassano quindi in una singola sezione, quella appunto dei dati locali. Si rende quindi necessario applicare una scelta precisa su quali dati memorizzare, tenendo in considerazione ciò che è strettamente necessario al funzionamento dell’applicazione e ciò che è strettamente pertinente all’utente. Questa selezione determina il partizionamento dei dati remoti da sincronizzare con la controparte locale. 57 5.1.5 Rivalidazione delle azioni eseguite client side Un ipotetica applicazione potrebbe seguire un principio di funzionamento fortemente orientato verso la componente offline: ogni modifica eseguita dall’utente viene applicata prima alla versione locale del database e successivamente riportata dal lato server. In questo approccio possono essere individuate una serie di problematiche complesse, legate soprattutto all’integrità logica dei dati, ed alla coerenza delle applicazioni rispetto ai permessi di manipolazione degli stessi da parte dell’utente. A causa della scarsa sicurezza lato client e della possibilità dell’utente di manipolare i dati ignorando completamente le logiche applicative, è necessaria una rivalidazione completa dei dati lato server. Questa limitazione ha delle importanti ripercussioni dal punto di vista della sincronizzazione, poiché porta a scartare completamente una sincronizzazione di tipo fisico in favore di quella di tipo logico. In caso si scegliesse di utilizzare un approccio fisico alla replicazione, ci si troverebbe ad avere nel server le due versioni (in un ottica decisamente semplicistica). A questo punto tentare di eseguire una validazione delle operazioni eseguite potrebbe essere un operazione decisamente ardua, specie considerando che nel frattempo i permessi di manipolazione dei dati potrebbero aver subito dei cambiamenti. Avendo le due versioni a confronto sarebbe estremamente difficile capire quali operazioni hanno portato la versione iniziale ad evolvere localmente fino a diventare la versione attualmente fornita dal client, senza contare che una volta verificata la legalità delle operazioni si avrebbe la problematica di relazionarle con istanze temporali differenti del database remoto. Una sincronizzazione logica avanzata potrebbe fornire in anticipo solamente le modifiche apportate, rendendo quindi più semplice per il server la verifica dell’effettiva legalità delle operazioni, anche confrontandole con delle politiche di manipolazione aggiornate nel frattempo. 5.2 Sincronizzazione a livello applicativo Valutando le problematiche di una sincronizzazione diretta tra il database server side e la versione locale dello stesso, si può optare per una sincronizzazione a livello di logica applicativa. Tale soluzione prevede in sostanza la registrazione delle variazioni subite dai database in formato SQL, variazioni che verranno poi applicate reciprocamente per ottenere una versione unificata. Mantenendo da parte le modifiche da applicare si possono proporre due tipologie differenti di sincronizzazione: trasparente e tramite cache temporanea. Nella sincronizzazione trasparente l’intera parte di trasferimento verso il server ed applicazione delle modifiche è completamente nascosta all’utente, che quindi può ignorare se lo stato della propria applicazione è online o offline poiché gli vengono fornite esattamente le stesse funzionalità. Un sistema di questo tipo ha delle grandi proprietà di tolleranza ai guasti. 58 Nella sincronizzazione tramite cache temporanea l’utente ha una netta distinzione tra i dati provenienti dal server e quelli generati localmente. Si può ipotizzare quindi che il database locale funzioni come un area temporanea, che conserva tutte le modifiche applicate dall’utente in attesa di inoltrarle al server. Un esempio potrebbe riguardare la creazione di un’applicazione di gestione di un magazzino: gli ordini creati in modalità offline vengono mantenuti in un area locale chiaramente identificabile. Alla prima possibilità di connessione gli ordini potrebbero essere caricati richiedendo all’utente un ulteriore approvazione. Una dinamica del genere permetterebbe ad esempio di presentare un riepilogo prima dell’effettivo inoltro dell’ordine, quando sono disponibili nuovi dati provenienti dal server, in modo da verificare ad esempio che la quantità di merce richiesta sia effettivamente disponibile, o che le specifiche del prodotto non siano variate nel frattempo. 5.2.1 Sincronizzazione trasparente L’obbiettivo della sincronizzazione trasparente è quello di fornire un’applicazione offline speculare alla versione online, in grado quindi di esporre le stesse funzionalità e lavorare sugli stessi dati. Questo approccio è particolarmente funzionale in un ambiente in cui ogni utente lavora in un area disgiunta del database, mentre presenta delle complicazioni importanti in caso il sistema sviluppato preveda un minimo di collaborazione o di condivisione di dati. Ripensando ad esempio all’applicazione descritta precedentemente possiamo trarre alcune considerazioni. Se l’utente non potesse distinguere gli ordini memorizzati localmente da quelli inoltrati al server si creerebbe una confusione che potrebbe portare ad una inconsistenza logica, nel momento in cui magari si attende una mail di risposta dal magazzino per un ordine che si ritiene già inoltrato. In un caso simile avere la sicurezza che il dato ordine sia effettivamente stato inoltrato al server è un punto fondamentale. Ragionando poi sulla rivalidazione dei dati lato server ci si potrebbe trovare nel caso in cui il server rifiuti le modifiche, e quindi vengano cancellate anche nella versione locale dell’applicazione. L’utente che considera certe operazioni eseguite e che le vede all’improvviso sparire dal flusso d’esecuzione del programma potrebbe sentirsi abbastanza disorientato. Per evitare questo effetto bisognerebbe inserire una serie di notifiche che avvisino l’utente che ciò che dava per fatto (e su cui magari nel frattempo ha applicato ulteriori modifiche) è stato in realtà rifiutato dal server. Da questo punto di vista mantenere distinte le operazioni eseguite solo localmente potrebbe rappresentare la scelta migliore in termini di chiarezza nei confronti dell’utente. Ricordando che la chiarezza consente un utilizzo più soddisfacente e produttivo è facile capire come l’adozione di componenti offline in grado di rappresentare chiaramente il proprio funzionamento sia un punto chiave per il successo dell’applicazione. 5.2.2 Sincronizzazione incrementale mediante copie delta In fase di progettazione si potrebbero mantenere due aree distinte del database: una dedicata all’ultima copia sincronizzata del server, ed una dedicata alle modifiche da applicare. Non applicando direttamente le modifiche nella versione locale sarebbe possibile mantenerla 59 rigidamente sincronizzata con il server, realizzando quindi una sincronizzazione monodirezionale, decisamente più semplice da gestire. In sostanza localmente verrebbero memorizzati due database, una copia di quello remoto in un determinato stato, senza modifiche aggiuntive, ed un database secondario contenente le differenze che si riscontrerebbero confrontando il database locale con lo stato remoto di riferimento. Dividendo il database locale in questo modo sarebbe possibile aggiornare la copia locale del database remoto senza curarsi di perdere le modifiche ad esso applicate durante il funzionamento offline. Si trasformerebbe dunque la sincronizzazione bidirezionale (in un qualche modo paritetica) in una comunicazione monodirezionale da server a client, con un canale secondario dedicato alle differenze da applicare. Ad un aggiornamento della logica applicativa sarebbe inoltre possibile verificare la legalità delle operazioni prima di tutto localmente, notificando all’utente l’impossibilità di completare determinate procedure a causa della variazione dei propri permessi. Le modifiche locali da inoltrare al server verrebbero in questo modo validate prima localmente, eventualmente potrebbero essere sottoposte all’utente per una conferma, ed inoltrate al server per l’applicazione definitiva. In caso la rivalidazione lato server risulti accettata sarebbe possibile ottenere una nuova copia del database remoto, del quale farebbero ormai parte anche i propri dati. Mano a mano che le modifiche proposte al server vengono accettate, esse possono essere rimosse dal database temporaneo, garantendo quindi un ottimizzazione di spazio. 5.3 Strumenti di sincronizzazione 5.3.1 Utilizzo di marcature di sincronizzazione In un ipotetico ambiente in cui l’applicazione client e quella server vengono utilizzate dallo stesso singolo utente, potrebbero essere considerate delle politiche di sincronizzazione basate sul log delle operazioni, o su un indice progressivo delle query eseguite. Utilizzando questo codice progressivo sarebbe possibile confrontare in maniera rapida e semplice il livello di sincronizzazione tra client e server. Purtroppo l’ipotesi di un’applicazione monoutente si applica ad un numero di casi molto ristretto. In un contesto più diffuso, in cui una serie di utenti lavorano sullo stesso database, si presenta il problema del partizionamento dei dati: ogni utente ha la possibilità di accedere ad una singola porzione dei dati contenuti: i propri, o quelli ad essi strettamente collegati. Le tecniche di sincronizzazione in questo ambiente includono la possibilità di creare un indice di progressione personale per le query eseguite dal tale utente, che indichi quindi l’avanzamento della propria partizione. Confrontando questo indice con quello archiviato localmente sarebbe possibile valutare il livello di sincronizzazione della propria porzione di dati ed avviare i meccanismi di trasferimento in caso ci si trovi ad essere rimasti ad una versione precedente, o vi siano operazioni mancanti nel server. Gli aggiornamenti dell’indice potrebbero essere realizzati tramite alcuni trigger dove presenti, limitando quindi la necessità di operare in maniera invasiva sulle query già predisposte per l’applicazione. 60 Purtroppo nel momento in cui le partizioni personali del database si trovano a non rispettare più limiti precisi, o ad essere addirittura condivise, si rende necessaria la costruzione di trigger da attivare in cascata, poiché la modifica di un record condiviso tra molti utenti comporta l’aumento di indice di progressione non solo per l’utente che l’ha scatenato, ma anche per tutti gli utenti con visibilità diretta o indiretta sul tale record. Nel caso in cui l’aggiornamento includa già in cascata l’aggiornamento di altri valori le cose si complicano ulteriormente. Dalle semplici considerazioni espresse finora si può capire come l’introduzione della condivisione dei dati comporta un esplosione di problematiche non indifferenti. 5.3.2 DBDigest Una tecnica di controllo di versione differente potrebbe consistere nell’eseguire una query di selezione sui dati (o su una parte dei dati) a cui l’utente ha accesso, ed estrarne un digest (riassunto) da trasferire al dispositivo client, il quale potrebbe eseguire la stessa query e confrontare i valori ottenuti. Creare una query di questo tipo è relativamente semplice: è sufficiente combinare insieme i comandi SQL CONCAT, GROUP CONCAT ed MD5 per trasformare una proiezione di dati ottenuti da un comando SELECT in un digest estremamente compatto e facile da trasferire. Elaborando inoltre comandi come LIMIT e OFFSET è possibile ipotizzare dei marcatori differenziati per blocchi di dati, permettendo quindi una sorta di ispezione ad albero dei blocchi di dati, permettendo quindi un controllo delle variazioni più specifico. Una query di db digest con 64 caratteri, contenente anche un meccanismo di autenticazione può essere di questo tipo: 1 2 3 4 5 6 7 8 9 SELECT CONCAT ( MD5 ( GROUP_CONCAT ( notes . title ) ) , MD5 ( GROUP_CONCAT ( notes . body ) ) ) FROM ‘ notes ‘ LEFT JOIN user ON notes . user_id = user . user_id WHERE notes . user_id =1 AND user . password = MD5 ( ’ password ’ ) Listing 5.1: esempio di DBDigest generico In sostanza si esegue una selezione, si serializzano i valori dei record concatenandoli in gruppo, ottenendo una proiezione su una singola riga. A questo punto si esegue un digest con MD5 del campo e si concatenano tutti i valori forniti per ottenere una stringa più o meno lunga in base al numero di campi coinvolti. Questo marcatore può essere ricalcolato in seguito a particolari eventi del server, o su richiesta del client, in base alle necessità dell’applicazione. Utilizzando questa tipologia di marcatori sarebbe possibile ottimizzare i confronti tra il database remoto e la controparte locale, agendo non più sulle operazioni che ne provocando le variazioni, ma direttamente sull’effetto che tali variazioni hanno sui dati. 61 5.3.3 Marcatori di operazioni incrementali legati a MVCC L’utilizzo di un marcatore di digest permetterebbe di rilevare differenze tra la versione client e quella server, ma non offre nessun vantaggio in fase di sincronizzazione, poiché non fornisce la possibilità di individuare efficientemente quale area del database abbia subito delle modifiche. Questa mancanza rende necessario un controllo successivo o un importazione in blocco dei dati contenuti nella tabella in esame. Un punto di appoggio molto importante per implementare delle politiche di sincronizzazione efficaci potrebbe essere l’utilizzo di un marcatore di transazione relativo ai record. Buona parte dei DBMS che implementano MVCC (Multi Version Concurrency Control, il sistema di concorrenza che ne regola il funzionamento in ambienti multithread) marcano per necessità del sistema ogni record con il codice dell’ultima transazione che lo ha modificato. Avere accesso a questo campo fornisce una possibilità veramente comoda: poter selezionare tutti i record di una tabella modificati da un determinato momento in poi. Una volta ottenuta una selezione cosı̀ specifica si può puntare a un sistema di sincronizzazione il più efficiente possibile, in grado di recuperare solamente le differenze tra database remoto e database locale, anche contestualizzate all’interno di una specifica finestra temporale. I DBMS che forniscono nativamente queste informazioni sono PostgreSQL, Oracle e MySql (con motore di memorizzazione InnoDB). SQLServer permette la creazione di un campo di tipo timestamp che viene aggiornato automaticamente dal sistema ad ogni modifica del record [8]. Da questo punto in poi viene assunto il DBMS PostgreSQL come riferimento per la ricerca delle caratteristiche e delle funzioni a supporto della sincronizzazione. Le funzionalità avanzate offerte da PostgreSQL sono generalmente offerte anche dagli altri BDMS, seppur in maniera meno accessibile ed in alcuni casi perfino nascosta. In PostgreSql è possibile recuperare i marcatori MVCC leggendo il campo nascosto xmin. Questo campo contiene il codice (XID) dell’ultima transazione che ha alterato il record. La query in esempio confronta due tabelle, ordinandole in base ai record che hanno subito l’ultimo aggiornamento. Il primo record della selezione conterrà sempre il codice attuale della transazione di selezione (quindi l’ultimo XID generato). 1 2 3 4 5 6 7 8 9 SELECT cod , ’ clienti ’ AS table , xmin :: text :: bigint , xmax FROM clienti UNION SELECT cod , ’ articoli ’ AS table , xmin :: text :: bigint , xmax FROM articoli UNION SELECT ’ this_xid ’ , ’ -- ’ , txid_current () , ’ -- ’ ORDER BY xmin DESC Listing 5.2: esempio di accesso al marcatore MVCC 62 Creando una VIEW con un istruzione di questo tipo sarebbe poi possibile ottenere l’elenco ordinato degli ultimi aggiornamenti avvenuti nel database. Utilizzando la VIEW, o una SELECT aggiuntiva è possibile eseguire una query che estragga solamente i valori necessari alla sincronizzazione. Una query d’esempio che non utilizza VIEW potrebbe essere di questo tipo: 1 2 3 4 5 6 7 8 9 10 11 12 SELECT * FROM ( SELECT cod , ’ clienti ’ AS table , xmin :: text :: bigint , xmax FROM clienti UNION SELECT cod , ’ articoli ’ AS table , xmin :: text :: bigint , xmax FROM articoli UNION SELECT ’ this_xid ’ , ’ -- ’ , txid_current () , ’ -- ’ ORDER BY xmin DESC ) AS transaction_merge WHERE xmin > 1101696 Listing 5.3: selezione dei record modificati dopo una determinata transazione Utilizzare una VIEW avrebbe però un vantaggio secondario: in caso di aggiornamento dello schema sarebbe sufficiente modificare il comando costituente della VIEW, senza badare alla modifica dell’insieme di query necessarie alla sincronizzazione. Mantenendo una struttura simile a quella proposta sarebbe molto semplice implementare un meccanismo di sincronizzazione: si tratterebbe semplicemente di interrogare la tabella dei risultati, richiedendo poi per ognuno la tabella di appartenenza (2’ campo) e la chiave primaria (1’ campo) per ottenere il record da modificare o inserire. Utilizzando un meccanismo simile combinato con funzioni di raggruppamento e comandi di tipo DISTINCT si otterrebbe l’elenco delle tabelle variate. Una volta ottenuto l’elenco sarebbe possibile procedere tabella per tabella alla creazione di specifiche query di importazione. 5.3.4 Problematiche legate al Vacuum A causa del limite di lunghezza del codice di transazione, il sistema può reggere fino a 4 miliardi di transazioni prima di riciclare i codici, i quali però vengono ristretti a 2 miliardi per necessità del sistema (viene implementato un sistema circolare). Nel momento in cui viene raggiunto l’ultimo codice disponibile il conteggio riparte da zero, trasformando quindi tutte le operazioni passate in operazioni future. In questo caso nessuna transazione ha più accesso ai dati di tali record, poiché essendo considerati futuri vengono resi inaccessibili dal sistema MVCC. Per risolvere il problema dell’accumulo di record inutilizzabili e del ricalcolo dei codici di transazione (fenomeno conosciuto come XID Wraparound [9]) PostgreSql mette a disposizione una funzione chiamata Vacuum che si occupa di “pulire” il database da tutti i parametri temporanei aggiuntivi generati dal sistema MVCC. 63 L’utilizzo della funzione Vacuum, con lo scopo di riallineare tutti i codici di transazione dei record, forzando quindi tutte le transazioni a comparire come passate, presenterebbe delle complicazioni nel caso la versione locale del database voglia sfruttare i codici MVCC. Nel momento in cui il database remoto riparte con il conteggio delle transazioni, il database locale non avrebbe modo di capire cosa sia avvenuto, e si troverebbe quindi in uno stato di inconsistenza. Anche avendo la facoltà di determinare una chiamata alla funzione Vacuum, non avrebbe idea di quali operazioni siano intercorse tra la sua ultima data di sincronizzazione ed il punto di Vacuum. In questo caso l’unica soluzione è verificare in altro modo quali dati siano cambiati, e nel caso peggiore riscaricare l’intero database. L’implementazione di MVCC fornita da PostgreSql ha delle caratteristiche che rendono il codice di transazione complesso da utilizzare, ma meno problematico. L’operazione di Vacuum, necessaria per riallineare i codici, non azzera i valori di un numero spropositato di record, ma sposta semplicemente avanti il punto di partenza, ragionando in un ottica circolare. Per progettare un sistema di sincronizzazione tenendo conto di questa caratteristica bisogna confrontare l’ultimo codice di transizione archiviato localmente con l’ultimo codice di transazione remoto. Se il codice di transazione remoto è inferiore a quello mantenuto in locale significa che il codice ha superato il limite ed è ricominciato da zero. A questo punto vanno eseguite non solo tutte le operazioni successive al codice dell’ultima transazione sincronizzata in locale, ma anche tutte quelle con un XID inferiore all’ultimo memorizzato. Il caso peggiore in cui ci si può venire a trovare è però quello in cui il database remoto ha effettuato talmente tante transazioni da aver non solo azzerato il contatore, ma anche superato il codice locale. In questo caso rilevare un errore di questo tipo risulta difficoltoso, e risulta ancora più difficile allineare le due copie dei database senza effettuare una copia globale dei dati memorizzati nel server. Per creare una politica di sincronizzazione efficace è necessario richiedere al server il codice dell’ultima transazione eseguita mediante il comando SQL SELECT txid_current(). Ottenuto il valore della transazione corrente è possibile verificare se esso sia successiva all’ultimo XID memorizzato localmente (in questo caso si intende lo XID proveniente dal server, dal momento che il database client non implementa MVCC), ed in caso lo si trovi successivo si procede all’inserimento o aggiornamento locale di tutti i record compresi nell’arco temporale tra le due transazioni. Se lo si trova inferiore si procede come già detto all’aggiornamento di tutti i record con XID superiore all’ultimo aggiornato, per poi passare all’aggiornamento di tutti i record con valore inferiore all’ultima transazione eseguita dal database remoto. 64 5.3.5 Aggiunta di timestamp ai fini della sincronizzazione Se fosse possibile affidarsi ai trigger (molto diffusi nei database relazionali standard), una soluzione molto comoda sarebbe quella di marcare un apposito campo con il timestamp dell’ultima modifica ad ogni operazione che si esegue sul record. Questa politica permetterebbe di eseguire dei confronti più semplici basati appunto sulle date di sincronizzazione, ed eviterebbe problemi di overflow, almeno fino al 19 gennaio 2038 [10]. L’utilizzo di trigger finalizzati all’aggiornamento dei timestamp potrebbe essere comunque evitata, modificando le query di inserimento o modifica in modo da settare a NULL i valori del determinato campo, che con un appropriato valore di default verrebbero automaticamente settati al timestamp attuale. 5.3.6 Inserire uno strato logico nel driver del database In caso non fosse possibile utilizzare correttamente i trigger come base per l’implementazione dei meccanismi di sincronizzazione, sarebbe possibile predisporre una propria implementazione di questi meccanismi, inserendosi se possibile nelle funzioni contenute nei driver del database. Avere accesso a questo tipo di funzioni permetterebbe allo sviluppatore la modifica di ogni query eseguita nel database, o su di una porzione specifica dello stesso. Le query modificate potrebbero includere dei dati aggiuntivi per la sincronizzazione, quali l’utente responsabile dell’ultima transazione ed appunto il timestamp. Una modifica di questo tipo potrebbe essere molto semplice da implementare su sistemi preesistenti, nei quali andare a modificare le centinaia di query potrebbe non rappresentare una scelta sicura e condivisibile. 5.4 Sincronizzare le eliminazioni Ragionando sui meccanismi di sincronizzazione è facile notare come sia possibile gestire più o meno facilmente inserimenti e modifiche di dati, mentre si rimane senza appigli per quanto riguarda i dati eliminati. Dal momento che tali dati non esistono più non vengono considerati dai DBDigest, né dalle politiche di sincronizzazione basate su marcatori di transizione come MVCC, né da meccanismi in grado di sfruttare il timestamp di un record. La gestione delle eliminazioni richiede a tutti gli effetti una gestione a sé stante. Sempre a causa del funzionamento interno del sistema MVCC vengono mantenuti in memoria anche record obsoleti che andrebbero eliminati. Avere accesso anche a questi record permetterebbe la determinazione non solo delle righe modificate, ma anche di quelle cancellate in un determinato arco temporale, permettendo quindi una sincronizzazione completa. Vi è però un problema: questi record eliminati logicamente (dead-rows nella documentazione PostgreSQL) non sono accessibili con la stessa facilità con cui si accede ai campi nascosti contenenti l’indice della transazione. 65 Esporre questi particolari dati potrebbe creare dei problemi di integrità, poiché esisterebbero ad esempio più valori per la stessa chiave. I DBMS in genere consentono l’accesso a questi dati solamente con funzioni rigidamente controllate, e solo per scopi interni di debug. Il problema dell’accesso ai record eliminati non è da sottovalutare poiché perdere l’accesso ai dati eliminati rende necessario l’utilizzo di sistemi alternativi in grado di registrare le cancellazioni. 5.4.1 Eliminazioni logiche Una prima soluzione al problema potrebbe essere quella di utilizzare delle eliminazioni logiche, basate su di un apposito flag, che marchi il record come obsoleto. Utilizzando questa tecnica sarebbe possibile sostituire le operazioni di eliminazione con operazioni di modifica e si guadagnerebbe la possibilità di utilizzare il campo nascosto con il codice di transazione per ottenere l’istante di eliminazione del record. In realtà vi sono alcuni problemi legati a questo sistema: innanzitutto è necessario modificare tutte le query di lettura in modo che lavorino solamente con dati logicamente non obsoleti, ed in caso ci si trovi a modificare un software con un parco query molto esteso, magari contenente anche query complesse, il rischio di introdurre errori potrebbe diventare alto. Un problema secondario è la modifica delle query di inserimento e cancellazione, che devono essere modificate a tempo d’esecuzione in caso esista già il record in esame. Per quanto riguarda la cancellazione le possibilità sono due: o si elimina completamente il comando delete dalle query possibili e lo si trasforma in un comando di modifica, o si impostano dei trigger che vengano attivati prima dell’eliminazione ed agiscano sul flag in modo analogo. Il problema principale di questa tecnica deriva dalla debolezza dei trigger, che non sono supportati da tutti i DBMS, e dove supportati spesso hanno implementazioni differenti. In base alle implementazioni vi sono poi tutta una serie di situazioni in cui il trigger non viene attivato, portando quindi il sistema in uno stato di inconsistenza logica. Un secondo problema non indifferente è l’occupazione della chiave primaria. Se il record è eliminato logicamente la chiave primaria risulta comunque occupata, e quindi un inserimento di un nuovo record con la stessa chiave primaria comporterebbe un problema di integrità. A questo punto la chiave primaria del record eliminato deve essere modificata contestualmente al flag, magari aggiungendo un marcatore alla fine. Si potrebbe in alternativa modificare l’operazione di inserimento facendo in modo che verifichi la possibilità di inserire l’elemento ex novo, ed in caso contrario elimini il precedente, o lo modifichi assegnandogli i propri valori. 66 5.4.2 Tabelle cestino Il problema del parco query esteso da modificare potrebbe essere risolto almeno in parte con due stratagemmi: il primo prevede la creazione di tabelle cestino, e il secondo la creazione di apposite view che mascherino i dati, in modo da mantenere tutte le informazioni aggiuntive ed i dati obsoleti nella tabella sottostante, invisibili tanto all’applicazione, quanto all’utente. L’utilizzo di una tabella cestino prevede la sostituzione delle operazioni di eliminazione con operazioni di spostamento da una tabella a quella cestino, in cui vengono memorizzati tutti i dati obsoleti. Questo meccanismo permette l’utilizzo delle query di selezione e modifica originali, e riduce quindi l’impatto nel sistema preesistente. Un problema che sorge però è legato alla dimensione del database, in quanto la duplicazione della maggior parte delle tabelle potrebbe comportare una difficoltà di gestione e soprattutto di manutenzione, poiché ad ogni modifica dei campi della tabella principale, deve essere eseguita una modifica speculare della tabella cestino. 5.4.3 View di facciata Una seconda alternativa per ridurre l’impatto di modifica richiesto dall’utilizzo delle eliminazioni logiche sfrutta il meccanismo delle VIEW per sostituire le tabelle preesistenti con delle tabelle virtuali, che elaborino coerentemente i dati provenienti dalle entità originali, a cui viene modificato il nome aggiungendo ad esempio un “ F” per indicare che si tratta della tabella fisica. Per implementare questa tecnica si dovrebbe prima di tutto modificare il nome della tabella, aggiungendo il marcatore fisico, dopo di che si passa all’aggiunta dei campi di eliminazione logica, timestamp e quant’altro. Una volta modificata la struttura originale della tabella si crea una vista che esegua una SELECT dei dati contenuti nella tabella sottostante, escludendo i dati eliminati logicamente e nascondendo i campi aggiuntivi. Se come nome della vista si sceglie il nome originale della tabella, tutte le funzioni di lettura non dovranno aggiornare la propria definizione dei campi, né delle logiche di selezione, garantendo quindi la piena compatibilità con le query preesistenti. In questo modo i dati sono mantenuti tutti all’interno della stessa unità e si elimina la necessità di una seconda tabella. Rimane però il problema delle query originali che comportano modifiche ed eliminazioni. Queste query devono essere riscritte in modo da lavorare con la tabella fisica e non con la view, oppure devono essere impostati correttamente alcuni trigger in grado di attivarsi al tentativo di inserimento, modifica ed eliminazione sulla VIEW. Tali operazioni andrebbero quindi indirizzate verso la tabella fisica sottostante. 67 5.4.4 Log delle eliminazioni Una soluzione semplice ed estremamente efficace potrebbe essere il mantenere semplicemente un apposito registro per le eliminazioni. In una tabella (unica per ogni database) potrebbero essere inseriti record contenenti un numero ristretto di campi: la chiave primaria originale del record, il nome della tabella (che combinato con la chiave primaria originale diventerebbe la chiave primaria della tabella di registro), il codice di transazione corrente o il timestamp ed infine se necessario il codice dell’utente che ha eseguito l’eliminazione. Mantenere un log temporizzato con le sole eliminazioni permetterebbe di abbandonare le doppie tabelle e le viste, sfruttando semplicemente i codici di transazione per ottenere dalle tabelle i dati variati e inseriti, e dalla tabella registro i record cancellati. In caso un record cancellato venga reinserito successivamente nella tabella di origine, per evitare che il sistema rischi di confondersi, non capendo se inserire od eliminare il proprio valore, sarebbe sempre possibile analizzare i codici di transazione per capire quale operazione è stata eseguita per ultima e sceglierla come più affidabile. 68 6 Conclusioni Questo stage mi ha dato modo di approfondire nel dettaglio le caratteristiche delle applicazioni offline, che se ben progettate risultano essere estremamente funzionali. La realizzazione di vari prototipi mi ha dato modo di valutare concretamente le difficoltà in cui ci si imbatte nel momento in cui si abbandona il filone principale dello sviluppo web. Ragionare in un ottica pienamente offline comporta delle modifiche sostanziali al modo di progettare un applicazione web, ed il modo migliore di comprendere queste modifiche è sicuramente addentrarsi nel dettaglio nello sviluppo, cercando di simulare alcuni dei sistemi già esistenti per comprenderne le funzionalità. I grandi scontri che avvengono tra le principali compagnie internazionali, riguardanti il futuro del web, lasciano intendere come questo ambiente abbia le carte in regola per rappresentare un punto di convergenza per alcuni ambiti della programmazione. La ricerca delle motivazioni alla base di questi scontri permette di capire quanto ogni parte protegga i propri progetti e tenga al loro sviluppo. L’approccio tecnico che ho mantenuto durante la ricerca degli aspetti problematici e le relative soluzioni mi ha dato modo di andare oltre le comuni istruzioni per l’uso ed addentrarmi alla ricerca delle caratteristiche interne più particolari, anche se spesso si sono rivelate inutilizzabili per le normali applicazioni web. Pur non essendo arrivato a definire un modello completo di applicazione offline, ho acquisito un bagaglio di conoscenze tale da permettermi di capire e distinguere le varie applicazioni offline, suddividendole in categorie differenti in base al loro scopo e alla loro implementazione. Comprendere le problematiche alla base di certe scelte richiede capacità di analisi, e l’ideazione di soluzioni, specialmente nell’ambito della sincronizzazione, richiede una certa dose di inventiva e creatività. Ritrovarsi ad immaginare le applicazioni di domani, e rendersi conto che vi si è già fatto qualche passo all’interno, è di sicuro una gran soddisfazione. 69 Bibliografia [1] Standard W3C per il file manifest e l’application cache http://www.w3.org/TR/2011/WD-html5-20110525/offline.html. [2] Standard W3C per il database locale WebSQL http://www.w3.org/TR/webdatabase/. [3] Standard W3C per il database locale IndexedDB http://www.w3.org/TR/IndexedDB/. [4] Specifiche degli eventi di caching relativi all’application cache http://www.whatwg.org/specs/web-apps/current-work/#appcacheevents. [5] Confronto tra i database locali a cura dei laboratori Mozilla. http://hacks.mozilla.org/2010/06/comparing-indexeddb-and-webdatabase/, http://blog.harritronics.com/2011/04/more-thoughts-on-indexeddb-and-web-sql. html. [6] Confronto tra database non relazionali: Cassandra, MongoDB, CouchDB, Redis http://kkovacs.eu/cassandra-vs-mongodb-vs-couchdb-vs-redis. [7] Confronto tra database relazionali e non relazionali http://www.readwriteweb.com/enterprise/2009/02/ is-the-relational-database-doomedp2.php. [8] Informazioni specifiche relative al sistema di controllo di concorrenza MVCC ed alla sua implementazione nei vari DBMS http://devcenter.heroku.com/articles/postgresql-concurrency, http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=166957, http://dev. mysql.com/doc/refman/5.0/en/innodb-multi-versioning.html, http://msdn.microsoft.com/en-us/library/cc645937(v=SQL.100).aspx, http://www.postgresql.org/docs/6.3/static/c0503.htm, http://msdn.microsoft.com/en-us/library/ms182776.aspx. [9] Informazioni relative all’implementazione PostgreSql della routine di Vacuum http://www.postgresql.org/docs/current/static/routine-vacuuming.html# VACUUM-FOR-WRAPAROUND [10] Considerazioni sul tempo limite offerto da Unix Timestamp http://en.wikipedia.org/wiki/Year_2038_problem. 70