Offline Management (Pt.1)
Non esiste la parola fine. Viviamo in un universo che cicla all’infinito lo spazio-tempo, quindi non può esistere la parola fine. Non può esistere nemmeno per quelle che sono le richieste del cliente. Niente potrà frapporsi tra lui e il progresso, nemmeno una semplice web app che possa funzionare senza web, ma che non è un app. Forse i service workers sono nati proprio perché la creazione non si accontenta mai del proprio operato.
Cosa potrà mai andare storto se la mia web app, nata basandosi sul TCP e la fibra ottica, ora ha la possibilità di funzionare anche offline?
Ecco alcune riflessioni da considerare:
- Data caching e cache invalidation
I dati non possono più essere recuperati dal server, ma coesistono sui client. Ciò significa che potrebbero differire, siccome sono in due posti diversi. Ci si dovrà quindi occupare di mantenerli sincronizzati, sia quando cambiano sul client, sia quando cambiano sul server. - Authentication
Essendo offline non ho modo di validare il mio JWT, o non ho modo di sapere fino a quando possa essere valido. - In diversi scenari può accadere che la stessa risorsa possa essere modificata da due dispositivi diversi e in modi diversi. Potrei avere un documento che ho modificato dal cellulare in treno stamattina, sono arrivato a casa e lo ho modificato dal mio laptop senza accorgermi che mancano le nuove modifiche, cosa succederà quando il cellulare cercherà di sincronizzare le modifiche? Un merge conflict per il nostro povero utente?
- In alcuni casi la connessione potrebbe non mancare completamente, potrebbe essere la classica fievole connessione che c’è nell’angolo in fondo a sinistra dell’ufficio. Alcune richieste potrebbero andare a buon fine e altre no. Non è semplice capire se è possibile riprovare una richiesta fallita oppure aspettare perchè la connessione è quasi inesistente.
Proprio riflettendo su tutto questo, mi sono imbattuto in un interessante articolo che descriveva questo modo di operare per supportare l’offline in una webApp con quelle che loro chiamano modifiersQueue. Ho voluto dare la mia interpretazione.
Concetto base
Si suppone che nell’applicazione ogni interazione utente si possa esprimere come una modifica, grande o piccola che sia, ai dati che compongono il sistema. Ogni qualvolta che l’utente intraprende un’azione essa viene rappresentata da un’istanza della classe Modifier. Tale istanza sarà poi inserita all’interno di una coda e servirà poi per applicare i cambiamenti ai dati immagazzinati nel dispositivo (per garantire il funzionamento offline) e quelli centralizzati sul server.
Di fatto non esiste una sola classe modifier, bensì tante quante sono le operazioni per cui si vuole garantire il funzionamento offline. La classe Modifier si può quindi definire astratta (come un’abstract factory). Nell’articolo di parla quindi di una classe così composta:
abstract class Modifier{ modify(args) persist(args) }
Persist sarebbe un metodo che applica il modifier lato server, modify invece lo applica ai dati disponibili offline lato client.
Nell’articolo si dice che modify è una pure function, ovvero una funzione che si comporta sempre nello stesso modo a parità di parametri, e che persist è idempotent, che significa che questa può essere chiamata 1 o più volte arbitrariamente senza ottenere effetti collaterali.
Queste due proprietà permettono di considerare un Modifier indipendente dal resto e capace di riprovare più volte in caso di fallimento.
Modifier queue
Quando creo un’istanza di un Modifier la devo inserire in una coda, che opera in modalità FIFO: i modifier devono essere eseguiti in ordine.
Ci deve essere quindi un meccanismo per cui questa coda venga consumata, e si conosca il modifier a cui ogni dispositivo è arrivato a consumare.
In pratica è necessario sapere lo stato di un modifier: se è stato applicato il modify, se è stato applicato il persist, se il persist è fallito e se è già stato riprovato.
È bene notare, come riportato nell’articolo, che il modify è un operazione che non ha a che fare con la rete e che difficilmente avrà esito negativo, a meno chiaramente di failure interni al dispositivo o di errori nel codice. Di fatto un modifier non ha quindi senso di esistere se il suo modify non è stato lanciato, uno stato in meno da considerare.
Data sync
Le ModifierQueue sono un metodo interessante per poter gestire l’offline, è come se tenessimo un journal delle azioni compiute dall’utente, lo utilizzassimo poi per assicurarci che effettivamente queste azioni riflettano sia sulla UI sia sui dati nel cloud.
Quello che manca nell’articolo a mio parere è un metodo che utilizzi i modifier per sincronizzare i dati: invalidare la cache locale ed aggiornarla, sincronizzare altri dispositivi agli stessi dati, gestire il back-online di un dispositivo.
In pratica nell’articolo è teorizzato il modo per sincronizzare i dati dal client al server, non viceversa.
Le possibilità sono queste:
- Il client chiede al server gli ultimi dati per una certa risorsa, elimina quelli locali e li sostituisce con quelli centrali.
- Il server manda al client un messaggio con dei nuovi dati per una certa risorsa, il client sostituisce i propri dati con quelli ricevuti.
In entrambi i casi il sync è indipendente dalla coda di modifier: è possibile sincronizzare una risorsa indipendentemente da quali modifier sono stati applicati in precedenza oppure verrano applicati nel futuro, a patto che alla fine i dati risultino coerenti con quelli sul server.
Ho deciso di chiamare la sync descritta nel punto 1 hard sync, quella descritta nel punto 2 soft sync.
Ci sono alcune osservazioni su entrambe:
- In caso di hard sync il carico del server è maggiore. Per riuscire a syncare un dato occorre cercarlo, serializzarlo e poi inviarlo, tutte le volte che esso cambia.
- In caso di hard sync è difficile lavorare con grandi quantità di dati. Ogni volta che vengono chiesti al server l’intenzione è quella di buttare via quelli esistenti (ricordiamoci sempre che comunque abbiamo i modifier e che non perdiamo le modifiche locali) e ricrearli in base a quelli ricevuti. In un caso in cui si parla di GB di dati forse l’operazione, se poi diventa anche frequente, potrebbe essere troppo invasiva.
- In caso di hard sync l’implementazione è più semplice. Non c’è nessuna logica da seguire per applicare di dati localmente: elimino e ricreo.
- In caso di hard sync e di chiamata andata in errore, è possibile in qualunque caso decidere di riprovare a richiedere i dati, che saranno quindi sempre aggiornati.
- In casi di soft sync, il dato può essere trasmesso dal server causando un cambiamento real-time. Quando cambia viene inviato, così la UI aggiorna.
- In caso di soft sync il pericolo di avere dati incoerenti aumenta: può capitare che un messaggio venga perso, che il client non riceva correttamente i dati dal server, per qualche problema di connessione possa perdere qualche messaggio, che possa proprio essere offline mentre il server trasmette.
- La logica di modifica ai dati locali deve essere implementata per ogni azione che il server riporta sui dati.
La domanda cruciale ora è: quando conviene fare una hard sync, quando conviene fare una soft sync?
Pianificare le sync
Quando un dispositivo avvia l’applicazione è sempre meglio fare un’hard sync su tutto ciò che è salvato localmente. A volte la quantità di dati che bisognerebbe aggiornare è troppa, si può adottare quindi una lazy sync, per la quale una risorsa viene aggiornata solo quando richiesta.
Da quando il dispositivo passa online allora si possono utilizzare solo soft sync, con i dati che il server invia. Nel caso ci fosse una disconnessione improvvisa non potremo essere sicuri di quello che è stato inviato, quindi una volta ritornati online occorrerà di nuovo ricorrere ad un hard sync, come se l’applicazione si riavviasse. Siccome con i modifier i cambiamenti li applichiamo sia localmente sia poi nel cloud, non ci dobbiamo preoccupare di quello che accadrà con essi. Un hard sync ce li riporterà se il persist è già stato applicato, ma li avremo già sul nostro dispositivo. Sarà raro che il persist non venga applicato nel caso in cui il dispositivo è connesso per ricevere un risultato da un’hard sync, significa che non ci sarà un caso di dovremo syncare i dati prima di applicare un persist, lo faremo solo dopo.
Conclusione
Sarebbe utile avere un’unico punto dove vengono definiti i comportamenti riguardo alle sync, come avvenuto per i modifier. Verrebbe comodo quindi avere, come esiste un modifier, un Syncer. Questa classe dovrà essere in grado di gestire i messaggi che arrivano riguardo quell’entità (soft sync) e di sostituire i dati locali con quelli sul server. Sarebbe oltretutto corretto avere un ordine di esecuzione dei modifier e dei syncer, inserendoli tutti nella stessa coda.