Skip to content

29. Ćwiczenie praktyczne: wersja 11

29.1. Wprowadzenie

W poprzednich wersjach aplikacji klient-serwer służącej do obliczania podatku warstwa [métier], która realizuje reguły biznesowe tego obliczenia, znajdowała się po stronie serwera. Obecnie proponuje się przeniesienie jej na stronę klienta. Jaka jest tego zaleta? Część zadań wykonywanych dotychczas przez serwer zostanie przeniesiona na stronę klienta. Jeśli weźmiemy pod uwagę sytuację, w której serwer jest obsługiwany przez N klientów, to N obliczeń podatkowych zostanie wykonanych przez samych klientów. W poprzednich wersjach serwer wykonywał te N obliczeń. Ponieważ serwer nie będzie już wykonywał obliczeń biznesowych, będzie szybciej odpowiadał na zapytania klientów i będzie mógł obsłużyć jednocześnie większą liczbę użytkowników.

Architektura klient-serwer wygląda następująco:

Image

  • warstwa [métier] [10] została zduplikowana jako [12] po stronie klienta;
  • do klienta dodano nowy skrypt [main2] [11];

Klient internetowy będzie miał dwie możliwości obliczenia podatku dla listy podatników znalezionej w pliku [3]:

  • wykorzystanie metody z poprzedniej wersji. Wykorzystuje ona warstwę serwera [métier] [10]. Skrypt [main] będzie korzystał z tej metody;
  • po prostu poprosić serwer o dane z urzędu skarbowego [2-4], a następnie skorzystać z warstwy [métier] i [12] dostępnej lokalnie u klienta;

Porównamy wydajność obu metod.

29.2. Serwer WWW

Struktura serwera internetowego będzie wyglądać następująco:

Image

  • Folder [http-servers/06] jest początkowo tworzony poprzez skopiowanie folderu [http-servers/05]. Zachowamy bowiem osiągnięcia poprzedniej wersji 10. Dodamy do niej po prostu nową funkcjonalność. Wyraża się ona obecnością nowego kontrolera o nazwie [get_admindata_controller] [1]. Drugi kontroler [calculate_tax_controller] to nic innego jak stary kontroler [index_controller], który został przemianowany;

29.3. Konfiguracja

Serwer będzie oferował dwie usługi URL:

  • [/calculate-tax] do obliczania podatku dla listy podatników przekazanej w treści POST. Odpowiada on zatem URL i [/] z poprzedniej wersji 10;
  • [/get-admindata] generuje ciąg jSON zawierający dane z urzędu skarbowego;

Konfiguracja [config] przypisuje każdy z tych identyfikatorów URL do kontrolera, który go przetwarza:


    # słownik kontrolerów aplikacji internetowej
    import calculate_tax_controller, get_admindata_controller
    controllers = {"calculate-tax": calculate_tax_controller, "get-admindata": get_admindata_controller}
    config['controllers'] = controllers

29.4. Główny skrypt [main]

Główny skrypt [main] przebudowuje skrypt [main] z poprzedniej wersji:


# oczekuje się parametru mysql lub pgres
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()

# konfiguracja aplikacji
import config
config = config.configure({'sgbd': sgbd})

# zależności


# pobieranie danych z urzędu skarbowego
erreur = False
try:
    # admindata będzie danymi o zasięgu aplikacji, dostępnymi wyłącznie do odczytu
    config["admindata"] = config["layers"]["dao"].get_admindata()
    # log pomyślnego wykonania
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # odnotowano błąd
    …

# w przypadku wystąpienia błędu proces zostaje zatrzymany
if erreur:
    sys.exit(2)

# aplikacja Flask może się uruchomić
app = Flask(__name__)

# główny kontroler
def main_controller() -> tuple:
    # pobieramy żądaną akcję
    dummy, action=request.path.split('/')
    logger = None
    try:
        # logger
        logger = Logger(config["logsFilename"])
        # zapisywanie w konfiguracji powiązanej z wątkiem
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # rejestruje się żądanie
        logger.write(f"[index] requête : {request}\n")
        # przerywamy wątek, jeśli zostało to zlecone
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # przerwa jest losowa, aby niektóre wątki zostały przerwane, a inne nie
            aléa = randint(01)
            if aléa == 1:
                # log przed przerwą
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # przerwa
                time.sleep(sleep_time)
        # żądanie jest wykonywane przez kontroler żądań
        controller = config['controllers'][action]
        résultat, status_code = controller.execute(request, config)
        # czy wystąpił błąd krytyczny?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # wysyłamy wiadomość e-mail do administratora aplikacji
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        # replika jest rejestrowana
        logger.write(f"[index] {résultat}\n")
        # wysyłamy odpowiedź
        print(résultat)
        return json_response(résultat, status_code)
    except BaseException as erreur:
        # rejestruje się błąd, jeśli to możliwe
        if logger:
            logger.write(f"[index] {erreur}")
        # przygotowuje się odpowiedź dla klienta
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        # wysyłamy odpowiedź
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # zamykamy plik dziennika, jeśli został otwarty
        if logger:
            logger.close()

# obliczanie podatku
@app.route('/calculate-tax', methods=['POST'])
@auth.login_required
def calculate_tax():
    # przekazanie kontroli do głównego kontrolera
    return main_controller()

# pobieranie danych z urzędu skarbowego
@app.route('/get-admindata', methods=['GET'])
@auth.login_required
def get_admindata():
    # przekazanie kontroli do głównego kontrolera
    return main_controller()

# tylko ręcznie
if __name__ == '__main__':
    # uruchamiamy serwer
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • wiersze 88–93: funkcja [calculate_tax] przetwarza skrypty URL i [/calculate-tax];
  • wiersze 95–100: funkcja [get_admindata] przetwarza funkcje URL i [/get-admindata];
  • te dwie funkcje same w sobie nic nie robią. Natychmiast przekazują kontrolę głównemu kontrolerowi [main_controller] z wierszy 37–86;
  • wiersze 37–86: główny kontroler [main_controller] to nic innego jak funkcja [index] z poprzedniej wersji, z jednym wyjątkiem: podczas gdy funkcja [index] przetwarzała tylko jedną instancję URL, tutaj [main_controller] przetwarza dwie instancje URL. Musi więc zlecić ich przetworzenie jednemu z dwóch kontrolerów [calculate_tax_controller, get_admin,data_controller];
  • wiersze 39–40: pobieramy żądaną akcję [calculate_tax] lub [get_admindata]. Informacja ta znajduje się w ścieżce URL [request.path]. W zależności od sytuacji [request.path] odpowiada [/get-admindata] lub [/calculate_tax]. Podział wiersza 40 da dwa elementy:
    • pusty ciąg znaków dla części poprzedzającej znak /;
    • nazwę żądanej akcji dla części następującej po znaku /;
  • wiersze 62–63: po pobraniu akcji z URL wiadomo, którego kontrolera należy użyć do przetworzenia URL. Informacje te znajdują się w konfiguracji [config];

29.5. Kontrolery

Kontroler [calculate_tax_controller] to nic innego jak kontroler [index_controller] z poprzedniej wersji.

Kontroler [get_admindata_controller] ma następujące parametry:


from flask_api import status
from werkzeug.local import LocalProxy

def execute(request: LocalProxy, config: dict) -> tuple:
    # zwracamy odpowiedź
    return {"réponse": {"result": config["admindata"].asdict()}}, status.HTTP_200_OK
  • URL [/get-admindata] musi zwrócić ciąg jSON z danych administracji podatkowej;
  • wiersz 6: dane te zostały pobrane przez główny skrypt [main] i umieszczone w słowniku [config] w postaci obiektu [AdminData]. Zwracamy słownik tego obiektu;

29.6. Testy w Postmanie

Uruchamiamy serwer WWW, SGBD oraz serwer pocztowy [hMailServer]. Następnie za pomocą klienta Postman obliczamy podatek dla kilku podatników:

Image

W konsoli Postmana dialog między klientem a serwerem wygląda następująco:


POST /calculate-tax HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 5e71461a-fec8-4315-85e8-41721de939e5
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824

[ 
{ 
"marié": "oui", 
"enfants": 2, 
"salaire": 55555 
}, 

{ 
"marié": "oui", 
"enfants": 3, 
"salaire": 200000 
} 
]

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:02:07 GMT

{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347…]}}

Teraz wyślijmy żądanie do serwera URL i [/get-admindata] przy użyciu GET:

Image

Dialog klient–serwer w konsoli Postman wygląda następująco:


GET /get-admindata HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 4af342c4-7ecb-4ab2-9e12-d653f81da424
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 596
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:07:24 GMT

{"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 93749.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}

29.7. Klient internetowy

Image

Image

Folder [http-clients/06] powstaje początkowo poprzez skopiowanie folderu [http-clients/05]. Praca nad modyfikacją polega głównie na:

  • zmianę konfiguracji [config_layers] tak, aby zawierała teraz warstwę [métier]. Wcześniej zawierała ona tylko warstwę [dao];
  • dodanie nowej metody do warstwy [dao];
  • napisać skrypt [main2], który będzie opierał się na warstwie [métier] klienta w celu obliczenia podatku podatników;

29.7.1. Konfiguracja warstw klienta

Konfiguracja warstw odbywa się w dwóch miejscach:

  • w konfiguracji [config], która musi uwzględniać w zależnościach klienta folder zawierający implementację warstwy [métier]. Folder ten był już uwzględniony w zależnościach:

absolute_dependencies = [
        # foldery projektu
        # 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",
        # Stałe, przedziały
        f"{root_dir}/impots/v05/entities",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # skrypty konfiguracyjne
        script_dir,
        # Rejestrator
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

Następnie należy zmodyfikować plik [config_layers]:


def configure(config: dict) -> dict:
    # instancjonowanie warstw aplikacji

    # warstwa [métier]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # warstwa DAO
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    # konfiguracja warstw
    return {
        "dao": dao,
        "métier": métier
    }
  • wiersze 4–6: instancja warstwy [métier];
  • wiersze 13–16: warstwa [métier] została dodana do słownika warstw;

29.7.2. Implementacja warstwy [dao]

Image

Warstwa [dao] będzie posiadać następujący interfejs [InterfaceImpôtsDaoWithHttpClient]:


from abc import abstractmethod

from AbstractImpôtsDao import AbstractImpôtsDao

class InterfaceImpôtsDaoWithHttpClient(AbstractImpôtsDao):

    # obliczanie podatku
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass
  • wiersz 5: interfejs [InterfaceImpôtsDaoWithHttpClient] dziedziczy po klasie abstrakcyjnej [AbstractImpôtsDao], która zarządza dostępem do systemu plików klienta. Przypomnijmy, że posiada ona metodę abstrakcyjną [get_admindata];
  • wiersze 7–10: metoda [calculate_tax_in_bulk_mode], którą zdefiniowaliśmy w poprzedniej wersji, umożliwia obliczenie podatku dla listy podatników;

Interfejs ten jest zaimplementowany przez następującą klasę [ImpôtsDaoWithHttpClient]:


# importy

import json

import requests
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpClient import InterfaceImpôtsDaoWithHttpClient

class ImpôtsDaoWithHttpClient(InterfaceImpôtsDaoWithHttpClient):

    # producent
    def __init__(self, config: dict):
        # inicjalizacja elementu nadrzędnego
        AbstractImpôtsDao.__init__(self, config)
        # zapisywanie elementów konfiguracji
        # ogólna konfiguracja
        self.__config = config
        # serwer
        self.__config_server = config["server"]
        # tryb debugowania
        self.__debug = config["debug"]
        # rejestrator
        self.__logger = None

    def get_admindata(self) -> AdminData:
        # zezwalamy na zgłaszanie wyjątków

        # adres URL usługi
        config_server = self.__config_server
        config_services = config_server['url_services']
        url_service = f"{config_server['urlServer']}{config_services['get-admindata']}"

        # połączenie
        if config_server['authBasic']:
            response = requests.get(url_service,
                                    auth=(
                                        config_server["user"]["login"],
                                        config_server["user"]["password"]))
        else:
            response = requests.get(url_service)

        # tryb debugowania?
        if self.__debug:
            # moduł logujący
            if not self.__logger:
                self.__logger = self.__config['logger']
            # rejestruje się
            self.__logger.write(f"{response.text}\n")

        # kod statusu
        status_code = response.status_code
        # wynik w postaci słownika
        résultat = json.loads(response.text)
        # jeśli kod statusu jest inny niż 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(58, résultat['réponse']['erreurs'])
        # zwracamy wynik (słownik)
        return AdminData().fromdict(résultat["réponse"]["result"])

    # obliczanie podatku w trybie zbiorczym
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        # pozwala na zgłaszanie wyjątków

        
  • wiersz 13: klasa [ImpôtsDaoWithHttpClient] implementuje interfejs [InterfaceImpôtsDaoWithHttpClient]. Wywodzi się zatem z klasy [AbstractImpôtsDao];
  • wiersze 65–66: metoda [calculate_tax_in_bulk_mode] omówiona w poprzedniej wersji;
  • wiersze 29–62: metoda [get_admindata], którą klasa nadrzędna [AbstractImpôtsDao] zadeklarowała jako abstrakcyjną. Jest ona zatem zaimplementowana w klasie podrzędnej;
  • wiersze 33–35: określa się identyfikator URL usługi internetowej, do której metoda [get-admindata] ma wysłać zapytanie. Te identyfikatory usługi URL są zdefiniowane w konfiguracji klienta [config]:

       # serwer obliczający podatek
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "authBasic"True,
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculate-tax",
                "get-admindata""/get-admindata"
            }
        },
  • (ciąg dalszy)
    • wiersze 9–12: dwa URL serwera internetowego;
  • wiersze 37–44: usługa URL jest wywoływana synchronicznie;
  • wiersze 46–42: jeśli wymaga tego konfiguracja, odpowiedź serwera jest rejestrowana;
  • wiersz 57: wiadomo, że serwer wysłał ciąg znaków jSON z słownika;
  • wiersze 58–60: jeśli status odpowiedzi HTTP nie wynosi 200, generowany jest wyjątek;
  • wiersze 61–62: zwracany jest obiekt [AdminData] zawierający dane urzędu skarbowego przesłane przez serwer;

29.8. Skrypty [main, main2]

Skrypt [main] pochodzi z poprzedniej wersji. Wykorzystuje on metodę [calculate_tax_in_bulk_mode] z warstwy [dao], a zatem korzysta z warstwy [métier] serwera;

Skrypt [main2] działa tak samo jak skrypt [main], ale korzysta z warstwy [métier] po stronie klienta:


# konfiguracja aplikacji

import config
config = config.configure({})

# zależności
from ImpôtsError import ImpôtsError
from Logger import Logger

logger = None
# kod
try:
    # logger
    logger = Logger(config["logsFilename"])
    # zapisujemy to w konfiguracji
    config["logger"] = logger
    # log początkowy
    logger.write("début du calcul de l'impôt des contribuables\n")
    # pobieramy warstwę [dao]
    dao = config["layers"]["dao"]
    # pobieramy podatników
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # podatników?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # pobieramy dane z urzędu skarbowego
    admindata = dao.get_admindata()
    # obliczanie podatku podatników przez warstwę [métier]
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    # zapisywanie wyników do pliku jSON
    dao.write_taxpayers_results(taxpayers)
# z wyjątkiem BaseException jako błąd:
# # wyświetlenie błędu
# print(f"Wystąpił następujący błąd: {błąd}")
finally:
    # zamykamy logger
    if logger:
        # log końcowy
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # zamknięcie modułu logującego
        logger.close()
    # zakończono
    print("Travail terminé...")
  • wiersze 26–27: pobierane są dane z urzędu skarbowego z serwera;
  • wiersze 28–31: następnie lokalnie obliczany jest podatek podatników;

29.9. Testy klienta

W każdym ze skryptów [main, main2] rejestruje się początek i koniec działania skryptu. Dzięki temu można obliczyć czas wykonania skryptu. Spróbujmy sformułować kilka prognoz:

  • skrypt [main] z poprzedniej wersji:
    • tworzy N wątków, które działają jednocześnie;
    • każdy wątek przetwarza partię podatników, dla których oblicza podatek za pomocą pojedynczego zapytania do serwera;
    • ponieważ N wątków działa jednocześnie, żądanie N+1 jest wysyłane, zanim żądanie N otrzyma odpowiedź. W ten sposób N żądań kosztuje więcej niż jedno żądanie, ale prawdopodobnie nieznacznie więcej. Ponadto na serwerze wykonywanych jest 11 (liczba podatników) obliczeń biznesowych;
  • skrypt [main2] w tej wersji:
    • wysyła jedno zapytanie do serwera;
    • wykonuje 11 obliczeń biznesowych lokalnie na kliencie;

Obliczenia biznesowe będą trwały tyle samo, niezależnie od tego, czy są wykonywane na serwerze, czy na kliencie. Różnica będzie więc wynikała z zapytań. Można zatem oczekiwać, że czas wykonania skryptu [main] będzie nieco dłuższy niż skryptu [main2].

Uruchamiamy serwer w wersji 11, SGBD oraz serwer pocztowy [hMailServer]. Po stronie serwera ustawiamy parametr [sleep_time] na zero, aby oba testy zostały przeprowadzone w takich samych warunkach.

Uruchomienie 1 [main]

Uruchomienie [main] generuje następujące logi:


2020-07-29 14:35:50.016079, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:35:50.016079, Thread-1 : début du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.016079, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.016079, Thread-3 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.016079, Thread-4 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.024426, Thread-5 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.050473, Thread-1 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.050473, Thread-1 : fin du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.050473, Thread-3 : {"réponse": {"results": [{"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-3 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-5 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-5 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-2 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.051214, Thread-4 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-4 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, MainThread : fin du calcul de l'impôt des contribuables

Czas trwania wykonania wyniósł [051214-016079] nanosekund (wiersz 17 – wiersz 1), czyli 35 milisekund i 135 nanosekund.

Widać, że między pierwszym żądaniem wysłanym do serwera a ostatnią odpowiedzią otrzymaną przez klienta upłynął ten sam czas [051214-016079] (wiersz 15 – wiersz 1), czyli 35 milisekund i 135 nanosekund.

Wykonanie 2 [main2]

Wykonanie [main2] generuje następujące logi:


2020-07-29 14:41:03.303520, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:41:03.345084, MainThread : {"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
2020-07-29 14:41:03.349975, MainThread : fin du calcul de l'impôt des contribuables

Czas wykonania wyniósł [349975-303520] nanosekund (wiersz 3 – wiersz 1), czyli 46 milisekund i 455 nanosekund. Zupełnie nieoczekiwanie [main] działa szybciej niż [main2].

Widać, że pojedyncze zapytanie o numerze [main2] trwało [345084-303520] (wiersz 2 – wiersz 1), czyli 41 milisekund i 564 nanosekund. Następnie obliczenie podatku trwało [349975-345084] (wiersz 3 – wiersz 2), czyli 4 milisekundy i 91 nanosekund. To właśnie zapytanie HTTP decyduje o czasie wykonania. Co zaskakujące, widać tutaj, że pojedyncze żądanie o numerze [main2] trwało dłużej niż cztery równoczesne żądania o numerach [main] i [35 millisecondes].

Po stronie serwera logi wyglądają następująco:


2020-07-29 14:35:27.047721, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:27.140927, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:28.790716, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:28.847518, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:50.039178, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.039178, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.043220, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.044307, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.045796, Thread-2 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.045796, Thread-3 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.046825, Thread-6 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}, {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}, {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-4 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-5 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:41:03.341582, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/get-admindata' [GET]>
2020-07-29 14:41:03.341582, Thread-7 : [index] {'réponse': {'result': {'limites': [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], 'coeffr': [0.0, 0.14, 0.3, 0.41, 0.45], 'coeffn': [0.0, 1394.96, 5798.0, 13913.7, 20163.4], 'plafond_decote_couple': 1970.0, 'valeur_reduc_demi_part': 3797.0, 'plafond_revenus_celibataire_pour_reduction': 21037.0, 'plafond_qf_demi_part': 1551.0, 'abattement_dixpourcent_max': 12502.0, 'plafond_impot_celibataire_pour_decote': 1595.0, 'plafond_decote_celibataire': 1196.0, 'plafond_revenus_couple_pour_reduction': 42074.0, 'id': 1, 'abattement_dixpourcent_min': 437.0, 'plafond_impot_couple_pour_decote': 2627.0}}}
  • wiersz 5: pierwsze żądanie klienta [main];
  • wiersz 14: ostatnia odpowiedź dla klienta [main]. Między nimi upłynęło 6 milisekund i 647 nanosekund;
  • wiersze 15–16: jedyne żądanie klienta [main2]. Odpowiedź jest natychmiastowa;