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
|