Run Rails jobs sequentially - ruby-on-rails

I have three Rails jobs to process a player yellow/red cards in a soccer tournament, and the penalties these players will have due to getting this cards.
The idea is that the first job collects all Incidences (an Incidence is when a Player gets a yellow card, to give an example), and counts all the cards a Player got.
class ProcessBookedPlayersJob < ApplicationJob
queue_as :default
#cards = []
def perform(*args)
#cards = []
yellows = calculate_cards(1)
reds = calculate_cards(2)
#cards << yellows << reds
end
def after_perform(*match)
#ProcessPenaltiesJob.perform_later #cards
ProcessPenalties.perform_later #cards
#PenaltiesFinalizerJob.perform_later match
PenaltiesFinalizer.perform_later match
end
def calculate_cards(card_type)
cards = Hash.new
players = Player.fetch_players_with_active_incidences
players.each { |p|
# 1 is yellow, 2 is red
counted_cards = Incidence.incidences_for_player_id_and_incidence_type(p.id, card_type).size
cards[p] = counted_cards
}
return cards
end
end
This first job is executed when an Instance is created.
class Incidence < ApplicationRecord
belongs_to :player
belongs_to :match
after_save :process_incidences, on: :create
def self.incidences_for_player_id_and_incidence_type(player_id, card_type)
return Incidence.where(status: 1).where(incidence_type: card_type).where(player_id: player_id)
end
protected
def process_incidences
ProcessBookedPlayers.perform_later
end
end
After this, another job runs and creates the necessary Penalties (a Penalty is a ban for the next Match, for example) according to the Hash output that the previous job created.
class ProcessPenaltiesJob < ApplicationJob
queue_as :default
def perform(*cards)
yellows = cards[0]
reds = cards[1]
create_penalties_for_yellow_cards(yellows)
create_penalties_for_red_cards(reds)
end
# rest of the job...
And also there's another job, that sets these bans as disabled, once they have expired.
class PenaltiesFinalizerJob < ApplicationJob
queue_as :default
def perform(match)
active_penalties = Penalty.find_by(status: 1)
active_penalties.each do |p|
#penalty.starting_match.order + penalty.length == el_match_que_inserte.order (ver si seria >=)
if p.match.order + p.length >= match.order
p.status = 2 # Inactivate
p.save!
end
end
end
end
As you can see in ProcessBookedPlayersJob's after_perform method
def after_perform(*match)
ProcessPenalties.perform_later #cards
PenaltiesFinalizer.perform_later match
end
I'm trying to get those two other jobs executed (ProcessPenaltiesJob and PenaltiesFinalizerJob) with no luck. The job ProcessBookedPlayersJob is being executed (because I can see this in the log)
[ActiveJob] [ProcessBookedPlayersJob] [dbb8445e-a706-4443-9cb8-2c45f49a4f8f] Performed ProcessBookedPlayersJob (Job ID: dbb8445e-a706-4443-9cb8-2c45f49a4f8f) from Async(default) in 38.81ms
But the other two jobs aren't executed. So, how can I get both ProcessPenaltiesJob and PenaltiesFinalizerJob run after ProcessBookedPlayersJob has finalized its execution? I don't mind if they run in parallel, but they need to be run after the first one finishes, since they need its output as their input.
I have searched for this, and the closest match I found was this answer. Quoting it:
If the sequential jobs you are talking about however are of different
jobs / class, then you can just call the other job once the first job
has finished.
That's exactly the behaviour I'm trying to have... but how can I get my jobs to run sequentially?
For now, I'm thinking in adding the first job's logic into Incidences's after_savehook, but that doesn't sound too natural. Is there any other way to pipeline the execution of my jobs?
Many thanks in advance

Related

How can I prevent many sidekiq jobs from exceeding the API calls limit

I am working on an Ruby On Rails application. We have many sidekiq workers that can process multiple jobs at a time. Each job will make calls to the Shopify API, the calls limit set by Shopify is 2 calls per second. I want to synchronize that, so that only two jobs can call the API in a given second.
The way I'm doing that right now, is like this:
# frozen_string_literal: true
class Synchronizer
attr_reader :shop_id, :queue_name, :limit, :wait_time
def initialize(shop_id:, queue_name:, limit: nil, wait_time: 1)
#shop_id = shop_id
#queue_name = queue_name.to_s
#limit = limit
#wait_time = wait_time
end
# This method should be called for each api call
def synchronize_api_call
raise "a block is required." unless block_given?
get_api_call
time_to_wait = calculate_time_to_wait
sleep(time_to_wait) unless Rails.env.test? || time_to_wait.zero?
yield
ensure
return_api_call
end
def set_api_calls
redis.del(api_calls_list)
redis.rpush(api_calls_list, calls_list)
end
private
def get_api_call
logger.log_message(synchronizer: 'Waiting for api call', color: :yellow)
#api_call_timestamp = redis.brpop(api_calls_list)[1].to_i
logger.log_message(synchronizer: 'Got api call.', color: :yellow)
end
def return_api_call
redis_timestamp = redis.time[0]
redis.rpush(api_calls_list, redis_timestamp)
ensure
redis.ltrim(api_calls_list, 0, limit - 1)
end
def last_call_timestamp
#api_call_timestamp
end
def calculate_time_to_wait
current_time = redis.time[0]
time_passed = current_time - last_call_timestamp.to_i
time_to_wait = wait_time - time_passed
time_to_wait > 0 ? time_to_wait : 0
end
def reset_api_calls
redis.multi do |r|
r.del(api_calls_list)
end
end
def calls_list
redis_timestamp = redis.time[0]
limit.times.map do |i|
redis_timestamp
end
end
def api_calls_list
#api_calls_list ||= "api-calls:shop:#{shop_id}:list"
end
def redis
Thread.current[:redis] ||= Redis.new(db: $redis_db_number)
end
end
the way I use it is like this
synchronizer = Synchronizer.new(shop_id: shop_id, queue_name: 'shopify_queue', limit: 2, wait_time: 1)
# this is called once the process started, i.e. it's not called by the jobs themselves but by the App from where the process is kicked off.
syncrhonizer.set_api_calls # this will populate the api_calls_list with 2 timestamps, those timestamps will be used to know when the last api call has been sent.
then when a job wants to make a call
syncrhonizer.synchronize_api_call do
# make the call
end
The problem
The problem with this is that if for some reason a job fails to return to the api_calls_list the api_call it took, that will make that job and the other jobs stuck for ever, or until we notice that and we call set_api_calls again. That problem won't affect that particular shop only, but also the other shops as well, because the sidekiq workers are shared between all the shops using our app. It happen sometimes that we don't notice that until a user calls us, and we find that it was stuck for many hours while it should be finished in a few minutes.
The Question
I just realised lately that Redis is not the best tool for shared locking. So I am asking, Is there any other good tool for this job?? If not in the Ruby world, I'd like to learn from others as well. I'm interested in the techniques as well as the tools. So every bit helps.
You may want to restructure your code and create a micro-service to process the API calls, which will use a local locking mechanism and force your workers to wait on the socket. It comes with the added complexity of maintaining the micro-service. But if you're in a hurry then Ent-Rate-Limiting looks cool too.

Resque: calling #perform_later from inside another job doesn't trigger those other jobs

I have an issue which is likely me doing something weird but I can't for the life of me figure it out:
I have a parent job that is supposed to kick off a bunch of child jobs, but those child jobs never execute.
Here's the parent job:
class TriggerJob < ApplicationJob
def perform
start_events
end
around_perform do |_, block|
p 'TriggerJob before_perform'
block.call
p 'TriggerJob after_perform'
end
private
def start_events
events = Events.all
events.each do |event|
event.connectors.each do |connector|
p "Stopping connector #{connector.id} for event #{event.id}"
StopConnectorJob.perform_later(connector.id)
end
end
end
end
And here's the child job:
class StopConnectorJob < ApplicationJob
#queue = :default
class StopConnectorFailed < StandardError; end
rescue_from(StandardError) do |exception|
p "ERROR! #{exception.message}"
raise(StopConnectorFailed, exception.message)
end
def perform(connector_id)
p 'STOPPING CHARGE'
connector = Connector.find(connector_id)
do_stuff_with_the_connector(connector)
end
around_enqueue do |job, block|
p "StopConnectorJob before_enqueue: #{Resque.count_all_scheduled_jobs}"
p job.arguments
block.call
p "StopConnectorJob after_enqueue: #{Resque.count_all_scheduled_jobs}"
end
around_perform do |job, block|
p 'StopConnectorJob before_perform'
p job.arguments
block.call
p 'StopConnectorJob after_perform'
end
private
def do_stuff_with_the_connector(connector)
# ...
end
end
When I run the base job, this is what gets output:
"TriggerJob before_perform"
"Stopping connector 4 for event 8"
"StopConnectorJob before_enqueue: 0"
[3]
"StopConnectorJob after_enqueue: 0"
"TriggerJob after_perform"
Then nothing. No jobs queued when I look at the jobs web interface. No record of redis persisting it when monitoring with redis-cli monitor.
If I change from #perform_later to #perform_now it works. But I want this to not be blocking.
Any idea why my jobs aren't being run even though it seems like they should have been enqueued but I can't see that they are?

How can I avoid deadlocks on my database when using ActiveJob in Rails?

I haven't had a lot of experience with deadlocking issues in the past, but the more I try to work with ActiveJob and concurrently processing those jobs, I'm running into this problem. An example of one Job that is creating it is shown below. The way it operates is I start ImportGameParticipationsJob and it queues up a bunch of CreateOrUpdateGameParticipationJobs.
When attempting to prevent my SQL Server from alerting me to a ton of deadlock errors, where is the cause likely happening below? Can I get a deadlock from simply selecting records to populate an object? Or can it really only happen when I'm attempting to save/update the record within my process_records method below when saving?
ImportGameParticipationsJob
class ImportGameParticipationsJob < ActiveJob::Base
queue_as :default
def perform(*args)
import_participations(args.first.presence)
end
def import_participations(*args)
games = Game.where(season: 2016)
games.each do |extract_record|
CreateOrUpdateGameParticipationJob.perform_later(extract_record.game_key)
end
end
end
CreateOrUpdateGameParticipationJob
class CreateOrUpdateGameParticipationJob < ActiveJob::Base
queue_as :import_queue
def perform(*args)
if args.first.present?
game_key = args.first
# get all particpations for a given game
game_participations = GameRoster.where(game_key: game_key)
process_records(game_participations)
end
end
def process_records(participations)
# Loop through participations and build record for saving...
participations.each do |participation|
if participation.try(:player_id)
record = create_or_find(participation)
record = update_record(record, participation)
end
begin
if record.valid?
record.save
else
end
rescue Exception => e
end
end
end
def create_or_find(participation)
participation_record = GameParticipation.where(
game_id: participation.game.try(:id),
player_id: participation.player.try(:id))
.first_or_initialize do |record|
record.game = Game.find_by(game_key: participation.game_key)
record.player = Player.find_by(id: participation.player_id)
record.club = Club.find_by(club_id: participation.club_id)
record.status = parse_status(participation.player_status)
end
return participation_record
end
def update_record(record, record)
old_status = record.status
new_status = parse_status(record.player_status)
if old_status != new_status
record.new_status = record.player_status
record.comment = "status was updated via participations import job"
end
return record
end
end
They recently updated and added an additional option you can set that should help with the deadlocking. I had the same issue and was on 4.1, moving to 4.1.1 fixed this issue for me.
https://github.com/collectiveidea/delayed_job_active_record
https://rubygems.org/gems/delayed_job_active_record
Problems locking jobs
You can try using the legacy locking code. It is usually slower but works better for certain people.
Delayed::Backend::ActiveRecord.configuration.reserve_sql_strategy = :default_sql

Rails: Finding records that overlap

I have a job model that has_many :fonctions and in my job model I'm creating a method to give me the number of similar fonction in all job compared to a given job.
ex: I want to compare all my job to Job1 job1 has this fonction("strategy", "management", "marketing", entrepreneurship")
another job job2 has this fonction( "strategy", "management", "data science")
So this must give me when doing (job1 & job2).size 2
for this i have this method in my job model that must do a similar job but the problem that i get this error undefined local variable or method for job'
def fonctions_score
(job.fonctions.collect(&:id) & self.fonctions.collect(&:id)).size
end
Update
This is the code that I'm trying now but still getting this error wrong number of arguments (0 for 1)
def fonctions_score(other_job)
these = other_job.fonctions.collect {|f| f.id }
those = self.fonctions.collect {|f| f.id }
logger.debug these logger.debug those # should just have lists of ids
common = (these & those)
logger.debug common # should be common ids
common.size
end
in my controller I'm ordering jobs like this
#related_jobs = Job.all
#related_jobs.sort_by do |related_job|
related_job.final_score
end
Try this
def calculate(j1, j2)
(j1.fonctions.pluck(:id) & j2.fonctions.pluck(:id)).size
end
Or
def calculate
self.fonctions.count
end

Ruby on Rails - weird behaviour logic by delayed job

I am doing the delayed_job by tobi and when I run the delayed_job but the fbLikes count is all wrong and it seems to increment each time I add one more company. Not sure wheres the logic wrong. The fbLikes method I tested before and it work(before I changed to delayed_job)
not sure where the "1" come from...
[output]
coca-cola
http://www.cocacola.com
Likes: 1 <--- Not sure why the fbLikes is 1 and it increment with second company fbLikes is 2 and so on...
.
[Worker(host:aname.local pid:1400)] Starting job worker
[Worker(host:aname.local pid:1400)] CountJob completed after 0.7893
[Worker(host:aname.local pid:1400)] 1 jobs processed at 1.1885 j/s, 0 failed ...
I am running the delayed_job in Model and trying to run the job of
counting the facebook likes
here is my code.
[lib/count_rb.job]
require 'net/http'
class CountJob< Struct.new(:fbid)
def perform
uri = URI("http://graph.facebook.com/#{fbid}")
data = Net::HTTP.get(uri)
return JSON.parse(data)['likes']
end
end
[Company model]
before_save :fb_likes
def fb_likes
self.fbLikes = Delayed::Job.enqueue(CountJob.new(self.fbId))
end
the issue is coming from
before_save :fb_likes
def fb_likes
self.fbLikes = Delayed::Job.enqueue(CountJob.new(self.fbId))
end
the enqueue method will not return the results of running the CountJob. I believe it will return whether the job successfully enqueued or not and when you are saving this to the fb_likes value it will evaluate to 1 when the job is enqueued successfully.
You should be setting fbLikes inside the job that is being run by delayed_job not as a result of the enqueue call.
before_save :enqueue_fb_likes
def fb_likes
Delayed::Job.enqueue(CountJob.new(self.fbId))
end
Your perform method in the CountJob class should probably take the model id for you to look up and have access to the fbId and the fbLikes attributes instead of just taking the fbId.
class CountJob< Struct.new(:id)
def perform
company = Company.find(id)
uri = URI("http://graph.facebook.com/#{company.fbid}")
data = Net::HTTP.get(uri)
company.fbLikes = JSON.parse(data)['likes']
company.save
end

Resources