ever 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/Gemfile.lock +4 -2
- data/README.md +23 -2
- data/ever.gemspec +1 -0
- data/examples/http_server_nio4r_single_thread.rb +122 -0
- data/examples/http_server_nio4r_worker_threads.rb +165 -0
- data/examples/{http_server.rb → http_server_single_thread.rb} +1 -8
- data/examples/http_server_worker_threads.rb +133 -0
- data/examples/http_server_worker_threads_separate_queues.rb +139 -0
- data/ext/ever/ever.h +1 -9
- data/ext/ever/extconf.rb +1 -1
- data/ext/ever/loop.c +4 -6
- data/ext/ever/watcher.c +8 -4
- data/lib/ever/version.rb +1 -1
- data/test/run.rb +5 -0
- data/test/test_loop.rb +2 -2
- metadata +23 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f926fe6eb2d131d56a1efdc1ba290ce20d76da83f1647253b3b685ccbf49a446
|
4
|
+
data.tar.gz: 699c3ffcaf8c24bdfaac6d8e5e774f75473de4f3293a1e938621dd3cc4c1a2f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7faa3b7153799409100ca9a9427e20b26c38962e97f5fd480013880cef105387c4b4c10441b80676c4f31336f2235c15617335d7536e9b2db710ff72156ee1c4
|
7
|
+
data.tar.gz: af0b88f3a990fcf4e8f3a1d1f4e564b6746860162d786c84bb46d6673590f385f3bd4ad9d6ff3318d1fbcb26ca412e9a9becbca30c80e249408769592a835122
|
data/.github/FUNDING.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
github: ciconia
|
data/Gemfile.lock
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ever (0.
|
4
|
+
ever (0.2)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
9
|
http_parser.rb (0.7.0)
|
10
10
|
minitest (5.14.4)
|
11
|
+
nio4r (2.5.8)
|
11
12
|
rake (13.0.6)
|
12
13
|
rake-compiler (1.1.1)
|
13
14
|
rake
|
@@ -19,7 +20,8 @@ DEPENDENCIES
|
|
19
20
|
ever!
|
20
21
|
http_parser.rb (= 0.7.0)
|
21
22
|
minitest (= 5.14.4)
|
23
|
+
nio4r (= 2.5.8)
|
22
24
|
rake-compiler (= 1.1.1)
|
23
25
|
|
24
26
|
BUNDLED WITH
|
25
|
-
2.
|
27
|
+
2.3.7
|
data/README.md
CHANGED
@@ -13,6 +13,8 @@ Ever is a [libev](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod)-based ev
|
|
13
13
|
- Callback-less API for getting events
|
14
14
|
- Events for I/O readiness
|
15
15
|
- Events for one-shot or recurring timers
|
16
|
+
- Application-defined event semantic (an event can be identifed by any object)
|
17
|
+
- Easy API for breaking a blocking poll, no need for setting a timeout or writing to a pipe
|
16
18
|
- Cross-thread signalling and emitting of events
|
17
19
|
|
18
20
|
## Rationale
|
@@ -29,10 +31,12 @@ After coming up with a bunch of different ideas for how to achieve this, I settl
|
|
29
31
|
- When the request is complete, the worker threads continues to run the Rack app, gets the response, and tries to write the response. If the response cannot be written, the connection is watched for write readiness.
|
30
32
|
- When the response has been written, the connection is watched again for read readiness in preparation for the next request.
|
31
33
|
|
32
|
-
(A working sketch for this design is included [here as an example](https://github.com/digital-fabric/ever/blob/main/examples/
|
34
|
+
(A working sketch for this design is included [here as an example](https://github.com/digital-fabric/ever/blob/main/examples/http_server_worker_threads.rb).)
|
33
35
|
|
34
36
|
What's interesting about this design is that any number of worker threads can (theoretically) handle any number of concurrent requests, since each worker thread is not tied to a specific connection, but rather work on each connection in the queue as it becomes ready (for reading or writing).
|
35
37
|
|
38
|
+
**Update**: actually I have seen performance degrade when adding more worker threads (also see the section below on [performance](#performance)). Switching to a [single-threaded design](https://github.com/digital-fabric/ever/blob/main/examples/http_server_single_thread.rb) improves throughput by ~25%!
|
39
|
+
|
36
40
|
## Installing
|
37
41
|
|
38
42
|
If you're using bundler just add it to your `Gemfile`:
|
@@ -159,7 +163,24 @@ evloop.each { |key| handle_event(key) }
|
|
159
163
|
|
160
164
|
## Performance
|
161
165
|
|
162
|
-
I did not yet explore all the performance implications of this new design, but [a sketch I made for an HTTP server](https://github.com/digital-fabric/ever/blob/main/examples/
|
166
|
+
I did not yet explore all the performance implications of this new design, but [a sketch I made for an HTTP server](https://github.com/digital-fabric/ever/blob/main/examples/http_server_worker_threads.rb) shows it performing consistently at >60,000 reqs/seconds on my development machine, with a single worker thread.
|
167
|
+
|
168
|
+
However, adding more worker threads actually degrades the performance. This is both due to the cost associated with the [GVL](https://www.speedshop.co/2020/05/11/the-ruby-gvl-and-scaling.html), and contention for the job queue.
|
169
|
+
|
170
|
+
A [slightly modified HTTP server script](https://github.com/digital-fabric/ever/blob/main/examples/http_server_worker_threads_separate_queues.rb), with a separate job queue for each thread, does not fare much better. A [separate design with no worker thread](https://github.com/digital-fabric/ever/blob/main/examples/http_server_single_thread.rb) (everything happens on the main thread), yields a throughput of ~75,000 reqs/seconds on the same machine, about ~25% better than the version with worker threads. Of course, when running a Rack app, having everything happen on the main thread means there's no concurrent handling of requests within a single process. Here are some indicative results, along with an equivalent implementation using [nio4r](https://github.com/socketry/nio4r/):
|
171
|
+
|
172
|
+
|Script|`-t2 -c10`|`-t4 -c64`|`-t8 -c256`|`-t8 -c1024`|
|
173
|
+
|------|---------:|---------:|----------:|-----------:|
|
174
|
+
|[nio4r single thread](https://github.com/digital-fabric/ever/blob/main/examples/http_server_nio4r_single_thread.rb)|63775 (4.27ms)|58420 (28.80ms)|51302 (584.78ms)|47294 (1.21s)|
|
175
|
+
|[nio4r 1 worker threads](https://github.com/digital-fabric/ever/blob/main/examples/http_server_nio4r.rb)|25276 (89.69ms)|13458 (394.90ms)|6559 (1.83s)|2660 (1.99s)|
|
176
|
+
|[nio4r 4 worker threads](https://github.com/digital-fabric/ever/blob/main/examples/http_server_nio4r.rb)|23785 (104.84ms)|12826 (439.89ms)|6471 (1.08s)|2660 (2.00s)|
|
177
|
+
|[nio4r 8 worker threads](https://github.com/digital-fabric/ever/blob/main/examples/http_server_nio4r.rb)|24028 (106.38ms)|12825 (429.98ms)|6409 (956.85ms)|2600 (2.00s)|
|
178
|
+
|[ever single thread](https://github.com/digital-fabric/ever/blob/main/examples/http_server_single_thread.rb)|77994 (3.91ms)|75271 (32.32ms)|65799 (443.88ms)|56425 (1.99s)|
|
179
|
+
|[ever 1 worker thread](https://github.com/digital-fabric/ever/blob/main/examples/http_server_nio4r.rb)|64475 (4.05ms)|69883 (31.68ms)|61917 (327.95ms)|52975 (4.56s)|
|
180
|
+
|[ever 4 worker threads](https://github.com/digital-fabric/ever/blob/main/examples/http_server_nio4r.rb)|48763 (4.15ms)|60829 (26.15ms)|56906 (482.95ms)|54713 (6.03s)|
|
181
|
+
|[ever 8 worker threads](https://github.com/digital-fabric/ever/blob/main/examples/http_server_nio4r.rb)|41378 (4.18ms)|55959 (36.51ms)|
|
182
|
+
|[ever 4 worker threads, separate queues](https://github.com/digital-fabric/ever/blob/main/examples/http_server_nio4r.rb)|
|
183
|
+
|[ever 8 worker threads, separate queues](https://github.com/digital-fabric/ever/blob/main/examples/http_server_nio4r.rb)|
|
163
184
|
|
164
185
|
## Contributing
|
165
186
|
|
data/ever.gemspec
CHANGED
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'nio'
|
5
|
+
require 'http/parser'
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
class Connection
|
9
|
+
attr_reader :io, :parser, :request_complete,
|
10
|
+
:request_headers, :request_body
|
11
|
+
attr_accessor :response, :monitor
|
12
|
+
|
13
|
+
def initialize(io)
|
14
|
+
@io = io
|
15
|
+
@parser = Http::Parser.new(self)
|
16
|
+
setup_read_request
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_read_request
|
20
|
+
@request_complete = nil
|
21
|
+
@request_headers = nil
|
22
|
+
@request_body = +''
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_headers_complete(headers)
|
26
|
+
@request_headers = headers
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_body(chunk)
|
30
|
+
@request_body << chunk
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_message_complete
|
34
|
+
@request_complete = true
|
35
|
+
end
|
36
|
+
|
37
|
+
def monitor(event)
|
38
|
+
@monitor&.close
|
39
|
+
@monitor = $selector.register(@io, event)
|
40
|
+
@monitor.value = self
|
41
|
+
end
|
42
|
+
|
43
|
+
def close
|
44
|
+
@monitor&.close
|
45
|
+
@io.close
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_connection(conn)
|
50
|
+
if !conn.request_complete
|
51
|
+
handle_read_request(conn)
|
52
|
+
else
|
53
|
+
handle_write_response(conn)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_read_request(conn)
|
58
|
+
result = conn.io.read_nonblock(16384, exception: false)
|
59
|
+
case result
|
60
|
+
when :wait_readable
|
61
|
+
conn.monitor(:r)
|
62
|
+
when :wait_writable
|
63
|
+
conn.monitor(:w)
|
64
|
+
when nil
|
65
|
+
conn.close
|
66
|
+
else
|
67
|
+
conn.parser << result
|
68
|
+
if conn.request_complete
|
69
|
+
conn.response = handle_request(conn.request_headers, conn.request_body)
|
70
|
+
handle_write_response(conn)
|
71
|
+
else
|
72
|
+
conn.monitor(:r)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
rescue HTTP::Parser::Error, SystemCallError, IOError
|
76
|
+
conn.close
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_request(headers, body)
|
80
|
+
response_body = "Hello, world!"
|
81
|
+
"HTTP/1.1 200 OK\nContent-Length: #{response_body.bytesize}\n\n#{response_body}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_write_response(conn)
|
85
|
+
result = conn.io.write_nonblock(conn.response, exception: false)
|
86
|
+
case result
|
87
|
+
when :wait_readable
|
88
|
+
conn.monitor(:r)
|
89
|
+
when :wait_writable
|
90
|
+
conn.monitor(:w)
|
91
|
+
when nil
|
92
|
+
conn.close
|
93
|
+
else
|
94
|
+
conn.setup_read_request
|
95
|
+
conn.monitor(:r)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def setup_connection(io)
|
100
|
+
conn = Connection.new(io)
|
101
|
+
conn.monitor(:r)
|
102
|
+
end
|
103
|
+
|
104
|
+
server = TCPServer.new('0.0.0.0', 1234)
|
105
|
+
puts "Listening on port 1234..."
|
106
|
+
trap('SIGINT') { exit! }
|
107
|
+
|
108
|
+
$selector = NIO::Selector.new
|
109
|
+
|
110
|
+
$selector.register(server, :r)
|
111
|
+
|
112
|
+
loop do
|
113
|
+
$selector.select do |monitor|
|
114
|
+
case monitor.io
|
115
|
+
when server
|
116
|
+
socket = server.accept
|
117
|
+
setup_connection(socket)
|
118
|
+
else
|
119
|
+
handle_connection(monitor.value)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'nio'
|
5
|
+
require 'http/parser'
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
class Connection
|
9
|
+
attr_reader :io, :parser, :request_complete,
|
10
|
+
:request_headers, :request_body
|
11
|
+
attr_accessor :response, :monitor
|
12
|
+
|
13
|
+
def initialize(io)
|
14
|
+
@io = io
|
15
|
+
@parser = Http::Parser.new(self)
|
16
|
+
setup_read_request
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_read_request
|
20
|
+
@request_complete = nil
|
21
|
+
@request_headers = nil
|
22
|
+
@request_body = +''
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_headers_complete(headers)
|
26
|
+
@request_headers = headers
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_body(chunk)
|
30
|
+
@request_body << chunk
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_message_complete
|
34
|
+
@request_complete = true
|
35
|
+
end
|
36
|
+
|
37
|
+
def emit_monitor(event)
|
38
|
+
$directive_queue << [:monitor, self, event]
|
39
|
+
$o << '.'
|
40
|
+
end
|
41
|
+
|
42
|
+
def emit_close
|
43
|
+
$directive_queue << [:close, self]
|
44
|
+
$o << '.'
|
45
|
+
end
|
46
|
+
|
47
|
+
def monitor(event)
|
48
|
+
@monitor&.close
|
49
|
+
@monitor = $selector.register(@io, event)
|
50
|
+
@monitor.value = self
|
51
|
+
rescue IOError
|
52
|
+
@monitor = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def close
|
56
|
+
@monitor&.close
|
57
|
+
@io.close
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def handle_connection(conn)
|
62
|
+
if !conn.request_complete
|
63
|
+
handle_read_request(conn)
|
64
|
+
else
|
65
|
+
handle_write_response(conn)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def handle_read_request(conn)
|
70
|
+
result = conn.io.read_nonblock(16384, exception: false)
|
71
|
+
case result
|
72
|
+
when :wait_readable
|
73
|
+
conn.emit_monitor(:r)
|
74
|
+
when :wait_writable
|
75
|
+
conn.emit_monitor(:w)
|
76
|
+
when nil
|
77
|
+
conn.emit_close
|
78
|
+
else
|
79
|
+
conn.parser << result
|
80
|
+
if conn.request_complete
|
81
|
+
conn.response = handle_request(conn.request_headers, conn.request_body)
|
82
|
+
handle_write_response(conn)
|
83
|
+
else
|
84
|
+
conn.emit_monitor(:r)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
rescue HTTP::Parser::Error, SystemCallError, IOError
|
88
|
+
conn.emit_close
|
89
|
+
end
|
90
|
+
|
91
|
+
def handle_request(headers, body)
|
92
|
+
response_body = "Hello, world!"
|
93
|
+
"HTTP/1.1 200 OK\nContent-Length: #{response_body.bytesize}\n\n#{response_body}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def handle_write_response(conn)
|
97
|
+
result = conn.io.write_nonblock(conn.response, exception: false)
|
98
|
+
case result
|
99
|
+
when :wait_readable
|
100
|
+
conn.emit_monitor(:r)
|
101
|
+
when :wait_writable
|
102
|
+
conn.emit_monitor(:w)
|
103
|
+
when nil
|
104
|
+
conn.emit_close
|
105
|
+
else
|
106
|
+
conn.setup_read_request
|
107
|
+
conn.emit_monitor(:r)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def setup_connection(io)
|
112
|
+
# happens in the main thread
|
113
|
+
conn = Connection.new(io)
|
114
|
+
conn.monitor(:r)
|
115
|
+
end
|
116
|
+
|
117
|
+
num_workers = ARGV[0] ? ARGV[0].to_i : 1
|
118
|
+
if num_workers < 1
|
119
|
+
puts "Invalid number of worker threads: #{ARGV[0].inspect}"
|
120
|
+
exit!
|
121
|
+
end
|
122
|
+
|
123
|
+
server = TCPServer.new('0.0.0.0', 1234)
|
124
|
+
puts "Listening on port 1234..."
|
125
|
+
trap('SIGINT') { exit! }
|
126
|
+
|
127
|
+
$selector = NIO::Selector.new
|
128
|
+
$i, $o = IO.pipe
|
129
|
+
$selector.register(server, :r)
|
130
|
+
$selector.register($i, :r)
|
131
|
+
|
132
|
+
$job_queue = Queue.new
|
133
|
+
$directive_queue = Queue.new
|
134
|
+
|
135
|
+
puts "Starting #{num_workers} worker threads..."
|
136
|
+
num_workers.times do
|
137
|
+
Thread.new do
|
138
|
+
while (job = $job_queue.shift)
|
139
|
+
handle_connection(job)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
loop do
|
145
|
+
$selector.select do |monitor|
|
146
|
+
case monitor.io
|
147
|
+
when server
|
148
|
+
socket = server.accept
|
149
|
+
setup_connection(socket)
|
150
|
+
when $i
|
151
|
+
$i.read(1) # flush pipe
|
152
|
+
while !$directive_queue.empty?
|
153
|
+
k, c, e = $directive_queue.shift
|
154
|
+
case k
|
155
|
+
when :monitor
|
156
|
+
c.monitor(e)
|
157
|
+
when :close
|
158
|
+
c.close
|
159
|
+
end
|
160
|
+
end
|
161
|
+
else
|
162
|
+
$job_queue << monitor.value
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -35,15 +35,8 @@ class Connection
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
-
$job_queue = Queue.new
|
39
38
|
$evloop = Ever::Loop.new
|
40
39
|
|
41
|
-
worker = Thread.new do
|
42
|
-
while (job = $job_queue.shift)
|
43
|
-
handle_connection(job)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
40
|
def handle_connection(conn)
|
48
41
|
if !conn.request_complete
|
49
42
|
handle_read_request(conn)
|
@@ -110,7 +103,7 @@ $evloop.each do |event|
|
|
110
103
|
socket = server.accept
|
111
104
|
setup_connection(socket)
|
112
105
|
when Connection
|
113
|
-
|
106
|
+
handle_connection(event)
|
114
107
|
when Array
|
115
108
|
cmd = event[0]
|
116
109
|
case cmd
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'ever'
|
5
|
+
require 'http/parser'
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
class Connection
|
9
|
+
attr_reader :io, :parser, :request_complete,
|
10
|
+
:request_headers, :request_body
|
11
|
+
attr_accessor :response
|
12
|
+
|
13
|
+
def initialize(io)
|
14
|
+
@io = io
|
15
|
+
@parser = Http::Parser.new(self)
|
16
|
+
setup_read_request
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_read_request
|
20
|
+
@request_complete = nil
|
21
|
+
@request_headers = nil
|
22
|
+
@request_body = +''
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_headers_complete(headers)
|
26
|
+
@request_headers = headers
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_body(chunk)
|
30
|
+
@request_body << chunk
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_message_complete
|
34
|
+
@request_complete = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
$job_queue = Queue.new
|
39
|
+
$evloop = Ever::Loop.new
|
40
|
+
|
41
|
+
def handle_connection(conn)
|
42
|
+
if !conn.request_complete
|
43
|
+
handle_read_request(conn)
|
44
|
+
else
|
45
|
+
handle_write_response(conn)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_read_request(conn)
|
50
|
+
result = conn.io.read_nonblock(16384, exception: false)
|
51
|
+
case result
|
52
|
+
when :wait_readable
|
53
|
+
$evloop.emit([:watch_io, conn, false, true])
|
54
|
+
when :wait_writable
|
55
|
+
$evloop.emit([:watch_io, conn, true, true])
|
56
|
+
when nil
|
57
|
+
$evloop.emit([:close, conn])
|
58
|
+
else
|
59
|
+
conn.parser << result
|
60
|
+
if conn.request_complete
|
61
|
+
conn.response = handle_request(conn.request_headers, conn.request_body)
|
62
|
+
handle_write_response(conn)
|
63
|
+
else
|
64
|
+
$evloop.emit([:watch_io, conn, false, true])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
rescue HTTP::Parser::Error, SystemCallError, IOError
|
68
|
+
$evloop.emit([:close, conn])
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_request(headers, body)
|
72
|
+
response_body = "Hello, world!"
|
73
|
+
"HTTP/1.1 200 OK\nContent-Length: #{response_body.bytesize}\n\n#{response_body}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_write_response(conn)
|
77
|
+
result = conn.io.write_nonblock(conn.response, exception: false)
|
78
|
+
case result
|
79
|
+
when :wait_readable
|
80
|
+
$evloop.emit([:watch_io, conn, false, true])
|
81
|
+
when :wait_writable
|
82
|
+
$evloop.emit([:watch_io, conn, true, true])
|
83
|
+
when nil
|
84
|
+
$evloop.emit([:close, conn])
|
85
|
+
else
|
86
|
+
conn.setup_read_request
|
87
|
+
$evloop.emit([:watch_io, conn, false, true])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def setup_connection(io)
|
92
|
+
conn = Connection.new(io)
|
93
|
+
$evloop.emit([:watch_io, conn, false, true])
|
94
|
+
end
|
95
|
+
|
96
|
+
num_workers = ARGV[0] ? ARGV[0].to_i : 1
|
97
|
+
if num_workers < 1
|
98
|
+
puts "Invalid number of worker threads: #{ARGV[0].inspect}"
|
99
|
+
exit!
|
100
|
+
end
|
101
|
+
|
102
|
+
server = TCPServer.new('0.0.0.0', 1234)
|
103
|
+
puts "Listening on port 1234..."
|
104
|
+
trap('SIGINT') { $evloop.stop }
|
105
|
+
$evloop.watch_io(:accept, server, false, false)
|
106
|
+
|
107
|
+
puts "Starting #{num_workers} worker threads..."
|
108
|
+
num_workers.times do
|
109
|
+
Thread.new do
|
110
|
+
while (job = $job_queue.shift)
|
111
|
+
handle_connection(job)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
$evloop.each do |event|
|
117
|
+
case event
|
118
|
+
when :accept
|
119
|
+
socket = server.accept
|
120
|
+
setup_connection(socket)
|
121
|
+
when Connection
|
122
|
+
$job_queue << event
|
123
|
+
when Array
|
124
|
+
cmd = event[0]
|
125
|
+
case cmd
|
126
|
+
when :watch_io
|
127
|
+
$evloop.watch_io(event[1], event[1].io, event[2], event[3])
|
128
|
+
when :close
|
129
|
+
conn = event[1]
|
130
|
+
conn.io.close
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'ever'
|
5
|
+
require 'http/parser'
|
6
|
+
require 'socket'
|
7
|
+
|
8
|
+
class Connection
|
9
|
+
attr_reader :io, :parser, :request_complete,
|
10
|
+
:request_headers, :request_body
|
11
|
+
attr_accessor :response
|
12
|
+
|
13
|
+
def initialize(io)
|
14
|
+
@io = io
|
15
|
+
@parser = Http::Parser.new(self)
|
16
|
+
setup_read_request
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_read_request
|
20
|
+
@request_complete = nil
|
21
|
+
@request_headers = nil
|
22
|
+
@request_body = +''
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_headers_complete(headers)
|
26
|
+
@request_headers = headers
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_body(chunk)
|
30
|
+
@request_body << chunk
|
31
|
+
end
|
32
|
+
|
33
|
+
def on_message_complete
|
34
|
+
@request_complete = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
$evloop = Ever::Loop.new
|
39
|
+
|
40
|
+
def handle_connection(conn)
|
41
|
+
if !conn.request_complete
|
42
|
+
handle_read_request(conn)
|
43
|
+
else
|
44
|
+
handle_write_response(conn)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_read_request(conn)
|
49
|
+
result = conn.io.read_nonblock(16384, exception: false)
|
50
|
+
case result
|
51
|
+
when :wait_readable
|
52
|
+
$evloop.emit([:watch_io, conn, false, true])
|
53
|
+
when :wait_writable
|
54
|
+
$evloop.emit([:watch_io, conn, true, true])
|
55
|
+
when nil
|
56
|
+
$evloop.emit([:close, conn])
|
57
|
+
else
|
58
|
+
conn.parser << result
|
59
|
+
if conn.request_complete
|
60
|
+
conn.response = handle_request(conn.request_headers, conn.request_body)
|
61
|
+
handle_write_response(conn)
|
62
|
+
else
|
63
|
+
$evloop.emit([:watch_io, conn, false, true])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
rescue HTTP::Parser::Error, SystemCallError, IOError
|
67
|
+
$evloop.emit([:close, conn])
|
68
|
+
end
|
69
|
+
|
70
|
+
def handle_request(headers, body)
|
71
|
+
response_body = "Hello, world!"
|
72
|
+
"HTTP/1.1 200 OK\nContent-Length: #{response_body.bytesize}\n\n#{response_body}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_write_response(conn)
|
76
|
+
result = conn.io.write_nonblock(conn.response, exception: false)
|
77
|
+
case result
|
78
|
+
when :wait_readable
|
79
|
+
$evloop.emit([:watch_io, conn, false, true])
|
80
|
+
when :wait_writable
|
81
|
+
$evloop.emit([:watch_io, conn, true, true])
|
82
|
+
when nil
|
83
|
+
$evloop.emit([:close, conn])
|
84
|
+
else
|
85
|
+
conn.setup_read_request
|
86
|
+
$evloop.emit([:watch_io, conn, false, true])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def setup_connection(io)
|
91
|
+
conn = Connection.new(io)
|
92
|
+
$evloop.emit([:watch_io, conn, false, true])
|
93
|
+
end
|
94
|
+
|
95
|
+
num_workers = ARGV[0] ? ARGV[0].to_i : 1
|
96
|
+
if num_workers < 1
|
97
|
+
puts "Invalid number of worker threads: #{ARGV[0].inspect}"
|
98
|
+
exit!
|
99
|
+
end
|
100
|
+
|
101
|
+
server = TCPServer.new('0.0.0.0', 1234)
|
102
|
+
puts "Listening on port 1234..."
|
103
|
+
trap('SIGINT') { $evloop.stop }
|
104
|
+
$evloop.watch_io(:accept, server, false, false)
|
105
|
+
|
106
|
+
puts "Starting #{num_workers} worker threads..."
|
107
|
+
|
108
|
+
$queues = []
|
109
|
+
def start_worker_thread
|
110
|
+
queue = Queue.new
|
111
|
+
$queues << queue
|
112
|
+
Thread.new do
|
113
|
+
while (job = queue.shift)
|
114
|
+
handle_connection(job)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
num_workers.times { start_worker_thread }
|
120
|
+
|
121
|
+
$evloop.each do |event|
|
122
|
+
case event
|
123
|
+
when :accept
|
124
|
+
socket = server.accept
|
125
|
+
setup_connection(socket)
|
126
|
+
when Connection
|
127
|
+
queue = $queues.sample
|
128
|
+
queue << event
|
129
|
+
when Array
|
130
|
+
cmd = event[0]
|
131
|
+
case cmd
|
132
|
+
when :watch_io
|
133
|
+
$evloop.watch_io(event[1], event[1].io, event[2], event[3])
|
134
|
+
when :close
|
135
|
+
conn = event[1]
|
136
|
+
conn.io.close
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/ext/ever/ever.h
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
#ifndef EVER_H
|
2
2
|
#define EVER_H
|
3
3
|
|
4
|
-
#include <execinfo.h>
|
5
4
|
#include "ruby.h"
|
6
5
|
#include "../libev/ev.h"
|
7
6
|
|
@@ -9,13 +8,6 @@
|
|
9
8
|
#define OBJ_ID(obj) (NUM2LONG(rb_funcall(obj, rb_intern("object_id"), 0)))
|
10
9
|
#define INSPECT(str, obj) { printf(str); VALUE s = rb_funcall(obj, rb_intern("inspect"), 0); printf(": %s\n", StringValueCStr(s)); }
|
11
10
|
#define TRACE_CALLER() { VALUE c = rb_funcall(rb_mKernel, rb_intern("caller"), 0); INSPECT("caller: ", c); }
|
12
|
-
#define TRACE_C_STACK() { \
|
13
|
-
void *entries[10]; \
|
14
|
-
size_t size = backtrace(entries, 10); \
|
15
|
-
char **strings = backtrace_symbols(entries, size); \
|
16
|
-
for (unsigned long i = 0; i < size; i++) printf("%s\n", strings[i]); \
|
17
|
-
free(strings); \
|
18
|
-
}
|
19
11
|
|
20
12
|
// exceptions
|
21
13
|
#define TEST_EXCEPTION(ret) (rb_obj_is_kind_of(ret, rb_eException) == Qtrue)
|
@@ -34,7 +26,7 @@ typedef struct Loop_t {
|
|
34
26
|
VALUE active_watchers;
|
35
27
|
VALUE free_watchers;
|
36
28
|
VALUE queued_events;
|
37
|
-
|
29
|
+
|
38
30
|
int stop;
|
39
31
|
int in_ev_loop;
|
40
32
|
} Loop_t;
|
data/ext/ever/extconf.rb
CHANGED
@@ -9,7 +9,7 @@ $defs << '-DEV_USE_POLL' if have_type('port_event_t', 'poll.h')
|
|
9
9
|
$defs << '-DEV_USE_EPOLL' if have_header('sys/epoll.h')
|
10
10
|
$defs << '-DEV_USE_KQUEUE' if have_header('sys/event.h') && have_header('sys/queue.h')
|
11
11
|
$defs << '-DEV_USE_PORT' if have_type('port_event_t', 'port.h')
|
12
|
-
$defs << '-DHAVE_SYS_RESOURCE_H' if have_header('sys/resource.h')
|
12
|
+
$defs << '-DHAVE_SYS_RESOURCE_H' if have_header('sys/resource.h')
|
13
13
|
|
14
14
|
$CFLAGS << " -Wno-comment"
|
15
15
|
$CFLAGS << " -Wno-unused-result"
|
data/ext/ever/loop.c
CHANGED
@@ -65,9 +65,7 @@ static inline int sym_to_events(VALUE rw) {
|
|
65
65
|
|
66
66
|
static inline int fd_from_io(VALUE io) {
|
67
67
|
rb_io_t *fptr;
|
68
|
-
|
69
|
-
if (underlying_io != Qnil) io = underlying_io;
|
70
|
-
GetOpenFile(io, fptr);
|
68
|
+
GetOpenFile(rb_convert_type(io, T_FILE, "IO", "to_io"), fptr);
|
71
69
|
return fptr->fd;
|
72
70
|
}
|
73
71
|
|
@@ -195,7 +193,7 @@ VALUE Loop_watch_fd(VALUE self, VALUE key, VALUE fd, VALUE rw, VALUE oneshot) {
|
|
195
193
|
if (rb_hash_aref(loop->active_watchers, key) != Qnil)
|
196
194
|
rb_raise(rb_eRuntimeError, "Duplicate event key detected, event key must be unique");
|
197
195
|
|
198
|
-
loop_watch_fd(loop, key,
|
196
|
+
loop_watch_fd(loop, key, FIX2INT(fd), sym_to_events(rw), RTEST(oneshot));
|
199
197
|
return self;
|
200
198
|
}
|
201
199
|
|
@@ -218,7 +216,7 @@ VALUE Loop_watch_timer(VALUE self, VALUE key, VALUE timeout, VALUE interval) {
|
|
218
216
|
rb_raise(rb_eRuntimeError, "Duplicate event key detected, event key must be unique");
|
219
217
|
|
220
218
|
loop_watch_timer(loop, key, NUM2DBL(timeout), NUM2DBL(interval));
|
221
|
-
return self;
|
219
|
+
return self;
|
222
220
|
}
|
223
221
|
|
224
222
|
VALUE Loop_unwatch(VALUE self, VALUE key) {
|
@@ -241,7 +239,7 @@ void Init_Loop() {
|
|
241
239
|
rb_define_alloc_func(cLoop, Loop_allocate);
|
242
240
|
|
243
241
|
rb_define_method(cLoop, "initialize", Loop_initialize, 0);
|
244
|
-
|
242
|
+
|
245
243
|
rb_define_method(cLoop, "each", Loop_each, 0);
|
246
244
|
rb_define_method(cLoop, "next_event", Loop_next_event, 0);
|
247
245
|
rb_define_method(cLoop, "emit", Loop_emit, 1);
|
data/ext/ever/watcher.c
CHANGED
@@ -51,6 +51,10 @@ static VALUE Watcher_allocate(VALUE klass) {
|
|
51
51
|
TypedData_Get_Struct((obj), Watcher_t, &Watcher_type, (watcher))
|
52
52
|
|
53
53
|
VALUE Watcher_initialize(VALUE self) {
|
54
|
+
Watcher_t *watcher;
|
55
|
+
GetWatcher(self, watcher);
|
56
|
+
|
57
|
+
watcher->key = Qnil;
|
54
58
|
return self;
|
55
59
|
}
|
56
60
|
|
@@ -60,11 +64,11 @@ inline void watcher_stop(Watcher_t *watcher) {
|
|
60
64
|
switch (watcher->type) {
|
61
65
|
case WATCHER_IO:
|
62
66
|
ev_io_stop(watcher->loop->ev_loop, &watcher->io);
|
63
|
-
|
67
|
+
break;
|
64
68
|
case WATCHER_TIMER:
|
65
69
|
ev_timer_stop(watcher->loop->ev_loop, &watcher->timer);
|
66
|
-
return;
|
67
70
|
}
|
71
|
+
watcher->key = Qnil;
|
68
72
|
}
|
69
73
|
|
70
74
|
void watcher_io_callback(EV_P_ ev_io *w, int revents)
|
@@ -72,8 +76,8 @@ void watcher_io_callback(EV_P_ ev_io *w, int revents)
|
|
72
76
|
Watcher_t *watcher = (Watcher_t *)w;
|
73
77
|
loop_emit(watcher->loop, watcher->key);
|
74
78
|
if (watcher->oneshot) {
|
75
|
-
watcher_stop(watcher);
|
76
79
|
loop_release_watcher(watcher->loop, watcher->key);
|
80
|
+
watcher_stop(watcher);
|
77
81
|
}
|
78
82
|
}
|
79
83
|
|
@@ -82,8 +86,8 @@ void watcher_timer_callback(EV_P_ ev_timer *w, int revents)
|
|
82
86
|
Watcher_t *watcher = (Watcher_t *)w;
|
83
87
|
loop_emit(watcher->loop, watcher->key);
|
84
88
|
if (watcher->oneshot) {
|
85
|
-
watcher_stop(watcher);
|
86
89
|
loop_release_watcher(watcher->loop, watcher->key);
|
90
|
+
watcher_stop(watcher);
|
87
91
|
}
|
88
92
|
}
|
89
93
|
|
data/lib/ever/version.rb
CHANGED
data/test/run.rb
CHANGED
data/test/test_loop.rb
CHANGED
@@ -23,7 +23,7 @@ class LoopTest < MiniTest::Test
|
|
23
23
|
|
24
24
|
def test_io
|
25
25
|
i, o = IO.pipe
|
26
|
-
|
26
|
+
|
27
27
|
@loop.watch_io('foo', i, false, true)
|
28
28
|
|
29
29
|
o << 'foo'
|
@@ -50,7 +50,7 @@ class LoopTest < MiniTest::Test
|
|
50
50
|
i, o = IO.pipe
|
51
51
|
@loop.watch_io('foo', i, false, true)
|
52
52
|
|
53
|
-
Thread.new {
|
53
|
+
Thread.new { @loop.emit('baz'); o << 'bar' }
|
54
54
|
|
55
55
|
event = @loop.next_event
|
56
56
|
assert_equal 'baz', event
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ever
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - '='
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.7.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: nio4r
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.5.8
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.5.8
|
55
69
|
description:
|
56
70
|
email: sharon@noteflakes.com
|
57
71
|
executables: []
|
@@ -60,6 +74,7 @@ extensions:
|
|
60
74
|
extra_rdoc_files:
|
61
75
|
- README.md
|
62
76
|
files:
|
77
|
+
- ".github/FUNDING.yml"
|
63
78
|
- ".github/workflows/test.yml"
|
64
79
|
- ".gitignore"
|
65
80
|
- CHANGELOG.md
|
@@ -69,7 +84,11 @@ files:
|
|
69
84
|
- README.md
|
70
85
|
- Rakefile
|
71
86
|
- ever.gemspec
|
72
|
-
- examples/
|
87
|
+
- examples/http_server_nio4r_single_thread.rb
|
88
|
+
- examples/http_server_nio4r_worker_threads.rb
|
89
|
+
- examples/http_server_single_thread.rb
|
90
|
+
- examples/http_server_worker_threads.rb
|
91
|
+
- examples/http_server_worker_threads_separate_queues.rb
|
73
92
|
- ext/ever/ever.h
|
74
93
|
- ext/ever/ever_ext.c
|
75
94
|
- ext/ever/extconf.rb
|
@@ -124,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
143
|
- !ruby/object:Gem::Version
|
125
144
|
version: '0'
|
126
145
|
requirements: []
|
127
|
-
rubygems_version: 3.
|
146
|
+
rubygems_version: 3.3.7
|
128
147
|
signing_key:
|
129
148
|
specification_version: 4
|
130
149
|
summary: Callback-less event reactor for Ruby
|