Skip to content

24. 练习题:第 7 版

24.1. 简介

税费计算应用程序的第7版与第6版完全相同,仅以下细节有所不同:

  • Web客户端将同时发送多个HTTP请求。在上一版本中,这些请求是依次发送的。因此,服务器每次只能处理一个请求;
  • 服务器将采用多线程架构:能够同时处理多个请求;
  • 为追踪这些请求的执行情况,Web 服务器将配备一个日志记录器,用于将请求处理过程中的关键时刻记录到文本文件中;
  • 当服务器遇到导致其无法启动的问题时(通常是与 Web 服务器关联的数据库问题),将向应用程序管理员发送电子邮件;

应用程序架构保持不变:

Image

脚本的目录结构如下:

Image

首先通过复制 [http-servers/01] 文件夹来创建 [http-servers/02] 文件夹,随后对其进行修改。

24.2. 实用工具

Image

24.2.1. [Logger] 类

[Logger] 类允许将某些 Web 服务器操作记录到文本文件中:

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

from ImpôtsError import ImpôtsError


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

    #  manufacturer
    def __init__(self, logs_filename: str):
        try:
            #  open the file in append mode (a)
            self.__resource = codecs.open(logs_filename, "a", "utf-8")
        except BaseException as erreur:
            raise ImpôtsError(18, f"{erreur}")

    #  writing a log
    def write(self, message: str):
        #  current date / time
        today = date.today()
        now = datetime.time(datetime.now())
        #  thread name
        thread_name = current_thread().name
        #  you don't want to be disturbed while writing to the log file
        #  we request the class's synchronization object (= lock) - only one thread will get it
        Logger.verrou.acquire()
        try:
            #  log entry
            self.__resource.write(f"{today} {now}, {thread_name} : {message}")
            #  write immediately - otherwise the text will only be written when the write stream is closed
            #  we want to track logs over time
            self.__resource.flush()
        finally:
            #  release the synchronization object (= lock) so that another thread can obtain it
            Logger.verrou.release()

    #  freeing up resources
    def close(self):
        #  close file
        if self.__resource:
            self.__resource.close()
  • 第 10–11 行:我们定义了一个类属性。类属性是该类所有实例共有的属性。使用 [Class.class_attribute] 这种语法来引用它(第 30、39 行)。类属性 [lock] 将作为所有执行第 31–36 行代码的线程的同步对象;
  • 第 14–19 行:构造函数接收日志文件的绝对路径。随后打开该文件,并将获取的文件描述符存储在类中;
  • 第 17 行:日志文件以“追加”模式 (a) 打开。写入的每一行都将追加到文件末尾;
  • 第 22–39 行:[write] 方法允许将作为参数传递的消息写入日志文件。该消息后会附加两项信息:
    • 第 24 行:当前日期;
    • 第 25 行:当前时间;
    • 第 27 行:写入日志的线程名称。这里需要记住的是,Web 应用程序会同时为多个用户提供服务。每个请求都会被分配一个线程来执行。如果该线程被暂停——通常是因为 I/O 操作(网络、文件、数据库)——则处理器控制权将移交给另一个线程。 由于可能发生此类中断,我们无法确保某个线程能在未受干扰的情况下成功向日志文件写入一行内容。因此,来自两个不同线程的日志可能会被混淆。这种风险很低,甚至可能为零,但我们仍决定演示如何同步两个线程对共享资源(本例中为日志文件)的访问;
  • 第 30 行:在写入之前,线程请求进入门的钥匙。所请求的钥匙即第 11 行创建的那把。它确实是唯一的:类属性对该类的所有实例而言都是唯一的;
    • 在时间点 T1,名为 Thread1 的线程获取了钥匙。随后它可以执行第 33 行;
    • 在时间点 T2,Thread1 线程在尚未完成日志写入前就被暂停;
    • 在时间点 T3,已获得处理器控制权的线程 Thread2 也必须写入日志。因此它到达第 30 行,请求前门钥匙。系统告知它另一条线程已持有该钥匙。随后它被自动暂停。所有请求此钥匙的线程都会遇到这种情况;
    • 在时间点 T4,此前被暂停的线程 Thread1 重新获得处理器控制权,随后完成日志写入;
  • 第 32–36 行:向日志文件的写入分为两个步骤:
  • 第33行:第17行获取的文件描述符关联了一个缓冲区。第33行的[write]操作将数据写入该缓冲区,而非直接写入文件。随后在满足特定条件时,缓冲区会被刷新到文件中:
        • 缓冲区已满;
        • 文件描述符被调用 [close] [flush] 操作;
  • 第 36 行:我们强制将日志行写入文件。这样做是因为我们希望看到不同线程的日志交错显示。如果不这样做,单个线程的日志将全部在同一时间写入——即第 45 行关闭描述符时。届时将很难判断某些线程是否已被停止:我们必须检查日志中的时间戳;
  • 第 39 行:Thread1 线程释放了分配给它的锁。现在该锁可以分配给另一个线程;
  • 第 22 行:因此 [write] 方法是同步的:每次只有一个线程向日志文件写入数据。该机制的关键在于第 30 行:无论发生什么情况,只有一个线程获取关键值以继续执行下一行。它会一直持有该关键值,直到将其归还(第 39 行);
  • 第 41–45 行:[close] 方法释放了分配给日志文件描述符的资源;

写入日志文件的日志内容将如下所示:

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

24.2.2. [SendAdminMail] 类

[SendAdminMail] 类允许您在应用程序崩溃时向应用程序管理员发送消息。

Image

[SendAdminMail] 类在 [config] 脚本 [2] 中配置如下:

        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        }

[SendAdminMail] 类接收第 2 至 13 行中的字典以及电子邮件发送配置。该类定义如下:

#  imports
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):
        #  sends message to smtp server config['smtp-server'] on port config[smtp-port]
        #  if config['tls'] is true, TLS support will be used
        #  mail is sent from config['from']
        #  for recipient config['to']
        #  message has subject config['subject']
        #  a logger reference can be found in config['logger']

        #  retrieve logger from config - can be None
        logger = config["logger"]
        #  server SMTP
        server = None
        #  we send the message
        try:
            #  the SMTP server
            server = smtplib.SMTP(config["smtp-server"])
            #  verbose mode
            server.set_debuglevel(verbose)
            #  secure connection?
            if config['tls']:
                #  start of safety dialogue
                server.starttls()
                #  authentication
                server.login(config["user"], config["password"])
            #  construction of a Multipart message - this is the message that will be sent
            msg = MIMEText(message)
            msg['From'] = config["from"]
            msg['To'] = config["to"]
            msg['Date'] = formatdate(localtime=True)
            msg['Subject'] = config["subject"]
            #  we send the message
            server.send_message(msg)
            #  log - the logger may not exist
            if logger:
                logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
        except BaseException as erreur:
            #  log- the logger may not exist
            if logger:
                logger.write(
                    f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
        finally:
            #  we're done - we release the resources mobilized by the function
            if server:
                server.quit()
  • 第 24-54 行:这是示例 |smtp/02| 中已介绍过的代码;
  • 第 20 行:我们获取日志记录器的引用。该引用在第 45 行和第 49 行中使用;

24.3. Web 服务器

24.3.1. 配置

该服务器的配置与之前讨论的服务器非常相似。仅 [config.py] 文件有轻微改动:

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

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

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

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  IndexController
        f"{root_dir}/impots/http-servers/01/controllers",
        #  scripts [config_database, config_layers]
        script_dir,
        #  Logger, SendAdminMail
        f"{script_dir}/../utilities",
    ]
    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config.update({
        #  users authorized to use the application
        "users": [
            {
                "login": "admin",
                "password": "admin"
            }
        ],
        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",
        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        },
        #  thread pause time in seconds
        "sleep_time": 0
    })

    #  step 3 ------
    #  database configuration
    import config_database
    config["database"] = config_database.configure(config)

    #  step 4 ------
    #  instantiation of application layers
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • 第 40–66 行:我们将与日志记录器相关的元素(第 49 行)以及与向应用程序管理员发送警报邮件相关的元素(第 51–63 行)添加到服务器的配置字典中;
  • 第 65 行:为了更直观地观察线程的运行情况,我们将强制部分线程暂停。[sleep_time] 表示暂停时长,单位为秒;
  • 第 27–28 行:请注意,我们正在使用上一版 6 中的 [index_controller]

24.3.2. 主脚本 [main]

主脚本 [main] 如下所示:

#  a mysql or pgres parameter is expected
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()

#  configure the application
import config
config = config.configure({'sgbd': sgbd})

#  dependencies
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

#  authentication manager
auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(login, password):
    #  user list
    users = config['users']
    #  browse this list
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    #  we didn't find
    return False


#  send an e-mail to the administrator
def send_adminmail(config: dict, message: str):
    #  send an e-mail to the application administrator
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)


#  check log file
logger = None
erreur = False
message_erreur = None
try:
    #  logger
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    #  log console
    print(f"L'erreur suivante s'est produite : {exception}")
    #  we note the error
    erreur = True
    message_erreur = f"{exception}"
#  store the logger in the config
config['logger'] = logger
#  error handling
if erreur:
    #  mail to administrator
    send_adminmail(config, message_erreur)
    #  end of application
    sys.exit(1)

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

#  data recovery from tax authorities
erreur = False
try:
    #  admindata will be read-only application data
    config["admindata"] = config["layers"]["dao"].get_admindata()
    #  success log
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    #  we note the error
    erreur = True
    #  error log
    log = f"L'erreur suivante s'est produite : {ex}"
    #  console
    print(log)
    #  log file
    logger.write(f"{log}\n")
    #  mail to administrator
    send_adminmail(config, log)

#  the main thread no longer needs the logger
logger.close()

#  if there has been an error, we stop
if erreur:
    sys.exit(2)

#  the Flask application can be started
app = Flask(__name__)


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


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • 第 1-10 行:脚本期望有一个 [mysql / pgres] 参数,用于指定要使用的数据库管理系统;
  • 第 12–14 行:配置应用程序(Python 路径、layers、数据库);
  • 第 16–28 行:应用程序所需的依赖项;
  • 第 30-43 行:身份验证管理;
  • 第 46–51 行:一个向应用程序管理员发送电子邮件的函数;
  • 该函数期望两个参数:
      • config:一个包含 [adminMail] [logger] 键的字典;
      • 待发送的消息;
    • 第 49–50 行:我们准备电子邮件配置;
    • 发送电子邮件;
  • 第 54–74 行:检查日志文件是否存在;
  • 第 70–74 行:如果无法打开日志文件,则向管理员发送电子邮件并退出;
  • 第 76–79 行:记录服务器启动信息;
  • 第 81–98 行:从数据库中检索税务管理数据;
  • 第 88–98 行:若无法检索到这些数据,则在控制台和日志文件中记录错误;
  • 第 100–101 行:主线程将不再进行日志记录(创建的线程不会使用相同的文件描述符);
  • 第 103–105 行:如果无法连接到数据库,则停止;
  • 第 122 行:以多线程模式启动服务器;

[index] 函数(第 114 行)如下:

#  Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[index] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  the request is executed by a controller
        résultat, status_code = index_controller.execute(request, config)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        #  we log the answer
        logger.write(f"[index] {résultat}\n")
        #  we send the answer
        return json_response(résultat, status_code)
    except BaseException as erreur:
        #  log the error if possible
        if logger:
            logger.write(f"[index] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send the answer
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • 第 4 行:当用户请求 URL / 时执行的函数。由于服务器是多线程的(第 112 行),将创建一个线程来执行该函数。该线程可能在任何时候被中断并暂停,稍后继续执行。当代码访问所有线程共享的资源时,务必牢记这一点。在此情况下,该资源即日志文件:所有线程都会向其写入数据;
  • 第 8 行:我们创建了一个日志器的实例。因此,所有线程将拥有不同的日志器实例。不过,这些日志器都指向同一个日志文件。需要注意的是,当一个线程关闭其日志器时,这不会影响其他线程的日志器;
  • 第 9–12 行:我们将日志器存储在应用程序的 [config] 字典中,键名取自线程名称。因此,如果有 n 个线程同时运行,[config] 字典中将创建 n 个条目。[config] 是所有线程共享的资源。因此,可能需要进行同步。 我在此做了一个假设。我假设如果两个线程同时在 [config] 文件中创建条目,且其中一个被另一个中断,这不会产生影响。被中断的线程稍后可以完成条目的创建。如果测试表明此假设不成立,则需要对第 12 行的访问进行同步;
  • 第 10 行:我们将日志器放入字典中;
  • 第 11 行:[threading.current_thread()] 表示正在执行本行的线程,即执行 [index] 函数的线程。我们记录其名称。每个线程都有一个唯一的名称;
  • 第 12 行:我们存储该线程的配置。从现在起,我们将始终遵循以下原则:如果存在无法在线程间共享的信息,它仍将放置在通用配置中,但会与该线程的名称相关联;
  • 第 14 行:我们记录当前正在执行的请求;
  • 第 15–24 行:我们随机暂停某些线程,以便它们将处理器让给另一个线程;
    • 第 16 行:我们从配置中获取暂停时长(以秒为单位);
    • 第 17 行:只有当暂停时长不为 0 时才会发生暂停;
    • 第 19 行:一个取值范围在 [0, 1] 内的随机整数。因此,可能的值只有 0 和 1;
    • 第 20 行:仅当随机数为 1 时,线程才会被暂停;
    • 第 22 行:记录线程即将被暂停的事实;
    • 第 24 行:线程暂停 [sleep_time] 秒;
  • 第 26 行:当线程唤醒时,由 [index_controller] 模块执行该请求;
  • 第 28–32 行:如果此次执行导致 [500 INTERNAL SERVER ERROR] 错误,则向管理员发送一封电子邮件;
    • 第 30–31 行:配置将传递给 [SendAdminMail] 类的 [config_mail] 字典;
    • 第 32 行:发送给管理员的消息是结果的 JSON 字符串,该结果将发送给客户端;
  • 第 33–34 行:我们记录将发送给客户端的响应(第 36 行);
  • 第 37–44 行:处理任何异常;
  • 第 39–40 行:如果日志器存在,则记录发生的错误;
  • 第 47–48 行:如果日志器存在,则关闭它。最终,该线程在请求开始时创建日志器,并在请求处理完成后关闭它;

24.3.3. 控制器 [index_controller]

执行请求的控制器 [index_controller] 与上一版本相同:

Image

24.3.4. 执行

我们启动 Flask 服务器、邮件服务器 |hMailServer| 以及邮件客户端 |Thunderbird|。我们不启动数据库管理系统 (DBMS)。服务器停止时,控制台输出以下日志:


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 

日志文件 [logs.txt] 内容如下:


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

使用 Thunderbird 检查管理员的电子邮件 [guest@localhost.com]

Image

然后启动数据库管理系统并请求该 URL [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]。日志内容如下:


2020-07-23 11:56:38.891753, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:38.987999, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:40.586747, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:40.655254, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:54.528360, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-23 11:56:54.530653, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
  • 第1-4行:请注意服务器启动了两次,因为[Debug=True]模式触发了第二次启动;
  • 第 5-6 行:日志让我们了解了一个请求的执行时间,这里是 2.293 毫秒;

24.4. Web 客户端

Image

通过复制 [http-clients/01] 目录来创建 [http-clients/02] 目录。随后我们进行一些修改。

24.4.1. 配置

[http-clients/02] 应用程序的 [config] 配置与 [http-clients/01] 应用程序的配置基本相同,仅有细微差异:

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

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

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

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        #  configuration scripts
        script_dir,
        #  Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration with constants
    config.update({
        #  taxpayer file
        "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
        #  results file
        "resultsFilename": f"{script_dir}/../data/output/résultats.json",
        #  error file
        "errorsFilename": f"{script_dir}/../data/output/errors.txt",
        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",
        #  tax calculation server
        "server": {
            "urlServer": "http://127.0.0.1:5000/",
            "authBasic": True,
            "user": {
                "login": "admin",
                "password": "admin"
            }
        },
        #  debug mode
        "debug": True
    }
    )

    #  step 3 ------
    #  layer instantiation
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • 第 31-32 行:我们将使用与服务器相同的日志记录器 |Logger|;
  • 第 49 行:日志文件的绝对路径;
  • 第 60 行:使用 [debug=True] 模式将 Web 服务器的响应写入日志文件;

24.4.2. [dao] 层

[ImpôtsDaoWithHttpClient] 类的代码略有更改:

#  imports

import requests
from flask_api import status




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

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  saving configuration items
        #  general configuration
        self.__config = config
        #  server
        self.__config_server = config["server"]
        #  debug mode
        self.__debug = config["debug"]
        #  logger
        self.__logger = None

    #  unused method
    def get_admindata(self) -> AdminData:
        pass

    #  tAX CALCULATION
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        #  we let the exceptions rise
        
        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")
        #  response status code HTTP
        status_code = response.status_code
        
  • 第 17 行:我们存储了通用配置。稍后我们将看到,当 [ImpôtsDaoWithHttpClient] 类的构造函数运行时,[config] 字典中尚未包含第 37 行使用的 [logger] 键。这就是为什么我们无法在构造函数中初始化 [self.__logger](第 23 行);
  • 第 21 行:我们在配置中添加了 [debug] 键,用于控制第 33–39 行中的日志记录;
  • 第 34 行:如果处于 [debug] 模式;
  • 第 36–37 行:对 [self.__logger] 属性的可选初始化。当调用 [calculate_tax] 方法时,[logger] 键已成为 [config] 字典的一部分;
  • 第 39 行:我们记录与服务器 HTTP 响应关联的文本文档;

[dao] 层将由多个线程同时执行。不过,此处我们仅创建该层的单个实例(参见 config_layers)。因此,我们必须确保代码中不涉及对共享数据的写入操作,通常指实现 [dao] 层的 [ImpôtsDaoWithHttpClient] 类的属性。然而,在上述代码中,第 37 行修改了类实例的某个属性。 在此情况下,这不会产生任何影响,因为所有线程共享同一个日志器。如果情况并非如此,则必须对第 37 行的访问进行同步。

24.4.3. 主脚本

主脚本 [main] 的演变过程如下:

#  configure the application

import config
config = config.configure({})

#  dependencies
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger


#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(dao, logger, taxpayers: list):
    


#  list of client threads
threads = []
logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  multi-threaded tax calculation for taxpayers
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        #  each thread will process from 1 to 4 contributors
        nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
        #  the list of taxpayers processed by the thread
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        #  increment i for the next thread
        i += nb_taxpayers
        #  create the thread
        thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
        #  we add it to the list of threads in the main script
        threads.append(thread)
        #  we launch the thread - this operation is asynchronous - we don't wait for the thread's result
        thread.start()
    #  the main thread waits for all threads it has launched to finish
    for thread in threads:
        thread.join()
    #  here all threads have finished their work - each has modified one or more objects [taxpayer]
    #  save the results in the jSON file
    dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        logger.close()
    #  we're done
    print("Travail terminé...")
    #  end of threads that might still exist if we stopped on error
    sys.exit()
  • 主脚本与之前的客户端脚本不同,它将生成多个执行线程来向服务器发送请求。版本 6 中的客户端是顺序发送所有请求的。只有在收到请求 #[i-1] 的响应后,才会发出请求 #i。在这里,我们想观察服务器在收到多个并发请求时的行为。为此,我们需要使用线程;
  • 第 21 行:生成的线程将被放入一个列表中。需要理解的是,[main] 脚本本身也是由一个名为 [MainThread] 的线程执行的。这个主线程将创建其他线程,这些线程负责计算一个或多个纳税人的税款;
  • 第 26 行:我们创建一个日志器。该日志器将由所有线程共享;
  • 第 32 行:我们获取所有需要计算税款的纳税人;
  • 第 39–51 行:我们将这些纳税人分配给多个线程;
  • 第 40–41 行:每个线程将处理 1 到 4 名纳税人。该数量由随机数决定;
    • [random.randint(1, 4)] 会从 [1, 2, 3, 4] 列表中随机生成一个数字;
    • 该线程中的纳税人数量不得超过 [l-i],其中 [l-i] 表示尚未被分配到线程的纳税人数量;
    • 因此我们取这两个值中的较小者;
  • 第 43 行:一旦知道 [nb_taxpayers](即该线程已处理的纳税人数量),我们就从纳税人列表中取出这些纳税人:
    • [slice(10,12)] 表示索引集合 [10, 11, 12]
    • [taxpayers[slice(10,12)]] 是列表 [taxpayers[10], taxpayers[11], taxpayers[12]];
  • 第 45 行:我们递增 i 的值,该变量控制第 39 行的循环;
  • 第 47 行:我们创建一个线程:
    • [target=thread_function] 设置线程将要执行的函数。这是第 16–17 行中的函数。它期望三个参数;
    • [ags] [thread_function] 函数所期望的三个参数的列表;

创建线程并不意味着立即执行它,它只是创建了一个对象;

  • 第 48–49 行:将刚刚创建的线程添加到主线程创建的线程列表中;
  • 第 51 行:线程被启动。随后它将与其他活动线程并行运行。在此,它将使用提供的参数执行 [thread_function]
  • 第 53–54 行:主线程等待其启动的每个线程完成。让我们举个例子:
    • 主线程已启动三个线程 [th1, th2, th3]
    • 主线程按 for 循环的顺序等待每个线程(第 53–54 行):[th1, th2, th3]
    • 假设线程按 [th2, th1, th3] 的顺序完成;
    • 主线程等待 th1 结束。当 th2 结束时,不会发生任何事情;
    • 当 th1 结束时,主线程等待 th2。然而,th2 已经结束。主线程随后转到下一个线程并等待 th3;
    • 当 th3 结束时,主线程已完成等待并继续执行第 57 行;
  • 第 57 行将结果写入结果文件。这是一个关于对象引用的好例子:
    • 第 43 行:与线程关联的 [thread_payers] 列表包含 [taxpayers] 列表中对象引用的副本;
    • 我们知道,税额计算会修改 [thread_payers] 列表中引用所指向的对象。这些对象将根据税额计算结果进行更新。然而,引用本身并未被修改。因此,初始 [taxpayers] 列表中的引用“看到”或“指向”了已被修改的对象;

线程执行的 [thread_function] 如下:

#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(dao, logger, taxpayers: list):
    #  log thread start
    thread_name = threading.current_thread().name
    logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
    #  taxpayers' taxes are calculated
    for taxpayer in taxpayers:
        #  log
        logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
        #  synchronous tax calculation
        dao.calculate_tax(taxpayer)
        #  log
        logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
    #  log end of thread
    logger.write(f"fin du thread [{thread_name}]\n")
  • 由多个线程同时执行的函数通常难以编写:你必须始终确保代码不会尝试修改线程间共享的数据。当这种情况发生时,你必须对即将被修改的共享数据实现同步访问;
  • 第 3 行:该函数接收三个参数:
    • [dao]:指向 [dao] 层的引用。该数据是共享的;
    • [logger]:对日志器的引用。该数据是共享的;
    • [taxpayers]:纳税人列表。该数据不共享:每个线程管理不同的列表;
  • 让我们来分析这两个引用 [dao, logger]
    • 我们看到,[dao] 引用所指向的对象有一个 [self.__logger] 引用,该引用会被线程修改,但修改的目的是为所有线程设置一个共同的值;
    • [logger] 引用指向一个文件描述符。我们发现向文件写入日志时可能会出现问题。因此,向文件的写入操作已被同步;
  • 第 5–6 行:我们记录线程的名称及其必须管理的纳税人数量;
  • 第 8–14 行:计算纳税人的税款;
  • 第 16 行:记录线程结束;

24.4.4. 执行

按照上一节所述启动 Web 服务器(Web 服务器、DBMS、hMailServer、Thunderbird),然后运行客户端的 [main] 脚本。在文件 [data/output/errors.txt, data/output/results.json] 中,我们得到与上一版本相同的结果。在文件 [data/logs/logs.txt] 中,我们有以下日志:


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]
  • 这些日志显示,系统启动了五个线程来计算 11 名纳税人的税款。这五个线程同时向税款计算服务器发送了请求。理解其工作原理非常重要:
    • 线程 [Thread-1] 最先启动。当它获得 CPU 资源时,会执行代码直至发送 HTTP 请求。由于必须等待该请求的结果,它会被自动挂起。随后它失去 CPU 资源,由另一个线程接管;
    • 第 1–10 行:对于这 5 个线程中的每一个,该过程都会重复。因此,在第 11 行线程 [Thread-1] 收到响应之前,这 5 个线程就已经全部启动了;
  • 线程的结束顺序与它们的启动顺序不同。因此,线程 [Thread-3] 最先结束,第 23 行;

在服务器端,文件 [data/logs/logs.txt] 中的日志如下:


2020-07-24 10:05:01.692980, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:01.877251, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:03.596162, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:03.661160, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:20.968053, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=50000' [GET]>
2020-07-24 10:05:20.969132, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.970316, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=100000' [GET]>
2020-07-24 10:05:20.970316, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.971335, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
2020-07-24 10:05:20.972563, Thread-4 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:20.974796, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=3&salaire=100000' [GET]>
2020-07-24 10:05:20.974796, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.976143, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=30000' [GET]>
2020-07-24 10:05:20.976143, Thread-6 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:21.970615, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}}}
2020-07-24 10:05:21.973914, Thread-3 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:21.977130, Thread-6 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:21.977130, Thread-5 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:22.001693, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-24 10:05:22.003013, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.003013, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=5&salaire=100000' [GET]>
2020-07-24 10:05:22.003013, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.005871, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=200000' [GET]>
2020-07-24 10:05:22.006370, Thread-9 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:22.014170, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-24 10:05:22.014170, Thread-10 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.003533, Thread-7 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}}}
2020-07-24 10:05:23.006434, Thread-8 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.018026, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=2&salaire=100000' [GET]>
2020-07-24 10:05:23.019074, Thread-11 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.021447, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=100000' [GET]>
2020-07-24 10:05:23.022447, Thread-12 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
  • 我们可以看到,11个线程处理了11名纳税人;
  • 部分线程被挂起(第6、8、12、14、20、22行),其余则未被挂起(第9、23、25、29、31行);

24.5. [DAO] 层测试

与 |上一版本| 一样,我们正在测试客户端的 [DAO] 层。其原理完全相同:

Image

测试类将在以下环境中执行:

Image

  • 配置 [2] 与我们刚刚分析过的配置 [1] 完全相同;

测试类 [TestHttpClientDao] 如下所示:

import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

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

        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  check
        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__':
    #  configure the application
    import config
    config = config.configure({})

    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]

    #  test methods are executed
    print("tests en cours...")
    unittest.main()
  • 我们为该测试创建一个 |执行配置|;
  • 我们启动 Web 服务器及其完整运行环境;
  • 运行测试;

结果如下:


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