Merge pull request #6106 from dearblue/fiber-limits-ease

Ease fiber limitations
This commit is contained in:
Yukihiro "Matz" Matsumoto
2023-12-13 16:10:42 +09:00
committed by GitHub
6 changed files with 464 additions and 201 deletions
-2
View File
@@ -1458,8 +1458,6 @@ MRB_API mrb_value mrb_fiber_new(mrb_state *mrb, const struct RProc *proc);
* Implemented in mruby-fiber
*
* Switches to the specified fiber and executes. Like the `Fiber#resume` method.
*
* @note It can only be called before entering the mruby VM (e.g. in the `main()` function).
*/
MRB_API mrb_value mrb_fiber_resume(mrb_state *mrb, mrb_value fib, mrb_int argc, const mrb_value *argv);
+20 -7
View File
@@ -170,6 +170,17 @@ fiber_check_cfunc(mrb_state *mrb, struct mrb_context *c)
}
}
static void
fiber_check_cfunc_recursive(mrb_state *mrb, struct mrb_context *c)
{
for (;; c = c->prev) {
fiber_check_cfunc(mrb, c);
if (c == mrb->root_c || !c->prev) {
break;
}
}
}
static void
fiber_switch_context(mrb_state *mrb, struct mrb_context *c)
{
@@ -275,14 +286,17 @@ fiber_switch(mrb_state *mrb, mrb_value self, mrb_int len, const mrb_value *a, mr
else {
value = fiber_result(mrb, a, len);
if (vmexec) {
if (c->ci > c->cibase) c->ci--; /* pop dummy callinfo */
c->ci[1].stack[0] = value;
}
}
if (vmexec) {
int cci = old_c->ci->cci;
c->vmexec = TRUE;
value = mrb_vm_exec(mrb, c->ci->proc, c->ci->pc);
mrb->c = old_c;
old_c->ci->cci = cci; /* restore values as they may have changed in Fiber.yield */
}
else {
MARK_CONTEXT_MODIFY(c);
@@ -304,19 +318,19 @@ fiber_switch(mrb_state *mrb, mrb_value self, mrb_int len, const mrb_value *a, mr
* to the next <code>Fiber.yield</code> statement inside the fiber's block
* or to the block value if it runs to completion without any
* <code>Fiber.yield</code>
*
* This method cannot be called from C using <code>mrb_funcall()</code>.
* Use <code>mrb_fiber_resume()</code> function instead.
*/
static mrb_value
fiber_resume(mrb_state *mrb, mrb_value self)
{
const mrb_value *a;
mrb_int len;
mrb_bool vmexec = FALSE;
fiber_check_cfunc(mrb, mrb->c);
mrb_get_args(mrb, "*!", &a, &len);
return fiber_switch(mrb, self, len, a, TRUE, FALSE);
if (mrb->c->ci->cci > 0) {
vmexec = TRUE;
}
return fiber_switch(mrb, self, len, a, TRUE, vmexec);
}
MRB_API mrb_value
@@ -429,7 +443,7 @@ fiber_transfer(mrb_state *mrb, mrb_value self)
const mrb_value* a;
mrb_int len;
fiber_check_cfunc(mrb, mrb->c);
fiber_check_cfunc_recursive(mrb, mrb->c);
mrb_get_args(mrb, "*!", &a, &len);
if (c->status == MRB_FIBER_RESUMED) {
@@ -472,7 +486,6 @@ mrb_fiber_yield(mrb_state *mrb, mrb_int len, const mrb_value *a)
if (c->vmexec) {
c->vmexec = FALSE;
mrb->c->ci->cci = CINFO_RESUMED;
c->ci--; /* pop callinfo for yield */
}
MARK_CONTEXT_MODIFY(mrb->c);
return fiber_result(mrb, a, len);
+197 -191
View File
@@ -1,204 +1,210 @@
assert('Fiber.new') do
f = Fiber.new{}
assert_kind_of Fiber, f
end
begin
$fiber_test_activity = __FILE__
assert('Fiber#resume') do
f = Fiber.new{|x| x }
assert_equal 2, f.resume(2)
end
assert('Fiber#transfer') do
ary = []
f2 = nil
f1 = Fiber.new{
ary << f2.transfer(:foo)
:ok
}
f2 = Fiber.new{
ary << f1.transfer(:baz)
:ng
}
assert_equal(:ok, f1.transfer)
assert_equal([:baz], ary)
assert_false f1.alive?
end
assert('Fiber#alive?') do
f = Fiber.new{ Fiber.yield }
f.resume
assert_true f.alive?
f.resume
assert_false f.alive?
end
assert('Fiber#==') do
root = Fiber.current
assert_equal root, root
assert_equal root, Fiber.current
assert_false root != Fiber.current
f = Fiber.new {
assert_false root == Fiber.current
}
f.resume
assert_false f == root
assert_true f != root
end
assert('Fiber.yield') do
f = Fiber.new{|x| Fiber.yield x }
assert_equal 3, f.resume(3)
assert_true f.alive?
end
assert('FiberError') do
assert_equal StandardError, FiberError.superclass
end
assert('Fiber iteration') do
f1 = Fiber.new{
[1,2,3].each{|x| Fiber.yield(x)}
}
f2 = Fiber.new{
[9,8,7].each{|x| Fiber.yield(x)}
}
a = []
3.times {
a << f1.resume
a << f2.resume
}
assert_equal [1,9,2,8,3,7], a
end
assert('Fiber with splat in the block argument list') {
assert_equal([1], Fiber.new{|*x|x}.resume(1))
}
assert('Fiber raises on resume when dead') do
assert_raise(FiberError) do
assert('Fiber.new') do
f = Fiber.new{}
assert_kind_of Fiber, f
end
assert('Fiber#resume') do
f = Fiber.new{|x| x }
assert_equal 2, f.resume(2)
end
assert('Fiber#transfer') do
ary = []
f2 = nil
f1 = Fiber.new{
ary << f2.transfer(:foo)
:ok
}
f2 = Fiber.new{
ary << f1.transfer(:baz)
:ng
}
assert_equal(:ok, f1.transfer)
assert_equal([:baz], ary)
assert_false f1.alive?
end
assert('Fiber#alive?') do
f = Fiber.new{ Fiber.yield }
f.resume
assert_true f.alive?
f.resume
assert_false f.alive?
f.resume
end
end
assert('Yield raises when called on root fiber') do
assert_raise(FiberError) { Fiber.yield }
end
assert('Fiber#==') do
root = Fiber.current
assert_equal root, root
assert_equal root, Fiber.current
assert_false root != Fiber.current
f = Fiber.new {
assert_false root == Fiber.current
}
f.resume
assert_false f == root
assert_true f != root
end
assert('Double resume of Fiber') do
f1 = Fiber.new {}
f2 = Fiber.new {
f1.resume
assert_raise(FiberError) { f2.resume }
Fiber.yield 0
assert('Fiber.yield') do
f = Fiber.new{|x| Fiber.yield x }
assert_equal 3, f.resume(3)
assert_true f.alive?
end
assert('FiberError') do
assert_equal StandardError, FiberError.superclass
end
assert('Fiber iteration') do
f1 = Fiber.new{
[1,2,3].each{|x| Fiber.yield(x)}
}
f2 = Fiber.new{
[9,8,7].each{|x| Fiber.yield(x)}
}
a = []
3.times {
a << f1.resume
a << f2.resume
}
assert_equal [1,9,2,8,3,7], a
end
assert('Fiber with splat in the block argument list') {
assert_equal([1], Fiber.new{|*x|x}.resume(1))
}
assert_equal 0, f2.resume
f2.resume
assert_false f1.alive?
assert_false f2.alive?
end
assert('Recursive resume of Fiber') do
f1, f2 = nil, nil
f1 = Fiber.new { assert_raise(FiberError) { f2.resume } }
f2 = Fiber.new {
f1.resume
Fiber.yield 0
}
f3 = Fiber.new {
assert('Fiber raises on resume when dead') do
assert_raise(FiberError) do
f = Fiber.new{}
f.resume
assert_false f.alive?
f.resume
end
end
assert('Yield raises when called on root fiber') do
assert_raise(FiberError) { Fiber.yield }
end
assert('Double resume of Fiber') do
f1 = Fiber.new {}
f2 = Fiber.new {
f1.resume
assert_raise(FiberError) { f2.resume }
Fiber.yield 0
}
assert_equal 0, f2.resume
f2.resume
}
assert_equal 0, f3.resume
f2.resume
assert_false f1.alive?
assert_false f2.alive?
assert_false f3.alive?
end
assert_false f1.alive?
assert_false f2.alive?
end
assert('Root fiber resume') do
root = Fiber.current
assert_raise(FiberError) { root.resume }
f = Fiber.new {
assert('Recursive resume of Fiber') do
f1, f2 = nil, nil
f1 = Fiber.new { assert_raise(FiberError) { f2.resume } }
f2 = Fiber.new {
f1.resume
Fiber.yield 0
}
f3 = Fiber.new {
f2.resume
}
assert_equal 0, f3.resume
f2.resume
assert_false f1.alive?
assert_false f2.alive?
assert_false f3.alive?
end
assert('Root fiber resume') do
root = Fiber.current
assert_raise(FiberError) { root.resume }
}
f.resume
assert_false f.alive?
end
assert('Fiber without block') do
assert_raise(ArgumentError) { Fiber.new }
end
assert('Transfer to self.') do
result = []
f = Fiber.new { result << :start; f.transfer; result << :end }
f.transfer
assert_equal [:start, :end], result
result = []
f = Fiber.new { result << :start; f.transfer; result << :end }
f.resume
assert_equal [:start, :end], result
end
assert('Resume transferred fiber') do
f = Fiber.new {
assert_raise(FiberError) { f.resume }
}
f.transfer
end
assert('Root fiber transfer.') do
result = nil
root = Fiber.current
f = Fiber.new {
result = :ok
root.transfer
}
f.transfer
assert_true f.alive?
assert_equal :ok, result
end
assert('Break nested fiber with root fiber transfer') do
root = Fiber.current
result = nil
f2 = nil
f1 = Fiber.new {
root.transfer(f2.transfer)
result = :f1
}
f2 = Fiber.new {
result = :to_root
root.transfer :from_f2
result = :f2
}
assert_equal :from_f2, f1.transfer
assert_equal :to_root, result
assert_equal :f2, f2.transfer
assert_equal :f2, result
assert_false f2.alive?
assert_equal nil, f1.transfer
assert_equal :f1, f1.transfer
assert_equal :f1, result
assert_false f1.alive?
end
assert('CRuby Fiber#transfer test.') do
ary = []
f2 = nil
f1 = Fiber.new{
ary << f2.transfer(:foo)
:ok
}
f2 = Fiber.new{
ary << f1.transfer(:baz)
:ng
}
assert_equal :ok, f1.transfer
assert_equal [:baz], ary
f = Fiber.new {
assert_raise(FiberError) { root.resume }
}
f.resume
assert_false f.alive?
end
assert('Fiber without block') do
assert_raise(ArgumentError) { Fiber.new }
end
assert('Transfer to self.') do
result = []
f = Fiber.new { result << :start; f.transfer; result << :end }
f.transfer
assert_equal [:start, :end], result
result = []
f = Fiber.new { result << :start; f.transfer; result << :end }
f.resume
assert_equal [:start, :end], result
end
assert('Resume transferred fiber') do
f = Fiber.new {
assert_raise(FiberError) { f.resume }
}
f.transfer
end
assert('Root fiber transfer.') do
result = nil
root = Fiber.current
f = Fiber.new {
result = :ok
root.transfer
}
f.transfer
assert_true f.alive?
assert_equal :ok, result
end
assert('Break nested fiber with root fiber transfer') do
root = Fiber.current
result = nil
f2 = nil
f1 = Fiber.new {
root.transfer(f2.transfer)
result = :f1
}
f2 = Fiber.new {
result = :to_root
root.transfer :from_f2
result = :f2
}
assert_equal :from_f2, f1.transfer
assert_equal :to_root, result
assert_equal :f2, f2.transfer
assert_equal :f2, result
assert_false f2.alive?
assert_equal nil, f1.transfer
assert_equal :f1, f1.transfer
assert_equal :f1, result
assert_false f1.alive?
end
assert('CRuby Fiber#transfer test.') do
ary = []
f2 = nil
f1 = Fiber.new{
ary << f2.transfer(:foo)
:ok
}
f2 = Fiber.new{
ary << f1.transfer(:baz)
:ng
}
assert_equal :ok, f1.transfer
assert_equal [:baz], ary
end
ensure
$fiber_test_activity = nil
end
+155
View File
@@ -0,0 +1,155 @@
# This file tests fiber switching crossing C functions
unless RUBY_ENGINE == "mruby"
class Fiber
alias resume_by_c_func resume
alias resume_by_c_method resume
class << self
alias yield_by_c_func yield
def yield_by_c_method(*args)
raise FiberError, "ycan't cross C function boundary"
end
end
end
def Proc.c_tunnel
yield
end
end
begin
$fiber_test_activity = __FILE__
assert('Call Fiber#resume nested with C') do
assert_equal "ok1", Fiber.new { Fiber.new { "ok1" }.resume_by_c_func }.resume_by_c_func
assert_equal "ok2", Fiber.new { Fiber.new { "ok2" }.resume_by_c_method }.resume_by_c_func
assert_equal "ok3", Fiber.new { Fiber.new { "ok3" }.resume_by_c_func }.resume_by_c_method
assert_equal "ok4", Fiber.new { Fiber.new { "ok4" }.resume_by_c_method }.resume_by_c_method
assert_equal "ok5", Fiber.new { Proc.c_tunnel { Fiber.new { "ok5" }.resume_by_c_func } }.resume_by_c_func
assert_equal "ok6", Fiber.new { Proc.c_tunnel { Fiber.new { "ok6" }.resume_by_c_method } }.resume_by_c_func
assert_equal "ok7", Fiber.new { Proc.c_tunnel { Fiber.new { "ok7" }.resume_by_c_func } }.resume_by_c_method
assert_equal "ok8", Fiber.new { Proc.c_tunnel { Fiber.new { "ok8" }.resume_by_c_method } }.resume_by_c_method
assert_equal "ok9", Fiber.new { Proc.c_tunnel { Fiber.new { "ok9" }.resume } }.resume_by_c_func
assert_equal "ok10", Fiber.new { Proc.c_tunnel { Fiber.new { "ok10" }.resume } }.resume_by_c_method
end
assert('Call Fiber#resume and Fiber.yield mixed with C.') do
assert_equal 1, Fiber.new { Fiber.yield 1 }.resume_by_c_func
assert_equal 2, Fiber.new { Fiber.yield 2 }.resume_by_c_method
assert_equal 3, Fiber.new { Fiber.yield_by_c_func 3 }.resume
assert_equal 4, Fiber.new { Fiber.yield_by_c_func 4 }.resume_by_c_func
assert_equal 5, Fiber.new { Fiber.yield_by_c_func 5 }.resume_by_c_method
assert_raise(FiberError) { Fiber.new { Fiber.yield_by_c_method "bad" }.resume }
assert_raise(FiberError) { Fiber.new { Fiber.yield_by_c_method "bad" }.resume_by_c_func }
assert_raise(FiberError) { Fiber.new { Fiber.yield_by_c_method "bad" }.resume_by_c_method }
result = []
f1 = Fiber.new { result << Fiber.new { Fiber.yield 1; "bad" }.resume_by_c_func; 2 }
f2 = Fiber.new { result << f1.resume; 3 }
result << f2.resume
assert_equal [1, 2, 3], result
f1 = Fiber.new {
-> {
Fiber.yield 1
Fiber.yield_by_c_func 2
f2 = Fiber.new {
-> {
Fiber.yield_by_c_func 3
Fiber.yield 4
Fiber.yield_by_c_func 5
Fiber.yield 6
}.call
7
}
Fiber.yield f2.resume_by_c_func
Fiber.yield f2.resume
Fiber.yield f2.resume_by_c_method
Fiber.yield f2.resume
Fiber.yield f2.resume_by_c_func
Fiber.yield 8
}.call
Fiber.yield 9
10
}
result = []
10.times { result << f1.resume }
assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], result
end
assert('Call Fiber#resume and Fiber.yield mixed with C and raising exceptions') do
f = Fiber.new do
raise ZeroDivisionError
rescue
Fiber.yield "rescue"
"pass1"
ensure
Fiber.yield "ensure"
end
assert_equal "rescue", f.resume_by_c_method
assert_equal "ensure", f.resume_by_c_method
assert_equal "pass1", f.resume_by_c_method
assert_raise(FiberError) { f.resume_by_c_method }
f = Fiber.new do
raise ZeroDivisionError
rescue
Fiber.yield "rescue"
"pass2"
ensure
Fiber.yield "ensure"
end
assert_equal "rescue", f.resume_by_c_func
assert_equal "ensure", f.resume_by_c_func
assert_equal "pass2", f.resume_by_c_func
assert_raise(FiberError) { f.resume_by_c_func }
f2 = Fiber.new do
-> do
Fiber.yield 1
raise "3"
ensure
Fiber.yield 2
end.call
"NOT REACH 1"
end
f1 = Fiber.new do
Fiber.yield f2.resume_by_c_func
begin
Fiber.yield f2.resume
Fiber.yield f2.resume_by_c_method
Fiber.yield "NOT REACH 2"
rescue => e
Fiber.yield e.message
Fiber.yield 4
ensure
Fiber.yield 5
end
Fiber.yield 6
7
end
result = []
7.times { result << f1.resume }
assert_equal [1, 2, "3", 4, 5, 6, 7], result
end
assert('Call Fiber#transfer with C') do
assert_equal "ok1", Fiber.new { Fiber.new { "ok1" }.resume_by_c_method }.transfer
assert_equal "ok2", Fiber.new { Fiber.new { "ok2" }.resume_by_c_func }.transfer
assert_raise(FiberError) { Proc.c_tunnel { Fiber.new { "BAD!" }.transfer } }
b = Fiber.current
a = Fiber.new {
Proc.c_tunnel {
Fiber.new {
b.transfer
}.resume
}
}
assert_raise(FiberError) { a.transfer }
end
ensure
$fiber_test_activity = nil
end
+87
View File
@@ -0,0 +1,87 @@
#include <mruby.h>
#include <mruby/string.h>
#include <mruby/variable.h>
#include <stdio.h>
#include <stdlib.h>
static mrb_value
fiber_s_yield_by_c_func(mrb_state *mrb, mrb_value self)
{
mrb_value a = mrb_get_arg1(mrb);
return mrb_fiber_yield(mrb, 1, &a);
}
static mrb_value
fiber_s_yield_by_c_method(mrb_state *mrb, mrb_value self)
{
mrb_value a = mrb_get_arg1(mrb);
return mrb_funcall_argv(mrb, self, mrb_intern_lit(mrb, "yield"), 1, &a);
}
static mrb_value
fiber_resume_by_c_func(mrb_state *mrb, mrb_value self)
{
int ci_index = mrb->c->ci - mrb->c->cibase;
mrb_value ret = mrb_fiber_resume(mrb, self, 0, NULL);
if (ci_index != mrb->c->ci - mrb->c->cibase) {
mrb_raisef(mrb, E_EXCEPTION,
"[BUG] INVALID CI POSITION (expected %d, but actual %d) [BUG]",
(int)ci_index, (int)(mrb->c->ci - mrb->c->cibase));
}
return ret;
}
static mrb_value
fiber_resume_by_c_method(mrb_state *mrb, mrb_value self)
{
int ci_index = mrb->c->ci - mrb->c->cibase;
mrb_value ret = mrb_funcall_argv(mrb, self, mrb_intern_lit(mrb, "resume"), 0, NULL);
if (ci_index != mrb->c->ci - mrb->c->cibase) {
mrb_raisef(mrb, E_EXCEPTION,
"[BUG] INVALID CI POSITION (expected %d, but actual %d) [BUG]",
(int)ci_index, (int)(mrb->c->ci - mrb->c->cibase));
}
return ret;
}
static mrb_value
fiber_transfer_by_c(mrb_state *mrb, mrb_value self)
{
return mrb_funcall_argv(mrb, self, mrb_intern_lit(mrb, "transfer"), 0, NULL);
}
static mrb_value
proc_s_c_tunnel(mrb_state *mrb, mrb_value self)
{
mrb_value b;
mrb_get_args(mrb, "&!", &b);
return mrb_yield_argv(mrb, b, 0, NULL);
}
static void
check_activity(mrb_state *mrb)
{
mrb_value act = mrb_gv_get(mrb, mrb_intern_lit(mrb, "$fiber_test_activity"));
if (mrb_test(act)) {
act = mrb_obj_as_string(mrb, act);
fprintf(stderr, "\n\t<<<%s%.*s>>>\n",
"mruby VM has an unexpected outage in ", (int)RSTRING_LEN(act), RSTRING_PTR(act));
abort();
}
}
void
mrb_mruby_fiber_gem_test(mrb_state *mrb)
{
struct RClass *fiber_class = mrb_class_get(mrb, "Fiber");
mrb_define_class_method(mrb, fiber_class, "yield_by_c_func", fiber_s_yield_by_c_func, MRB_ARGS_ANY());
mrb_define_class_method(mrb, fiber_class, "yield_by_c_method", fiber_s_yield_by_c_method, MRB_ARGS_ANY());
mrb_define_method(mrb, fiber_class, "resume_by_c_func", fiber_resume_by_c_func, MRB_ARGS_NONE());
mrb_define_method(mrb, fiber_class, "resume_by_c_method", fiber_resume_by_c_method, MRB_ARGS_NONE());
mrb_define_method(mrb, fiber_class, "transfer_by_c", fiber_transfer_by_c, MRB_ARGS_NONE());
mrb_define_class_method(mrb, mrb->proc_class, "c_tunnel", proc_s_c_tunnel, MRB_ARGS_NONE() | MRB_ARGS_BLOCK());
mrb_gv_set(mrb, mrb_intern_lit(mrb, "$fiber_test_activity"), mrb_nil_value());
mrb_state_atexit(mrb, check_activity);
}
+5 -1
View File
@@ -1839,6 +1839,7 @@ RETRY_TRY_BLOCK:
}
recv = MRB_METHOD_FUNC(m)(mrb, recv);
}
mrb_assert(mrb->c->ci > mrb->c->cibase);
mrb_gc_arena_shrink(mrb, ai);
if (mrb->exc) goto L_RAISE;
ci = mrb->c->ci;
@@ -2245,7 +2246,10 @@ RETRY_TRY_BLOCK:
mrb->c = c->prev;
if (!mrb->c) mrb->c = mrb->root_c;
else c->prev = NULL;
goto L_RAISE;
if (!c->vmexec) goto L_RAISE;
mrb->jmp = prev_jmp;
if (!prev_jmp) return mrb_obj_value(mrb->exc);
MRB_THROW(prev_jmp);
}
}