Skip to content

24. Anwendungsübung: Version 7

24.1. Einleitung

Version 7 der Steuerberechnungsanwendung entspricht bis auf die folgenden Details der Version 6:

  • Der Webclient wird mehrere Anfragen HTTP gleichzeitig starten. In der vorherigen Version wurden diese Anfragen nacheinander gestartet. Der Server bearbeitete zu jedem Zeitpunkt nur eine einzige Anfrage;
  • Der Server wird multithreaded sein: Er kann mehrere Anfragen gleichzeitig bearbeiten;
  • Um die Ausführung dieser Anfragen zu verfolgen, wird der Webserver mit einem Logger ausgestattet, mit dem wichtige Zeitpunkte der Anfrageverarbeitung in einer Textdatei protokolliert werden;
  • Der Server sendet eine E-Mail an den Administrator der Anwendung, wenn er auf ein Problem stößt, das den Start verhindert, typischerweise ein Problem mit der dem Webserver zugeordneten Datenbank;

Die Architektur der Anwendung bleibt unverändert:

Die Verzeichnisstruktur der Skripte sieht wie folgt aus:

Image

Der Ordner „[http-servers/02]“ wird zunächst durch Kopieren des Ordners „[http-servers/01]“ erstellt. Anschließend werden darin Änderungen vorgenommen.

24.2. Die Dienstprogramme

Image

24.2.1. Die Klasse [Logger]

Die Klasse [Logger] ermöglicht es, bestimmte Aktionen des Webservers in einer Textdatei zu protokollieren:


import codecs
import threading
from datetime import date, datetime
from threading import current_thread

from ImpôtsError import ImpôtsError


class Logger:
    # Klassenattribut
    verrou = threading.RLock()

    # Konstruktor
    def __init__(self, logs_filename: str):
        try:
            # Die Datei wird im Anfüge-Modus (a) geöffnet
            self.__resource = codecs.open(logs_filename, "a", "utf-8")
        except BaseException as erreur:
            raise ImpôtsError(18, f"{erreur}")

    # Schreiben eines Protokolls
    def write(self, message: str):
        # Datum/Uhrzeit des aktuellen Zeitpunkts
        today = date.today()
        now = datetime.time(datetime.now())
        # Name des Threads
        thread_name = current_thread().name
        # Man möchte während des Schreibvorgangs in die Protokolldatei nicht gestört werden
        # Das Synchronisationsobjekt (= die Sperre) der Klasse wird angefordert – nur ein Thread erhält es
        Logger.verrou.acquire()
        try:
            # Schreiben des Logs
            self.__resource.write(f"{today} {now}, {thread_name} : {message}")
            # Sofortiges Schreiben – andernfalls wird der Text erst beim Schließen des Schreibstroms geschrieben
            # oder man möchte die Protokolle im Zeitverlauf verfolgen
            self.__resource.flush()
        finally:
            # Das Synchronisationsobjekt (= die Sperre) wird freigegeben, damit ein anderer Thread es erhalten kann
            Logger.verrou.release()

    # Freigabe der Ressourcen
    def close(self):
        # Schließen der Datei
        if self.__resource:
            self.__resource.close()
  • Zeilen 10–11: Hier wird ein Klassenattribut definiert. Ein Klassenattribut ist eine Eigenschaft, die von allen Instanzen der Klasse gemeinsam genutzt wird. Es wird mit der Notation [Classe.attribut_de_classe] referenziert (Zeilen 30, 39). Das Klassenattribut [verrou] dient als Synchronisationsobjekt für alle Threads, die den Code der Zeilen 31–36 ausführen;
  • Zeilen 14–19: Der Konstruktor erhält den absoluten Pfadnamen der Protokolldatei. Diese Datei wird daraufhin geöffnet und der abgerufene Dateideskriptor in der Klasse gespeichert;
  • Zeile 17: Die Protokolldatei wird im Modus „Append“ (a) geöffnet. Jede geschriebene Zeile wird an das Ende der Datei angehängt;
  • Zeilen 22–39: Die Methode [write] ermöglicht es, eine als Parameter übergebene Meldung in die Protokolldatei zu schreiben. An diese werden zwei Informationen angehängt:
    • Zeile 24: das aktuelle Datum;
    • Zeile 25: die aktuelle Uhrzeit;
    • Zeile 27: der Name des Threads, der das Protokoll schreibt. Dabei darf nicht vergessen werden, dass eine Webanwendung mehrere Benutzer gleichzeitig bedient. Jeder Anfrage wird ein Thread zugewiesen, um sie auszuführen. Wird dieser Thread angehalten – typischerweise für einen Ein-/Ausgabevorgang (Netzwerk, Dateien, Datenbank) –, wird der Prozessor einem anderen Thread zugewiesen. Aufgrund dieser möglichen Unterbrechungen kann man nicht sicher sein, dass es einem Thread gelingt, eine Zeile in die Protokolldatei zu schreiben, ohne unterbrochen zu werden. Es besteht daher die Gefahr, dass sich Protokolle von zwei verschiedenen Threads vermischen. Das Risiko ist gering, vielleicht sogar gleich null, aber wir haben uns dennoch entschlossen, zu zeigen, wie man den Zugriff zweier Threads auf eine gemeinsame Ressource – in diesem Fall die Protokolldatei – synchronisiert;
  • Zeile 30: Vor dem Schreiben fordert der Thread den Schlüssel für die Eingangstür an. Der angeforderte Schlüssel ist der in Zeile 11 erstellte. Er ist tatsächlich eindeutig: Ein Klassenattribut ist für alle Instanzen der Klasse eindeutig;
    • Zum Zeitpunkt T1 erhält der Thread „Thread1“ den Schlüssel. Er kann nun Zeile 33 ausführen;
    • zum Zeitpunkt T2 wird der Thread „Thread1“ angehalten, noch bevor er das Schreiben des Protokolls abgeschlossen hat;
    • Zum Zeitpunkt T3 muss auch der Thread „Thread2“, der die Prozessorausführung erhalten hat, ein Protokoll schreiben. Er gelangt dabei zu Zeile 30, wo er den Schlüssel für die Eingangstür anfordert. Es wird ihm mitgeteilt, dass ein anderer Thread diesen bereits besitzt. Daraufhin wird er automatisch angehalten. Dies gilt für alle Threads, die diesen Schlüssel anfordern;
    • Zum Zeitpunkt T4 erhält der Thread Thread1, der zuvor angehalten worden war, wieder die Prozessorsteuerung. Er beendet daraufhin das Schreiben des Protokolls;
  • Zeilen 32–36: Das Schreiben in die Protokolldatei erfolgt in zwei Schritten:
  • Zeile 33: Der in Zeile 17 erhaltene Dateideskriptor arbeitet mit einem Puffer. Die Operation [write] in Zeile 33 schreibt in diesen Puffer, jedoch nicht direkt in die Datei. Der Puffer wird anschließend unter bestimmten Bedingungen in die Datei entleert:
        • der Puffer ist voll;
        • der Dateideskriptor ist Gegenstand einer Operation [close] oder [flush];
  • Zeile 36: Das Schreiben der Protokollzeile in die Datei wird erzwungen. Dies geschieht, weil wir sehen wollen, wie sich die Protokolle der verschiedenen Threads untereinander einfügen. Würden wir dies nicht tun, würden die Protokolle eines Threads alle gleichzeitig beim Schließen des Dateideskriptors (Zeile 45) geschrieben. Es wäre dann viel schwieriger zu erkennen, dass bestimmte Threads beendet wurden: Man müsste die Zeitangaben in den Protokollen überprüfen;
  • Zeile 39: Der Thread „Thread1“ gibt den ihm zugewiesenen Schlüssel zurück. Dieser kann nun einem anderen Thread zugewiesen werden;
  • Zeile 22: Die Methode [write] ist somit synchronisiert: Es schreibt jeweils nur ein Thread in die Logdatei. Der Schlüssel des Mechanismus ist Zeile 30: Was auch immer geschieht, nur ein Thread erhält den Schlüssel für den Übergang zur nächsten Zeile. Er behält ihn, bis er ihn zurückgibt (Zeile 39);
  • Zeilen 41–45: Die Methode [close] ermöglicht es, die dem Deskriptor der Protokolldatei zugewiesenen Ressourcen freizugeben;

Die in die Logdatei geschriebenen Logs sehen wie folgt aus:

2020-07-22 20:03:52.992152, Thread-2 : …

24.2.2. Die Klasse [SendAdminMail]

Die Klasse [SendAminMail] ermöglicht es, eine Nachricht an den Administrator der Anwendung zu senden, wenn diese „abstürzt“.

Image

Die Klasse [SendAdminMail] wird im Skript [config] [2] wie folgt konfiguriert:


        # Serverkonfiguration SMTP
        "adminMail": {
            # Server SMTP
            "smtp-server": "localhost",
            # Server-Port SMTP
            "smtp-port": "25",
            # Administrator
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            # Betreff der E-Mail
            "subject": "plantage du serveur de calcul d'impôts",
            # tls auf „True“, wenn der Server SMTP eine Autorisierung erfordert, andernfalls auf „False“
            "tls": False
        }

Die Klasse [SendAdminMail] erhält das Wörterbuch aus den Zeilen 2–13 sowie die Konfiguration für den E-Mail-Versand. Die Klasse sieht wie folgt aus:


# Importe
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate


class SendAdminMail:

    # -----------------------------------------------------------------------
    @staticmethod
    def send(config: dict, message: str, verbose: bool = False):
        # sendet eine Nachricht an den SMTP-Server config['smtp-server'] auf dem Port config[smtp-port]
        # Wenn config['tls'] wahr ist, wird das Format TLS verwendet
        # Die E-Mail wird im Namen von config['from'] versendet
        # an den Empfänger config['to']
        # Die Nachricht hat den Betreff „config['subject']“
        # In der Konfiguration findet man die Referenz eines Loggers: config['logger']

        # Der Logger wird aus der Konfiguration abgerufen – kann auch „None“ lauten
        logger = config["logger"]
        # Server SMTP
        server = None
        # Die Nachricht wird gesendet
        try:
            # der Server SMTP
            server = smtplib.SMTP(config["smtp-server"])
            # Ausführlicher Modus
            server.set_debuglevel(verbose)
            # Sichere Verbindung?
            if config['tls']:
                # Start des Sicherheitsdialogs
                server.starttls()
                # Authentifizierung
                server.login(config["user"], config["password"])
            # Erstellung einer Multipart-Nachricht – diese Nachricht wird gesendet
            msg = MIMEText(message)
            msg['From'] = config["from"]
            msg['To'] = config["to"]
            msg['Date'] = formatdate(localtime=True)
            msg['Subject'] = config["subject"]
            # Die Nachricht wird gesendet
            server.send_message(msg)
            # Protokoll – der Logger ist möglicherweise nicht vorhanden
            if logger:
                logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
        except BaseException as erreur:
            # Protokoll – der Logger existiert möglicherweise nicht
            if logger:
                logger.write(
                    f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
        finally:
            # Fertig – die von der Funktion belegten Ressourcen werden freigegeben
            if server:
                server.quit()
  • Zeilen 24–54: Hier findet sich der bereits im Beispiel |smtp/02| behandelte Code;
  • Zeile 20: Hier wird die Referenz eines Loggers abgerufen. Diese wird in den Zeilen 45 und 49 verwendet;

24.3. Der Webserver

Image

24.3.1. Konfiguration

Image

Die Konfiguration des Servers ist der des zuvor untersuchten Servers sehr ähnlich. Lediglich die Datei [config.py] weicht geringfügig ab:


def configure(config: dict) -> dict:
    import os

    # Schritt 1 ------

    # Ordner dieser Datei
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # Stammverzeichnis
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # Absolute Abhängigkeiten
    absolute_dependencies = [
        # Projektordner
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # Konstanten, Tranchen
        f"{root_dir}/impots/v05/entities",
        # IndexController
        f"{root_dir}/impots/http-servers/01/controllers",
        # Skripte [config_database, config_layers]
        script_dir,
        # Logger, SendAdminMail
        f"{script_dir}/../utilities",
    ]
    # Der Syspath wird festgelegt
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Schritt 2 ------
    # Konfiguration der Anwendung
    config.update({
        # Benutzer, die zur Nutzung der Anwendung berechtigt sind
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],
        # Protokolldatei
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # Serverkonfiguration SMTP
        "adminMail": {
            # Server SMTP
            "smtp-server""localhost",
            # Port des Servers SMTP
            "smtp-port""25",
            # Administrator
            "from""guest@localhost.com",
            "to""guest@localhost.com",
            # Betreff der E-Mail
            "subject""plantage du serveur de calcul d'impôts",
            # TLS auf „True“, wenn der Server SMTP eine Autorisierung erfordert, andernfalls auf „False“
            "tls"False
        },
        # Pausenzeit des Threads in Sekunden
        "sleep_time"0
    })

    # Schritt 3 ------
    # Datenbankkonfiguration
    import config_database
    config["database"] = config_database.configure(config)

    # Schritt 4 ------
    # Instanziierung der Anwendungsschichten
    import config_layers
    config['layers'] = config_layers.configure(config)

    # Die Konfiguration wird zurückgegeben
    return config
  • Zeilen 40–66: Im Konfigurationswörterbuch des Servers werden die Elemente zum Logger (Zeile 49) sowie diejenigen zum Versand einer Warn-E-Mail an den Anwendungsadministrator (Zeilen 51–63) hinzugefügt;
  • Zeile 65: Um die Threads besser in Aktion beobachten zu können, werden einige davon zwangsweise angehalten. [sleep_time] ist die Dauer der Unterbrechung in Sekunden;
  • Zeilen 27–28: Beachten Sie, dass wir den Controller [index_controller] aus der vorherigen Version 6 verwenden;

24.3.2. Das Hauptskript [main]

Das Hauptskript [main] lautet wie folgt:


# Es wird ein MySQL- oder PostgreSQL-Parameter erwartet
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# Die Anwendung wird konfiguriert
import config
config = config.configure({'sgbd': sgbd})

# Abhängigkeiten
from flask import request, Flask
from flask_httpauth import HTTPBasicAuth
import json
import index_controller
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError

# Authentifizierungsmanager
auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(login, password):
    # Benutzerliste
    users = config['users']
    # Diese Liste wird durchlaufen
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    # Es wurde nichts gefunden
    return False


# E-Mail an den Administrator senden
def send_adminmail(config: dict, message: str):
    # Es wird eine E-Mail an den Administrator der Anwendung gesendet
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)


# Überprüfung der Protokolldatei
logger = None
erreur = False
message_erreur = None
try:
    # Protokollierung
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    # Konsolenprotokoll
    print(f"L'erreur suivante s'est produite : {exception}")
    # Der Fehler wird notiert
    erreur = True
    message_erreur = f"{exception}"
# Logger in der Konfiguration speichern
config['logger'] = logger
# Fehlerbehandlung
if erreur:
    # E-Mail an den Administrator
    send_adminmail(config, message_erreur)
    # Beenden der Anwendung
    sys.exit(1)

# Startprotokoll
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

# Abruf der Daten von der Steuerbehörde
erreur = False
try:
    # „admindata“ wird zu einer schreibgeschützten, anwendungsweiten Variable
    config["admindata"] = config["layers"]["dao"].get_admindata()
    # Erfolgsprotokoll
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # Fehler wird protokolliert
    erreur = True
    # Fehlerprotokoll
    log = f"L'erreur suivante s'est produite : {ex}"
    # Konsole
    print(log)
    # Protokolldatei
    logger.write(f"{log}\n")
    # E-Mail an den Administrator
    send_adminmail(config, log)

# Der Hauptthread benötigt den Logger nicht mehr
logger.close()

# Bei einem Fehler wird das Programm beendet
if erreur:
    sys.exit(2)

# Die Flask-Anwendung kann gestartet werden
app = Flask(__name__)


# Startseite URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():



# nur main
if __name__ == '__main__':
    # Der Server wird gestartet
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • Zeilen 1–10: Das Skript erwartet einen Parameter [mysql / pgres], der ihm angibt, welches SGBD verwendet werden soll;
  • Zeilen 12–14: Die Anwendung wird konfiguriert (Python-Pfad, Schichten, Datenbank);
  • Zeilen 16–28: Die für die Anwendung erforderlichen Abhängigkeiten;
  • Zeilen 30–43: Verwaltung der Authentifizierung;
  • Zeilen 46–51: Eine Funktion, die eine E-Mail an den Administrator der Anwendung sendet;
  • Die Funktion erwartet zwei Parameter:
      • config: ein Wörterbuch mit den Schlüsseln [adminMail] und [logger];
      • die zu versendende Nachricht;
    • Zeilen 49–50: Vorbereitung der Versandkonfiguration;
    • die E-Mail wird versendet;
  • Zeilen 54–74: Es wird überprüft, ob die Protokolldatei vorhanden ist;
  • Zeilen 70–74: Wenn die Logdatei nicht geöffnet werden konnte, wird eine E-Mail an den Administrator gesendet und der Vorgang abgebrochen;
  • Zeilen 76–79: Der Start des Servers wird protokolliert;
  • Zeilen 81–98: Die Daten der Steuerbehörde werden aus der Datenbank abgerufen;
  • Zeilen 88–98: Wenn es nicht gelungen ist, diese Daten abzurufen, wird der Fehler sowohl auf der Konsole als auch in der Protokolldatei protokolliert;
  • Zeilen 100–101: Der Haupt-Thread führt keine Protokollierung mehr durch (die erstellten Threads verwenden nicht denselben Dateideskriptor);
  • Zeilen 103–105: Wenn keine Verbindung zur Datenbank hergestellt werden konnte, wird der Vorgang abgebrochen;
  • Zeile 122: Der Server wird im Multithread-Modus gestartet;

Die Funktion [index] (Zeile 114) lautet wie folgt:


# Startseite URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    logger = None
    try:
        # Protokollieren
        logger = Logger(config["logsFilename"])
        # wird in einer dem Thread zugeordneten Konfiguration gespeichert
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # Die Anfrage wird protokolliert
        logger.write(f"[index] requête : {request}\n")
        # Der Thread wird unterbrochen, falls dies angefordert wurde
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # Die Pause erfolgt zufällig, damit einige Threads unterbrochen werden und andere nicht
            aléa = randint(0, 1)
            if aléa == 1:
                # Protokollierung vor der Pause
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # Pause
                time.sleep(sleep_time)
        # Die Anfrage wird von einem Controller ausgeführt
        résultat, status_code = index_controller.execute(request, config)
        # Ist ein schwerwiegender Fehler aufgetreten?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # Es wird eine E-Mail an den Administrator der Anwendung gesendet
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        # Die Antwort wird protokolliert
        logger.write(f"[index] {résultat}\n")
        # Die Antwort wird gesendet
        return json_response(résultat, status_code)
    except BaseException as erreur:
        # Der Fehler wird protokolliert, sofern dies möglich ist
        if logger:
            logger.write(f"[index] {erreur}")
        # Die Antwort an den Kunden wird vorbereitet
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        # die Antwort wird gesendet
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # Die Protokolldatei wird geschlossen, falls sie geöffnet war
        if logger:
            logger.close()
  • Zeile 4: Die Funktion, die ausgeführt wird, wenn ein Benutzer die Funktion URL aufruft. Da der Server multithreaded ist (Zeile 112), wird ein Thread erstellt, um die Funktion auszuführen. Dieser Thread kann jederzeit unterbrochen und angehalten werden, um die Ausführung etwas später fortzusetzen. Diesen Punkt muss man immer im Hinterkopf behalten, wenn der Code auf eine Ressource zugreift, die von allen Threads gemeinsam genutzt wird. Eine solche Ressource ist hier die Protokolldatei: Alle Threads schreiben in sie hinein;
  • Zeile 8: Es wird eine Instanz des Loggers erstellt. Somit verfügen alle Threads über eine eigene Instanz des Loggers. Dennoch verweisen alle diese Logger auf dieselbe Logdatei. Es ist jedoch wichtig zu beachten, dass das Schließen des Loggers durch einen Thread keine Auswirkungen auf die Logger der anderen Threads hat;
  • Zeilen 9–12: Der Logger wird im Wörterbuch [config] der Anwendung gespeichert, zugeordnet einem Schlüssel mit dem Namen des Threads. Wenn also n Threads gleichzeitig ausgeführt werden, werden n Einträge im Wörterbuch [config] angelegt. [config] ist eine von allen Threads gemeinsam genutzte Ressource. Daher kann eine Synchronisation erforderlich sein. Ich bin hier von einer Annahme ausgegangen. Ich bin davon ausgegangen, dass es keine Auswirkungen hätte, wenn zwei Threads gleichzeitig ihren Eintrag in der Datei „[config]“ erstellen würden und einer von ihnen durch den anderen unterbrochen würde. Der unterbrochene Thread könnte die Erstellung des Eintrags später abschließen. Sollte sich im Test herausstellen, dass diese Annahme falsch ist, müsste der Zugriff auf Zeile 12 synchronisiert werden;
  • Zeile 10: Der Logger wird in ein Wörterbuch aufgenommen;
  • Zeile 11: [threading.current_thread()] ist der Thread, der diese Zeile ausführt, also der Thread, der die Funktion [index] ausführt. Sein Name wird notiert. Jeder Thread hat einen eindeutigen Namen;
  • Zeile 12: Die Konfiguration des Threads wird gespeichert. Von nun an werden wir immer so vorgehen: Wenn es Informationen gibt, die nicht zwischen den Threads geteilt werden können, werden sie dennoch in die allgemeine Konfiguration aufgenommen, jedoch dem Namen des Threads zugeordnet;
  • Zeile 14: Die gerade ausgeführte Anfrage wird protokolliert;
  • Zeilen 15–24: Wir setzen bestimmte Threads nach dem Zufallsprinzip in den Pausenmodus, damit sie den Prozessor einem anderen Thread überlassen;
    • Zeile 16: Die Dauer der Pause (in Sekunden) wird aus der Konfiguration abgerufen;
    • Zeile 17: Es erfolgt nur dann eine Pause, wenn die Pausendauer ungleich 0 ist;
    • Zeile 19: Eine Zufallszahl im Intervall [0, 1]. Es sind also nur die Werte 0 und 1 möglich;
    • Zeile 20: Der Thread wird nur angehalten, wenn die Zufallszahl 1 ist;
    • Zeile 22: Es wird protokolliert, dass der Thread unterbrochen wird;
    • Zeile 24: Der Thread wird für [sleep_time] Sekunden unterbrochen;
  • Zeile 26: Wenn der Thread wieder aktiv wird, lässt er die Abfrage durch das Modul [index_controller] ausführen;
  • Zeilen 28–32: Wenn diese Ausführung einen Fehler vom Typ [500 INTERNAL SERVER ERROR] verursacht, wird eine E-Mail an den Administrator gesendet;
    • Zeilen 30–31: Das Wörterbuch [config_mail] wird konfiguriert und anschließend an die Klasse [SendAdminMail] übergeben;
    • Zeile 32: Die an den Administrator gesendete Nachricht ist die Zeichenkette jSON aus dem Ergebnis, das an den Kunden gesendet wird;
  • Zeilen 33–34: Die Antwort, die an den Kunden gesendet wird (Zeile 36), wird protokolliert;
  • Zeilen 37–44: Behandlung einer möglichen Ausnahme;
  • Zeilen 39–40: Falls der Logger vorhanden ist, wird der aufgetretene Fehler protokolliert;
  • Zeilen 47–48: Der Logger wird geschlossen, sofern er vorhanden ist. Letztendlich erstellt der Thread zu Beginn der Anfrage einen Logger und schließt ihn, sobald diese bearbeitet wurde;

24.3.3. Der Controller [index_controller]

Der Controller [index_controller], der die Anfragen ausführt, ist der aus der vorherigen Version:

Image

24.3.4. Ausführung

Wir starten den Flask-Server, den Mailserver |hMailServer| sowie das E-Mail-Programm |Thunderbird|. SGBD wird nicht gestartet. Der Server wird mit den folgenden Konsolenprotokollen beendet:


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 

Die Protokolldatei [logs.txt] sieht wie folgt aus:


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)]]

Mit Thunderbird werden die E-Mails des Administrators [guest@localhost.com] überprüft:

Image

Anschließend starten wir SGBD und fordern URL sowie [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000] an. Die Protokolle sehen nun wie folgt aus:


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/?verheiratet=ja&Kinder=3&Gehalt=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}}}
  • Zeilen 1–4: Zur Erinnerung: Es gibt zwei Serverstarts, da der Modus [Debug=True] einen zweiten Start auslöst;
  • Zeilen 5–6: Die Protokolle geben Aufschluss über die Ausführungszeit einer Anfrage, hier 2,293 Millisekunden;

24.4. Der Webclient

Image

Image

Die Datei [http-clients/02] wird durch Kopieren der Datei [http-clients/01] erstellt. Anschließend werden einige Änderungen vorgenommen.

24.4.1. Die Konfiguration

Die Konfiguration [config] der Anwendung [http-clients/02] entspricht bis auf wenige Details der Konfiguration der Anwendung [http-clients/01]:


def configure(config: dict) -> dict:
    import os

    # Schritt 1 ------

    # Ordner dieser Datei
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # Stammverzeichnis
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # Absolute Abhängigkeiten
    absolute_dependencies = [
        # Projektordner
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # Konstanten, Tranchen
        f"{root_dir}/impots/v05/entities",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # Konfigurationsskripte
        script_dir,
        # Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    # Syspath festlegen
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Schritt 2 ------
    # Konfiguration der Anwendung mit Konstanten
    config.update({
        # Steuerzahlerdatei
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # Ergebnisdatei
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # Fehlerdatei
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # Protokolldatei
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # Steuerberechnungsserver
        "server": {
            "urlServer""http://127.0.0.1:5000/",
            "authBasic"True,
            "user": {
                "login""admin",
                "password""admin"
            }
        },
        # Debug-Modus
        "debug"True
    }
    )

    # Schritt 3 ------
    # Instanziierung der Schichten
    import config_layers
    config['layers'] = config_layers.configure(config)

    # Konfiguration wird zurückgesetzt
    return config
  • Zeilen 31–32: Wir verwenden denselben Logger |Logger| wie für den Server;
  • Zeile 49: Der absolute Pfad zur Logdatei;
  • Zeile 60: Der Modus [debug=True] dient dazu, die Antworten des Webservers in die Logdatei zu schreiben;

24.4.2. Die Schicht [dao]

Der Code der Klasse [ImpôtsDaoWithHttpClient] ändert sich geringfügig:


# Importe

import requests
from flask_api import status




class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):

    # Konstruktor
    def __init__(self, config: dict):
        # Initialisierung des übergeordneten Objekts
        AbstractImpôtsDao.__init__(self, config)
        # Speicherung der Konfigurationselemente
        # allgemeine Konfiguration
        self.__config = config
        # Server
        self.__config_server = config["server"]
        # Debug-Modus
        self.__debug = config["debug"]
        # Logger
        self.__logger = None

    # nicht verwendete Methode
    def get_admindata(self) -> AdminData:
        pass

    # Steuerberechnung
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        # Ausnahmen werden weitergeleitet
        
        # Debug-Modus?
        if self.__debug:
            # Logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            # Protokollierung
            self.__logger.write(f"{response.text}\n")
        # Antwort-Statuscode HTTP
        status_code = response.status_code
        
  • Zeile 17: Die allgemeine Konfiguration wird gespeichert. Wir werden später sehen, dass beim Aufruf des Konstruktors der Klasse [ImpôtsDaoWithHttpClient] das Wörterbuch [config] den in Zeile 37 verwendeten Schlüssel [logger] noch nicht enthält. Aus diesem Grund kann [self.__logger] (Zeile 23) im Konstruktor nicht initialisiert werden;
  • Zeile 21: In der Konfiguration wurde ein Schlüssel [debug] hinzugefügt, der das Protokoll der Zeilen 33–39 steuert;
  • Zeile 34: wenn man sich im Modus [debug] befindet;
  • Zeilen 36–37: Gegebenenfalls Initialisierung der Eigenschaft [self.__logger]. Bei Verwendung der Methode [calculate_tax] ist der Schlüssel [logger] Teil des Wörterbuchs [config];
  • Zeile 39: Das mit der Antwort HTTP des Servers verbundene Textdokument wird protokolliert;

Die Schicht [dao] wird gleichzeitig von mehreren Threads ausgeführt. Hier wird jedoch nur eine einzige Instanz dieser Schicht erstellt (siehe config_layers). Es muss daher überprüft werden, ob der Code keinen Schreibzugriff auf gemeinsam genutzte Daten beinhaltet, typischerweise die Eigenschaften der Klasse [ImpôtsDaoWithHttpClient], die die Schicht [dao] implementiert. In Zeile 37 wird jedoch eine Eigenschaft der Klasseninstanz geändert. Hier hat dies keine Auswirkungen, da alle Threads denselben Logger gemeinsam nutzen. Wäre dies nicht der Fall gewesen, hätte der Zugriff auf Zeile 37 synchronisiert werden müssen.

24.4.3. Das Hauptskript

Das Hauptskript [main] entwickelt sich wie folgt:


# Anwendung wird konfiguriert

import config
config = config.configure({})

# Abhängigkeiten
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger


# Ausführung der Schicht [dao] in einem Thread
# „taxpayers“ ist eine Liste von Steuerzahlern
def thread_function(dao, logger, taxpayers: list):
    


# Liste der Client-Threads
threads = []
logger = None
# Code
try:
    # Logger
    logger = Logger(config["logsFilename"])
    # wird in der Konfiguration gespeichert
    config["logger"] = logger
    # die Ebene wird abgerufen [dao]
    dao = config["layers"]["dao"]
    # Auslesen der Steuerzahlerdaten
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # der Steuerzahler?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # Berechnung der Steuer der Steuerzahler mit mehreren Threads
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        # Jeder Thread verarbeitet 1 bis 4 Steuerzahler
        nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
        # die Liste der vom Thread bearbeiteten Steuerzahler
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        # i wird für den nächsten Thread erhöht
        i += nb_taxpayers
        # Der Thread wird erstellt
        thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
        # Er wird zur Liste der Threads des Hauptskripts hinzugefügt
        threads.append(thread)
        # Der Thread wird gestartet – dieser Vorgang ist asynchron – das Ergebnis des Threads wird nicht abgewartet
        thread.start()
    # Der Haupt-Thread wartet auf das Ende aller von ihm gestarteten Threads
    for thread in threads:
        thread.join()
    # Hier haben alle Threads ihre Arbeit beendet – jeder hat ein oder mehrere Objekte geändert [taxpayer]
    # Die Ergebnisse werden in der Datei jSON gespeichert
    dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
    # Anzeige des Fehlers
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # Das Logger-Programm wird geschlossen
    if logger:
        logger.close()
    # Fertig
    print("Travail terminé...")
    # Beendigung der Threads, die möglicherweise noch bestehen, falls der Vorgang aufgrund eines Fehlers abgebrochen wurde
    sys.exit()
  • Das Hauptskript unterscheidet sich von dem des vorherigen Clients dadurch, dass es mehrere Ausführungsthreads generiert, um die Anfragen an den Server zu stellen. Der Client der Version 6 führte alle seine Anfragen nacheinander aus. Die Anfrage Nr. i wurde erst gestellt, nachdem die Antwort auf die Anfrage Nr. [i-1] eingegangen war. Hier wollen wir untersuchen, wie sich der Server verhält, wenn er mehrere gleichzeitige Anfragen erhält. Dazu benötigen wir Threads;
  • Zeile 21: Die erzeugten Threads werden in eine Liste aufgenommen. Dabei ist zu beachten, dass das Skript [main] ebenfalls von einem Thread namens [MainThread] ausgeführt wird. Dieser Haupt-Thread erstellt weitere Threads, die für die Berechnung der Steuer eines oder mehrerer Steuerzahler zuständig sind;
  • Zeile 26: Es wird ein Logger erstellt. Dieser wird von allen Threads gemeinsam genutzt;
  • Zeile 32: Es werden alle Steuerpflichtigen abgerufen, deren Steuer berechnet werden muss;
  • Zeilen 39–51: Diese Steuerzahler werden auf mehrere Threads verteilt;
  • Zeilen 40–41: Jeder Thread bearbeitet 1 bis 4 Steuerzahler. Diese Anzahl wird zufällig festgelegt;
    • [random.randint(1, 4)] gibt zufällig eine Zahl aus der Liste [1, 2, 3, 4] aus;
    • Der Thread darf nicht mehr als [l-i] Steuerzahler haben, wobei [l-i] die Anzahl der Steuerzahler angibt, denen noch kein Thread zugewiesen wurde;
    • daher wird der kleinere der beiden Werte genommen;
  • Zeile 43: Sobald [nb_taxpayers], die Anzahl der vom Thread bearbeiteten Steuerzahler, bekannt ist, werden diese aus der Liste der Steuerzahler entnommen:
    • [slice(10,12)] ist die Menge der Indizes [10, 11, 12];
    • [résultat["réponse"]] ist die Liste [taxpayers[10], taxpayers[11], taxpayers[12];
  • Zeile 45: Der Wert von i, der die Schleife in Zeile 39 steuert, wird erhöht;
  • Zeile 47: Es wird ein Thread erstellt:
    • [target=thread_function] legt die Funktion fest, die der Thread ausführen wird. Es handelt sich um die Funktion in den Zeilen 16–17. Sie erwartet drei Parameter;
    • [ags] ist die Liste der drei Parameter, die von der Funktion [thread_function] erwartet werden;

Das Erstellen eines Threads führt diesen noch nicht aus. Es wird lediglich ein Objekt erstellt;

  • Zeilen 48–49: Der soeben erstellte Thread wird der Liste der vom Hauptthread erstellten Threads hinzugefügt;
  • Zeile 51: Der Thread wird gestartet. Er wird nun parallel zu den anderen aktiven Threads ausgeführt. Hier führt er die Funktion [thread_function] mit den übergebenen Argumenten aus;
  • Zeilen 53–54: Der Hauptthread wartet auf jeden der von ihm gestarteten Threads. Nehmen wir ein Beispiel:
    • Der Hauptthread hat drei Threads vom Typ [th1, th2, th3] gestartet;
    • der Hauptthread wartet auf jeden einzelnen Thread (Zeilen 53–54) in der Reihenfolge der for-Schleife: [th1, th2, th3];
    • Nehmen wir an, die Threads enden in der Reihenfolge [th2, th1, th3];
    • Der Hauptthread wartet auf das Ende von th1. Wenn th2 endet, passiert nichts;
    • Wenn th1 endet, wartet der Hauptthread auf th2. Dieser ist jedoch bereits beendet. Der Hauptthread wechselt dann zum nächsten Thread und wartet auf th3;
    • Wenn th3 beendet ist, hat der Haupt-Thread das Warten beendet und fährt mit der Ausführung von Zeile 57 fort;
  • Zeile 57 schreibt die erzielten Ergebnisse in die Ergebnisdatei. Dies ist ein gutes Beispiel für Objektreferenzen:
    • Zeile 43: Die einem Thread zugeordnete Liste [thread_payers] enthält Kopien der Objektreferenzen, die in der Liste [taxpayers] enthalten sind;
    • Es ist bekannt, dass die Steuerberechnung die Objekte verändert, auf die die Referenzen der Liste [thread_payers] verweisen. Diese Objekte werden mit den Ergebnissen der Steuerberechnung ergänzt. Die Referenzen selbst werden jedoch nicht verändert. Die Referenzen der ursprünglichen Liste [taxpayers] „sehen“ also die geänderten Objekte oder „verweisen“ auf diese;

Die von den Threads ausgeführte Funktion [thread_function] lautet wie folgt:


# Ausführung der Ebene [dao] in einem Thread
# „taxpayers“ ist eine Liste von Steuerzahlern
def thread_function(dao, logger, taxpayers: list):
    # Protokoll: Start des Threads
    thread_name = threading.current_thread().name
    logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
    # Die Steuern der Steuerzahler werden berechnet
    for taxpayer in taxpayers:
        # Protokoll
        logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
        # Synchrone Steuerberechnung
        dao.calculate_tax(taxpayer)
        # Protokoll
        logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
    # Protokoll: Ende des Threads
    logger.write(f"fin du thread [{thread_name}]\n")
  • Funktionen, die gleichzeitig von mehreren Threads ausgeführt werden, sind oft schwierig zu programmieren: Man muss stets sicherstellen, dass der Code nicht versucht, gemeinsam genutzte Daten zwischen den Threads zu ändern. In diesem Fall muss ein synchronisierter Zugriff auf die gemeinsam genutzten Daten eingerichtet werden, die geändert werden sollen;
  • Zeile 3: Die Funktion erhält drei Parameter:
    • [dao]: eine Referenz auf die Ebene [dao]. Diese Daten werden gemeinsam genutzt;
    • [logger]: eine Referenz auf den Logger. Diese Daten werden gemeinsam genutzt;
    • [taxpayers]: eine Liste von Steuerzahlern. Diese Daten werden nicht gemeinsam genutzt: Jeder Thread verwaltet eine eigene Liste;
  • Betrachten wir die beiden Referenzen [dao, logger]:
    • Wir haben gesehen, dass das Objekt, auf das die Referenz [dao] verweist, eine Referenz [self.__logger] hatte, die von den Threads geändert wurde, allerdings nur, um einen für alle Threads gemeinsamen Wert einzutragen;
    • Die Referenz [logger] verweist auf einen Dateideskriptor. Wir haben gesehen, dass es beim Schreiben der Protokolle in die Datei zu einem Problem kommen kann. Aus diesem Grund wurde das Schreiben in die Datei synchronisiert;
  • Zeilen 5–6: Der Name des Threads und die Anzahl der Steuerzahler, die er verwalten muss, werden protokolliert;
  • Zeilen 8–14: Berechnung der Steuern der Steuerzahler;
  • Zeile 16: Das Ende des Threads wird protokolliert;

24.4.4. Ausführung

Starten wir den Webserver wie im vorherigen Abschnitt beschrieben (Webserver, SGBD, hMailServer, Thunderbird) und führen wir anschließend das Skript [main] auf dem Client aus. In den Dateien [data/output/errors.txt, data/output/résultats.json] erhalten wir dieselben Ergebnisse wie in der vorherigen Version. In der Datei [data/logs/logs.txt] finden sich folgende Protokolleinträge:


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]
  • Diese Protokolle zeigen, dass fünf Threads gestartet wurden, um die Steuer für 11 Steuerzahler zu berechnen. Diese fünf Threads haben gleichzeitig Anfragen an den Steuerberechnungsserver gesendet. Man muss verstehen, wie das funktioniert:
    • Der Thread [Thread-1] wird als erster gestartet. Sobald er die Prozessorausführung erhält, durchläuft er den Code, bis er seine Anfrage HTTP sendet. Da er auf das Ergebnis dieser Anfrage warten muss, wird er automatisch in den Wartezustand versetzt. Er verliert daraufhin die Prozessorausführung, und ein anderer Thread erhält diese;
    • Zeilen 1–10: Der gleiche Vorgang wiederholt sich für jeden der 5 Threads. Somit werden die 5 Threads gestartet, noch bevor der Thread [Thread-1] seine Antwort erhalten hat (Zeile 11);
  • Die Threads werden nicht in der Reihenfolge beendet, in der sie gestartet wurden. Somit wird der Thread [Thread-3] als erster beendet (Zeile 23);

Auf der Serverseite lauten die Protokolleinträge in der Datei [data/logs/logs.txt] wie folgt:


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/?verheiratet=ja&Kinder=2&Gehalt=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/?verheiratet=ja&Kinder=3&Gehalt=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/?verheiratet=ja&Kinder=2&Gehalt=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/?verheiratet=nein&Kinder=3&Gehalt=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/?verheiratet=ja&Kinder=2&Gehalt=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/?verheiratet=ja&Kinder=3&Gehalt=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/?verheiratet=ja&Kinder=5&Gehalt=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/?verheiratet=nein&Kinder=0&Gehalt=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/?verheiratet=ja&Kinder=3&Gehalt=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/?verheiratet=nein&Kinder=2&Gehalt=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/?verheiratet=nein&Kinder=0&Gehalt=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}}}
  • Man sieht, dass 11 Threads die 11 Steuerzahler bearbeitet haben;
  • Einige Threads wurden in die Warteschlange gestellt (Zeilen 6, 8, 12, 14, 20, 22), andere nicht (Zeilen 9, 23, 25, 29, 31);

24.5. Tests der Schicht [dao]

Wie bereits in der |vorherigen Version| testen wir die Client-Schicht [dao]. Das Prinzip ist genau dasselbe:

Image

Die Testklasse wird in der folgenden Umgebung ausgeführt:

Image

  • Die Konfiguration [2] ist identisch mit der Konfiguration [1], die wir gerade untersucht haben;

Die Testklasse [TestHttpClientDao] lautet wie folgt:


import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        # {'verheiratet': 'ja', 'Kinder': 2, 'Gehalt': 55555,
        # 'Steuer': 2814, 'Zuschlag': 0, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        # Überprüfung
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    


if __name__ == '__main__':
    # Die Anwendung wird konfiguriert
    import config
    config = config.configure({})

    # Logger
    logger = Logger(config["logsFilename"])
    # wird in der Konfiguration gespeichert
    config["logger"] = logger
    # die Ebene [dao] wird abgerufen
    dao = config["layers"]["dao"]

    # die Testmethoden werden ausgeführt
    print("tests en cours...")
    unittest.main()
  • Wir erstellen eine |Ausführungskonfiguration| für diesen Test;
  • Wir starten den Webserver mit seiner gesamten Umgebung;
  • Wir führen den Test aus;

Die Ergebnisse lauten wie folgt:


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