I have a worker method which looks like this
class ScoreWorker
include Sidekiq::Worker
sidekiq_options queue: 'score_worker'
def perform(scores, notification, devices)
threads = []
scores.each do |gg|
threads << Thread.new { PushNotificationService.new(notification, devices).deliver }
end
threads.each { |thr| thr.join }
end
I am writing an rspec test for my worker
describe ScoreWorker do
self.use_transactional_fixtures
before(:all) do
Rails.cache.clear
end
describe '.perform' do
self.use_transactional_fixtures
context 'create' do
it 'creates notification' do
job = create(:job)
notification = create(:notification)
devices = create(:device)
scores = ScoringService.new(job, 25).score
worker = ScoreWorker.new
expect do
worker.perform(scores, notification, devices)
end.to change(Notification, :count).by(1)
end
end
end
end
But whenever i run my tests, it keeps failing as rspec doesnt seem to support threading. So my job, notification and devices object are all empty.
How can i run my tests when using ruby threads in rspec
Related
I have been experimenting multi-threading concept in Ruby for the past a week.
For practising, I am designing a file downloader that makes parallel requests for a collection of URLs. Currently I need to safely shutdown threads when interrupt signal is triggered. I have read the theory of multi-threading and catching a signal at runtime. Yet despite the whole those theoretical knowledge, I still don't have any idea about how to use them in practice.
I am leaving my proof of concept work below, anyhow.
class MultiThread
attr_reader :limit, :threads, :queue
def initialize(limit)
#limit = limit
#threads = []
#queue = Queue.new
end
def add(*args, &block)
queue << [block, args]
end
def invoke
1.upto(limit).each { threads << spawn_thread }
threads.each(&:join)
end
private
def spawn_thread
Thread.new do
Thread.handle_interrupt(RuntimeError => :on_blocking) do
# Nothing to do
end
until queue.empty?
block, args = queue.pop
block&.call(*args)
end
end
end
end
urls = %w[https://example.com]
thread = MultiThread.new(2)
urls.each do |url|
thread.add do
puts "Downloading #{url}..."
sleep 1
end
end
thread.invoke
Yeah, the docs for handle_interrupt are confusing. Try this, which I based on the connection_pool gem used by e.g. puma.
$stdout.sync = true
threads = 3.times.map { |i|
Thread.new {
Thread.handle_interrupt(Exception => :never) do
begin
Thread.handle_interrupt(Exception => :immediate) do
puts "Thread #{i} doing work"
sleep 1000
end
ensure
puts "Thread #{i} cleaning up"
end
end
}
}
Signal.trap("INT") {
puts 'Exiting gracefully'
threads.each { |t|
puts 'killing thread'
t.kill
}
exit
}
threads.each { |t| t.join }
Output:
Thread 1 doing work
Thread 2 doing work
Thread 0 doing work
^CExiting gracefully
killing thread
killing thread
killing thread
Thread 0 cleaning up
Thread 1 cleaning up
Thread 2 cleaning up
I'm trying to run a Sinatra app in a new Thread in order to also run some other thing inside my script but when I do:
require 'sinatra/base'
class App < Sinatra::Base
...some routes...
end
Thread.new do
App.run!
end
Nothing happens and Sinatra server is not started. Is there anything I'm missing in order to achieve this?
Finally I run the other ruby process in a Thread but from Sinatra application and it works just fine.
class App < Sinatra::Base
threads = []
threads <<
Thread.new do
Some::Other::Thing
rescue StandardError => e
$stderr << e.message
$stderr << e.backtrace.join("\n")
end
trap('INT') do
puts 'trapping'
threads.each do |t|
puts 'killing'
Thread.kill t
end
end
run!
end
And I added a control when Sinatra app is exited also kill the open thread.
I have a jruby class which contains a heartbeat that does something every certain number of seconds: (simplified code below)
class Client
def initialise
#interval = 30
#heartbeat = Thread.new do
begin
loop do
puts "heartbeat"
sleep #interval
end
rescue Exception => e
Thread.main.raise e
end
end
end
end
And I have a range of rspec tests that instantiate this class.
At the end of each test, I would expect the object to be destroyed, but the threads seem to remain.
At the moment I've fixed this with:
client.rb:
def kill
#heartbeat.kill
end
rspec:
after(:all) do
client.kill
end
Which seems to do the job - but this doesn't feel like the best way to do it.
What is the best way to approach this?
Using version jruby-9.1.10.0 & rspec 3.7.0
Edit:
As per http://ruby-doc.org/core-2.4.0/Thread.html I would expect the thread to normally terminate when the main thread does
In my tests I instantiate the client with
describe Client do
context 'bla' do
let(:client) do
described_class.new
end
it 'blas' do
end
end
end
You should replace after(:all) with after(:each).
Should be the correct syntax for what you want to do because after(:all) evaluates after all test cases have been run.
I have a class that start a Thread when initialize and I can push some actions in this thread from public methods :
class Engine
def initialize
#actions = []
self.start_thread()
end
def push_action(action)
start = #actions.empty?
#actions.push(action)
if start
#thread.run
end
end
protected
def start_thread
#thread = Thread.new do
loop do
if #actions.empty?
Thread.stop
end
#actions.each do |act|
# [...]
end
#actions.clear
sleep 1
end
end
end
end
I'd like to test this class with RSpec to check what happen when I pass some actions. But I don't know how to do that.
Thanks in advance
OK I've found a solution but it's quite dirty :
describe Engine do
describe "#push_action play" do
it "should do the play action" do
# Construct the Engine with a mock thread
mock_thread = double("Useless Thread")
allow(mock_thread).to receive(:run)
expect(Thread).to receive(:new).and_return(mock_thread)
engine = Engine.new
allow(engine).to receive(:sleep)
# Expect that the actual play action will be processed
expect(engine).to receive(:play)
# push the action in the actions' list
engine.push_action(:play)
# Stop the thread loop when the stop() method is called
expect(Thread).to receive(:stop).and_raise(StandardError)
# Manually call again the start_thread() protected method with a yielded thread
# in order to process the action
expect(Thread).to receive(:new).and_yield
expect {engine.send(:start_thread)}.to raise_error(StandardError)
end
end
end
If someone has a better solution I'd be very pleased :)
I have the following test setup for a sidekiq test using the fake testing in https://github.com/mperham/sidekiq/wiki/Testing.
spec_helper.rb
require 'sidekiq/testing'
Sidekiq::Testing.fake!
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
RSpec.configure do |config|
config.before(:suite) do
FactoryGirl.reload
FactoryGirl.define do
to_create { |instance| instance.save }
end
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
Airbrake.configure do |c|
c.project_id = ENV['AIRBRAKE_PROJECT_ID']
c.project_key = ENV['AIRBRAKE_PROJECT_KEY']
end
end
config.before(:each, job: true) do
Sidekiq::Worker.clear_all #make sure jobs don't linger between tests
end
config.around(:each) do |example|
DatabaseCleaner.cleaning do
example.run
end
end
config.include FactoryGirl::Syntax::Methods
end
notification_spec.rb
require 'spec_helper'
RSpec.describe NotificationWorker do
it "perform should call Airbrake#notify", job: true do
notification_worker = NotificationWorker.new
message = "This is your error message"
expect { notification_worker.perform(message) }.to change(NotificationWorker.jobs, :size).by(1)
end
end
notification_worker.rb
class NotificationWorker
include Sidekiq::Worker
sidekiq_options queue: :high
def perform(message)
Airbrake.notify(message)
end
end
Yet, why do I receive the following error message:
Failure/Error: expect { notification_worker.perform(message) }.to change(NotificationWorker.jobs, :size).by(1)
expected #size to have changed by 1, but was changed by 0
It seems as if the jobs array should be incremented by 1. What is going on? Is it a threading issue caused by the interaction between RSpec and Database Cleaner?
Because it's calling the perform method directly and not Sidekiq's API.
Try NotificationWorker.perform_async(message)