#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, но перенос изменений в транзакцию не решает проблему, я думаю, что это больше вопрос потоков, чем транзакций