oldmoe-reactor 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +42 -0
- data/lib/reactor.rb +226 -0
- data/lib/timer.rb +40 -0
- data/lib/util.rb +36 -0
- data/reactor.gemspec +22 -0
- metadata +58 -0
data/README
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
Reactor
|
2
|
+
|
3
|
+
A reactor library with the very original name of "Reactor".
|
4
|
+
|
5
|
+
What is a reactor any way?
|
6
|
+
|
7
|
+
A reactor library is one that provides an asynchronus event handling mechanism. Ruby already has a couple of those. The most prominent are EventMachine and Rev. Many high performing Ruby applications like Thin and Evented Mongrel are utilizing EventMachine for event handling. Both Rev and EventMachine build atop native reactor implementations written in C or C++. While this ensures high performance it makes some integration aspects with Ruby a bit quirky. Sometimes even at a noticable performance cost.
|
8
|
+
|
9
|
+
This is why I thought of building Reactor. A much simpler reactor library in pure Ruby that attempts to use as much of the Ruby built in classes and standard libraries as possible. It only provides a minimal API that does not attempt to be so smart. It differs from EventMachine and Rev in the following aspects.
|
10
|
+
|
11
|
+
1 - Pure Ruby, no C or C++ code involved
|
12
|
+
2 - Very small (~100 lines of code)
|
13
|
+
3 - Uses the vanilla Ruby socket and server implementations
|
14
|
+
4 - Decent (high) performance on Ruby 1.9.1
|
15
|
+
5 - Ruby threading friendly (naturally)
|
16
|
+
6 - You can have multiple reactors running (like Rev and unlike EventMachine)
|
17
|
+
|
18
|
+
Usage is simple, here's a simple Echo server that uses Reactor
|
19
|
+
|
20
|
+
require 'reactor'
|
21
|
+
require 'socket'
|
22
|
+
|
23
|
+
reactor = Reactor::Base.new
|
24
|
+
server = TCPServer.new("0.0.0.0",8080)
|
25
|
+
|
26
|
+
reactor.attach(:read, server) do |server|
|
27
|
+
conn = server.accept
|
28
|
+
conn.write(conn.gets)
|
29
|
+
conn.close
|
30
|
+
end
|
31
|
+
|
32
|
+
reactor.run # blocking call, will run for ever (no signal handling currently)
|
33
|
+
|
34
|
+
The server is a normal Ruby TCPServer. It attaches itself to the reactor and asks to be notified if there is data to be read on the wire. A block is provided that will handle those notifications. Alternatively, the server can implement a notify_readable method that will be fired instead.
|
35
|
+
|
36
|
+
Any IO object can be attached to the reactor but it doesn't make much sense to attach actual files since they will block upon reading or writing anyway. Sockets and pipes will work in a non-blocking manner though.
|
37
|
+
|
38
|
+
Reactor is using Ruby's IO.select behind the scenes. This limits its ability to scale in comparison to something like EventMachine or Rev which are able to utilize Epoll and Kqueue which scale much better. This is not a major concern though. Most servers listen to a few fds most of the time, which is a bit faster when using select. Besides one can hope that Ruby will be able to use Epoll and Kqueue some day which will translate to direct benefit to Reactor.
|
39
|
+
|
40
|
+
Todo
|
41
|
+
|
42
|
+
The timers code needs to be reimplemented as a red black tree or a skip list to avoid the current O(n) cost. It works just fine at its current form for a few timers though (tens or even hundreds)
|
data/lib/reactor.rb
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
$:.unshift File.expand_path(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'util'
|
4
|
+
require 'timer'
|
5
|
+
|
6
|
+
module Reactor
|
7
|
+
# A small, fast, pure Ruby reactor library
|
8
|
+
#
|
9
|
+
# Has the following features:
|
10
|
+
#
|
11
|
+
# - Pure Ruby, no compilation involved
|
12
|
+
# - Attach/detach IO objects for readability and writability notifications
|
13
|
+
# - Add blocks of code that get executed after some time
|
14
|
+
# - Multiple reactors can co-exist (each in a separate thread of course)
|
15
|
+
#
|
16
|
+
# Lacks the following features:
|
17
|
+
#
|
18
|
+
# - No Epoll or Kqueue support since it relies on Ruby's IO.select
|
19
|
+
# (Yaki Schloba's Ktools can help here)
|
20
|
+
# - While you can have several reactors in several threads you cannot manipulate
|
21
|
+
# a single reactor from multiple threads.
|
22
|
+
#
|
23
|
+
# Rationale
|
24
|
+
#
|
25
|
+
# - Reactor libraries are re-implementing every bit of Ruby,
|
26
|
+
# I would like to see that effort go to Ruby and its standard library
|
27
|
+
# - I needed better integration with some Ruby built in classes.
|
28
|
+
# - Some people consider using EventMachine with Ruby as cheating!
|
29
|
+
#
|
30
|
+
# Example TCP server (an echo server)
|
31
|
+
#
|
32
|
+
# require 'reactor'
|
33
|
+
# require 'socket'
|
34
|
+
#
|
35
|
+
# reactor = Reactor::Base.new
|
36
|
+
# server = TCPServer.new("0.0.0.0",8080)
|
37
|
+
#
|
38
|
+
# reactor.attach(:read, server) do |server|
|
39
|
+
# connection = server.accept
|
40
|
+
# connection.write(connection.read)
|
41
|
+
# connection.close
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# reactor.run # blocking call, will run for ever (no signal handling currently)
|
45
|
+
#
|
46
|
+
# You can see a working example by running "ruby reactor.rb"
|
47
|
+
class Base
|
48
|
+
# Initializes a new reactor object
|
49
|
+
def initialize
|
50
|
+
@selectables = {:read => {:dirty=> false, :ios => {}, :callbacks => {}, :io_list => []},
|
51
|
+
:write=> {:dirty=> false, :ios => {}, :callbacks => {}, :io_list => []}}
|
52
|
+
@next_procs, @timers, @running = [], [], false
|
53
|
+
end
|
54
|
+
|
55
|
+
# Starts the reactor loop
|
56
|
+
#
|
57
|
+
# If a block is given it is run and the reactor itself is sent as a parameter
|
58
|
+
# The block will be run while the reactor is in the running state but before
|
59
|
+
# the actual loop.
|
60
|
+
#
|
61
|
+
# Each run of the loop will fire all expired time objects and will wait a few
|
62
|
+
# melliseconds for notifications on the list of IO objects, if any occurs
|
63
|
+
# the corresponding callbacks are fired and the loop resumes, otherwise it resumes
|
64
|
+
# directly
|
65
|
+
def run
|
66
|
+
@running = true
|
67
|
+
yield self if block_given?
|
68
|
+
loop do
|
69
|
+
break unless @running
|
70
|
+
run_once
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# A single select run, it will fire all expired timers and the callbacks on IO objects
|
75
|
+
# but it will return immediately after that. This is useful if you need to create your
|
76
|
+
# own loop to interleave the IO event notifications with other operations
|
77
|
+
def run_once
|
78
|
+
update_list(@selectables[:read])
|
79
|
+
update_list(@selectables[:write])
|
80
|
+
if res = IO.select(@selectables[:read][:io_list], @selectables[:write][:io_list], nil, 0.005)
|
81
|
+
fire_ios(:read, res[0])
|
82
|
+
fire_ios(:write, res[1])
|
83
|
+
end
|
84
|
+
fire_procs
|
85
|
+
fire_timers
|
86
|
+
end
|
87
|
+
|
88
|
+
# Stops the reactor loop
|
89
|
+
# It does not detach any of the attached IO objects, the reactor can be started again
|
90
|
+
# and it will keep notifying on the same set of attached IO objects
|
91
|
+
#
|
92
|
+
# Stop does not stop the reactor immediately, rather it is stopped at the next cycle,
|
93
|
+
# the current cycle continues to completion
|
94
|
+
def stop
|
95
|
+
@running = false
|
96
|
+
end
|
97
|
+
|
98
|
+
# Attach an IO object to the reactor.
|
99
|
+
#
|
100
|
+
# mode can be either :read or :write
|
101
|
+
#
|
102
|
+
# A block must be provided and it will be used as the callback to handle the event,
|
103
|
+
# once the event fires the block will be called with the IO object and the reactor
|
104
|
+
# passed as block parameters.
|
105
|
+
#
|
106
|
+
# A third argument (which defaults to true) tells the method what to do if the IO
|
107
|
+
# object is already attached. If it is set to true (default) then the reactor will
|
108
|
+
# append the new call back till the original caller detaches it. If set to false
|
109
|
+
# then the reactor will just override the old callback with the new one
|
110
|
+
|
111
|
+
def attach(mode, io, wait_if_attached = true, &callback)
|
112
|
+
selectables = @selectables[mode] || raise("mode is not :read or :write")
|
113
|
+
raise "you must supply a callback block" if callback.nil?
|
114
|
+
if wait_if_attached && selectables[:ios][io.object_id]
|
115
|
+
selectables[:callbacks][io.object_id] << callback
|
116
|
+
else
|
117
|
+
selectables[:ios][io.object_id] = io
|
118
|
+
selectables[:callbacks][io.object_id] = [callback]
|
119
|
+
selectables[:dirty] = true
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Detach an IO object from the reactor
|
124
|
+
#
|
125
|
+
# mode can be either :read or :write
|
126
|
+
def detach(mode, io, force=false)
|
127
|
+
selectables = @selectables[mode] || raise("mode is not :read or :write")
|
128
|
+
if !force && selectables[:callbacks][io.object_id].length > 0
|
129
|
+
selectables[:callbacks][io.object_id].shift
|
130
|
+
else
|
131
|
+
selectables[:ios].delete(io.object_id)
|
132
|
+
selectables[:callbacks].delete(io.object_id)
|
133
|
+
selectables[:dirty] = true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Detach all IO objects of a certain mode from the reactor
|
138
|
+
#
|
139
|
+
# mode can be either :read or :write
|
140
|
+
def detach_all(mode)
|
141
|
+
raise("mode is not :read or :write") unless [:read, :write].include? mode
|
142
|
+
@selectables[mode] = {:ios => {}, :callbacks => {}, :io_list => []}
|
143
|
+
end
|
144
|
+
|
145
|
+
# Ask the reactor if an IO object is attached in some mode
|
146
|
+
#
|
147
|
+
# mode can be either :read or :write
|
148
|
+
def attached?(mode, io)
|
149
|
+
@selectables[mode][:ios].include? io
|
150
|
+
end
|
151
|
+
|
152
|
+
# Add a block of code that will fire after some time
|
153
|
+
def add_timer(time, periodical=false, &block)
|
154
|
+
timer = Timer.new(@timers, time, periodical, &block)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Add a block of code that will fire periodically after some time passes
|
158
|
+
def add_periodical_timer(time, &block)
|
159
|
+
add_timer(time, true, &block)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Register a block to be called at the next reactor tick
|
163
|
+
def next_tick &block
|
164
|
+
@next_procs << block
|
165
|
+
end
|
166
|
+
|
167
|
+
# Is the reactor running?
|
168
|
+
def running?
|
169
|
+
@running
|
170
|
+
end
|
171
|
+
|
172
|
+
protected
|
173
|
+
|
174
|
+
def update_list(selectables)
|
175
|
+
selectables[:io_list], selectables[:dirty] = selectables[:ios].values, false if selectables[:dirty]
|
176
|
+
end
|
177
|
+
|
178
|
+
def fire_procs
|
179
|
+
length = @next_procs.length
|
180
|
+
length.times { @next_procs.shift.call }
|
181
|
+
end
|
182
|
+
|
183
|
+
def fire_timers
|
184
|
+
return if @timers.length == 0
|
185
|
+
t = (Time.now.to_f * 1000).to_i
|
186
|
+
while @timers.length > 0 && @timers.first.time_of_fire <= t
|
187
|
+
@timers.shift.fire
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def fire_ios(mode, ios)
|
192
|
+
ios.each {|io|@selectables[mode][:callbacks][io.object_id][0].call(io, self)}
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
if __FILE__ == $0
|
198
|
+
trap('INT') do
|
199
|
+
puts "why did you have to press CTRL+C? why? why?"
|
200
|
+
puts "off to the darkness.. again!"
|
201
|
+
exit
|
202
|
+
end
|
203
|
+
require 'socket'
|
204
|
+
port = (ARGV[0] || 3333).to_i
|
205
|
+
puts ">> Reactor library version 1.0 ()"
|
206
|
+
puts ">> This is an interactive test"
|
207
|
+
puts ">> The console will echo everything you type"
|
208
|
+
puts ">> At the same time it will *secretly* listen"
|
209
|
+
puts ">> to connections on port #{port} and send"
|
210
|
+
puts ">> all that you wrote to whoever asks for it"
|
211
|
+
puts ">> Have fun.."
|
212
|
+
buffer = ""
|
213
|
+
reactor = Reactor::Base.new
|
214
|
+
server = TCPServer.new("0.0.0.0", port)
|
215
|
+
reactor.attach(:read, server) do |server|
|
216
|
+
conn = server.accept
|
217
|
+
conn.write("HTTP/1.1 200 OK\r\nContent-Length:#{buffer.length}\r\nContent-Type:text/plain\r\n\r\n#{buffer}")
|
218
|
+
conn.close
|
219
|
+
end
|
220
|
+
reactor.attach(:read, STDIN) do
|
221
|
+
data = gets
|
222
|
+
puts data
|
223
|
+
buffer << data
|
224
|
+
end
|
225
|
+
reactor.run
|
226
|
+
end
|
data/lib/timer.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Reactor
|
2
|
+
# Timer objects created by Reactor::Base#add_to_timer are instances
|
3
|
+
# of this class. You can call cancel on them to stop them from executing
|
4
|
+
class Timer
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
attr_reader :time_of_fire, :periodical
|
8
|
+
|
9
|
+
def initialize(timers, time, periodical, &block)
|
10
|
+
@timers = timers
|
11
|
+
@time = time * 1000
|
12
|
+
@periodical = periodical
|
13
|
+
@block = block
|
14
|
+
@active = true
|
15
|
+
add_to_timers
|
16
|
+
end
|
17
|
+
|
18
|
+
def fire
|
19
|
+
return unless @active
|
20
|
+
@block.call
|
21
|
+
add_to_timers if @periodical
|
22
|
+
end
|
23
|
+
|
24
|
+
# Cancels the timer
|
25
|
+
# It will be lazily removed from the timer's list later
|
26
|
+
def cancel
|
27
|
+
@active = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_to_timers
|
31
|
+
@time_of_fire = (Time.now.to_f * 1000).to_i + @time
|
32
|
+
@timers.insert_sorted(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
def <=>(other)
|
36
|
+
self.time_of_fire <=> other.time_of_fire
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/lib/util.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class Array
|
2
|
+
def insert_sorted(value)
|
3
|
+
#move across all values
|
4
|
+
if self.length == 0
|
5
|
+
self << value
|
6
|
+
elsif self.first >= value
|
7
|
+
self.unshift value
|
8
|
+
elsif self.last <= value
|
9
|
+
self << value
|
10
|
+
else
|
11
|
+
#find the first value that is bigger than value
|
12
|
+
start, finish = 0, self.length - 1
|
13
|
+
loop do
|
14
|
+
median = ((start + finish)/2).to_i
|
15
|
+
if self[median] > value
|
16
|
+
if self[median-1] < value
|
17
|
+
self.insert(median, value)
|
18
|
+
break
|
19
|
+
end
|
20
|
+
finish = median - 1
|
21
|
+
elsif self[median] < value
|
22
|
+
if self[median+1] > value
|
23
|
+
self.insert(median + 1, value)
|
24
|
+
break
|
25
|
+
end
|
26
|
+
start = median + 1
|
27
|
+
else
|
28
|
+
self.insert(median, value)
|
29
|
+
break
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
data/reactor.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "reactor"
|
3
|
+
s.version = "0.2.0"
|
4
|
+
s.date = "2009-07-04"
|
5
|
+
s.summary = "A pure Ruby reactor library"
|
6
|
+
s.email = "oldmoe@gmail.com"
|
7
|
+
s.homepage = "http://github.com/oldmoe/reactor"
|
8
|
+
s.description = "A simple, fast reactor library in pure Ruby"
|
9
|
+
s.has_rdoc = true
|
10
|
+
s.authors = ["Muhammad A. Ali"]
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.files = [
|
13
|
+
"reactor.gemspec",
|
14
|
+
"README",
|
15
|
+
"lib/reactor.rb",
|
16
|
+
"lib/util.rb",
|
17
|
+
"lib/timer.rb"
|
18
|
+
]
|
19
|
+
s.rdoc_options = ["--main", "README"]
|
20
|
+
s.extra_rdoc_files = ["README"]
|
21
|
+
end
|
22
|
+
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: oldmoe-reactor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Muhammad A. Ali
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-07-04 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A simple, fast reactor library in pure Ruby
|
17
|
+
email: oldmoe@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
files:
|
25
|
+
- reactor.gemspec
|
26
|
+
- README
|
27
|
+
- lib/reactor.rb
|
28
|
+
- lib/util.rb
|
29
|
+
- lib/timer.rb
|
30
|
+
has_rdoc: true
|
31
|
+
homepage: http://github.com/oldmoe/reactor
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options:
|
34
|
+
- --main
|
35
|
+
- README
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: "0"
|
43
|
+
version:
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 1.2.0
|
54
|
+
signing_key:
|
55
|
+
specification_version: 2
|
56
|
+
summary: A pure Ruby reactor library
|
57
|
+
test_files: []
|
58
|
+
|