Sidekiq - usunięcie

Cześć,

Piszę do was z pytaniem dotyczącym sidekiqa. Mianowicie w celach treningowych robię sobie wysyłkę maila przypominającego o terminie oddanej książki za pomocą ActiveJoba. Zauważyłem jednak jeden problem. Jeżeli użytkownik odda książkę przed wysłaniem wiadomości, to wiadomość może się i nie wyśle, ale w momencie, gdyby użytkownik pożyczył znowu tą samą książkę jeszcze zanim zniknie z zaplanowanych, to dostanie wiadomość o błędnym terminie zwrotu. Wymyśliłem sobie, aby JID każdego zakolejkowanego joba zapisywać w bazie danych i później przy oddawaniu przed czasem znajdywać joba o takim id i go usuwać. Jednak wszędzie czytam, że nie jest to optymalny proces. Czy znacie może jakieś sposoby znajdywania zakolejkowanych jobów w sidekiqu i ich usuwania, bądź nadpisywania terminu wykonania?

Pozdrawiam,
Michał Stróż

Odradzałbym usuwanie tasków z Sidekiqa, to kiepski pomysł. Lepiej przekazać do zadania dodatkowy argument, taki jak np id rekordu wypożyczenia/jego timestamp, i w momencie wykonywania porównać czy nic się w międzyczasie nie zmieniło.

Najlepiej nie ruszać raz odpalonego zadania :). Czy to ma być usunięcie, czy sprawdzenie dodatkowego parametru.
Powinieneś zadanie zakolejkowane traktować jako do wykonania, a poźniej wykonane. W Twoim przypadku kluczowym momentem jest kiedy stwierdzasz, że powinno zostać wysłane powiadomienie.
Spróbuj użyć crona do wywoływania logiki sprawdzania terminów zwrotu dla wypożyczonych książek (np. 1 raz na dzień) i wysyłać powiadomienia przez ActiveJob użytkownikom, którzy powinni je otrzymać :slight_smile:.

@soso Bardzo dobry pomysł, ale w takim razie mam pytanie. Po co w takim razie podpinać sidekiqa pod cron, skoro można bezpośrednio podłączyć pod cron wysyłkę maili?

Dobre pytanie :slight_smile: Do tego zadania rzeczywiście nie musisz podpinać Sidekiqa.
Różnica w podejściu może być jednak znaczna.

1 - Cron) Lecisz po wszystkich wypożyczeniach, sprawdzasz termin i albo wysyłasz maila inline, albo go kolejkujesz (np. przez deliver_later). Pierwsze podejście niesie za sobą kilka problemów, głównie z obsługą błędów - jeśli wystąpi jakiś błąd z wysyłką musisz wiedzieć jakie maile zostały wysłane, żeby przy ponowieniu nie wysłać ich podwójnie. Drugie podejście jest bardziej bezpieczne, gdyż każda wysyła jest “izolowana” i odporna na błędy innych.

Przykład pseudokodu:

BookBorrowing.with_due_date_soon.each do |borrowing|
  ReminderMailer.deliver_later(borrowing)
end

lub

BookBorrowing.all.each do |borrowing|
  ReminderMailer.deliver_later(borrowing) if borrowing.due_date_soon?
end

Czasem logiki wyciągnięcia odpowiednich danych nie da się załatwić zapytaniem SQL, lub z jakiegoś powodu nie chce się tego robić (drugi przykład). I tu wchodzi Sidekiq, gdyż możesz dodać do niego więcej logiki.

2 - Cron + Sidekiq)
Lecisz Cronem po wszystkich wypożyczeniach i dla każdego wypożyczenia kolejkujesz joba, który nie tylko odpowiada za wysyłkę maila, ale również za sprawdzenie, czy mail powinien zostać wysłany. Tu już masz większe pole do popisu, można ugrać o wiele “bezpieczniejszy”, odporniejszy na błędy kod. Za darmo dostajesz też pogląd obecnej sytuacji przez UI (sidekiq ma to wbudowane) i możliwość ponowienia joba który się nie wykonał prawidłowo (np. dla jednego rekordu wystąpił błąd sprawdzania terminu).

Przykład pseudokodu:
Cron:

BookBorrowing.all.each do |borrowing|
  ReminderJob.perform_async(borrowing.id)
end

Sidekiq:

class ReminderJob
  def perform(borrowing_id)
    # find borrowing...
    if borrowing.due_date_soon?
      send_email
    else
      do_nothing
    end
  end
end

Jest to tylko liźnięcie tematu ale mam nadzieję że dało Ci lepszy pogląd na sytuacje :slight_smile:. Pomyśl jak najlepiej połączyć narzędzia aby ugrać kod bardziej odporny na błędy, a jeśli już wystąpią, to abyś wiedział co się sypnęło i mógł to łatwo poprawić :slight_smile:. W produkcyjnych aplikacjach zwróć uwagę na skalowalność, w ostatnim przykładzie każde wypożyczenie dokłada kolejne zadanie, cały proces może spuchnąć jeśli będzie dużo wypożyczeń.

@soso Ok, zrobiłem jak zasugerowałeś
Po pierwsze zrobiłem rake, który sprawdza w bazie danych czy jutro są terminy zwrotu książek, i jeżeli tak to wrzucam do sidekiqa taska z wysyłką na jutro.

namespace :mail_sending do

desc ‘Sending sailings to group of users day beforeto expires_at’
task sending_mails_before: :environment do

expires_reservations = Reservation.where(status: 'TAKEN').where('Date(expires_at) = ?', Date.tomorrow)
if expires_reservations.blank?
  'Tomorrow none of any reservations will end.'
else
  expires_reservations.each do |res|
    BookReservationExpireJobJob.set(wait_until: res.expires_at-1.day).perform_later(res.book)
  end
end

end
end

Następnie ustawiłem crona codziennie na minutę po północy( jeżeli dajmy na to ktoś miałby termin oddania zwrotu np. o 2:00), aby wykonywał tego rake’a.

env :PATH, ENV['PATH']

job_type :rbenv_rake, %Q{export PATH=/home/deploy/.rbenv/shims:/home/deploy/.rbenv/bin:/usr/bin:$PATH; eval "$(rbenv init -)"; \
                         cd :path && :environment_variable=:environment :bundle_command rake :task --silent :output }

job_type :rbenv_runner, %Q{export PATH=/home/deploy/.rbenv/shims:/home/deploy/.rbenv/bin:/usr/bin:$PATH; eval "$(rbenv init -)"; \
                         cd :path && :bundle_command :runner_command -e :environment ':task' :output }

# set :output, "/home/michal/cron_log.log"


every '1 0 * * *' do
  rbenv_rake 'mail_sending:sending_mails_before', environment: 'development'
end