ever 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ba6404ba736a8c3c519cba46a8bf8a9df85ecc4a9532790c15821bff12f348e
4
- data.tar.gz: 162398e3f59e7a638cf7de58b47e0a05c746f4be49e46f62bbd23b60305a1f77
3
+ metadata.gz: f926fe6eb2d131d56a1efdc1ba290ce20d76da83f1647253b3b685ccbf49a446
4
+ data.tar.gz: 699c3ffcaf8c24bdfaac6d8e5e774f75473de4f3293a1e938621dd3cc4c1a2f2
5
5
  SHA512:
6
- metadata.gz: a3e8236c119bdb97b0c5aa0b26f60cbc40f740201218de11494b2368e0c60e7ab3dc4b0235751b903483dc441fcc55f07bc9da61e6c06e5a26135656e909cfc8
7
- data.tar.gz: aeef774232228911357f7c472a937695f319615d31f0bf11d93554177c3b184a32003b4489a5193ca803d90dbb67cfda5a64417e6c6045be11f73bc98d341b21
6
+ metadata.gz: 7faa3b7153799409100ca9a9427e20b26c38962e97f5fd480013880cef105387c4b4c10441b80676c4f31336f2235c15617335d7536e9b2db710ff72156ee1c4
7
+ data.tar.gz: af0b88f3a990fcf4e8f3a1d1f4e564b6746860162d786c84bb46d6673590f385f3bd4ad9d6ff3318d1fbcb26ca412e9a9becbca30c80e249408769592a835122
@@ -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.1)
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.2.26
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/http_server.rb).)
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/http_server.rb) shows it performing consistently at >60000 reqs/seconds on my development machine.
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
@@ -23,4 +23,5 @@ Gem::Specification.new do |s|
23
23
  s.add_development_dependency 'rake-compiler', '1.1.1'
24
24
  s.add_development_dependency 'minitest', '5.14.4'
25
25
  s.add_development_dependency 'http_parser.rb', '0.7.0'
26
+ s.add_development_dependency 'nio4r', '2.5.8'
26
27
  end
@@ -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
- $job_queue << event
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
- VALUE underlying_io = rb_ivar_get(io, ID_ivar_io);
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, NUM2INT(fd), sym_to_events(rw), RTEST(oneshot));
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
- return;
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ever
4
- VERSION = '0.1'
4
+ VERSION = '0.2'
5
5
  end
data/test/run.rb CHANGED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{__dir__}/test_*.rb").each do |path|
4
+ require(path)
5
+ end
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 { o << 'bar'; @loop.emit('baz') }
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.1'
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: 2021-08-30 00:00:00.000000000 Z
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/http_server.rb
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.1.4
146
+ rubygems_version: 3.3.7
128
147
  signing_key:
129
148
  specification_version: 4
130
149
  summary: Callback-less event reactor for Ruby