DEV: new lock step for services (#30872)

That allows services to wrap steps within a Distributed lock (mutex).
This commit is contained in:
Régis Hanol 2025-01-29 14:28:22 +01:00 committed by GitHub
parent 049b6b8f54
commit a16b2f2248
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 86 additions and 0 deletions

View File

@ -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

View File

@ -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