10 settembre 2018

Anatomia di un Data Migration Manager in Outsystems

English version

 Ovvero come trasformare un limite nell’opportunità di creare un tool generico e riutilizzabile.

Descriveremo l’architettura di un possibile Data Migration Manager realizzato con la piattaforma Outsystems. Il progetto nasce dall’esigenza di spostare dati tra DB di ambienti diversi. In seguito scopriremo che la piattaforma (nella versione cloud) non consente questa operazione in modo agevole.
L'architettura che presenteremo è migliorabile, così come gli algoritmi proposti. Le scelte implementative sono state spesso condizionate da fattori esterni.
Tuttavia il tool realizzato è generico ed efficace, seppur limitato e con problemi di efficienza per via della tecnologia a servizi REST utilizzata.
Ma grazie ad Outsystems, i tempi e i costi di realizzazione sono stati minimi.

Cos’è Outsystem?

OutSystems is a low-code platform that lets you visually develop your entire application, easily integrate with existing systems, and add your own custom code when you need it.
Questa è la definizione che troviamo sulla home page. Aggiungiamo che la piattaforma è una Paas ospitata su cloud AWS. Questo può aiutare un processo di legacy modernization sia in aziende che hanno già puntato in questa direzione, che nelle realtà che ancora devono muovere i primi passi.
Inoltre la velocità di realizzazione, i sistemi di feedback integrati, gli strumenti di gestione del codice, delle release e delle dipendenze rendono questo prodotto fortemente abilitante al DevOps, orientato alla continuos integration e al countinuos delivery.

A cosa serve?

Outsystems consente di sviluppare in modo visuale, senza scrivere codice, e di farlo molto velocemente. Le applicazioni create possono essere estese con codice custom attraverso estensioni .Net.
Inoltre gestisce l’intero ciclo di vita applicativo: version control, release e dependency management e anche database release automation (cfr Liquibase o DBmaestro), tasto spesso ancora delicato.
In pochissimi minuti siamo in grado di realizzare un’applicazione da zero e di rilasciarla in ambiente di produzione.
Ma se avessi la necessità di spostare i dati da un ambiente ad un altro?
Outsystems (nella versione cloud) non consente in modo nativo la migrazione di dati tra tabelle di ambienti diversi.
In un mondo perfetto, seguendo un corretto processo di sviluppo, questo problema non si porrebbe. Ma nel mondo reale, onestamente, capita spesso…

Friends don’t let friends manage servers

Recita così un mantra AWS. E infatti, per ovvi motivi di sicurezza, non si può accedere direttamente al DB con tool di amministrazione. Sarebbe possibile solo attraverso una VPN creata con l’aiuto del supporto tecnico Outsystems.
Esistono però altre due strade:
  1. Infosistema Data Migration Manager è una soluzione commerciale che colma proprio questa lacuna e consente anche di applicare trasformazioni ai dati in transito (ETL).
  2. La strada punk che prevede il fai da te: un Data Migration Manager home made.
Noi scegliamo la seconda strada :)

Il modello self producer-self consumer

L’idea generale parte da due assunti:
  1. una infrastruttura Outsystems è composta da più ambienti, tutti pubblicamente raggiungibili attraverso internet.
  2. le applicazioni possono esporre e consumare servizi.
Quindi i servizi esposti da un’applicazione A in ambiente di test potrebbero essere consumati dalla stessa applicazione in esecuzione anche in ambiente di sviluppo.
O meglio: l’istanza di A in sviluppo potrebbe estrarre i suoi dati e passarli, attraverso un servizio REST, all’istanza in test, secondo un modello che potremmo definire self producer - self consumer.
Ci si può lavorare. D’altra parte se lo fa Infosistema una strada deve esserci 😊

L’architettura potrebbe essere la seguente:

In questo modo però ogni singola applicazione sarebbe gravata dalla logica della migrazione il cui codice andrebbe replicato n volte, una per ogni applicazione. Ovviamente si può fare di meglio.Possiamo incapsulare la logica di migrazione in un Data Migration Tool (che chiameremo GearDMT).
Outsystems, nell’ottica del riuso, consente di definire “public” le tabelle di un modulo applicativo e di importarle ed usarle in altri moduli.
Quindi ogni applicazione definirà le sue tabelle “public” e GearDMT  le importerà come dipendenze.
Pro: soluzione generica che isola la logica di migrazione.
Contro: non elegante, viola diversi pattern, linee guida e principi di programmazione.
L’architettura diventerebbe qualcosa del genere:
Ci sono anche altre implicazioni di carattere tecnico:
  1. Ogni volta che dovremo migrare una nuova tabella si dovrà aggiungere in GearDMT la dipendenza verso le nuove tabelle e ricompilare entrambe le applicazioni.
  2. GearDMT, per poter leggere la struttura della tabella da migrare, dovrà importare anche alcune tabelle di sistema (Entity, Entity_Attr, …) che mantengono proprio queste informazioni.
  3. Per leggere e scrivere su tabelle che non conosce GearDMT deve poter eseguire query generiche e dinamiche, impossibili da scrivere con gli strumenti base messi a disposizione dalla piattaforma.

Sporchiamoci le mani

Fortunatamente abbiamo la possibilità di integrare codice custom attraverso le estensioni .Net. Potremmo dunque scrivere un'estensione che sfrutti le Runtime Public DB API della piattaforma per dialogare con il data layer.
Pro:
  • L’estensione sarebbe coinvolta in fase di lettura e in fase di scrittura;
  • consentirebbe facilmente di eseguire query dinamiche e generiche su tabelle sconosciute inferendo i tipi di dato
  • dialogando col DB a basso livello avrebbe “naturalmente” accesso a tutte le tabelle della piattaforma
  • introdurrebbe un disaccoppiamento tra l’applicazione GearDMT ed il DB evitando la necessità del consumo diretto delle tabelle
  • gestirebbe autonomamente la transazione garantendo la coerenza del dato
Qualcosa del genere:
Nel diagramma si fa riferimento anche alle LifeTime Services API che serviranno per ottenere dalla piattaforma le necessarie informazioni sugli ambienti presenti e le applicazioni installate.
Lo strato REST, oltre ad essere necessario per consentire il dialogo tra istanze dell’applicazione in esecuzione su ambienti diversi, disaccoppia ulteriormente e consente possibili evoluzioni future (alimentazione delle tabelle da sorgenti esterne ad esempio)
Ovviamente i servizi devono essere protetti in modo adeguato: sono sempre una porta di ingresso verso il DB.

Il processo

Una volta definita l’architettura il processo è molto semplice, riportiamo solo i passaggi principali.
Due step fondamentali:
  1. Verifica se sono presenti le condizioni per eseguire la migrazione e carica i dati da migrare (CheckEnvAndLoadSourceTable)
  2. Esegue la migrazione (MigrateTable)

CheckEnvAndLoadSourceTable

In dettaglio il primo step:
  1. verifica che anche in ambiente di destinazione sia presente GearDMT (attraverso il servizio REST ping)
  2. verifica che in ambiente di destinazione la tabella da migrare sia presente ed abbia la stessa struttura della tabella sorgente (Il confronto viene fatto nel metodo checkDestTable confrontando gli hash degli oggetti json che rappresentano le strutture)
  3. carica il contenuto della tabella sorgente attraverso il metodo readTable. E' proprio questo metodo che chiama i metodi di basso livello messi a disposizione dall’estensione .Net

2. migrateTable

Successivamente lo step 2 chiama il metodo migrateTable che esegue la migrazione chiamando il metodo postData in ambiente di destinazione passando in input la struttura tabellare sorgente e i dati.postData, al suo interno, invocherà di nuovo i metodi di basso livello messi a disposizione dall’estensione .Net per scrivere sulla base dati.
Ora sono più evidenti i limiti di questo approccio: i dati passano tutti insieme su una chiamata POST del protocollo HTTP. Se la tabella contiene una grande quantità di dati - grandi file ad esempio - si rischia il timeout.
Migrare una riga alla volta non migliorerebbe molto la situazione: se la tabella avesse molte righe rischieremmo di avere molte chiamate HTTP pressoché concorrenti. Potremmo introdurre una gestione request-response sincronizzata: facciamo una nuova chiamata solo quando la precedente si è già conclusa. Ma non saremmo in grado di gestire in modo efficace la transazione: ogni riga sarebbe una transazione isolata e non avremmo possibilità di eseguire una rollback completa in caso di errore.
Tutto sommato il primo approccio sembra il male minore.

Strategie di Id matching

Un aspetto fondamentale della migrazione dei dati è la garanzia di Id matching: ovviamente ci si aspetta che al record r con id x in ambiente sorgente corrisponda un record r’ del tutto identico r con id x in ambiente di destinazione. Questo non è banale perché Outsystems gestisce le chiavi primarie con campi autoincrement.
Potremmo disabilitare l’autoincrement, inserire i dati e riabilitarlo ma sfortunatamente l’utenza con cui Outsystems si collega al DB non ha privilegi sufficienti per farlo.
Una strategia per risolvere il problema può essere “Insert-delete until match
Ovvero:
1 ordina i record da copiare per id crescente
2 scrivi il primo record
3 leggi id assegnato ambiente destinazione
4 fintanto che id destinazione < id sorgente
5    elimina il record appena inserito
6    inserisci di nuovo il record
7 se invece id destinazione > id sorgente lancia un’eccezione e rollback
E’ una soluzione arrangiata, non è elegante, ma funziona.
Implementiamo anche una seconda strategia “Don’t care” che consenta l’inserimento del record a prescindere dal valore della chiave. Questa strategia, oltre a consentire test e debug di GearDMT in modo più agevole, in futuro potrà essere usata anche come strategia per l’alimentazione di tabelle da sorgenti esterne.

Id resolving per le static entities

La piattaforma distingue una tabella normale (Entity) da una tabella “tipologica” (Static Entity). Le seconde, a differenza delle prime, vengono migrate automaticamente, complete di dati, in fase di rilascio.
Purtroppo però il processo di rilascio non garantisce la conservazione della chiave primaria a parità di record.
Ad esempio supponiamo di avere in ambiente di sviluppo la Static Entity TipoFile
 Durante lo sviluppo ci accorgiamo che il tipo file “audio”, inserito per errore, non ci servirà mai e decidiamo di eliminarlo. Otteniamo quindi:
Quando rilasceremo in ambiente di test, la piattaforma genererà i record in questo modo:
Quando useremo GearDMT per migrare i dati da sviluppo a test, tutti i file di tipo “immagine” diventeranno “video” e l’inserimento di quelli di tipo “video” causerebbe una reference key violation.
Per risolvere questo problema dobbiamo tentare la risoluzione dell’Id del record nell’ambiente destinazione partendo dall'attributo Label.
Consideriamo un’ipotetica riga della tabella File:
Prima di scrivere il record cerchiamo nell’ambiente di destinazione l’Id del tipoFile che abbia label = “immagine”. Il risultato sarebbe Id 2. E quindi il record da scrivere sarebbe:
E' una strategia molto debole e fallace. Ma anche in questo caso non abbiamo altre alternative.

Limiti e Assunti

GearDMT è un Data Migration Tool nato per risolvere rapidamente un’esigenza con costi minimi. Non ha pretese di essere un DMM, è limitato e migliorabile. Non può e non vuole essere un’architettura di riferimento.
Il corretto funzionamento dipende dal rispetto di alcuni assunti e condizioni preliminari:
  1. La struttura della chiave deve essere standard (un solo attributo chiave primaria chiamato Id di tipo autoincrement)
  2. Se si usa la strategia insert-delete la tabella di destinazione deve essere vuota e con indice resettato.
Inoltre ha dei punti critici legati ai vincoli architetturali:
L’application server si fa carico di tutta la migrazione. Il costo computazionale potrebbe essere troppo alto e impattare su tutte le applicazioni in esecuzione sullo stesso server causando rallentamenti o interruzioni di servizio.
I dati di una tabella viaggiano interamente all’interno di una chiamata a servizi REST. Si sfrutta il protocollo HTTP mettendo i record nel body di una POST request.
L’alternativa può essere quella di fare una chiamata per ogni record della tabella da scrivere.
Questo approccio però non consentirebbe di gestire la transazione in modo efficace: un’eccezione causerebbe la rollback dell’ultimo record inserito e non dell’intero dataset. Il primo approccio però soffre di problemi di efficienza ed è esposto a errori di timeout nel caso di trasferimenti di grandi quantità di dati.
Infine anche il meccanismo di risoluzione degli id delle Static Entities a partire dagli attributi label è evidentemente fallace e si basa sull’assunto, di buon senso, ma debole, che in una Static Entity non esistano due o più record con lo stesso valore per l’attributo label.

Evoluzioni

Lo sviluppo di GearDMT ci ha permesso di approfondire la conoscenza di Outsystems sfruttando anche le sue caratteristiche più avanzate e introducendo una forte customizzazione del prodotto e il tool è stato usato con successo in diverse occasioni.
C'è ancora spazio per evoluzioni future:
  • Si potrebbe sfruttare il codice di ritorno HTTP 102 Processing. Potrebbe limitare il rischio di timeout lato client durante un trasferimento oneroso di dati.
  • Se ci fosse la necessità di applicare trasformazioni ai dati in transito potremmo introdurre un componente di ETL. E se questo fosse estendibile attraverso plugin riusciremmo a far fronte a necessità future non immediatamente prevedibili.
  • Inoltre i servizi esposti possono essere chiamati da qualsiasi applicazione. Immaginiamo di utilizzare Outsystems in un contesto di legacy modernization per riscrivere un’applicazione obsoleta in tempi rapidi. Ovviamente non vogliamo perdere i dati pregressi. Nulla ci vieta di chiamare i servizi di GearDMT per alimentare la nuova applicazione da un batch o un'applicazione esterna.