aboutsummaryrefslogtreecommitdiffstats
path: root/lib/bundler/safe_catch.rb
blob: b71e51ad128338a396784d64ad9bfb6680b86d5a (plain)
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
# SafeCatch provides a mechanism to safely deepen the stack, performing
# stack-unrolling similar to catch/throw, but using Fiber or Thread to avoid
# deepening the stack too quickly.
#
# The API is the same as that of catch/throw: SafeCatch#safe_catch takes a "tag"
# to be rescued when some code deeper in the process raises it. If the catch
# block completes successfully, that value is returned. If the tag is "thrown"
# by safe_throw, the tag's value is returned. Other exceptions propagate out as
# normal.
#
# The implementation, however, uses fibers or threads along with raise/rescue to
# handle "deepening" the stack and unrolling it. On implementations where Fiber
# is available, it will be used. If Fiber is not available, Thread will be used.
# If neither of these classes are available, Proc will be used, effectively
# deepening the stack for each recursion as in normal catch/throw.
#
# In order to avoid causing a new issue of creating too many fibers or threads,
# especially on implementations where fibers are actually backed by native
# threads, the "safe" recursion mechanism is only used every 20 recursions.
# Based on experiments with JRuby (which seems to suffer the most from
# excessively deep stacks), this appears to be a sufficient granularity to
# prevent stack overflow without spinning up excessive numbers of fibers or
# threads. This value can be adjusted with the BUNDLER_SAFE_RECURSE_EVERY env
# var; setting it to zero effectively disables safe recursion.

module Bundler
  module SafeCatch
    def safe_catch(tag, &block)
      if Bundler.current_ruby.jruby?
        Internal.catch(tag, &block)
      else
        catch(tag, &block)
      end
    end

    def safe_throw(tag, value = nil)
      if Bundler.current_ruby.jruby?
        Internal.throw(tag, value)
      else
        throw(tag, value)
      end
    end

    module Internal
      SAFE_RECURSE_EVERY = (ENV['BUNDLER_SAFE_RECURSE_EVERY'] || 20).to_i

      SAFE_RECURSE_CLASS, SAFE_RECURSE_START = case
      when defined?(Fiber)
        [Fiber, :resume]
      when defined?(Thread)
        [Thread, :join]
      else
        [Proc, :call]
      end

      @recurse_count = 0

      def self.catch(tag, &block)
        @recurse_count += 1
        if SAFE_RECURSE_EVERY >= 0 && @recurse_count % SAFE_RECURSE_EVERY == 0
          SAFE_RECURSE_CLASS.new(&block).send(SAFE_RECURSE_START)
        else
          block.call
        end
      rescue Result.matcher(tag)
        $!.value
      end

      def self.throw(tag, value = nil)
        raise Result.new(tag, value)
      end

      class Result < StopIteration
        def initialize(tag, value)
          @tag = tag
          @value = value
        end

        attr_reader :tag, :value

        # The Matcher class is never instantiated; it is dup'ed and used as a
        # rescue-clause argument to match Result exceptions based on their tags.
        module Matcher
          class << self
            attr_accessor :tag

            def ===(other)
              other.respond_to? :tag and @tag.equal? other.tag
            end
          end
        end

        def self.matcher(tag)
          matcher = Matcher.dup
          matcher.tag = tag
          matcher
        end
      end
    end
  end
end