Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/week04 #4

Merged
merged 13 commits into from
Nov 21, 2023
Merged

Feat/week04 #4

merged 13 commits into from
Nov 21, 2023

Conversation

ekhvalov
Copy link
Owner

@ekhvalov ekhvalov commented Nov 11, 2023

Message Producing

  • Реализован services/outbox
  • Реализован services/msg-producer
  • Реализован services/outbox/jobs/send-client-message
  • Можно включить/выключить шифрование для публикующихся сообщений

Managers Load

  • Реализован services/manager-pool/in-mem
  • Реализован services/manager-load

Manager API

  • Генерация кода на базе контракта менеджерского API поддержана в task gen:api
  • Контракт менеджерского API можно получить через debug server
  • Контракт менеджерского API можно посмотреть в Swagger UI
  • Создание сервера унифицировано с помощью пакета internal/server
  • В Keycloak заведёны новые клиент chat-ui-manager, роль support-chat-manager и пользователь-менеджер с этой ролью
  • Реализован хендлер и юзкейс для /v1/getFreeHandsBtnAvailability, недостающие юнит-тесты написаны
  • Реализован хендлер и юзкейс для /v1/freeHands
  • /v1/freeHands возвращает ошибку с кодом 5000 в случае "перегрузки" менеджера
  • [Optional] /getFreeHandsBtnAvailability возвращает более детальный статус, чем true / false

Validation

  • Поля в конфигах и опциях валидируются, по возможности используются более специфичные теги, чем просто required

Testing

  • Новый код покрыт юнит-тестами, statements coverage > 90%
  • Client UI: Можно отправить сообщение и увидеть его через kcat
  • Manager UI: После нажатия на кнопку Ready to Problems она задизейблится и сохранит это состояние после обновлении страницы
  • Юнит-тесты проходят (task tests)
  • Интеграционные тесты проходят (task tests:integration)
  • E2E тесты проходят (task tests:e2e)

Copy link
Collaborator

@MysterySuperhero MysterySuperhero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Посмотрите, пожалуйста, комменты по репозиторию jobs и пулу менеджеров 🙂

var j *store.Job

findAndReserve := func(ctx context.Context) error {
tx := store.TxFromContext(ctx)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Важный момент, отталкиваясь от которого надо внести правки – репозиторий ничего не знает про транзакции. Он должен каким-то образом работать, как в режиме выполнения запроса в транзакции, так и в режиме выполнения запроса просто на соединении, и сам он этим управлять не может. Этим управляет юзкейс / сервис.
Могут возникнуть вопросы (хотя по коду ниже видно, что вы в целом понимаете суть, но на всякий случай ещё раз озвучим её). Абстрактный "как это сделать" и вполне конкретный вопрос "как нужно сделать в этом случае".

Как это сделать?

В третьем модуле вы как раз реализовали и использовали метод RunInTx() структуры Database, которая из недр пакета store. Обратите внимание, как там написан код и на что был сделан основной расчет. Теория была в шагах 1, 2 и в ссылках в них, поэтому пройдитесь, пожалуйста, по ним повторно на всякий случай.

Краткая выжимка:

  • Для прозрачной передачи транзакции между слоями используется контекст.
  • Транзакцию стартует юзкейс / сервис, вызывая метод RunInTx.
  • RunInTx кладет запущенную транзакцию в контекст и отдает контекст в функцию-коллбек, которую будет с этим контекстом вызывать (обратите внимание, что ещё этот метод делает).
  • Внутри функции-коллбека вызываем методы репозиториев, которые хотим выполнить транзакционно.
  • Репозитории уже в своих методах используют структуру Database, и вызывают у неё методы для выполнения запрсов. Например, Query().
  • Внутри Database.Query() есть вызов ещё одного метода loadClient, который как раз и решает где выполнять запрос: в транзакции или на соединении. И определяет он это как раз по наличию или отсутствию транзакции в контексте, как вы это сделали здесь.

P.S. можем в Телеграмах в канале / личке обсудить, если возникнут вопросы.

Как нужно сделать в этом случае?

Вспомним задачу: атомарно найти незанятую задачу и занять её. Атомарность требует выполнения этого всего либо одним запросом, либо в транзакции несколькими запросами.
Спойлер: мы это сделали одним запросом. Можете посмотреть в сторону CTE. Но и против выполнения в двух запросах не возражаем (возможно, потребуется правка заготовок и тестов).

Поэтому либо делаем одним запросом и тогда транзакции не нужны.
Либо уносим RunInTx на уровень сервиса и внутри вызываем методы репозитория, которые проделают нужные нам штуки.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Переписал с использованием двух запросов. Про CTE сходу не нашёл как сделать, по этому подсмотрю ваше решение.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Транзакцию стартует юзкейс / сервис, вызывая метод RunInTx.

Вы упустили этот момент 🙂 Т.к. старт RunInTx остался в репозитории.

Не будем вас больше мучать, посмотрите, пожалуйста, в авторское решение

findAndReserve := func(ctx context.Context) error {
tx := store.TxFromContext(ctx)
if nil == tx {
return fmt.Errorf("no transaction in context")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если нет аргументов для форматирования, то используйте errors.New() 🙂


j, err = j.Update().AddAttempts(1).SetReservedUntil(until).Save(ctx)
if err != nil {
if isAttemptsExceededError(err) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это похоже на "логику", которой в репозитории не место. Вы и так возвращаете job-у с количеством попыток, поэтому на это количество попыток стоит смотреть в сервисе и в зависимости от него что-то делать 🙂

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Убрал

case errors.Is(err, freehands.ErrInvalidRequest):
return internalerrors.NewServerError(http.StatusBadRequest, "invalid request", err)
case errors.Is(err, freehands.ErrManagerOverloaded):
return internalerrors.NewServerError(int(ErrorCodeManagerOverloadedError), "manager overloaded", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лайк

return nil
}

func (s *Service) Get(_ context.Context) (types.UserID, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: сплошное полотно текста тяжеловато читается. Делите, по возможности, отступами код на смысловые группы

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Исправил

s.mutex.RLock()
_, exists := s.store[managerID]
s.mutex.RUnlock()
if exists {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кмк, здесь возможна гонка, в результате которой один менеджер окажется дважды в пуле.

Параллельно одновременно вызывается дважды этот метод с одним и тем же методом.
Параллельно дважды захватывается мьютекс на чтение. Оба раза убедились, что менеджера в мапе нет. Проскакиваем 61-ую строку. Дважды добавляем менеджера в мапу.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кмк, здесь и в Get оптимизации с RLock излишни и приводят к усложнению кода. А профит какой от них непонятно

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Исправил

s.rw.RLock()
_, exists := s.registry[job.Name()]
s.rw.RUnlock()
if exists {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вроде как, аналогичная проблема может быть.

Но мы даже и не парились здесь насчет мьютексов, честно говоря

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Исправил


func idle(ctx context.Context, idleTime time.Duration) <-chan struct{} {
ch := make(chan struct{})
go func() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А зачем стартовать отдельную горутину, чтобы потом все равно заблокироваться на канале? Кмк, просто селекта и таймера достаточно должно быть

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Исправил

}

func (s *Service) handleJob(ctx context.Context, job jobsrepo.Job) error {
s.rw.RLock()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тоже особого смысла не имеет, кмк, т.к. в рантайме не предполагается, что зарегистрированные джобы будут удаляться

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Исправил

@Antonboom Antonboom self-requested a review November 18, 2023 12:07
@Antonboom Antonboom assigned Antonboom and unassigned Antonboom Nov 18, 2023
@Antonboom Antonboom removed their request for review November 18, 2023 12:08
Copy link
Collaborator

@MysterySuperhero MysterySuperhero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🖖 🖖 🖖

@ekhvalov ekhvalov merged commit a1d9543 into main Nov 21, 2023
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants