When distributed locks might be helpful in Ruby on Rails application

Artur Trzop
Sep 15, 2017 · 7 min read

But where is the potential risk of the concurrency problem?

def test_files
create_queue unless queue_exists?

test_files_from_top_of_the_queue
end
# bin/api_test/unicorn.rb
worker_processes 5
timeout 40
preload_app true

before_fork do |server, worker|
Signal.trap 'TERM' do
puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
Process.kill 'QUIT', Process.pid
end

defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
end

after_fork do |server, worker|
Signal.trap 'TERM' do
puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT'
end

defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
end
#!/usr/bin/env ruby
# bin/api_test/initialize_queue
require 'knapsack_pro'
require 'rspec'

# Start rails server in development with unicorn
# to test concurrent requests
# $ bundle exec unicorn -p 3000 -c bin/api_test/unicorn.rb
#
# Run this file with rspec
# $ rspec bin/api_test/initialize_queue

node_total = 5

# use development API
ENV['KNAPSACK_PRO_MODE'] = 'development'
ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN'] = '333e7b8d1b64fd6447df34a77e3662eb'
ENV['KNAPSACK_PRO_LOG_LEVEL'] = 'warn'

test_files = [
{"path"=>"spec/bar_spec.rb"},
{"path"=>"spec/controllers/articles_controller_spec.rb"},
{"path"=>"spec/controllers/welcome_controller_spec.rb"},
{"path"=>"spec/dir with spaces/foobar_spec.rb"},
{"path"=>"spec/features/calculator_spec.rb"},
{"path"=>"spec/features/homepage_spec.rb"},
{"path"=>"spec/foo_spec.rb"},
{"path"=>"spec/services/calculator_spec.rb"},
{"path"=>"spec/services/meme_spec.rb"},
{"path"=>"spec/timecop_spec.rb"},
{"path"=>"spec/vcr_spec.rb"}
]
expected_test_files = KnapsackPro::TestFilePresenter.paths(test_files).sort

def test_files_from_queue(can_initialize_queue, commit_hash, branch, node_total, node_index, test_files)
action = KnapsackPro::Client::API::V1::Queues.queue(
can_initialize_queue: can_initialize_queue,
commit_hash: commit_hash,
branch: branch,
node_total: node_total,
node_index: node_index,
node_build_id: 'missing-build-id',
test_files: test_files,
)
connection = KnapsackPro::Client::Connection.new(action)
response = connection.call
if connection.success?
raise ArgumentError.new(response) if connection.errors?
KnapsackPro::TestFilePresenter.paths(response['test_files'])
else
raise ArgumentError.new("Couldn't connect with Knapsack Pro API. Response: #{response}")
end
end


commit_hash = SecureRandom.hex
all_test_files = []

threads = []

node_total.times do |node_index|
threads << Thread.new do
can_initialize_queue = true
node_all_test_files = []
while true
node_subset_test_files = test_files_from_queue(
can_initialize_queue,
commit_hash,
'api_test',
node_total,
node_index,
test_files
)
node_all_test_files += node_subset_test_files
can_initialize_queue = false

puts
puts "CI node: #{node_index}"
puts node_subset_test_files.inspect

break if node_subset_test_files.empty?
end
node_all_test_files
end
end

threads.each do |thr|
all_test_files += thr.join.value
end

describe 'Ensure queue API returns all test files without duplicates' do
it do
expect(all_test_files.sort).to eq expected_test_files
end
end

How to deal with concurrency problem

Distributed locking for the rescue

What distributed lock does?

Why you want a lock in a distributed application?

def test_files
semaphore_name = :ci_build_id # unique ID of CI build
expire_lock_after = 5 # seconds

semaphore = Redis::Semaphore.new(semaphore_name, host: "localhost")
semaphore.lock(expire_lock_after) do
create_queue unless queue_exists?
end

test_files_from_top_of_the_queue
end

Concurrency problem you most likely have too

def save
build = find_build || new_build # similar to find_or_initialize_by
do_something_complex_with_build
build.save
end

What should you remember?

Final tips


Artur Trzop

Written by

I work with Ruby and Elixir. I run https://knapsackpro.com. I write blog posts at http://docs.knapsackpro.com and http://beyondscheme.com/blog

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade