polyphony 0.43.3 → 0.43.9
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/.github/workflows/test.yml +1 -1
- data/CHANGELOG.md +44 -0
- data/Gemfile.lock +1 -1
- data/README.md +21 -4
- data/TODO.md +1 -2
- data/bin/stress.rb +28 -0
- data/docs/_includes/head.html +40 -0
- data/docs/_includes/title.html +1 -0
- data/docs/_user-guide/web-server.md +11 -11
- data/docs/getting-started/overview.md +4 -4
- data/docs/index.md +4 -3
- data/docs/main-concepts/design-principles.md +23 -34
- data/docs/main-concepts/fiber-scheduling.md +1 -1
- data/docs/polyphony-logo.png +0 -0
- data/examples/core/xx-channels.rb +4 -2
- data/examples/core/xx-using-a-mutex.rb +2 -1
- data/examples/io/xx-happy-eyeballs.rb +21 -22
- data/examples/io/xx-zip.rb +19 -0
- data/examples/performance/fiber_transfer.rb +47 -0
- data/examples/performance/mem-usage.rb +34 -28
- data/examples/performance/messaging.rb +29 -0
- data/examples/performance/multi_snooze.rb +11 -9
- data/examples/xx-spin.rb +32 -0
- data/ext/polyphony/event.c +86 -0
- data/ext/polyphony/fiber.c +0 -5
- data/ext/polyphony/libev_agent.c +181 -24
- data/ext/polyphony/polyphony.c +0 -2
- data/ext/polyphony/polyphony.h +14 -7
- data/ext/polyphony/polyphony_ext.c +4 -2
- data/ext/polyphony/queue.c +187 -0
- data/ext/polyphony/ring_buffer.c +96 -0
- data/ext/polyphony/ring_buffer.h +28 -0
- data/ext/polyphony/thread.c +18 -12
- data/lib/polyphony.rb +5 -14
- data/lib/polyphony/core/channel.rb +3 -34
- data/lib/polyphony/core/global_api.rb +1 -1
- data/lib/polyphony/core/resource_pool.rb +13 -75
- data/lib/polyphony/core/sync.rb +12 -9
- data/lib/polyphony/core/thread_pool.rb +1 -1
- data/lib/polyphony/extensions/core.rb +34 -0
- data/lib/polyphony/extensions/fiber.rb +9 -2
- data/lib/polyphony/extensions/io.rb +17 -16
- data/lib/polyphony/extensions/openssl.rb +8 -0
- data/lib/polyphony/extensions/socket.rb +12 -0
- data/lib/polyphony/version.rb +1 -1
- data/test/helper.rb +1 -1
- data/test/q.rb +24 -0
- data/test/test_agent.rb +1 -1
- data/test/test_event.rb +12 -0
- data/test/test_global_api.rb +2 -2
- data/test/test_io.rb +24 -2
- data/test/test_queue.rb +59 -1
- data/test/test_resource_pool.rb +0 -43
- data/test/test_trace.rb +18 -17
- metadata +15 -5
- data/ext/polyphony/libev_queue.c +0 -217
- data/lib/polyphony/event.rb +0 -27
@@ -0,0 +1,96 @@
|
|
1
|
+
#include "polyphony.h"
|
2
|
+
#include "ring_buffer.h"
|
3
|
+
|
4
|
+
void ring_buffer_init(ring_buffer *buffer) {
|
5
|
+
buffer->size = 1;
|
6
|
+
buffer->count = 0;
|
7
|
+
buffer->entries = malloc(buffer->size * sizeof(VALUE));
|
8
|
+
buffer->head = 0;
|
9
|
+
buffer->tail = 0;
|
10
|
+
}
|
11
|
+
|
12
|
+
void ring_buffer_free(ring_buffer *buffer) {
|
13
|
+
free(buffer->entries);
|
14
|
+
}
|
15
|
+
|
16
|
+
int ring_buffer_empty_p(ring_buffer *buffer) {
|
17
|
+
return buffer->count == 0;
|
18
|
+
}
|
19
|
+
|
20
|
+
VALUE ring_buffer_shift(ring_buffer *buffer) {
|
21
|
+
VALUE value;
|
22
|
+
if (buffer->count == 0) return Qnil;
|
23
|
+
|
24
|
+
value = buffer->entries[buffer->head];
|
25
|
+
buffer->head = (buffer->head + 1) % buffer->size;
|
26
|
+
buffer->count--;
|
27
|
+
// INSPECT(value);
|
28
|
+
return value;
|
29
|
+
}
|
30
|
+
|
31
|
+
void ring_buffer_resize(ring_buffer *buffer) {
|
32
|
+
unsigned int old_size = buffer->size;
|
33
|
+
buffer->size = old_size == 1 ? 4 : old_size * 2;
|
34
|
+
buffer->entries = realloc(buffer->entries, buffer->size * sizeof(VALUE));
|
35
|
+
for (unsigned int idx = 0; idx < buffer->head && idx < buffer->tail; idx++)
|
36
|
+
buffer->entries[old_size + idx] = buffer->entries[idx];
|
37
|
+
buffer->tail = buffer->head + buffer->count;
|
38
|
+
}
|
39
|
+
|
40
|
+
void ring_buffer_unshift(ring_buffer *buffer, VALUE value) {
|
41
|
+
if (buffer->count == buffer->size) ring_buffer_resize(buffer);
|
42
|
+
|
43
|
+
buffer->head = (buffer->head - 1) % buffer->size;
|
44
|
+
buffer->entries[buffer->head] = value;
|
45
|
+
buffer->count++;
|
46
|
+
}
|
47
|
+
|
48
|
+
void ring_buffer_push(ring_buffer *buffer, VALUE value) {
|
49
|
+
if (buffer->count == buffer->size) ring_buffer_resize(buffer);
|
50
|
+
|
51
|
+
buffer->entries[buffer->tail] = value;
|
52
|
+
buffer->tail = (buffer->tail + 1) % buffer->size;
|
53
|
+
buffer->count++;
|
54
|
+
}
|
55
|
+
|
56
|
+
void ring_buffer_mark(ring_buffer *buffer) {
|
57
|
+
for (unsigned int i = 0; i < buffer->count; i++)
|
58
|
+
rb_gc_mark(buffer->entries[(buffer->head + i) % buffer->size]);
|
59
|
+
}
|
60
|
+
|
61
|
+
void ring_buffer_shift_each(ring_buffer *buffer) {
|
62
|
+
for (unsigned int i = 0; i < buffer->count; i++)
|
63
|
+
rb_yield(buffer->entries[(buffer->head + i) % buffer->size]);
|
64
|
+
|
65
|
+
buffer->count = buffer->head = buffer->tail = 0;
|
66
|
+
}
|
67
|
+
|
68
|
+
VALUE ring_buffer_shift_all(ring_buffer *buffer) {
|
69
|
+
VALUE array = rb_ary_new_capa(buffer->count);
|
70
|
+
for (unsigned int i = 0; i < buffer->count; i++)
|
71
|
+
rb_ary_push(array, buffer->entries[(buffer->head + i) % buffer->size]);
|
72
|
+
buffer->count = buffer->head = buffer->tail = 0;
|
73
|
+
return array;
|
74
|
+
}
|
75
|
+
|
76
|
+
void ring_buffer_delete_at(ring_buffer *buffer, unsigned int idx) {
|
77
|
+
for (unsigned int idx2 = idx; idx2 != buffer->tail; idx2 = (idx2 + 1) % buffer->size) {
|
78
|
+
buffer->entries[idx2] = buffer->entries[(idx2 + 1) % buffer->size];
|
79
|
+
}
|
80
|
+
buffer->count--;
|
81
|
+
buffer->tail = (buffer->tail - 1) % buffer->size;
|
82
|
+
}
|
83
|
+
|
84
|
+
void ring_buffer_delete(ring_buffer *buffer, VALUE value) {
|
85
|
+
for (unsigned int i = 0; i < buffer->count; i++) {
|
86
|
+
unsigned int idx = (buffer->head + i) % buffer->size;
|
87
|
+
if (buffer->entries[idx] == value) {
|
88
|
+
ring_buffer_delete_at(buffer, idx);
|
89
|
+
return;
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
void ring_buffer_clear(ring_buffer *buffer) {
|
95
|
+
buffer->count = buffer->head = buffer->tail = 0;
|
96
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#ifndef RING_BUFFER_H
|
2
|
+
#define RING_BUFFER_H
|
3
|
+
|
4
|
+
#include "ruby.h"
|
5
|
+
|
6
|
+
typedef struct ring_buffer {
|
7
|
+
VALUE *entries;
|
8
|
+
unsigned int size;
|
9
|
+
unsigned int count;
|
10
|
+
unsigned int head;
|
11
|
+
unsigned int tail;
|
12
|
+
} ring_buffer;
|
13
|
+
|
14
|
+
void ring_buffer_init(ring_buffer *buffer);
|
15
|
+
void ring_buffer_free(ring_buffer *buffer);
|
16
|
+
void ring_buffer_mark(ring_buffer *buffer);
|
17
|
+
int ring_buffer_empty_p(ring_buffer *buffer);
|
18
|
+
void ring_buffer_clear(ring_buffer *buffer);
|
19
|
+
|
20
|
+
VALUE ring_buffer_shift(ring_buffer *buffer);
|
21
|
+
void ring_buffer_unshift(ring_buffer *buffer, VALUE value);
|
22
|
+
void ring_buffer_push(ring_buffer *buffer, VALUE value);
|
23
|
+
|
24
|
+
void ring_buffer_shift_each(ring_buffer *buffer);
|
25
|
+
VALUE ring_buffer_shift_all(ring_buffer *buffer);
|
26
|
+
void ring_buffer_delete(ring_buffer *buffer, VALUE value);
|
27
|
+
|
28
|
+
#endif /* RING_BUFFER_H */
|
data/ext/polyphony/thread.c
CHANGED
@@ -11,10 +11,9 @@ ID ID_runnable_next;
|
|
11
11
|
ID ID_stop;
|
12
12
|
|
13
13
|
static VALUE Thread_setup_fiber_scheduling(VALUE self) {
|
14
|
-
VALUE queue;
|
14
|
+
VALUE queue = rb_funcall(cQueue, ID_new, 0);
|
15
15
|
|
16
16
|
rb_ivar_set(self, ID_ivar_main_fiber, rb_fiber_current());
|
17
|
-
queue = rb_ary_new();
|
18
17
|
rb_ivar_set(self, ID_run_queue, queue);
|
19
18
|
|
20
19
|
return self;
|
@@ -54,14 +53,12 @@ VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
|
|
54
53
|
if (rb_fiber_alive_p(fiber) != Qtrue) return self;
|
55
54
|
|
56
55
|
FIBER_TRACE(3, SYM_fiber_schedule, fiber, value);
|
57
|
-
// if fiber is already scheduled, just set the scheduled value, then return
|
58
56
|
rb_ivar_set(fiber, ID_runnable_value, value);
|
59
|
-
if
|
60
|
-
|
61
|
-
}
|
57
|
+
// if fiber is already scheduled, just set the scheduled value, then return
|
58
|
+
if (rb_ivar_get(fiber, ID_runnable) != Qnil) return self;
|
62
59
|
|
63
60
|
queue = rb_ivar_get(self, ID_run_queue);
|
64
|
-
|
61
|
+
Queue_push(queue, fiber);
|
65
62
|
rb_ivar_set(fiber, ID_runnable, Qtrue);
|
66
63
|
|
67
64
|
if (rb_thread_current() != self) {
|
@@ -88,13 +85,13 @@ VALUE Thread_schedule_fiber_with_priority(VALUE self, VALUE fiber, VALUE value)
|
|
88
85
|
|
89
86
|
// if fiber is already scheduled, remove it from the run queue
|
90
87
|
if (rb_ivar_get(fiber, ID_runnable) != Qnil) {
|
91
|
-
|
88
|
+
Queue_delete(queue, fiber);
|
92
89
|
} else {
|
93
90
|
rb_ivar_set(fiber, ID_runnable, Qtrue);
|
94
91
|
}
|
95
92
|
|
96
93
|
// the fiber is given priority by putting it at the front of the run queue
|
97
|
-
|
94
|
+
Queue_unshift(queue, fiber);
|
98
95
|
|
99
96
|
if (rb_thread_current() != self) {
|
100
97
|
// if the fiber scheduling is done across threads, we need to make sure the
|
@@ -115,6 +112,7 @@ VALUE Thread_switch_fiber(VALUE self) {
|
|
115
112
|
VALUE value;
|
116
113
|
VALUE agent = rb_ivar_get(self, ID_ivar_agent);
|
117
114
|
int ref_count;
|
115
|
+
int agent_was_polled = 0;1;
|
118
116
|
|
119
117
|
if (__tracing_enabled__) {
|
120
118
|
if (rb_ivar_get(current_fiber, ID_ivar_running) != Qfalse) {
|
@@ -124,9 +122,9 @@ VALUE Thread_switch_fiber(VALUE self) {
|
|
124
122
|
|
125
123
|
ref_count = LibevAgent_ref_count(agent);
|
126
124
|
while (1) {
|
127
|
-
next_fiber =
|
125
|
+
next_fiber = Queue_shift_no_wait(queue);
|
128
126
|
if (next_fiber != Qnil) {
|
129
|
-
if (ref_count > 0) {
|
127
|
+
if (agent_was_polled == 0 && ref_count > 0) {
|
130
128
|
// this mechanism prevents event starvation in case the run queue never
|
131
129
|
// empties
|
132
130
|
LibevAgent_poll(agent, Qtrue, current_fiber, queue);
|
@@ -136,6 +134,7 @@ VALUE Thread_switch_fiber(VALUE self) {
|
|
136
134
|
if (ref_count == 0) break;
|
137
135
|
|
138
136
|
LibevAgent_poll(agent, Qnil, current_fiber, queue);
|
137
|
+
agent_was_polled = 1;
|
139
138
|
}
|
140
139
|
|
141
140
|
if (next_fiber == Qnil) return Qnil;
|
@@ -151,9 +150,15 @@ VALUE Thread_switch_fiber(VALUE self) {
|
|
151
150
|
value : rb_funcall(next_fiber, ID_transfer, 1, value);
|
152
151
|
}
|
153
152
|
|
153
|
+
VALUE Thread_run_queue_trace(VALUE self) {
|
154
|
+
VALUE queue = rb_ivar_get(self, ID_run_queue);
|
155
|
+
Queue_trace(queue);
|
156
|
+
return self;
|
157
|
+
}
|
158
|
+
|
154
159
|
VALUE Thread_reset_fiber_scheduling(VALUE self) {
|
155
160
|
VALUE queue = rb_ivar_get(self, ID_run_queue);
|
156
|
-
|
161
|
+
Queue_clear(queue);
|
157
162
|
Thread_fiber_reset_ref_count(self);
|
158
163
|
return self;
|
159
164
|
}
|
@@ -182,6 +187,7 @@ void Init_Thread() {
|
|
182
187
|
rb_define_method(rb_cThread, "schedule_fiber_with_priority",
|
183
188
|
Thread_schedule_fiber_with_priority, 2);
|
184
189
|
rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
|
190
|
+
rb_define_method(rb_cThread, "run_queue_trace", Thread_run_queue_trace, 0);
|
185
191
|
|
186
192
|
ID_deactivate_all_watchers_post_fork = rb_intern("deactivate_all_watchers_post_fork");
|
187
193
|
ID_ivar_agent = rb_intern("@agent");
|
data/lib/polyphony.rb
CHANGED
@@ -4,9 +4,6 @@ require 'fiber'
|
|
4
4
|
require_relative './polyphony_ext'
|
5
5
|
|
6
6
|
module Polyphony
|
7
|
-
# Map Queue to Libev queue implementation
|
8
|
-
Queue = LibevQueue
|
9
|
-
|
10
7
|
# replace core Queue class with our own
|
11
8
|
verbose = $VERBOSE
|
12
9
|
$VERBOSE = nil
|
@@ -26,7 +23,6 @@ require_relative './polyphony/core/global_api'
|
|
26
23
|
require_relative './polyphony/core/resource_pool'
|
27
24
|
require_relative './polyphony/net'
|
28
25
|
require_relative './polyphony/adapters/process'
|
29
|
-
require_relative './polyphony/event'
|
30
26
|
|
31
27
|
# Main Polyphony API
|
32
28
|
module Polyphony
|
@@ -100,17 +96,12 @@ module Polyphony
|
|
100
96
|
Polyphony::Process.watch(cmd, &block)
|
101
97
|
end
|
102
98
|
|
103
|
-
def emit_signal_exception(exception, fiber = Thread.main.main_fiber)
|
104
|
-
Thread.current.break_out_of_ev_loop(fiber, exception)
|
105
|
-
end
|
106
|
-
|
107
|
-
def install_terminating_signal_handler(signal, exception_class)
|
108
|
-
trap(signal) { emit_signal_exception(exception_class.new) }
|
109
|
-
end
|
110
|
-
|
111
99
|
def install_terminating_signal_handlers
|
112
|
-
|
113
|
-
|
100
|
+
trap('SIGTERM', SystemExit)
|
101
|
+
orig_trap('SIGINT') do
|
102
|
+
orig_trap('SIGINT') { exit! }
|
103
|
+
Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, Interrupt.new)
|
104
|
+
end
|
114
105
|
end
|
115
106
|
|
116
107
|
def terminate_threads
|
@@ -5,42 +5,11 @@ require_relative './exceptions'
|
|
5
5
|
module Polyphony
|
6
6
|
# Implements a unidirectional communication channel along the lines of Go
|
7
7
|
# (buffered) channels.
|
8
|
-
class Channel
|
9
|
-
|
10
|
-
@payload_queue = []
|
11
|
-
@waiting_queue = []
|
12
|
-
end
|
8
|
+
class Channel < Polyphony::Queue
|
9
|
+
alias_method :receive, :shift
|
13
10
|
|
14
11
|
def close
|
15
|
-
|
16
|
-
@waiting_queue.slice(0..-1).each { |f| f.schedule(stop) }
|
17
|
-
end
|
18
|
-
|
19
|
-
def <<(value)
|
20
|
-
if @waiting_queue.empty?
|
21
|
-
@payload_queue << value
|
22
|
-
else
|
23
|
-
@waiting_queue.shift&.schedule(value)
|
24
|
-
end
|
25
|
-
snooze
|
26
|
-
end
|
27
|
-
|
28
|
-
def receive
|
29
|
-
Thread.current.agent.ref
|
30
|
-
if @payload_queue.empty?
|
31
|
-
@waiting_queue << Fiber.current
|
32
|
-
suspend
|
33
|
-
else
|
34
|
-
receive_from_queue
|
35
|
-
end
|
36
|
-
ensure
|
37
|
-
Thread.current.agent.unref
|
38
|
-
end
|
39
|
-
|
40
|
-
def receive_from_queue
|
41
|
-
payload = @payload_queue.shift
|
42
|
-
snooze
|
43
|
-
payload
|
12
|
+
flush_waiters(Polyphony::MoveOn.new)
|
44
13
|
end
|
45
14
|
end
|
46
15
|
end
|
@@ -10,13 +10,10 @@ module Polyphony
|
|
10
10
|
# @param &block [Proc] allocator block
|
11
11
|
def initialize(opts, &block)
|
12
12
|
@allocator = block
|
13
|
-
|
14
|
-
@stock = []
|
15
|
-
@queue = []
|
16
|
-
@acquired_resources = {}
|
17
|
-
|
18
13
|
@limit = opts[:limit] || 4
|
19
14
|
@size = 0
|
15
|
+
@stock = Polyphony::Queue.new
|
16
|
+
@acquired_resources = {}
|
20
17
|
end
|
21
18
|
|
22
19
|
def available
|
@@ -25,58 +22,17 @@ module Polyphony
|
|
25
22
|
|
26
23
|
def acquire
|
27
24
|
fiber = Fiber.current
|
28
|
-
if @acquired_resources[fiber]
|
29
|
-
yield @acquired_resources[fiber]
|
30
|
-
else
|
31
|
-
begin
|
32
|
-
Thread.current.agent.ref
|
33
|
-
resource = wait_for_resource
|
34
|
-
return unless resource
|
35
|
-
|
36
|
-
@acquired_resources[fiber] = resource
|
37
|
-
yield resource
|
38
|
-
ensure
|
39
|
-
@acquired_resources[fiber] = nil
|
40
|
-
Thread.current.agent.unref
|
41
|
-
release(resource) if resource
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def wait_for_resource
|
47
|
-
fiber = Fiber.current
|
48
|
-
@queue << fiber
|
49
|
-
ready_resource = from_stock
|
50
|
-
return ready_resource if ready_resource
|
25
|
+
return @acquired_resources[fiber] if @acquired_resources[fiber]
|
51
26
|
|
52
|
-
|
27
|
+
add_to_stock if @size < @limit && @stock.empty?
|
28
|
+
resource = @stock.shift
|
29
|
+
@acquired_resources[fiber] = resource
|
30
|
+
yield resource
|
53
31
|
ensure
|
54
|
-
@
|
55
|
-
|
56
|
-
|
57
|
-
def release(resource)
|
58
|
-
if resource.__discarded__
|
59
|
-
@size -= 1
|
60
|
-
elsif resource
|
61
|
-
return_to_stock(resource)
|
62
|
-
dequeue
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def dequeue
|
67
|
-
return if @queue.empty? || @stock.empty?
|
68
|
-
|
69
|
-
@queue.shift.schedule(@stock.shift)
|
32
|
+
@acquired_resources.delete(fiber)
|
33
|
+
@stock.push resource if resource
|
70
34
|
end
|
71
|
-
|
72
|
-
def return_to_stock(resource)
|
73
|
-
@stock << resource
|
74
|
-
end
|
75
|
-
|
76
|
-
def from_stock
|
77
|
-
@stock.shift || (@size < @limit && allocate)
|
78
|
-
end
|
79
|
-
|
35
|
+
|
80
36
|
def method_missing(sym, *args, &block)
|
81
37
|
acquire { |r| r.send(sym, *args, &block) }
|
82
38
|
end
|
@@ -85,33 +41,15 @@ module Polyphony
|
|
85
41
|
true
|
86
42
|
end
|
87
43
|
|
88
|
-
# Extension to allow discarding of resources
|
89
|
-
module ResourceExtensions
|
90
|
-
def __discarded__
|
91
|
-
@__discarded__
|
92
|
-
end
|
93
|
-
|
94
|
-
def __discard__
|
95
|
-
@__discarded__ = true
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
44
|
# Allocates a resource
|
100
45
|
# @return [any] allocated resource
|
101
|
-
def
|
102
|
-
@size += 1
|
103
|
-
@allocator.().tap { |r| r.extend ResourceExtensions }
|
104
|
-
end
|
105
|
-
|
106
|
-
def <<(resource)
|
46
|
+
def add_to_stock
|
107
47
|
@size += 1
|
108
|
-
|
109
|
-
@stock << resource
|
110
|
-
dequeue
|
48
|
+
@stock << @allocator.call
|
111
49
|
end
|
112
50
|
|
113
51
|
def preheat!
|
114
|
-
|
52
|
+
add_to_stock while @size < @limit
|
115
53
|
end
|
116
54
|
end
|
117
55
|
end
|
data/lib/polyphony/core/sync.rb
CHANGED
@@ -4,18 +4,21 @@ module Polyphony
|
|
4
4
|
# Implements mutex lock for synchronizing access to a shared resource
|
5
5
|
class Mutex
|
6
6
|
def initialize
|
7
|
-
@
|
7
|
+
@store = Queue.new
|
8
|
+
@store << :token
|
8
9
|
end
|
9
10
|
|
10
11
|
def synchronize
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
return yield if @holding_fiber == Fiber.current
|
13
|
+
|
14
|
+
begin
|
15
|
+
token = @store.shift
|
16
|
+
@holding_fiber = Fiber.current
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
@holding_fiber = nil
|
20
|
+
@store << token
|
21
|
+
end
|
19
22
|
end
|
20
23
|
end
|
21
24
|
end
|