Задание Sidekiq mailer доступ к БД до сохранения модели

#ruby-on-rails #ruby #jobs #mailer

Вопрос:

Вероятно, название не говорит само за себя, ситуация такова:

 # user.points: 0
user.update!(points: 1000)

UserMailer.notify(user).deliver_later. # user.points = 0 => Error !!!!
 

user экземпляр обновляется, и после этого вызывается почтовая программа с параметром user as, а в письме, в котором изменений нет: user.points=0 вместо 1000

Но, sleep 1 сразу после user_update отправки электронного письма с обновленными изменениями, кажется, что задание по электронной почте выполняется быстрее, чем обновление данных в базе данных.

 # user.points: 0
user.update!(points: 1000)

sleep 1

UserMailer.notify(user).deliver_later. # user.points = 1000 => OK
 

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

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

Итак, есть ли другой способ решить эту проблему, сохранив экземпляр пользователя в качестве параметра и избегая after_commit обратного вызова?

Спасибо

Комментарии:

1. как уже ответил Ричард: вокруг этих заявлений должна быть сделка. Если вы добавляете свою собственную, это просто вложенная транзакция, изменения не будут видны другим транзакциям до тех пор, пока не будет совершена самая внешняя транзакция. Можете ли вы показать больше кода?

2. Я думаю, что транзакция-это не вариант, я уже проверил, транзакции полезны для обеспечения сохранения более 1 таблицы в атомарной фиксации, в данном случае это всего лишь одно обновление таблицы

3. Поскольку задание не видит изменений (или только по истечении времени ожидания), это означает, что изменение происходит в незафиксированной транзакции. Из того немногого, что вы показываете, мы не можем сказать вам, откуда исходит транзакция и где она совершается. Но вы должны поставить свою работу в очередь только после этого момента, и тогда изменения будут видны на работе. Тайм-аут не является надежным вариантом. это может быть 0,01 с или 2 с, пока TX не будет отправлен.

4. Извините за задержку, код почти такой же, как в примере: обновление сохраняет еще пару полей, а между обновлением и электронной почтой есть только пара вставок в другую таблицу/транзакцию.

5. транзакция с обновлением модели 1 экземпляра 1 записи, как указано в «rmlockerd», является избыточной.

Ответ №1:

Помните, что Sidekiq запускает копию вашего приложения Rails в отдельном процессе, используя Redis в качестве носителя. Когда вы звоните deliver_later , он на самом деле не «переходит» user к заданию отправителя. Он порождает поток, который ставит задание в очередь в Redis, передавая сериализованный хэш user свойств, включая идентификатор.

Когда задание почтовой программы выполняется в процессе Sidekiq, оно загружает свежую копию пользователя из базы данных. Если транзакция, содержащаяся update! в приложении main Rails, еще не завершена, Sidekiq получает старую запись из базы данных. Итак, это условие гонки.

( update! уже обертывает неявную транзакцию вокруг себя, если ее нет, поэтому обертывание ее в вашу собственную транзакцию является избыточным и не способствует состоянию гонки, поскольку вложенные транзакции ActiveRecord фиксируются только тогда, когда фиксируется самая внешняя транзакция.)

В крайнем случае, вы можете отложить постановку в очередь на работу с помощью чего-то вроде .deliver_later(wait_until: 10.seconds.from_now) взлома , но лучше всего поместить уведомление отправителя в after_commit обратный вызов на вашей модели.

 class User < ApplicationRecord
  after_commit :send_points_mailer

  def send_points_mailer
    return unless previous_changes.includes?(:points)

    UserMailer.notify(self).deliver_later
  end
end
 

after_commit Обратные вызовы модели гарантированно выполняются после завершения окончательной транзакции, поэтому, как и ядерная бомбардировка с орбиты, это единственный способ быть уверенным.

Комментарии:

1. Согласитесь, after_commit это вариант, и на самом деле я забыл упомянуть об этом возможном решении как о решении, которого я хотел бы избежать — в этом случае user модель слишком велика, и это не должно быть задачей самой модели —

2. Редактирование вопроса

3. В более старых версиях Rails был Observer класс, который мог отслеживать события жизненного цикла. Он был выделен в отдельный драгоценный камень с рельсов 4. Он не выглядит очень ухоженным, но вы, возможно, сможете его раскошелить и привести в порядок для своих целей.

4. @rmlockerd я не думаю, что сериализованная версия объекта отправляется в sidekiq (если sidekiq используется с ActiveJob), но повторное представление модели globalId.

5. Спасибо, вы правы. Я отредактирую ответ. Однако эффект тот же; sidekiq загружает свежую копию модели из базы данных при выполнении задания.

Ответ №2:

Вы не упоминали об этом, но я предполагаю, что вы используете ActiveRecord? Если это так, вам, вероятно, потребуется убедиться, что вы очистили транзакцию базы данных до того, как будет запланировано задание sidekiq.

https://api.rubyonrails.org/v6.1.4/classes/ActiveRecord/Transactions/ClassMethods.html

Комментарии:

1. Спасибо за ваш ответ, Да, с ActiveRecord, но перенос изменений в транзакцию не решает проблему, я думаю, что это больше вопрос потоков, чем транзакций