Skip to content

29. Практичне завдання: версія 11

29.1. Вступ

У попередніх версіях клієнт-серверного додатка для розрахунку податку рівень [métier], що реалізує бізнес-правила цього розрахунку, знаходився на стороні сервера. Тепер пропонується перемістити його на сторону клієнта. У чому полягає перевага? Частина роботи, яку виконував сервер, буде перенесена на сторону клієнта. Якщо уявити ситуацію, коли до сервера звертаються N клієнтів, то N бізнес-розрахунків податку будуть виконуватися клієнтами. У попередніх версіях сервер виконував ці N бізнес-розрахунків. Оскільки сервер більше не виконує бізнес-розрахунки, він буде швидше відповідати своїм клієнтам і, отже, зможе обслуговувати більше клієнтів одночасно.

Архітектура «клієнт/сервер» набуває такого вигляду:

Image

  • шар [métier] [10] було продубльовано на клієнті як [12];
  • на стороні клієнта додано новий скрипт [main2] [11];

Веб-клієнт матиме два способи розрахунку податку для списку платників податків, знайденого у [3]:

  • використовувати метод попередньої версії. Він використовує серверний рівень [métier] [10]. Цей метод використовуватиме скрипт [main];
  • задовольнитися запитом до сервера щодо даних податкової адміністрації [2-4], а потім використовувати локальний для клієнта шар [métier] [12];

Ми порівняємо продуктивність обох методів.

29.2. Веб-сервер

Структура веб-сервера буде такою:

Image

  • Папка [http-servers/06] спочатку створюється шляхом копіювання папки [http-servers/05]. Ми збережемо всі переваги попередньої версії 10. Просто додамо до неї нову функцію. Вона реалізована за допомогою нового контролера [get_admindata_controller] [1]. Інший контролер [calculate_tax_controller] — це ніщо інше, як колишній контролер [index_controller], який було перейменовано;

29.3. Конфігурація

Сервер надаватиме дві служби URL:

  • [/calculate-tax] — для розрахунку податку за списком платників податків, переданим у тілі запиту POST. Отже, вона відповідає URL та [/] з попередньої версії 10;
  • [/get-admindata] видає рядок jSON з даними податкової адміністрації;

Конфігурація [config] пов’язує кожну з цих URL з контролером, який її обробляє:


    # словник контролерів веб-додатку
    import calculate_tax_controller, get_admindata_controller
    controllers = {"calculate-tax": calculate_tax_controller, "get-admindata": get_admindata_controller}
    config['controllers'] = controllers

29.4. Головний скрипт [main]

Головний скрипт [main] реструктурує скрипт [main] попередньої версії:


# очікується параметр mysql або 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()

# налагоджуємо додаток
import config
config = config.configure({'sgbd': sgbd})

# залежності


# отримання даних від податкової служби
erreur = False
try:
    # admindata буде даними на рівні додатка, доступними лише для читання
    config["admindata"] = config["layers"]["dao"].get_admindata()
    # журнал успішного виконання
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # фіксується помилка
    …

# якщо сталася помилка, процес зупиняється
if erreur:
    sys.exit(2)

# додаток Flask може запуститися
app = Flask(__name__)

# головний контролер
def main_controller() -> tuple:
    # отримуємо запитувану дію
    dummy, action=request.path.split('/')
    logger = None
    try:
        # реєстратор
        logger = Logger(config["logsFilename"])
        # зберігаємо його у конфігурації, пов’язаній із потоком
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # запис запиту в журнал
        logger.write(f"[index] requête : {request}\n")
        # потоку припиняється, якщо це було запропоновано
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # пауза є випадковою, щоб одні потоки були перервані, а інші — ні
            aléa = randint(01)
            if aléa == 1:
                # запис у журнал перед паузою
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # пауза
                time.sleep(sleep_time)
        # запит виконується контролером запитів
        controller = config['controllers'][action]
        résultat, status_code = controller.execute(request, config)
        # чи сталася фатальна помилка?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # надсилається електронний лист адміністратору додатка
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        # відповідь записується в журнал
        logger.write(f"[index] {résultat}\n")
        # відправляється відповідь
        print(résultat)
        return json_response(résultat, status_code)
    except BaseException as erreur:
        # записуємо помилку в журнал, якщо це можливо
        if logger:
            logger.write(f"[index] {erreur}")
        # готується відповідь клієнту
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        # відправляється відповідь
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # закрити файл журналу, якщо він був відкритий
        if logger:
            logger.close()

# розрахунок податку
@app.route('/calculate-tax', methods=['POST'])
@auth.login_required
def calculate_tax():
    # передача управління головному контролеру
    return main_controller()

# отримання даних від податкової служби
@app.route('/get-admindata', methods=['GET'])
@auth.login_required
def get_admindata():
    # передача управління головному контролеру
    return main_controller()

# тільки управління
if __name__ == '__main__':
    # запуск сервера
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • рядки 88–93: функція [calculate_tax] обробляє URL та [/calculate-tax];
  • рядки 95–100: функція [get_admindata] обробляє URL та [/get-admindata];
  • ці дві функції самі по собі нічого не роблять. Вони негайно передають управління головному контролеру [main_controller] у рядках 37–86;
  • рядки 37–86: головний контролер [main_controller] — це ніщо інше, як функція [index] з попередньої версії, за винятком однієї деталі: там, де функція [index] обробляла лише одну URL, тут [main_controller] обробляє дві URL. Тому їй потрібно передати їх на обробку одному з двох контролерів [calculate_tax_controller, get_admin,data_controller];
  • рядки 39–40: отримуємо запитувану дію [calculate_tax] або [get_admindata]. Ця інформація міститься у шляху URL [request.path]. Залежно від випадку, [request.path] дорівнює [/get-admindata] або [/calculate_tax]. Розбиття рядка 40 дасть два елементи:
    • порожній рядок для частини, що передує символу /;
    • ім’я запитуваної дії для частини, що йде після /;
  • рядки 62–63: після отримання дії URL стає відомо, який контролер слід використовувати для обробки URL. Ця інформація міститься в конфігурації [config];

29.5. Контролери

Контролер [calculate_tax_controller] — це той самий контролер [index_controller] з попередньої версії.

Контролер [get_admindata_controller] має такий вигляд:


from flask_api import status
from werkzeug.local import LocalProxy

def execute(request: LocalProxy, config: dict) -> tuple:
    # повертається відповідь
    return {"réponse": {"result": config["admindata"].asdict()}}, status.HTTP_200_OK
  • URL [/get-admindata] повинен повертати рядок jSON з даних податкової адміністрації;
  • рядок 6: ці дані були отримані головним скриптом [main] і поміщені в словник [config] у вигляді об’єкта [AdminData]. Повертається словник цього об’єкта;

29.6. Тестування за допомогою Postman

Запускаємо веб-сервер SGBD та поштовий сервер [hMailServer]. Потім за допомогою клієнта Postman обчислюємо податок для кількох платників податків:

Image

У консолі Postman діалог між клієнтом і сервером виглядає так:


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…]}}

Тепер запитаємо URL [/get-admindata] разом із GET:

Image

Діалог «клієнт/сервер» у консолі Postman виглядає так:


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. Веб-клієнт

Image

Image

Папка [http-clients/06] спочатку створюється шляхом копіювання папки [http-clients/05]. Робота з модифікації полягає, в основному, у:

  • змінити конфігурацію [config_layers], щоб вона тепер містила шар [métier]. Раніше вона мала лише шар [dao];
  • додати новий метод до шару [dao];
  • написати скрипт [main2], який використовуватиме шар [métier] клієнта для розрахунку податків платників;

29.7.1. Налаштування шарів клієнта

Налаштування шарів відбувається у двох місцях:

  • у конфігурації [config], яка повинна включати до залежностей клієнта папку, що містить реалізацію шару [métier]. Ця папка вже була включена до залежностей:

absolute_dependencies = [
        # папки проєкту
        # 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",
        # Константи, діапазони
        f"{root_dir}/impots/v05/entities",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # конфігураційні скрипти
        script_dir,
        # Реєстратор
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

Потім слід змінити файл [config_layers]:


def configure(config: dict) -> dict:
    # інстанціювання шарів додатка

    # шар [métier]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # шар DAO
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    # видаємо конфігурацію шарів
    return {
        "dao": dao,
        "métier": métier
    }
  • рядки 4–6: інстанціювання шару [métier];
  • рядки 13–16: шар [métier] додано до словника шарів;

29.7.2. Реалізація шару [dao]

Image

Шар [dao] матиме такий інтерфейс [InterfaceImpôtsDaoWithHttpClient]:


from abc import abstractmethod

from AbstractImpôtsDao import AbstractImpôtsDao

class InterfaceImpôtsDaoWithHttpClient(AbstractImpôtsDao):

    # розрахунок податку
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass
  • рядок 5: інтерфейс [InterfaceImpôtsDaoWithHttpClient] успадковує абстрактний клас [AbstractImpôtsDao], який керує доступом до файлової системи клієнта. Нагадаємо, що він має абстрактний метод [get_admindata];
  • рядки 7–10: метод [calculate_tax_in_bulk_mode], який ми визначили в попередній версії, дозволяє обчислити податок для списку платників податків;

Цей інтерфейс реалізовано наступним класом [ImpôtsDaoWithHttpClient]:


# імпорт

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

    # виробник
    def __init__(self, config: dict):
        # ініціалізація батьківського елемента
        AbstractImpôtsDao.__init__(self, config)
        # збереження елементів конфігурації
        # загальна конфігурація
        self.__config = config
        # сервер
        self.__config_server = config["server"]
        # режим налагодження
        self.__debug = config["debug"]
        # реєстратор
        self.__logger = None

    def get_admindata(self) -> AdminData:
        # дозволити передачу винятків

        # URL-адреса сервісу
        config_server = self.__config_server
        config_services = config_server['url_services']
        url_service = f"{config_server['urlServer']}{config_services['get-admindata']}"

        # підключення
        if config_server['authBasic']:
            response = requests.get(url_service,
                                    auth=(
                                        config_server["user"]["login"],
                                        config_server["user"]["password"]))
        else:
            response = requests.get(url_service)

        # режим налагодження?
        if self.__debug:
            # логгер
            if not self.__logger:
                self.__logger = self.__config['logger']
            # запис у журнал
            self.__logger.write(f"{response.text}\n")

        # код статусу
        status_code = response.status_code
        # результат у вигляді словника
        résultat = json.loads(response.text)
        # якщо код статусу відмінний від 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(58, résultat['réponse']['erreurs'])
        # повертається результат (словник)
        return AdminData().fromdict(résultat["réponse"]["result"])

    # розрахунок податку в масовому режимі
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        # дозволяється передавати винятки

        
  • рядок 13: клас [ImpôtsDaoWithHttpClient] реалізує інтерфейс [InterfaceImpôtsDaoWithHttpClient]. Отже, він походить від класу [AbstractImpôtsDao];
  • рядки 65–66: метод [calculate_tax_in_bulk_mode], розглянутий у попередній версії;
  • рядки 29–62: метод [get_admindata], який батьківський клас [AbstractImpôtsDao] оголосив абстрактним. Отже, він реалізований у дочірньому класі;
  • рядки 33–35: визначається URL веб-сервісу, до якого має звертатися метод [get-admindata]. Ці URL сервісу визначені в конфігурації [config] клієнта:

       # сервер розрахунку податку
        "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"
            }
        },
  • (продовження)
    • рядки 9–12: два параметри URL веб-сервера;
  • рядки 37–44: службовий URL запитується синхронно;
  • рядки 46–42: якщо це передбачено конфігурацією, відповідь сервера записується в журнал;
  • рядок 57: відомо, що сервер надіслав рядок jSON із словника;
  • рядки 58–60: якщо статус відповіді HTTP не дорівнює 200, то генерується виняток;
  • рядки 61–62: повертається об’єкт [AdminData], що інкапсулює дані податкової адміністрації, надіслані сервером;

29.8. Скрипти [main, main2]

Скрипт [main] належить до попередньої версії. Він використовує метод [calculate_tax_in_bulk_mode] з шару [dao] і, отже, використовує шар [métier] сервера;

Скрипт [main2] виконує ті самі дії, що й скрипт [main], але використовує рівень [métier] клієнта:


# конфігурація додатка

import config
config = config.configure({})

# залежності
from ImpôtsError import ImpôtsError
from Logger import Logger

logger = None
# код
try:
    # логгер
    logger = Logger(config["logsFilename"])
    # зберігаємо його в конфігурації
    config["logger"] = logger
    # початковий журнал
    logger.write("début du calcul de l'impôt des contribuables\n")
    # отримуємо шар [dao]
    dao = config["layers"]["dao"]
    # отримуємо платників податків
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # платників податків?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # отримуємо дані податкової адміністрації
    admindata = dao.get_admindata()
    # розрахунок податку платників податків за допомогою шару [métier]
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    # результати записуються у файл jSON
    dao.write_taxpayers_results(taxpayers)
# крім BaseException як помилка:
# # виведення повідомлення про помилку
# print(f"Сталася така помилка: {помилка}")
finally:
    # закриваємо логгер
    if logger:
        # завершення запису в журнал
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # закриття логгера
        logger.close()
    # завершено
    print("Travail terminé...")
  • рядки 26–27: отримуються дані податкової адміністрації з сервера;
  • рядки 28–31: потім обчислення податку платників здійснюється локально;

29.9. Тестування клієнта

У кожному зі скриптів [main, main2] фіксується початок і кінець виконання скрипта. Таким чином можна буде обчислити тривалість виконання скрипта. Давайте зробимо кілька прогнозів:

  • скрипт [main] з попередньої версії:
    • створює N потоків, які виконуються одночасно;
    • кожен потік обробляє партію платників податків, для яких він розраховує податок за допомогою єдиного запиту до сервера;
    • оскільки N потоків виконуються одночасно, запит N+1 запускається до того, як запит N отримав відповідь. Таким чином, N запитів обходяться дорожче, ніж один запит, але, ймовірно, не набагато дорожче. Крім того, на сервері виконується 11 (кількість платників податків) бізнес-розрахунків;
  • скрипт [main2] цієї версії:
    • здійснює єдиний запит до сервера;
    • виконує 11 бізнес-розрахунків локально на клієнті;

Бізнес-розрахунки триватимуть однаково незалежно від того, чи виконуються вони на сервері, чи на клієнті. Отже, різниця полягатиме у запитах. Тому можна очікувати, що час виконання [main] буде дещо довшим, ніж у [main2].

Запускаємо сервер версії 11, SGBD та поштовий сервер [hMailServer]. На стороні сервера встановлюємо параметр [sleep_time] на нуль, щоб обидва тести виконувалися за однакових умов.

Виконання 1 [main]

Виконання [main] дає такі журнали:


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

Тривалість виконання склала [051214-016079] наносекунд (рядок 17 – рядок 1), тобто 35 мілісекунд і 135 наносекунд.

Бачимо, що між першим запитом до сервера та останньою відповіддю, отриманою клієнтом, минув однаковий проміжок часу [051214-016079] (рядок 15 – рядок 1), 35 мілісекунд і 135 наносекунд.

Виконання 2 [main2]

Виконання [main2] дає такі записи в журналі:


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

Тривалість виконання склала [349975-303520] наносекунд (рядок 3 – рядок 1), тобто 46 мілісекунд і 455 наносекунд. Цілком несподівано [main] виявився швидшим за [main2].

Бачимо, що єдиний запит [main2] тривав [345084-303520] (рядок 2 – рядок 1), тобто 41 мілісекунду та 564 наносекунди. Потім обчислення податку тривало [349975-345084] (рядок 3 – рядок 2), тобто 4 мілісекунди та 91 наносекунду. Саме запит HTTP визначає тривалість виконання. Дивно, але тут видно, що єдиний запит [main2] тривав довше, ніж чотири одночасні запити [main] та [35 millisecondes].

Що стосується сервера, то журнали мають такий вигляд:


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}}}
  • рядок 5: перший запит клієнта [main];
  • рядок 14: остання відповідь клієнту [main]. Між ними — 6 мілісекунд і 647 наносекунд;
  • рядки 15–16: єдиний запит клієнта [main2]. Відповідь надходить миттєво;