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.
Related
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
I have built an app that consumes a json api. I removed active record from my app because the data in the api can theoretically change and I don't want to wipe the database each time.
Right now I have a method called self.all for each class that loops through the json creating ruby objects. I then call that method in various functions in order to work with the data finding sums and percentages. This all works fine, but seems a bit slow. I was wondering if there is somewhere I should be storing my .all call rather than instantiating new objects for each method that works with the data.
...response was assign above using HTTParty...
def self.all
puppies = []
if response.success?
response['puppies'].each do |puppy|
accounts << new(puppy['name'],
puppy['price'].to_money,
puppy['DOB'])
end
else
raise response.response
end
accounts
end
# the methods below only accept arguments to allow testing with Factories
# puppies is passed in as Puppy.all
def self.sum(puppies)
# returns money object
sum = Money.new(0, 'USD')
puppies.each do |puppy|
sum += puppy.price
end
sum
end
def self.prices(puppies)
prices = puppies.map { |puppy| puppy.price }
end
def self.names(puppies)
names = puppies.map { |puppy| puppy.name }
end
....many more methods that take an argument of Puppy.all in the controller....
Should I use cacheing? should I bring back active record? or is how I'm doing it fine? Should I store Puppy.all somewhere rather than calling the method each time?
What I guess is happening is that you are making a request with HTTParty every time you call any class method. What you can consider is creating a class variable for the response and a class variable called expires_at. Then you can do some basic caching.
##expires_at = Time.zone.now
##http_response
def make_http_call
renew_http_response if ##expires_at.past?
end
def renew_http_response
# make HTTParty request here
##http_response = # HTTParty response
##expires_at = 30.minutes.from_now
end
# And in your code, change response to ##response
# ie response.success? to ##response.success?
This is all in memory and you lose everything if you restart your server. If you want more robust caching, the better thing to do would probably to look into rails low-level caching
I'm using sidekiq to deal with async jobs, and after some complexity added, I'm having difficulties to be aware of the state of the jobs.
Here's the deal:
I have a model Batch that calls an async method after it's commited:
# app/models/batch.rb
class Batch < ActiveRecord::Base
after_commit :calculate, on: :create
def calculate
job_id = BatchWorker.perform_async(self.id)
# update_column skips callbacks and validations!
self.update_column(:job_id, job_id)
end
end
The worker reads data from the model and calls an async job for each data, as follows:
# app/workers/batch_worker.rb
class BatchWorker
def perform(batch_id)
batch = Batch.find(batch_id)
## read data to 'tab'
tab.each do |ts|
obj = batch.item.create(name: ts[0], data: ts[1])
job_id = ItemWorker.perform_async(obj.id)
obj.update_attribute(:job_id, job_id)
end
end
end
The problem is: Those async jobs perform calculations, and I can't allow the download results link be available before it's complete, so I need to know when all "children-jobs" are done, so I can change a status attribute from the Batch model. In other words, I don't need to know if all jobs have been queued, but instead, if all async jobs generated by ItemWorker have been performed, and are now complete.
What would be the best way to attain this? Does it make sense in the "parallel computation world"?
Obs.: I'm not sure about storing the job_id in db, since it seems to be volatile.
Perhaps using Redis for this could be a good fit, seeing as you already have it in your infrastructure and configured in your Rails app (due to Sidekiq)
Redis has an inbuilt publish/subscribe engine, as well as atomic operations on keys - making it suitable for managing the type of concurrency you are looking for.
Maybe something roughly like this:
class BatchWorker
def perform(batch_id)
batch = Batch.find(batch_id)
redis = Redis.new
redis.set "jobs_remaining_#{batch_id}", tab.count
redis.subscribe("batch_task_complete.#{batch_id}") do |on|
on.message do |event, data|
if redis.decr("jobs_remaining_#{batch_id}") < 1
#UPDATE STATUS HERE
redis.del "jobs_remaining_#{batch_id}"
end
end
end
tab.each do |ts|
obj = batch.item.create(name: ts[0], data: ts[1])
job_id = ItemWorker.perform_async(obj.id, batch_id)
end
end
end
class ItemWorker
def perform item_id, batch_id=nil
#DO STUFF
if batch_id
Redis.new.publish "batch_task_complete.#{batch_id}"
end
end
end
Is there a way to pre-build a page cache without calling the actual page via a http request?
I looked at solutions like this and this, but these don't generate the cache.
I have a relatively complicated view, and want to cache the entire thing. I want to pre-build this cached version in the application so when a user actually hits it, it will already be there.
Thanks
We had a need to do something similar from a rake task -- we had a partial that would need to display a very long list of entities (~700) which were somewhat context specific and which, due to a series of database structure issues and custom sorting criteria, would easily take > 25 seconds to render the first time before going into cache> This would often time out because our HTTP servers were set to terminate HTTP requests with no response after 30 seconds, and pre-caching this custom list was a solution.
What you need to do is create an instance of ActiveController::Base, or of one of your controllers if you need helper methods or other entities, then pass its lookup_context reference to a new instance of ActionView.Renderer.
In our rake task, we did the following
namespace :rake_for_time_consuming_nonsense do
task :pre_cache_long_list do
PreCacher.pre_fetch_partials
end
end
class PreCacher
def self.pre_fetch_partials
the_controller = ActionController::Base.new
# Set any instance variables required by your partial in the controller,
# they will be passed to the partial with the view_context reference
the_controller.instance_variable_set "#cache_key", cache_key
the_controller.instance_variable_set "#the_object", MyModel.first
view_renderer = ActionView::Renderer.new the_controller.lookup_context
view_renderer.render the_controller.view_context, {partial: 'my_model/the_partial', layout: false}
end
end
This works in Rails 3.2.13.
I think following link should give you a good start.
How do I get the rendered output of a controller's action without visiting the web page?
I try to accomplish the same and as far i can see, your fake request should have the correct host, because the cache-key includes host informations.
I accomplished caching by using ActionController::Integration::Session
ais = ActionController::Integration::Session.new
ais.host = host
ais.xml_http_request(:post, url, params, headers)
I'v got another one:
class FakeRequest
include ActionController::UrlWriter
def initialize(url, params, session, host)
#url = url
#params = params
#session = session
default_url_options[:host] = URI.parse(host).host
end
def post
process(:post)
end
def get
process(:get)
end
def xhr
process(:post, true)
end
def process(method, ajax = false)
uri = URI.parse(url_for(#url))
request = ActionController::TestRequest.new({'HTTP_HOST' => uri.host,'rack.input' => '','rack.url_scheme' => 'http'})
request.query_parameters = #params
request.path = uri.path
request.host = uri.host
request.env['REQUEST_METHOD'] = method.to_s.upcase
if ajax
request.headers['X-Requested-With'] = 'XMLHttpRequest'
end
#session.each_pair do |k,v|
request.session[k] = v
end
response = ActionController::TestResponse.new
controller = ActionController::Routing::Routes.recognize(request).new
return controller.process(request, response)
end
end
This will also return the response object.
I have been trying to get my head around render_to but I haven't had much success.
Essentially I have controller methods:
def first
#I want to get the value of VAR1 here
end
def second
VAR1 = ["Hello", "Goodbye"]
render_to ??
end
What I can't figure out is how to accomplish that. Originally I just wanted to render the first.html.erb file but that didn't seem to work either.
Thanks
Edit: I appreciate the answers I have received, however all of them tend to avoid using the render method or redirect_to. Is it basically the case then that a you cannot pass variables from controller to controller? I have to think that there is some way but I can't seem to find it.
It is not a good idea to assign the object to a constant. True this is in a global space, but it is global for everyone so any other user going to this request will get this object. There are a few solutions to this.
I am assuming you have a multi-step form you are going through. In that case you can pass the set attributes as hidden fields.
<%= f.hidden_field :name %>
If there are a lot of fields this can be tedious so you may want to loop through the params[...] hash or column_names method to determine which attributes to pass.
Alternatively you can store attributes in the session.
def first
#item = Item.new(params[:item])
session[:item_attributes] = #item.attributes
end
def second
#item = Item.new(session[:item_attributes])
#item.attributes = params[:item]
end
Thirdly, as Paul Keeble mentioned you can save the model to the database but mark it as incomplete. You may want to use a state machine for this.
Finally, you may want to take a look at the Acts As Wizard plugin.
I usually don't have my controllers calling each other's actions. If you have an identifier that starts with a capital letter, in Ruby that is a constant. If you want to an instance level variable, have it start with #.
#var1 = ["Hello", "Goodbye"]
Can you explain what your goal is?
Have you considered using the flash hash? A lot of people use it solely for error messages and the like, it's explicitly for the sort of transient data passing you might be interested in.
Basically, the flash method returns a hash. Any value you assign to a key in the hash will be available to the next action, but then it's gone. So:
def first
flash[:var] = ["hello", "goodbye"]
redirect_to :action => :second
end
def second
#hello = flash[:var].first
end
way 1
Global variable
(fail during concurrent requests)
way 2
class variable
(fail during concurrent requests)
way 3
Stash the object on the server between requests. The typical way is to save it in the session, since it automatically serializes/deserializes the object for you.
Serialize the object and include it in the form somewhere, and
deserialize it from the parameters in the next request. so you can store attributes in the session.
def first
#item = Item.new(params[:item])
session[:item_attributes] = #item.attributes
end
def second
#item = Item.new(session[:item_attributes])
#item.attributes = params[:item]
end
way 4
The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed to the very next action and then cleared out.
def new
#test_suite_run = TestSuiteRun.new
#tests = Test.find(:all, :conditions => { :test_suite_id => params[:number] })
flash[:someval] = params[:number]
end
def create
#test_suite_run = TestSuiteRun.new(params[:test_suite_run])
#tests = Test.find(:all, :conditions => { :test_suite_id => flash[:someval] })
end
way 5
you can use rails cache.
Rails.cache.write("list",[1,2,3])
Rails.cache.read("list")
But what happens when different sessions have different values?
Unless you ensure the uniqueness of the list name across the session this solution will fail during concurrent requests
way 6
In one action store the value in db table based on the session id and other action can retrieve it from db based on session id.
way 7
class BarsController < UsersController
before_filter :init_foo_list
def method1
render :method2
end
def method2
#foo_list.each do | item|
# do something
end
end
def init_foo_list
#foo_list ||= ['Money', 'Animals', 'Ummagumma']
end
end
way 8
From action sent to view and again from view sent to other actions in controller.