|
import asyncio |
|
import random |
|
from datetime import datetime |
|
from enum import Enum |
|
from dataclasses import dataclass |
|
|
|
|
|
class CMD_STATE(Enum): |
|
Undefined = 0 |
|
Ask = 1 |
|
In_Progress = 2 |
|
Get = 3 |
|
|
|
|
|
@dataclass |
|
class CommandClient: |
|
""" Une commande pour un client contient : |
|
- un burger |
|
- des frites |
|
- un soda |
|
""" |
|
burger: CMD_STATE |
|
frites: CMD_STATE |
|
soda: CMD_STATE |
|
|
|
@staticmethod |
|
def _format(value): |
|
"""Renvoi une représentation textuel de l'état d'une sous-commande (burger, frites ou soda)""" |
|
if value == CMD_STATE.Ask: |
|
return "+" |
|
elif value == CMD_STATE.In_Progress: |
|
return "o" |
|
elif value == CMD_STATE.Get: |
|
return "-" |
|
else: |
|
return " " |
|
|
|
def __init__(self, burger=CMD_STATE.Undefined, frites=CMD_STATE.Undefined, soda=CMD_STATE.Undefined): |
|
"""Constructeur""" |
|
super().__init__() |
|
self.burger = burger |
|
self.frites = frites |
|
self.soda = soda |
|
|
|
def __repr__(self): |
|
"""Renvoi une représentation textuel de l'état d'une commande au format 'BFS' (burger, frites, soda)""" |
|
return CommandClient._format(self.burger) + \ |
|
CommandClient._format(self.frites) + \ |
|
CommandClient._format(self.soda) |
|
|
|
def do_release_finished(self): |
|
"""Clôture les sous-commandes terminée""" |
|
def do_update(param): |
|
if param == CMD_STATE.Get: |
|
return CMD_STATE.Undefined |
|
return param |
|
|
|
self.burger = do_update(self.burger) |
|
self.frites = do_update(self.frites) |
|
self.soda = do_update(self.soda) |
|
|
|
|
|
class FastFood: |
|
"""Classe gérant la préparation de commande du fastfood |
|
|
|
Attributes: |
|
_soda_lock : mutex, only one soda machine |
|
_burger_semaphore : semaphore = 3, only 3 servers |
|
_fries_counter : remaining portions |
|
_fries_lock : mutex, only one deep fryer |
|
_last_comment : last message |
|
""" |
|
|
|
_soda_lock = asyncio.Lock() |
|
_burger_semaphore = asyncio.Semaphore(3) |
|
_fries_counter = 0 |
|
_fries_lock = asyncio.Lock() |
|
_last_comment = "" |
|
|
|
def __init__(self, name, nb_commande): |
|
"""Constructeur |
|
- name : nom du fastfood |
|
- nb_commande : nombre de commande total |
|
""" |
|
# Nom du fastfood |
|
self.name = name |
|
# lancement de la boucle par défaut |
|
self.loop = asyncio.new_event_loop() |
|
# initialisation du tableau des commandes "vides" |
|
self.cmd_clients = [CommandClient()] * nb_commande |
|
# lancement du timer |
|
self.start_timer = datetime.now() |
|
# affichage de l'entete de sortie |
|
self._print_line(True) |
|
|
|
def _print_line(self, is_header=False): |
|
""" |
|
Affiche le tableau de résultat ligne par ligne |
|
Si is_header alors affiche la légende et l'entête |
|
""" |
|
# affichage de l'entete de sortie |
|
if is_header: |
|
# nombre de client |
|
nb_clients = len(self.cmd_clients) |
|
# taille d'une ligne (hors préfixe d'horodatage) |
|
raw_len = nb_clients * 4 + 1 |
|
# espace qui sera utilisé pour afficher les temps d'exécution |
|
prefix_time = "\n" + " " * 14 |
|
# legende |
|
legende = "(B)urger | (F)rites | (S)oda" |
|
# header 1 : colonnes de 3 chr : id par client |
|
hd1 = "|" + "|".join(map(str, [f"{x:^3}" for x in range(1, nb_clients + 1)])) + "|" |
|
# header 2 : sous-colonnes Burger/Frites/Soda par client |
|
hd2 = "|BFS" * nb_clients + "|" |
|
# header 3 : ligne vide pour timer |
|
hd3 = " |" + "|".join(map(str, [f" "] * nb_clients)) + "|" |
|
# concat header |
|
raw = prefix_time + f"{legende:^{raw_len}}" + \ |
|
prefix_time + hd1 + \ |
|
prefix_time + hd2 + \ |
|
prefix_time + "=" * raw_len + \ |
|
"\n" + self._get_tick() + hd3 |
|
|
|
else: |
|
# Etat de la commande pour chaque client |
|
# en colonnes : (B)urger | (F)rites | (S)oda |
|
try : |
|
raw = self._get_tick() + " |" + "|".join(map(str, self.cmd_clients)) + '|' + self._last_comment |
|
self._last_comment = "" |
|
except Exception as e: |
|
print(type(e), e) |
|
exit(1) |
|
|
|
print(raw) |
|
|
|
def _get_tick(self): |
|
""" |
|
Renvoi la période écoulé depuis l'ouverture (string formaté) |
|
""" |
|
duration = datetime.now() - self.start_timer |
|
return f"{duration.seconds:>3}s + {duration.microseconds // 1000:03d}ms" |
|
|
|
async def _client_change_state(self, client, burger=CMD_STATE.Undefined, frites=CMD_STATE.Undefined, |
|
soda=CMD_STATE.Undefined, notify=True): |
|
""" |
|
Change l'état de la commande d'un client dans le tableau cmd_clients |
|
Pour chaque paramètre (burger, frites, soda) de chaque client |
|
Voir CMD_STATE |
|
Undefined = 0 : pas de commande en cours |
|
Ask = 1 : commande initiée |
|
In_Progress = 2 : commande en cours de préparation |
|
Get = 3 : commande terminée |
|
notify |
|
On affiche le changement si True |
|
|
|
ALGO : |
|
1 - Pour chaque changement d'état et pour l'ensemble des clients : |
|
les valeurs Get(3) passent à Undefined(0) |
|
|
|
2 - Pour client en param, pour chaque argument (burger, frites, soda) : |
|
si argument != Undefined(0) |
|
alors affectation de l'argument |
|
|
|
3 - on affiche la ligne complète |
|
""" |
|
|
|
# 1 on remet les états à jour |
|
for clt in self.cmd_clients: |
|
clt.do_release_finished() |
|
|
|
# 2 on modifie l'état de la commande du client passé en paramètre |
|
burger = burger if burger != CMD_STATE.Undefined else self.cmd_clients[client].burger |
|
frites = frites if frites != CMD_STATE.Undefined else self.cmd_clients[client].frites |
|
soda = soda if soda != CMD_STATE.Undefined else self.cmd_clients[client].soda |
|
self.cmd_clients[client] = CommandClient(burger, frites, soda) |
|
|
|
# on affiche la ligne |
|
if notify : |
|
self._print_line() |
|
|
|
async def _get_soda(self, client): |
|
""" |
|
Tâche _get_soda : préparation d'un soda pour un client |
|
Contexte : Une seule machine à soda, on ne peut en faire qu'un à la fois |
|
""" |
|
# Acquisition du verrou |
|
# la syntaxe 'async with FOO' peut être lue comme 'with (yield from FOO)' |
|
# print(" > Commande du soda pour {}".format(client)) |
|
await self._client_change_state(client, soda=CMD_STATE.Ask) |
|
async with self._soda_lock: |
|
# Une seule tâche à la fois peut exécuter ce bloc |
|
self._last_comment = f"** Préparation du soda du client {client}" |
|
await self._client_change_state(client, soda=CMD_STATE.In_Progress, notify=False) |
|
# print(" X Remplissage du soda pour {}".format(client)) |
|
await asyncio.sleep(1) |
|
await self._client_change_state(client, soda=CMD_STATE.Get) |
|
# print(" < Le soda de {} est prêt".format(client)) |
|
|
|
async def _get_burger(self, client): |
|
""" |
|
Tâche _get_burger : préparation d'un burger |
|
Contexte : 3 serveurs, on ne peut faire qu'un burger par serveur |
|
""" |
|
await self._client_change_state(client, burger=CMD_STATE.Ask) |
|
# print(" > Commande du burger en cuisine pour {}".format(client)) |
|
async with self._burger_semaphore: |
|
# accès à _burger_semaphore._value comme un malpropre (valeur non accessible autrement) |
|
self._last_comment = f"** Préparation du burger du client {client} - " \ |
|
f"{self._burger_semaphore._value} serveur(s) disponible(s)" |
|
await self._client_change_state(client, burger=CMD_STATE.In_Progress, notify=False) |
|
# print(" X Préparation du burger pour {}".format(client)) |
|
await asyncio.sleep(3) |
|
await self._client_change_state(client, burger=CMD_STATE.Get) |
|
# print(" < Le burger de {} est prêt".format(client)) |
|
|
|
async def _get_fries(self, client): |
|
""" |
|
Tâche _get_fries : préparation des frites |
|
Contexte : |
|
- le bac à frite contient 5 portions |
|
- une fois vide, la cuisson d'un nouveau bac prend 4 secondes |
|
""" |
|
await self._client_change_state(client, frites=CMD_STATE.Ask) |
|
# print(" > Commande des frites pour {}".format(client)) |
|
await self._client_change_state(client, frites=CMD_STATE.In_Progress, notify=False) |
|
async with self._fries_lock: |
|
if self._fries_counter == 0: |
|
self._last_comment = "** Démarrage de la cuisson des frites" |
|
# print(f"{self._get_tick()} ** Démarrage de la cuisson des frites") |
|
await asyncio.sleep(4) |
|
self._fries_counter = 5 |
|
# print(f"{self._get_tick()} ** Les frites sont cuites") |
|
self._last_comment = "** Les frites sont cuites" |
|
self._fries_counter -= 1 |
|
self._last_comment = f"** les frites du client {client} - " \ |
|
f"{self._fries_counter} portions restantes" |
|
await self._client_change_state(client, frites=CMD_STATE.Get) |
|
# print(" < Les frites de {} sont prêtes".format(client)) |
|
|
|
async def nv_commande(self, client, delay): |
|
""" |
|
Tâche nv_commande : nouvelle commande passée |
|
Elle va initié les sous-commandes qui en découlent (burger, frites, soda) |
|
""" |
|
try : |
|
if not isinstance(client,int) : |
|
raise TypeError("Client doit être de type 'int'") |
|
if client > len(self.cmd_clients)-1 : |
|
raise Exception("Client doit correspondre à l'ordre d'arrivée (index dans la file)") |
|
if not isinstance(delay,(int,float)) : |
|
raise TypeError("Delay represente le délai entre l'ouverture et l'arrivée du client, " |
|
"doit être de type 'int' ou 'float'") |
|
except Exception as e: |
|
print("\n", type(e)) |
|
print("\n", e) |
|
print("\n\nSortie en erreur") |
|
exit(1) |
|
|
|
await asyncio.sleep(delay) |
|
start_time = datetime.now() |
|
print( self._get_tick(), "=> Commande passée par {}".format(client)) |
|
await asyncio.wait( |
|
[ |
|
self._get_soda(client), |
|
self._get_fries(client), |
|
self._get_burger(client) |
|
] |
|
) |
|
total = datetime.now() - start_time |
|
print(self._get_tick(), "<= {} servi en {}".format(client, datetime.now() - start_time)) |
|
return total |
|
|
|
# ### ancienne méthode ### |
|
# async def on_ferme(self): |
|
# """Tâche on_ferme : plus de commande, on ferme le fastfood""" |
|
# while True: |
|
# await asyncio.sleep(1) |
|
# if len(asyncio.Task.all_tasks()) == 1: |
|
# print("Plus personne, on ferme !") |
|
# asyncio.get_event_loop().stop() |
|
|
|
|
|
def main(): |
|
random.seed() |
|
# nombre de commande à réaliser |
|
nb_cmd = 3 |
|
|
|
# simulation des arrivées clients (délai aléatoire pour chacun d'entre eux) |
|
delay = [random.randint(1, 50) / 10 for x in range(nb_cmd)] |
|
delay.sort() |
|
print("\n\ndélai d'arriver des clients en seconde : \n\t\t", delay) |
|
|
|
# initialisation de notre objet coroutine |
|
ff = FastFood("BestOf", nb_cmd) |
|
|
|
# on passe les commandes (création des tâches dans la boucle d'événement) |
|
tasks = [ff.nv_commande(num_commande, delay[num_commande]) for num_commande in range(nb_cmd)] |
|
|
|
# ### ancienne méthode ### |
|
# for num_commande in range(nb_cmd): |
|
# asyncio.ensure_future(ff.nv_commande(num_commande, delay[num_commande])) |
|
# on passe la tâche en charge de la fermeture (si pluys de commande) |
|
# asyncio.ensure_future(ff.on_ferme()) |
|
|
|
try: |
|
# on lance la boucle |
|
|
|
# ### ancienne méthode ### |
|
# asyncio.get_event_loop().run_forever() |
|
asyncio.get_event_loop().run_until_complete(asyncio.gather(*tasks)) |
|
print("Plus personne ? on ferme !") |
|
except KeyboardInterrupt as e: |
|
print("Barrez-vous ! c'est fermé et tant pis pour vos comandes !") |
|
except Exception as e: |
|
print("OUPS", type(e), e) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |
|
|
self.loop = asyncio.new_event_loop()
: Ne crée pas ta propre boucle asyncio car une seule peut tourner en même temps, si tu crée la tienne, et que tu veux faire tourner ta lib dans un programme qui a deja sa boucle, tu ne sera pas compatible. Prend juste la boucle actuelle.