armstrong 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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,6 @@
1
+ AddRoute = Struct.new :route, :method
2
+ AddRoutes = Struct.new :routes
3
+ ShowRoutes = Struct.new :this
4
+ Request = Struct.new :data
5
+ ConnectionInformation = Struct.new :connection
6
+ Reply = Struct.new :data, :body, :code, :headers
@@ -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
+