polyphony 0.79 → 0.80

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/Gemfile.lock +2 -1
  4. data/examples/core/zlib_stream.rb +15 -0
  5. data/ext/polyphony/backend_common.c +2 -1
  6. data/ext/polyphony/backend_common.h +7 -2
  7. data/lib/polyphony/adapters/fs.rb +4 -0
  8. data/lib/polyphony/adapters/process.rb +14 -1
  9. data/lib/polyphony/adapters/redis.rb +28 -0
  10. data/lib/polyphony/adapters/sequel.rb +19 -1
  11. data/lib/polyphony/core/debug.rb +129 -72
  12. data/lib/polyphony/core/exceptions.rb +21 -6
  13. data/lib/polyphony/core/global_api.rb +228 -73
  14. data/lib/polyphony/core/resource_pool.rb +65 -20
  15. data/lib/polyphony/core/sync.rb +57 -12
  16. data/lib/polyphony/core/thread_pool.rb +42 -5
  17. data/lib/polyphony/core/throttler.rb +21 -5
  18. data/lib/polyphony/core/timer.rb +125 -1
  19. data/lib/polyphony/extensions/exception.rb +36 -6
  20. data/lib/polyphony/extensions/fiber.rb +238 -57
  21. data/lib/polyphony/extensions/io.rb +4 -2
  22. data/lib/polyphony/extensions/kernel.rb +9 -4
  23. data/lib/polyphony/extensions/object.rb +8 -0
  24. data/lib/polyphony/extensions/openssl.rb +3 -1
  25. data/lib/polyphony/extensions/socket.rb +458 -39
  26. data/lib/polyphony/extensions/thread.rb +108 -43
  27. data/lib/polyphony/extensions/timeout.rb +12 -1
  28. data/lib/polyphony/extensions.rb +1 -0
  29. data/lib/polyphony/net.rb +59 -0
  30. data/lib/polyphony/version.rb +1 -1
  31. data/lib/polyphony.rb +0 -2
  32. data/test/test_backend.rb +6 -2
  33. data/test/test_global_api.rb +0 -23
  34. data/test/test_resource_pool.rb +1 -1
  35. data/test/test_throttler.rb +0 -6
  36. data/test/test_trace.rb +87 -0
  37. metadata +9 -8
  38. data/lib/polyphony/core/channel.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34ed5a7685a8986f1fe5fd6cd714ee049633952746ecb8bced1aee73553c6c99
4
- data.tar.gz: 03ded408e8849a921d0cfbbe1a232c5cea83cfbd58710efade4900f6240f511c
3
+ metadata.gz: cf3658689c8bb7614624ad75492b16fc31401c5c8ee37cd510bb4aab1c9d1449
4
+ data.tar.gz: 5e2520d758db10e9dfe8022d6a85df61e1702f2e1e6560a95968e37b6f4f6e82
5
5
  SHA512:
6
- metadata.gz: 91fe5a0979c0158315e942bba88f563a47926e78ad3da0ff8e483ee91aff1482af1f2fc3e3aa4cacd49e787531627bfc3295d45439e99d8400b4736b1a749443
7
- data.tar.gz: 3c1eaaccec661c4f3e4c07853fbaaaab19222837d5ac25b4098856bd1b1367833cbc35a58602fb9c30ff8e7cd63d04f79206bf07438a1e842eb86871c8036063
6
+ metadata.gz: 7bd42d8fed28064941281c0e010f86a7ff7fff56e28c8e8190c8bc5c68eebd26a8d568e03ce20120db64684a036afb8d9b8586668c59f39fadebdaba2624ba75
7
+ data.tar.gz: cd91d0394a0642fbb43d59cc3ba6a3100377be955f1cabeebc051f8f6159d6fb0e78fa7deedabc6bc4fabc20b12453c963b85ed9ce76002211d3423b05f51542
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.80 2022-02-28
2
+
3
+ - Prevent reentry into `trace_proc`
4
+ - Rename `__parser_read_method__` to `__read_method__`
5
+ - Rename `ResourcePool#preheat!` to `#fill`.
6
+ - Remove ability to use `#cancel_after` or `#move_on` without a block
7
+ - Add #move_on alias to `Fiber#interrupt`
8
+ - Allow specifying exception in `Fiber#cancel`
9
+ - Remove deprecated `Polyphony::Channel` class
10
+
1
11
  ## 0.79 2022-02-19
2
12
 
3
13
  - Overhaul trace events system (#73)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyphony (0.79)
4
+ polyphony (0.80)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -60,6 +60,7 @@ GEM
60
60
  unicode-display_width (1.8.0)
61
61
 
62
62
  PLATFORMS
63
+ ruby
63
64
  universal-darwin
64
65
  universal-freebsd
65
66
  universal-linux
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+ require 'zlib'
6
+
7
+ r, w = IO.pipe
8
+ writer = Zlib::GzipWriter.new(w)
9
+
10
+ writer << 'chunk'
11
+ writer.flush
12
+ p pos: writer.pos
13
+ w.close
14
+
15
+ p r.read
@@ -17,6 +17,7 @@ inline void backend_base_initialize(struct Backend_base *base) {
17
17
  base->idle_gc_last_time = 0;
18
18
  base->idle_proc = Qnil;
19
19
  base->trace_proc = Qnil;
20
+ base->in_trace_proc = 0;
20
21
  }
21
22
 
22
23
  inline void backend_base_finalize(struct Backend_base *base) {
@@ -137,7 +138,7 @@ inline void backend_base_unpark_fiber(struct Backend_base *base, VALUE fiber) {
137
138
  }
138
139
 
139
140
  inline void backend_trace(struct Backend_base *base, int argc, VALUE *argv) {
140
- if (base->trace_proc == Qnil) return;
141
+ if (base->trace_proc == Qnil || base->in_trace_proc) return;
141
142
 
142
143
  rb_funcallv(base->trace_proc, ID_call, argc, argv);
143
144
  }
@@ -32,6 +32,7 @@ struct Backend_base {
32
32
  double idle_gc_last_time;
33
33
  VALUE idle_proc;
34
34
  VALUE trace_proc;
35
+ unsigned int in_trace_proc;
35
36
  };
36
37
 
37
38
  void backend_base_initialize(struct Backend_base *base);
@@ -46,8 +47,12 @@ void backend_trace(struct Backend_base *base, int argc, VALUE *argv);
46
47
  struct backend_stats backend_base_stats(struct Backend_base *base);
47
48
 
48
49
  // tracing
49
- #define SHOULD_TRACE(base) ((base)->trace_proc != Qnil)
50
- #define TRACE(base, ...) rb_funcall((base)->trace_proc, ID_call, __VA_ARGS__)
50
+ #define SHOULD_TRACE(base) ((base)->trace_proc != Qnil && !(base)->in_trace_proc)
51
+ #define TRACE(base, ...) { \
52
+ (base)->in_trace_proc = 1; \
53
+ rb_funcall((base)->trace_proc, ID_call, __VA_ARGS__); \
54
+ (base)->in_trace_proc = 0; \
55
+ }
51
56
  #define COND_TRACE(base, ...) if (SHOULD_TRACE(base)) { TRACE(base, __VA_ARGS__); }
52
57
 
53
58
 
@@ -6,6 +6,8 @@ require_relative '../core/thread_pool'
6
6
 
7
7
  ::File.singleton_class.instance_eval do
8
8
  alias_method :orig_stat, :stat
9
+
10
+ # Offloads `File.stat` to the default thread pool.
9
11
  def stat(path)
10
12
  ThreadPool.process { orig_stat(path) }
11
13
  end
@@ -13,6 +15,8 @@ end
13
15
 
14
16
  ::IO.singleton_class.instance_eval do
15
17
  alias_method :orig_read, :read
18
+
19
+ # Offloads `IO.read` to the default thread pool.
16
20
  def read(path)
17
21
  ThreadPool.process { orig_read(path) }
18
22
  end
@@ -1,9 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- # Process patches
4
+ # Process extensions
5
5
  module Process
6
6
  class << self
7
+
8
+ # Watches a forked or spawned process, waiting for it to terminate. If
9
+ # `cmd` is given it is spawned, otherwise the process is forked with the
10
+ # given block.
11
+ #
12
+ # If the operation is interrupted for any reason, the spawned or forked
13
+ # process is killed.
14
+ #
15
+ # @param cmd [String, nil] command to spawn
16
+ # @param &block [Proc] block to fork
17
+ # @return [void]
7
18
  def watch(cmd = nil, &block)
8
19
  terminated = nil
9
20
  pid = cmd ? Kernel.spawn(cmd) : Polyphony.fork(&block)
@@ -13,6 +24,8 @@ module Polyphony
13
24
  kill_process(pid) unless terminated || pid.nil?
14
25
  end
15
26
 
27
+ private
28
+
16
29
  def kill_process(pid)
17
30
  cancel_after(5) do
18
31
  kill_and_await('TERM', pid)
@@ -7,6 +7,10 @@ require 'hiredis/reader'
7
7
 
8
8
  # Polyphony-based Redis driver
9
9
  class Polyphony::RedisDriver
10
+
11
+ # Connects to a Redis server using the given config.
12
+ #
13
+ # @return [TCPSocket, UNIXSocket, SSLSocket] client connectio
10
14
  def self.connect(config)
11
15
  raise 'unix sockets not supported' if config[:scheme] == 'unix'
12
16
 
@@ -20,28 +24,49 @@ class Polyphony::RedisDriver
20
24
  # connection.connect(config[:host], config[:port], connect_timeout)
21
25
  end
22
26
 
27
+ # Initializes a Redis client connection.
28
+ #
29
+ # @param host [String] hostname
30
+ # @param port [Integer] port number
23
31
  def initialize(host, port)
24
32
  @connection = Polyphony::Net.tcp_connect(host, port)
25
33
  @reader = ::Hiredis::Reader.new
26
34
  end
27
35
 
36
+ # Returns true if connected to server.
37
+ #
38
+ # @return [bool] is connected to server
28
39
  def connected?
29
40
  @connection && !@connection.closed?
30
41
  end
31
42
 
43
+ # Sets a timeout for the connection.
44
+ #
45
+ # @return [void]
32
46
  def timeout=(timeout)
33
47
  # ignore timeout for now
34
48
  end
35
49
 
50
+ # Disconnects from the server.
51
+ #
52
+ # @return [void]
36
53
  def disconnect
37
54
  @connection.close
38
55
  @connection = nil
39
56
  end
40
57
 
58
+ # Sends a command to the server.
59
+ #
60
+ # @param command [Array] Redis command
61
+ # @return [void]
41
62
  def write(command)
42
63
  @connection.write(format_command(command))
43
64
  end
44
65
 
66
+ # Formats a command for sending to server.
67
+ #
68
+ # @param args [Array] command
69
+ # @return [String] formatted command
45
70
  def format_command(args)
46
71
  args = args.flatten
47
72
  (+"*#{args.size}\r\n").tap do |s|
@@ -52,6 +77,9 @@ class Polyphony::RedisDriver
52
77
  end
53
78
  end
54
79
 
80
+ # Reads from the connection, feeding incoming data to the parser.
81
+ #
82
+ # @return [void]
55
83
  def read
56
84
  reply = @reader.gets
57
85
  return reply if reply
@@ -4,14 +4,23 @@ require_relative '../../polyphony'
4
4
  require 'sequel'
5
5
 
6
6
  module Polyphony
7
+
7
8
  # Sequel ConnectionPool that delegates to Polyphony::ResourcePool.
8
9
  class FiberConnectionPool < Sequel::ConnectionPool
10
+
11
+ # Initializes the connection pool.
12
+ #
13
+ # @param db [any] db to connect to
14
+ # @opts [Hash] connection pool options
9
15
  def initialize(db, opts = OPTS)
10
16
  super
11
17
  max_size = Integer(opts[:max_connections] || 4)
12
18
  @pool = Polyphony::ResourcePool.new(limit: max_size) { make_new(:default) }
13
19
  end
14
20
 
21
+ # Holds a connection from the pool, passing it to the given block.
22
+ #
23
+ # @return [any] block's return value
15
24
  def hold(_server = nil)
16
25
  @pool.acquire do |conn|
17
26
  yield conn
@@ -23,16 +32,25 @@ module Polyphony
23
32
  end
24
33
  end
25
34
 
35
+ # Returns the pool's size.
36
+ #
37
+ # @return [Integer] size of pool
26
38
  def size
27
39
  @pool.size
28
40
  end
29
41
 
42
+ # Returns the pool's maximal size.
43
+ #
44
+ # @return [Integer] maximum pool size
30
45
  def max_size
31
46
  @pool.limit
32
47
  end
33
48
 
49
+ # Fills pool and preconnects all db instances in pool.
50
+ #
51
+ # @return [void]
34
52
  def preconnect(_concurrent = false)
35
- @pool.preheat!
53
+ @pool.fill!
36
54
  end
37
55
  end
38
56
 
@@ -1,8 +1,15 @@
1
+ # Kernel extensions
1
2
  module ::Kernel
3
+ # Prints a trace message to `STDOUT`, bypassing the Polyphony backend.
4
+ #
5
+ # @return [void]
2
6
  def trace(*args)
3
7
  STDOUT.orig_write(format_trace(args))
4
8
  end
5
9
 
10
+ # Formats a trace message.
11
+ #
12
+ # @return [String] trace message
6
13
  def format_trace(args)
7
14
  if args.size > 1 && args.first.is_a?(String)
8
15
  format("%s: %p\n", args.shift, args.size == 1 ? args.first : args)
@@ -15,14 +22,29 @@ module ::Kernel
15
22
  end
16
23
 
17
24
  module Polyphony
25
+
26
+ # Trace provides tools for tracing the activity of the current thread's
27
+ # backend.
18
28
  module Trace
19
29
  class << self
30
+
31
+ # Starts tracing, emitting events converted to hashes to the given block.
32
+ # If an IO instance is given, events are dumped to it instead.
33
+ #
34
+ # @param io [IO, nil] IO instance
35
+ # @param &block [Proc] event handler block
36
+ # @return [void]
20
37
  def start_event_firehose(io = nil, &block)
21
38
  Thread.backend.trace_proc = firehose_proc(io, block)
22
39
  end
23
40
 
24
41
  private
25
42
 
43
+ # Returns a firehose proc for the given io and block.
44
+ #
45
+ # @param io [IO, nil] IO instance
46
+ # @param block [Proc] event handler block
47
+ # @return [Proc] firehose proc
26
48
  def firehose_proc(io, block)
27
49
  if io
28
50
  ->(*e) { io.orig_write("#{trace_event_info(e).inspect}\n") }
@@ -33,53 +55,50 @@ module Polyphony
33
55
  end
34
56
  end
35
57
 
58
+ # Converts an event (expressed as an array) to a hash.
59
+ #
60
+ # @param e [Array] event as emitted by the backend
61
+ # @return [Hash] event hash
36
62
  def trace_event_info(e)
37
63
  {
38
- stamp: format_current_time,
64
+ stamp: Time.now,
39
65
  event: e[0]
40
66
  }.merge(
41
67
  send(:"event_props_#{e[0]}", e)
42
68
  )
43
69
  end
44
-
45
- def format_trace_event_message(e)
46
- props = send(:"event_props_#{e[0]}", e).merge(
47
- timestamp: format_current_time,
48
- event: e[0]
49
- )
50
- # templ = send(:"event_format_#{e[0]}", e)
51
-
52
- # msg = format("%<timestamp>s #{templ}\n", **props)
53
- end
54
-
55
- def format_current_time
56
- Time.now.strftime('%Y-%m-%d %H:%M:%S')
57
- end
58
70
 
59
- def generic_event_format
60
- '%<event>-12.12s'
61
- end
62
-
63
- def fiber_event_format
64
- "#{generic_event_format} %<fiber>-44.44s"
71
+ # Returns an event hash for a `:block` event.
72
+ #
73
+ # @param e [Array] event array
74
+ # @return [Hash] event hash
75
+ def event_props_block(e)
76
+ {
77
+ fiber: e[1],
78
+ caller: e[2]
79
+ }
65
80
  end
66
81
 
82
+ # Returns an event hash for a `:enter_poll` event.
83
+ #
84
+ # @param e [Array] event array
85
+ # @return [Hash] event hash
67
86
  def event_props_enter_poll(e)
68
87
  {}
69
88
  end
70
89
 
71
- def event_format_enter_poll(e)
72
- generic_event_format
73
- end
74
-
90
+ # Returns an event hash for a `:leave_poll` event.
91
+ #
92
+ # @param e [Array] event array
93
+ # @return [Hash] event hash
75
94
  def event_props_leave_poll(e)
76
95
  {}
77
96
  end
78
97
 
79
- def event_format_leave_poll(e)
80
- generic_event_format
81
- end
82
-
98
+ # Returns an event hash for a `:schedule` event.
99
+ #
100
+ # @param e [Array] event array
101
+ # @return [Hash] event hash
83
102
  def event_props_schedule(e)
84
103
  {
85
104
  fiber: e[1],
@@ -89,22 +108,22 @@ module Polyphony
89
108
  }
90
109
  end
91
110
 
92
- def event_format_schedule(e)
93
- "#{fiber_event_format} %<value>-24.24p %<caller>-120.120s <= %<origin_fiber>s"
94
- end
95
-
96
- def event_props_unblock(e)
111
+ # Returns an event hash for a `:spin` event.
112
+ #
113
+ # @param e [Array] event array
114
+ # @return [Hash] event hash
115
+ def event_props_spin(e)
97
116
  {
98
117
  fiber: e[1],
99
- value: e[2],
100
- caller: e[3],
118
+ caller: e[2],
119
+ source_fiber: Fiber.current
101
120
  }
102
121
  end
103
122
 
104
- def event_format_unblock(e)
105
- "#{fiber_event_format} %<value>-24.24p %<caller>-120.120s"
106
- end
107
-
123
+ # Returns an event hash for a `:terminate` event.
124
+ #
125
+ # @param e [Array] event array
126
+ # @return [Hash] event hash
108
127
  def event_props_terminate(e)
109
128
  {
110
129
  fiber: e[1],
@@ -112,48 +131,86 @@ module Polyphony
112
131
  }
113
132
  end
114
133
 
115
- def event_format_terminate(e)
116
- "#{fiber_event_format} %<value>-24.24p"
117
- end
118
-
119
- def event_props_block(e)
134
+ # Returns an event hash for a `:unblock` event.
135
+ #
136
+ # @param e [Array] event array
137
+ # @return [Hash] event hash
138
+ def event_props_unblock(e)
120
139
  {
121
140
  fiber: e[1],
122
- caller: e[2]
141
+ value: e[2],
142
+ caller: e[3],
123
143
  }
124
144
  end
125
145
 
126
- def event_format_block(e)
127
- "#{fiber_event_format} #{' ' * 24} %<caller>-120.120s"
128
- end
146
+ # TODO: work on text formatting of events
147
+ # def format_trace_event_message(e)
148
+ # props = send(:"event_props_#{e[0]}", e).merge(
149
+ # timestamp: format_current_time,
150
+ # event: e[0]
151
+ # )
152
+ # templ = send(:"event_format_#{e[0]}", e)
153
+ # msg = format("%<timestamp>s #{templ}\n", **props)
154
+ # end
129
155
 
130
- def event_props_spin(e)
131
- {
132
- fiber: e[1],
133
- caller: e[2],
134
- source_fiber: Fiber.current
135
- }
136
- end
156
+ # def format_current_time
157
+ # Time.now.strftime('%Y-%m-%d %H:%M:%S')
158
+ # end
137
159
 
138
- def event_format_spin(e)
139
- "#{fiber_event_format} #{' ' * 24} %<caller>-120.120s <= %<origin_fiber>s"
140
- end
160
+ # def generic_event_format
161
+ # '%<event>-12.12s'
162
+ # end
141
163
 
142
- def fibe_repr(fiber)
143
- format("%-6x %-20.20s %-10.10s", fiber.object_id, fiber.tag, "(#{fiber.state})")
144
- end
164
+ # def fiber_event_format
165
+ # "#{generic_event_format} %<fiber>-44.44s"
166
+ # end
145
167
 
146
- def fiber_compact_repr(fiber)
147
- if fiber.tag
148
- format("%-6x %-.20s %-.10s", fiber.object_id, fiber.tag, "(#{fiber.state})")
149
- else
150
- format("%-6x %-.10s", fiber.object_id, "(#{fiber.state})")
151
- end
152
- end
168
+ # def event_format_enter_poll(e)
169
+ # generic_event_format
170
+ # end
153
171
 
154
- def caller_repr(c)
155
- c.map { |i| i.gsub('/home/sharon/repo/polyphony/lib/polyphony', '') }.join(' ')
156
- end
172
+ # def event_format_leave_poll(e)
173
+ # generic_event_format
174
+ # end
175
+
176
+
177
+ # def event_format_schedule(e)
178
+ # "#{fiber_event_format} %<value>-24.24p %<caller>-120.120s <= %<origin_fiber>s"
179
+ # end
180
+
181
+
182
+ # def event_format_unblock(e)
183
+ # "#{fiber_event_format} %<value>-24.24p %<caller>-120.120s"
184
+ # end
185
+
186
+ # def event_format_terminate(e)
187
+ # "#{fiber_event_format} %<value>-24.24p"
188
+ # end
189
+
190
+ # def event_format_block(e)
191
+ # "#{fiber_event_format} #{' ' * 24} %<caller>-120.120s"
192
+ # end
193
+
194
+
195
+ # def event_format_spin(e)
196
+ # "#{fiber_event_format} #{' ' * 24} %<caller>-120.120s <= %<origin_fiber>s"
197
+ # end
198
+
199
+ # def fibe_repr(fiber)
200
+ # format("%-6x %-20.20s %-10.10s", fiber.object_id, fiber.tag, "(#{fiber.state})")
201
+ # end
202
+
203
+ # def fiber_compact_repr(fiber)
204
+ # if fiber.tag
205
+ # format("%-6x %-.20s %-.10s", fiber.object_id, fiber.tag, "(#{fiber.state})")
206
+ # else
207
+ # format("%-6x %-.10s", fiber.object_id, "(#{fiber.state})")
208
+ # end
209
+ # end
210
+
211
+ # def caller_repr(c)
212
+ # c.map { |i| i.gsub('/home/sharon/repo/polyphony/lib/polyphony', '') }.join(' ')
213
+ # end
157
214
  end
158
215
  end
159
216
  end
@@ -1,15 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- # Common exception class for interrupting fibers. These exceptions allow
5
- # control of fibers. BaseException exceptions can encapsulate a value and thus
6
- # provide a way to interrupt long-running blocking operations while still
7
- # passing a value back to the call site. BaseException exceptions can also
8
- # references a cancel scope in order to allow correct bubbling of exceptions
9
- # through nested cancel scopes.
4
+
5
+ # Base exception class for interrupting fibers. These exceptions allow control
6
+ # of fibers. BaseException exceptions can encapsulate a value and thus provide
7
+ # a way to interrupt long-running blocking operations while still passing a
8
+ # value back to the call site. BaseException exceptions can also references a
9
+ # cancel scope in order to allow correct bubbling of exceptions through nested
10
+ # cancel scopes.
10
11
  class BaseException < ::Exception
12
+
13
+ # Exception value, used mainly for `MoveOn` exceptions.
11
14
  attr_reader :value
12
15
 
16
+ # Initializes the exception, setting the caller and the value.
17
+ #
18
+ # @param value [any] Exception value
19
+ # @return [void]
13
20
  def initialize(value = nil)
14
21
  @caller_backtrace = caller
15
22
  @value = value
@@ -33,10 +40,18 @@ module Polyphony
33
40
 
34
41
  # Interjection is used to run arbitrary code on arbitrary fibers at any point
35
42
  class Interjection < BaseException
43
+
44
+ # Initializes an Interjection with the given proc.
45
+ #
46
+ # @param proc [Proc] interjection proc
47
+ # @return [void]
36
48
  def initialize(proc)
37
49
  @proc = proc
38
50
  end
39
51
 
52
+ # Invokes the exception by calling the associated proc.
53
+ #
54
+ # @return [void]
40
55
  def invoke
41
56
  @proc.call
42
57
  end