Creare un podcast in automatico a partire da audio vocali e musica

Nel 2011, mentre studiavo architettura al Politecnico di Torino e vivevo nel collegio universitario di Grugliasco, mi sono imbattuto in una delle più belle esperienze della mia vita. Insieme al mio amico Angelo ho progettato e realizzato una webradio studentesca che trasmetteva direttamente dal collegio. Il progetto, durato 5 anni è stato di grande scuola per me, sia dal punto di vista umano, sia professionale. Ero il classico “tecnico tutto fare” 🧑‍🔧 : in quel periodo mi sono occupato di piccoli software radiofonici, della programmazione e della gestione del palinsesto, ma ovviamente anche nella manutenzione del server della radio e dello studio dal quale trasmettevamo. Ho conosciuto piu di 60 ragazzi, divisi in gruppi di lavoro, uno per tramissione!

Nell’ultimo periodo (coinciso più o meno con l’ascesa dei podcast su Spotify) è tornata la mia curiosità verso il mondo della radio. Mi sono chiesto quanto fosse possibile migliorare/ottimizzare il processo di creazione di una trasmissione radiofonica o un podcast. Avevo già trattato quest’argomento in un precedente articolo in cui mi concentravo sul come mixare la voce di uno speaker insieme ad altri suoni. In questi giorni ho realizzato un piccolo script che passo passo crea un’intera trasmissione a partire da una cartella di files.

Struttura di una puntata radiofonica

Per realizzare questo piccolo progetto ho utilizzato FFMPEG, nota libreria per la manipolazione del suono (leggi il mio articolo al riguardo con degli esempi d’uso). Questa libreria permette di applicare dei filtri al suono in maniera programmatica, quindi attraverso un linguaggio di programmazione. Studiando la ricchissima documentazione (e con l’aiuto dell’ottima community di FFMPEG) sono riuscito a realizzare questo Batch che programmaticamente mette in sequenza dei file audio, tramite l’uso di dissolvenze o somme di suoni. Vediamo uno schema:

Traducendo questo semplice schema in un codice programmabile, questo somiglia ad un array di oggetti (file audio) che dovranno essere messi in sequenza, ma a certe condizioni:

  • il parlato dovrà sempre avere un sottofondo leggero di musica
  • i brani partiranno con una dissolvenza (fade) nel momento in cui il parlato sta per finire
  • il parlato comincia un momento prima del termine del brano

Ecco uno schema piu preciso di quello che succederà:

Script per elaborare il parlato con un sottofondo

Questo script va eseguito con l’ultima versione di NodeJs installata, e richiede diverse librerie esterne, la piu importante è fluent-ffmpeg, ovvero un wrapper di FFMPEG per NodeJs. Lo script converte una serie di audio registrati con solo voce in diversi file in cui la voce è accompagnata da un sottofondo musicale leggero che parte e finisce in dissolvenza.

var mp3Duration = require('mp3-duration');
var ffmpeg = require('fluent-ffmpeg');
var fs = require('fs')
 
let array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; //un generico array di file audio di voce
 
let promises = [];
 
array.forEach(speech => {
 
    let speechFile = speech + '.mp3';
    let backgroundFile = 'sottofondo.mp3';
    let backgroundLooped = speech + '_onlybackground.mp3';
    let speechWithBackground = speech + '_.mp3'
 
    let p = mp3Duration(speechFile, function (err, dur) {
 
        //recupero la durata dello speech
        let durataVoce = Math.round(dur);
 
        //creo un loop della durata dello speech
        ffmpeg(backgroundFile)
            .inputOption("-stream_loop -1") //loop infinito del sottofondo
            .audioFilters('afade=t=in:ss=0:d=8')  //fadein che dura 8 secondi
            .audioFilters('afade=t=out:st=' + (durataVoce - 10) + ':d=5')   //fadeout che dura 8 secondi e parte 10 secondi prima della fine dell'audio
            .duration(durataVoce) //taglio alla durata della voce
            .audioBitrate(320)
            .output(backgroundLooped)
            .on('end', function () {
 
                //faccio un mix con la voce
 
                ffmpeg()
                    .addInput(backgroundLooped)
                    .addInput(speechFile)
                    .complexFilter(['[1]compand=attacks=0.4:points=-80/-80|-12.4/-12.4|-6/-8|0/-6.8|20/-2.8:gain=12:volume=-40[a1]',
                        '[a1]channelsplit=channel_layout=stereo:channels=FR[a2]',
                        '[0][a2]amix=inputs=2:dropout_transition=0.2:weights=1 10,dynaudnorm'])
                    .audioBitrate(320)
                    .save(speechWithBackground)
                    .on('end', function () {
                        //rimuovo il file del background
                        console.log(speech + " - Completato");
                        fs.unlinkSync(backgroundLooped);
 
                    });
            }).run();
 
    });
 
    promises.push(p);
});

Script per mettere in sequenza i file audio

Una volta terminata l’elaborazione dei file audio del parlato, non ci resta che mixare il tutto seguendo lo schema precedente. I brani saranno leggermente anticipati rispetto al termine del parlato, in modo da accentuare l’effetto radiofonico finale. Ogni brano inizierà e finirà con una dissolvenza automatica di 10 secondi (impostata nella variabile fadeDuration). Inoltre verrà applicato un filtro normalizzazione audio, che consentirà di assottigliare le differenze di volume tra i brani, migliorando l’esperienza di ascolto.

var ffmpeg = require('fluent-ffmpeg');
 
let speeches = [
    {
        tipo: 'musica',
        file: "sigla.mp3"
    },
    {
        tipo: 'voce',
        file: "1_.mp3"
    },
    {
        tipo: 'musica',
        file: "musica1.mp3"
    },
    {
        tipo: 'voce',
        file: "2_.mp3"
    },
    {
        tipo: 'musica',
        file: "musica2.mp3"
    }
]
 
 
let command = ffmpeg();
let filters = [];
let combo = 0;
let fadeDuration = 10;
 
for (let i = 0; i < speeches.length; i++) {
 
    //aggiungo l'input
 
    speech = speeches[i];
    command.addInput(speech.file);
    successivo = i + 1;
 
    //se sono all'ultimo non faccio niente perche mi interessa la combo precedente
    if (i < speeches.length - 1) {
 
        //se l'attuale è voce
        if (speech.tipo == 'voce') {
 
            //abbasso il volume della musica successiva
            filters.push('[' + successivo + ']volume=0.3[low' + successivo + ']');
 
            if (i < 1) {
                filters.push('[' + i + '][low' + successivo + ']acrossfade=d=' + fadeDuration + ':c1=nofade:c2=exp[combo' + combo + ']');
            } else {
 
                output = '[combo' + (combo + 1) + ']';
                if (successivo == speeches.length - 1) {
                    output = ",loudnorm";
                }
 
                filters.push('[combo' + combo + '][low' + successivo + ']acrossfade=d=' + fadeDuration + ':c1=nofade:c2=exp' + output);
                combo++;
            }
 
 
            //se l'attuale è musica
        } else if (speech.tipo == 'musica') {
 
            if (i < 1) {
                filters.push('[' + i + ']volume=0.4[low' + i + ']');
                filters.push('[low' + i + '][' + successivo + ']acrossfade=d=' + fadeDuration + ':c1=exp:c2=nofade[combo' + combo + ']');
            } else {
 
                output = '[combo' + (combo + 1) + ']';
                if (successivo == speeches.length - 1) {
                    output = ",loudnorm";
                }
 
                filters.push('[combo' + combo + '][' + successivo + ']acrossfade=d=' + fadeDuration + ':c1=exp:c2=nofade' + output);
                combo++;
            }
 
        }
    }
 
}
 
command.complexFilter(filters)
    .save("totale1.mp3")
    .audioBitrate(320)
    .on('end', function () {
        //rimuovo il file del background
        console.log(" - Completato");
    });

Risultato

Ho utilizzato questo piccolo script per realizzare una trasmissione di prova, a partire da un file vocale registrato con lo smartphone. Chiaramente è un progetto ancora molto semplice, ma che crea interessanti spunti su come automatizzare il processo di missaggio (mix) di puntate radiofoniche o podcast.

Articolo a cura di Carlo Peluso, 18.05.2021

Link articolo originale: https://straquenzu.p3lus0s.net/creare-un-podcast-in-automatico-a-partire-da-audio-vocali-e-musica

#jointherevolution

Introduzione alla teoria dei grafi per la teoria dei giochi

Nell’articolo precedente (https://orbyta.it/teoria-dei-giochi/) abbiamo illustrato i principi base della teoria dei giochi e, tramite qualche esempio, abbiamo scoperto come può essere utilizzata per attuare la strategia più conveniente in una situazione in cui il guadagno finale dipende dalle mosse effettuate dagli altri giocatori.

In questo secondo articolo scopriremo cos’è la teoria dei grafi e come può essere integrata nella teoria dei giochi. Inoltre, introdurremo un altro tipo di equilibrio di Nash, ovvero gli equilibri di Nash per strategie miste.

La teoria dei grafi

I grafi possono essere utilizzati per schematizzare delle situazioni o processi e ne consentono l’analisi in termini quantitativi e algoritmici.

Tecnicamente, un grafo (o rete) G è una coppia (V, E) dove V è un insieme finito i cui elementi sono detti vertici o nodi ed E è un sottoinsieme i cui elementi, detti archi o lati, sono coppie di oggetti in V.

Per esempio, nella seguente immagine vediamo un grafo con vertici V={A,B,C,D} ed archi E={(A,B), (B,A), (B,D), (D,B), (D,C), (C,D), (B,C), (C,B)}.

Una digrafo è un grafo che possiede almeno un arco orientato, cioè un arco caratterizzato da un verso che non può essere percorso nel verso opposto.

Per esempio modificando il grafo precedente come segue otteniamo un digrafo con archi E={(B,A), (B,D), (D,B), (D,C), (C,D), (B,C), (C,B)}.

Introduciamo ora qualche esempio pratico per capire come i grafi possono essere utilizzati nella teoria dei giochi.

Esempio numerico [4]

Consideriamo una situazione in cui sono presenti due giocatori e il primo di essi, A, sceglie un numero tra 1,2 e 3. Il secondo partecipante, B, somma al numero detto 1,2 o 3. A farà lo stesso nel turno successivo e così via finché uno dei due arriverà ad esclamare 31 vincendo.
Tale gioco può essere rappresentato tramite il seguente digrafo:

I vertici sono i numeri naturali dall’1 al 31 e gli archi che partono dal vertice n entrano in quelli n+1, n+2 e n+3 se 1 ≤ n ≤ 28. Dal 29 escono degli archi entranti in 30 e 31, dal 30 in 31 e dal 31 nessuno.

Dunque, il giocatore che riesce a dire 27 ha vinto perché il giocatore successivo potrà selezionare solo i vertici 28, 29, o 30 e, in ognuno di questi casi, il primo riuscirà a posizionarsi sul 31.

Possiamo quindi porci come obiettivo di arrivare al 27 e non al 31. Ma a questo punto chi dirà 23 riuscirà a vincere e, applicando il ragionamento iterativamente, concludiamo che i nodi da toccare per vincere sono X = {3, 7, 11, 15, 19, 23, 27, 31}.

Osserviamo che:

  • Se un giocatore dice un numero non appartenente a X allora l’avversario ha sempre la possibilità di farlo e dunque di vincere.
  • Se un giocatore dice un numero appartenente a X allora l’avversario non potrà che scegliere un numero ad esso non appartenente.

In conclusione, analizzando il grafo, scopriamo che l’obiettivo è quello di occupare sempre le posizioni di X.

Esempio della protesta

Supponiamo di voler manifestare contro una data azione di un governo dittatoriale. Tale evento risulterà vantaggioso solo se il numero di manifestanti sarà sufficientemente elevato, d’altro canto se ciò non dovesse accadere andremmo in contro ad un payoff assai negativo in quanto potremmo supporre che, in tal caso, lo stato sederà la manifestazione in modo violento.

Supponiamo che ogni persona decida di partecipare alla protesta solo se sa che almeno un numero sufficiente di cittadini vi aderirà.

Supponiamo di avere 4 cittadini e rappresentiamo il fatto che il cittadino w conosca il comportamento di quello u e viceversa con la presenza del lato (w,u). La cifra accanto a un nodo indica il numero minimo di manifestanti totali affinché il nodo in questione si unisca all’impresa.

Consideriamo le seguenti situazioni:

Notiamo che nel caso A la rivolta non avrà luogo in quanto ognuno dei partecipanti non ha modo di sapere il comportamento che adotterà il nodo ad esso non collegato.

Nel caso B invece ognuno tra u, v e w saprà che esistono almeno altri due nodi che richiedono almeno tre partecipanti totali e che a loro volta hanno questa informazione. Dunque la protesta si svolgerà, in particolare u, v e w saranno i manifestanti.

Equilibrio di Nash per strategie miste

Esistono dei giochi in cui non sono presenti gli equilibri di Nash che abbiamo introdotto nell’articolo precedente, ovvero quelli basati su strategia pure. Una strategia pura infatti fornisce una descrizione completa del modo in cui un individuo gioca una partita. In particolare, essa determina quale scelta farà il giocatore in qualsiasi situazione che potrebbe affrontare.

Una strategia mista per un giocatore è una distribuzione di probabilità sull’insieme delle strategie pure che ha a disposizione. Ogni strategia pura P può essere vista come un caso particolare di strategia mista che assegna probabilità pari a 1 a P e pari a 0 a tutte le altre strategie pure.

Abbiamo quindi due tipi di equilibri di Nash: quelli per le strategie pure, analizzati finora, si hanno quando tutti i giocatori hanno a disposizione solo strategie pure, altrimenti si parla di equilibri di Nash per strategie miste.

Limitandoci ad un gioco a due partecipanti, un equilibrio di Nash per le strategie miste è una coppia di scelte (che ora sono probabilità) tale che ognuna sia la miglior risposta all’altra.

Introduciamo un esempio.

Esempio dell’attaccante/difensore

Si consideri un gioco in cui c’è un attaccante A e un difensore D.

L’attaccante può scegliere tra le strategie di attacco a1 o a2 mentre il difensore può difendersi da a1, e quindi applicare la strategia d1, o viceversa difendersi da a2, e quindi applicare la strategia d2.

Supponiamo che:

  • se D scegliesse la giusta strategia di difesa, cioè di contro ai (i = 1, 2), avremmo per A un guadagno di 0;
  • se D scegliesse d1 e A scegliesse a2, A otterrebbe un guadagno di 5 e D una perdita pari;
  • se D scegliesse d2 mentre A scegliesse a1, A ricaverebbe un guadagno di 10 e D una perdita pari.


Riassumiamo la situazione del gioco nella seguente tabella indicando in ogni riquadro: a sinistra della virgola il guadagno ottenuto da A e a destra della virgola quello ottenuto da D.

Nel nostro esempio a1 e a2 sono le strategie pure a disposizione dell’attaccante, mentre difendere da a1 e difendere da a2 quelle del difensore.

Dato che i due giocatori otterrebbero dei guadagni nettamente contrastanti potremmo concludere che in questo genere di giochi (detti strettamente competitivi) non esistano equilibri di Nash.

Se uno dei due giocatori sapesse il comportamento dell’altro allora potrebbe scegliere la strategia atta a massimizzare il suo profitto e inevitabilmente a minimizzare quello dell’avversario. Ognuno dei giocatori cercherà perciò di rendere imprevedibile la propria strategia.

Sia p la probabilità che A scelga a1 e q la probabilità che D scelga d1. Per ora sappiamo solo che esiste almeno un equilibrio per le strategie miste ma non quali debbano essere i valori effettivi di p e q.

Usiamo il principio secondo cui un equilibrio misto sorge quando le probabilità utilizzate da ciascun giocatore fanno sì che il suo avversario non abbia motivo di preferire una delle due opzioni disponibili all’altra.

Se supponiamo che D abbia una probabilità q di giocare d1 allora abbiamo che i possibili guadagni per A sono:

  • (0)(q) + (10)(1 − q) = 10 − 10q se scegliesse a1;
  •  (5)(q) + (0)(1 − q) = 5q se scegliesse a2.

Per far in modo che per A sia indifferente scegliere tra a1 e a2 imponiamo 10−10q = 5q da cui q = 2/3.

Ora supponiamo che A abbia una probabilità p di mettere in atto a1. I possibili guadagni per D allora sono:

  • (0)(p) + (−5)(1 − p) = 5p − 5 se scegliesse d1
  • (−10)(p) + (0)(1 − p) = −10p se scegliesse d2

Imponendo 5p − 5 = −10p otteniamo p = 1/3.

Quindi abbiamo che gli unici possibili valori di probabilità che possono apparire nell’equilibrio per la strategia mista sono p = 1/3 per l’attaccante e q = 2/3 per il difensore.

Si noti inoltre che il guadagno atteso di A nel caso in cui scelga a1 e D scelga d1 è di 10/3 e quello di D è di -10/3.

Ciò ci suggerisce un’analisi controintuitiva: la probabilità di A di sferrare l’attacco più forte è di un terzo, ovvero, in un modello continuo, potremmo immaginare che solo per un terzo del tempo A provi ad attaccare con a1.

Perché usare così poco la strategia più potente?

La risposta è che se A provasse sempre ad attaccare con a1 allora D sarebbe persuaso a rispondere spesso con d1, il che ridurrebbe il payoff atteso da A. D’altro canto si consideri che poiché p = 1/3 fa sì che D scelga senza preferenza una delle due strategie e abbiamo che, quando A usa tale valore di probabilità, allora, indipendentemente dalle scelte di D, esso si assicura un payoff di 10/3.

Esempio delle imprese

Consideriamo ora il fatto che anche se un giocatore non ha una strategia dominante, esso potrebbe avere strategie che sono dominate da altre. Si consideri il seguente esempio.

Supponiamo che due imprese, F1 e F2, stiano progettando di aprire un negozio in una delle sei città situate lungo sei uscite consecutive su una strada. Possiamo rappresentare la disposizione di queste città utilizzando un grafo a sei nodi come quello nella figura sottostante [2].

Supponiamo che F1 possa aprire il suo negozio in A, C o E mentre F2 in B, D o F. Una volta che i negozi apriranno, i clienti delle varie città faranno compere nel centro a loro più vicino. Si assuma che ogni città abbia lo stesso numero di clienti e che i guadagni dei negozi siano direttamente proporzionali al numero di clienti attirati. Otteniamo facilmente la tabella di guadagni sottostante.

Si può verificare che nessuno dei giocatori ha una strategia dominante: per esempio se F1 scegliesse la locazione A allora la risposta strettamente migliore di F2 sarebbe B, mentre se F1 scegliesse E la risposta strettamente migliore di F2 sarebbe D. Nonostante ciò notiamo che A è una strategia strettamente dominata per F1, infatti in ogni situazione in cui F1 ha l’opzione di scegliere A, esso riceverà un guadagno strettamente migliore scegliendo C. Analogamente F è una strategia strettamente dominata per F2. Abbiamo dunque che F1 non sceglierà A e F2 non sceglierà F. Possiamo a questo punto non considerare i nodi F e A, e da ciò ricaviamo la seguente tabella di payoff:

B ed E divengono rispettivamente le nuove strategie strettamente dominate per F2 e F1 e quindi possono essere a loro volta eliminate. Arriviamo alla conclusione che F1 sceglierà C e F2 D.

Questo modo di procedere è chiamato cancellazione iterativa delle strategie strettamente dominate. Notiamo inoltre che la coppia (C,D) costituisce l’unico equilibrio di Nash del gioco ed infatti questa metodologia di studio è anche utile a trovare gli equilibri di Nash. Generalizziamo di seguito il processo appena presentato.

Dato un numero arbitrario di n giocatori abbiamo che la cancellazione iterativa delle strategie strettamente dominate procede come segue:

  1. Si parte da un giocatore, si trovano tutte le sue strategie strettamente dominate e le si eliminano;
  2. Si considera il gioco semplificato ottenuto. Si eliminano eventuali nuove strategie strettamente dominate;
  3. Si itera il processo finché non si trovano più strategie strettamente dominate.

Si può dimostrare che l’insieme degli equilibri di Nash della versione originale del gioco coincide con quello della versione finale così ottenuta.

Consideriamo un problema in cui due giocatori A e B possano optare per la strategia a o b con i seguenti guadagni simmetrici:

In questo caso a è una strategia debolmente dominata poiché in ogni caso ogni giocatore può solo migliorare il suo guadagno scegliendo b. Inoltre si noti che (b,b) è un equilibrio di Nash.

Quando abbiamo delle strategie debolmente dominate non è consigliabile procedere con il metodo della cancellazione, poiché questa operazione potrebbe distruggere degli equilibri.

D’altro canto è intuitivo pensare che nessun giocatore scelga di assecondare l’equilibrio (a,a) composto da strategie debolmente dominate se non ha modo di prevedere il comportamento dell’altro giocatore: perché non usare la strategia (b,b) che nel peggiore dei casi comunque non inficia il  guadagno?

In questo articolo abbiamo visto come la teoria dei grafi può essere usata in quella dei giochi andando a risolvere giochi che hanno dato risultati controintuitivi. Nel prossimo articolo, approfondiremo le reti andando a studiare situazioni più complesse che ci permetteranno di analizzare la propagazione delle strategie all’interno di una rete.

Bibliografia e sitografia

[2] D. Easley e J. Kleinberg, Networks, Crowds, and Markets: Reasoning about a Highly Con- nected World, Cambridge University Press, 2010.

[3] R. Gibbons, Teoria dei giochi, Bologna, Il Mulino, 2005.

[4] E. Martìn Novo e A. Mendez Alonso, Aplicaciones de la teoría de grafos a algunos juegos de estrategia, numero 64 di Suma, Universidad Politécnica de Madrid, 2004.

[8] P. Serafini, Teoria dei Grafi e dei Giochi, a.a. 2014-15 (revisione: 28 novembre 2014).

[9] I. S. Stievano e M. Biey, Cascading behavior in networks, DET, Politecnico di Torino, 2015.

[10] I. S. Stievano e M. Biey, Interactions within a network, DET, Politecnico di Torino, 2015.

[11] I. S. Stievano, M. Biey e F. Corinto, Reti e sistemi complessi, DET, Politecnico di Torino, 2015.

[12] A. Ziggioto e A. Piana, Modello di Lotka-Volterra, reperibile all’indirizzo http://www.itismajo.it/matematica/Lezioni/Vecchi%20Documenti%20a.s.%202011-12/Modello

%20di%20Lotka-Volterra.pdf, consultato il 15/05/2015.

 [15] http://web.econ.unito.it/vannoni/docs/thgiochi.pdf consultato il 14/05/2015.

Articolo a cura di Monica Mura e Carla Melia, Data Scientist in Orbyta Tech, 01.05.2021

#jointherevolution

Introduzione alla teoria dei giochi: equilibri di Nash ed equilibri multipli

La teoria dei giochi è quella scienza matematica sviluppata al fine di capire quale sia la strategia migliore che un soggetto possa attuare in situazioni che mutano non solo al variare delle sue decisioni ma anche di quelle dei soggetti a esso connessi. Tale teoria, com’è facilmente intuibile, viene applicata a molti ambiti, per esempio quelli in cui ci si prefigge di studiare un piano di marketing o una politica proficua.

Un esempio che consigliamo si può trovare al seguente link: 

Definiamo gioco una qualsiasi situazione in cui:

  • Esiste un insieme di partecipati che chiameremo giocatori;
  • Ogni giocatore ha a disposizione una serie di possibili opzioni di comportamento che chiameremo strategie;
  • Per ogni scelta di strategie ogni giocatore riceve un guadagno, detto anche payoff, generalmente rappresentato da un numero.

Ogni giocatore in generale cercherà di massimizzare il suo profitto ma non è detto che il suo unico interesse sia questo, infatti, potrebbe anche cercare si massimizzare il tornaconto di altri giocatori e, in tal caso, il concetto di guadagno andrà rivisto affinché esso descriva con completezza il grado di soddisfazione del soggetto. 

Nella teoria dei giochi è fondamentale il concetto dell’equilibrio di Nash sviluppato da John F. Nash, economista e matematico statunitense, i cui studi all’interno di questo ambito hanno portato allo sviluppo di quello che viene chiamato, il “dilemma del prigioniero”. Di seguito vediamo una sua rappresentazione schematica.

How to win at game theory | New Scientist

Rimandiamo al seguente video per una spiegazione più estesa di questo Dilemma:

Ora introduciamo qualche nuova definizione.

Una scelta di strategie, una per ogni giocatore, è socialmente ottimale se massimizza la somma dei guadagni di tutti i giocatori, mentre è Pareto-ottimale se non esiste un’altra combinazione di mosse tale che migliori i payoff di almeno un giocatore senza diminuire quello degli altri. Non lo dimostreremo qua per motivi di brevità ma se una soluzione è socialmente ottimale allora è anche Pareto-ottimale

Supporremo che ogni giocatore conosca integralmente la struttura del gioco (cioè che il gioco sia completo) e quindi che sia a conoscenza di tutte le possibili strategie e guadagni di ogni partecipante. Inoltre, assumeremo che ogni partecipante sia intelligente (cioè in grado di capire, senza commettere errori, dato un insieme di strategie possibili quale sia la più conveniente) e razionale (cioè tale che, una volta riconosciuta la/le strategia/e a massimo profitto la/le preferisca alle altre [14]).

Modelli in cui tali assunzioni non vengono fatte possono divenire estremamente complicati e non li approfondiremo nel corso di questa trattazione. 

Si consideri il seguente esempio tratto dal sesto capitolo di [2].

Esempio degli studenti

Helpful Study Partner | Funny dog memes, Dog quotes funny, Funny animals

Uno studente deve affrontare un esame e una presentazione per il giorno seguente ma non può prepararsi adeguatamente per entrambe. Per semplicità supponiamo che egli sia in grado stimare con ottima precisione quale sarà il voto ottenuto (calcolato in centesimi) da entrambe le prove al variare della sua preparazione. 

In particolare, per quel che riguarda l’esame lo studente si aspetta una votazione pari a 92 se studia e di 80 altrimenti. La presentazione, invece, deve essere fatta con un compagno e nel caso in cui entrambi lavorino su di essa la votazione relativa sarà di 100, se solo uno dei due (indipendentemente da chi) ci lavorasse di 92 e se non lo facesse nessuno di 84. Anche il compagno deve scegliere se studiare per l’esame o concentrarsi sulla preparazione e le sue previsioni sui voti sono le stesse.

Si presuma infine che i due compagni non possano comunicare e quindi mettersi d’accordo sul da farsi. L’obiettivo per entrambi è quello di massimizzare il valore medio delle due votazioni che poi andrà a costituire il voto definitivo. Schematizziamo di seguito i possibili risultati:

  • Se entrambi preparassero la presentazione prenderebbero 100 in essa e 80 nell’esame ottenendo quindi una votazione finale di 90;
  • Se entrambi studiassero per l’esame prenderebbero 92 in esso e 84 nella presentazione per una media di 88;
  • Se uno studiasse per l’esame e l’altro preparasse la presentazione avremmo che quest’ultimo prenderebbe 92 in essa ma 80 nell’esame arrivando a una media di 86, mentre l’altro prenderebbe anch’esso 92 per la presentazione (poiché ci avrebbe lavorato l’altro) e 92 all’esame ottenendo una media di 92.

Cos’è meglio fare quindi? Possiamo ragionare in questo modo:

  1. Se si sapesse che il compagno studierà per l’esame, lo studente dovrebbe scegliere di fare lo stesso in quanto ciò gli permetterebbe di ottenere una votazione media di 88, mentre focalizzarsi sulla presentazione di 86;
  2. Se si sapesse che il compagno preparerà la presentazione, lo studente dovrebbe scegliere di studiare perché così otterrebbe una media finale di 92 mentre in caso contrario di 90.

Possiamo dunque concludere che la miglior cosa da fare sarebbe, in ogni caso, studiare per l’esame.

What advice do you have for someone studying for the bar exam?

Quando, come nell’esempio presentato, un giocatore ha una strategia strettamente più conveniente delle altre, indipendentemente dal comportamento degli altri giocatori, chiameremo tale scelta strategia strettamente dominante e, supponendo la razionalità del soggetto, daremo per scontato che la adotti. 

Nell’esempio precedente, per la stessa natura del problema, ci aspettiamo un comportamento simmetrico da parte dei due giocatori che dunque sceglieranno entrambi di studiare ottenendo la votazione complessiva di 88.

È interessante però notare che se gli studenti avessero potuto accordarsi sul preparare entrambi la presentazione il risultato finale non sarebbe variato, infatti, in tal caso, lo studente si sarebbe aspettato un voto medio di 90 e dunque avrebbe deciso di studiare per l’esame sapendo che l’altro avrebbe preparato la presentazione, infatti ciò gli permetterebbe di raggiungere il 92. 

In realtà, tale piano non avrebbe funzionato perché, a una più attenta analisi, ci si accorge che anche il compagno, in un’ottica meccanicamente razionalistica incentrata sulla massimizzazione del proprio profitto, avrebbe attuato allo stesso modo e dunque entrambi avrebbero ottenuto un punteggio medio di 88, mentre, non giocando razionalmente, avrebbero potuto raggiungere la votazione di 90.

Un’ultima “definizione” prima di spiegare un tema centrale della teoria dei giochi: senza entrare in tecnicismi diremo che in pratica la miglior risposta è la scelta più conveniente che un giocatore, che crede in un dato comportamento degli altri giocatori, possa fare. 

Equilibrio di Nash

Consigliamo vivamente di guardare questo video tratto dal celeberrimo film “A Beautiful Mind” prima di proseguire. 

Dato un gioco, se nessuno dei partecipanti ha una strategia strettamente dominante, per predire l’evolversi della situazione, introduciamo il concetto di equilibrio di Nash secondo cui, in una situazione del genere, dobbiamo aspettarci che i giocatori usino le strategie che danno le migliori risposte le une alle altre. 

Si rimanda a [13] per una definizione più precisa, ma in pratica, se un gioco ammette almeno un equilibrio di Nash, ogni partecipante ha a disposizione almeno una strategia S1 alla quale non ha alcun interesse ad allontanarsi se tutti gli altri giocatori hanno giocato la propria strategia Sn. Questo perché se il giocatore i giocasse una qualsiasi altra strategia a sua disposizione, mentre tutti gli altri hanno giocato la propria strategia se, potrebbe solo peggiorare il proprio guadagno o, al più, lasciarlo invariato. Poiché questo vale per tutti i giocatori se esiste uno e un solo equilibrio di Nash, esso costituisce la soluzione del gioco in quanto nessuno dei giocatori ha interesse a cambiare strategia.

In altre parole si definisce equilibrio di Nash un profilo di strategie (una per ciascun giocatore) rispetto al quale nessun giocatore ha interesse ad essere l’unico a cambiare.

The Value of Coordination - 80,000 Hours

Esistono però giochi che presentano più equilibri di Nash.

Equilibri multipli: giochi di coordinazione

Si supponga, riprendendo l’esempio precedente, che gli studenti debbano preparare, una volta essersi divisi il lavoro, le slide della presentazione. Lo studente, senza possibilità di comunicare col compagno, deve decidere se creare le slide col programma A o col programma B considerando che sarebbe molto più facile unirle a quelle del compagno se fossero fatte con lo stesso software.

Un gioco di questo tipo è detto di coordinazione perché l’obiettivo dei due giocatori è quello di coordinarsi. In questo caso notiamo che ci sono più equilibri di Nash, cioè (A,A) e (B,B). Cosa bisogna aspettarsi?

La teoria dei punti focali (detti anche punti di Schelling) ci dice che possiamo usare caratteristiche intrinseche del gioco per prevedere quale equilibrio sarà quello scelto, cioè quello in grado di dare a tutti i giocatori un guadagno maggiore. Thomas Schelling ne “La strategia del conflitto” descrive un punto focale come: “l’aspettativa di ogni giocatore su quello che gli altri si aspettano che lui si aspetti di fare[7]. 

Premium Vector | People with question marks. man and woman with question,  thinking guy

Per esempio, ritornando al gioco degli studenti, se lo studente S1 sapesse che il compagno S2 predilige A, allora sceglierà A, infatti sapendo che S2 a sua volta sa che lui è a conoscenza di questo fatto assumerà che quest’ultimo sceglierà effettivamente A sapendo che S1 si conformerà di conseguenza.

Schelling illustra questo concetto tramite il seguente esempio: “domani devi incontrare un estraneo a New York, che luogo e ora sceglieresti?”

Che cosa fare a New York, Stati Uniti d'America - Eventi ed attività |  Eventbrite

Questo è un gioco di coordinamento, dove tutti gli orari e tutti i luoghi della città possono essere una soluzione di equilibrio. Proponendo il quesito a un gruppo di studenti constatò che la risposta più comune era: “alla Grand Central Station a mezzogiorno”. La GCS non è un luogo che porterebbe a un guadagno maggiore (il giocatore potrebbe facilmente incontrare qualcuno al bar, o in un parco), ma la sua tradizione come luogo di incontro la rende speciale e costituisce, perciò, un punto di Schelling. Nella teoria dei giochi un punto di Schelling è una soluzione che i giocatori tendono ad adottare in assenza di comunicazione, poiché esso appare naturale, speciale o rilevante per loro.

La teoria dei giochi ben si combina con quella dei grafi e la introdurremo nei prossimi articoli per vedere altri esempi di come possa esistere un possibile conflitto tra razionalità individuale, nel senso di massimizzazione dell’interesse personale, ed efficienza, ovvero la ricerca del miglior risultato possibile, sia individuale sia collettivo.

Abbiamo anche visto che applicando una strategia individualistica si ottiene a volte un esito inferiore rispetto a quanto ottenibile nel caso in cui si possa raggiungere un accordo e che se esiste un equilibrio di Nash ed è unico, esso rappresenta la soluzione del gioco poiché nessuno dei giocatori ha interesse a cambiare strategia. 

Bibliografia e sitografia

[1] R. Dawkins, Il gene egoista, I edizione collana Oscar saggi, Arnoldo Mondadori Editore, 1995.

[2] D. Easley e J. Kleinberg, Networks, Crowds, and Markets: Reasoning about a Highly Con- nected World, Cambridge University Press, 2010.

[3] R. Gibbons, Teoria dei giochi, Bologna, Il Mulino, 2005.

[4] S. Rizzello e A. Spada, Economia cognitiva e interdisciplinarità, Giappichelli Editore, 2011.

[5] G. Romp,Game Theory: Introduction and Applications, Mishawaka, Oxford University Press, 1997

[6] T. C. Schelling, The Strategy of Conflict, Cambridge, Massachusetts: Harvard, University Press, 1960.

[7] P. Serafini, Teoria dei Grafi e dei Giochi, a.a. 2014-15 (revisione: 28 novembre 2014).

[8] http://it.wikipedia.org/wiki/Equilibrio_di_Nash consultato il 12/05/2015.

[9] http://www.oilproject.org/lezione/teoria-dei-giochi-equilibrio-di-nash-e-altri-concetti-introduttivi-2471.html consultato il 13/05/2015.

[10] https://www.youtube.com/watch?v=jILgxeNBK_8 consultato il 19/01/2021.

[11] https://it.wikipedia.org/wiki/Teoria_dei_giochi

[12] https://fiscomania.com/teoria-dei-giochi-prigioniero-nash/ 

Articolo a cura di Carla Melia e Lucia Campomaggiore, Data Scientist in Orbyta Tech, 25.03.2021

#jointherevolution

Recommender systems: principali metodologie degli algoritmi di suggerimento

Introduzione

Vi siete mai chiesti come faccia Netflix a suggerirvi il tipo di film adatto a voi? O Amazon a mostrarvi l’articolo di cui avevate bisogno? O come mai, le pubblicità che vi appaiono nei siti web facciano riferimento a qualcosa di vostro interesse? Questi sono solo alcuni esempi di un tipo di algoritmi che vengono oggigiorno usati dalla maggior parte dei siti web e dalle applicazioni per fornire agli utenti dei suggerimenti personalizzati, si tratta dei sistemi di raccomandazione.

In questo articolo scopriremo cosa sono, con quali metodi vengono implementati e come vengono valutate le loro performance.

Cosa sono i sistemi di raccomandazione 

I sistemi di raccomandazione sono un tipo di sistemi di filtraggio dei contenuti. Possono essere descritti come degli algoritmi che hanno lo scopo di suggerire all’utente di un sito web o di un’applicazione degli articoli che possano risultare di suo interesse. Davanti ad una serie di prodotti devono essere in grado di selezionare e proporre quelli più adatti per ogni utente, quindi di fornire dei suggerimenti personalizzati.

Questo tipo di algoritmi vengono utilizzati in svariati settori. Gli esempi più evidenti possono essere quelli già citati all’inizio dell’articolo quindi nei servizi di e-commerce (es. Amazon), nei servizi di streaming di film, video o musica (es. Netflix, YouTube, Spotify), ma anche nelle piattaforme social (es. Instagram), nei servizi di delivery (es. Uber Eats) e così via. In generale, ogni qualvolta ci sia la possibilità di suggerire ad un utente un contenuto, può essere utilizzato un sistema di raccomandazione per renderlo specifico per l’utente stesso. 

Immagine che contiene elettronico, vicino, cellulare

Descrizione generata automaticamente



Come vengono implementati

I sistemi di raccomandazione possono essere suddivisi principalmente in due macrocategorie: metodi collaborative filtering e metodi content-based. Inoltre, questi due approcci possono essere combinati per dare origine a delle soluzioni ibride che sfruttano i vantaggi di entrambi.  

Metodi collaborative filtering

I sistemi di raccomandazione collaborative filtering utilizzano le interazioni avvenute tra utenti e articoli in passato per costruire la cosiddetta matrice di interazione utenti-articoli e da questa estrarre i nuovi suggerimenti. Si basano sull’assunzione che queste interazioni siano sufficienti per riconoscere gli utenti e/o gli articoli simili fra loro e che si possano fare delle predizioni concentrandosi su queste similarità.

Immagine che contiene testo

Descrizione generata automaticamente

Questa classe di metodi si divide a sua volta in due sottocategorie, sulla base della tecnica utilizzata per individuare le similarità tra gli utenti e/o gli articoli: metodi memory-based e metodi model-based. I primi utilizzano direttamente i valori contenuti nella matrice di interazione utenti-articoli per ricercare “il vicinato” dell’utente o dell’articolo target, i secondi assumono che dai valori della matrice sia possibile estrarre un modello con cui effettuare le nuove predizioni. 

Immagine che contiene testo

Descrizione generata automaticamente

Il vantaggio principale dei metodi collaborative filtering è dato dal fatto che non richiedono l’estrazione di informazioni sugli utenti o sugli articoli dunque possono essere utilizzati in svariati contesti. Inoltre, più gli utenti interagiscono con gli articoli, maggiori informazioni si avranno a disposizione e più le nuove raccomandazioni saranno accurate.

Il loro svantaggio emerge nel momento in cui si hanno nuovi utenti o nuovi articoli perché non ci sono informazioni passate sulle loro interazioni, questa situazione viene definita cold start problem. In questo caso per stabilire quali debbano essere le nuove raccomandazioni si sfruttano diverse tecniche: si raccomandano articoli scelti casualmente ai nuovi utenti o nuovi articoli ad utenti scelti casualmente, si raccomandano articoli popolari ai nuovi utenti o nuovi articoli agli utenti più attivi, si raccomandano un set di vari articoli ai nuovi utenti o un nuovo articolo ad un set di vari utenti, oppure, si evita di utilizzare un approccio collaborative filtering in questa fase.

Metodi memory-based

I metodi memory-based si possono a loro volta suddividere in metodi user-based e metodi item-based

I metodi user-based rappresentano gli utenti considerando le loro interazioni con gli articoli e sulla base di questo valutano la similarità tra un utente e l’altro. In generale, due utenti sono considerati simili se hanno interagito con tanti articoli allo stesso modo. Per fare una nuova raccomandazione ad un utente si cerca di identificare quelli con i “profili di interazione” più simili al suo, in modo tale da suggerirgli gli articoli più popolari tra il suo vicinato. 

Un esempio di applicazione del metodo user-based viene utilizzato da Youtube per suggerirci i video presenti nella nostra Homepage.

I metodi item-based rappresentano gli articoli basandosi sulle interazioni che gli utenti hanno avuto con loro. Due articoli vengono considerati simili se la maggior parte degli utenti che ha interagito con entrambi lo ha fatto allo stesso modo. Per fare una nuova raccomandazione ad un utente, questi metodi cercano articoli simili a quelli con la quale l’utente ha interagito positivamente.

Figura 6. Illustrazione del metodo item-based

Un esempio di applicazione del metodo item-based viene utilizzato da Amazon quando clicchiamo su un articolo e ci appare la sezione “i clienti che hanno visto questo articolo hanno visto anche” mostrandoci altri articoli simili a quello che abbiamo selezionato. 

Uno degli svantaggi dei metodi memory-based è il fatto che la ricerca del vicinato può richiedere molto tempo su grandi quantità di dati, quindi deve essere implementata attentamente e nel modo più efficiente possibile. Inoltre, bisogna evitare che il sistema raccomandi solo gli articoli più popolari e che agli utenti vengano suggeriti solo articoli molto simili a quelli che gli sono piaciuti in passato, deve essere in grado di garantire una certa diversità nei suggerimenti effettuati.

Figura 7. Confronto tra il metodo user-based e item-based

Metodi model-based

I metodi model-based si basano sull’assunzione che le interazioni tra articoli e utenti possano essere spiegate tramite un modello “nascosto”. 

Un esempio di algoritmo per l’estrazione del modello è la matrix-factorization, questo consiste sostanzialmente nella decomposizione della matrice di interazione utenti-articoli nel prodotto di due sottomatrici, una contenente la rappresentazione degli utenti e l’altra la rappresentazione degli articoli. Utenti simili in termini di preferenze e articoli simili in termini di caratteristiche avranno delle rappresentazioni simili nelle nuove matrici.

Figura 8. Illustrazione del metodo matrix-factorization

Metodi content-based

A differenza dei sistemi di raccomandazione collaborative filtering che si basano solo sull’interazione tra utenti e articoli, i sistemi di raccomandazione content-based ricercano delle informazioni aggiuntive.

Supponiamo di avere un sistema di raccomandazione che deve occuparsi di suggerire film agli utenti, in questo caso le informazioni aggiuntive potrebbero essere l’età, il sesso e il lavoro per gli utenti così come la categoria, gli attori principali e il regista per i film.

I metodi content-based cercano di costruire un modello che sappia spiegare la matrice di interazione utenti-articoli basandosi sulle features disponibili per gli utenti e gli articoli.

Dunque, considerando l’esempio precedente, si cerca il modello che spieghi come ad utenti con certe features piacciano film con altrettante features. Una volta che questo modello è stato ottenuto, fare delle predizioni per un nuovo utente è facile, basta considerare le sue features e di conseguenza verranno fatte le nuove predizioni. 

Nei metodi content-based il problema di raccomandazione viene trattato come un problema di classificazione (predire se ad un utente possa piacere o meno un articolo) o di regressione (predire il voto che un utente assegnerebbe ad un articolo).

In entrambi i casi il problema si può basare sulle features dell’utente (metodo item-centred), o sulle features dell’articolo (metodo user-centred). Nel primo caso si costruisce un modello per articolo cercando di capire qual è la probabilità che ad ogni utente piaccia quell’ articolo, nel secondo caso si costruisce un modello per utente per capire qual è la probabilità che a quell’utente piacciano gli articoli a disposizione. In alternativa si può anche valutare un modello che contenga sia le features degli utenti che quelle degli articoli.

Il vantaggio dei metodi content-based è che non soffrono del cold start problem perché i nuovi utenti e i nuovi articoli sono definiti dalle loro features e le raccomandazioni vengono fatte sulla base di queste. 



Come vengono valutati

Per valutare le performance di un sistema di raccomandazione, quindi per cercare di capire se le raccomandazioni che sta effettuando sono appropriate, vengono utilizzati principalmente tre tipi di valutazioni: user studies, la valutazione online e la valutazione offline.

La valutazione user studies prevede di proporre agli utenti delle raccomandazioni effettuate da diversi sistemi di raccomandazione e di chiedergli di valutare quali raccomandazioni ritengono migliori.

La valutazione online, chiamata anche A/B test, prevede di proporre agli utenti in real-time diverse raccomandazioni per poter valutare quali sono quelle che ottengono più “click”.

La valutazione offline prevede di fare delle simulazioni sul comportamento degli utenti partendo dai dataset passati che si hanno a disposizione.

Fonti

Articolo a cura di Monica Mura, Data Scientist in Orbyta, 11.03.2021

#jointherevolution

Quando, anni fa, ho iniziato a lavorare come consulente IT ho dovuto sviluppare e modificare script che venivano eseguiti su server UNIX. Non avevo a disposizione strumenti di tendenza. Fondamentalmente Notepad ++ per si scrive localmente sulla mia macchina e PuTTY o FileZilla per raggiungere i server remoti. Quindi, sui server, erano installati i “temibili” Vi o Vim . Così ho iniziato a usare anche Vim , e la prima cosa che ho dovuto imparare era “come uscire da Vim”.

Vim ha una curva di apprendimento ripida . In quel momento ho imparato solo pochi comandi di base, quelli fondamentali per lavorare e poi appena ho avuto la possibilità di passare a qualcos’altro l’ho fatto. Ma Vim ha anche un grande potenziale , se sai come usarlo. Ricordo che il mio capo poteva usarlo abbastanza bene ed era molto abile in questo. Certo è difficile e devi usarlo molto per iniziare a vedere miglioramenti, ma lo sforzo è ben pagato.

Alcuni giorni fa, in un tweet, ho scoperto l’esistenza di questo sito: vimforvscode.com . Ho pensato: “Wow, imparare Vim usando VS Code? 10 $ è un prezzo per cui vale la pena provare ”.  Ho installato l’ estensione VS Code : VSCodeVim e poi ho iniziato le lezioni che ho acquistato dal sito. Gli esercizi non sono difficili e ti permettono, attraverso la pratica e i suggerimenti, di imparare 22 comandi di base.

Ovviamente le lezioni di vimforvscode.com non bastano per padroneggiare appieno Vim , ti consiglio di provare ad esercitarti molto, cercando di aggiungere gradualmente più comandi. Queste risorse possono essere utili:

  1. Barbarian meets coding è un sito meraviglioso (la home page è qualcosa di incredibile, ti consiglio – anche se non sei interessato a Vim – di dare un’occhiata!) Che tra l’altro spiega come usare VSCodeVim , c’è un libro gratuito , che puoi leggere online, con tutte le funzionalità di Vim in VS Code , e se ti serve solo un breve riepilogo puoi usare il cheat sheet
  2. Vim Cheat sheet è un cheat sheet completo per Vim , ma anche se ci sono alcuni comandi che non sono supportati nell’estensione VS Code ce ne sono altri che nel cheat sheet “barbaro” sono assenti.
  3. Vim adventures è un gioco molto carino che ti può insegnare Vim : è molto divertente, i primi 3 livelli sono gratuiti, poi devi acquistare una licenza personale da 25 $. Ho provato fino al livello 3, ma se tutti i livelli sono simili a quelli gratuiti, penso che valga il prezzo!

E così sono tornato su Vim , in VS Code ora. Devo ammettere che all’inizio mi sentivo abbastanza lento, ma, giorno dopo giorno, sento che sto migliorando e che forse sarò più efficiente di prima nel prossimo futuro.
Penso che usare Vim in VS Code possa essere un’esperienza “strana” se non ci sei abituato, ma con un po ‘di impegno e molta pratica può essere davvero soddisfacente!


Articolo a cura di Federico Gambarino

#jointherevolution

System Versioned Tables

Le tabelle con controllo delle versioni di sistema spiegate col Signore degli Anelli

Se entraste in una stanza piena di informatici e domandaste ad alta voce: “Chi di voi non ha mai cancellato dei dati per sbaglio?”, contando le mani alzate avreste una stima piuttosto accurata di quanti bugiardi avete di fronte.

Nei miei anni di lavoro su SQL Server, ammetto che mi è capitato più di una volta, per distrazione, per una convinzione errata, o per avere scritto male una condizione, di lanciare un comando che ha distrutto o modificato molti più record di quanto fosse la mia intenzione. Un’azione del genere spalanca prospettive raramente rosee, ma con gradi di gravità che possono distribuirsi su un ventaglio molto ampio: dalla semplice seccatura di doversi inventare 10 nuovi casi in ambiente di sviluppo, allo scenario da incubo di avere destinato all’oblio migliaia di righe in produzione.

Non è scopo di questo articolo una disamina approfondita sulle buone pratiche di backup di un DB, ma vorrei raccontarvi di uno strumento messo a punto su Management Studio, attivo dalla versione 2016, che può rivelarsi molto utile non solo in caso di emergenza, ma anche nella gestione abituale di qualche tabella cruciale di cui si vuole mantenere o analizzare la storia nel tempo.

L’argomento di cui vorrei parlare in questo articolo riguarda le “Temporal Tables” oppure “System-Versioned Tables”; tra le due nomenclature la mia simpatia vira con decisione verso la seconda, dato che la prima (benché sia la più usata) potrebbe generare confusione con le tabelle cancelletto (Temporary Tables), nate con una funzione completamente diversa. La dicitura più esaustiva e corretta sarebbe “System-Versioned Temporal Tables” non immune da una certa prolissità, ma d’ora in poi le chiamerò “tabelle con controllo delle versioni di sistema”, come riportato nella versione italiana di SSMS.

Partiamo con la definizione nuda e cruda:

Una tabella con controllo delle versioni di sistema è un tipo di tabella definita dall’utente, progettata per mantenere una storia completa dei cambiamenti sui dati e permettere una facile gestione nell’analisi di questi cambiamenti nel tempo. Questo tipo di tabella è detta “System-Versioned” perché il periodo di validità di ciascuna riga è gestito dal sistema (cioè dal motore di database).

Per entrare subito in medias res, fornirò un esempio pratico dell’utilizzo di questa funzionalità.

Qui di seguito ho riportato lo script di creazione di una tipica tabella con controllo delle versioni di sistema

 

 

Se fino alla quinta riga la sintassi è del tutto abituale, quelle successive aprono scenari molto più interessanti; questo script infatti non si limita a creare una sola tabella, ma due, strettamente collegate tra di loro. La prima è la tabella principale (nel nostro caso la dbo.LOTR), dal comportamento del tutto simile a una normalissima tabella, la seconda è la tabella dello storico (la dbo.LOTRHistory, definita a riga 9 dello script), nella quale sarà registrata la cronologia di tutti i cambiamenti operati nel tempo sulla tabella principale.

Senza specificare un nome personalizzato per la tabella dello storico, SQL Server avrebbe generato automaticamente un nome del genere: dbo.MSSQL_TemporalHistoryFor_xxx, con l’id dell’oggetto al posto di xxx. Suppongo sia meglio scegliersi il nome da soli.

Lo stretto legame tra le due tabelle si può riscontrare facilmente guardando l’esplora oggetti.

Le colonne della tabella dbo.LOTRHistory, creata automaticamente dallo script, saranno (per nome e per tipo), esattamente uguali a quelle della dbo.LOTR, ma senza nessuno dei vincoli originali (chiave primaria, chiavi esterne etc.); anche gli indici e le statistiche non discendono dalla tabella principale e potranno essere gestiti in modo del tutto indipendente.

 

Che non siano tabelle normali lo si può riscontrare anche osservando che, nel menu di entrambe, non è prevista l’opzione “Elimina”.

Se volessimo eliminare una delle due entità sarebbe necessario utilizzare questo comando per svincolarle dal versionamento.

 

Un’altra caratteristica essenziale per la creazione di una tabella con controllo delle versioni di sistema è la presenza di due colonne di tipo datetime2, cioè la SysStartTime e la SysEndTime (righe 6 e 7 dello script di creazione), il cui contenuto è infatti gestito direttamente dal sistema; questa loro particolarità ci impedirà di apportare qualunque intervento o modifica su di loro. Grazie a queste due colonne sarà possibile tenere traccia del momento preciso in cui un record della tabella dbo.LOTR ha subìto una variazione di qualche tipo.

Ma procediamo con il nostro esempio pratico, inserendo un po’ di dati all’interno della tabella principale. La popoleremo con i personaggi del romanzo “Il Signore degli Anelli”, di Tolkien, e la qualifica che hanno all’inizio del libro

 

 

Proviamo a fare una select della tabella e vediamo cosa restituisce.

 

 

Finora non sembrano esserci particolari soprese, anche se avrete notato che le due colonne SysStartTime e SysEndTime, pur non essendo state valorizzate esplicitamente nella insert, hanno assunto rispettivamente i valori della data in cui è stato effettuato l’inserimento, e della data più grande disponibile nel formato datetime2, ovvero: 99991231 23:59:59.9999999.

Proviamo a vedere cosa accade se eseguiamo un’operazione di update sui personaggi del nostro elenco che diventeranno parte della Compagnia dell’Anello, aggiornando la loro qualifica con il valore “Membro della compagnia dell’Anello”.

 

 

Se interroghiamo la tabella principale, troviamo esattamente quello che possiamo aspettarci da un’operazione compiuta su una qualsiasi tabella della nostra base dati, ovvero:

 

È immediato notare che non è cambiata solo la qualifica dei membri interessati, ma anche la loro SysStartTime, che riporta l’ora della modifica. Il record relativo a Saruman, non essendo coinvolto dall’update, ha mantenuto invariata la SysStartTime di prima, corrispondente al momento della sua creazione.

Se non ci interessa visualizzare il valore delle colonne SysStartTime e SysEndTime a ogni select, sarà sufficiente dichiarare “hidden” le due colonne in questione nello script di creazione iniziale.

 

 

 

La faccenda si fa più interessante se guardiamo cosa è successo nella tabella dbo.LOTRHistory, di cui per semplicità tireremo fuori solo i risultati relativi a Boromir.

 

 

Dato che la tabella dello storico contiene solo i dati relativi a ciò che c’era nel passato, troveremo un unico record, corrispondente al valore della ormai obsoleta qualifica di Boromir, con data SysEndTime identica alla SysStartTime assegnata allo stesso personaggio sulla tabella dbo.LOTR.

Ovviamente, se cercheremo di ripetere la stessa query sulla dbo.LOTRHistory per Saruman, che non ha subìto modifiche dopo essere stato creato, non troveremo nessun record.

La cronologia sulla dbo.LOTRHistory terrà traccia anche di tutti i cambiamenti successivi, tant’è vero che se eseguo un nuovo update su Boromir, una select su dbo.LOTR restituirà il valore aggiornato come avrebbe fatto una normalissima tabella.

 

 

Nella dbo.LOTRHistory, a questo punto, troveremo due record: uno per ciascuno degli stati precedenti di Boromir.

 

 

In questo frangente si capisce come mai la tabella dbo.LOTRHistory non abbia gli stessi vincoli della sua gemella: due cambiamenti sul record con Id = 1 nella tabella dbo.LOTR portano ad avere due record con Id = 1 sulla tabella dello storico, condizione che avrebbe generato un errore di violazione di chiave se la dbo.LOTRHistory avesse ereditato la PK dalla tabella principale.

È anche facile notare come tutti i cambiamenti consecutivi registrati nella dbo.LOTRHistory abbiano la SysEndTime del record più vecchio coincidente con la SysStartTime del nuovo, proprio come la SysEndTime del nuovo coinciderà con la SysStartTime del valore corrispondente sulla tabella principale.

Se conoscete il Signore degli Anelli, saprete che alla fine del primo episodio Boromir passa a miglior vita, per questo motivo saremo costretti a cancellarlo dalla nostra tabella dbo.LOTR.

 

 

Il risultato che otteniamo dopo questa operazione non è molto diverso da quanto possiamo aspettarci, cioè la tabella principale non avrà più nessun record relativo a Boromir, ma la sua tabella dello storico dbo.LOTRHistory ospiterà tutte le sue precedenti incarnazioni.

 

 

Finora abbiamo incontrato una netta separazione tra presente e passato; o abbiamo eseguito una query sulla dbo.LOTR per avere un quadro della situazione al momento attuale, o abbiamo eseguito una query sulla dbo.LOTRHistory, per ottenere lo scenario completo sugli stati passati di uno dei personaggi. Verrebbe da chiedersi se esiste un modo per avere una visione d’insieme di tutti gli stati, quelli passati e quello attuale; la risposta ovviamente è sì, e consiste nell’eseguire una query sulla dbo.LOTR utilizzando la clausola FOR SYSTEM_TIME ALL.

Dato che Boromir non è più nella tabella principale, faremo la prova con un altro personaggio ancora attivo, cioè Sam.

 

 

È chiaro che, nella storia di ciascun personaggio, possiamo sempre riconoscere come attualmente valido il solo e unico record che reca come data nella SysEndTime il valore 9999-12-31 23:59:59.999999; nel caso di valori cancellati, invece (come per Boromir), nessuna SysEndTime assumerà tale valore, essendo la SysEndTime con data più alta quella corrispondente al momento dell’eliminazione del record.

Ci sono in tutto cinque possibili condizioni per la clausola FOR SYSTEM_TIME, che ci consentono una buona flessibilità al fine di ottenere le informazioni che vogliamo rispetto al tempo, vale a dire

  • ALL
  • AS OF
  • FROM TO
  • BETWEEN AND
  • CONTAINED IN ( , )

Scorrendo velocemente il loro utilizzo abbiamo che la AS OF dà l’immagine del dato in un preciso istante nel tempo. Sapendo che il personaggio di Boromir è stato creato alle 8:56 della mattina del 2 Agosto 2020 con il titolo di Capitano di Gondor e modificato più di tre ore dopo, non è sconvolgente scoprire che il risultato della query che ne richiede lo stato alle 8:57 restituisce la sua qualifica iniziale.

 

 

La FROM … TO e la BETWEEN hanno sostanzialmente lo stesso scopo, cioè quello di fornire tutta la storia di un record in un determinato intervallo di date

 

 

La sottile differenza tra le due è data dal fatto che la BETWEEN mostrerà anche un cambiamento il cui inizio coincide esattamente con l’estremo superiore dell’intervallo di date scelto. Abbiamo perciò che il cambiamento di stato di Boromir a “Membro poco convinto della Compagnia dell’Anello” avvenuto in questa data: 2020-08-02 12:55:23.3687849, a parità di intervalli sarà mostrato solo dalla BETWEEN e non dalla FROM … TO.

 

 

Da ultima, la CONTAINED si limita a restituire i periodi contenuti interamente (quindi iniziati e conclusi) nell’intervallo di tempo specificato 

 

Per avere un promemoria del funzionamento di queste clausole, trovo sempre utile aiutarmi con una rappresentazione grafica; in questo caso i quattro stati rappresentati racchiudono tutta la storia degli stati, compreso quello in corso (se esiste).

 

Conclusioni:

Le tabelle con controllo delle versioni di sistema sono uno strumento molto utile non solo per recuperare dati cancellati inavvertitamente da tabelle sensibili (una sorta di backup tabellare), ma anche per ricostruire la storia di un determinato dato e il suo andamento nel tempo senza dover ricorrere a procedure, trigger o altri mezzi dalla manutenzione macchinosa. Sicuramente, data la grande mole di informazioni che può accumularsi nel tempo (ad es. una tabella con molte colonne che viene aggiornata spesso), può valere la pena di considerare qualche meccanismo di pulizia periodica di tali tabelle; è però importante tenere a mente che questo strumento è stato creato pensando ad una conservazione del dato per lunghi periodi, e che un uso intelligente di indici e statistiche sulla tabella dello storico (indipendenti dalla tabella principale) può aumentarne considerevolmente l’efficienza senza dover ricorrere a misure che rischierebbero di snaturarne lo scopo.

 

Script con il codice usato nell’articolo:

https://bit.ly/2PvKyMo

 

Fonti:

https://www.mssqltips.com/sqlservertip/3680/introduction-to-sql-server-temporal-tables/

https://docs.microsoft.com/en-us/sql/relational-databases/tables/temporal-tables

https://www.sqlshack.com/temporal-tables-in-sql-server/

 

#jointherevolution

PlayStation 5 e l'accesso ai dati: un cambio architetturale?

Uno sguardo ad un particolare dell’architettura della nuova console Sony che potrebbe avere ripercussioni anche nel mondo dei Personal Computer

Il 18 marzo 2020 Mark Cerny, Lead System Architect di Sony, fece una presentazione tecnica sull’architettura hardware della futura console PlayStation 5 (PS5).

Questa presentazione non fu molto apprezzata da parte del pubblico, dato che non mostrò ciò che agli appassionati premeva vedere di più: i giochi e il design della console. Inoltre, si rivelò eccessivamente tecnica e inadatta al pubblico generalista.

 

Essa però conteneva moltissimi dettagli sull’architettura di PS5 e sulle sue caratteristiche: potenza di processore (CPU) e scheda video (GPU), supporto a 8k e 120 FPS, gestione dell’illuminazione (raytracing) hardware, suono ambientale, gestione termica e così via.

 

In questo articolo ci concentreremo però su un aspetto specifico, a mio avviso cruciale: il trasferimento dati. Qualche tempo dopo quella presentazione, qualcuno disse: “Sono riusciti a far diventare sexy gli SSD”. Che significa? Andiamo per gradi.

 

I tecnici di Sony, parlando anche con le grandi case di videogames, decisero di implementare un controller customizzato per il trasferimento dati del disco a stato solido (SSD): una console non è “costretta” a seguire un’architettura hardware identica a quella PC (e, in effetti, poche console lo hanno fatto: quelle più simili ai PC sono solo quelle della generazione attuale, Xbox One e PS4).

L’SSD custom di PS5

Questo ha permesso di incrementare le linee di comunicazione interne all’SSD (che quindi non è un normale SSD, già di per se molto più veloce dei dischi meccanici attuali) e di sfruttare la nuovissima interfaccia PCIe 4.0, oltre che di implementare in HW tutta una serie di ottimizzazioni (SoC integrato, coprocessori di I/O, scrubber della cache video, …).

Gli elementi di I/O custom del System-on-Chip (SoC) di PS5

La vera novità però è un’altra: quella di permettere che i dati transitino direttamente tra l’archivio dati (l’SSD) e l’usufruitore, che sia la CPU o la GPU. Nell’architettura PC tradizionale invece, per trasferire i dati tra disco e GPU, occorre passare dalla RAM.

Come se non bastasse, è stato implementato anche un layer di decompressione in harware (Kraken decompressor).

In termini numerici, PS5 è in grado di trasferire 5.5 GB di dati compressi al secondo e decomprimerli mentre vengono trasferiti. Nel caso migliore si parla di 22 GB/s di dati grezzi. Per fare un paragone, un buon SSD per PC ha prestazioni di circa 3.5 GB/s.

 

Un trasferimento dati estremamente veloce implica sì tempi di caricamento ridotti (di due ordini di grandezza rispetto all’hardware attuale, sostengono i tecnici Sony), ma non è tutto! La richiesta di un dato ora può essere soddisfatta direttamente nel ciclo di rendering attuale, o alla peggio nel successivo: ad un frame rate di 60 FPS (lo standard per i futuri giochi) stiamo parlando di 16 ms di latenza massima.

 

Quest’ultima affermazione, sulla carta interessante, non ha lasciato il segno più di tanto… finché qualcuno non ha dato una diretta dimostrazione delle implicazioni che delle simili prestazioni comportino veramente.

 

Il 13 maggio 2020 Epic Games, conosciuta principalmente per il videogioco Fortnite e per essere il creatore del motore grafico Unreal Engine, ha mostrato il nuovo Unreal Engine 5 girare su HW PS5.

Le innovazioni tecnologiche e grafiche sono numerose, ma quella che ci interessa relativamente al discorso I/O è la tecnologia chiamata Nanite.

Epic ha riscritto il sottosistema di I/O del suo software per adattarlo all’hardware PS5 e per sfruttare la maggior larghezza di banda e le latenze ridotte. Il risultato ha sbalordito gran parte degli “addetti ai lavori”, perché il concetto stesso di streaming degli asset è stato portato ad un nuovo livello.

Unreal Engine 5 su PS5: viene mostrato l’alto dettaglio grafico e l’illuminazione globale in tempo reale

Su disco ora risiede l’intera geometria da renderizzare, non semplificata, e ad ogni frame l’engine si preoccupa di prelevare da essa solo i triangoli che andranno effettivamente a comporre l’immagine su schermo.

In fase di creazione degli asset, le geometrie non devono più essere semplificate in versioni low poly e i dati non devono più essere trasferiti a runtime nella RAM. Inoltre, sempre in fase di creazione, non è più necessario creare mappe di texture per simulare in maniera fittizia i dettagli degli oggetti, dato che le geometrie, arbitrariamente complesse, possono essere direttamente utilizzate.

Visualizzazione delle geometrie della scena di Unreal Engine 5: i singoli triangoli sono talmente piccoli che l’occhio umano non riesce a distinguerli

Stando alla presentazione, è possibile avere su disco scenari composti da miliardi di triangoli (ben più che qualsiasi sistema sia in grado di riprodurre in tempo reale) e utilizzare questa base dati per renderizzare ad ogni frame, 60 volte al secondo, solo quelli effettivamente utili a riprodurre l’immagine da mostrare a schermo (nell’ordine di 10-20 milioni, equivalente alle capacità attuali di un PC di fascia alta).

 

Nanite sembra essere il sacro graal della computer graphics, infatti comporta vantaggi tali da stravolgere in parte il processo creativo di contenuti 3D realtime (rimasto pressoché inalterato negli ultimi 20 anni):

  • la creazione degli asset è enormemente velocizzata, dato che intere fasi di lavorazione ora possono essere evitate (semplificazioni, creazione di livelli di dettaglio – LoD, materiali e mappe);
  • gli asset per i contenuti realtime (videogiochi, serious games, …) e prerenderizzati (film, animazione, …) possono essere gli stessi, avvicinando sempre di più i due mondi;
  • in fase di visualizzazione è possibile usufruire, in tempo reale, di un livello di dettaglio delle geometrie semplicemente impensabile fino ad ora.
Unreal Engine 5 su PS5: ogni statua è composta da più di 33 milioni di triangoli

Tim Sweeney, CEO di Epic Games, ha detto: “For PC enthusiasts, the exciting thing about the PS5 architecture is that it’s an existence proof for high bandwidth SSD decompression straight to video memory. Is this enough to get Microsoft, Intel, NVIDIA, Intel, and AMD to work together and do it? We’ll see!”. Traducendo liberamente: “La parte eccitante di PS5 è che è la prova materiale del trasferimento ad alta velocità e decompressione direttamente nella memoria video. Sarà sufficiente a far sì che Microsoft, Intel, NVIDIA, Intel e AMD lavorino assieme per realizzarla (nei PC)?”.

 

Senza entrare nel merito della cosiddetta console war (tanto cara agli appassionati di Sony e Microsoft), vorrei precisare che si tratta solo di un tassello del puzzle che decreterà il successo di questa o quest’altra console. Posso però affermare che Sony ha effettuato delle scelte sicuramente originali in termini architetturali. Microsoft, a dirla tutta, sta seguendo un approccio non del tutto dissimile (lo chiamano DirectStorage), ma la soluzione Sony sembra avere una marcia in più.

 

Quello che sarà interessante vedere, piuttosto, sarà come il mondo PC reagirà a questa innovazione e come gli sviluppatori sapranno interpretarla per utilizzarla al meglio. E non parlo solo di videogiochi, ma anche del mondo professionale: al giorno d’oggi i progetti CAD possono tranquillamente raggiungere diversi TeraByte di dimensione su disco, e poterli vedere in tempo reale su di uno schermo (o perché no, in realtà virtuale o aumentata) porterebbe dei guadagni enormi in termini di efficienza ed efficacia di progettazione/testing/training.

di Christian Bar

Orbyta Tech

VR/AR Specialist

#jointherevolution

I vantaggi di un Message Broker

I vantaggi di un Message Broker nelle moderne architetture distribuite (con riferimenti a RabbitMQ)

Le architetture moderne si stanno sviluppando in una direzione fortemente decentralizzata (si pensi a quanti software oggigiorno vengano progettati in un’ottica di microservizi, o come la distribuzione del consenso sia il cardine di un intero nuovo filone tecnologico quali le blockchain e le criptovalute). Non è in realtà un argomento nuovo nel mondo dell’informatica, già nei giorni della sua creazione nel 1969, il sistema operativo UNIX dovette operare una scelta nel disegno del kernel fra un approccio monolitico (struttura che oggi chiameremmo centralizzata) e uno a microkernel (decisamente prossimo al concetto di decentralizzato). Poiché nessuno dei due approcci presenta solo vantaggi, come spesso accade, si è assistito a guerre di religione in proposito, dove non sono mancati tentativi di compromesso con architetture kernel-ibride (ossia gli attuali Windows e MacOS). Al momento uno dei requisiti su cui si pone maggiore attenzione è fornire sempre e comunque un risultato, senza per questo doversi affidare unicamente alla replicazione di intere strutture hardware e software per garantire la continuità servizio.

 


Dividere quello che un tempo era un singolo servizio su N microservizi porta una serie indubbia di vantaggi:

  1. maggiore facilità nella scalabilità: trattandosi di un’attività micro è ragionevole pensare che possa essere avviata e replicata molto in fretta (approccio che vede il suo attuale apice nei docker e in kubernates);

     

  2. maggiore manutenibilità del software: è di nuovo ragionevole pensare che un microservizio faccia un numero veramente ridotto di attività, portando vantaggi nella scrittura, nel testing e nel bug fixing;

     

  3. elevato disaccoppiamento dei processi: ognuno, almeno in teoria, dovrebbe poter vivere di vita propria attendendo richieste a cui dare risposta, disinteressandosi del sistema macroscopico in cui è stato inserito;

     

  4. minore impatto in caso di disservizio: se un microservizio non fosse disponibile questo non impatterebbe sul resto del sistema che continuerebbe a fornire risposta (sebbene magari solo parziale).

     

e questi solo per elencare i principali. C’è però un ovvio rovescio della medaglia: per quanto sarebbe perfetto poter disegnare un’architettura in cui ogni componente non ha mai necessità di scambio dati con gli altri, questo non si rivela possibile nella pratica. Ogni componente è costantemente interessato in uno scambio di informazioni con gli altri.

 


Il colloquio fra i diversi processi (o moduli) può essere affrontato in svariati modi:

  1. Remote Procedure Call (RPC): il più classico e utilizzato fin dagli albori dei sistemi distribuiti. Ogni processo conosce il protocollo di colloquio verso ciascuno degli altri componenti, ed è compito di ognuno stabilire e mantenere i canali di comunicazione. Se la controparte sia o meno attiva al momento della chiamata non è dato saperlo a meno di prevedere sempre e comunque una risposta all’interno del protocollo (cosa spesso controproducente o impossibile in caso di vincoli stringenti sulle performance). In questo approccio è l’utente finale a non sapere se la sua richiesta è stata eseguita localmente o su un dispositivo remoto.

     

  2. Object Request Broker (ORB): la naturale evoluzione di RPC, con l’avvento dell’astrazione introdotto dalla programmazione orientata agli oggetti. In questo paradigma i processi non sono consapevoli se la loro richiesta sia locale o remota, dal momento che la risoluzione della chiamata è affidata ad uno strato software che maschera questo aspetto. Inoltre, tutte le attività di trasmissione dei messaggi (serializzazione, deserializzazione e eventuali verifiche di integrità e ritrasmissione) risulta trasparente. In questo approccio si vuole mascherare ai processi stessi se la loro richiesta sia locale o remota.

     

  3. Message Oriented Middleware (MOM): l’ultimo approccio nato. In questo paradigma il colloquio tra i processi è egli stesso un processo, dedicato esclusivamente a mascherare ogni dettaglio relativo a protocollo di rete e di trasporto. Questi processi nascono per coprire il maggior numero di piattaforme software possibile (sia per quanto riguarda si sistemi operativi supportati, sia per i linguaggi di programmazione in cui queste librerie vengono fornite). Questo approccio porta ad un maggiore disaccoppiamento tra i processi rispetto ai due precedenti, che sono consapevoli di delegare ad un modulo terzo la gestione della loro comunicazione.

     

Per fare un’analogia, potremmo pensare ai due estremi: RPC e MOM. Il primo è l’equivalente di un messaggero medievale, istruito dal proprio committente sui tempi e le modalità con cui un messaggio accuratamente preparato e sigillato dovrà essere recapitato, ed eventualmente fornito di una scorta più o meno numerosa che possa garantire la sicurezza della missiva da consegnare. Sicuramente, essendo ogni aspetto curato e calibrato sull’importanza del messaggio, da ogni comunicazione otterremo il massimo dell’efficienza e dell’affidabilità, a fronte di un dispendio di energie e risorse notevole. Il secondo è più simile ad un ufficio postale, a cui affidiamo il nostro messaggio con un numero minimo di istruzioni a riguardo, a cui assegniamo una priorità e eventuali istruzioni per una notifica di recapito. Non ci si cura di altri aspetti, sapendo che l’ufficio postale è sicuro e affidabile (se non dovessimo fidarci delle poste potremmo sempre affidarci ad altri corrieri volendo ☺ ).

I Message Broker sono i software che implementano il paradigma MOM e forniscono tutti i servizi (in forma di API) necessari alla distribuzione di messaggi, definendo al loro interno le strutture necessarie a definire le rotte di instradamento e le politiche di recapito. 

Uno dei più vecchi software di questo tipo è IBM MQ, nato nel 1993, mentre ai giorni nostri i sistemi più diffusi sono RabbitMQ e Kafka. I sistemi collocati all’interno dei Cloud possiedono ovviamente le proprie versioni gestite, abbiamo ad esempio Azure Service Bus fornito da Microsoft e Simple Queue Service fornito da Amazon. Fondamentalmente tutti questi sistemi offrono le medesime funzionalità, sebbene le specifiche implementazioni presentino punti deboli e punti forti rispetto ai contesti iniziali in cui sono stati concepiti. Giusto per fare un esempio:

  1. RabbitMQ: nasce principalmente come simulatore di reti sfruttando il protocollo AMPQ (Advanced Message Queuing Protocol), è pertanto facilitata la creazione di nuovi nodi e di nuovi collegamenti fra i vari processi che lo sfruttano.

     

  2. Service Bus: nasce come Message Broker puro, pertanto è possibile creare dinamicamente nodi e collegamenti, ma l’approccio è più complicato, in quanto lo scenario di utilizzo privilegiato è quello in cui la topologia della rete di scambio messaggi sia definita a livello architetturale prima dell’avvio dei processi.

     

  3. Kafka: nasce con lo scopo di distribuire il maggior numero di messaggi possibili all’interno di chat e gruppi. Avendo come obiettivo quello di massimizzare il throughput di messaggi recapitati, è poco adatto ad essere utilizzato negli scenari in cui la sequenza di consegna dei messaggi è un vincolo, dal momento che tutte le ottimizzazioni del sistema sono orientate alla consegna in qualsiasi ordine essa avvenga, e forzarlo ad un comportamento “sequenziale” fa perdere il grosso dei vantaggi di questo message broker.

     


Poste queste indicazioni di massima, possiamo però definire quelli che sono i capi saldi del paradigma MOM:

  1. I messaggi vengono inseriti all’interno di code (da cui il costante riferimento di molti software all’acronimo MQ, Message Queue) per cui è garantita la persistenza. Se il destinatario non fosse disponibile, il messaggio resterebbe in coda in attesa di essere recapitato;

     

  2. È possibile definire dei nodi di smistamento. Questi possono prendere diversi nomi a seconda del broker (ad es. Topic in Service Bus o Exchange in RabbitMQ) ma la loro funzione è quella di distribuire i messaggi a tutti coloro che si sottoscrivono a quel nodo.

     

Con questi due semplici oggetti, è possibile costruire topologie in cui la comunicazione avviene punto-punto (tra mittente e destinatario è presente una coda) oppure punto-multipunto (in cui un mittente inoltra un messaggio ad un nodo, e questo lo distribuisce a tutti i sottoscrittori collegati).

Punto di forza di questi sistemi è la possibilità di definire delle politiche di sottoscrizione ai nodi. I nodi più semplici, di base, replicano il medesimo messaggio a tutti i sottoscrittori, ma impostando delle regole, ed eventualmente ponendo dei nodi in cascata, è possibile recapitare i messaggi con certe caratteristiche ai sottoscrittori che le hanno richieste. Vediamo alcuni esempi per chiarire tutti questi concetti.

Creazione di una coda in RabbitMQ (in C#)

Nel classico paradigma Produttore/Consumatore immaginiamo di voler mettere in collegamento le due entità grazie a RabbitMQ. Lo strumento adottato è la Coda.

Il produttore deve semplicemente inserire il messaggio nella coda, e confidare nella bontà di RabbitMQ. La coda conserverà i messaggi fintanto che il consumatore non avrà la possibilità di prenderli in carico, fornendo implicitamente un meccanismo persistente di gestione dei dati. La lettura dei messaggi può avvenire in modo:

  1. Sincrono: il consumatore interroga la coda per farsi restituire uno o più messaggi

     

  2. Asincrono: la coda notifica al consumatore la presenta di un messaggio, consegnandolo al metodo preposto a fungere da Event Handler

     

La coda può essere definita con una serie di proprietà:

  1. Time-To-Live: ad ogni messaggio può essere impostato un TTL, oltre il quale viene rimosso dalla coda in quanto “scaduto”

     

  2. Queue Expiring: se una coda non è ascoltata da nessun consumatore, può rimuoversi dal sistema

     

  3. Dead Letter Queue: è possibile definire una coda in cui tutti i messaggi scaduti o finiti in errore per qualche motivo (solitamente errori di instradamento nelle topologie più complesse) finiscano, così da poterne tenere traccia coi log ed eventualmente gestirli in modo puntuale con un processo dedicato. Sarà compito di RabbitMQ spostare i messaggi nella Dead Letter senza alcun intervento da parte dei processi o degli sviluppatori

     

Il codice necessario per implementare questo dialogo punto-punto prevede la creazione di un metodo di spedizione (che chiameremo Send) e uno di ricezione (analogamente Receive):

Punti di interesse di questo codice sono la factory, ossia la classe fornita dalla libreria RabbitMQ .NET per la creazione e la gestione del servizio. È sufficiente indicare l’indirizzo del server su cui RabbitMQ è in funzione, ed è possibile aggiungere i classici dati generali quali user e password (N.B. se RabbitMQ è installato in localhost, non è necessario nulla per poter funzionare oltre all’HostName, ma se ci si collega da remoto è indispensabile creare un utente, dal momento che l’utente di default è abilitato alle API solo se queste richieste provengono dal sistema locale).

Una volta ottenuta la factory, è possibile usarla per creare una connection, ovvero aprire una connessione TCP per dialogare con RabbitMQ. Attraverso la connection possiamo creare un model, esso rappresenta il collegamento verso le strutture interne di RabbitMQ. Possiamo pensare alle due entità in questo modo:

  1. Connection: il canale con cui si dialoga con il servizio server rappresentato da RabbitMQ

  2. Model: il canale con cui si dialoga con nodi e code presenti all’interno di RabbitMQ

La flessibilità è totale, ma le best practice consigliano di creare una sola connection verso RabbitMQ da tenere attiva il più a lungo possibile, mentre di creare i model al bisogno quando si vuole spedire. Nulla vieta di mantenere un model per un tempo indefinito, specie se si avesse un traffico continuo e l’overhead di creazione e distruzione del model fosse eccessivo.

Una volta ottenuto il model è possibile creare o collegarsi ad una coda. Una coda deve avere un nome univoco e una serie di caratteristiche che possono essere definite alla creazione, e devono coincidere per chiunque intenda collegarcisi sia in spedizione che in ricezione. L’invio del messaggio avviene semplicemente attraverso il metodo BasicPublish, a cui è normalmente sufficiente fornire il messaggio sotto forma di byte array. Gli altri elementi del BasicPublish li vedremo tra poco nel prossimo esempio.

Il Receive invece verrà scritto in questo modo:

I punti di interesse in questo caso sono due: la registrazione dell’EventingBasicConsumer, ossia il gestore a cui sarà legato l’evento di ricezione e il metodo da invocare ogni qual volta la coda notificherà la presenza di un nuovo messaggio; il metodo BasicConsume, in cui è possibile definire una peculiarità del ricevente: quando il messaggio viene notificato è possibile stabilire due momenti differenti in cui confermare la rimozione di quest’ultimo dalla coda:

  1. Con Ack esplicito: restituito dal metodo di gestione del messaggio al termine della sua elaborazione (al termine per convenzione, nulla vieta di farlo al principio, ma se si sceglie per l’Ack esplicito tipicamente è per avere il maggior controllo possibile su quanto succede)

  2. Con Ack implicito: in questo caso l’Ack viene restituito immediatamente appena il metodo viene invocato, cancellando il messaggio dalla coda. Questo approccio espone al rischio di perdita del messaggio qualora la sua elaborazione fallisca, ma si rivela più performante e mantiene la coda mediamente più scarica, dal momento che l’Ack viene restituito immediatamente e non al termine di eventuali lunghe elaborazioni

È possibile, mettere più consumatori in concorrenza sulla medesima coda, in questo caso RabbitMQ notificherà ai consumatori rispetto ai parametri che questi hanno impostato. Tipicamente, in presenza di Ack impliciti, la risoluzione avviene in round robin sui consumatori. Se invece gli Ack sono espliciti, possiamo andare a definire il BasicQos (Quality of Service).

channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);

Aggiungendo questa istruzione dopo la QueueDeclare è possibile definire a livello globale (qualora si voglia fornire questi parametri a tutti i model) o solo al model specifico che sta invocando il metodo BasicQos, due parametri: il prefetchSize e il prefetchCount.

  1. Il Prefetch Size specifica quanti messaggi possono essere inviati in anticipo, ossia quanti messaggi possono essere notificati senza aver ricevuto un Ack (ha pertanto senso solo quando viene utilizzato l’Ack esplicito). In questo modo si genererà una “minicoda” interna al consumatore, fatta con gli eventi di notifica dei messaggi che si accumulano. Il valore 0 indica l’assenza di limiti

  2. Il Prefetch Count indica la finestra del consumatore, ossia quanti messaggi sono destinabili a quel consumatore prima che questi abbia iniziato a notificare gli Ack. Va a definire in pratica quando la “minicoda” di eventi può essere lunga per uno specifico consumatore

Per inviare un Ack esplicito, normalmente al termine del metodo usato come handler del consumer.Received si pone

channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);

In cui il DeliveryTag indica il messaggio per cui si sta notificando l’Ack, mentre il flag seguente serve a gestire le politiche di rimozione, ossia se un messaggio debba essere rimosso subito alla prima notifica oppure se debba essere recapitato a tutti i consumatori, pertanto fin quando non sarà ricevuto un numero di Ack pari al numero di consumatori collegati (o un Ack che non richieda ulteriori attese), il messaggio resterà in coda (fatte salve eventuali politiche legate al TTL). Sarà compito di RabbitMQ notificare un nuovo messaggio coerentemente con le politiche impostate a ciascun consumatore. Se ad esempio C1 e C2 debbono entrambi processare gli stessi messaggi, ma C1 è molto più veloce di C2, avremo che per ogni nuovo messaggio, C1 riceverà una notifica coerente con la sua impostazione di prefetch, e i messaggi gli verranno inoltrati non soltanto dalla testa della coda, dove sono presenti messaggi a lui già recapitati che sono in attesa di essere notificati anche a C2 o che semplicemente sono ancora in attesa di un Ack. È importante notare come tutta questa gestione sarebbe normalmente molto complessa, ma viene compresa tra le comuni configurazioni di base di RabbitMQ che fornisce questo scenario senza alcuno sforzo o degrado di performance.

Creazione di un nodo di distribuzione in RabbitMQ (in C#)

Molto più interessante è la possibilità di creare nodi di distribuzione (chiamati Exchange) e definirne le politiche di instradamento. È giusto svelare che le immagini viste poco fa sono in realtà la concettualizzazione della comunicazione punto-punto, ma nella realtà le implementazioni prevedono tutte l’introduzione di un nodo di distribuzione tra il produttore e la coda di consegna. Da notare quindi, in riferimento al precedente Send, che quelle due proprietà lasciate un attimo in sospeso (exchange: “”, routingKey: “hello”) vanno ad indicare che non abbiamo dato un nome all’exchange (sebbene esista per costruzione, ma di cui ci disinteressiamo in quanto parte del funzionamento interno di RabbitMQ) e opzionalmente una routingKey. Tale routingKey è inutile nell’esempio ma ci permette di dire che volendo possiamo precisare al consumatore che deve conoscere la routingKey dei messaggi per poterli ascoltare, e anche se non rappresenta un meccanismo di sicurezza, è utile definirne sempre una per evitare errori e non veder recapitati messaggi sbagliati ad un consumatore che stiamo testando.

Quel nodo, chiamato Exchange, diventa molto importante qualora la distribuzione non sia su una coda soltanto, ma su una serie di Subscriber, vengono infatti chiamati sottoscrittori i consumatori che collegano una coda ad un nodo di distribuzione.

I nodi in RabbitMQ sono di 4 tipi, e bene o male ritroviamo concetti analoghi in altri software, anche se con nomi differenti:

  1. Fanout: il nodo replica i messaggi su tutti i sottoscrittori

  2. Direct: ogni sottoscrittore chiede una o più routing key, quella che potremmo intendere come la categoria dei messaggi a cui vorrei fare l’abbonamento

  3. Topic: analogo al Direct, solo che in questo caso le routing key possono essere descritte mediante l’uso di caratteri speciali (wildcard)

  4. Header: il meccanismo più complesso, consente di scegliere le politiche di distribuzione dei messaggi rispetto a campi specifici che compaiono nell’header del messaggio. Servirà definire i campi di interesse, i valori e i criteri di confronto su cui basare l’instradamento.


Facendo un esempio analogo al precedente, avremo un produttore fatto in questo modo:

In sostanza l’unica differenza consiste nell’uso di ExchangeDeclare in sostituzione al QueueDeclare

Uno dei sottoscrittori invece, avrà questa forma:

I punti di interesse qui sono i seguenti:

  1. Notiamo che anche il ricevitore dichiara l’exchange. Questo perché la ExchangeDeclare (e analogamente la QueueDeclare) crea o trova l’exchange con quel nome. Questa è una peculiarità di RabbitMQ in effetti, altri message broker hanno classi e metodi dedicati alla creazione e al collegamento con nodi e code, mentre in RabbitMQ questa distinzione è trasparente allo sviluppatore. Questo rappresenta un vantaggio qualora volessimo creare una struttura con delle date di scadenza (gli expiring citati in precedenza). Se le strutture si autoeliminano dopo un certo tempo, quando un processo si attiva e prova a inviare o ricevere un messaggio rischia di non trovare la coda o il nodo a cui collegarsi (cosa frequente per chi usa Service Bus, che predilige un approccio statico della topografia) generando un errore. In RabbitMQ invece, chi parte per primo crea la struttura, che i successivi utilizzatori, siano essi produttori o consumatori, si troveranno già fatta, e dovranno semplicemente collegarcisi.

  2. Il sottoscrittore procede poi creando una coda (lasciando che venga generato un nome univoco dalla libreria)


Il sottoscrittore collega la coda appena creata al nodo (con l’operazione di QueueBind). In questo modo andiamo a disegnare la struttura della rete che poi useremo per la distribuzione dei messaggi. In particolare in RabbitMQ si osservi che il QueueBind va ripetuto per ogni RoutingKey che si vuole ricevere su quella sottoscrizione, mentre altri broker consentono sottoscrizioni multikey (passando ad esempio degli array di string).

Questa potrebbe, ad esempio, essere la topologia di un sistema di log differenziato per livello.

E infine un esempio di Topic, in cui lo smistamento avviene per mezzo di caratteri speciali:

In RabbitMQ abbiamo che:

  1. * indica esattamente un parola
  2. # indica zero o più parole

Adottando questi caratteri speciali con questi significati, per costruzione le Routing Key prendono la forma di frasi separate da punto.

Infine, volendo schematizzare un exchange di tipo header:

In questo caso è bene precisare che l’estrema potenza nella definizione delle regole di instradamento si paga con un degrado del throughput finale del sistema.

Parlando di prestazioni

Ogni message broker ha punti di forza su cui cerca di fare leva per guadagnarsi una posizione di rilievo in uno specifico settore. Kafka punta sul numero di messaggi trasmessi, ActiveMQ punta ad essere il compromesso migliore tra dimensione dei messaggi, numeri di messaggi e latenza complessiva senza eccellere in nulla, RabbitMQ punta sulla estrema flessibilità della sua capacità di routing senza che questa impatti troppo sul risultato finale. Prima di adottare uno di questi è sempre bene documentarsi e cercare i benchmark che rispecchiano il proprio caso di utilizzo, e non cercare di risolvere ogni problema con il medesimo strumento. Quasi tutti i message broker possono reggere messaggi di piccole dimensioni (1Kb) su normali PC di sviluppo possono raggiungere i 50.000 messaggi al secondo, un valore discreto per ogni genere di test. 

Giusto per dare un’idea del tipo di analisi che si possono fare su un message broker, vediamo brevemente alcuni dei risultati pubblicati di test effettuati con RabbitMQ.

Un test molto importante è il confronto fra messaggi inviati e byte inviati. Mandare molti messaggi di pochi byte comporta un overhead notevole, con conseguente degrado di performance, mentre inviare pochi messaggi di alcuni mega si rivela particolarmente deleterio per il numero di messaggi che si possono recapitare. Qualora si fosse nella prima condizione, una possibile strategia è l’invio di liste di messaggi, in modo da avvicinarmi all’incrocio del trade off. Con il grafico riportato, ad esempio, vediamo che abbiamo la possibilità di inviare 4000 messaggi da circa 256 byte. Se i nostri messaggi fossero da 20 byte, opportunamente serializzati ci permetterebbero un risultato finale di 4000 messaggi da liste di 240 byte (approfittando quindi di un fattore moltiplicativo pari a 12, portando il numero di messaggi reali inviati a 48.000)

Questa tecnica rientra nell’approccio batch processing, e si possono ottenere moltiplicatori importanti se ben sfruttata.

Un altro test riguarda la valutazione del message rate rispetto alla dimensione del messaggio in byte all’aumentare del numero di produttori. 

Da confrontarsi con il corrispettivo andamento del traffico dati se si analizzano i byte trasmessi anziché i singoli messaggi.

Tirando le fila

In conclusione, i vantaggi nell’utilizzare i message broker sono da ricercarsi nell’elevata efficienza e scalabilità che possono fornire all’interno di un’architettura distribuita e nella rapidità dello sviluppo del software secondo l’approccio a microservizi, soluzione nella quale la parte di colloquio tra processi può essere ampiamente delegata ai servizi di message broking. Gli svantaggi sono principalmente due: il primo è l’aumento della complessità dell’intera architettura, dal momento che si va ad inserire un modulo molto vasto e a cui vengono affidati compiti di primaria importanza (è bene una fase di studio preliminare molto ampia in modo da scegliere il software che meglio si adatta alle esigenze specifiche del progetto); in secondo luogo non sono uno strumento adatto per implementare chiamate sincrone. Sebbene sia possibile simulare chiamare RPC con l’uso di più code per trasmissione e ricezione dei dati, gli stessi autori ne sconsigliano l’uso se non in casi davvero eccezionali, dal momento che è un uso degenere dello strumento, nato per essere asincrono. Restano quindi svariate attività per cui è comunque richiestoi di implementare interfacce e protocolli di comunicazione allo sviluppatore, sebbene anche per questo scenario esistono ormai valide librerie che semplificano molto la vita (ad esempio gRPC).

Come corollario e per completezza è bene fare alcune precisazioni: l’approccio Message Oriented non necessariamente prevede la persistenza come requisito. Esistono casi in cui è preferibile una comunicazione transiente. I sistemi paralleli sono un caso particolare dei sistemi distribuiti, e nel caso di sistemi paralleli ci sono delle peculiarità che possono essere ottimizzate con soluzioni specifiche. Non esiste una linea di demarcazione netta tra ciò che è calcolo distribuito e calcolo parallelo, ma osservando gli estremi delle definizioni, possiamo dire che nei processi distribuiti un singolo problema viene diviso in più attività, e ciascuna attività viene svolta da un componente specifico, che dialoga in un flusso più o meno continuo con gli altri componenti. Nelle applicazioni parallele, ciascun componente svolge il medesimo compito concentrandosi su un sotto insieme dei dati disponibili. Le necessità di comunicazione in questi due scenari sono differenti, e quindi è preferibile adottare un sistema di scambio messaggi che non sfrutti le code e la loro persistenza, come il Message Passing Interface (MPI), dove il messaggio è più un sistema di coordinamento che non un vero e proprio input per gli altri componenti.

Per chi volesse approfondire un buon testo che presenta i diversi scenari dei paradigmi accennati è:
S. Tanenbaum, M. Van Steen, “Sistemi distribuiti. Principi e paradigmi”, Pearson Education Italia, 2007.

Mentre per approfondire i dettagli di RabbitMQ: 

https://www.rabbitmq.com/documentation.html 

https://www.cloudamqp.com/

Infine, un ottimo esempio di test comparativo tra RabbitMQ e Kafka: https://arxiv.org/pdf/1704.00411.pdf

Scritto da
Michel Lamoure

#jointherevolution

Protezione dei Web Services

Una panoramica sui metodi usati per la protezione dell’accesso alle applicazioni

di Roberto Bellucci
Orbyta Basedue
Senior System Programmer 

Web Service e Web API

La differenza tra Web Service (WS) e Web API (API) è molto tecnica ma la loro utilità è la stessa, ossia consentire lo scambio di dati tra le applicazioni attraverso il protocollo di trasporto HTTP(S), indipendentemente dal sistema operativo su cui girano e dal linguaggio usato.

Grazie ai WS ed alle API, le aziende connettono tra loro i servizi e trasferiscono i dati. L’aspetto tecnico è molto importante ed è utile sapere che i Web Service si basano su standard molto rigidi e devono sottostare ad un insieme di regole che definiscono la struttura (WSDL) dei messaggi scambiati che possono essere solo in formato XML, mentre le Web API accettano i dati anche in JSON ed essendo molto più snelle sono più adatte ad essere impiegate su dispositivi con banda limitata. Da un punto di vista tecnico queste sono le differenze principali, tuttavia come ho detto prima, l’utilità delle due soluzioni è la stessa, quindi per semplicità userò in questo articolo indifferentemente i termini Web Service(WS), Web API e API, come fossero sinonimi per parlare della loro “messa in sicurezza”.

La API Economy

“Le aziende di oggi si stanno trasformando e uno degli imperativi è quello di ottenere o fornire servizi e informazioni alle persone attraverso il Web.” Ho scritto questa frase quando stavo preparando la presentazione del seminario dallo stesso titolo destinato ai colleghi di Orbyta e benché l’evento si sia svolto in modalità webinar a causa della crisi già in atto, non mi è venuto in mente all’epoca di enfatizzarla come sto per fare ora, per sottolineare il fatto che a causa di quanto stiamo vivendo, la trasformazione a cui accennavo, stia prendendo un’accelerazione ancora più importante e che non ci saremmo mai aspettati!

Viene stimolata una nuova economia, denominata appunto API Economy, in cui l’Application Programmig Interface consente un accesso a servizi, applicazioni e sistemi per condividere le informazioni in maniera innovativa. 

Per poter operare in queste nuove realtà è quanto mai necessario che l’accesso ai dati e ai servizi risulti protetto.

WS/API Gateway

Sappiamo che i Firewall tradizionali proteggono solamente il traffico a livello IP controllando il traffico solamente in base alla porta. Per questo risultano incapaci di difendersi da attacchi sempre più sofisticati che si celano dentro payload applicativi, trasportati dai pacchetti IP che attraversano i firewall in un tunnel HTTP(S) esponendo le aziende a sempre nuovi attacchi.

E’ quindi indispensabile assicurarsi che passino solamente richieste valide, per servizi validi, da altrettanti validi clienti; ed ecco che entrano in gioco i Web Service & API Gateway, ovvero i firewall delle applicazioni. Vediamo cosa sono e come svolgono il loro compito.

Esistono molti tipi di WS e API Gateway, alcuni sono degli appliance, altri sono solo in versione virtuale e in cloud. I vendor che offrono queste soluzioni sono molti, tuttavia non sono tanti quelli che possono vantare livelli di sicurezza e prestazioni di alto livello.

maxresdefault

La mia esperienza in questo campo si basa sulla conoscenza dell’appliance IBM DataPower® Gateway considerato da molti uno dei leader del mercato.

Detto in poche parole il DataPower® Gateway funge da proxy tra le applicazioni di front-end (FE) e le API di back-end (BE); in pratica le applicazioni di FE non chiameranno direttamente le API ma il Datapower che ha il compito di intercettare tutte le Request alle API ed eseguire svariate funzioni di sicurezza ma non solo come vedremo, prima di inoltrare al server di BE dove risiedono le API le richieste alle quali erano indirizzate. 

Tra le funzioni più comuni che un API Gateway deve fare e che il DataPower® svolge molto bene, troviamo la gestione della connessione SSL/TLS, l’autenticazione attraverso i certificati, il controllo della grandezza del payload, lo schema-validation se si tratta di WS,  la limitazione di velocità, il routing, la conversione di protocolli, ad esempio da XML a JSON e viceversa ma anche CSV, Cobol, binary eccetera. 

Il DataPower® inoltre, offre un’ampia scelta sul fronte dell’integrazione, permettendo la connessione al Mainframe e al Cics, a molti database quali Oracle, MS SQL, Sybase, DB2 e IMS. 

Molte funzionalità, come ad esempio l’autenticazione e lo schema-validation, sono feature già pre configurate e vengono attivate con semplici comandi dalla GUI, altre funzioni come ad esempio la conversione di protocolli necessitano invece di un’attività di implementazione a cura del sistemista attraverso la programmazione con linguaggi di scripting come Java Script e XSLT, quest’ultimo più adatto a gestire payload in formato XML.

Dunque, grazie ai linguaggi JavaScript e XSLT, si è in grado di personalizzare ciascun WS e API esposti dal DataPower®, consentendoci di innescare qualsiasi azione sia a fronte del messaggio in ingresso dal FE che durante la fase di ricezione del messaggio di risposta da parte dell’API. Ad esempio, oltre al già citato caso della conversione dei protocolli, pensiamo a quanto potrebbe essere utile tracciare “chi fa che cosa”, ovvero salvare le Request e le Response relative ai WS, scrivendo su un DB tutta una serie di informazioni utili ai fini di audit e debug, il tutto, vi posso assicurare, con tempi medi di esecuzione di pochissimi millisecondi soprattutto nel caso di trasformazioni XML grazie alla capacità del Datapower di gestire queste operazioni come si suol dire “at wirespeed”.

Roberto Bellucci

Senior System Programmer 

Inizia come sistemista Mainframe al CED della Olivetti, dopo qualche anno lascia l’azienda di Ivrea per dedicarsi alla consulenza, in seguito alcune iniziative imprenditoriali lo porteranno alla co-fondazione di Basedue. Dopo 15 anni lascia l’esperienza imprenditoriale e torna a dedicarsi alla consulenza. Attualmente fa parte di Orbyta Basedue e svolge la sua attività di consulenza in IntesaSanpaolo.

#jointherevolution

Come migliorare l’efficienza e la produttività dell’AREA 51, di questi tempi…

In ORBYTA, in questo periodo di lavoro remoto del team di sviluppo, abbiamo recentemente introdotto una modalità di pair-programming per mezzo di strumenti di collaborazione real-time. I risultati ottenuti sono quelli di aver prodotto sessioni di programmazione più attente, una collaborazione più semplice e un migliore lavoro di squadra nel suo insieme. Nelle righe di questo articolo cercheremo di esporre le nostre considerazioni a riguardo e il bagaglio di esperienza che potremo portare con noi ad emergenza finita.

Live Share: la funzionalità che rende possibile la magia 

Alcuni strumenti integrati di sviluppo, come VS Code e Visual Studio .net 2019, forniscono estensioni per consentire la collaborazione di codice in tempo reale. La funzionalità in questione si chiama “Live Share”. Per mezzo di Live Share ci si può collegare ad una sessione di collaborazione e modificare gli stessi file in tempo reale, proprio come è possibile editare un documento di testo in maniera collaborativa con Google Docs

Per mezzo di questa estensione è possibile partecipare al lavoro del collega, suggerire modifiche, intervenire, osservare il cursore, la selezione e tutto ciò che le altre persone stanno digitando.

Schermata di VS Code in Live Share
Schermata di VS Code in Live Share

Pair-programming in ufficio e in remoto

In tempi differenti, il team di sviluppo era solito effettuare sessioni di pair-programming in cui 2 sviluppatori potevano condividere una postazione di lavoro e si alternavano alla tastiera per effettuare task particolarmente complicati, refactoring, progettazione di componenti e revisione di codice.
Non potendo più usufruire del nostro ufficio, ci siamo trovati nella condizione di dover estendere questa pratica di lavoro anche lavorando da remoto. 

In principio, come tutto il mondo, abbiamo fatto uso massivo di sessioni di condivisione schermo con i nostri canali di comunicazione, che vanno da Microsoft Teams, Hangouts, Cisco Webex, Skype per mezzo di call con condivisione schermo. Dopo i primi giorni in cui lo slancio generale era maggiore, questa modalità ha evidenziato alcune problematiche che ci hanno spinto a provare vie differenti.

Live Session in VS Code
Live Session in VS Code

Di seguito proveremo a riassumere alcuni degli aspetti positivi rilevati utilizzando Live Share in luogo di una semplice condivisione schermo.

1. Aumenta il coinvolgimento e aiuta a mantenere l’attenzione

Quando si fa pair-programming, di solito si finisce per avere una persona che scrive tutto il codice (il “driver”) e l’altra che discute delle scelte di implementazione, dando suggerimenti (il “navigatore”). Ma è difficile per questa persona rimanere concentrati dopo poche ore. Soprattutto dopo che l’attività è stata chiaramente definita e tutto ciò che resta da fare è scrivere il codice.
È ancora più difficile quando lavori in remoto, perché perdere la concentrazione durante un Hangouts o Teams è molto facile.

Live Session in Visual Studio .net 2019
Live Session in Visual Studio .net 2019

Quando il livello della comunicazione decade si è costretti ad interrompere la programmazione, dividere il lavoro o scambiarsi di ruolo o … lasciare addormentare la persona che non fa nulla. La cosa eccezionale della collaborazione live è che ciò non accade facilmente. Dato che state modificando entrambi lo stesso codice in tempo reale, è possibile accedere rapidamente al codice sorgente. 

Durante una sessione di Live Share è anche possibile suddividere le attività, senza interrompere la programmazione in coppia, il che è più semplice e rende immediata la sincronizzazione del codice, non necessitando differenti push di codice. Ciò, infatti, evita conflitti sullo strumento di controllo della versione quando si lavora sulla stessa funzionalità. Il codice sorgente modificato risulta essere solo quello dell’utente che avvia la sessione di condivisione.

2. Migliora la comunicazione

In sessioni di condivisione schermo ci si ritrova quasi sempre a dettare il codice al collega, a provare a spiegare qualcosa senza riuscirci facilmente. Con la collaborazione live, non è necessario farlo: puoi semplicemente digitarlo, il che ti farà risparmiare innumerevoli “Non l’ho capito” o “Potresti inviarmelo in chat?”.

3. Permette collaborazione ed esplorazione

Di solito è difficile partecipare a una sessione di programmazione in coppia in corso: è necessario comprendere l’attività, riconoscere cosa è stato fatto, cosa è rimasto da fare e come vengono costruite le cose.

Quando si partecipa a una sessione di collaborazione live, d’altra parte, è possibile impiegare del tempo per esplorare e comprendere da soli, senza dover interrompere gli altri programmatori. Abbiamo trovato che ciò è particolarmente utile, perché è anche un buon modo per avere un’opinione esterna su qualcosa senza ottenere spiegazioni forse distorte. E se vuoi che i tuoi colleghi ti diano una presentazione, c’è una modalità presentatore che seguirà il cursore del presentatore. Un bel modo di mostrare la tua idea.

4. Rimani nel tuo ambiente di sviluppo preferito 

Quando lavori in coppia alla postazione di un collega sei costretto ad usare un ambiente di sviluppo, tema, snippets, estensioni, colori di sistema, tastiera e mouse a cui non sei abituato e che ti rallentano nel lavoro. Con Live Share puoi saltare direttamente all’attività senza dover decifrare la strana configurazione di Visual Studio o utilizzare il mouse verticale che non sai minimamente utilizzare.

5. Riduce le distanze e il senso di alienazione

Poter lavorare sullo stesso task o su funzionalità differenti in contemporanea, con uno dei due che scrive la parte di test automatici o effettua una verifica in tempo reale del tuo codice, permette ai componenti del team di sentirsi sempre parte del progetto, meno soli e alienati dal mondo esterno, a cui si era soliti appartenere. Lo sviluppo del codice di test in pair-programming consente, oltretutto, di verificare se entrambi hanno capito il compito allo stesso modo e hanno pensato agli stessi casi limite o alle stesse scelte progettuali. È anche un modo per attenuare il disagio che l’attuale processo di lavoro oggettivamente comporta.

Configurazione e condivisione

Cosa bisogna fare per provare Live Share? È possibile farlo per mezzo di Visual Studio 2019, senza dover installare estensioni o anche utilizzando VS Code e l’estensione Live Share.

VS Code

In VS Code sarà sufficiente installare l’estensione VS Code Live Share, aprire un progetto e cliccare sul link per condividere una sessione di codice.

Installazione di Live Share Extension per VS Code
Installazione di Live Share Extension per VS Code

Di seguito la schermata di benvenuto di Live Share con la descrizione di tutte le funzionalità.

Funzionalità di Live Share Extension per VS Code
Funzionalità di Live Share Extension per VS Code

Una volta generata la sessione, l’utente destinatario riceverà un link che potrà utilizzare per accedere anche lui alla sessione.

Schermata di condivisione link di sessione di Live Share
Schermata di condivisione link di sessione di Live Share

Oltre che condividere il codice, in VS Code, è possibile aprire un terminale condiviso, disponibile per tutti nella sessione. Questo è utile per lanciare comandi, eseguire test quando non si è host, perché non hai i file sorgente sul tuo computer.

In VS Code, puoi anche avviare una chat vocale da una sessione di collaborazione e hai una chat di testo. Quindi è superfluo utilizzare altre applicazioni per comunicare a voce.

E qualcosa rimane

In questa situazione globale in cui ci troviamo, per un motivo o per un altro, il numero, la frequenza delle chiamate, delle sessioni video è aumentato a dismisura. Non altrettanto questo sta influenzando positivamente la produttività dei team di sviluppo. Col passare dei giorni, poi delle settimane, quella sorta di slancio legato alla nuova modalità di lavoro da remoto si è senza dubbio affievolita. Sta nascendo, però, la consapevolezza che un modo nuovo di lavorare è necessario oggi, ma lo sarà ancor di più nelle fasi successive di questa nostra epoca. 

Dobbiamo consolidare e possibilmente migliorare questa nuova modalità di fare gruppo, essere efficienti e produttivi, anche se solo fisicamente distanti. Restiamo uniti… anche in Live Share.

#jointherevolution