24. Esercizio pratico: Versione 7
24.1. Introduzione
La versione 7 dell'applicazione per il calcolo delle imposte è identica alla versione 6, ad eccezione dei seguenti dettagli:
- il client web invierà più richieste HTTP contemporaneamente. Nella versione precedente, queste richieste venivano inviate in sequenza. Il server poteva quindi elaborare solo una singola richiesta alla volta;
- il server sarà multithread: sarà in grado di elaborare più richieste contemporaneamente;
- Per monitorare l'esecuzione di queste richieste, il server web sarà dotato di un logger che registrerà i momenti chiave dell'elaborazione delle richieste in un file di testo;
- il server invierà un'e-mail all'amministratore dell'applicazione quando incontra un problema che gli impedisce di avviarsi, tipicamente un problema con il database associato al server web;
L'architettura dell'applicazione rimane invariata:

La struttura delle directory degli script è la seguente:

La cartella [http-servers/02] viene creata inizialmente copiando la cartella [http-servers/01]. Successivamente vengono apportate delle modifiche.
24.2. Le utility

24.2.1. La classe [Logger]
La classe [Logger] consentirà di registrare determinate azioni del server web in un file di testo:
- Righe 10–11: Definiamo un attributo di classe. Un attributo di classe è una proprietà condivisa da tutte le istanze della classe. Si fa riferimento ad esso utilizzando la notazione [Classe.attributo_di_classe] (righe 30, 39). L'attributo di classe [lock] fungerà da oggetto di sincronizzazione per tutti i thread che eseguono il codice nelle righe 31–36;
- righe 14–19: Il costruttore riceve il percorso assoluto del file di log. Questo file viene quindi aperto e il descrittore di file recuperato viene memorizzato nella classe;
- riga 17: il file di log viene aperto in modalità "append" (a). Ogni riga scritta verrà aggiunta alla fine del file;
- righe 22–39: il metodo [write] consente di scrivere nel file di log un messaggio passato come parametro. A questo messaggio vengono aggiunte due informazioni:
- riga 24: la data corrente;
- riga 25: l'ora corrente;
- riga 27: il nome del thread che scrive il log. È importante ricordare che un'applicazione web serve più utenti contemporaneamente. A ogni richiesta viene assegnato un thread per eseguirla. Se questo thread viene messo in pausa — tipicamente per un'operazione di I/O (rete, file, database) — allora il processore viene ceduto a un altro thread. A causa di queste possibili interruzioni, non possiamo essere certi che un thread riesca a scrivere una riga nel file di log senza essere interrotto. Esiste quindi il rischio che i log di due thread diversi possano confondersi. Il rischio è basso, forse addirittura nullo, ma abbiamo comunque deciso di mostrare come sincronizzare l’accesso di due thread a una risorsa condivisa, in questo caso il file di log;
- riga 30: prima di scrivere, il thread richiede la chiave della porta d'ingresso. La chiave richiesta è quella creata alla riga 11. È infatti unica: un attributo di classe è unico per tutte le istanze della classe;
- Al tempo T1, un thread denominato Thread1 ottiene la chiave. Può quindi eseguire la riga 33;
- Al tempo T2, il thread Thread1 viene messo in pausa prima ancora di aver finito di scrivere il log;
- Al tempo T3, anche il thread Thread2, che ha acquisito il processore, deve scrivere un log. Raggiunge quindi la riga 30, dove richiede la chiave della porta d'ingresso. Gli viene comunicato che un altro thread la possiede già. Viene quindi automaticamente messo in pausa. Questo accadrà per tutti i thread che richiedono questa chiave;
- Al tempo T4, il thread Thread1, che era stato messo in pausa, riacquista il processore. Quindi termina la scrittura del log;
- Righe 32–36: La scrittura nel file di log avviene in due fasi:
- riga 33: il descrittore di file ottenuto alla riga 17 funziona con un buffer. L'operazione [write] alla riga 33 scrive su questo buffer ma non direttamente sul file. Il buffer viene quindi scaricato sul file in determinate condizioni:
- il buffer è pieno;
- il descrittore di file subisce un'operazione [close] o [flush];
- riga 36: forziamo la scrittura della riga di log nel file. Lo facciamo perché vogliamo vedere i log dei diversi thread intercalati. Se non lo facessimo, i log di un singolo thread verrebbero scritti tutti contemporaneamente — quando il descrittore viene chiuso alla riga 45. Sarebbe quindi molto più difficile vedere che alcuni thread sono stati arrestati: dovremmo controllare i timestamp nei log;
- riga 39: il thread Thread1 restituisce il blocco che gli era stato assegnato. Ora può essere assegnato a un altro thread;
- riga 22: il metodo [write] è quindi sincronizzato: solo un thread alla volta scrive nel file di log. La chiave del meccanismo è la riga 30: qualunque cosa accada, solo un thread recupera la chiave per procedere alla riga successiva. La mantiene fino a quando non la restituisce (riga 39);
- righe 41–45: il metodo [close] libera le risorse allocate al descrittore del file di log;
I log scritti nel file di log avranno questo aspetto:
24.2.2. La classe [SendAdminMail]
La classe [SendAdminMail] consente di inviare un messaggio all'amministratore dell'applicazione quando l'applicazione va in crash.

La classe [SendAdminMail] è configurata nello script [config] [2] come segue:
La classe [SendAdminMail] riceve il dizionario dalle righe 2–13 e la configurazione per l'invio delle e-mail. La classe è la seguente:
- righe 24-54: questo è il codice già trattato nell'esempio |smtp/02|;
- riga 20: recuperiamo il riferimento di un logger. Questo viene utilizzato alle righe 45 e 49;
24.3. Il server web
24.3.1. Configurazione
La configurazione del server è molto simile a quella del server discusso in precedenza. Solo il file [config.py] è leggermente cambiato:
- righe 40–66: aggiungiamo al dizionario di configurazione del server gli elementi relativi al logger (riga 49) e quelli relativi all'invio di un'e-mail di avviso all'amministratore dell'applicazione (righe 51–63);
- riga 65: per vedere meglio i thread in azione, ne forzeremo alcuni a mettersi in pausa. [sleep_time] è la durata della pausa espressa in secondi;
- righe 27–28: si noti che stiamo utilizzando [index_controller] della precedente versione 6;
24.3.2. Lo script principale [main]
Lo script principale [main] è il seguente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
- righe 1-10: lo script si aspetta un parametro [mysql / pgres] che specifichi il DBMS da utilizzare;
- righe 12–14: l'applicazione viene configurata (Python Path, livelli, database);
- righe 16–28: dipendenze richieste dall'applicazione;
- righe 30-43: gestione dell'autenticazione;
- righe 46–51: una funzione che invia un'e-mail all'amministratore dell'applicazione;
- la funzione richiede due parametri:
- config: un dizionario con le chiavi [adminMail] e [logger];
- il messaggio da inviare;
- righe 49–50: prepariamo la configurazione dell'e-mail;
- invieremo l'e-mail;
- righe 54–74: verifichiamo la presenza del file di log;
- righe 70–74: se non siamo riusciti ad aprire il file di log, inviamo un'e-mail all'amministratore e usciamo;
- righe 76–79: registriamo l'avvio del server;
- righe 81–98: recuperiamo i dati dell'amministrazione fiscale dal database;
- righe 88–98: se non siamo riusciti a recuperare questi dati, registriamo l'errore sia sulla console che nel file di log;
- righe 100–101: il thread principale non registrerà più (i thread creati non useranno lo stesso descrittore di file);
- righe 103–105: se non siamo riusciti a connetterci al database, ci fermiamo;
- riga 122: il server viene avviato in modalità multithread;
La funzione [index] (riga 114) è la seguente:
- Riga 4: la funzione eseguita quando un utente richiede l'URL /. Poiché il server è multithread (riga 112), verrà creato un thread per eseguire la funzione. Questo thread può essere interrotto e messo in pausa in qualsiasi momento per riprendere l'esecuzione poco dopo. Tenetelo sempre presente quando il codice accede a una risorsa condivisa da tutti i thread. In questo caso, quella risorsa è il file di log: tutti i thread vi scrivono;
- riga 8: creiamo un'istanza del logger. Pertanto, tutti i thread avranno un'istanza diversa del logger. Tuttavia, tutti questi logger puntano allo stesso file di log. È comunque importante notare che quando un thread chiude il proprio logger, ciò non ha alcun effetto sui logger degli altri thread;
- Righe 9–12: memorizziamo il logger nel dizionario [config] dell'applicazione sotto una chiave che prende il nome dal thread. Pertanto, se ci sono n thread in esecuzione simultanea, verranno create n voci nel dizionario [config]. [config] è una risorsa condivisa tra tutti i thread. Pertanto, potrebbe essere necessaria la sincronizzazione. Ho fatto un'ipotesi qui. Ho ipotizzato che se due thread creassero simultaneamente le loro voci nel file [config] e uno di essi fosse interrotto dall'altro, ciò non avrebbe alcun impatto. Il thread interrotto potrebbe completare in seguito la creazione della voce. Se i test dimostrassero che questa ipotesi è falsa, l'accesso alla riga 12 dovrebbe essere sincronizzato;
- riga 10: inseriamo il logger in un dizionario;
- riga 11: [threading.current_thread()] è il thread che esegue questa riga e quindi il thread che esegue la funzione [index]. Ne registriamo il nome. Ogni thread ha un nome univoco;
- riga 12: memorizziamo la configurazione del thread. D'ora in poi, procederemo sempre come segue: se ci sono informazioni che non possono essere condivise tra i thread, saranno comunque inserite nella configurazione generale, ma associate al nome del thread;
- riga 14: registriamo la richiesta che stiamo attualmente eseguendo;
- righe 15–24: mettiamo in pausa in modo casuale alcuni thread in modo che cedano il processore a un altro thread;
- riga 16: recuperiamo la durata della pausa (in secondi) dalla configurazione;
- riga 17: si verifica una pausa solo se la durata della pausa non è 0;
- riga 19: un numero intero casuale nell'intervallo [0, 1]. Pertanto, sono possibili solo i valori 0 e 1;
- riga 20: il thread viene messo in pausa solo se il numero casuale è 1;
- riga 22: registriamo il fatto che il thread sta per essere messo in pausa;
- riga 24: il thread viene messo in pausa per [sleep_time] secondi;
- riga 26: quando il thread si riattiva, fa eseguire la richiesta al modulo [index_controller];
- righe 28–32: se questa esecuzione causa un [500 INTERNAL SERVER ERROR], viene inviata un'e-mail all'amministratore;
- righe 30-31: configuriamo il dizionario [config_mail] che passeremo alla classe [SendAdminMail];
- riga 32: il messaggio inviato all'amministratore è la stringa JSON del risultato che verrà inviato al client;
- righe 33–34: registriamo la risposta che verrà inviata al client (riga 36);
- righe 37–44: gestiamo eventuali eccezioni;
- righe 39–40: se il logger esiste, registriamo l'errore verificatosi;
- righe 47–48: chiudiamo il logger se esiste. In definitiva, il thread crea un logger all'inizio della richiesta e lo chiude una volta che la richiesta è stata elaborata;
24.3.3. Il controller [index_controller]
Il controller [index_controller] che esegue le richieste è quello della versione precedente:

24.3.4. Esecuzione
Avviamo il server Flask, il server di posta |hMailServer| e il client di posta |Thunderbird|. Non avviamo il DBMS. Il server si arresta con i seguenti log di console:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/02/flask/main.py mysql
[serveur] démarrage du serveur
L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
Process finished with exit code 2
Il file di log [logs.txt] è il seguente:
2020-07-23 11:51:38.324752, MainThread : [serveur] démarrage du serveur
2020-07-23 11:51:40.355510, MainThread : L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
2020-07-23 11:51:42.464206, MainThread : [SendAdminMail] Message envoyé à [guest@localhost.com] : [L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]]
Utilizzando Thunderbird, controllare le e-mail dell'amministratore [guest@localhost.com]:

Quindi avvia il DBMS e richiedi l'URL [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]. I log diventano come segue:
2020-07-23 11:56:38.891753, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:38.987999, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:40.586747, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:40.655254, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:54.528360, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-23 11:56:54.530653, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
- righe 1-4: si noti che il server si avvia due volte perché la modalità [Debug=True] innesca un secondo avvio;
- righe 5-6: i log ci danno un'idea del tempo di esecuzione di una richiesta, qui 2,293 millisecondi;
24.4. Il client web

La directory [http-clients/02] viene creata copiando la directory [http-clients/01]. Successivamente apportiamo alcune modifiche.
24.4.1. La configurazione
La configurazione [config] dell'applicazione [http-clients/02] è la stessa di quella dell'applicazione [http-clients/01], con alcune piccole differenze:
- righe 31-32: useremo lo stesso logger |Logger| utilizzato per il server;
- riga 49: il percorso assoluto del file di log;
- riga 60: la modalità [debug=True] viene utilizzata per scrivere le risposte del server web nel file di log;
24.4.2. Il livello [dao]
Il codice della classe [ImpôtsDaoWithHttpClient] cambia leggermente:
- Riga 17: memorizziamo la configurazione generale. Vedremo in seguito che quando viene eseguito il costruttore della classe [ImpôtsDaoWithHttpClient], il dizionario [config] non contiene ancora la chiave [logger] utilizzata alla riga 37. Questo è il motivo per cui non possiamo inizializzare [self.__logger] (riga 23) nel costruttore;
- riga 21: abbiamo aggiunto una chiave [debug] alla configurazione che controlla la registrazione nelle righe 33–39;
- riga 34: se siamo in modalità [debug];
- righe 36–37: inizializzazione opzionale della proprietà [self.__logger]. Quando viene utilizzato il metodo [calculate_tax], la chiave [logger] fa parte del dizionario [config];
- riga 39: registriamo il documento di testo associato alla risposta HTTP del server;
Il livello [dao] verrà eseguito simultaneamente da più thread. Tuttavia, qui creiamo una singola istanza di questo livello (vedi config_layers). Dobbiamo quindi verificare che il codice non comporti l'accesso in scrittura a dati condivisi, tipicamente le proprietà della classe [ImpôtsDaoWithHttpClient] che implementa il livello [dao]. Tuttavia, nel codice sopra riportato, la riga 37 modifica una proprietà dell'istanza della classe. In questo caso, ciò non ha conseguenze perché tutti i thread condividono lo stesso logger. Se così non fosse stato, l'accesso alla riga 37 avrebbe dovuto essere sincronizzato.
24.4.3. Lo script principale
Lo script principale [main] si sviluppa come segue:
- Lo script principale differisce da quello del client precedente in quanto genererà più thread di esecuzione per inviare richieste al server. Il client nella versione 6 inviava tutte le sue richieste in modo sequenziale. La richiesta #i veniva effettuata solo dopo aver ricevuto la risposta alla richiesta #[i-1]. Qui, vogliamo vedere come si comporta il server quando riceve più richieste simultanee. Per questo, abbiamo bisogno di thread;
- riga 21: i thread generati saranno inseriti in una lista. È importante comprendere che anche lo script [main] viene eseguito da un thread chiamato [MainThread]. Questo thread principale creerà altri thread che saranno responsabili del calcolo dell'imposta per uno o più contribuenti;
- riga 26: creiamo un logger. Questo sarà condiviso da tutti i thread;
- riga 32: recuperiamo tutti i contribuenti per i quali è necessario calcolare le imposte;
- righe 39–51: distribuiamo questi contribuenti su diversi thread;
- righe 40–41: ogni thread elaborerà da 1 a 4 contribuenti. Questo numero viene determinato in modo casuale;
- [random.randint(1, 4)] genera in modo casuale un numero dall'elenco [1, 2, 3, 4];
- il thread non può contenere più di [l-i] contribuenti, dove [l-i] rappresenta il numero di contribuenti a cui non è stato ancora assegnato un thread;
- prendiamo quindi il minimo dei due valori;
- riga 43: una volta noto [nb_taxpayers], il numero di contribuenti elaborati dal thread, li prendiamo dall'elenco dei contribuenti:
- [slice(10,12)] è l'insieme degli indici [10, 11, 12];
- [contribuenti[slice(10,12)]] è l'elenco [contribuenti[10], contribuenti[11], contribuenti[12] ;
- riga 45: incrementiamo il valore di i, che controlla il ciclo alla riga 39;
- riga 47: creiamo un thread:
- [target=thread_function] imposta la funzione che il thread eseguirà. Si tratta della funzione delle righe 16–17. Essa richiede tre parametri;
- [ags] è l'elenco dei tre parametri previsti dalla funzione [thread_function];
La creazione di un thread non ne comporta l'esecuzione. Si crea semplicemente un oggetto;
- Righe 48–49: il thread appena creato viene aggiunto all'elenco dei thread creati dal thread principale;
- riga 51: il thread viene avviato. Verrà quindi eseguito in parallelo con gli altri thread attivi. Qui, eseguirà [thread_function] con gli argomenti forniti;
- righe 53–54: il thread principale attende il completamento di ciascuno dei thread che ha lanciato. Facciamo un esempio:
- il thread principale ha lanciato tre thread [th1, th2, th3];
- Il thread principale attende ciascuno dei thread (righe 53–54) nell'ordine del ciclo for: [th1, th2, th3];
- Supponiamo che i thread terminino nell'ordine [th2, th1, th3];
- Il thread principale attende che th1 finisca. Quando th2 finisce, non succede nulla;
- Quando th1 termina, il thread principale attende th2. Tuttavia, th2 ha già terminato. Il thread principale passa quindi al thread successivo e attende th3;
- quando th3 termina, il thread principale ha terminato l'attesa e procede all'esecuzione della riga 57;
- la riga 57 scrive i risultati nel file dei risultati. Questo è un buon esempio di riferimenti agli oggetti:
- riga 43: la lista [thread_payers] associata a un thread contiene copie dei riferimenti agli oggetti contenuti nella lista [taxpayers];
- sappiamo che il calcolo delle imposte modificherà gli oggetti a cui puntano i riferimenti nella lista [thread_payers]. Questi oggetti saranno aggiornati con i risultati del calcolo delle imposte. Tuttavia, i riferimenti stessi non vengono modificati. Pertanto, i riferimenti nella lista iniziale [taxpayers] “vedono” o “puntano” agli oggetti modificati;
La [thread_function] eseguita dai thread è la seguente:
- Le funzioni eseguite simultaneamente da più thread sono spesso difficili da scrivere: è necessario verificare sempre che il codice non tenti di modificare i dati condivisi tra i thread. Quando ciò si verifica, è necessario implementare un accesso sincronizzato ai dati condivisi che stanno per essere modificati;
- Riga 3: La funzione riceve tre parametri:
- [dao]: un riferimento al livello [dao]. Questi dati sono condivisi;
- [logger]: un riferimento al logger. Questi dati sono condivisi;
- [taxpayers]: un elenco di contribuenti. Questi dati non sono condivisi: ogni thread gestisce un elenco diverso;
- Esaminiamo i due riferimenti [dao, logger]:
- abbiamo visto che l'oggetto puntato dal riferimento [dao] aveva un riferimento [self.__logger] che veniva modificato dai thread, ma per impostare un valore comune a tutti i thread;
- il riferimento [logger] punta a un descrittore di file. Abbiamo visto che potrebbe esserci un problema durante la scrittura dei log nel file. Per questo motivo, la scrittura nel file è stata sincronizzata;
- righe 5–6: registriamo il nome del thread e il numero di contribuenti che deve gestire;
- righe 8–14: calcolo delle imposte dei contribuenti;
- riga 16: registriamo la fine del thread;
24.4.4. Esecuzione
Avviamo il server web come descritto nella sezione precedente (server web, DBMS, hMailServer, Thunderbird), quindi eseguiamo lo script [main] del client. Nei file [data/output/errors.txt, data/output/results.json], otteniamo gli stessi risultati della versione precedente. Nel file [data/logs/logs.txt], abbiamo i seguenti log:
2020-07-24 10:05:20.942404, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-24 10:05:20.943458, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-24 10:05:20.943458, Thread-2 : début du thread [Thread-2] avec 3 contribuable(s)
2020-07-24 10:05:20.946502, Thread-3 : début du thread [Thread-3] avec 1 contribuable(s)
2020-07-24 10:05:20.946502, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-24 10:05:20.947003, Thread-3 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.947003, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-24 10:05:20.950324, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.948449, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-24 10:05:20.953645, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-24 10:05:20.976143, Thread-1 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:20.976695, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-24 10:05:20.976695, Thread-1 : fin du thread [Thread-1]
2020-07-24 10:05:21.973914, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
2020-07-24 10:05:21.973914, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-24 10:05:21.973914, Thread-2 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-24 10:05:21.977130, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:21.977130, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.977130, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-24 10:05:21.982634, Thread-3 : {"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:21.982634, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:21.983134, Thread-3 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.983134, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.983134, Thread-3 : fin du thread [Thread-3]
2020-07-24 10:05:21.983763, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-24 10:05:22.008562, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:22.008562, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-24 10:05:22.009062, Thread-5 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-24 10:05:22.016848, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:22.017349, Thread-5 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:22.017349, Thread-5 : fin du thread [Thread-5]
2020-07-24 10:05:23.008486, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
2020-07-24 10:05:23.008486, Thread-2 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-24 10:05:23.009749, Thread-2 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-24 10:05:23.011722, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:23.013723, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.013723, Thread-4 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-24 10:05:23.024135, Thread-2 : {"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:23.024135, Thread-2 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.025178, Thread-2 : fin du thread [Thread-2]
2020-07-24 10:05:23.025178, Thread-4 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:23.026191, Thread-4 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.026191, Thread-4 : fin du thread [Thread-4]
- Questi log mostrano che sono stati avviati cinque thread per calcolare le imposte di 11 contribuenti. Questi cinque thread hanno inviato richieste simultanee al server di calcolo delle imposte. È importante capire come funziona:
- Il thread [Thread-1] viene avviato per primo. Quando ha la CPU, esegue il codice fino a quando non invia la sua richiesta HTTP. Dato che deve attendere il risultato di questa richiesta, viene automaticamente messo in attesa. Quindi perde la CPU e un altro thread la prende in carico;
- righe 1–10: lo stesso processo si ripete per ciascuno dei 5 thread. Pertanto, i 5 thread vengono avviati prima ancora che il thread [Thread-1] abbia ricevuto la sua risposta alla riga 11;
- I thread non terminano nell'ordine in cui sono stati avviati. Pertanto, il thread [Thread-3] termina per primo, riga 23;
Sul lato server, i log nel file [data/logs/logs.txt] sono i seguenti:
2020-07-24 10:05:01.692980, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:01.877251, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:03.596162, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:03.661160, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:20.968053, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=50000' [GET]>
2020-07-24 10:05:20.969132, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.970316, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=100000' [GET]>
2020-07-24 10:05:20.970316, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.971335, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
2020-07-24 10:05:20.972563, Thread-4 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:20.974796, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=3&salaire=100000' [GET]>
2020-07-24 10:05:20.974796, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.976143, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=30000' [GET]>
2020-07-24 10:05:20.976143, Thread-6 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:21.970615, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}}}
2020-07-24 10:05:21.973914, Thread-3 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:21.977130, Thread-6 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:21.977130, Thread-5 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:22.001693, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-24 10:05:22.003013, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.003013, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=5&salaire=100000' [GET]>
2020-07-24 10:05:22.003013, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.005871, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=200000' [GET]>
2020-07-24 10:05:22.006370, Thread-9 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:22.014170, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-24 10:05:22.014170, Thread-10 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.003533, Thread-7 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}}}
2020-07-24 10:05:23.006434, Thread-8 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.018026, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=2&salaire=100000' [GET]>
2020-07-24 10:05:23.019074, Thread-11 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.021447, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=100000' [GET]>
2020-07-24 10:05:23.022447, Thread-12 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
- Possiamo vedere che 11 thread hanno elaborato gli 11 contribuenti;
- alcuni thread sono stati messi in attesa (righe 6, 8, 12, 14, 20, 22) e altri no (righe 9, 23, 25, 29, 31);
24.5. Test del livello [DAO]
Come abbiamo fatto nella |versione precedente|, stiamo testando il livello [DAO] del client. Il principio è esattamente lo stesso:

La classe di test verrà eseguita nel seguente ambiente:

- La configurazione [2] è identica alla configurazione [1], che abbiamo appena esaminato;
La classe di test [TestHttpClientDao] è la seguente:
- Creiamo una |configurazione di esecuzione| per questo test;
- Avviamo il server web con il suo intero ambiente;
- eseguiamo il test;
I risultati sono i seguenti:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/02/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 6.128s
OK
Process finished with exit code 0