FIX: Prevent infinite loop of automations triggering each other (#26814)

It's currently possible to setup multiple automation rules that trigger each other resulting in an infinite loop. To prevent that, this commit adds a global "circuit breaker" that prevents all automations from triggering while an automation rule is executing.

Internal topic: t/124365.
This commit is contained in:
Osama Sayegh 2024-04-30 20:13:29 +03:00 committed by GitHub
parent ff8e1f4ed6
commit 0e44072b2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 72 additions and 15 deletions

View File

@ -134,12 +134,24 @@ module DiscourseAutomation
def trigger!(context = {})
if enabled
if active_id = DiscourseAutomation.get_active_automation
Rails.logger.warn(<<~TEXT.strip)
[automation] potential automations infinite loop detected: skipping automation #{self.id} because automation #{active_id} is still executing.")
TEXT
return
end
begin
DiscourseAutomation.set_active_automation(self.id)
if scriptable.background && !running_in_background
trigger_in_background!(context)
else
triggerable&.on_call&.call(self, serialized_fields)
scriptable.script.call(context, serialized_fields, self)
end
ensure
DiscourseAutomation.set_active_automation(nil)
end
end
end

View File

@ -28,6 +28,14 @@ module ::DiscourseAutomation
AUTO_RESPONDER_TRIGGERED_IDS = "auto_responder_triggered_ids"
USER_GROUP_MEMBERSHIP_THROUGH_BADGE_BULK_MODIFY_START_COUNT = 1000
def self.set_active_automation(id)
@active_automation_id = id
end
def self.get_active_automation
@active_automation_id
end
end
require_relative "lib/discourse_automation/engine"

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require_relative "../discourse_automation_helper"
describe "Infinite loop protection" do
fab!(:automation_1) do
Fabricate(:automation, script: "auto_responder", trigger: "post_created_edited", enabled: true)
end
fab!(:automation_2) do
Fabricate(:automation, script: "auto_responder", trigger: "post_created_edited", enabled: true)
end
before do
SiteSetting.discourse_automation_enabled = true
automation_1.upsert_field!(
"word_answer_list",
"key-value",
{ value: [{ key: "", value: "this is the reply" }].to_json },
)
automation_2.upsert_field!(
"word_answer_list",
"key-value",
{ value: [{ key: "", value: "this is the reply" }].to_json },
)
automation_1.upsert_field!(
"answering_user",
"user",
{ value: Fabricate(:user).username },
target: "script",
)
automation_2.upsert_field!(
"answering_user",
"user",
{ value: Fabricate(:user).username },
target: "script",
)
end
it "prevents infinite loop of 2 auto_responder automations triggering each other" do
expect do
PostCreator.create!(Fabricate(:user), raw: "post", title: "topic", skip_validations: true)
end.to change { Post.count }.by(3)
end
end

View File

@ -46,16 +46,6 @@ describe DiscourseAutomation::Automation do
Jobs::DiscourseAutomationTrigger.jobs.size
}.by(1)
end
it "also runs the script properly" do
Jobs.run_immediately!
post = Fabricate(:post)
user = post.user
list = capture_contexts { automation.trigger!({ post: post, user: user, test: :test }) }
expect(list[0]["post"].id).to eq(post.id)
expect(list[0]["user"].id).to eq(user.id)
expect(list[0]["test"]).to eq(:test)
end
end
describe "#detach_custom_field" do