How to start bunny thread in Rails and Thin - ruby-on-rails-3

I'm integrating Bunny gem for RabbitMQ with Rails, should I start Bunny thread in an initializer that Rails starts with application start or do it in a separate rake task so I can start it in a separate process ?
I think if I'm producing messages only then I need to do it in Rails initializer so it can be used allover the app, but if I'm consuming I should do it in a separate rake task, is this correct ?

You are correct: you should not be consuming from the Rails application itself. The Rails application should be a producer, in which case, an initializer is the correct place to start the Bunny instance.
I essentially have this code in my Rails applications which publish messages to RabbitMQ:
# config/initializers/bunny.rb
MESSAGING_SERVICE = MessagingService.new(ENV.fetch("AMQP_URL"))
MESSAGING_SERVICE.start
# app/controllers/application_controller.rb
class ApplicationController
def messaging_service
MESSAGING_SERVICE
end
end
# app/controllers/uploads_controller.rb
class UploadsController < ApplicationController
def create
# save the model
messaging_service.publish_resize_image_request(model.id)
redirect_to uploads_path
end
end
# lib/messaging_service.rb
class MessagingService
def initialize(amqp_url)
#bunny = Bunny.new(amqp_url)
#bunny.start
at_exit { #bunny.stop }
end
attr_reader :bunny
def publish_resize_image_request(image_id)
resize_image_exchange.publish(image_id.to_s)
end
def resize_image_exchange
#resize_image_exchange ||=
channel.exchange("resize-image", passive: true)
end
def channel
#channel ||= bunny.channel
end
end
For consuming messages, I prefer to start executables without Rake involved. Rake will fork a new process, which will use more memory.
# bin/image-resizer-worker
require "bunny"
bunny = Bunny.new(ENV.fetch("AMQP_URL"))
bunny.start
at_exit { bunny.stop }
channel = bunny.channel
# Tell RabbitMQ to send this worker at most 2 messages at a time
# Else, RabbitMQ will send us as many messages as we can absorb,
# which would be 100% of the queue. If we have multiple worker
# instances, we want to load-balance between each of them.
channel.prefetch(2)
exchange = channel.exchange("resize-image", type: :direct, durable: true)
queue = channel.queue("resize-image", durable: true)
queue.bind(exchange)
queue.subscribe(manual_ack: true, block: true) do |delivery_info, properties, payload|
begin
upload = Upload.find(Integer(payload))
# somehow, resize the image and/or post-process the image
# Tell RabbitMQ we processed the message, in order to not see it again
channel.acknowledge(delivery_info.delivery_tag, false)
rescue ActiveRecord::RecordNotFound => _
STDERR.puts "Model does not exist: #{payload.inspect}"
# If the model is not in the database, we don't want to see this message again
channel.acknowledge(delivery_info.delivery_tag, false)
rescue Errno:ENOSPC => e
STDERR.puts "Ran out of disk space resizing #{payload.inspect}"
# Do NOT ack the message, in order to see it again at a later date
# This worker, or another one on another host, may have free space to
# process the image.
rescue RuntimeError => e
STDERR.puts "Failed to resize #{payload}: #{e.class} - #{e.message}"
# The fallback should probably be to ack the message.
channel.acknowledge(delivery_info.delivery_tag, false)
end
end
Given all that though, you may be better off with pre-built gems and using Rails' abstraction, ActiveJob.

Related

elixir test fails with "Reason: already started"

I'm currently working on my first big elixir project and wanted to properly utilize testing this time.
However, if I add my Modules to the "normal" supervisor, i cannot start them again with start_supervised! and all tests fail with Reason: already started: #PID<0.144.0>
Here is my code:
(application.ex)
defmodule Websocks.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
#moduledoc false
use Application
def start(_type, _args) do
children = [
{Websocks.PoolSupervisor, []},
{Websocks.PoolHandler, %{}}
# {Websocks.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Websocks.Supervisor]
Supervisor.start_link(children, opts)
end
end
Some of my tests:
defmodule PoolHandlerTest do
use ExUnit.Case, async: true
alias Websocks.PoolHandler
doctest PoolHandler
setup do
start_supervised!({PoolHandler, %{}})
%{}
end
test "adding two pools and checking if they are there" do
assert PoolHandler.add(:first) == :ok
assert PoolHandler.add(:second) == :ok
assert PoolHandler.get_pools() == {:ok,%{:first => nil, :second => nil}}
end
and the pool handler:
defmodule Websocks.PoolHandler do
use GenServer
# Client
def start_link(default) when is_map(default) do
GenServer.start_link(__MODULE__, default, name: __MODULE__)
end
# Server (callbacks)
#impl true
def init(arg) do
{:ok, arg}
end
end
(I cut out the stuff i think is not necessary, but the complete code is on github here: github)
Thanks in advance for any help i get!
As #Everett mentioned in the comment - your application will already be started for you when you mix test, so there is no need to start your GenServers again. It seems like you're interacting with the global instance in your test, so if that's what you want, then it should just work.
However, if you'd like to start a separate instance just for your test, you need to start an unnamed one. For example, you could add an optional pid argument to your wrapper functions:
defmodule Websocks.PoolHandler do
# ...
def add(server \\ __MODULE__, value) do
GenServer.call(server, {:add, value})
end
# ...
end
Then, instead of using using start_supervised! like you do, you can start an unnamed instance in your setup and use it in your tests like so:
setup do
{:ok, pid} = GenServer.start_link(PoolHandler, %{})
{:ok, %{handler: pid}}
end
test "adding two pools and checking if they are there", %{handler: handler} do
PoolHandler.add(handler, :first)
# ...
end

How to ensure background jobs run in integration tests?

I've got a integration test that is run through capybara. It visits a webpage, creates an object, and renders the results. When the object is created, the controller enqueues several jobs that create some related objects.
When I run my integration test, I want to be able to examine the rendered page as if those jobs had finished. The 2 obvious solutions are:
Set the queue adapter as :inline
Manually execute/clear the enqueued jobs after creating the object.
For 1), I've attempted to set the queue adapter in a before(:each) to :inline, but this does not change the adapter, it continues to use the test adapter (which is set in my test.rb config file):
before(:each) { ActiveJob::Base.queue_adapter = :inline }
after(:each) { ActiveJob::Base.queue_adapter = :test }
it "should work" do
puts ActiveJob::Base.queue_adapter
end
which outputs: #<ActiveJob::QueueAdapters::TestAdapter:0x007f93a1061ee0 #enqueued_jobs=[], #performed_jobs=[]>
For 2), I'm not sure if this is actually possible. ActiveJob::TestHelpers provides perform_enqueued_jobs, but this methods isn't helpful, as it seems to work only for jobs explicitly referenced in the passed in block.
Assuming you're using RSpec the easiest way to use perform_enqueued_jobs is with an around block. Combining that with metatdata tags you can do something like
RSpec.configure do |config|
config.include(RSpec::ActiveJob)
# clean out the queue after each spec
config.after(:each) do
ActiveJob::Base.queue_adapter.enqueued_jobs = []
ActiveJob::Base.queue_adapter.performed_jobs = []
end
config.around :each, perform_enqueued: true do |example|
#old_perform_enqueued_jobs = ActiveJob::Base.queue_adapter.perform_enqueued_jobs
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
example.run
ActiveJob::Base.queue_adapter.perform_enqueued_jobs = #old_perform_enqueued_jobs
end
config.around :each, peform_enququed_at: true do |example|
#old_perform_enqueued_at_jobs = ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs
ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
example.run
ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = #old_perform_enqueued_at_jobs
end
end
Note: you need to specify the queue_adapter as :test in your config/environments/test.rb if it's not already set
You can then specify :perform_enqueued metadata on a test and any jobs specified will be run
it "should work", :perform_enqueued do
# Jobs triggered in this test will be run
end

Sinatra app as rack middleware TimesOut Rails 3

While in the Rails development environment, I am attempting to add a Sinatra app as a middleware. The Sinatra app uses the geoip gem that processes a user's ip address and returns json with their city.
I can view the returned json by going directly to the example url in the browser or using curl in the command line, http://local.fqdn.org/geoip/locate.json?ip=24.18.211.123. However when I attempt to call the url with wget from within a Rails controller, the Rails app stops responding often crashing my browser and my rails server wont exit using the control+C command.
Any clue to what is happening here? Why would going directly to the url in the browser return the proper response but my call in the controller results in a Time Out?
sinatra-geoip.rb
require 'sinatra'
require 'geoip'
require 'json'
# http://localhost/geoip/locate.json?ip=24.18.211.123
#
# {
# latitude: 47.684700012207
# country_name: "United States"
# area_code: 206
# city: "Seattle"
# region: "WA"
# longitude: -122.384803771973
# postal_code: "98117"
# country_code3: "USA"
# country_code: "US"
# dma_code: 819
# }
class GeoIPServer < Sinatra::Base
get '/geoip/locate.json' do
c = GeoIP.new('/var/www/mywebsite.org/current/GeoLiteCity.dat').city(params[:ip])
body c.to_h.to_json
end
end
routes.rb
mount GeoIPServer => "/geoip"
config/environments/development.rb
Website::Application.configure do
require "sinatra-geoip"
config.middleware.use "GeoIPServer"
...
end
controller
raw_geo_ip = Net::HTTP.get(URI.parse("http://#{geoip_server}/geoip/locate.json?ip=#{request.ip}"))
#geo_ip = JSON.parse(raw_geo_ip)
Our solution was difficult to find. We ended up finding a method in the sinatra source code call forward.
new sinatra-geoip.rb
class GeoIPServer < Sinatra::Base
if defined?(::Rails)
get '/properties.json' do
env["geo_ip.lookup"] = geo_ip_lookup(request.ip)
forward
end
end
def geo_ip_lookup(ip = nil)
ip = ip.nil? ? params[:ip] : ip
result = GeoIP.new('/var/www/mywebsite.org/current/GeoLiteCity.dat').city(ip)
result.to_h.to_json
end
end
Essentially, we removed the /geoip/locate.json route from the file and converted it to a simple method. We needed the geoip lookup to occur when the properties.json was being called, so a new param was added with the geoip information. Then we set the new param equal to #geo_ip variable in the controller.
New properties controller
if Rails.env.development? or Rails.env.test?
# Retrieves param set by sinatra-geoip middleware.
#geo_ip = JSON.parse(env["geo_ip.lookup"] || "{}")
else
# Production and staging code
end
Rather obscure problem and solution. Hopefully it will help someone out there. Cheers.

How to determine ActiveModel::Errors validation type

With the migration from Rails 2 to Rails 3 validation errors were moved from ActiveRecord::Error to ActiveModel::Errors.
In rails 2 the validation error had a type and a message (among other things) and you could check the type of the validation error by doing something like the following:
rescue ActiveRecord::RecordInvalid => e
e.record.errors.each do |attr, error|
if error.type == :foo
do_something
end
end
end
But with Rails 3 it seems everything but the invalid attribute and message has been lost. As a result the only way to determine the type is to compare the error message:
rescue ActiveRecord::RecordInvalid => e
e.record.errors.each do |attr, error|
if error == "foobar"
do_something
end
end
end
Which is not at all ideal (eg. what if you have several validations which use the same message?).
Question:
Is there a better way in rails 3.0 to determine the type of validation error?
Check for added? on ActiveModel::Errors:
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/errors.rb#L331
That allows you to do this:
record.errors.added?(:field, :error)
I needed it not only for test purposes, but also for API. I've ended up with monkey patch:
module CoreExt
module ActiveModel
module Errors
# When validation on model fails, ActiveModel sets only human readable
# messages. This does not allow programmatically identify which
# validation rule exactly was violated.
#
# This module patches {ActiveModel::Errors} to have +details+ property,
# that keeps name of violated validators.
#
# #example
# customer.valid? # => false
# customer.errors.messages # => { email: ["must be present"] }
# customer.errors.details # => { email: { blank: ["must be present"] } }
module Details
extend ActiveSupport::Concern
included do
if instance_methods.include?(:details)
fail("Can't monkey patch. ActiveModel::Errors already has method #details")
end
def details
#__details ||= Hash.new do |attr_hash, attr_key|
attr_hash[attr_key] = Hash.new { |h, k| h[k] = [] }
end
end
def add_with_details(attribute, message = nil, options = {})
error_type = message.is_a?(Symbol) ? message : :invalid
normalized_message = normalize_message(attribute, message, options)
details[attribute][error_type] << normalized_message
add_without_details(attribute, message, options)
end
alias_method_chain :add, :details
def clear_with_details
details.clear
clear_without_details
end
alias_method_chain :clear, :details
end
end
end
end
end
# Apply monkey patches
::ActiveModel::Errors.send(:include, ::CoreExt::ActiveModel::Errors::Details)

Why after_commit not running even with use_transactional_fixtures = false

Transactional fixtures in rspec prevent after_commit from being called, but even when I disable them with
RSpec.configure do |config|
config.use_transactional_fixtures = false
end
The after_commit callback does not run.
Here is a rails app with the latest rspec / rails that I have produced the issue on:
git://github.com/sheabarton/after_commit_demo.git
One way around this is to trigger the commit callbacks manually. Example:
describe SomeModel do
subject { ... }
context 'after_commit' do
after { subject.run_callbacks(:commit) }
it 'does something' do
subject.should_receive(:some_message)
end
end
end
A little late, but hope this helps others.
In my case I resolved such problem with database_cleaner's settings placed below:
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.strategy = :deletion
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
Thanks to Testing after_commit/after_transaction with Rspec
This is similar to #jamesdevar's answer above, but I couldn't add a code block, so I have to make a separate entry.
You don't have the change the strategy for the whole spec suite. You can keep using :transaction globally then just use :deletion or :truncation (they both work) as needed. Just add a flag to the relevant spec.
config.use_transactional_fixtures = false
config.before(:suite) do
# The :transaction strategy prevents :after_commit hooks from running
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each, :with_after_commit => true) do
DatabaseCleaner.strategy = :truncation
end
then, in your specs:
describe "some test requiring after_commit hooks", :with_after_commit => true do
If you're using database_cleaner you'll still run into this. I'm using the test_after_commit gem, and that seems to do the trick for me.
This Gist helped me.
It monkey-patches ActiveRecord to fire after_commit callbacks even if using transactional fixtures.
module ActiveRecord
module ConnectionAdapters
module DatabaseStatements
#
# Run the normal transaction method; when it's done, check to see if there
# is exactly one open transaction. If so, that's the transactional
# fixtures transaction; from the model's standpoint, the completed
# transaction is the real deal. Send commit callbacks to models.
#
# If the transaction block raises a Rollback, we need to know, so we don't
# call the commit hooks. Other exceptions don't need to be explicitly
# accounted for since they will raise uncaught through this method and
# prevent the code after the hook from running.
#
def transaction_with_transactional_fixtures(options = {}, &block)
rolled_back = false
transaction_without_transactional_fixtures do
begin
yield
rescue ActiveRecord::Rollback => e
rolled_back = true
raise e
end
end
if !rolled_back && open_transactions == 1
commit_transaction_records(false)
end
end
alias_method_chain :transaction, :transactional_fixtures
#
# The #_current_transaction_records is an stack of arrays, each one
# containing the records associated with the corresponding transaction
# in the transaction stack. This is used by the
# `rollback_transaction_records` method (to only send a rollback hook to
# models attached to the transaction being rolled back) but is usually
# ignored by the `commit_transaction_records` method. Here we
# monkey-patch it to temporarily replace the array with only the records
# for the top-of-stack transaction, so the real
# `commit_transaction_records` method only sends callbacks to those.
#
def commit_transaction_records_with_transactional_fixtures(commit = true)
unless commit
real_current_transaction_records = #_current_transaction_records
#_current_transaction_records = #_current_transaction_records.pop
end
begin
commit_transaction_records_without_transactional_fixtures
rescue # works better with that :)
ensure
unless commit
#_current_transaction_records = real_current_transaction_records
end
end
end
alias_method_chain :commit_transaction_records, :transactional_fixtures
end
end
end
Put this a new file in your Rails.root/spec/support directory, e.g. spec/support/after_commit_with_transactional_fixtures.rb.
Rails 3 will automatically load it in the test environment.