polyphony 0.79 → 0.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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