evt 0.3.2 → 0.4.0
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/README.md +47 -9
- data/evt.gemspec +1 -1
- data/ext/evt/epoll.h +21 -16
- data/ext/evt/evt.c +1 -0
- data/ext/evt/evt.h +1 -0
- data/ext/evt/select.h +11 -8
- data/lib/evt/backends/bundled.rb +3 -2
- data/lib/evt/backends/epoll.rb +54 -0
- data/lib/evt/backends/iocp.rb +3 -0
- data/lib/evt/backends/kqueue.rb +3 -0
- data/lib/evt/backends/select.rb +6 -0
- data/lib/evt/backends/uring.rb +9 -5
- data/lib/evt/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3275dc2e231b57c3e740671f2d431ede41ca23339ba799b280edeed1c7be8d2c
|
|
4
|
+
data.tar.gz: 4107ecb57ec3ff7e566187013348a056773ac654eb2b369b7fa8bb057675750e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db3172fe0ee6776480a64ee036aedb03b09ee5d5c8a5088fe68e4a969d7058964e8c55b004bb69962f61bffdb13a1c6c28ce49d339f8194e616ae44d877a9616
|
|
7
|
+
data.tar.gz: 94e287ddb35ae5f99e15ebf27ef76ce54ce3272008fe7c535483706111a8a39ff3168d2289135a027934d45c4382b51527c240fc9d7fbf6b9ce13f2ca7b016e3
|
data/README.md
CHANGED
|
@@ -19,30 +19,59 @@ The Event Library that designed for Ruby 3.0 Fiber Scheduler.
|
|
|
19
19
|
| epoll | ✅ (See 2) | ❌ | ❌ | ❌ |
|
|
20
20
|
| kqueue | ❌ | ❌ | ✅ (⚠️ See 5) | ✅ |
|
|
21
21
|
| IOCP | ❌ | ❌ (⚠️See 3) | ❌ | ❌ |
|
|
22
|
-
| Ruby (`IO.select`) | ✅
|
|
22
|
+
| Ruby (`IO.select`) | ✅ (See 6) | ✅ (⚠️See 4) | ✅ (See 6) | ✅ (See 6) |
|
|
23
23
|
|
|
24
24
|
1. when liburing is installed. (Currently fixing)
|
|
25
25
|
2. when kernel version >= 2.6.9
|
|
26
26
|
3. WOULD NOT WORK until `FILE_FLAG_OVERLAPPED` is included in I/O initialization process.
|
|
27
|
-
4. Some I/Os are not able to be nonblock under Windows. See [Scheduler Docs](https://docs.ruby-lang.org/en/master/doc/scheduler_md.html#label-IO).
|
|
27
|
+
4. Some I/Os are not able to be nonblock under Windows. Using POSIX select, **SLOW**. See [Scheduler Docs](https://docs.ruby-lang.org/en/master/doc/scheduler_md.html#label-IO).
|
|
28
28
|
5. `kqueue` performance in Darwin is very poor. **MAY BE DISABLED IN THE FUTURE.**
|
|
29
|
+
6. Using poll
|
|
29
30
|
|
|
30
31
|
### Benchmark
|
|
31
32
|
|
|
32
|
-
The benchmark is running under `v0.3.
|
|
33
|
+
The benchmark is running under `v0.3.6` version. See `example.rb` in [midori](https://github.com/midori-rb/midori.rb) for test code, the test is running under a single-thread server.
|
|
33
34
|
|
|
34
|
-
The test command is `wrk -t4 -c8192 -d30s http://localhost:
|
|
35
|
+
The test command is `wrk -t4 -c8192 -d30s http://localhost:8080`.
|
|
35
36
|
|
|
36
37
|
All of the systems have set their file descriptor limit to maximum.
|
|
37
38
|
On systems raising "Fiber unable to allocate memory", `sudo sysctl -w vm.max_map_count=1000000` is set.
|
|
38
39
|
|
|
39
40
|
| OS | CPU | Memory | Backend | req/s |
|
|
40
|
-
| ----- | ----------- | ------ | ---------------------- |
|
|
41
|
-
| Linux | Ryzen 2700x | 64GB | epoll |
|
|
41
|
+
| ----- | ----------- | ------ | ---------------------- | ------------- |
|
|
42
|
+
| Linux | Ryzen 2700x | 64GB | epoll | 2035742.59 |
|
|
42
43
|
| Linux | Ryzen 2700x | 64GB | io_uring | require fixes |
|
|
43
|
-
| Linux | Ryzen 2700x | 64GB | IO.select (using poll) |
|
|
44
|
-
| macOS | i7-6820HQ | 16GB | kqueue |
|
|
45
|
-
| macOS | i7-6820HQ | 16GB | IO.select (using poll) |
|
|
44
|
+
| Linux | Ryzen 2700x | 64GB | IO.select (using poll) | 1837640.54 |
|
|
45
|
+
| macOS | i7-6820HQ | 16GB | kqueue | 257821.78 |
|
|
46
|
+
| macOS | i7-6820HQ | 16GB | IO.select (using poll) | 338392.12 |
|
|
47
|
+
|
|
48
|
+
We also test the server with Redis request, with a [monkey-patched redis library](https://github.com/midori-rb/midori-contrib/blob/master/lib/midori-contrib/redic.rb). The example code is following:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
require 'evt'
|
|
52
|
+
require 'midori'
|
|
53
|
+
require 'midori-contrib/redic'
|
|
54
|
+
|
|
55
|
+
Fiber.set_scheduler Evt::Scheduler.new
|
|
56
|
+
REDIS = Redic.new
|
|
57
|
+
|
|
58
|
+
class HelloWorldAPI < Midori::API
|
|
59
|
+
get '/' do
|
|
60
|
+
REDIS.call 'GET', 'foo'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Fiber.schedule do
|
|
65
|
+
Midori::Runner.new(HelloWorldAPI).start
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The benckmark result is as following:
|
|
70
|
+
|
|
71
|
+
| OS | CPU | Memory | Backend | req/s |
|
|
72
|
+
| ----- | ----------- | ------ | ------- | --------- |
|
|
73
|
+
| Linux | Ryzen 2700x | 64GB | epoll | 378060.30 |
|
|
74
|
+
| macOS | i7-6820HQ | 16GB | kqueue | 204460.32 |
|
|
46
75
|
|
|
47
76
|
## Install
|
|
48
77
|
|
|
@@ -74,6 +103,15 @@ end
|
|
|
74
103
|
# "Hello World"
|
|
75
104
|
```
|
|
76
105
|
|
|
106
|
+
| | Windows `WaitFor...` | Windows `select` | Windows IOCP | `io_uring` | `epoll` | `kqueue` | `poll` | *NIX `select` |
|
|
107
|
+
| -------------- | -------------------- | ---------------- | ------------ | ---------- | ------- | -------- | ------ | ------------- |
|
|
108
|
+
| Anonymous Pipe | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
109
|
+
| Named Pipe | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
110
|
+
| Socket | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
111
|
+
| File | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
77
115
|
## Roadmap
|
|
78
116
|
|
|
79
117
|
- [x] Support epoll/kqueue/select
|
data/evt.gemspec
CHANGED
|
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
spec.description = "A low-level Event Handler designed for Ruby 3 Scheduler for better performance"
|
|
11
11
|
spec.homepage = "https://github.com/dsh0416/evt"
|
|
12
12
|
spec.license = 'BSD-3-Clause'
|
|
13
|
-
spec.required_ruby_version = '>= 3.0.0
|
|
13
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
14
14
|
|
|
15
15
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
16
|
spec.metadata["source_code_uri"] = "https://github.com/dsh0416/evt"
|
data/ext/evt/epoll.h
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include "evt.h"
|
|
4
4
|
|
|
5
5
|
#if HAVE_SYS_EPOLL_H
|
|
6
|
+
|
|
6
7
|
VALUE method_scheduler_epoll_init(VALUE self) {
|
|
7
8
|
rb_iv_set(self, "@epfd", INT2NUM(epoll_create(1))); // Size of epoll is ignored after Linux 2.6.8.
|
|
8
9
|
return Qnil;
|
|
@@ -25,7 +26,7 @@ VALUE method_scheduler_epoll_register(VALUE self, VALUE io, VALUE interest) {
|
|
|
25
26
|
event.events |= EPOLLOUT;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
event.events |=
|
|
29
|
+
event.events |= EPOLLRDHUP;
|
|
29
30
|
|
|
30
31
|
event.data.ptr = (void*) io;
|
|
31
32
|
|
|
@@ -35,14 +36,13 @@ VALUE method_scheduler_epoll_register(VALUE self, VALUE io, VALUE interest) {
|
|
|
35
36
|
|
|
36
37
|
VALUE method_scheduler_epoll_wait(VALUE self) {
|
|
37
38
|
int n, epfd, i, event_flag, timeout;
|
|
38
|
-
VALUE next_timeout, obj_io,
|
|
39
|
+
VALUE next_timeout, obj_io, iovs, result;
|
|
39
40
|
ID id_next_timeout = rb_intern("next_timeout");
|
|
40
41
|
ID id_push = rb_intern("push");
|
|
41
42
|
|
|
42
43
|
epfd = NUM2INT(rb_iv_get(self, "@epfd"));
|
|
43
44
|
next_timeout = rb_funcall(self, id_next_timeout, 0);
|
|
44
|
-
|
|
45
|
-
writables = rb_ary_new();
|
|
45
|
+
iovs = rb_ary_new();
|
|
46
46
|
|
|
47
47
|
if (next_timeout == Qnil) {
|
|
48
48
|
timeout = -1;
|
|
@@ -59,23 +59,28 @@ VALUE method_scheduler_epoll_wait(VALUE self) {
|
|
|
59
59
|
|
|
60
60
|
for (i = 0; i < n; i++) {
|
|
61
61
|
event_flag = events[i].events;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (event_flag & EPOLLOUT) {
|
|
68
|
-
obj_io = (VALUE) events[i].data.ptr;
|
|
69
|
-
rb_funcall(writables, id_push, 1, obj_io);
|
|
70
|
-
}
|
|
62
|
+
obj_io = (VALUE) events[i].data.ptr;
|
|
63
|
+
VALUE e = rb_ary_new2(2);
|
|
64
|
+
rb_ary_store(e, 0, obj_io);
|
|
65
|
+
rb_ary_store(e, 1, INT2NUM(event_flag));
|
|
66
|
+
rb_funcall(iovs, id_push, 1, e);
|
|
71
67
|
}
|
|
72
68
|
|
|
73
|
-
result = rb_ary_new2(
|
|
74
|
-
rb_ary_store(result, 0,
|
|
75
|
-
rb_ary_store(result, 1,
|
|
69
|
+
result = rb_ary_new2(3);
|
|
70
|
+
rb_ary_store(result, 0, rb_ary_new());
|
|
71
|
+
rb_ary_store(result, 1, rb_ary_new());
|
|
72
|
+
rb_ary_store(result, 2, iovs);
|
|
76
73
|
return result;
|
|
77
74
|
}
|
|
78
75
|
|
|
76
|
+
VALUE method_scheduler_epoll_deregister(VALUE self, VALUE io) {
|
|
77
|
+
ID id_fileno = rb_intern("fileno");
|
|
78
|
+
int epfd = NUM2INT(rb_iv_get(self, "@epfd"));
|
|
79
|
+
int fd = NUM2INT(rb_funcall(io, id_fileno, 0));
|
|
80
|
+
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); // Require Linux 2.6.9 for NULL event.
|
|
81
|
+
return Qnil;
|
|
82
|
+
}
|
|
83
|
+
|
|
79
84
|
VALUE method_scheduler_epoll_backend(VALUE klass) {
|
|
80
85
|
return rb_str_new_cstr("epoll");
|
|
81
86
|
}
|
data/ext/evt/evt.c
CHANGED
|
@@ -24,6 +24,7 @@ void Init_evt_ext()
|
|
|
24
24
|
rb_define_singleton_method(Bundled, "epoll_backend", method_scheduler_epoll_backend, 0);
|
|
25
25
|
rb_define_method(Bundled, "epoll_init_selector", method_scheduler_epoll_init, 0);
|
|
26
26
|
rb_define_method(Bundled, "epoll_register", method_scheduler_epoll_register, 2);
|
|
27
|
+
rb_define_method(Bundled, "epoll_deregister", method_scheduler_epoll_deregister, 1);
|
|
27
28
|
rb_define_method(Bundled, "epoll_wait", method_scheduler_epoll_wait, 0);
|
|
28
29
|
#endif
|
|
29
30
|
#if HAVE_SYS_EVENT_H
|
data/ext/evt/evt.h
CHANGED
|
@@ -45,6 +45,7 @@ void Init_evt_ext();
|
|
|
45
45
|
#if HAVE_SYS_EPOLL_H
|
|
46
46
|
VALUE method_scheduler_epoll_init(VALUE self);
|
|
47
47
|
VALUE method_scheduler_epoll_register(VALUE self, VALUE io, VALUE interest);
|
|
48
|
+
VALUE method_scheduler_epoll_deregister(VALUE self, VALUE io);
|
|
48
49
|
VALUE method_scheduler_epoll_wait(VALUE self);
|
|
49
50
|
VALUE method_scheduler_epoll_backend(VALUE klass);
|
|
50
51
|
#include <sys/epoll.h>
|
data/ext/evt/select.h
CHANGED
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
#include "evt.h"
|
|
4
4
|
|
|
5
5
|
VALUE method_scheduler_select_wait(VALUE self) {
|
|
6
|
-
// return IO.select(@readable.keys, @writable.keys, [], next_timeout)
|
|
7
|
-
VALUE readable, writable, readable_keys, writable_keys, next_timeout;
|
|
6
|
+
// return IO.select(@readable.keys, @writable.keys, [], next_timeout / 1000.0)
|
|
8
7
|
ID id_select = rb_intern("select");
|
|
9
8
|
ID id_next_timeout = rb_intern("next_timeout");
|
|
9
|
+
ID id_div = rb_intern("/");
|
|
10
|
+
ID id_to_f = rb_intern("to_f");
|
|
10
11
|
|
|
11
|
-
readable = rb_iv_get(self, "@readable");
|
|
12
|
-
writable = rb_iv_get(self, "@writable");
|
|
12
|
+
VALUE readable = rb_iv_get(self, "@readable");
|
|
13
|
+
VALUE writable = rb_iv_get(self, "@writable");
|
|
13
14
|
|
|
14
|
-
readable_keys = rb_funcall(readable, rb_intern("keys"), 0);
|
|
15
|
-
writable_keys = rb_funcall(writable, rb_intern("keys"), 0);
|
|
16
|
-
next_timeout = rb_funcall(self, id_next_timeout, 0);
|
|
15
|
+
VALUE readable_keys = rb_funcall(readable, rb_intern("keys"), 0);
|
|
16
|
+
VALUE writable_keys = rb_funcall(writable, rb_intern("keys"), 0);
|
|
17
|
+
VALUE next_timeout = rb_funcall(self, id_next_timeout, 0);
|
|
18
|
+
next_timeout = rb_funcall(next_timeout, id_to_f, 0);
|
|
19
|
+
VALUE secs = rb_funcall(next_timeout, id_div, 1, DBL2NUM(1000.0));
|
|
17
20
|
|
|
18
|
-
return rb_funcall(rb_cIO, id_select, 4, readable_keys, writable_keys, rb_ary_new(),
|
|
21
|
+
return rb_funcall(rb_cIO, id_select, 4, readable_keys, writable_keys, rb_ary_new(), secs);
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
VALUE method_scheduler_select_backend(VALUE klass) {
|
data/lib/evt/backends/bundled.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Evt::Bundled
|
|
4
|
-
MAXIMUM_TIMEOUT =
|
|
4
|
+
MAXIMUM_TIMEOUT = 5000
|
|
5
5
|
COLLECT_COUNTER_MAX = 16384
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
@@ -26,7 +26,7 @@ class Evt::Bundled
|
|
|
26
26
|
_fiber, timeout = @waiting.min_by{ |key, value| value }
|
|
27
27
|
|
|
28
28
|
if timeout
|
|
29
|
-
offset = timeout - current_time
|
|
29
|
+
offset = (timeout - current_time) * 1000 # Use mililisecond
|
|
30
30
|
return 0 if offset < 0
|
|
31
31
|
return offset if offset < MAXIMUM_TIMEOUT
|
|
32
32
|
end
|
|
@@ -101,6 +101,7 @@ class Evt::Bundled
|
|
|
101
101
|
@writable[io] = Fiber.current unless (events & IO::WRITABLE).zero?
|
|
102
102
|
self.register(io, events)
|
|
103
103
|
Fiber.yield
|
|
104
|
+
self.deregister(io)
|
|
104
105
|
true
|
|
105
106
|
end
|
|
106
107
|
|
data/lib/evt/backends/epoll.rb
CHANGED
|
@@ -17,6 +17,60 @@ class Evt::Epoll < Evt::Bundled
|
|
|
17
17
|
epoll_register(io, interest)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def deregister(io)
|
|
21
|
+
epoll_deregister(io)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def io_wait(io, events, duration)
|
|
25
|
+
@iovs[io] = Fiber.current
|
|
26
|
+
self.register(io, events)
|
|
27
|
+
Fiber.yield
|
|
28
|
+
self.deregister(io)
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# def io_read(io, buffer, offset, length)
|
|
33
|
+
# result = +('')
|
|
34
|
+
|
|
35
|
+
# self.register(io, IO::READABLE)
|
|
36
|
+
# direct_read = io.read_nonblock(length, exception: false)
|
|
37
|
+
|
|
38
|
+
# unless direct_read == :wait_readable
|
|
39
|
+
# result << direct_read
|
|
40
|
+
# end
|
|
41
|
+
|
|
42
|
+
# until result.length >= length or io.closed? or io.eof?
|
|
43
|
+
# @iovs[io] = Fiber.current
|
|
44
|
+
# Fiber.yield
|
|
45
|
+
# todo = length - result.length
|
|
46
|
+
# result << io.read_nonblock(todo)
|
|
47
|
+
# end
|
|
48
|
+
|
|
49
|
+
# self.deregister(io)
|
|
50
|
+
# buffer[offset...offset+result.length] = result
|
|
51
|
+
# result.length
|
|
52
|
+
# end
|
|
53
|
+
|
|
54
|
+
# def io_write(io, buffer, offset, length)
|
|
55
|
+
# finished = 0
|
|
56
|
+
|
|
57
|
+
# self.register(io, IO::WRITABLE)
|
|
58
|
+
# direct_write = io.write_nonblock(buffer.byteslice(offset+finished..-1), exception: false)
|
|
59
|
+
# unless direct_write == :wait_writable
|
|
60
|
+
# finished += direct_write
|
|
61
|
+
# end
|
|
62
|
+
|
|
63
|
+
# until finished >= length or io.closed?
|
|
64
|
+
# @iovs[io] = Fiber.current
|
|
65
|
+
# Fiber.yield
|
|
66
|
+
# finished += io.write_nonblock(buffer.byteslice(offset+finished..-1))
|
|
67
|
+
# end
|
|
68
|
+
|
|
69
|
+
# self.deregister(io)
|
|
70
|
+
|
|
71
|
+
# finished
|
|
72
|
+
# end
|
|
73
|
+
|
|
20
74
|
def wait
|
|
21
75
|
epoll_wait
|
|
22
76
|
end
|
data/lib/evt/backends/iocp.rb
CHANGED
data/lib/evt/backends/kqueue.rb
CHANGED
data/lib/evt/backends/select.rb
CHANGED
|
@@ -17,8 +17,14 @@ class Evt::Select < Evt::Bundled
|
|
|
17
17
|
# Select is stateless
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def deregister(io)
|
|
21
|
+
end
|
|
22
|
+
|
|
20
23
|
def wait
|
|
21
24
|
select_wait
|
|
25
|
+
rescue IOError => _
|
|
26
|
+
collect(true)
|
|
27
|
+
return [], []
|
|
22
28
|
rescue Errno::EBADF => _
|
|
23
29
|
collect(true)
|
|
24
30
|
return [], []
|
data/lib/evt/backends/uring.rb
CHANGED
|
@@ -17,13 +17,17 @@ class Evt::Uring < Evt::Bundled
|
|
|
17
17
|
uring_register(io, interest)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
uring_io_read(io, buffer, offset, length)
|
|
20
|
+
def deregister(io)
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
# Disable direct I/Os for now
|
|
24
|
+
# def io_read(io, buffer, offset, length)
|
|
25
|
+
# uring_io_read(io, buffer, offset, length)
|
|
26
|
+
# end
|
|
27
|
+
|
|
28
|
+
# def io_write(io, buffer, offset, length)
|
|
29
|
+
# uring_io_write(io, buffer, offset, length)
|
|
30
|
+
# end
|
|
27
31
|
|
|
28
32
|
def wait
|
|
29
33
|
uring_wait
|
data/lib/evt/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Delton Ding
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2020-12-
|
|
11
|
+
date: 2020-12-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake-compiler
|
|
@@ -100,14 +100,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
100
100
|
requirements:
|
|
101
101
|
- - ">="
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: 3.0.0
|
|
103
|
+
version: 3.0.0
|
|
104
104
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
105
|
requirements:
|
|
106
106
|
- - ">="
|
|
107
107
|
- !ruby/object:Gem::Version
|
|
108
108
|
version: '0'
|
|
109
109
|
requirements: []
|
|
110
|
-
rubygems_version: 3.2.
|
|
110
|
+
rubygems_version: 3.2.3
|
|
111
111
|
signing_key:
|
|
112
112
|
specification_version: 4
|
|
113
113
|
summary: The Event Library that designed for Ruby 3.0 Fiber Scheluer.
|