diff --git a/lib/service/base.rb b/lib/service/base.rb index ecf1537be65..7d12a460efa 100644 --- a/lib/service/base.rb +++ b/lib/service/base.rb @@ -112,6 +112,10 @@ module Service steps << TransactionStep.new(&block) end + def lock(*keys, &block) + steps << LockStep.new(*keys, &block) + end + def options(&block) klass = Class.new(Service::OptionsBase).tap { _1.class_eval(&block) } const_set("Options", klass) @@ -257,6 +261,43 @@ module Service end end + # @!visibility private + class LockStep < Step + include StepsHelpers + + attr_reader :steps + + def initialize(*keys, &block) + @keys = keys + @steps = [] + instance_exec(&block) + end + + def run_step + success = + begin + DistributedMutex.synchronize(lock_name) do + steps.each { |step| step.call(instance, context) } + :success + end + rescue Discourse::ReadOnly + :read_only + end + + if success != :success + context[result_key].fail(lock_not_aquired: true) + context.fail! + end + end + + def lock_name + [ + context.__service_class__.to_s.underscore, + *@keys.flat_map { |key| [key, context[:params].send(key)] }, + ].join(":") + end + end + # @!visibility private class TryStep < Step include StepsHelpers diff --git a/spec/lib/service/runner_spec.rb b/spec/lib/service/runner_spec.rb index 825ef6efaac..44e82988f25 100644 --- a/spec/lib/service/runner_spec.rb +++ b/spec/lib/service/runner_spec.rb @@ -167,6 +167,20 @@ RSpec.describe Service::Runner do end end + class LockService + include Service::Base + + params do + attribute :post_id, :integer + attribute :user_id, :integer + end + + lock(:post_id, :user_id) { step :locked_step } + + def locked_step + end + end + describe ".call" do subject(:runner) { described_class.call(service, dependencies, &actions_block) } @@ -501,5 +515,36 @@ RSpec.describe Service::Runner do expect(runner).to eq :success end end + + context "when aquiring a lock" do + let(:service) { LockService } + let(:dependencies) { { params: { post_id: 123, user_id: 456 } } } + let(:actions) { <<-BLOCK } + proc do + on_success { :success } + on_failure { :failure } + end + BLOCK + + it "runs successfully" do + expect(runner).to eq :success + end + end + + context "when failing to acquire a lock" do + let(:service) { LockService } + let(:dependencies) { { params: { post_id: 123, user_id: 456 } } } + let(:actions) { <<-BLOCK } + proc do + on_success { :success } + on_failure { :failure } + end + BLOCK + + it "fails the service" do + DistributedMutex.stubs(:synchronize).returns + expect(runner).to eq :failure + end + end end end