Этот проект - работа за 5-й спринт Яндекс Лицея. Он представляет из себя суперпупермега калькулятор, который содержит 3 микросервиса однако это больше соответсвует SOA архитекутре чем микросервисной, так как все 3 микросервиса работают от одной базы данных.
- Агент - посылает по gRPC запросы на оркестратор для получения выражений (Server-Streaming)
- Оркестратор1 - отдаёт выражения агенту по gRPC, если таковые имеются (Polling)
- Оркестратор2 - служит для взаимодействия с пользователем
По шагам:
- Открываете VS Code/Goland
- Добавляете .env файл в директорию
internal/orchestrator - Настраиваете зависимости в .env файле: Пример приведён ниже:
TIME_ADDITION_MS = #время выполнения операции сложения в миллисекундах
TIME_SUBTRACTION_MS = #время выполнения операции вычитания в миллисекундах
TIME_MULTIPLICATIONS_MS = # время выполнения операции умножения в миллисекундах
TIME_DIVISIONS_MS = #время выполнения операции деления в миллисекундах
PORT_ORCHESTRATOR = # Порт на котором запускается оркестратор
JWT_SECRET = #указывается секретная строка для подписи токена
TOKEN_EXPIRY = #указывается через сколько исткекает токен в часах
REFRESH_EXPIRY = #указывается через сколько обновляется токен в часах- Далее вам нужно создать 2 терминала. В VS code - это сочетание клавиш
Ctrl+Shift+5, в Goland придётся просто открыть 2 вкладки терминала:View→Tool Windows→Terminal(или нажмитеAlt + F12). - Переходите в директории, где лежат main файлы для агента и для оркестратора:
cd .\cmd\agentиcd .\cmd\orchestrator - Далее компилируете main файлы и запускаете их в разных терминалах соответственно
Команды для запуска main:
go build main.gogo run main.go
- Готово, если у вас всё получилось, раз в несколько секунд вы увидите логи Агента о выполнении запросов, а Оркестратор покажет на каком порту запустились его 2 микросервиса. Один микросервис общается по
gRPCс агентом, второй же обрабатывает запросы пользователя.- Это сообщение вы увидите после запуска:
После запуска Агента и Оркестратора вы наконец-то можете делать запросы на сервер.
Для начала использования сервиса необходимо пройти регистрацию. Таким образом система будет знать, что такой пользователь есть в её базе данных. Создайте запрос в удобном вам формате: (Советую перейти на postman, так как он запоминает запросы, и не нужно какждый раз это переписывать)
curl --location 'http://localhost:<ВАШ_ПОРТ>/api/v1/register' \
--header 'Content-Type: application/json' \
--data '{
"email": "<ВАШ_EMAIL>",
"password": "<ВАШ_ПАРОЛЬ>"
}'Чтобы выполнять запросы на любые handlerы необходимо войти в систему, получить свой личный jwt-токен и отправлять его дальше в заголовках запроса.
Создадим запросы в удобном для нас формате:
curl --location 'http://localhost:<ВАШ_ПОРТ>/api/v1/login' \
--header 'Content-Type: application/json' \
--data '{
"email": "<ВАШ_EMAIL>",
"password": "<ВАШ_ПАРОЛЬ>"
}'Этот пункт я решил выделить из-за его важности для проведения дальнейшних запросов.
- Полученные после
loginтокены необходимо сохранить (в тг Saved Messages закиньте 😁)и передавать в каждом запросе, так как передо мной не стояло задачи реализовать cookie для сохранения токенов. - Чтобы использовать токен в
Postmanнеобходимо передавать его в соответствующей вкладке, на каринке ниже представлено где именно:
Выражение добавляется в хранилище выражений Expressions_storage, которые в порядке очереди будут поступать в Оркестратор.
Для добавления выражения создайте запрос:
curl --location 'http://localhost:<ВАШ_ПОРТ>/api/v1/calculate' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <ВАШ_ТОКЕН>' \
--data '{
"expression": "<ВАШЕ_ВЫРАЖЕНИЕ>"
}'- Обязательно после
localhost- укажите значение порта, на котором у вас запускается сервер. Это значение указывается в.envфайле. - В expression добавьте строку выражения в формате
"x+y*z...". - Добавьте ваш токен в заголовок авторизации.
Сверху - выражение, снизу ответ Оркестратора
curl -X GET http://localhost:<ВАШ_ПОРТ>/api/v1/expressions' \
-H "Authorization: Bearer <ВАШ_ТОКЕН>"- Обязательно после
localhost- укажите значение порта, на котором у вас запускается сервер. Это значение указывается в.envфайле.
Сверху - выражение, снизу ответ Оркестратора
curl --location 'localhost:<ВАШ_ПОРТ>/api/v1/expressions/:id' \
-H "Authorization: Bearer <ВАШ_ТОКЕН>"- Обязательно после
localhost- укажите значение порта, на котором у вас запускается сервер. Это значение указывается в.envфайле. - Поле
:idв запросе поменяйте на именование id выражения, которое вы хотите достать
curl --location 'localhost:<ВАШ_ПОРТ>/api/v1/profile' \
-H "Authorization: Bearer <ВАШ_ТОКЕН>"- Ошибиться в этом запросе можно только в передаче токена, поэтому не вижу смысла прикреплять пример с ошибкой.
curl --location 'localhost:<ВАШ_ПОРТ>/api/v1/refresh-token' \
-H "Authorization: Bearer <ВАШ_ТОКЕН>"- Ошибиться в этом запросе можно только в передаче токена, поэтому не вижу смысла прикреплять пример с ошибкой.
- Пользователь делает запрос, отправляя свои
emailиpassword - Они парсятся и проходят проверку на валидность
- Далее проверяется есть ли уже такой пользователь в базе данных
- Затем хэшируется пароль
- Открывается транзакция на вставку данных в БД
- Если всё успешно, то транзакция закрывается и выводится сообщение об успешной регистрации
Реализацию можете глянуть в internal/database/handlers/handlers.go.
Сигнатура:
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) - Пользователь делает запрос, отправляя свои
emailиpassword - Они парсятся и проходят проверку на валидность
- Далее проверяется есть ли уже такой пользователь в базе данных
- Пароль сверяется с его захешированной версией
- Если всё удачно, то создаётся
jwtтокен, который отправляется пользователю
Реализацию можете глянуть в internal/database/handlers/handlers.go.
Сигнатура:
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) - Пользователь делает запрос
- Middleware проверяет присутствует ли токен, если это защищённая ручка.
- Middleware разбирает этот токен, после чего в контексте передаёт всем защищённым ручкам идентификатор пользователя и его email.
- Выражение парсится из этого запроса в данные, которые можно запихнуть в структуру
- Ну и соответственно эта структура - массивчик, из которого мы по мере необходимости будем отдавать выражения на развыраженивание (разбиение на параллельные задачи), а затем эти подзадачки будем отдавать агенту, но об этом позже.
Вот так выглядит массив в который парсятся выражения
type Expressions_parametres struct {
ID string `json:"id"`
Status string `json:"status"`
Result string `json:"result"`
}
type Expressions_storage struct {
Expressions []Expressions_parametres `json:"expressions"`
}
// массивчик с выражениями от пользователя
var Expressions_storage_variable Expressions_storage- Это выражение добавляется в базу данных для конкретного пользователя (повторюсь, id взяли из контекста)
- Выражение получает свой уникальный идентификатор, который создаётся при помощи
uuid - Ставится статус
StatusCreated - В
Resultпока записывается просто само выражение, а не его результат
Реализацию можете глянуть в \internal\orhestrator\orchestrator.go, сигнатура handlerа:
func OrchestratorHandler(w http.ResponseWriter, r *http.Request)- Пользователь делает запрос
- Middleware проверяет есть ли токен и валиден ли он
- Middleware парсит из
jwtи передаётidпользователя в обработчик по адресуlocalhost:<ВАШ_АДРЕС>/api/v1/expressions - Далее делается запрос в базу данных на получение выражений пользователя.
- Отдаём пользователю все выражения с их
id, уравнением и результатами
Реализацию можете глянуть в \internal\orhestrator\orchestrator.go, сигнатура handlerа:
func GetExpressionsHandler(w http.ResponseWriter, r *http.Request) Маленькая поправка: Если выражение посчиталось, то будет записан его результат, если выражение не посчиталось, то будет записано само выражение.
- Пользователь делает запрос
- Middleware проверяет есть ли токен и валиден ли он
- Middleware парсит из
jwtи передаётidпользователя в обработчик по адресуlocalhost:<ВАШ_АДРЕС>/api/v1/expressions/ - Далее выполняется запрос в базу данных, используя
idвыражения иidпользователя - Получаем выражение
- Парсим это выражение в json
- Отдаём пользователю это выражение
Реализацию можете глянуть в \internal\orchestrator\orchestrator.go, сигнатура handlerа:
func GetExpressionByIdHandler(w http.ResponseWriter, r *http.Request)- Пользователь делает запрос
- Middleware проверяет есть ли токен и валиден ли он
- Middleware парсит из
jwtи передаётidиemailпользователя в обработчик по адресуlocalhost:<ВАШ_АДРЕС>/api/v1/profile - Из контекста достаются
idиemail, собираются в json - json отправляется пользователю
Реализацию можете глянуть в internal/database/handlers/handlers.go.
Сигнатура:
func (h *AuthHandler) GetUserData(w http.ResponseWriter, r *http.Request) Вот мы и подошли к самой сложной части реализации кода :) Общение Агента и Оркестратора происходит по gRPC. Агент делает запрос, получает данные, вычисляет, отдаёт Оркестратору. Соответственно для Оркестратора наоборот: он получает запрос, готовит данные, отдаёт их, и получает результат от Агента.
И всё бы было просто, но вот только в синхронной программе. Используя асинхронность и параллельность сложность реализации такой структуры растёт с геометрической прогрессией.
И если говорить понятно и в 2-х словах, то это работает так:
- Разбор выражения в AST (парсер).
- Рекурсивный обход AST:
- Атомарные выражения отправляются через канал в очередь.
- Обработка запроса на Оркестраторе:
- Очередь хранит узлы (Node).
- По запросу Агента Оркестратор отдаёт накопленные выражения.
- Обработка выражений на Агенте:
- Создаёт пул воркеров.
- Воркеры получают задания, считают и отправляют ответ в Оркестратор.
- **Обновление дерева в Оркестраторе:
- Вставка вычисленных значений по id.
- Проверка, схлопнулось является ли корневой узел числом.
- Финальный результат – когда остался один узел, Оркестратор возвращает ответ и записывает в базу данных и массив expressions.
Код вы можете глянуть в internal/orchestrator/grpc_orchestrator.go и также internal/agent/grpc_agent.go. Я на самом деле прям очень доволен этой частью и рад, что всё запускается и работает так как нужно.
Вот вам Роналдо с сыном: