oldmoe-reactor 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/README +42 -0
  2. data/lib/reactor.rb +226 -0
  3. data/lib/timer.rb +40 -0
  4. data/lib/util.rb +36 -0
  5. data/reactor.gemspec +22 -0
  6. 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
+