ever 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ba6404ba736a8c3c519cba46a8bf8a9df85ecc4a9532790c15821bff12f348e
4
+ data.tar.gz: 162398e3f59e7a638cf7de58b47e0a05c746f4be49e46f62bbd23b60305a1f77
5
+ SHA512:
6
+ metadata.gz: a3e8236c119bdb97b0c5aa0b26f60cbc40f740201218de11494b2368e0c60e7ab3dc4b0235751b903483dc441fcc55f07bc9da61e6c06e5a26135656e909cfc8
7
+ data.tar.gz: aeef774232228911357f7c472a937695f319615d31f0bf11d93554177c3b184a32003b4489a5193ca803d90dbb67cfda5a64417e6c6045be11f73bc98d341b21
@@ -0,0 +1,29 @@
1
+ name: Tests
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ strategy:
8
+ fail-fast: false
9
+ matrix:
10
+ os: [ubuntu-latest]
11
+ ruby: [2.6, 2.7, 3.0]
12
+
13
+ name: >-
14
+ ${{matrix.os}}, ${{matrix.ruby}}
15
+
16
+ runs-on: ${{matrix.os}}
17
+ steps:
18
+ - uses: actions/checkout@v1
19
+ - uses: actions/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{matrix.ruby}}
22
+ - name: Install dependencies
23
+ run: |
24
+ gem install bundler
25
+ bundle install
26
+ - name: Compile C-extension
27
+ run: bundle exec rake compile
28
+ - name: Run tests
29
+ run: bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,57 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ # Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
57
+ *.so
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1 2021-08-30
2
+
3
+ - First working version
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,25 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ever (0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ http_parser.rb (0.7.0)
10
+ minitest (5.14.4)
11
+ rake (13.0.6)
12
+ rake-compiler (1.1.1)
13
+ rake
14
+
15
+ PLATFORMS
16
+ x86_64-linux
17
+
18
+ DEPENDENCIES
19
+ ever!
20
+ http_parser.rb (= 0.7.0)
21
+ minitest (= 5.14.4)
22
+ rake-compiler (= 1.1.1)
23
+
24
+ BUNDLED WITH
25
+ 2.2.26
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Digital Fabric
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # Ever - a callback-less event reactor for Ruby
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/ever.svg)](http://rubygems.org/gems/ever)
4
+ [![Ever Test](https://github.com/digital-fabric/ever/workflows/Tests/badge.svg)](https://github.com/digital-fabric/ever/actions?query=workflow%3ATests)
5
+ [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/digital-fabric/ever/blob/master/LICENSE)
6
+
7
+ Ever is a [libev](http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod)-based event reactor for Ruby with a callback-less design. Events are emitted to an application-provided block inside a tight loop without registering and invoking callbacks.
8
+
9
+ ## Features
10
+
11
+ - Simple, minimalistic API
12
+ - Zero dependencies
13
+ - Callback-less API for getting events
14
+ - Events for I/O readiness
15
+ - Events for one-shot or recurring timers
16
+ - Cross-thread signalling and emitting of events
17
+
18
+ ## Rationale
19
+
20
+ I'm planning to add a compatibility mode to [Tipi](https://github.com/digital-fabric/tipi), a new [Polyphony](https://github.com/digital-fabric/polyphony)-based web server for Ruby. In this mode, Tipi will not be using Polyphony, but will employ multiple worker threads for handling concurrent requests. The problem is that we have X number of threads that need to be able to deal with Y number of concurrent connections.
21
+
22
+ After coming up with a bunch of different ideas for how to achieve this, I settled on the following design:
23
+
24
+ - The main thread runs a libev-based event reactor, and deals with accepting connections and distributing events.
25
+ - One or more worker threads wait for jobs to execute.
26
+ - When a new connection is accepted, the main thread starts watching for I/O readiness.
27
+ - When a connection is ready for reading, the main thread puts the connection on the job queue.
28
+ - A worker thread pulls the connection from the job queue and tries to read an incoming request. If the request is not complete, the connection is watched again for read readiness.
29
+ - 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
+ - When the response has been written, the connection is watched again for read readiness in preparation for the next request.
31
+
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).)
33
+
34
+ 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
+
36
+ ## Installing
37
+
38
+ If you're using bundler just add it to your `Gemfile`:
39
+
40
+ ```ruby
41
+ source 'https://rubygems.org'
42
+
43
+ gem 'ever'
44
+ ```
45
+
46
+ You can then run `bundle install` to install it. Otherwise, just run `gem install ever`.
47
+
48
+ ## Usage
49
+
50
+ Start by creating an instance of Ever::Loop:
51
+
52
+ ```ruby
53
+ require 'ever'
54
+
55
+ evloop = Ever::Loop.new
56
+ ```
57
+
58
+ ### Setting up event watchers
59
+
60
+ All events are identified using an application-provided key. This means that your app should provide a unique key for each event you wish to watch. To watch for I/O readiness, use `Loop#watch_io(key, io, read_write, oneshot)` where:
61
+
62
+ - `key`: unique event key (this can be *any* value, and in many cases you can just use the `IO` instance.)
63
+ - `io`: `IO` instance to watch.
64
+ - `read_write`: `false` for read, `true` for write.
65
+ - `oneshot`: `true` for one-shot event monitoring, `false` otherwise.
66
+
67
+ Example:
68
+
69
+ ```ruby
70
+ result = socket.read_nonblock(16384, exception: false)
71
+ case result
72
+ when :wait_readable
73
+ evloop.watch_io(socket, socket, false, true)
74
+ else
75
+ ...
76
+ end
77
+ ```
78
+
79
+ To setup up timers, use `Loop#watch_timer(key, duration, interval)` where:
80
+
81
+ - `key`: unique event key
82
+ - `duration`: timer duration in seconds.
83
+ - `interval`: recurring interval in seconds. `0` for a one-shot timer.
84
+
85
+ ```ruby
86
+ evloop.watch_timer(:timer, 1, 1)
87
+ evloop.each do |key|
88
+ case key
89
+ when :timer
90
+ puts "Got timer event"
91
+ end
92
+ end
93
+ ```
94
+
95
+ ### Stopping watchers
96
+
97
+ To stop a specific watcher, use `Loop#unwatch(key)` and provide the key previously provided to `#watch_io` or `#watch_timer`:
98
+
99
+ ```ruby
100
+ evloop.watch_timer(:timer, 1, 1)
101
+ count = 0
102
+ evloop.each do |key|
103
+ case key
104
+ when :timer
105
+ puts "Got timer event"
106
+ count += 1
107
+ evloop.unwatch(:timer) if count == 10
108
+ end
109
+ end
110
+ ```
111
+
112
+ ### Processing events
113
+
114
+ To process events as they happen, use `Loop#each`, which will block waiting for events and will yield events as they happen. The application-provided block will be called with the event key for each event:
115
+
116
+ ```ruby
117
+ evloop.each do |key|
118
+ distribute_event(key)
119
+ end
120
+ ```
121
+
122
+ Alternatively you can use `Loop#next_event` to process events one by one, or using a custom loop. Note that while this method can block, it can also return `nil` in case no event was generated.
123
+
124
+ ### Emitting custom events
125
+
126
+ You can emit events using `Loop#emit(key)`. In case the event loop is currently polling for events, it immediately return and the emitted event will be available.
127
+
128
+ ### Signalling the event loop
129
+
130
+ You can signal the event loop in order to stop it from blocking by using `Loop#signal`.
131
+
132
+ ### Stopping the event loop
133
+
134
+ An event loop that is currently blocking on `Loop#each` can be stopped using `Loop#stop` or by calling `Loop#emit(:stop)`.
135
+
136
+ ### Signal handling
137
+
138
+ The created event loop will not trap signals by itself. You can setup signal traps and emit events that tell the app what to do. Here's an example:
139
+
140
+ ```ruby
141
+ evloop = Ever::Loop.new
142
+ trap('SIGINT') { evloop.stop }
143
+ evloop.each { |key| handle_event(key) }
144
+ ```
145
+
146
+ ## API Summary
147
+
148
+ |Method|Description|
149
+ |------|-----------|
150
+ |`Loop.new()`|create a new event loop.|
151
+ |`Loop#each(&block)`|Handle events in an infinite loop.|
152
+ |`Loop#next_event()`|Wait for an event and return its key.|
153
+ |`Loop#watch_io(key, io, read_write, oneshot)`|Watch an IO instance for readiness.|
154
+ |`Loop#watch_timer(key, duration, interval)`|Setup a one-shot/recurring timer.|
155
+ |`Loop#unwatch(key)`|Stop watching specific event key.|
156
+ |`Loop#emit(key)`|Emit a custom event.|
157
+ |`Loop#signal()`|Signal the event loop, causing it to break if currently blocking.|
158
+ |`Loop#stop()`|Stop an event loop currently blocking in `#each`.|
159
+
160
+ ## Performance
161
+
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.
163
+
164
+ ## Contributing
165
+
166
+ Issues and pull requests will be gladly accepted. If you have found this gem
167
+ useful, please let me know.
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/clean'
5
+
6
+ require 'rake/extensiontask'
7
+ Rake::ExtensionTask.new('ever_ext') do |ext|
8
+ ext.ext_dir = 'ext/ever'
9
+ end
10
+
11
+ task :recompile => [:clean, :compile]
12
+ task :default => [:compile, :test]
13
+
14
+ task :test do
15
+ exec 'ruby test/test_loop.rb'
16
+ end
17
+
18
+ CLEAN.include '**/*.o', '**/*.so', '**/*.so.*', '**/*.a', '**/*.bundle', '**/*.jar', 'pkg', 'tmp'
data/ever.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ require_relative './lib/ever/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'ever'
5
+ s.version = Ever::VERSION
6
+ s.licenses = ['MIT']
7
+ s.summary = 'Callback-less event reactor for Ruby'
8
+ s.author = 'Sharon Rosner'
9
+ s.email = 'sharon@noteflakes.com'
10
+ s.files = `git ls-files`.split
11
+ s.homepage = 'https://digital-fabric.github.io/ever'
12
+ s.metadata = {
13
+ "source_code_uri" => "https://github.com/digital-fabric/ever",
14
+ "homepage_uri" => "https://github.com/digital-fabric/ever",
15
+ "changelog_uri" => "https://github.com/digital-fabric/ever/blob/master/CHANGELOG.md"
16
+ }
17
+ s.rdoc_options = ["--title", "ever", "--main", "README.md"]
18
+ s.extra_rdoc_files = ["README.md"]
19
+ s.extensions = ["ext/ever/extconf.rb"]
20
+ s.require_paths = ["lib"]
21
+ s.required_ruby_version = '>= 2.6'
22
+
23
+ s.add_development_dependency 'rake-compiler', '1.1.1'
24
+ s.add_development_dependency 'minitest', '5.14.4'
25
+ s.add_development_dependency 'http_parser.rb', '0.7.0'
26
+ end
@@ -0,0 +1,124 @@
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
+ worker = Thread.new do
42
+ while (job = $job_queue.shift)
43
+ handle_connection(job)
44
+ end
45
+ end
46
+
47
+ def handle_connection(conn)
48
+ if !conn.request_complete
49
+ handle_read_request(conn)
50
+ else
51
+ handle_write_response(conn)
52
+ end
53
+ end
54
+
55
+ def handle_read_request(conn)
56
+ result = conn.io.read_nonblock(16384, exception: false)
57
+ case result
58
+ when :wait_readable
59
+ $evloop.emit([:watch_io, conn, false, true])
60
+ when :wait_writable
61
+ $evloop.emit([:watch_io, conn, true, true])
62
+ when nil
63
+ $evloop.emit([:close, conn])
64
+ else
65
+ conn.parser << result
66
+ if conn.request_complete
67
+ conn.response = handle_request(conn.request_headers, conn.request_body)
68
+ handle_write_response(conn)
69
+ else
70
+ $evloop.emit([:watch_io, conn, false, true])
71
+ end
72
+ end
73
+ rescue HTTP::Parser::Error, SystemCallError, IOError
74
+ $evloop.emit([:close, conn])
75
+ end
76
+
77
+ def handle_request(headers, body)
78
+ response_body = "Hello, world!"
79
+ "HTTP/1.1 200 OK\nContent-Length: #{response_body.bytesize}\n\n#{response_body}"
80
+ end
81
+
82
+ def handle_write_response(conn)
83
+ result = conn.io.write_nonblock(conn.response, exception: false)
84
+ case result
85
+ when :wait_readable
86
+ $evloop.emit([:watch_io, conn, false, true])
87
+ when :wait_writable
88
+ $evloop.emit([:watch_io, conn, true, true])
89
+ when nil
90
+ $evloop.emit([:close, conn])
91
+ else
92
+ conn.setup_read_request
93
+ $evloop.emit([:watch_io, conn, false, true])
94
+ end
95
+ end
96
+
97
+ def setup_connection(io)
98
+ conn = Connection.new(io)
99
+ $evloop.emit([:watch_io, conn, false, true])
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
+ $evloop.each do |event|
108
+ case event
109
+ when :accept
110
+ socket = server.accept
111
+ setup_connection(socket)
112
+ when Connection
113
+ $job_queue << event
114
+ when Array
115
+ cmd = event[0]
116
+ case cmd
117
+ when :watch_io
118
+ $evloop.watch_io(event[1], event[1].io, event[2], event[3])
119
+ when :close
120
+ conn = event[1]
121
+ conn.io.close
122
+ end
123
+ end
124
+ end