Skip to content

About

No description or website provided.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Этот проект - работа за 5-й спринт Яндекс Лицея. Он представляет из себя суперпупермега калькулятор, который содержит 3 микросервиса однако это больше соответсвует SOA архитекутре чем микросервисной, так как все 3 микросервиса работают от одной базы данных.

Микросервисы:

  1. Агент - посылает по gRPC запросы на оркестратор для получения выражений (Server-Streaming)
  2. Оркестратор1 - отдаёт выражения агенту по gRPC, если таковые имеются (Polling)
  3. Оркестратор2 - служит для взаимодействия с пользователем

🔧Технологии

The Go Programming Language

Postman

gRPC

sqlLite

🚀Как запустить проект

По шагам:

  1. Открываете VS Code/Goland
  2. Добавляете .env файл в директорию internal/orchestrator
  3. Настраиваете зависимости в .env файле: Пример приведён ниже:
TIME_ADDITION_MS =  #время выполнения операции сложения в миллисекундах
TIME_SUBTRACTION_MS =  #время выполнения операции вычитания в миллисекундах
TIME_MULTIPLICATIONS_MS =  # время выполнения операции умножения в миллисекундах
TIME_DIVISIONS_MS =  #время выполнения операции деления в миллисекундах
PORT_ORCHESTRATOR =  # Порт на котором запускается оркестратор
JWT_SECRET =  #указывается секретная строка для подписи токена
TOKEN_EXPIRY =  #указывается через сколько исткекает токен в часах
REFRESH_EXPIRY =  #указывается через сколько обновляется токен в часах
  1. Далее вам нужно создать 2 терминала. В VS code - это сочетание клавиш Ctrl+Shift+5, в Goland придётся просто открыть 2 вкладки терминала: View → Tool Windows → Terminal (или нажмите Alt + F12).
  2. Переходите в директории, где лежат main файлы для агента и для оркестратора: cd .\cmd\agent и cd .\cmd\orchestrator
  3. Далее компилируете main файлы и запускаете их в разных терминалах соответственно Команды для запуска main:
    • go build main.go
    • go run main.go
  4. Готово, если у вас всё получилось, раз в несколько секунд вы увидите логи Агента о выполнении запросов, а Оркестратор покажет на каком порту запустились его 2 микросервиса. Один микросервис общается по gRPC с агентом, второй же обрабатывает запросы пользователя.
    • Это сообщение вы увидите после запуска:
image

⚡Использование

После запуска Агента и Оркестратора вы наконец-то можете делать запросы на сервер.

Регистрация

Для начала использования сервиса необходимо пройти регистрацию. Таким образом система будет знать, что такой пользователь есть в её базе данных. Создайте запрос в удобном вам формате: (Советую перейти на postman, так как он запоминает запросы, и не нужно какждый раз это переписывать)

Постман:

image

Curl:

curl --location 'http://localhost:<ВАШ_ПОРТ>/api/v1/register' \
--header 'Content-Type: application/json' \
--data '{
  "email": "<ВАШ_EMAIL>",
  "password": "<ВАШ_ПАРОЛЬ>"
}'

Примеры ответов:

Если запрос был выполнен успешно, то вы получите соответствующее сообщение:

image

Если запрос не удался, то выведется сообщение об ошибке, например:

image

Если пользователь уже зарегистрирован, то выведется соответствующее сообщение:

image

Логин

Чтобы выполнять запросы на любые handlerы необходимо войти в систему, получить свой личный jwt-токен и отправлять его дальше в заголовках запроса. Создадим запросы в удобном для нас формате:

Постман:

image

Curl:

curl --location 'http://localhost:<ВАШ_ПОРТ>/api/v1/login' \
--header 'Content-Type: application/json' \
--data '{
  "email": "<ВАШ_EMAIL>",
  "password": "<ВАШ_ПАРОЛЬ>"
}'

Примеры ответов:

Успешный запрос:

image

Запрос с ошибкой:

image

Пользователь не был добавлен в Базу Данных:

image

Запрос с неверным паролем:

image
  • Вы также можете запустить интеграционные тесты, которые находятся в пакете database/handlers/, чтобы проверить всё что я описал выше.

🤝 Дальнейшее взаимодействие

Этот пункт я решил выделить из-за его важности для проведения дальнейшних запросов.

  • Полученные после login токены необходимо сохранить (в тг Saved Messages закиньте 😁)и передавать в каждом запросе, так как передо мной не стояло задачи реализовать cookie для сохранения токенов.
  • Чтобы использовать токен в Postman необходимо передавать его в соответствующей вкладке, на каринке ниже представлено где именно:
image

Добавление выражения

Выражение добавляется в хранилище выражений 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...".
  • Добавьте ваш токен в заголовок авторизации.

Пример:

image

Сверху - выражение, снизу ответ Оркестратора

Пример с ошибкой:

image

Пример с инвалидным токеном:

image

Пример без передачи токена:

image
  • Для остальных защищённых обработчиков при передачи ивналидного токена или без его передачи будут те же самые ошибки

Получение списка выражений

curl -X GET http://localhost:<ВАШ_ПОРТ>/api/v1/expressions' \
  -H "Authorization: Bearer <ВАШ_ТОКЕН>"
  • Обязательно после localhost - укажите значение порта, на котором у вас запускается сервер. Это значение указывается в .env файле.

Пример:

image

Сверху - выражение, снизу ответ Оркестратора

Пример с ошибкой

image

Получение выражения по его идентификатору

curl --location 'localhost:<ВАШ_ПОРТ>/api/v1/expressions/:id' \
-H "Authorization: Bearer <ВАШ_ТОКЕН>"
  • Обязательно после localhost - укажите значение порта, на котором у вас запускается сервер. Это значение указывается в .env файле.
  • Поле :id в запросе поменяйте на именование id выражения, которое вы хотите достать

Пример:

image

Пример с ошибкой:

image

Получение данных профиля пользователя

curl --location 'localhost:<ВАШ_ПОРТ>/api/v1/profile' \
-H "Authorization: Bearer <ВАШ_ТОКЕН>"

Пример:

image
  • Ошибиться в этом запросе можно только в передаче токена, поэтому не вижу смысла прикреплять пример с ошибкой.

Запрос на обновление токена

curl --location 'localhost:<ВАШ_ПОРТ>/api/v1/refresh-token' \
-H "Authorization: Bearer <ВАШ_ТОКЕН>"

Пример:

image
  • Ошибиться в этом запросе можно только в передаче токена, поэтому не вижу смысла прикреплять пример с ошибкой.

🤔 Как это всё работает

Регистрация

  • Пользователь делает запрос, отправляя свои 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. Я на самом деле прям очень доволен этой частью и рад, что всё запускается и работает так как нужно.

Логика работы на примере:

  1. Отправляем Post-запрос image
  2. Выражение разбивается image
  3. Выражение подсчитывается image

Спасибо за внимание, если вам понравился проект, пожалуйста поставьте ему звёздочку

Вот вам Роналдо с сыном:

ronaldo-with-son-in-pool

About

No description or website provided.

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages