After make a big class with many method, I want all of these method be called in a delayed jobs.
But the practice of the Delayed::job, is that you have to create a class with a perform method, like that :
class Me < Struct.new(:something)
def perform
puts "GO"
end
end
and call it like :
Delayed::Job.enqueue Me.new(1)
But the problem is that my class as already many method like this type
class NameHandler
def self.init
ap "TODO : Delayed jobs "
end
def self.action_one
...
end
def self.action_two
...
end
etc.
end
and I want to call it like :
Delayed::Job.enqueue NameHandler.action_one params...
Theres is an best practice for that ? Or I have to follow the classic Delayed::job way and lose many times ?
In the README it has a number of ways:
Me.new.delay.action_one
or
class NameHandler
handle_asynchronously :action_one
def action_one
end
def self.action_one
new.action_one
end
end
NameHandler.action_one
Related
Lets say I have a method in my model
class Mod < ...
after_create :update_some_stuff
private
def update_some_stuff
....
end
end
And I want to ensure that "update some stuff" is only called by after create, and to raise an error if it is called in any other context. Is there a way to do this in Ruby on Rails?
maybe you can use a block and avoid doing weird tricks to prevent the method getting called outside the context you want:
after_create do
....
end
It's hacky, and you probably don't need to be testing this because Rails tests this for you, but this should work:
class Mod < ...
after_create :update_some_stuff
private
def update_some_stuff
return unless id_previously_changed?
# do something
end
end
This hooks into previous_changes which is implemented by ActiveModel::Dirty.
How can I refactor this ruby code using the Open/Closed principle or Strategy pattern ?
I know that the main thought is 'software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification' but how can I use this in practice?
class PaymentService
def initialize(payment, payment_type)
#payment = payment
#payment_type = payment_type
end
def process
result = case payment_type
when 'first'
process_first
when 'second'
process_second
end
payment.save(result)
end
def process_first
'process_first'
end
def process_second
'process_second'
end
end
In this example, instead of passing a payment_type you can build an object with a class that processes a payment:
class FirstPayment
def process
'process_first'
end
end
class SecondPayment
def process
'process_second'
end
end
class PaymentService
def initialize(payment, payment_strategy)
#payment = payment
#payment_strategy = payment_strategy
end
def process
result = #payment_stategy.process
payment.save(result)
end
end
PaymentService.new(payment, FirstPayment.new)
As a result, PaymentService behaviour can be extended by passing a new strategy (for example, ThirdPayment), but the class doesn't need to be modified, if the logic of processing the first or the second payments is changed.
Below is passing!
Controller code:
class OrdersController
def create
...
#order.save
end
end
Spec code:
describe OrdersController do
it "should call save method" do
Order.any_instance.should_receive(:save)
post :create
end
end
But if only it were that easy... I have some custom job objects that are executed after the save, so the code actually looks like this:
Controller code:
class OrdersController
def create
...
#order.save
RoadrunnerEmailAlert.new.async.perform(#order.id, true)
CalendarInvite.new.async.perform(#order.id)
RoadrunnerTwilioAlert.new.async.perform(#order.id)
end
end
I would love to test that the custom objects are receiving the chain of methods with the right parameters, but not sure how, short of creating something in the spec code like this:
before do
class RoadrunnerEmailAlert
def async
end
end
end
But that's so contrived, it certainly isn't right... advice appreciated!
In case this helps other people... this is a very comprehensive answer.
Context & design notes
The async framework is Sucker Punch gem
(http://brandonhilkert.com/blog/why-i-wrote-the-sucker-punch-gem/).
Back then, this was the easiest thing for me to use after looking at
Delayed Job, Sidekick, etc
Basically it works like this: in Controller reference a Job that then references anything else (in my case, some POROs)
If I were really rigidly testing, I'd want to test that A) the Controller calls the Job appropriately and passes the right parameters, and B) the Job calls the appropriate POROs and passes the right parameters. But instead, I just tested that the Controller calls the appropriate POROs and passes the right parameters, i.e., the Jobs are already working.
Controller code
#order.save
RoadrunnerEmailAlert.new.async.perform(#order.id, true)
CalendarInvite.new.async.perform(#order.id)
RoadrunnerTwilioAlert.new.async.perform(#order.id)
Job code
# app/jobs/roadrunner_email_alert.rb
class RoadrunnerEmailAlert
include SuckerPunch::Job
def perform(order_id, require_tos)
ActiveRecord::Base.connection_pool.with_connection do
OrderMailer.success_email(order_id, require_tos).deliver
end
end
end
# app/jobs/calendar_invite.rb
class CalendarInvite
include SuckerPunch::Job
def perform(order_id)
ActiveRecord::Base.connection_pool.with_connection do
CreateCalendar.new(order_id).perform
end
end
end
# app/jobs/roadrunner_twilio_alert.rb
class RoadrunnerTwilioAlert
include SuckerPunch::Job
def perform(order_id)
ActiveRecord::Base.connection_pool.with_connection do
CreateAlert.new(order_id).perform
end
end
end
Test code
The really big thing here that I don't know why I keep forgetting (but only in testing) is class vs. instance of class. For the POROs, since I'm instantiating the object, I needed to test 2 different "layers" (first that the object is instantiated appropriately, second that the instantiated object is acted upon appropriately).
require 'sucker_punch/testing/inline'
describe "Controller code" do
before do
OrderMailer.any_instance.stub(:success_email)
mock_calendar = CreateCalendar.new(1)
CreateCalendar.stub(:new).and_return(mock_calendar)
CreateCalendar.any_instance.stub(:perform)
mock_alert = CreateAlert.new(1)
CreateAlert.stub(:new).and_return(mock_alert)
CreateAlert.any_instance.stub(:perform)
end
it "should call appropriate async jobs" do
expect_any_instance_of(OrderMailer).to receive(:success_email).with(1, true)
expect(CreateCalendar).to receive(:new).with(1)
expect_any_instance_of(CreateCalendar).to receive(:perform)
expect(CreateAlert).to receive(:new).with(1)
expect_any_instance_of(CreateAlert).to receive(:perform)
post :create
end
end
How I can do something like that at sidekiq?
Gateway::AddUser.delay.new(6).call
For now, Gateway::AddUser.delay.new(6) return a string, and call method trying to run on it. But I want to call just Gateway::AddUser.new(6).call delayed
Solved just like this:
Gateway::AddUser.delay.perform(6)
Where perform method is:
def self.perform(params)
new(params).call
end
Just rewrite 2 methods to 1 =)
why don't you just wrap that in other method?
class User
def self.add_user_via_gateway(attributes)
Gateway::AddUser.new(attributes).call
end
end
User.delay.add_user_via_gatway(attributes)
EDIT:
If you prefer, you can also create a worker class.
class AddUserViaGatewayWorker
include Sidekiq::Worker
def perform(attributes)
Gateway::AddUser.new(attributes).call
end
end
AddUserViaGatewayWorker.perform_async(attributes)
In order to start delayed_job's on a schedule you need to have helper classes with a perform method that delayed_job can call. These need to be defined before any of the classes that use them to create scheduled delayed_jobs are called. All very short, and many of them in my case. For example:
class AccountUpdateJob < Struct.new(:account_id)
def perform
acct = Account.find(account_id)
acct.api_update
end
end
I'm doing this in a file called "dj_helper_classes" in the initializers folder. Is that the right thing to do?
I keep mine in lib/jobs, one file per class. So, your example would be in lib/jobs/account_update_job.rb
module Jobs
class AccountUpdateJob < Struct.new(:account_id)
def perform
acct = Account.find(account_id)
acct.api_update
end
end
end