Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

mercoledì 20 maggio 2020

C'era una volta un... Allarme
come scrivere un Alarm Manager in C - pt.1

Rick Dalton: Gli attori devono fare un sacco di cose pericolose. Cliff qui ha la parte più dura.
Intervistatore: E' così che descrivi il tuo lavoro, Cliff?
Cliff Booth: Cioè che ho la parte più dura? Sì, e me ne vanto.
Nel grande C'era una volta a... Hollywood dell'altrettanto grande Quentin Tarantino (grande appassionato di Cinema Italiano, per chi non lo sapesse), Rick"DiCaprio"Dalton (attore) descrive la durezza del ruolo di Cliff"Pitt"Booth (stuntman): ecco, anche noi programmatori abbiamo (senza doppi sensi) parti dure del lavoro, tipo trattare problemi inaspettati che possono sorgere durante l'esecuzione di un programma. E cosa dovrebbe fare una applicazione robusta e industriale quando c'è un problema in corso? Ma alzare un allarme! Elementare, mio caro Watson!. Beh, visto che ho analizzato molte applicazioni che si fregiano dell'aggettivo "industriale" senza avere un Gestore di Allarmi (Alarm Manager per gli amici) ho pensato che era il caso di scriverci sopra e un articolo, perché diciamolo chiaramente: una applicazione industriale senza un Alarm Manager "è come un giardino senza fiori" [semicit.].
...ero molto allarmato, capisci...
Allora, visto che non ho voglia di spiegare il come, cosa e perché del Alarm Manager, ho deciso di riportarvi la trascrizione fedele di una piccola discussione che ha avuto quell'irascibile di mio cuggino con un suo collega programmatore:
programmatore: Ma allora cosa fa un Alarm Manager?
mio cuggino: Vediamo, un po'... serve a mantenere una lista di allarmi attivi e, in parallelo, a mantenere una lista di allarmi storici, gli allarmi non più attivi.
programmatore: Ma quindi a cosa serve?
mio cuggino: Beh, io direi che serve a darci, in tempo reale, una immagine dei problemi attivi del programma che stiamo eseguendo. Ovviamente i problemi devono essere ben descritti e facilmente identificabili rispondendo alle domande: "cosa è successo" e "quando è successo". Poi serve a darci, in tempo reale, una immagine dei problemi che si sono verificati ma che sono stati risolti con un intervento di auto-correzione, o con una azione esterna, o con svariati altri metodi. Rispetto agli allarmi attivi deve essere evidenziata anche la risposta alla domanda "quando è terminato".
programmatore: A, vabbè, ma io queste cose le faccio con un Logger!
mio cuggino: No! Il Logger ti serve a priori, ad esempio se il programma si è schiantato e vuoi analizzare quello che è successo, mentre l'Alarm Manager ti offre un immagine filtrata, in tempo reale, di quello che succede e che è successo recentemente: è utile anche e soprattutto per l'utente finale dell'applicazione, tipo un tecnico che controlla una macchina di produzione. Il Logger, che convive sempre con l'Alarm Manager in una buona Applicazione Industriale, serve più ai programmatori che agli utilizzatori.
programmatore: Continuo a pensare che è meglio il Logger...
mio cuggino: Continuo a pensare che non capisci una mazza... detto con tutto il rispetto, però!
Ok, è giunto il momento di mostrare il codice, che è, al solito, così ben commentato che mi permetterà di evitare di discutere tutti i micro-dettagli. Vai con l'header!

#ifndef ALARMER_H
#define ALARMER_H

#include <pthread.h>
#include <stdbool.h>
#include <time.h>

#define TEXT_SIZE       80  // size del testo
#define MAX_ALARMS      256 // numero max di allarmi
#define MAX_HIST_ALARMS 8   // numero max di allarmi storici

// structure Alarm definition
typedef struct {
    int    id;              // ID allarme
    int    owner;           // owner allarme
    char   fmt[TEXT_SIZE];  // testo base allarme (formato printf)
    char   text[TEXT_SIZE]; // testo allarme
    bool   active;          // flag di attività (true=attivo)
    time_t on_time;         // istante di attivazione (secondi da Epoch)
    time_t off_time;        // istante di disatttivazione (secondi da Epoch)
} Alarm;

// structure Alarmer
typedef struct {
    // tabelle allarmi attivi/storici
    Alarm *alarms_table[MAX_ALARMS];
    Alarm hist_alarms_table[MAX_HIST_ALARMS];

    // indici per find first/next allarmi attivi/storici
    int find_active_index;
    int find_hist_index;

    // dati per gestione circolare allarmi storici
    int  hist_index;        // indice per gestione circolare allarmi storici
    int  num_hist_found;
    bool restart_hist;

    // mutex di sincronizzazione (per uso thread-safe)
    pthread_mutex_t mount_mutex;      // mutex per operazioni mount
    pthread_mutex_t add_mutex;        // mutex per operazioni add
    pthread_mutex_t onoff_mutex;      // mutex per operazioni on/off
} Alarmer;

// prototipi globali
int   setAlarmer(Alarmer *alarmer);
//void  delAlarmer(Alarmer *alarmer);
void  mountAlarms(Alarmer *alarmer, const Alarm *alarms/*, int offset, int instance*/);
//void  umountAlarms(Alarmer *alarmer, int owner, int offset, int instance);
void  addAlarm(Alarmer *alarmer, int id, int owner, const char *fmt);
//void  delAlarm(Alarmer *alarmer, int id, int owner);
void  onAlarm(Alarmer *alarmer, int id, int owner, ...);
void  offAlarm(Alarmer *alarmer, int id, int owner);
char* getAlarms(Alarmer *alarmer, char *dest, size_t size);
char* getHistAlarms(Alarmer *alarmer, char *dest, size_t size);

#endif /* ALARMER_H */
Come avrete notato, abbiamo due strutture, una definisce "un allarme" e l'altra definisce l'Alarm Manager stesso (che ho chiamato Alarmer per essere un po' più sintetico). L'allarme contiene tutti i dati che ne consentono l'identificazione: un identificatore unico, un proprietario, un testo, un flag di attività e gli istanti di inizio e fine. L'Alarmer contiene invece tutti i dati necessari a trattare gli allarmi attivi e storici, tra l'altro in modo thread-safe (le cose bisogna farle bene fin dall'inizio). E poi ci sono i prototipi delle funzioni globali, quelle che permettono l'uso dell'Alarmer.

Avrete anche notato che alcuni prototipi sono commentati: infatti quello che sto per offrirvi è una versione ridotta e semplificata di un Alarm Manager, perché quello completo usa molto codice e non voglio annoiare nessuno: vi assicuro però che, una volta letto l'articolo e compilato il demo (funzionante), vi sarà tutto così chiaro che potrete scrivere quello che manca senza problemi (e comunque anche questa versione light è utilizzabile quasi così com'è).

E adesso andiamo avanti con il codice del demo, mentre l'implementazione vera e propria sarà nella seconda parte dell'articolo: è una scelta non casuale, perché grazie all'esempio d'uso saranno più chiare alcune scelte di implementazione, fidatevi.

Il nostro demo è composto da tre sorgenti: un main() e due utilizzatori del Alarm Manager: è una scelta reale che rispecchia la filosofia del progetto: un utilizzatore (che può essere un client, un server, un processore di dati, ecc.) si registra al Alarm Manager montando i propri allarmi (questo meccanismo l'avevamo già visto col Watchdog, ricordate?), e può anche smontarli prima di uscire (usando la funzione di umount, che è una di quelle commentate). L'operazione di mount inserisce gli allarmi nelle posizioni opportune della tabella interna, e può gestire anche "gli stessi allarmi" più volte: ad esempio una applicazione che contiene due istanze dello stesso tipo di utilizzatore, avrà due gruppi identici di allarmi montati che saranno differenziati usando i campi instance e offset (che in questa versione light sono commentati, occhio!).

Bando alle ciance, vai col main()!
#include "alarmer.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

// prototipi esterni
void *tcpServer(void *arg);
void *udpServer(void *arg);

// funzione main()
int main(int argc, char* argv[])
{
    int error;

    // init del alarmer
    Alarmer alarmer;
    if (setAlarmer(&alarmer) != 0) {
        printf("%s: non posso usare l'alarmer\n", argv[0]);
        return EXIT_FAILURE;
    }

    // avvio tcpServer
    pthread_t tid_A;
    if ((error = pthread_create(&tid_A, NULL, &tcpServer, (void *)&alarmer)) != 0) {
        printf("%s: non posso creare il tcpServer (%s)\n", argv[0], strerror(error));
        return EXIT_FAILURE;
    }

    // avvio udpServer
    pthread_t tid_B;
    if ((error = pthread_create(&tid_B, NULL, &udpServer, (void *)&alarmer)) != 0) {
        printf("%s: non posso creare il tcpServer (%s)\n", argv[0], strerror(error));
        return EXIT_FAILURE;
    }

    // loop infinito del main
    for (;;) {
        char buf[4096];

        // mostro gli allarmi attivi
        getAlarms(&alarmer, buf, sizeof(buf));
        if (strlen(buf)) {
            printf("ALLARMI\n");
            printf("%s  %2s %-80s  %-20s\n", "ID", "Pr.", "Allarme", "Attivato il");
            printf("%s\n", buf);
        }

        // mostro gli allarmi storici
        getHistAlarms(&alarmer, buf, sizeof(buf));
        if (strlen(buf)) {
            printf("ALLARMI STORICI\n");
            printf("%s  %2s %-80s  %-20s %s\n", "ID", "Pr.", "Allarme", "Attivato il",
                   "Disattivato il");
            printf("%s\n", buf);
        }

        // sleep del loop
        sleep(5);
    }

    // attesa terminazione tcpServer
    if ((error = pthread_join(tid_A, NULL)) != 0) {
        printf("%s: non posso unire il tcpServer (%s)\n", argv[0], strerror(error));
        return EXIT_FAILURE;
    }

    // attesa terminazione udpServer
    if ((error = pthread_join(tid_B, NULL)) != 0) {
        printf("%s: non posso unire il udpServer (%s)\n", argv[0], strerror(error));
        return EXIT_FAILURE;
    }

    // esce con Ok
    return EXIT_SUCCESS;
}
Come avrete notato il main() è un semplice programma multithread, simile a quello visto nell'articolo sul Watchdog. La struttura della nostra applicazione è modulare, ed ogni modulo è un thread del main(). In questo demo abbiamo due moduli, che ho chiamato, tanto per fare un esempio, tcpServer e udpServer. Riepilogando: abbiamo un Alarm Manager, instanziato dal main(), che viene passato ai moduli, che possono essere anche molti, e ogni modulo monterà i propri allarmi nell'Alarm Manager.

E adesso e`il momento di mostrare il codice del tcpServer che, essendo un esempio, di server TCP ha solo il nome, mentre è, in realtà, solo un loop di attivazione/disattivazione allarmi per mostrare visualmente il funzionamento. Vi aggiungo che il codice dell'udpServer è inutile mostrarlo: é praticamente identico all'altro modulo, con qualche nome diverso e con i testi degli allarmi cambiati per differenziarsi dal tcpServer (ricordarsi, però, di usare un nuovo owner: se il tcpServer ha owner=0 l'udpServer deve avere owner=1). Vai col codice!
#include "alarmer.h"
#include <unistd.h>

// owner degli allarmi
#define O_TCPSERVER 0

// ID degli allarmi
#define A_TCPSERVER_INDEX_ERR   0
#define A_TCPSERVER_WATCH_ERR   1

// alarm offset per addAlarm()
//#define OFFS_A_TCPSERVER    2

// tabella allarmi tcpserver
static const Alarm alarms_tcpsrv[] = {
    // id                    owner        fmt
    { A_TCPSERVER_INDEX_ERR, O_TCPSERVER, "%s: indice non valido (%d)"        },
    { A_TCPSERVER_WATCH_ERR, O_TCPSERVER, "%s: non posso usare il watch (%d)" },
    { -1,                    0,           ""                                  },
};

// tcpServer() - un (finto) tcpServer da lanciare come thread del main()
void *tcpServer(void *arg)
{
    // ottengo i dati del thread con un cast (Alarmer *) di void *arg
    Alarmer *alarmer = (Alarmer *)arg;

    // monto gli allarmi
    mountAlarms(alarmer, alarms_tcpsrv);

    // lancio i due allarmi per il test
    onAlarm(alarmer, A_TCPSERVER_INDEX_ERR, O_TCPSERVER, __func__, 10);
    onAlarm(alarmer, A_TCPSERVER_WATCH_ERR, O_TCPSERVER, __func__, 20);

    // loop del thread
    for (int i = 0; ; i++) {
        // per il test il thread attiva (on) e disattiva (off) gli allarmi periodicamente
        if (i == 7)     // ogni 7 secondi
            offAlarm(alarmer, A_TCPSERVER_WATCH_ERR, O_TCPSERVER);

        if (i == 14) {  // ogni 14 secondi
            onAlarm(alarmer, A_TCPSERVER_WATCH_ERR, O_TCPSERVER, __func__, 30);
            i = 0;
        }

        // sleep del thread (1 secondo)
        sleep(1);
    }

    // il thread esce
    return NULL;
}
Ricetta finale: prendere il main e il tcpServer, aggiungere il modulo udpServer (ispirato al tcpServer), compilare bene (uhm... quando disponibile il codice dell'implementazione), aggiungere un pizzico di sale ed eseguire. Vedrete che ogni 7 secondi si mostreranno gli allarmi attivi e storici, in maniera dinamica. Tra l'altro gli allarmi storici (che hanno, nel demo, un buffer di 8) quando raggiungono il limite cominciano a "circolare", cioè i più vecchi vengono cancellati per far posto ai nuovi. Provare per credere.

E per oggi è tutto, nella seconda parte vi mostreró l'implementazione che è, secondo me, abbastanza interessante. Non trattenete il respiro nell'attesa, mi raccomando...

Ciao, e al prossimo post!