File: execution_wrapper.rb

package info (click to toggle)
rails 2%3A6.0.3.7%2Bdfsg-2%2Bdeb11u2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 70,976 kB
  • sloc: ruby: 271,623; javascript: 19,043; yacc: 46; sql: 43; makefile: 28; sh: 18
file content (132 lines) | stat: -rw-r--r-- 3,137 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# frozen_string_literal: true

require "active_support/callbacks"
require "concurrent/hash"

module ActiveSupport
  class ExecutionWrapper
    include ActiveSupport::Callbacks

    Null = Object.new # :nodoc:
    def Null.complete! # :nodoc:
    end

    define_callbacks :run
    define_callbacks :complete

    def self.to_run(*args, &block)
      set_callback(:run, *args, &block)
    end

    def self.to_complete(*args, &block)
      set_callback(:complete, *args, &block)
    end

    RunHook = Struct.new(:hook) do # :nodoc:
      def before(target)
        hook_state = target.send(:hook_state)
        hook_state[hook] = hook.run
      end
    end

    CompleteHook = Struct.new(:hook) do # :nodoc:
      def before(target)
        hook_state = target.send(:hook_state)
        if hook_state.key?(hook)
          hook.complete hook_state[hook]
        end
      end
      alias after before
    end

    # Register an object to be invoked during both the +run+ and
    # +complete+ steps.
    #
    # +hook.complete+ will be passed the value returned from +hook.run+,
    # and will only be invoked if +run+ has previously been called.
    # (Mostly, this means it won't be invoked if an exception occurs in
    # a preceding +to_run+ block; all ordinary +to_complete+ blocks are
    # invoked in that situation.)
    def self.register_hook(hook, outer: false)
      if outer
        to_run RunHook.new(hook), prepend: true
        to_complete :after, CompleteHook.new(hook)
      else
        to_run RunHook.new(hook)
        to_complete CompleteHook.new(hook)
      end
    end

    # Run this execution.
    #
    # Returns an instance, whose +complete!+ method *must* be invoked
    # after the work has been performed.
    #
    # Where possible, prefer +wrap+.
    def self.run!(reset: false)
      if reset
        lost_instance = active.delete(Thread.current)
        lost_instance&.complete!
      else
        return Null if active?
      end

      new.tap do |instance|
        success = nil
        begin
          instance.run!
          success = true
        ensure
          instance.complete! unless success
        end
      end
    end

    # Perform the work in the supplied block as an execution.
    def self.wrap
      return yield if active?

      instance = run!
      begin
        yield
      ensure
        instance.complete!
      end
    end

    class << self # :nodoc:
      attr_accessor :active
    end

    def self.inherited(other) # :nodoc:
      super
      other.active = Concurrent::Hash.new
    end

    self.active = Concurrent::Hash.new

    def self.active? # :nodoc:
      @active.key?(Thread.current)
    end

    def run! # :nodoc:
      self.class.active[Thread.current] = self
      run_callbacks(:run)
    end

    # Complete this in-flight execution. This method *must* be called
    # exactly once on the result of any call to +run!+.
    #
    # Where possible, prefer +wrap+.
    def complete!
      run_callbacks(:complete)
    ensure
      self.class.active.delete Thread.current
    end

    private
      def hook_state
        @_hook_state ||= {}
      end
  end
end