-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: allow custom locking mechanism #20
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,6 @@ gemfiles | |
|
||
## Ignore lock file | ||
Gemfile.lock | ||
|
||
# Ignore all logfiles | ||
/log/* |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,5 @@ group :development do | |
gem 'appraisal', '~> 2.4' | ||
gem 'rspec' | ||
gem 'sqlite3', '~> 1.4' | ||
gem 'with_advisory_lock' | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
require 'with_advisory_lock' if defined?(WithAdvisoryLock) | ||
|
||
# The default locking mechanism is to use the with_advisory_lock gem | ||
# But this can be overriden using an initializer in the host Rails application (refer to README.md) | ||
# This is set in lib/metricks/engine.rb | ||
# Because of this, the with_advisory_lock gem is not a hard dependency. | ||
module Metricks | ||
class Lock | ||
|
||
class << self | ||
attr_accessor :with_lock | ||
|
||
def with_lock(key, opts = {}, &block) | ||
with_lock_block = @with_lock || default_with_lock | ||
|
||
instance_exec(key, opts, block, &with_lock_block) | ||
end | ||
|
||
def validate! | ||
return if @with_lock.present? | ||
return if defined?(WithAdvisoryLock) | ||
|
||
|
||
raise Metricks::Error.new( | ||
'ConfigurationMissing', | ||
message: 'By default Metricks requires with_advisory_lock gem to be installed. ' \ | ||
'Alternatively a custom locking mechanism can be configured via config.metricks.with_lock' | ||
) | ||
end | ||
|
||
private | ||
|
||
def default_with_lock | ||
proc do |key, opts, block| | ||
ActiveRecord::Base.with_advisory_lock(key, opts, &block) | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,5 +15,4 @@ Gem::Specification.new do |gem| | |
gem.email = ['help@krystal.uk'] | ||
gem.required_ruby_version = '>= 2.7' | ||
gem.add_runtime_dependency 'activerecord', '>= 5.0' | ||
gem.add_runtime_dependency 'with_advisory_lock', '>= 4.6', '< 5.0' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one thing to watch out for when upgrading this gem is that these versions of with_advisory_lock will no longer be enforced. FWIW I ran this gem with version 5.1.0 of with_advisory_lock in my testing and it seemed fine. The changelog shows v5 dropped support for ruby below 2.7 and activerecord below 6.1 Ultimately the host app can decide what version of with_advisory_lock they want. |
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
require 'spec_helper' | ||
require 'metricks/engine' | ||
|
||
describe Metricks::Engine do | ||
|
||
let(:mock_app) do | ||
Class.new(Rails::Application) do | ||
config.eager_load = false | ||
end | ||
end | ||
|
||
before do | ||
allow(Metricks::Lock).to receive(:validate!).and_call_original | ||
end | ||
|
||
it 'allows with_lock to be configured' do | ||
success = false | ||
|
||
allow(mock_app.config.metricks).to receive(:with_lock) | ||
.and_return(->(result, opts, block) { block.call(result, opts) }) | ||
|
||
expect { | ||
mock_app.initialize! | ||
}.not_to raise_error | ||
|
||
Metricks::Lock.with_lock(true, {}) do |result| | ||
success = result | ||
end | ||
|
||
expect(success).to be(true) | ||
|
||
expect(Metricks::Lock).to have_received(:validate!) | ||
end | ||
Comment on lines
+16
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In an ideal world we might test various different rails app configurations. I did try to get that working, but Rails basically doesn't expect multiple apps to be declared and they were leaking into different tests. In the end I went with just verifying the override works and that we call the expected validation. The new Lock class is unit tested for all the different scenarios anyway. |
||
|
||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
require 'spec_helper' | ||
require 'with_advisory_lock' | ||
require 'metricks/lock' | ||
|
||
describe Metricks::Lock do | ||
|
||
describe ".with_lock" do | ||
before do | ||
allow(ActiveRecord::Base).to receive(:with_advisory_lock).and_call_original | ||
end | ||
|
||
context "when with_lock is set" do | ||
before do | ||
Metricks::Lock.with_lock = ->(key, opts, block) { block.call(key, opts) } | ||
end | ||
|
||
it "calls the block with the args" do | ||
success = false | ||
passed_opts = {} | ||
|
||
Metricks::Lock.with_lock(true, {hi: 'there'}) do |result, opts| | ||
success = result | ||
passed_opts = opts | ||
end | ||
|
||
expect(success).to be(true) | ||
expect(passed_opts).to eq({hi: 'there'}) | ||
expect(ActiveRecord::Base).not_to have_received(:with_advisory_lock) | ||
end | ||
end | ||
|
||
context "when with_lock is not set" do | ||
before do | ||
Metricks::Lock.with_lock = nil | ||
end | ||
|
||
it "uses with_advisory_lock" do | ||
success = false | ||
|
||
Metricks::Lock.with_lock(true, timeout_seconds: 5) do |result, opts| | ||
success = true | ||
end | ||
|
||
expect(success).to be(true) | ||
expect(ActiveRecord::Base).to have_received(:with_advisory_lock) | ||
.with(true, {timeout_seconds: 5}) | ||
end | ||
end | ||
end | ||
|
||
describe ".validate!" do | ||
context "when with_lock is set" do | ||
before do | ||
Metricks::Lock.with_lock = ->(key, opts, block) { block.call } | ||
hide_const("WithAdvisoryLock") | ||
end | ||
|
||
it "does not raise an error" do | ||
expect { Metricks::Lock.validate! }.not_to raise_error | ||
end | ||
end | ||
|
||
context "when with_lock is not set and WithAdvisoryLock is defined" do | ||
before do | ||
stub_const("WithAdvisoryLock", true) | ||
end | ||
|
||
it "does not raise an error" do | ||
expect { Metricks::Lock.validate! }.not_to raise_error | ||
end | ||
end | ||
|
||
context "when with_lock is not set and WithAdvisoryLock is not defined" do | ||
before do | ||
Metricks::Lock.with_lock = nil | ||
hide_const("WithAdvisoryLock") | ||
end | ||
|
||
it "raises an error" do | ||
expect { Metricks::Lock.validate! }.to raise_error(Metricks::Error) | ||
end | ||
end | ||
end | ||
|
||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added rails to the test scenarios because of the new engine_spec.rb file which checks the engine initialization works.