polyphony 0.78 → 0.81
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +2 -1
- data/examples/core/pingpong.rb +7 -4
- data/examples/core/zlib_stream.rb +15 -0
- data/ext/polyphony/backend_common.c +16 -8
- data/ext/polyphony/backend_common.h +9 -3
- data/ext/polyphony/backend_io_uring.c +85 -31
- data/ext/polyphony/backend_libev.c +33 -17
- data/ext/polyphony/fiber.c +27 -27
- data/ext/polyphony/polyphony.c +9 -8
- data/ext/polyphony/polyphony.h +21 -7
- data/ext/polyphony/thread.c +6 -2
- data/lib/polyphony/adapters/fs.rb +4 -0
- data/lib/polyphony/adapters/process.rb +14 -1
- data/lib/polyphony/adapters/redis.rb +28 -0
- data/lib/polyphony/adapters/sequel.rb +19 -1
- data/lib/polyphony/core/debug.rb +201 -0
- data/lib/polyphony/core/exceptions.rb +21 -6
- data/lib/polyphony/core/global_api.rb +228 -73
- data/lib/polyphony/core/resource_pool.rb +65 -20
- data/lib/polyphony/core/sync.rb +57 -12
- data/lib/polyphony/core/thread_pool.rb +42 -5
- data/lib/polyphony/core/throttler.rb +21 -5
- data/lib/polyphony/core/timer.rb +125 -1
- data/lib/polyphony/extensions/exception.rb +36 -6
- data/lib/polyphony/extensions/fiber.rb +244 -61
- data/lib/polyphony/extensions/io.rb +4 -2
- data/lib/polyphony/extensions/kernel.rb +9 -4
- data/lib/polyphony/extensions/object.rb +8 -0
- data/lib/polyphony/extensions/openssl.rb +3 -1
- data/lib/polyphony/extensions/socket.rb +458 -39
- data/lib/polyphony/extensions/thread.rb +108 -43
- data/lib/polyphony/extensions/timeout.rb +12 -1
- data/lib/polyphony/extensions.rb +1 -0
- data/lib/polyphony/net.rb +66 -7
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +0 -2
- data/test/test_backend.rb +6 -2
- data/test/test_global_api.rb +0 -23
- data/test/test_io.rb +7 -7
- data/test/test_resource_pool.rb +1 -1
- data/test/test_signal.rb +15 -15
- data/test/test_thread.rb +1 -1
- data/test/test_throttler.rb +0 -6
- data/test/test_trace.rb +189 -24
- metadata +9 -8
- data/lib/polyphony/core/channel.rb +0 -15
data/ext/polyphony/polyphony.h
CHANGED
@@ -61,13 +61,13 @@ extern ID ID_switch_fiber;
|
|
61
61
|
extern ID ID_to_s;
|
62
62
|
extern ID ID_transfer;
|
63
63
|
|
64
|
-
extern VALUE
|
65
|
-
extern VALUE
|
66
|
-
extern VALUE
|
67
|
-
extern VALUE
|
68
|
-
extern VALUE
|
69
|
-
extern VALUE
|
70
|
-
extern VALUE
|
64
|
+
extern VALUE SYM_spin;
|
65
|
+
extern VALUE SYM_enter_poll;
|
66
|
+
extern VALUE SYM_leave_poll;
|
67
|
+
extern VALUE SYM_unblock;
|
68
|
+
extern VALUE SYM_schedule;
|
69
|
+
extern VALUE SYM_block;
|
70
|
+
extern VALUE SYM_terminate;
|
71
71
|
|
72
72
|
VALUE Fiber_auto_watcher(VALUE self);
|
73
73
|
void Fiber_make_runnable(VALUE fiber, VALUE value);
|
@@ -121,15 +121,29 @@ VALUE Backend_wait_event(VALUE self, VALUE raise_on_exception);
|
|
121
121
|
VALUE Backend_wakeup(VALUE self);
|
122
122
|
VALUE Backend_run_idle_tasks(VALUE self);
|
123
123
|
VALUE Backend_switch_fiber(VALUE self);
|
124
|
+
|
124
125
|
void Backend_schedule_fiber(VALUE thread, VALUE self, VALUE fiber, VALUE value, int prioritize);
|
125
126
|
void Backend_unschedule_fiber(VALUE self, VALUE fiber);
|
126
127
|
void Backend_park_fiber(VALUE self, VALUE fiber);
|
127
128
|
void Backend_unpark_fiber(VALUE self, VALUE fiber);
|
128
129
|
|
130
|
+
VALUE Backend_snooze(VALUE self);
|
131
|
+
|
129
132
|
void Thread_schedule_fiber(VALUE thread, VALUE fiber, VALUE value);
|
130
133
|
void Thread_schedule_fiber_with_priority(VALUE thread, VALUE fiber, VALUE value);
|
131
134
|
VALUE Thread_switch_fiber(VALUE thread);
|
132
135
|
|
133
136
|
VALUE Polyphony_snooze(VALUE self);
|
134
137
|
|
138
|
+
struct raw_buffer {
|
139
|
+
char *base;
|
140
|
+
int size;
|
141
|
+
};
|
142
|
+
|
143
|
+
struct io_buffer {
|
144
|
+
char *base;
|
145
|
+
int size;
|
146
|
+
int raw;
|
147
|
+
};
|
148
|
+
|
135
149
|
#endif /* POLYPHONY_H */
|
data/ext/polyphony/thread.c
CHANGED
@@ -23,11 +23,15 @@ VALUE Thread_fiber_unschedule(VALUE self, VALUE fiber) {
|
|
23
23
|
}
|
24
24
|
|
25
25
|
inline void Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
|
26
|
-
|
26
|
+
Backend_schedule_fiber(self, rb_ivar_get(self, ID_ivar_backend), fiber, value, 0);
|
27
|
+
|
28
|
+
// schedule_fiber(self, fiber, value, 0);
|
27
29
|
}
|
28
30
|
|
29
31
|
inline void Thread_schedule_fiber_with_priority(VALUE self, VALUE fiber, VALUE value) {
|
30
|
-
|
32
|
+
Backend_schedule_fiber(self, rb_ivar_get(self, ID_ivar_backend), fiber, value, 1);
|
33
|
+
|
34
|
+
// schedule_fiber(self, fiber, value, 1);
|
31
35
|
}
|
32
36
|
|
33
37
|
VALUE Thread_switch_fiber(VALUE self) {
|
@@ -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
|
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)
|
@@ -21,6 +32,8 @@ module Polyphony
|
|
21
32
|
kill_and_await(-9, pid)
|
22
33
|
end
|
23
34
|
|
35
|
+
private
|
36
|
+
|
24
37
|
def kill_and_await(sig, pid)
|
25
38
|
::Process.kill(sig, pid)
|
26
39
|
Polyphony.backend_waitpid(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.
|
53
|
+
@pool.fill!
|
36
54
|
end
|
37
55
|
end
|
38
56
|
|
data/lib/polyphony/core/debug.rb
CHANGED
@@ -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)
|
@@ -13,3 +20,197 @@ module ::Kernel
|
|
13
20
|
end
|
14
21
|
end
|
15
22
|
end
|
23
|
+
|
24
|
+
module Polyphony
|
25
|
+
|
26
|
+
# Trace provides tools for tracing the activity of the current thread's
|
27
|
+
# backend.
|
28
|
+
module Trace
|
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]
|
37
|
+
def start_event_firehose(io = nil, &block)
|
38
|
+
Thread.backend.trace_proc = firehose_proc(io, block)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
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
|
48
|
+
def firehose_proc(io, block)
|
49
|
+
if io
|
50
|
+
->(*e) { io.orig_write("#{trace_event_info(e).inspect}\n") }
|
51
|
+
elsif block
|
52
|
+
->(*e) { block.(trace_event_info(e)) }
|
53
|
+
else
|
54
|
+
raise "Please provide an io or a block"
|
55
|
+
end
|
56
|
+
end
|
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
|
62
|
+
def trace_event_info(e)
|
63
|
+
{
|
64
|
+
stamp: Time.now,
|
65
|
+
event: e[0]
|
66
|
+
}.merge(
|
67
|
+
send(:"event_props_#{e[0]}", e)
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
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
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns an event hash for a `:enter_poll` event.
|
83
|
+
#
|
84
|
+
# @param e [Array] event array
|
85
|
+
# @return [Hash] event hash
|
86
|
+
def event_props_enter_poll(e)
|
87
|
+
{}
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns an event hash for a `:leave_poll` event.
|
91
|
+
#
|
92
|
+
# @param e [Array] event array
|
93
|
+
# @return [Hash] event hash
|
94
|
+
def event_props_leave_poll(e)
|
95
|
+
{}
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns an event hash for a `:schedule` event.
|
99
|
+
#
|
100
|
+
# @param e [Array] event array
|
101
|
+
# @return [Hash] event hash
|
102
|
+
def event_props_schedule(e)
|
103
|
+
{
|
104
|
+
fiber: e[1],
|
105
|
+
value: e[2],
|
106
|
+
caller: e[4],
|
107
|
+
source_fiber: Fiber.current
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
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)
|
116
|
+
{
|
117
|
+
fiber: e[1],
|
118
|
+
caller: e[2],
|
119
|
+
source_fiber: Fiber.current
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns an event hash for a `:terminate` event.
|
124
|
+
#
|
125
|
+
# @param e [Array] event array
|
126
|
+
# @return [Hash] event hash
|
127
|
+
def event_props_terminate(e)
|
128
|
+
{
|
129
|
+
fiber: e[1],
|
130
|
+
value: e[2],
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
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)
|
139
|
+
{
|
140
|
+
fiber: e[1],
|
141
|
+
value: e[2],
|
142
|
+
caller: e[3],
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
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
|
155
|
+
|
156
|
+
# def format_current_time
|
157
|
+
# Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
158
|
+
# end
|
159
|
+
|
160
|
+
# def generic_event_format
|
161
|
+
# '%<event>-12.12s'
|
162
|
+
# end
|
163
|
+
|
164
|
+
# def fiber_event_format
|
165
|
+
# "#{generic_event_format} %<fiber>-44.44s"
|
166
|
+
# end
|
167
|
+
|
168
|
+
# def event_format_enter_poll(e)
|
169
|
+
# generic_event_format
|
170
|
+
# end
|
171
|
+
|
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
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -1,15 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Polyphony
|
4
|
-
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# through nested
|
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
|