I need to do a delayed job to count fbLikes in Model but I have the error report of "undefined send_later() method". Is there any way to do delayed job to my fb_likes function in model?
==============================Latest===================================================
This is my latest code in my project. Things still the same, fb_likes does not display likes count.
[Company.rb]-MODEL
require "delayed_job"
require "count_job.rb"
class Company < ActiveRecord::Base
before_save :fb_likes
def fb_likes
Delayed::Job.enqueue(CountJob.new(self.fbId))
end
end
[config/lib/count_job.rb]
class CountJob<Struct.new(:fbId)
def perform
uri = URI("http://graph.facebook.com/#{fbId}")
data = Net::HTTP.get(uri)
self.fbLikes = JSON.parse(data)['likes']
end
end
[controller]
def create
#company = Company.new(params[:company])
if #company.save!
flash[:success] = "New company successfully registered."
----and other more code----
Library files are not required by default.
Rename the job file to count_job.rb. Using camelCase for filenames is insane and will burn you in unpredictable ways.
Create an initializer and add require 'count_job.rb'
One way is to create a separate worker that will get queued, the run to fetch the updated Model and call its fb_likes method on it, but the method will need to be public. Or take the logic into the worker itself.
Related
I have a n00b question. I'm using Rails 5, and would like to have example data in the application. When a user creates a new project, the project should already contain sample "tasks" that the user can delete or edit.
I know I can use seeds.rb to create sample data in my development environment. What is the best way to do it in a production environment for new users, and how? Should I use seeds.rb, a migration, or a rake task?
Example controller:
def create
#project = Project.new(project_params)
#project.user = current_user
if #project.save
// add sample content
redirect_to #project
else
render :new
end
end
In the Project model:
belongs_to :user
has_many :tasks, dependent: :destroy
When a new user joins and creates a new project, how do I add sample "tasks" automatically on the new project that the user creates?
UPDATE:
To create a task, I need a description and the current user's id (I'm using Devise, so I can use the current_user helper), for example:
#project.tasks.create!(description: "hello", user_id: current_user.id)
You could build a simple ServiceObject that does the job. It allows you to keep your controller skinny and you can user your current_user Devise helper to keep track of which user is creating the project
if #project.save
SetupBaseProject.new(project).call
redirect_to #project
else
# handle errors
end
In app/services/setup_base_project.rb
class SetupBaseProject
def initialize(project, user)
#project = project
end
def call
# Create example tasks and any additional setup you want to add
#project.tasks.create(description: 'Hello World', user: #project.user)
end
end
There are two possible scenarios considering your question.
The first project created by a user needs to have sample tasks included by default
Whenever a new project is created, sample tasks are created by default. Irrespective of the user is new user/existing user.
For first scenario,
We need to track whether project is created by new user by adding a boolean field to user, for example: new_user which defaults true.
We can use active record callbacks for generating sample tasks after project is created.
For Example,
Project Model :
belongs_to :user
has_many :tasks, dependent: destroy
after_create :generate_tasks
def generate_tasks
if self.user.new_user #This conditional block can be modified as needed
(1..3).each do |n|
self.tasks.create!(description: "Sample task #{n}", user_id: self.user.id)
end
end
end
For the second scenario,
We can use the same projects model file and just remove the conditional statement which will help create sample tasks by after project is created.
If you need any clarification, please comment out.
I've done this quite a few times in the past.
From my experience, you eventually have to give other people the ability to manage those defaults (product owners, marketing, etc)
What I've done in the past is to have a test user with a project that acts as 'the default' project.
Whenever anyone wants to create a new project, you clone it.
I used https://github.com/amoeba-rb/amoeba for that. It offers out of the bow way to override attributes that I'd want to change and can cascade the cloning to any associations you'd want to clone.
Say sample data is on model Detail which was populated with seeds.rb and belongs to 'Project'. You can dup that record and asign it to the new project (not tested):
def create
#project = Project.new(project_params)
#project.user = current_user
#project.details << Detail.find_by_name('sample').dup
if #project.save
redirect_to #company
else
render :new
end
end
Also, consider use a transaction when saving data on more than one model.
Full disclosure, I work in Rails 4...
If it were me, I would use FactoryBot to get the dummy data you want. Factories are great for testing so if you use them for testing, why not borrow them for this? This post shows an example where someone wanted to mock dummy data in console, same ideas could apply for you here.
Once you've got your factories mocked up... maybe for tasks something like:
require 'faker'
FactoryBot.define do
factory :task do
transient do
parent_project { nil }
end
description { Faker::Hacker.say_something_smart }
project_id { parent_project.id }
end
end
Maybe create a method in the project model like:
def create_dummy_data
require 'factory_bot'
require 'faker'
include FactoryBot::Syntax::Methods
# create_list will spit out 3 tasks associated with your project
create_list(:task, 3, parent_project: self)
end
Then in your example: after calling save...
if #project.save
#project.create_dummy_data
redirect_to #company
else
I can't think of a reason you couldn't go this route... noodling around in console I didn't have any problems, but I'd look at this answer as a starting point and not a final solution =P
I'm trying to use delayed_jobs (background workers) to process my incoming email.
class EmailProcessor
def initialize(email)
#raw_html = email.raw_html
#subject = email.subject
end
def process
do something with #raw_html & #subject
end
handle_asynchronously :process, :priority => 20
end
The problem is I can't pass instance variables (#raw_html & #subject) into delayed jobs. Delayed jobs requests that I save data into the model to be retrieved in the background task, but I would prefer to have a background worker complete the entire task (including saving the record).
Any thoughts?
Use delay to pass params to a method that you want to run in the background:
class EmailProcessor
def self.process(email)
# do something with the email
end
end
# Then somewhere down the line:
EmailProcessor.delay.process(email)
I'm trying to get the job which starts an action in this particular action.
Let me explain.
class MyClass
def go_for_it(delay = true)
if delay
delay(run_at: 2.minutes.from_now).go_for_it(false)
else
# How can I know if I was called by a DelayedJob AND if yes, which one ?
puts "I'll do it"
end
end
end
my_class = MyClass.new
my_class.delay(run_at: 2.minutes.from_now).go_for_it
My aim here is to make restrictions on jobs creation. I don't want go_for_it method called twice but this method can delay again itself according to some reasons. If I add those lines to go_for_it:
calling_method = caller_locations[0].label
job = Delayed::Job.where(queue: "my_queue").first
puts job.payload_object.id
# => id of MyClass if recorded
puts job.payload_object.method_name
# => :go_for_it
In the case of go_for_it delaying itself, these data are not enough because job variable can be itself and then it's not a second different call of got_for_it. It's just itself delayed again.
What I need to know here is which job call run or invoke_job on go_for_it method.
If I'm understanding well, you need to know which job is actually running.
You can use a custom job with a before hook to do an action before running the job, also you'll have totally access to job object at this moment.
Example :
class MyClassJob
def initialize(my_object: MyClass.new)
#my_object = my_object
end
def before(job)
binding.pry
another_job = Delayed::Job.where(queue: "my_queue").where('id <> ?', job.id)
end
def perform
#my_object.go_for_it
end
end
MyClassJob.new().delay.perform
In my Rails app, I'm trying to take my working API calls and have them handled by background workers.
I have the following in app/jobs/api_request_job.rb:
class ApiRequestJob
def self.perform(params)
Query.new(params).start
end
end
The Query class is where the HTTParty requests are being executed (there are lots of methods for different query types with the same basic format as the parks method:
require 'ostruct'
class Query
include FourSquare
attr_reader :results,
:first_address,
:second_address,
:queries,
:radius
def initialize(params)
#results = OpenStruct.new
#queries = params["query"]
#first_address = params["first_address"]
#second_address = params["second_address"]
#radius = params["radius"].to_f
end
def start
queries.keys.each do |query|
results[query] = self.send(query)
end
results
end
def parks
category_id = "4bf58dd8d48988d163941735"
first_address_results = FourSquare.send_request(#first_address, radius_to_meters, category_id)["response"]["venues"]
second_address_results = FourSquare.send_request(#second_address, radius_to_meters, category_id)["response"]["venues"]
response = [first_address_results, second_address_results]
end
And, finally, the controller. Before trying to farm this action out to background workers, this line was working fine: #results = Query.new(params).start
class ComparisonsController < ApplicationController
attr_reader :first_address, :second_address
def new
end
def show
#first_address = Address.new(params["first_address"])
#second_address = Address.new(params["second_address"])
if #first_address.invalid?
flash[:notice] = #first_address.errors.full_messages
redirect_to :back
elsif Query.new(params).queries.nil?
flash[:notice] = "You must choose at least one criteria for your comparison."
redirect_to comparisons_new_path(request.params)
else
#queries = params["query"].keys
#results = Resque.enqueue(ApiRequestJob, params) # <-- this is where I'm stuck
end
end
end
I'm running redis, have resque installed, and am running the task/starting the workers. The current value being returned for #results is true instead of the hash of results I was need to get back. Is there a way to have the results of the Resque job persist and return data instead of true? What am I missing about how to get background workers to return the same type of data my regular api calls were returning?
Many thanks in advance!
The true you are receiving means the job was scheduled enqueued successfully. The worker will pick it up and run it on the background asynchronously, which means, not at same time as the thread that enqueued it. So there's no way to retrieve the returned value from the job.
If you need the value from that process, you have to run it from the controller without the worker. Also, you wouldn't gain anything from just pushing the work to be done by another process as the web process would have to wait for the response to then keep going anyway.
If you need that returned value right away and are doing this for performance reasons, then you could look into other forms of concurrency, like having another thread doing the request and then only grabbing the result when you need it on the view like:
class AsyncValue
def initialize(&block)
#thr = Thread.new(&block)
end
def value
#thr.join
end
end
on the controller
#results = AsyncValue.new { Query.new(params).start }
and on the view
<%= #results.value.each .... %>
but you'd still have to work around error handling which can get pretty complicated, but is doable.
Personally, I'd just make the request in place, but you know your domain better than me.
I have a method like this that goes through an array to find different APIs and launch a delayed_job instance for every API found like this.
def refresh_users_list
apis_array.each do |api|
api.myclass.new.delay.get_and_create_or_update_users
end
end
I have an after_filter on users#index controller to trigger this method. This is creating many jobs to be triggered that will eventually cause too many connections problems on Heroku.
I'm wondering if there's a way I can check for the presence of a Job in the database by each of the API that the array iterates. This would be very helpful so I can only trigger a particular refresh if that api wasn't updated on a given time.
Any idea how to do this?
In config/application.rb, add the following
config.autoload_paths += Dir["#{config.root}/app/jobs/**/"]
Create a new directory at app/jobs/.
Create a file at app/jobs/api_job.rb that looks like
class ApiJob < Struct.new(:attr1, :attr2, :attr3)
attr_accessor :token
def initialize(*attrs)
self.token = self.class.token(attr1, attr2, attr3)
end
def display_name
self.class.token(attr1, attr2, attr3)
end
#
# Class methods
#
def self.token(attr1, attr2, attr3)
[name.parameterize, attr1.id, attr2.id, attr3.id].join("/")
end
def self.find_by_token(token)
Delayed::Job.where("handler like ?", "%token: #{token}%")
end
end
Note: You will replace attr1, attr2, and attr3 with whatever number of attributes you need (if any) to pass to the ApiJob to perform the queued task. More on how to call this in a moment
For each of your API's that you queue some get_and_create_or_update_users method for you'll create another Job. For example, if I have some Facebook api model, I might have a class at app/jobs/facebook_api_job.rb that looks like
class FacebookApiJob < ApiJob
def perform
FacebookApi.new.get_and_create_or_update_users(attr1, attr2, attr3)
end
end
Note: In your Question you did not pass any attributes to get_and_create_or_update_users. I am just showing you where you would do this if you need the job to have attributes passed to it.
Finally, wherever your refresh_users_list is defined, define something like this job_exists? method
def job_exists?(tokens)
tokens = [tokens] if !tokens.is_a?(Array) # allows a String or Array of tokens to be passed
tokens.each do |token|
return true unless ApiJob.find_by_token(token).empty?
end
false
end
Now, within your refresh_users_list and loop, you can build new tokens and call job_exists? to check if you have queued jobs for the API. For example
# Build a token
def refresh_users_list
apis_array.each do |api|
token = ApiJob.token(attr1, attr2, attr3)
next if job_exists?(token)
api.myclass.new.delay.get_and_create_or_update_users
end
end
Note: Again I want to point out, you won't be able to just drop in the code above and have it work. You must tailor it to your application and the job's you're running.
Why is this so complicated?
From my research, there's no way to "tag" or uniquely identify a queued job through what delayed_job provides. Sure, each job has a unique :id attribute. You could store the ID values for each created job in some hash somewhere
{
"FacebookApi": [1, 4, 12],
"TwitterApi": [3, 193, 44],
# ...
}
and then check corresponding hash key for an ID, but I find this limiting, and not always sufficient for the problem When you need to identify a specific job by multiple attributes like above, we must create a way to find these jobs (without loading every job into memory and looping over them to see if one matches our criteria).
How is this working?
The Struct that the ApiJob extends has a :token attribute. This token is based on the attributes passed (attr1, attr2, attr3) and is built when a new class extending ApiJob is instantiated.
The find_by_token class method simply searches the string representation of the job in the delayed_job queue for a match based on a token built using the same token class method.