armstrong 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +70 -0
- data/demo/armstrong_test.rb +15 -0
- data/demo/mongrel2.conf +32 -0
- data/lib/armstrong/connection.rb +115 -0
- data/lib/armstrong/data_structures.rb +6 -0
- data/lib/armstrong/main_actors.rb +108 -0
- data/lib/armstrong.rb +148 -0
- metadata +132 -0
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# Armstrong #
|
2
|
+
An evented, fiber-based server for Ruby. This project is heavily based on [Brubeck](http://brubeck.io). The goal was to make it really easy to make an evented server that acted quick and scaled infinitely. This is accomplished by using [Mongrel2](http://mongrel2.org), [ZeroMQ](http://zeromq.org) and [Rubinius](rubini.us). Rubinius has the actor gem included already so it makes it really convenient to just use rubinius. Also, the 2.0.0dev branch has super nice thread handling, allowing for true Ruby concurrency that MRI just can't offer with its GIL.
|
3
|
+
|
4
|
+
## Mongrel2 and ZeroMQ ##
|
5
|
+
Although it seems like a strange direction to start writing servers in, eventually most companies end up in the realm of evented servers. This is because it offers nearly infinite scalability for free.
|
6
|
+
|
7
|
+
This is possible because of Mongrel2 and ZeroMQ. Mongrel2 acts as your server and parses requests. It then sends out ZeroMQ messages to your handlers and proxies and then returns their responses. Because it uses ZeroMQ messages, Mongrel2 can send messages anywhere and to any language. Conversely, it sends messages in a round-robin style, so scalability is achieved by just starting up another instance of your server.
|
8
|
+
|
9
|
+
## setup ##
|
10
|
+
#### Rubinius ####
|
11
|
+
|
12
|
+
rvm install rbx
|
13
|
+
rvm use rbx
|
14
|
+
|
15
|
+
#### ZeroMQ ####
|
16
|
+
Go grab the zip from [zeromq/zeromq2-1](https://github.com/zeromq/zeromq2-1), unzip it, and in the directory run:
|
17
|
+
|
18
|
+
./autogen.sh; ./configure; make; sudo make install
|
19
|
+
|
20
|
+
#### ZMQ and other gems ####
|
21
|
+
gem install zmq
|
22
|
+
gem install lazy
|
23
|
+
|
24
|
+
it should also install `ffi` and `ffi-rzmq` which are to dynamically load libs and call functions from them. Interesting stuff, but out of the scope of this measly README.
|
25
|
+
|
26
|
+
#### Mongrel2 ####
|
27
|
+
Finally, go grab a copy of mongrel2 (1.7.5 tested) from the [Mongrel2](http://mongrel2.org) website.
|
28
|
+
|
29
|
+
There's a sample `mongrel2.conf` and `config.sqlite` in the `demo` folder, feel free to use those. Otherwise, load the `mongrel2.conf` into `m2sh` and then start the server.
|
30
|
+
|
31
|
+
m2sh load -config mongrel2.conf -db config.sqlite
|
32
|
+
m2sh start -host localhost
|
33
|
+
|
34
|
+
## minimal example ##
|
35
|
+
|
36
|
+
require './lib/armstrong'
|
37
|
+
|
38
|
+
get "/" do
|
39
|
+
output_string "hello world"
|
40
|
+
end
|
41
|
+
|
42
|
+
Just like in Sinatra, we state the verb we want to use, the path, and give it a block with the relevant code to execute. So far only 'GET' requests are supported but more will come out in later builds.
|
43
|
+
|
44
|
+
You can also call the `get_message` method which returns the request from the browser, and then reply to the request with `reply(request, message)`. `reply_string(message)` is a helper function that grabs the message and instantly replies to it with `message`.
|
45
|
+
|
46
|
+
Now you should run `ruby armstrong_test.rb` and then visit [localhost:6767](http://localhost:6767/) and relish in the 'Hello World'.
|
47
|
+
|
48
|
+
## more functionality ##
|
49
|
+
|
50
|
+
commit e86c74aed added functionality for parameters in your path. These are simply demonstrated in the `demo/armstrong_test.rb` file. For instance, you can extract the id of a certain part of your path like so:
|
51
|
+
|
52
|
+
require 'armstrong'
|
53
|
+
|
54
|
+
get "/:id" do
|
55
|
+
req = get_request
|
56
|
+
reply req, "id: #{req[:params]["id"]}"
|
57
|
+
end
|
58
|
+
|
59
|
+
The params are always going to be stored in the request, naturally.
|
60
|
+
|
61
|
+
## benchmarking ##
|
62
|
+
|
63
|
+
$ time curl localhost:6767/
|
64
|
+
Hello World
|
65
|
+
real 0m0.014s
|
66
|
+
user 0m0.007s
|
67
|
+
sys 0m0.004s
|
68
|
+
|
69
|
+
## License ##
|
70
|
+
GPLv3
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require '../lib/armstrong'
|
2
|
+
|
3
|
+
get "/" do
|
4
|
+
reply_string "hello world"
|
5
|
+
end
|
6
|
+
|
7
|
+
get "/:id" do
|
8
|
+
req = get_request
|
9
|
+
reply req, "id: #{req[:params]["id"]}"
|
10
|
+
end
|
11
|
+
|
12
|
+
get "/:id/do" do
|
13
|
+
req = get_request
|
14
|
+
reply req, "do: #{req[:params]["id"]}"
|
15
|
+
end
|
data/demo/mongrel2.conf
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
armstrong_handler = Handler(
|
2
|
+
send_spec='tcp://127.0.0.1:9999',
|
3
|
+
send_ident='34f9ceee-cd52-4b7f-b197-88bf2f0ec378',
|
4
|
+
recv_spec='tcp://127.0.0.1:9998',
|
5
|
+
recv_ident='')
|
6
|
+
|
7
|
+
media_dir = Dir(
|
8
|
+
base='media/',
|
9
|
+
index_file='index.html',
|
10
|
+
default_ctype='text/plain')
|
11
|
+
|
12
|
+
armstrong_host = Host(
|
13
|
+
name="localhost",
|
14
|
+
routes={
|
15
|
+
'/media/': media_dir,
|
16
|
+
'/': armstrong_handler})
|
17
|
+
|
18
|
+
armstrong_serv = Server(
|
19
|
+
uuid="f400bf85-4538-4f7a-8908-67e313d515c2",
|
20
|
+
access_log="/log/mongrel2.access.log",
|
21
|
+
error_log="/log/mongrel2.error.log",
|
22
|
+
chroot="./",
|
23
|
+
default_host="localhost",
|
24
|
+
name="armstrong test",
|
25
|
+
pid_file="/run/mongrel2.pid",
|
26
|
+
port=6767,
|
27
|
+
hosts = [armstrong_host]
|
28
|
+
)
|
29
|
+
|
30
|
+
settings = {"zeromq.threads": 2, "limits.min_ping": 15, "limits.kill_limit": 2}
|
31
|
+
|
32
|
+
servers = [armstrong_serv]
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'ffi-rzmq'
|
2
|
+
|
3
|
+
class Connection
|
4
|
+
attr_reader :app_id, :sub_addr, :pub_addr, :request_sock, :response_sock
|
5
|
+
|
6
|
+
def initialize(app_id, zmq_sub_pub_addr=["tcp://127.0.0.1", 9999, "tcp://127.0.0.1", 9998])
|
7
|
+
@app_id = app_id
|
8
|
+
@sub_addr = zmq_sub_pub_addr[0..1].join(":")
|
9
|
+
@pub_addr = zmq_sub_pub_addr[2..3].join(":")
|
10
|
+
|
11
|
+
@request_sock = @response_sock = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def connect
|
15
|
+
context = ZMQ::Context.new 1
|
16
|
+
@request_sock = context.socket ZMQ::PULL
|
17
|
+
@request_sock.connect @sub_addr
|
18
|
+
|
19
|
+
@response_sock = context.socket ZMQ::PUB
|
20
|
+
@response_sock.setsockopt ZMQ::IDENTITY, @app_id
|
21
|
+
@response_sock.connect @pub_addr
|
22
|
+
end
|
23
|
+
|
24
|
+
#raw recv
|
25
|
+
def recv
|
26
|
+
msg = ""
|
27
|
+
@request_sock.recv_string msg
|
28
|
+
return msg
|
29
|
+
end
|
30
|
+
|
31
|
+
#parse the request, this is the best way to get stuff back, as a Hash
|
32
|
+
def receive
|
33
|
+
data = parse(self.recv)
|
34
|
+
return data
|
35
|
+
end
|
36
|
+
|
37
|
+
def send(uuid, conn_id, msg)
|
38
|
+
header = "%s %d:%s" % [uuid, conn_id.join(' ').length, conn_id.join(' ')]
|
39
|
+
string = header + ', ' + msg
|
40
|
+
puts "\t\treplying to #{conn_id} with: ", string.inspect
|
41
|
+
@response_sock.send_string string, ZMQ::NOBLOCK
|
42
|
+
puts "send string"
|
43
|
+
end
|
44
|
+
|
45
|
+
def reply(request, message)
|
46
|
+
self.send(request[:uuid], [request[:id]], message)
|
47
|
+
end
|
48
|
+
|
49
|
+
def reply_http(req, body, code=200, headers={"Content-type" => "text/html"})
|
50
|
+
self.reply(req, http_response(body, code, headers))
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def http_response(body, code, headers)
|
55
|
+
headers['Content-Length'] = body.size
|
56
|
+
headers_s = headers.map{|k, v| "%s: %s" % [k,v]}.join("\r\n")
|
57
|
+
|
58
|
+
"HTTP/1.1 #{code} #{StatusMessage[code.to_i]}\r\n#{headers_s}\r\n\r\n#{body}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse(msg)
|
62
|
+
if(msg.empty?)
|
63
|
+
puts "msg is nil: [#{msg}]"
|
64
|
+
return nil
|
65
|
+
end
|
66
|
+
|
67
|
+
uuid, id, path, header_size, headers, body_size, body = msg.match(/^(.{36}) (\d+) (.*?) (\d+):(.*?),(\d+):(.*?),$/).to_a[1..-1]
|
68
|
+
|
69
|
+
return {:uuid => uuid, :id => id, :path => path, :header_size => header_size, :headers => headers, :body_size => body_size, :body => body}
|
70
|
+
end
|
71
|
+
|
72
|
+
# From WEBrick: thanks dawg.
|
73
|
+
StatusMessage = {
|
74
|
+
100 => 'Continue',
|
75
|
+
101 => 'Switching Protocols',
|
76
|
+
200 => 'OK',
|
77
|
+
201 => 'Created',
|
78
|
+
202 => 'Accepted',
|
79
|
+
203 => 'Non-Authoritative Information',
|
80
|
+
204 => 'No Content',
|
81
|
+
205 => 'Reset Content',
|
82
|
+
206 => 'Partial Content',
|
83
|
+
300 => 'Multiple Choices',
|
84
|
+
301 => 'Moved Permanently',
|
85
|
+
302 => 'Found',
|
86
|
+
303 => 'See Other',
|
87
|
+
304 => 'Not Modified',
|
88
|
+
305 => 'Use Proxy',
|
89
|
+
307 => 'Temporary Redirect',
|
90
|
+
400 => 'Bad Request',
|
91
|
+
401 => 'Unauthorized',
|
92
|
+
402 => 'Payment Required',
|
93
|
+
403 => 'Forbidden',
|
94
|
+
404 => 'Not Found',
|
95
|
+
405 => 'Method Not Allowed',
|
96
|
+
406 => 'Not Acceptable',
|
97
|
+
407 => 'Proxy Authentication Required',
|
98
|
+
408 => 'Request Timeout',
|
99
|
+
409 => 'Conflict',
|
100
|
+
410 => 'Gone',
|
101
|
+
411 => 'Length Required',
|
102
|
+
412 => 'Precondition Failed',
|
103
|
+
413 => 'Request Entity Too Large',
|
104
|
+
414 => 'Request-URI Too Large',
|
105
|
+
415 => 'Unsupported Media Type',
|
106
|
+
416 => 'Request Range Not Satisfiable',
|
107
|
+
417 => 'Expectation Failed',
|
108
|
+
500 => 'Internal Server Error',
|
109
|
+
501 => 'Not Implemented',
|
110
|
+
502 => 'Bad Gateway',
|
111
|
+
503 => 'Service Unavailable',
|
112
|
+
504 => 'Gateway Timeout',
|
113
|
+
505 => 'HTTP Version Not Supported'
|
114
|
+
}
|
115
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Aleph
|
2
|
+
class Base
|
3
|
+
class << self
|
4
|
+
attr_accessor :replier, :request_handler, :supervisor
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def process_route(route, pattern, keys, values = [])
|
10
|
+
return unless match = pattern.match(route)
|
11
|
+
values += match.captures.map { |v| URI.decode(v) if v }
|
12
|
+
params = {}
|
13
|
+
|
14
|
+
if values.any?
|
15
|
+
keys.zip(values) { |k,v| (params[k] ||= '') << v if v }
|
16
|
+
end
|
17
|
+
return params
|
18
|
+
end
|
19
|
+
|
20
|
+
Aleph::Base.replier = Proc.new do
|
21
|
+
@name = "replier"
|
22
|
+
puts "started (#{@name})"
|
23
|
+
Actor.register(:replier, Actor.current)
|
24
|
+
conn = nil
|
25
|
+
|
26
|
+
loop do
|
27
|
+
Actor.receive do |msg|
|
28
|
+
msg.when(ConnectionInformation) do |c|
|
29
|
+
#puts "replier: got connection information #{c.inspect}"
|
30
|
+
conn = c.connection
|
31
|
+
end
|
32
|
+
msg.when(Reply) do |m|
|
33
|
+
begin
|
34
|
+
conn.reply_http(m.data, m.body, m.code, m.headers)
|
35
|
+
rescue Exception => e
|
36
|
+
puts "Actor[:replier]: I messed up with exception: #{e.message}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Aleph::Base.request_handler = Proc.new do
|
44
|
+
@name = "request_handler"
|
45
|
+
puts "started (#{@name})"
|
46
|
+
Actor.register(:request_handler, Actor.current)
|
47
|
+
Actor.trap_exit = true
|
48
|
+
|
49
|
+
routes = []
|
50
|
+
loop do
|
51
|
+
Actor.receive do |f|
|
52
|
+
f.when(AddRoute) do |r|
|
53
|
+
routes << [r.route, r.method]
|
54
|
+
end
|
55
|
+
|
56
|
+
f.when(AddRoutes) do |r|
|
57
|
+
r.routes.each do |k|
|
58
|
+
routes << [k.route, k.method]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
f.when(ShowRoutes) do |r|
|
63
|
+
routes.each {|s| puts s}
|
64
|
+
end
|
65
|
+
|
66
|
+
f.when(Request) do |r|
|
67
|
+
failure = true
|
68
|
+
routes.each do |route|
|
69
|
+
if route[0][0].match(r.data[:path])
|
70
|
+
r.data[:params] = process_route(r.data[:path], route[0][0], route[0][1])
|
71
|
+
#puts r.data.inspect
|
72
|
+
Actor.spawn_link(&route[1]) << r.data
|
73
|
+
failure = false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
Actor[:replier] << Reply.new(r.data, "404") if failure
|
77
|
+
end
|
78
|
+
|
79
|
+
f.when(Actor::DeadActorError) do |exit|
|
80
|
+
puts "#{exit.actor} died with reason: [#{exit.reason}]"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
#if this dies, all hell will break loose
|
87
|
+
Aleph::Base.supervisor = Proc.new do
|
88
|
+
puts "started (supervisor)"
|
89
|
+
Actor.register(:supervisor, Actor.current)
|
90
|
+
Actor.trap_exit = true
|
91
|
+
|
92
|
+
Actor.link(Actor[:replier])
|
93
|
+
Actor.link(Actor[:request_handler])
|
94
|
+
|
95
|
+
loop do
|
96
|
+
Actor.receive do |f|
|
97
|
+
f.when(Actor::DeadActorError) do |exit|
|
98
|
+
"#{exit.actor.name} died with reason: #{exit.reason}"
|
99
|
+
case exit.actor.name
|
100
|
+
when "request_handler"
|
101
|
+
Actor.spawn_link(&@request_handler)
|
102
|
+
when "replier"
|
103
|
+
Actor.spawn_link(&@replier)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/armstrong.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'actor'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'lazy'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
libdir = File.dirname(__FILE__)
|
7
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
8
|
+
|
9
|
+
require "armstrong/connection"
|
10
|
+
require 'armstrong/data_structures'
|
11
|
+
require 'armstrong/main_actors'
|
12
|
+
|
13
|
+
def get_request
|
14
|
+
return Actor.receive
|
15
|
+
end
|
16
|
+
|
17
|
+
def reply(request, body, code=200, headers={"Content-type" => "text/html"})
|
18
|
+
Actor[:replier] << Reply.new(request, body, code, headers)
|
19
|
+
end
|
20
|
+
|
21
|
+
def reply_string(body, code=200, headers={"Content-type" => "text/html"})
|
22
|
+
Actor[:replier] << Reply.new(Actor.receive, body, code, headers)
|
23
|
+
end
|
24
|
+
|
25
|
+
module Aleph
|
26
|
+
class Base
|
27
|
+
class << self
|
28
|
+
attr_accessor :conn, :routes, :pairs
|
29
|
+
|
30
|
+
def new_uuid
|
31
|
+
values = [
|
32
|
+
rand(0x0010000),
|
33
|
+
rand(0x0010000),
|
34
|
+
rand(0x0010000),
|
35
|
+
rand(0x0010000),
|
36
|
+
rand(0x0010000),
|
37
|
+
rand(0x1000000),
|
38
|
+
rand(0x1000000),
|
39
|
+
]
|
40
|
+
"%04x%04x-%04x-%04x-%04x%06x%06x" % values
|
41
|
+
end
|
42
|
+
|
43
|
+
def get(path, &block)
|
44
|
+
(@pairs ||= []) << AddRoute.new(compile(path), block)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def compile(path)
|
49
|
+
keys = []
|
50
|
+
if path.respond_to? :to_str
|
51
|
+
pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) }
|
52
|
+
pattern.gsub!(/((:\w+)|\*)/) do |match|
|
53
|
+
if match == "*"
|
54
|
+
keys << 'splat'
|
55
|
+
"(.*?)"
|
56
|
+
else
|
57
|
+
keys << $2[1..-1]
|
58
|
+
"([^/?#]+)"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
[/^#{pattern}$/, keys]
|
62
|
+
elsif path.respond_to?(:keys) && path.respond_to?(:match)
|
63
|
+
[path, path.keys]
|
64
|
+
elsif path.respond_to?(:names) && path.respond_to?(:match)
|
65
|
+
[path, path.names]
|
66
|
+
elsif path.respond_to? :match
|
67
|
+
[path, keys]
|
68
|
+
else
|
69
|
+
raise TypeError, path
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def encoded(char)
|
74
|
+
enc = URI.encode(char)
|
75
|
+
enc = "(?:#{Regexp.escape enc}|#{URI.encode char, /./})" if enc == char
|
76
|
+
enc = "(?:#{enc}|#{encoded('+')})" if char == " "
|
77
|
+
enc
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Armstrong < Base
|
83
|
+
def self.run!
|
84
|
+
uuid = new_uuid
|
85
|
+
puts "starting Armstrong as mongrel2 service #{uuid}"
|
86
|
+
@conn = Connection.new uuid
|
87
|
+
@conn.connect
|
88
|
+
|
89
|
+
#ensure that all actors are launched. Yea.
|
90
|
+
done = Lazy::demand(Lazy::promise do |done|
|
91
|
+
Actor.spawn(&Aleph::Base.replier)
|
92
|
+
done = Lazy::demand(Lazy::promise do |done|
|
93
|
+
Actor.spawn(&Aleph::Base.request_handler)
|
94
|
+
done = Lazy::demand(Lazy::promise do |done|
|
95
|
+
Actor.spawn(&Aleph::Base.supervisor)
|
96
|
+
done = true
|
97
|
+
end)
|
98
|
+
end)
|
99
|
+
end)
|
100
|
+
|
101
|
+
Actor[:replier] << ConnectionInformation.new(@conn) if done
|
102
|
+
|
103
|
+
done = Lazy::demand(Lazy::Promise.new do |done|
|
104
|
+
Actor[:request_handler] << AddRoutes.new(@pairs)
|
105
|
+
done = true
|
106
|
+
end)
|
107
|
+
|
108
|
+
puts "","="*56,"Armstrong has launched on #{Time.now}","="*56, "" if done
|
109
|
+
|
110
|
+
puts "s:#{Actor[:supervisor]} h:#{Actor[:request_handler]} r: #{Actor[:replier]}"
|
111
|
+
# main loop
|
112
|
+
loop do
|
113
|
+
req = @conn.receive
|
114
|
+
puts "s:#{Actor[:supervisor]} h:#{Actor[:request_handler]} r: #{Actor[:replier]}"
|
115
|
+
Actor[:request_handler] << Request.new(req) if !req.nil?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# thank you sinatra!
|
121
|
+
# Sinatra delegation mixin. Mixing this module into an object causes all
|
122
|
+
# methods to be delegated to the Aleph::Armstrong class. Used primarily
|
123
|
+
# at the top-level.
|
124
|
+
module Delegator
|
125
|
+
def self.delegate(*methods)
|
126
|
+
methods.each do |method_name|
|
127
|
+
define_method(method_name) do |*args, &block|
|
128
|
+
return super(*args, &block) if respond_to? method_name
|
129
|
+
Delegator.target.send(method_name, *args, &block)
|
130
|
+
end
|
131
|
+
private method_name
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
delegate :get
|
136
|
+
|
137
|
+
class << self
|
138
|
+
attr_accessor :target
|
139
|
+
end
|
140
|
+
|
141
|
+
self.target = Armstrong
|
142
|
+
end
|
143
|
+
|
144
|
+
at_exit { Armstrong.run! }
|
145
|
+
end
|
146
|
+
|
147
|
+
include Aleph::Delegator
|
148
|
+
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: armstrong
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 2998277272998775549
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 1
|
10
|
+
version: 0.2.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Artem Titoulenko
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-11-06 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: ffi
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 4428665182548103036
|
29
|
+
segments:
|
30
|
+
- 1
|
31
|
+
- 0
|
32
|
+
version: "1.0"
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
hash: 4428537186080172355
|
36
|
+
segments:
|
37
|
+
- 1
|
38
|
+
- 0
|
39
|
+
- 10
|
40
|
+
version: 1.0.10
|
41
|
+
type: :runtime
|
42
|
+
version_requirements: *id001
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: ffi-rzmq
|
45
|
+
prerelease: false
|
46
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ~>
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 2854635824043747355
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
- 9
|
55
|
+
version: "0.9"
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 1509009585894205680
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
- 9
|
62
|
+
- 0
|
63
|
+
version: 0.9.0
|
64
|
+
type: :runtime
|
65
|
+
version_requirements: *id002
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: lazy
|
68
|
+
prerelease: false
|
69
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 2770302470405238287
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
- 9
|
78
|
+
- 6
|
79
|
+
version: 0.9.6
|
80
|
+
type: :runtime
|
81
|
+
version_requirements: *id003
|
82
|
+
description: Armstrong is an Mongrel2 fronted, actor-based web development framework similar in style to sinatra. With natively-threaded interpreters (Rubinius2), Armstrong provides true concurrency and high stability, by design.
|
83
|
+
email: artem.titoulenko@gmail.com
|
84
|
+
executables: []
|
85
|
+
|
86
|
+
extensions: []
|
87
|
+
|
88
|
+
extra_rdoc_files: []
|
89
|
+
|
90
|
+
files:
|
91
|
+
- README.md
|
92
|
+
- demo/armstrong_test.rb
|
93
|
+
- demo/mongrel2.conf
|
94
|
+
- lib/armstrong.rb
|
95
|
+
- lib/armstrong/connection.rb
|
96
|
+
- lib/armstrong/data_structures.rb
|
97
|
+
- lib/armstrong/main_actors.rb
|
98
|
+
homepage: https://www.github.com/artemtitoulenko/armstrong
|
99
|
+
licenses: []
|
100
|
+
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
hash: 2002549777813010636
|
112
|
+
segments:
|
113
|
+
- 0
|
114
|
+
version: "0"
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
hash: 2002549777813010636
|
121
|
+
segments:
|
122
|
+
- 0
|
123
|
+
version: "0"
|
124
|
+
requirements: []
|
125
|
+
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 1.8.11
|
128
|
+
signing_key:
|
129
|
+
specification_version: 3
|
130
|
+
summary: Highly concurrent, sinatra-like framework
|
131
|
+
test_files: []
|
132
|
+
|