polyphony 0.29 → 0.30
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +1 -1
- data/TODO.md +15 -10
- data/docs/getting-started/tutorial.md +3 -3
- data/docs/index.md +2 -3
- data/docs/{technical-overview → main-concepts}/concurrency.md +62 -15
- data/docs/{technical-overview → main-concepts}/design-principles.md +21 -8
- data/docs/{technical-overview → main-concepts}/exception-handling.md +80 -38
- data/docs/{technical-overview → main-concepts}/extending.md +4 -3
- data/docs/{technical-overview → main-concepts}/fiber-scheduling.md +3 -3
- data/docs/{technical-overview.md → main-concepts.md} +2 -2
- data/examples/core/xx-at_exit.rb +29 -0
- data/examples/core/xx-fork-terminate.rb +27 -0
- data/examples/core/xx-pingpong.rb +18 -0
- data/examples/core/xx-stop.rb +20 -0
- data/ext/gyro/async.c +1 -1
- data/ext/gyro/extconf.rb +0 -3
- data/ext/gyro/gyro.c +7 -8
- data/ext/gyro/gyro.h +2 -0
- data/ext/gyro/queue.c +6 -6
- data/ext/gyro/selector.c +32 -1
- data/ext/gyro/thread.c +55 -9
- data/ext/gyro/timer.c +1 -0
- data/lib/polyphony/core/exceptions.rb +4 -1
- data/lib/polyphony/core/global_api.rb +1 -6
- data/lib/polyphony/core/thread_pool.rb +3 -3
- data/lib/polyphony/extensions/core.rb +7 -1
- data/lib/polyphony/extensions/fiber.rb +159 -72
- data/lib/polyphony/extensions/io.rb +2 -4
- data/lib/polyphony/extensions/openssl.rb +0 -17
- data/lib/polyphony/extensions/thread.rb +46 -22
- data/lib/polyphony/version.rb +1 -1
- data/lib/polyphony.rb +20 -18
- data/test/coverage.rb +1 -1
- data/test/helper.rb +7 -3
- data/test/test_fiber.rb +285 -72
- data/test/test_global_api.rb +7 -52
- data/test/test_io.rb +8 -0
- data/test/test_signal.rb +1 -0
- data/test/test_thread.rb +76 -56
- data/test/test_thread_pool.rb +27 -5
- data/test/test_throttler.rb +1 -0
- metadata +12 -12
- data/lib/polyphony/core/supervisor.rb +0 -114
- data/lib/polyphony/line_reader.rb +0 -82
- data/test/test_gyro.rb +0 -25
- data/test/test_supervisor.rb +0 -180
data/ext/gyro/gyro.c
CHANGED
@@ -47,11 +47,11 @@ VALUE SYM_fiber_switchpoint;
|
|
47
47
|
VALUE SYM_fiber_terminate;
|
48
48
|
|
49
49
|
|
50
|
-
|
51
|
-
//
|
52
|
-
|
53
|
-
|
54
|
-
|
50
|
+
static VALUE Gyro_break_set(VALUE self) {
|
51
|
+
// break_flag = 1;
|
52
|
+
ev_break(Gyro_Selector_current_thread_ev_loop(), EVBREAK_ALL);
|
53
|
+
return Qnil;
|
54
|
+
}
|
55
55
|
|
56
56
|
// static VALUE Gyro_break_get(VALUE self) {
|
57
57
|
// return (break_flag == 0) ? Qfalse : Qtrue;
|
@@ -82,7 +82,6 @@ static VALUE Gyro_unref(VALUE self) {
|
|
82
82
|
}
|
83
83
|
|
84
84
|
static VALUE Gyro_suspend(VALUE self) {
|
85
|
-
rb_ivar_set(self, ID_runnable_value, Qnil);
|
86
85
|
VALUE ret = Thread_switch_fiber(rb_thread_current());
|
87
86
|
|
88
87
|
if (RTEST(rb_obj_is_kind_of(ret, rb_eException))) {
|
@@ -117,7 +116,7 @@ static VALUE Fiber_state(VALUE self) {
|
|
117
116
|
return SYM_waiting;
|
118
117
|
}
|
119
118
|
|
120
|
-
|
119
|
+
void Gyro_schedule_fiber(VALUE fiber, VALUE value) {
|
121
120
|
VALUE thread = rb_ivar_get(fiber, ID_ivar_thread);
|
122
121
|
if (thread != Qnil) {
|
123
122
|
Thread_schedule_fiber(thread, fiber, value);
|
@@ -140,7 +139,7 @@ void Init_Gyro() {
|
|
140
139
|
rb_define_singleton_method(mGyro, "unref", Gyro_unref, 0);
|
141
140
|
rb_define_singleton_method(mGyro, "trace", Gyro_trace, 1);
|
142
141
|
|
143
|
-
|
142
|
+
rb_define_singleton_method(mGyro, "break!", Gyro_break_set, 0);
|
144
143
|
// rb_define_singleton_method(mGyro, "break?", Gyro_break_get, 0);
|
145
144
|
|
146
145
|
rb_define_global_function("snooze", Gyro_snooze, 0);
|
data/ext/gyro/gyro.h
CHANGED
@@ -48,7 +48,9 @@ VALUE Thread_switch_fiber(VALUE thread);
|
|
48
48
|
VALUE Fiber_await();
|
49
49
|
VALUE Thread_schedule_fiber(VALUE thread, VALUE fiber, VALUE value);
|
50
50
|
VALUE Thread_post_fork(VALUE thread);
|
51
|
+
VALUE Gyro_Selector_break_out_of_ev_loop(VALUE self);
|
51
52
|
|
53
|
+
VALUE Gyro_Queue_push(VALUE self, VALUE value);
|
52
54
|
|
53
55
|
#define OBJ_ID(obj) (NUM2LONG(rb_funcall(obj, rb_intern("object_id"), 0)))
|
54
56
|
#define INSPECT(...) (rb_funcall(rb_cObject, rb_intern("p"), __VA_ARGS__))
|
data/ext/gyro/queue.c
CHANGED
@@ -75,7 +75,9 @@ VALUE Gyro_Queue_shift(VALUE self) {
|
|
75
75
|
|
76
76
|
VALUE async = rb_funcall(cGyro_Async, ID_new, 0);
|
77
77
|
rb_ary_push(queue->wait_queue, async);
|
78
|
-
|
78
|
+
VALUE ret = Gyro_Async_await(async);
|
79
|
+
RB_GC_GUARD(async);
|
80
|
+
return ret;
|
79
81
|
}
|
80
82
|
|
81
83
|
VALUE Gyro_Queue_shift_no_wait(VALUE self) {
|
@@ -85,7 +87,7 @@ VALUE Gyro_Queue_shift_no_wait(VALUE self) {
|
|
85
87
|
return rb_ary_shift(queue->queue);
|
86
88
|
}
|
87
89
|
|
88
|
-
VALUE
|
90
|
+
VALUE Gyro_Queue_shift_each(VALUE self) {
|
89
91
|
struct Gyro_Queue *queue;
|
90
92
|
GetGyro_Queue(self, queue);
|
91
93
|
|
@@ -97,9 +99,7 @@ VALUE Gyro_Queue_shift_all(VALUE self) {
|
|
97
99
|
for (long i = 0; i < len; i++) {
|
98
100
|
rb_yield(RARRAY_AREF(old_queue, i));
|
99
101
|
}
|
100
|
-
|
101
|
-
// rb_yield(rb_ary_shift(old_queue));
|
102
|
-
// }
|
102
|
+
RB_GC_GUARD(old_queue);
|
103
103
|
return self;
|
104
104
|
}
|
105
105
|
else {
|
@@ -135,7 +135,7 @@ void Init_Gyro_Queue() {
|
|
135
135
|
|
136
136
|
rb_define_method(cGyro_Queue, "shift_no_wait", Gyro_Queue_shift_no_wait, 0);
|
137
137
|
|
138
|
-
rb_define_method(cGyro_Queue, "shift_each",
|
138
|
+
rb_define_method(cGyro_Queue, "shift_each", Gyro_Queue_shift_each, 0);
|
139
139
|
rb_define_method(cGyro_Queue, "clear", Gyro_Queue_clear, 0);
|
140
140
|
rb_define_method(cGyro_Queue, "empty?", Gyro_Queue_empty_p, 0);
|
141
141
|
}
|
data/ext/gyro/selector.c
CHANGED
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
struct Gyro_Selector {
|
4
4
|
struct ev_loop *ev_loop;
|
5
|
-
long
|
5
|
+
long run_no_wait_count;
|
6
|
+
int ev_loop_running;
|
7
|
+
struct ev_async async;
|
6
8
|
};
|
7
9
|
|
8
10
|
VALUE cGyro_Selector = Qnil;
|
@@ -13,6 +15,7 @@ static void Gyro_Selector_mark(void *ptr) {
|
|
13
15
|
|
14
16
|
static void Gyro_Selector_free(void *ptr) {
|
15
17
|
struct Gyro_Selector *selector = ptr;
|
18
|
+
ev_async_stop(selector->ev_loop, &selector->async);
|
16
19
|
if (selector->ev_loop && !ev_is_default_loop(selector->ev_loop)) {
|
17
20
|
// printf("Selector garbage collected before being stopped!\n");
|
18
21
|
ev_loop_destroy(selector->ev_loop);
|
@@ -67,6 +70,11 @@ long Gyro_Selector_pending_count(VALUE self) {
|
|
67
70
|
return ev_pending_count(selector->ev_loop);
|
68
71
|
}
|
69
72
|
|
73
|
+
void dummy_async_callback(struct ev_loop *ev_loop, struct ev_async *ev_async, int revents) {
|
74
|
+
// This callback does nothing, the selector's async is used solely for waking
|
75
|
+
// up the event loop.
|
76
|
+
}
|
77
|
+
|
70
78
|
static VALUE Gyro_Selector_initialize(VALUE self, VALUE thread) {
|
71
79
|
struct Gyro_Selector *selector;
|
72
80
|
GetGyro_Selector(self, selector);
|
@@ -75,6 +83,9 @@ static VALUE Gyro_Selector_initialize(VALUE self, VALUE thread) {
|
|
75
83
|
selector->ev_loop = use_default_loop ? EV_DEFAULT : ev_loop_new(EVFLAG_NOSIGMASK);
|
76
84
|
selector->run_no_wait_count = 0;
|
77
85
|
|
86
|
+
ev_async_init(&selector->async, dummy_async_callback);
|
87
|
+
ev_async_start(selector->ev_loop, &selector->async);
|
88
|
+
|
78
89
|
ev_run(selector->ev_loop, EVRUN_NOWAIT);
|
79
90
|
|
80
91
|
return Qnil;
|
@@ -86,7 +97,9 @@ inline VALUE Gyro_Selector_run(VALUE self, VALUE current_fiber) {
|
|
86
97
|
if (selector->ev_loop) {
|
87
98
|
selector->run_no_wait_count = 0;
|
88
99
|
FIBER_TRACE(2, SYM_fiber_ev_loop_enter, current_fiber);
|
100
|
+
selector->ev_loop_running = 1;
|
89
101
|
ev_run(selector->ev_loop, EVRUN_ONCE);
|
102
|
+
selector->ev_loop_running = 0;
|
90
103
|
FIBER_TRACE(2, SYM_fiber_ev_loop_leave, current_fiber);
|
91
104
|
}
|
92
105
|
return Qnil;
|
@@ -118,6 +131,23 @@ VALUE Gyro_Selector_stop(VALUE self) {
|
|
118
131
|
return Qnil;
|
119
132
|
}
|
120
133
|
|
134
|
+
VALUE Gyro_Selector_break_out_of_ev_loop(VALUE self) {
|
135
|
+
struct Gyro_Selector *selector;
|
136
|
+
GetGyro_Selector(self, selector);
|
137
|
+
|
138
|
+
if (selector->ev_loop_running) {
|
139
|
+
// Since the loop will run until at least one event has occurred, we signal
|
140
|
+
// the associated async watcher, which will cause the ev loop to return. In
|
141
|
+
// contrast to using `ev_break` to break out of the loop, which should be
|
142
|
+
// called from the same thread (from within the ev_loop), using an
|
143
|
+
// `ev_async` allows us to interrupt the event loop across threads.
|
144
|
+
ev_async_send(selector->ev_loop, &selector->async);
|
145
|
+
return Qtrue;
|
146
|
+
}
|
147
|
+
|
148
|
+
return Qnil;
|
149
|
+
}
|
150
|
+
|
121
151
|
inline static VALUE Gyro_Selector_wait_readable(VALUE self, VALUE io) {
|
122
152
|
VALUE watcher = IO_read_watcher(io);
|
123
153
|
return Gyro_IO_await(watcher);
|
@@ -143,4 +173,5 @@ void Init_Gyro_Selector() {
|
|
143
173
|
rb_define_method(cGyro_Selector, "wait_readable", Gyro_Selector_wait_readable, 1);
|
144
174
|
rb_define_method(cGyro_Selector, "wait_writable", Gyro_Selector_wait_writable, 1);
|
145
175
|
rb_define_method(cGyro_Selector, "wait_timeout", Gyro_Selector_wait_timeout, 1);
|
176
|
+
rb_define_method(cGyro_Selector, "break_out_of_ev_loop", Gyro_Selector_break_out_of_ev_loop, 0);
|
146
177
|
}
|
data/ext/gyro/thread.c
CHANGED
@@ -53,30 +53,33 @@ static VALUE Thread_stop_event_selector(VALUE self) {
|
|
53
53
|
if (selector != Qnil) {
|
54
54
|
rb_funcall(selector, ID_stop, 0);
|
55
55
|
}
|
56
|
+
// Nullify the selector in order to prevent running the
|
57
|
+
// selector after the thread is done running.
|
58
|
+
rb_ivar_set(self, ID_ivar_event_selector, Qnil);
|
56
59
|
|
57
60
|
return self;
|
58
61
|
}
|
59
62
|
|
60
|
-
|
63
|
+
VALUE Thread_ref(VALUE self) {
|
61
64
|
VALUE count = rb_ivar_get(self, ID_fiber_ref_count);
|
62
65
|
int new_count = NUM2INT(count) + 1;
|
63
66
|
rb_ivar_set(self, ID_fiber_ref_count, INT2NUM(new_count));
|
64
67
|
return self;
|
65
68
|
}
|
66
69
|
|
67
|
-
|
70
|
+
VALUE Thread_unref(VALUE self) {
|
68
71
|
VALUE count = rb_ivar_get(self, ID_fiber_ref_count);
|
69
72
|
int new_count = NUM2INT(count) - 1;
|
70
73
|
rb_ivar_set(self, ID_fiber_ref_count, INT2NUM(new_count));
|
71
74
|
return self;
|
72
75
|
}
|
73
76
|
|
74
|
-
|
77
|
+
int Thread_fiber_ref_count(VALUE self) {
|
75
78
|
VALUE count = rb_ivar_get(self, ID_fiber_ref_count);
|
76
79
|
return NUM2INT(count);
|
77
80
|
}
|
78
81
|
|
79
|
-
|
82
|
+
void Thread_fiber_reset_ref_count(VALUE self) {
|
80
83
|
rb_ivar_set(self, ID_fiber_ref_count, INT2NUM(0));
|
81
84
|
}
|
82
85
|
|
@@ -97,7 +100,10 @@ static VALUE Thread_fiber_scheduling_stats(VALUE self) {
|
|
97
100
|
return stats;
|
98
101
|
}
|
99
102
|
|
100
|
-
|
103
|
+
VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
|
104
|
+
if (rb_fiber_alive_p(fiber) != Qtrue) {
|
105
|
+
return self;
|
106
|
+
}
|
101
107
|
FIBER_TRACE(3, SYM_fiber_schedule, fiber, value);
|
102
108
|
// if fiber is already scheduled, just set the scheduled value, then return
|
103
109
|
rb_ivar_set(fiber, ID_runnable_value, value);
|
@@ -108,13 +114,24 @@ inline VALUE Thread_schedule_fiber(VALUE self, VALUE fiber, VALUE value) {
|
|
108
114
|
VALUE queue = rb_ivar_get(self, ID_run_queue);
|
109
115
|
rb_ary_push(queue, fiber);
|
110
116
|
rb_ivar_set(fiber, ID_runnable, Qtrue);
|
117
|
+
|
118
|
+
if (rb_thread_current() != self) {
|
119
|
+
// if the fiber scheduling is done across threads, we need to make sure the
|
120
|
+
// target thread is woken up in case it is in the middle of running its
|
121
|
+
// event selector. Otherwise it's gonna be stuck waiting for an event to
|
122
|
+
// happen, not knowing that it there's already a fiber ready to run in its
|
123
|
+
// run queue.
|
124
|
+
VALUE selector = rb_ivar_get(self, ID_ivar_event_selector);
|
125
|
+
if (selector != Qnil) {
|
126
|
+
Gyro_Selector_break_out_of_ev_loop(selector);
|
127
|
+
}
|
128
|
+
}
|
111
129
|
return self;
|
112
130
|
}
|
113
131
|
|
114
132
|
VALUE Thread_switch_fiber(VALUE self) {
|
115
|
-
VALUE current_fiber;
|
133
|
+
VALUE current_fiber = rb_fiber_current();
|
116
134
|
if (__tracing_enabled__) {
|
117
|
-
current_fiber = rb_fiber_current();
|
118
135
|
if (rb_ivar_get(current_fiber, ID_ivar_running) != Qfalse) {
|
119
136
|
rb_funcall(rb_cObject, ID_fiber_trace, 2, SYM_fiber_switchpoint, current_fiber);
|
120
137
|
}
|
@@ -170,7 +187,7 @@ VALUE Thread_post_fork(VALUE self) {
|
|
170
187
|
return self;
|
171
188
|
}
|
172
189
|
|
173
|
-
|
190
|
+
VALUE Fiber_await() {
|
174
191
|
VALUE thread = rb_thread_current();
|
175
192
|
Thread_ref(thread);
|
176
193
|
VALUE ret = Thread_switch_fiber(thread);
|
@@ -179,10 +196,37 @@ inline VALUE Fiber_await() {
|
|
179
196
|
return ret;
|
180
197
|
}
|
181
198
|
|
182
|
-
|
199
|
+
VALUE Thread_current_event_selector() {
|
183
200
|
return rb_ivar_get(rb_thread_current(), ID_ivar_event_selector);
|
184
201
|
}
|
185
202
|
|
203
|
+
VALUE Thread_fiber_break_out_of_ev_loop(VALUE self, VALUE resume_obj) {
|
204
|
+
VALUE selector = rb_ivar_get(self, ID_ivar_event_selector);
|
205
|
+
Thread_schedule_fiber(self, rb_fiber_current(), resume_obj);
|
206
|
+
|
207
|
+
if (Gyro_Selector_break_out_of_ev_loop(selector) == Qnil) {
|
208
|
+
// we're not inside the ev_loop, so we just do a switchpoint
|
209
|
+
Thread_switch_fiber(self);
|
210
|
+
}
|
211
|
+
|
212
|
+
return self;
|
213
|
+
}
|
214
|
+
|
215
|
+
VALUE Thread_join_perform(VALUE self) {
|
216
|
+
if (!RTEST(rb_funcall(self, rb_intern("alive?"), 0))) {
|
217
|
+
return self;
|
218
|
+
}
|
219
|
+
|
220
|
+
VALUE async = rb_funcall(cGyro_Async, ID_new, 0);
|
221
|
+
VALUE wait_queue = rb_ivar_get(self, rb_intern("@join_wait_queue"));
|
222
|
+
|
223
|
+
Gyro_Queue_push(wait_queue, async);
|
224
|
+
|
225
|
+
VALUE ret = Gyro_Async_await(async);
|
226
|
+
RB_GC_GUARD(async);
|
227
|
+
return ret;
|
228
|
+
}
|
229
|
+
|
186
230
|
void Init_Thread() {
|
187
231
|
cQueue = rb_const_get(rb_cObject, rb_intern("Queue"));
|
188
232
|
|
@@ -196,10 +240,12 @@ void Init_Thread() {
|
|
196
240
|
rb_define_method(rb_cThread, "stop_event_selector", Thread_stop_event_selector, 0);
|
197
241
|
rb_define_method(rb_cThread, "reset_fiber_scheduling", Thread_reset_fiber_scheduling, 0);
|
198
242
|
rb_define_method(rb_cThread, "fiber_scheduling_stats", Thread_fiber_scheduling_stats, 0);
|
243
|
+
rb_define_method(rb_cThread, "break_out_of_ev_loop", Thread_fiber_break_out_of_ev_loop, 1);
|
199
244
|
|
200
245
|
rb_define_method(rb_cThread, "schedule_fiber", Thread_schedule_fiber, 2);
|
201
246
|
rb_define_method(rb_cThread, "switch_fiber", Thread_switch_fiber, 0);
|
202
247
|
|
248
|
+
rb_define_method(rb_cThread, "join_perform", Thread_join_perform, 0);
|
203
249
|
|
204
250
|
ID_create_event_selector = rb_intern("create_event_selector");
|
205
251
|
ID_empty = rb_intern("empty?");
|
data/ext/gyro/timer.c
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
export :Interrupt, :MoveOn, :Cancel
|
3
|
+
export :Interrupt, :MoveOn, :Cancel, :Terminate
|
4
4
|
|
5
5
|
# Common exception class for interrupting fibers. These exceptions allow
|
6
6
|
# control of fibers. Interrupt exceptions can encapsulate a value and thus
|
@@ -24,3 +24,6 @@ class MoveOn < Interrupt; end
|
|
24
24
|
# Cancel is used to interrupt a long-running blocking operation, bubbling the
|
25
25
|
# exception up through cancel scopes and supervisors.
|
26
26
|
class Cancel < Interrupt; end
|
27
|
+
|
28
|
+
# Terminate is used to interrupt a fiber once its parent fiber has terminated.
|
29
|
+
class Terminate < Interrupt; end
|
@@ -6,7 +6,6 @@ import '../extensions/core'
|
|
6
6
|
import '../extensions/fiber'
|
7
7
|
|
8
8
|
Exceptions = import '../core/exceptions'
|
9
|
-
Supervisor = import '../core/supervisor'
|
10
9
|
Throttler = import '../core/throttler'
|
11
10
|
|
12
11
|
# Global API methods to be included in ::Object
|
@@ -30,7 +29,7 @@ module API
|
|
30
29
|
end
|
31
30
|
|
32
31
|
def spin(tag = nil, &block)
|
33
|
-
Fiber.spin(tag, caller, &block)
|
32
|
+
Fiber.current.spin(tag, caller, &block)
|
34
33
|
end
|
35
34
|
|
36
35
|
def spin_loop(&block)
|
@@ -78,10 +77,6 @@ module API
|
|
78
77
|
Thread.current.fiber_unref
|
79
78
|
end
|
80
79
|
|
81
|
-
def supervise(&block)
|
82
|
-
Supervisor.new.await(&block)
|
83
|
-
end
|
84
|
-
|
85
80
|
def throttled_loop(rate, count: nil, &block)
|
86
81
|
throttler = Throttler.new(rate)
|
87
82
|
if count
|
@@ -22,9 +22,9 @@ class ThreadPool
|
|
22
22
|
def process(&block)
|
23
23
|
setup unless @task_queue
|
24
24
|
|
25
|
-
|
26
|
-
@task_queue << [block,
|
27
|
-
|
25
|
+
async = Gyro::Async.new
|
26
|
+
@task_queue << [block, async]
|
27
|
+
async.await
|
28
28
|
end
|
29
29
|
|
30
30
|
def cast(&block)
|
@@ -14,6 +14,8 @@ class ::Exception
|
|
14
14
|
|
15
15
|
alias_method :orig_initialize, :initialize
|
16
16
|
|
17
|
+
EXIT_EXCEPTION_CLASSES = [::Interrupt, ::SystemExit].freeze
|
18
|
+
|
17
19
|
def initialize(*args)
|
18
20
|
@__raising_fiber__ = Fiber.current
|
19
21
|
orig_initialize(*args)
|
@@ -21,11 +23,15 @@ class ::Exception
|
|
21
23
|
|
22
24
|
alias_method_once :orig_backtrace, :backtrace
|
23
25
|
def backtrace
|
24
|
-
unless @first_backtrace_call
|
26
|
+
unless @first_backtrace_call || EXIT_EXCEPTION_CLASSES.include?(self.class)
|
25
27
|
@first_backtrace_call = true
|
26
28
|
return orig_backtrace
|
27
29
|
end
|
28
30
|
|
31
|
+
sanitized_backtrace
|
32
|
+
end
|
33
|
+
|
34
|
+
def sanitized_backtrace
|
29
35
|
if @__raising_fiber__
|
30
36
|
backtrace = orig_backtrace || []
|
31
37
|
sanitize(backtrace + @__raising_fiber__.caller)
|