morozov_vladimir_lab_4 is ready #174

Closed
VoldemarProger wants to merge 3 commits from morozov_vladimir_lab_4 into main
37 changed files with 729 additions and 0 deletions

1
morozov_vladimir_lab_2/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

View File

@ -0,0 +1,23 @@
version: '3'
# Объявляем сервисы
services:
worker_0: # сервис приложения для генерации данных
build: ./worker_0/ # путь к его докер файлу
volumes: # монтируем том
- data:/usr/src/myData
worker_1: # сервис 1го приложения
build: ./worker_1/ # путь к его докер файлу
volumes: # монтируем том
- data:/usr/src/myData
depends_on: # объявляем, что данный сервис запуститься только после запуска сервиса worker_0
worker_0:
condition: service_completed_successfully
worker_2: # сервис 2го приложения
build: ./worker_2/ # путь к его докер файлу
volumes: # монтируем том
- data:/usr/src/myData
depends_on:
- worker_1 # объявляем, что данный сервис запуститься только после запуска сервиса worker_1
volumes: # объявляем тома
data:

View File

@ -0,0 +1,45 @@
# Лабораторная работа №2 - разработка простейшего распределенного приложения
## Разработанные приложения
- worker_0 - программа, которая создаст папки /var/data и сгенерирует в них изначальные данные
- worker_1 - 1я программа. Вариант 4: Формирует файл /var/result/data.txt так, что каждая строка файла - количество символов в именах файлов из каталога /var/data
- worker_2 - 2я программа. Вариант 2: Ищет наименьшее число из файла /var/data/data.txt и сохраняет его третью степень в /var/result/result.txt
Все программы были разработаны на языке Python.
## DockerFile
Для каждого приложения был создан отдельный dockerfile, который позволяет нам создавать свои образы, на основе которых будут созданы контейнеры. Все файлы лежат в папках программ, описание имеет только dockerfile для `worker_0`, так как по структуре все они одинаковы
## DockerCompose
Для того, чтобы запустить все программы нам нужно создать `docker-compose.yml`, в котором определим как запускать наши сервисы. В отличие от 1й л/р в `docker-compose.yml` были добавлены команды `build`, которая позволяет нам указать путь до dockerfile нашего приложения, а также `depends_on`, которая позволяет установить порядок запусков сервисов. Более подробное описание `docker-compose.yml` находится в нем самом в качестве комментариев
## Как запустить
Для того, чтобы запустить наше распределенное приложение нужно выполнить несколько шагов
1) Запустить докер приложение. Я использую doker desktop.
2) Открыть терминал
3) перейти в папку, в которой лежит `docker-compose.yml`. Сделать это можно с помощью команды `cd`
3) Запустить команду:
```
docker-compose up --build
```
После этого будут созданые контейнеры, которые построены на основе образов, указанных в `docker-compose.yml`. В терминале будут отабражено следующее:
```[+] Running 5/5
✔ Network morozov_vladimir_lab_2_default Created
✔ Volume "morozov_vladimir_lab_2_data" Created
✔ Container morozov_vladimir_lab_2-worker_0-1 Created
✔ Container morozov_vladimir_lab_2-worker_1-1 Created
✔ Container morozov_vladimir_lab_2-worker_2-1 Created
Attaching to worker_0-1, worker_1-1, worker_2-1
worker_0-1 | Start generating data
worker_0-1 | Create dir and files
worker_0-1 exited with code 0
worker_1-1 | Start first app
worker_1-1 | Create dir
worker_1-1 exited with code 0
worker_2-1 | Start second app
worker_2-1 | 0
worker_2-1 exited with code 0
```
## Запись тестирования
Работа приложения представлена в [видео](https://disk.yandex.ru/i/0KAySyP5eaBg0g)

View File

@ -0,0 +1,55 @@
import os.path
import random
import sys
# программа для создания первоначальных данных
# кол-во создаваемых файлов
count_files = 10
# буквы для создани случайных названий файлов
letters = "a b c d e f g h i j k l m n o p q r s t u v w x y z"
# создание папки /var/data
def create_dir(path):
os.makedirs(f"{path}/var/data")
# заполнение файлов случайным кол-вом строк, состоящих из случайных чисел
def create_strings(file):
count_strings = random.randint(5,10)
for i in range(0, count_strings):
for _ in range(0,10):
file.write(f"{random.randint(0, 100)} ")
if i != count_strings-1:
file.write('\n')
# Создание файлов с данными
def create_files(path):
arr = letters.split()
with open(f"{path}/var/data/data.txt", 'w') as f:
create_strings(f)
for i in range(1, count_files):
name = ""
for _ in range(0, random.randint(3, 10)):
name += arr[random.randint(0, len(arr)-1)]
with open(f"{path}/var/data/{name}{i}.txt",'w') as f:
create_strings(f)
# проверка на наличие папок и файлов
def check_dir(path):
if os.path.exists(f"{path}/var/data"):
if os.path.isfile(f"{path}/var/data/data.txt") and len(os.listdir(f"{path}/var/data")) >= count_files:
print("Already create")
else:
print("Create files")
create_files(path)
else:
print("Create dir and files")
create_dir(path)
create_files(path)
if __name__ == '__main__' :
print("Start generating data")
path = sys.argv[1]
check_dir(path)

View File

@ -0,0 +1,8 @@
# объявляем базовый образ, на основе которого будет все построено
FROM "python:3.9-slim"
# назначаем основную рабочую директорию
WORKDIR /usr/src/app
# копируем содержимое текущей папке (скрипт питона) в контейнер, в основную рабочую папку
COPY . .
# при запуске образа выполняем команду запуска приложения
CMD [ "python", "./app.py", "/usr/src/myData"]

View File

@ -0,0 +1,30 @@
import os
import sys
# 1я программа.
# Вариант 4: Формирует файл /var/result/data.txt так,
# что каждая строка файла - количество символов в именах файлов из каталога /var/data
# Создание папки
def create_dir(path):
os.makedirs(f"{path}/var/result")
# Создание файла result и заполнение его
def create_file(path):
with open(f"{path}/var/result/data.txt", 'w') as f:
files = os.listdir(f"{path}/var/data")
for i, file in enumerate(files):
f.write(f"{file} - {len(file.replace('.txt', ''))}")
if i != len(files)-1:
f.write('\n')
# Проверка на наличие файла result
def check(path):
if not os.path.exists(f"{path}/var/result"):
print("Create dir")
create_dir(path)
create_file(path)
if __name__ == '__main__' :
print("Start first app")
path = sys.argv[1]
check(path)

View File

@ -0,0 +1,4 @@
FROM "python:3.9-slim"
WORKDIR /usr/src/app
COPY . .
CMD [ "python", "./app.py", "/usr/src/myData"]

View File

@ -0,0 +1,27 @@
import sys
# 2я Программа
# Вариант 2: Ищет наименьшее число из файла /var/data/data.txt
# и сохраняет его третью степень в /var/result/result.txt
# Основной метод, в котором открываем файл data.txt, ищем в нем наименьшее число и записываем его степень в result
def work(path):
result = 0
with open(f"{path}/var/data/data.txt", 'r') as f:
strings = f.readlines()
min = int(strings[0].replace('\n','').split()[0])
for string in strings:
arr = string.replace('\n','').split()
for num in arr:
if int(num) <= min:
min = int(num)
result = int(min)**3
with open(f"{path}/var/result/result.txt", 'w') as f:
f.write(f"{result}")
print(result)
if __name__ == '__main__' :
print("Start second app")
path = sys.argv[1]
work(path)

View File

@ -0,0 +1,4 @@
FROM "python:3.9-slim"
WORKDIR /usr/src/app
COPY . .
CMD [ "python", "./app.py", "/usr/src/myData"]

0
morozov_vladimir_lab_3/.gitignore vendored Normal file
View File

View File

@ -0,0 +1,18 @@
version: '3'
# Объявляем сервисы
services:
artists_srv: # сервис приложения артистов
build: ./service_artists/
paints_srv: # сервис приложения картин
build: ./service_paints/
gateway: # сервис nginx
image: nginx:latest # назначаем его образ
ports: # пробрасываем порта
- 8080:8080
volumes: # перекидываем конфигурационный файл
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on: # ожидаем запуска предыдущих сервисов
- artists_srv
- paints_srv

View File

@ -0,0 +1,39 @@
events {
worker_connections 1024;
}
http
{
server {
listen 8080;
listen [::]:8080;
server_name localhost;
location /artists/ {
proxy_pass http://artists_srv:8001/artists/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /artists/docs {
proxy_pass http://artists_srv:8001/docs;
proxy_set_header Host $host;
}
location /paints/docs {
proxy_pass http://paints_srv:8002/docs;
proxy_set_header Host $host;
}
location /paints/ {
proxy_pass http://paints_srv:8002/paints/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}

View File

@ -0,0 +1,26 @@
# Лабораторная работа №3 - REST API, Gateway и синхронный обмен между микросервисами
## Разработанные приложения
В рамках л/р были разработаны 2 приложения:
1) Сервис для работы с сущностью художник(artist). Сущность имеет следующие поля:
1) uid (номер)
2) name (ФИО)
3) year (год рождения)
2) Сервис для работы с сущностью картина(paint). Сущность имеет следующие поля:
1) uid (номер)
2) title (название)
3) year (год создания)
4) author (номер художника)
Один художник(один) может создать несколько картин(многие)
## Приготовления
Для обеспечения нужной работоспособности нашего приложения был использован веб-сервер Nginx, который можем использовать в роли прокси сервера.
Для этого нужно было развернуть его в отдельном контейнере и настроить его конфигурационный файл, в которым мы описывали прослушиваемые адреса и куда нужно перенаправлять.
Сервисы развернули в обычном порядке: написали код приложения, создали докер-файл, записали в докер-композе.
## Запуск
Для запуска приложения используем команду:
```
docker-compose up --build
```
## Запись тестирования
Работа приложения представлена в [видео](https://disk.yandex.ru/i/8yuoSm0QSR9wGw)

View File

@ -0,0 +1,77 @@
import sys
import uuid
import uvicorn
from fastapi import FastAPI, Body
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
app = FastAPI(title="Artists service")
# Сервис для работы с художниками
# класс сущности художник
class artist:
def __init__(self, name, year, uid=""):
if len(uid) == 0:
self.uid = str(uuid.uuid4())
else:
self.uid = uid
self.name = name
self.year = year
# Первоначальные данные
artists = [
artist("Vincent van Gogh","1853","d1c02854-76d8-4a35-be5c-076f091a67c0"),
artist("Claude Monet","1840","41e2dadc-1aa8-4f14-b1e8-7fc5664d1cdf"),
artist("Ilya Repin","1844", "591f8c2f-1937-44a1-b1f8-ff1f8b52cece")]
# Получение всех художников
@app.get("/artists/")
async def get_all():
json_data = jsonable_encoder(artists)
return JSONResponse(json_data)
# Получение конкретного художника
@app.get("/artists/{uid}")
def get_uid(uid):
for art in artists:
if art.uid == uid:
json_data = jsonable_encoder(art)
return JSONResponse(json_data)
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
# Создание нового художника
@app.post("/artists/")
def create(name = Body(embed=True), year = Body(embed=True)):
for art in artists:
if art.name.lower() == name.lower():
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
new_artist = artist(name,year)
artists.append(new_artist)
json_data = jsonable_encoder(new_artist)
return JSONResponse(json_data)
# Изменение данных о художники
@app.put("/artists/{uid}")
def update(uid, name = Body(embed=True), year = Body(embed=True)):
for art in artists:
if uid == art.uid:
art.name = name
art.year = year
json_data = jsonable_encoder(art)
return JSONResponse(json_data)
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
# Удаление художника
@app.delete("/artists/{uid}")
def delete(uid):
ind = 0
find = False
for art in artists:
if art.uid == uid:
find = True
break
ind += 1
if find:
artists.pop(ind)
return JSONResponse(content={"message": "Resource Deleted"}, status_code=200)
else:
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
if __name__ == '__main__' :
uvicorn.run("app:app",host="0.0.0.0", port=8001,)

View File

@ -0,0 +1,10 @@
# объявляем базовый образ, на основе которого будет все построено
FROM "python:3.9-slim"
# назначаем основную рабочую директорию
WORKDIR /usr/src/app
# копируем содержимое текущей папке (скрипт питона) в контейнер, в основную рабочую папку
COPY . .
# установка req
RUN pip install -r req.txt
# при запуске образа выполняем команду запуска приложения
CMD [ "python", "app.py"]

View File

@ -0,0 +1,4 @@
fastapi[all]
uuid
uvicorn
requests

View File

@ -0,0 +1,95 @@
import sys
import uuid
import requests
import uvicorn
from fastapi import FastAPI, Body
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
app = FastAPI(title="Paints service")
# Сервис для работы с картинами
# Класс картины
class paint:
def __init__(self, title,year,author):
self.uid = str(uuid.uuid4())
self.title = title
self.year = year
self.author = author
# Первоначальные данные
paints = [
paint("Sunflowers","1887", "d1c02854-76d8-4a35-be5c-076f091a67c0"),
paint("Self-portrait with a severed ear and a tube","1889", "d1c02854-76d8-4a35-be5c-076f091a67c0"),
paint("Boatmen on the Volga","1873", "591f8c2f-1937-44a1-b1f8-ff1f8b52cece")]
# Получение всех картин
@app.get("/paints/")
async def get_all():
json_data = jsonable_encoder(paints)
return JSONResponse(json_data)
# Получение конкретной картины
@app.get("/paints/{uid}")
def get_uid(uid):
for art in paints:
if art.uid == uid:
response = requests.get(f"http://artists_srv:8001/artists/{art.author}")
author = response.json()
result = {
"uid":art.uid,
"title":art.title,
"year":art.year,
"authorUid":art.author,
"author":author
}
json_data = jsonable_encoder(result)
return JSONResponse(json_data)
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
# Создание новой картины
@app.post("/paints/")
def create(title = Body(embed=True), year = Body(embed=True), author = Body(embed=True)):
for art in paints:
if art.title.lower() == title.lower():
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
response = requests.get(f"http://artists_srv:8001/artists/{author}")
if response.status_code == 404:
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
new_paint = paint(title,year, author)
paints.append(new_paint)
json_data = jsonable_encoder(new_paint)
return JSONResponse(json_data)
# Обновление данных о картине
@app.put("/paints/{uid}")
def update(uid, title = Body(embed=True), year = Body(embed=True), author = Body(embed=True)):
response = requests.get(f"http://artists_srv:8001/artists/{author}")
print(author)
print(response.status_code)
if response.status_code == 404:
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
for art in paints:
if uid == art.uid:
art.title = title
art.year = year
art.author = author
json_data = jsonable_encoder(art)
return JSONResponse(json_data)
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
# Удаление картины
@app.delete("/paints/{uid}")
def delete(uid):
ind = 0
find = False
for art in paints:
if art.uid == uid:
find = True
break
ind += 1
if find:
paints.pop(ind)
return JSONResponse(content={"message": "Resource Deleted"}, status_code=200)
else:
return JSONResponse(content={"message": "Resource Not Found"}, status_code=404)
if __name__ == '__main__' :
uvicorn.run("app:app", host="0.0.0.0", port=8002,)

View File

@ -0,0 +1,10 @@
# объявляем базовый образ, на основе которого будет все построено
FROM "python:3.9-slim"
# назначаем основную рабочую директорию
WORKDIR /usr/src/app
# копируем содержимое текущей папке (скрипт питона) в контейнер, в основную рабочую папку
COPY . .
# установка зависимостей
RUN pip install -r req.txt
# при запуске образа выполняем команду запуска приложения
CMD [ "python", "app.py"]

View File

@ -0,0 +1,4 @@
fastapi[all]
uuid
uvicorn
requests

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,24 @@
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.basic_qos(prefetch_count=1)
channel.exchange_declare(exchange='grade_book', exchange_type='fanout')
result = channel.queue_declare(queue='cool_student', exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange='grade_book', queue=queue_name)
print(' [*] Waiting for logs. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(f" [x] Receive: {body}")
print(f" [+] Complete: {body}")
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_consume(
queue=queue_name, on_message_callback=callback)
channel.start_consuming()

View File

@ -0,0 +1,31 @@
import random
import time
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.basic_qos(prefetch_count=1)
channel.exchange_declare(exchange='grade_book', exchange_type='fanout')
result = channel.queue_declare(queue='normal_student', durable=True)
queue_name = result.method.queue
channel.queue_bind(exchange='grade_book', queue=queue_name)
print(' [*] Waiting for tasks. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(f" [x] Receive: {body}")
time.sleep(random.randint(2,4))
print(f" [+] Complete: {body}")
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_consume(
queue=queue_name, on_message_callback=callback)
channel.start_consuming()

View File

@ -0,0 +1,25 @@
import sys
import time
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='grade_book',
exchange_type='fanout')
count = 0
while True:
count+= 1
time.sleep(1)
message = f"Homework #{count}"
channel.basic_publish(exchange='grade_book',
routing_key='',
body=message,
properties=pika.BasicProperties(
delivery_mode=pika.DeliveryMode.Persistent
))
print(f" [x] Sent {message}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@ -0,0 +1,25 @@
import pika, sys, os
def main():
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')
def callback(ch, method, properties, body):
print(f" [x] Received {body}")
channel.basic_consume(queue='hello', on_message_callback=callback, auto_ack=True)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('Interrupted')
try:
sys.exit(0)
except SystemExit:
os._exit(0)

View File

@ -0,0 +1,11 @@
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='hello')
channel.basic_publish(exchange='', routing_key='hello', body='Hello World!')
print(" [x] Sent 'Hello World!'")
connection.close()

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

View File

@ -0,0 +1,18 @@
import sys
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='task_queue', durable=True)
message = ' '.join(sys.argv[1:]) or "Hello World!"
channel.basic_publish(exchange='',
routing_key="task_queue",
body=message,
properties=pika.BasicProperties(
delivery_mode = pika.DeliveryMode.Persistent
))
print(f" [x] Sent {message}")

View File

@ -0,0 +1,29 @@
import time
import pika, sys, os
def main():
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.basic_qos(prefetch_count=1)
channel.queue_declare(queue='task_queue', durable=True)
def callback(ch, method, properties, body):
print(f" [x] Received {body.decode()}")
time.sleep(body.count(b'.'))
print(" [x] Done")
ch.basic_ack(delivery_tag=method.delivery_tag)
channel.basic_consume(queue='task_queue', on_message_callback=callback)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('Interrupted')
try:
sys.exit(0)
except SystemExit:
os._exit(0)

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

@ -0,0 +1,22 @@
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs', exchange_type='fanout')
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange='logs', queue=queue_name)
print(' [*] Waiting for logs. To exit press CTRL+C')
def callback(ch, method, properties, body):
print(f" [x] {body}")
channel.basic_consume(
queue=queue_name, on_message_callback=callback, auto_ack=True)
channel.start_consuming()

View File

@ -0,0 +1,20 @@
import sys
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs',
exchange_type='fanout')
message = ' '.join(sys.argv[1:]) or "Hello World!"
channel.basic_publish(exchange='logs',
routing_key='',
body=message,
properties=pika.BasicProperties(
delivery_mode=pika.DeliveryMode.Persistent
))
print(f" [x] Sent {message}")

View File

@ -0,0 +1,44 @@
# Лабораторная работа №4 - Работа с брокером сообщений
## Прохождение уроков
Урок №1.
![img.png](lesson_1%2Fimg.png)
Урок №2.
![img.png](lesson_2%2Fimg.png)
Урок №3.
![img.png](lesson_3%2Fimg.png)
Все приложения были разработаны на Python
## Разработанные приложения
В качестве предметной области было выбрано общение учителя и его учеников. Учитель дает задание ученикам, а они их выполняют. Я выделил 2 вида учеников: обычные, которым нужно некоторое время на то, чтобы выполнить задание; крутые ученики, которые выполняют задание моментально, как только получают его.
Были созданы следующие приложения:
- teacher - программа, которая представляет учителя и отправляет задания ученикам
- Student_Normal - программа, которая представялет обычного ученика, получает и обрабатывает, с задержкой, полученные задания.
- Student_Cool - программа, которая представялет крутого ученика, получает и моментально обрабатывает полученные задания.
Все программы были разработаны на языке Python.
Проведенные тесты:
#### Тест №1. Запущены 1 учитель, 1 обычный ученик, 1 крутой ученик:
![FirstTest.png](Teacher_student_message%2FFirstTest.png)
Показатели очереди обычного ученика:
![FirstTest_Normal.png](Teacher_student_message%2FFirstTest_Normal.png)
Показатели очереди крутого ученика:
![FirstTest_Cool.png](Teacher_student_message%2FFirstTest_Cool.png)
Вывод: одного обычного ученика не хватает на то, чтобы выполнять все полученные в срок, ему тяжело :(
Крутому же ученику все дается с легкостью.
#### Тест №2. Запущены 1 учитель, 4 обычных ученика:
![SecondTest.png](Teacher_student_message%2FSecondTest.png)
Показатели очереди обычного ученика:
![FirstTest_Normal.png](Teacher_student_message%2FFirstTest_Normal.png)
Вывод: если 4 обычных ученика объединяться и будут делать задания вместе, то они смогут избежать переполнения очереди заданий.
## Запись тестирования
Работа приложения представлена в [видео](https://disk.yandex.ru/i/zzwvXXpZhavh7A)