diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1625a29..8dff996 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby_version: [2.7.8, 3.0.7] + ruby_version: [3.3.10] steps: - uses: actions/checkout@v2 - name: Set up Ruby diff --git a/lib/active_operation.rb b/lib/active_operation.rb index c207b74..48d0bf1 100644 --- a/lib/active_operation.rb +++ b/lib/active_operation.rb @@ -6,6 +6,12 @@ module ActiveOperation class Error < RuntimeError; end class AlreadyCompletedError < Error; end + + # Internal control-flow signals raised by #halt / #succeed from inside an + # operation's #execute. Inheriting from Exception (not StandardError) so + # user code's bare `rescue` does not silently swallow them. + class Halted < Exception; end + class Succeeded < Exception; end end require_relative "active_operation/version" diff --git a/lib/active_operation/base.rb b/lib/active_operation/base.rb index 33af0d2..75cea69 100644 --- a/lib/active_operation/base.rb +++ b/lib/active_operation/base.rb @@ -149,8 +149,15 @@ def perform run_callbacks :execute do catch(:abort) do next if completed? - @output = execute - self.state = :succeeded + @in_execute = true + begin + @output = execute + self.state = :succeeded + rescue ActiveOperation::Halted, ActiveOperation::Succeeded + # state and @output already set by #halt / #succeed + ensure + @in_execute = false + end end end @@ -197,7 +204,11 @@ def halt(*args) self.state = :halted @output = args.length > 1 ? args : args.first - throw :abort + # Inside #execute, raise so we propagate cleanly through any enclosing + # ActiveRecord transaction (which will roll back). Outside #execute + # (i.e. from before/after callbacks), keep using `throw :abort` so + # ActiveSupport's callback chain halts and after-callbacks still run. + @in_execute ? raise(ActiveOperation::Halted) : throw(:abort) end def succeed(*args) @@ -205,6 +216,6 @@ def succeed(*args) self.state = :succeeded @output = args.length > 1 ? args : args.first - throw :abort + @in_execute ? raise(ActiveOperation::Succeeded) : throw(:abort) end end