angstrom 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ gem 'lazy'
2
+ gem 'open-uri'
3
+ gem 'ffi'
4
+ gem 'ffi-rzmq'
5
+ gem 'cgi'
@@ -0,0 +1,109 @@
1
+ # Angstrom #
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
+ #### Angstrom as a gem ####
21
+
22
+ gem install angstrom
23
+
24
+ #### ZMQ and other gems ####
25
+ gem install ffi-rzmq
26
+ gem install lazy
27
+
28
+ 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.
29
+
30
+ #### Mongrel2 ####
31
+ Finally, go grab a copy of mongrel2 (1.7.5 tested) from the [Mongrel2](http://mongrel2.org) website.
32
+
33
+ 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.
34
+
35
+ m2sh load -config mongrel2.conf -db config.sqlite
36
+ m2sh start -host localhost
37
+
38
+ ## minimal example ##
39
+
40
+ require 'angstrom'
41
+
42
+ get "/" do
43
+ "hello world"
44
+ end
45
+
46
+ 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.
47
+
48
+ Now you should run `ruby angstrom_test.rb` and then visit [localhost:6767](http://localhost:6767/) and relish in the 'Hello World'.
49
+
50
+ ## more functionality ##
51
+
52
+ commit e86c74aed added functionality for parameters in your path. These are simply demonstrated in the `demo/angstrom_test.rb` file. For instance, you can extract the id of a certain part of your path like so:
53
+
54
+ require 'angstrom'
55
+
56
+ get "/:id" do |env|
57
+ "id: #{env[:params]["id"]}"
58
+ end
59
+
60
+ The params are always going to be stored in `env`, naturally.
61
+
62
+ You can also return other codes and custom headers by returning an array with the signature:
63
+ [code, headers, response]
64
+
65
+ ## benchmarking ##
66
+
67
+ #### Armstrong ####
68
+ $ siege -d 1 -c 150 -t 10s localhost:6767/
69
+ ** SIEGE 2.70
70
+ ** Preparing 150 concurrent users for battle.
71
+ The server is now under siege...
72
+ Lifting the server siege... done.
73
+ Transactions: 5029 hits
74
+ Availability: 100.00 %
75
+ Elapsed time: 9.06 secs
76
+ Data transferred: 0.05 MB
77
+ Response time: 0.26 secs
78
+ Transaction rate: 555.08 trans/sec
79
+ Throughput: 0.01 MB/sec
80
+ Concurrency: 146.56
81
+ Successful transactions: 5029
82
+ Failed transactions: 0
83
+ Longest transaction: 0.67
84
+ Shortest transaction: 0.02
85
+
86
+ #### Sinatra ####
87
+
88
+ _These benchmarks were done using Rubinius as the Ruby interpreter. You will get much better results for sinatra with MRI 1.9.2 but the concurrency will still plateau at about 110. I could not start up more than 110 concurrent users without sinatra closing all connections and blowing up._
89
+
90
+ $ siege -d1 -c 110 -t 10s localhost:4567/
91
+ ** SIEGE 2.70
92
+ ** Preparing 20 concurrent users for battle.
93
+ The server is now under siege...
94
+ Lifting the server siege... done.
95
+ Transactions: 1192 hits
96
+ Availability: 97.23 %
97
+ Elapsed time: 9.39 secs
98
+ Data transferred: 0.01 MB
99
+ Response time: 0.70 secs
100
+ Transaction rate: 126.94 trans/sec
101
+ Throughput: 0.00 MB/sec
102
+ Concurrency: 88.98
103
+ Successful transactions: 1192
104
+ Failed transactions: 34
105
+ Longest transaction: 1.39
106
+ Shortest transaction: 0.20
107
+
108
+ ## License ##
109
+ GPLv3
@@ -0,0 +1,22 @@
1
+ desc "Push actors to github, switch to master, merge actors, push master to github"
2
+ namespace :super do
3
+ task :push do
4
+ `go master`
5
+ `git merge actors`
6
+ `git push github`
7
+ `go actors`
8
+ end
9
+ end
10
+
11
+ desc "Build gem"
12
+ task :gb do
13
+ `gem build angstrom.gemspec`
14
+ end
15
+
16
+ desc "Push gem"
17
+ task :gp do
18
+ Rake::Task[:gb].invoke
19
+ gem_file = `ls *.gem`.to_a.last.chomp
20
+ puts "pushing #{gem_file}"
21
+ puts `gem push #{gem_file}`
22
+ end
@@ -0,0 +1,16 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+ require 'angstrom/version'
3
+
4
+ Gem::Specification.new 'angstrom', Aleph::VERSION do |s|
5
+ s.description = "Angstrom is an Mongrel2 fronted, actor-based web development framework similar in style to sinatra. With natively-threaded interpreters (Rubinius2), Angstrom provides true concurrency and high stability, by design."
6
+ s.summary = "Highly concurrent, sinatra-like framework"
7
+ s.author = "Artem Titoulenko"
8
+ s.email = "artem.titoulenko@gmail.com"
9
+ s.homepage = "https://www.github.com/artemtitoulenko/angstrom"
10
+ s.files = `git ls-files`.split("\n") - %w[.gitignore .travis.yml response_benchmark.rb demo/config.sqlite]
11
+ s.executables = %w[ angstrom ]
12
+
13
+ s.add_dependency 'ffi', '~> 1.0', '>= 1.0.10'
14
+ s.add_dependency 'ffi-rzmq', '~> 0.9', '>= 0.9.0'
15
+ s.add_dependency 'lazy', '>= 0.9.6'
16
+ end
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ include FileUtils
5
+
6
+ # this is a cli tool to make getting running with angstrom easier.
7
+ def inform
8
+ puts """
9
+ Angstrom is an asynchronous ruby web framework that's fronted by mongrel2
10
+ and makes use of Actors in order to handle requests. It is preferred to use
11
+ rubinius2.0.0dev in order to take advantage of true concurrency in Ruby.
12
+
13
+ usage: angstrom <command>
14
+
15
+ commands:
16
+ create <name> [port] Creates an angstrom app with a sample
17
+ mongrel2.conf and config.sqlite running on
18
+ [port] or the default 6767.
19
+
20
+ start [host] [db.sqlite] Starts a mongrel2 server in this directory
21
+ and then runs the app called by the current
22
+ directorys name. This is equivalent to:
23
+ $m2sh start -host localhost -db config.sqlite >> /dev/null &
24
+ $ruby app.rb
25
+
26
+ stop [host] Kills the default running mongrel2
27
+ server or [host]. This is like:
28
+ $m2sh stop -host localhost
29
+
30
+ short commands:
31
+ c = create
32
+ s = start
33
+ t = stop
34
+ """
35
+ end
36
+
37
+ mongrel2_conf = """
38
+ angstrom_handler = Handler(
39
+ send_spec='tcp://127.0.0.1:9999',
40
+ send_ident='34f9ceee-cd52-4b7f-b197-88bf2f0ec378',
41
+ recv_spec='tcp://127.0.0.1:9998',
42
+ recv_ident='')
43
+
44
+ media_dir = Dir(
45
+ base='media/',
46
+ index_file='index.html',
47
+ default_ctype='text/plain')
48
+
49
+ angstrom_host = Host(
50
+ name=\"localhost\",
51
+ routes={
52
+ '/media/': media_dir,
53
+ '/': angstrom_handler})
54
+
55
+ angstrom_serv = Server(
56
+ uuid=\"%s\",
57
+ access_log=\"/log/mongrel2.access.log\",
58
+ error_log=\"/log/mongrel2.error.log\",
59
+ chroot=\"./\",
60
+ default_host=\"localhost\",
61
+ name=\"angstrom test\",
62
+ pid_file=\"/run/mongrel2.pid\",
63
+ port=%i,
64
+ hosts = [angstrom_host]
65
+ )
66
+
67
+ settings = {\"zeromq.threads\": 2, \"limits.min_ping\": 15, \"limits.kill_limit\": 2}
68
+
69
+ servers = [angstrom_serv]
70
+ """
71
+
72
+ inform if ARGV.empty?
73
+
74
+ case ARGV[0]
75
+ when 'create', 'c'
76
+ inform if ARGV[1].nil?
77
+ port = (ARGV[2].nil? ? 6767 : ARGV[2])
78
+ mkdir ARGV[1]
79
+ cd ARGV[1]
80
+ %w[log run tmp].map(&method(:mkdir))
81
+ File.open("mongrel2.conf", 'w') {|file| file.puts(mongrel2_conf % [`m2sh uuid`.chomp, port]) }
82
+ puts "loading in the mongrel2.conf config"
83
+ puts `m2sh load -config mongrel2.conf -db config.sqlite`
84
+ File.open("#{ARGV[1]}.rb", 'w') {|file| file.puts "require 'rubygems'\nrequire 'angstrom'\n\n"}
85
+ puts "Created app #{ARGV[1]} that will run on port #{port}"
86
+
87
+ when 'start', 's'
88
+ host = (ARGV[1].nil? ? 'localhost' : ARGV[1])
89
+ db = (ARGV[2].nil? ? 'config.sqlite' : ARGV[2])
90
+
91
+ output = `m2sh start -host #{host} -config #{db} > /dev/null &`
92
+ if output.match /Aborting/
93
+ puts "Error starting up mongrel2.\n\nTrace:\n#{output}"
94
+ break
95
+ end
96
+ puts "Started #{host} using the #{db} db"
97
+
98
+ file = Dir.pwd.split('/').last
99
+ puts "now running #{file}.rb"
100
+ puts `ruby #{file}.rb`
101
+
102
+ when 'stop', 't'
103
+ host = (ARGV[1].nil? ? 'localhost' : ARGV[1])
104
+ puts `m2sh stop -host #{host}`
105
+ puts "Stopped #{host}, I think"
106
+ else
107
+ inform
108
+ end
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ include FileUtils
5
+
6
+ # this is a cli tool to make getting running with armstrong easier.
7
+ def inform
8
+ puts """
9
+ Armstrong is an asynchronous ruby web framework that's fronted by mongrel2
10
+ and makes use of Actors in order to handle requests. It is preferred to use
11
+ rubinius2.0.0dev in order to take advantage of true concurrency in Ruby.
12
+
13
+ usage: armstrong <command>
14
+
15
+ commands:
16
+ create <name> [port] Creates an armstrong app with a sample
17
+ mongrel2.conf and config.sqlite running on
18
+ [port] or the default 6767.
19
+
20
+ start [host] [db.sqlite] Starts a mongrel2 server in this directory
21
+ and then runs the app called by the current
22
+ directorys name. This is equivalent to:
23
+ $m2sh start -host localhost -db config.sqlite >> /dev/null &
24
+ $ruby app.rb
25
+
26
+ stop [host] Kills the default running mongrel2
27
+ server or [host]. This is like:
28
+ $m2sh stop -host localhost
29
+
30
+ short commands:
31
+ c = create
32
+ s = start
33
+ t = stop
34
+ """
35
+ end
36
+
37
+ mongrel2_conf = """
38
+ armstrong_handler = Handler(
39
+ send_spec='tcp://127.0.0.1:9999',
40
+ send_ident='34f9ceee-cd52-4b7f-b197-88bf2f0ec378',
41
+ recv_spec='tcp://127.0.0.1:9998',
42
+ recv_ident='')
43
+
44
+ media_dir = Dir(
45
+ base='media/',
46
+ index_file='index.html',
47
+ default_ctype='text/plain')
48
+
49
+ armstrong_host = Host(
50
+ name=\"localhost\",
51
+ routes={
52
+ '/media/': media_dir,
53
+ '/': armstrong_handler})
54
+
55
+ armstrong_serv = Server(
56
+ uuid=\"%s\",
57
+ access_log=\"/log/mongrel2.access.log\",
58
+ error_log=\"/log/mongrel2.error.log\",
59
+ chroot=\"./\",
60
+ default_host=\"localhost\",
61
+ name=\"armstrong test\",
62
+ pid_file=\"/run/mongrel2.pid\",
63
+ port=%i,
64
+ hosts = [armstrong_host]
65
+ )
66
+
67
+ settings = {\"zeromq.threads\": 2, \"limits.min_ping\": 15, \"limits.kill_limit\": 2}
68
+
69
+ servers = [armstrong_serv]
70
+ """
71
+
72
+ inform if ARGV.empty?
73
+
74
+ case ARGV[0]
75
+ when 'create', 'c'
76
+ inform if ARGV[1].nil?
77
+ port = (ARGV[2].nil? ? 6767 : ARGV[2])
78
+ mkdir ARGV[1]
79
+ cd ARGV[1]
80
+ %w[log run tmp].map(&method(:mkdir))
81
+ File.open("mongrel2.conf", 'w') {|file| file.puts(mongrel2_conf % [`m2sh uuid`.chomp, port]) }
82
+ puts "loading in the mongrel2.conf config"
83
+ puts `m2sh load -config mongrel2.conf -db config.sqlite`
84
+ File.open("#{ARGV[1]}.rb", 'w') {|file| file.puts "require 'rubygems'\nrequire 'armstrong'\n\n"}
85
+ puts "Created app #{ARGV[1]} that will run on port #{port}"
86
+
87
+ when 'start', 's'
88
+ host = (ARGV[1].nil? ? 'localhost' : ARGV[1])
89
+ db = (ARGV[2].nil? ? 'config.sqlite' : ARGV[2])
90
+
91
+ output = `m2sh start -host #{host} -config #{db} > /dev/null &`
92
+ if output.match /Aborting/
93
+ puts "Error starting up mongrel2.\n\nTrace:\n#{output}"
94
+ break
95
+ end
96
+ puts "Started #{host} using the #{db} db"
97
+
98
+ file = Dir.pwd.split('/').last
99
+ puts "now running #{file}.rb"
100
+ puts `ruby #{file}.rb`
101
+
102
+ when 'stop', 't'
103
+ host = (ARGV[1].nil? ? 'localhost' : ARGV[1])
104
+ puts `m2sh stop -host #{host}`
105
+ puts "Stopped #{host}, I think"
106
+ else
107
+ inform
108
+ end
@@ -0,0 +1,32 @@
1
+ angstrom_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
+ angstrom_host = Host(
13
+ name="localhost",
14
+ routes={
15
+ '/media/': media_dir,
16
+ '/': angstrom_handler})
17
+
18
+ angstrom_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="angstrom test",
25
+ pid_file="/run/mongrel2.pid",
26
+ port=6767,
27
+ hosts = [angstrom_host]
28
+ )
29
+
30
+ settings = {"zeromq.threads": 2, "limits.min_ping": 15, "limits.kill_limit": 2}
31
+
32
+ servers = [angstrom_serv]
@@ -0,0 +1,130 @@
1
+ require 'actor'
2
+ require 'rubygems'
3
+ require 'lazy'
4
+
5
+ libdir = File.dirname(__FILE__)
6
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
7
+
8
+ require "angstrom/connection"
9
+ require 'angstrom/data_structures'
10
+ require 'angstrom/main_actors'
11
+
12
+ module Aleph
13
+ class Base
14
+ class << self
15
+ attr_accessor :conn, :routes
16
+
17
+ def get(path, &block) route "GET", path, &block end
18
+ def put(path, &block) route "PUT", path, &block end
19
+ def post(path, &block) route "POST", path, &block end
20
+ def head(path, &block) route "HEAD", path, &block end
21
+ def delete(path, &block) route "DELETE", path, &block end
22
+ def patch(path, &block) route "PATCH", path, &block end
23
+
24
+ def route(verb, path, &block)
25
+ @routes ||= {}
26
+ (@routes[verb] ||= []) << AddRoute.new(compile(path), block)
27
+ end
28
+
29
+ private
30
+ def compile(path)
31
+ keys = []
32
+ if path.respond_to? :to_str
33
+ pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) }
34
+ pattern.gsub!(/((:\w+)|\*)/) do |match|
35
+ if match == "*"
36
+ keys << 'splat'
37
+ "(.*?)"
38
+ else
39
+ keys << $2[1..-1]
40
+ "([^/?#]+)"
41
+ end
42
+ end
43
+ [/^#{pattern}$/, keys]
44
+ elsif path.respond_to?(:keys) && path.respond_to?(:match)
45
+ [path, path.keys]
46
+ elsif path.respond_to?(:names) && path.respond_to?(:match)
47
+ [path, path.names]
48
+ elsif path.respond_to? :match
49
+ [path, keys]
50
+ else
51
+ raise TypeError, path
52
+ end
53
+ end
54
+
55
+ def encoded(char)
56
+ enc = URI.encode(char)
57
+ enc = "(?:#{Regexp.escape enc}|#{URI.encode char, /./})" if enc == char
58
+ enc = "(?:#{enc}|#{encoded('+')})" if char == " "
59
+ enc
60
+ end
61
+ end
62
+ end
63
+
64
+ class Angstrom < Base
65
+
66
+ # the kicker. It all gets launched from here.
67
+ # this function makes a new connection object to handle the communication,
68
+ # promises to start the replier, request handler, and their supervisor,
69
+ # gives the replier the connection information, tells the request_handler
70
+ # what routes it should be able to match, then checks that all of the services
71
+ # are running correctly, gives us a launch time, then jumps into our main loop
72
+ # that waits for an incoming message, parses it, and sends it off to be
73
+ # operated on by the request handler. Boom.
74
+ def self.run!
75
+ #ensure that all actors are launched. Yea.
76
+ done = Lazy::demand(Lazy::promise do |done|
77
+ Actor.spawn(&Aleph::Base.supervisor_proc)
78
+ done = true
79
+ end)
80
+
81
+ if done
82
+ done2 = Lazy::demand(Lazy::Promise.new do |done2|
83
+ Actor[:supervisor] << SpawnRequestHandlers.new(4)
84
+ Actor[:supervisor] << SpawnReceivers.new(1)
85
+ Actor[:supervisor] << AddRoutes.new(@routes)
86
+ done2 = true
87
+ end)
88
+ end
89
+
90
+ if Aleph::Base.supervisor && Aleph::Base.replier && done2
91
+ puts "","="*56,"Angstrom has launched on #{Time.now}","="*56, ""
92
+ end
93
+
94
+ # main loop
95
+ loop do
96
+ gets
97
+ end
98
+ end
99
+ end
100
+
101
+ # thank you sinatra!
102
+ # Sinatra delegation mixin. Mixing this module into an object causes all
103
+ # methods to be delegated to the Aleph::Angstrom class. Used primarily
104
+ # at the top-level.
105
+ module Delegator
106
+ def self.delegate(*methods)
107
+ methods.each do |method_name|
108
+ define_method(method_name) do |*args, &block|
109
+ return super(*args, &block) if respond_to? method_name
110
+ Delegator.target.send(method_name, *args, &block)
111
+ end
112
+ private method_name
113
+ end
114
+ end
115
+
116
+ delegate :get, :post, :put, :patch, :delete, :head
117
+
118
+ class << self
119
+ attr_accessor :target
120
+ end
121
+
122
+ self.target = Angstrom
123
+ end
124
+
125
+ # Sinatras secret sauce.
126
+ at_exit { Angstrom.run! }
127
+ end
128
+
129
+ include Aleph::Delegator
130
+
@@ -0,0 +1,142 @@
1
+ require 'ffi'
2
+ require 'ffi-rzmq'
3
+ require 'json'
4
+ require 'cgi'
5
+
6
+ class Connection
7
+ attr_reader :app_id, :sub_addr, :pub_addr, :request_sock, :response_sock, :context
8
+
9
+ def initialize(app_id, zmq_sub_pub_addr=["tcp://127.0.0.1", 9999, "tcp://127.0.0.1", 9998])
10
+ @app_id = app_id
11
+ @sub_addr = zmq_sub_pub_addr[0..1].join(":")
12
+ @pub_addr = zmq_sub_pub_addr[2..3].join(":")
13
+
14
+ @request_sock = @response_sock = nil
15
+ end
16
+
17
+ def connect
18
+ @context = ZMQ::Context.new 1
19
+ @request_sock = @context.socket ZMQ::PULL
20
+ @request_sock.connect @sub_addr
21
+
22
+ @response_sock = @context.socket ZMQ::PUB
23
+ @response_sock.setsockopt ZMQ::IDENTITY, @app_id
24
+ @response_sock.connect @pub_addr
25
+ end
26
+
27
+ #raw recv, unparsed message
28
+ def recv
29
+ msg = ""
30
+ rc = @request_sock.recv_string(msg)
31
+ puts "errno [#{ZMQ::Util.errno}] with description [#{ZMQ::Util.error_string}]" unless ZMQ::Util.resultcode_ok?(rc)
32
+ msg
33
+ end
34
+
35
+ #parse the request, this is the best way to get stuff back, as a Hash
36
+ def receive
37
+ parse(recv)
38
+ end
39
+
40
+ # sends the message off, formatted for Mongrel2 to understand
41
+ def send(uuid, conn_id, msg)
42
+ header = "%s %d:%s" % [uuid, conn_id.join(' ').length, conn_id.join(' ')]
43
+ string = header + ', ' + msg
44
+ #puts "\t\treplying to #{conn_id} with: ", string
45
+ rc = @response_sock.send_string string, ZMQ::NOBLOCK
46
+ puts "errno [#{ZMQ::Util.errno}] with description [#{ZMQ::Util.error_string}]" unless ZMQ::Util.resultcode_ok?(rc)
47
+ end
48
+
49
+ # reply to an env with `message` string
50
+ def reply(env, message)
51
+ self.send(env[:sender], [env[:conn_id]], message)
52
+ end
53
+
54
+ # reply to a req with a valid http header
55
+ def reply_http(env, body, code=200, headers={"Content-type" => "text/html"})
56
+ self.reply(env, http_response(body, code, headers))
57
+ end
58
+
59
+ private
60
+ def http_response(body, code, headers)
61
+ headers['Content-Length'] = body.size
62
+ headers_s = headers.map{|k, v| "%s: %s" % [k,v]}.join("\r\n")
63
+
64
+ "HTTP/1.1 #{code} #{StatusMessage[code.to_i]}\r\n#{headers_s}\r\n\r\n#{body}"
65
+ end
66
+
67
+ def parse_netstring(ns)
68
+ len, rest = ns.split(':', 2)
69
+ len = len.to_i
70
+ raise "Netstring did not end in ','" unless rest[len].chr == ','
71
+ [ rest[0...len], rest[(len+1)..-1] ]
72
+ end
73
+
74
+ def parse(msg)
75
+ if msg.nil? || msg.empty?
76
+ return nil
77
+ end
78
+
79
+ env = {}
80
+ env[:sender], env[:conn_id], env[:path], rest = msg.split(' ', 4)
81
+ env[:headers], head_rest = parse_netstring(rest)
82
+ env[:body], _ = parse_netstring(head_rest)
83
+
84
+ env[:headers] = JSON.parse(env[:headers])
85
+ if(env[:headers]["METHOD"] == "POST")
86
+ env[:post] = parse_params(env)
87
+ end
88
+
89
+ return env
90
+ end
91
+
92
+ def parse_params(env)
93
+ r = {}
94
+ env[:body].split('&').map{|x| x.scan(/(.*?)=(.*?)$/)}.each_slice(2) { |k| r[CGI::unescape(k[0].to_s)] = CGI::unescape(k[1]) }
95
+ return r
96
+ end
97
+
98
+ # From WEBrick: thanks dawg.
99
+ StatusMessage = {
100
+ 100 => 'Continue',
101
+ 101 => 'Switching Protocols',
102
+ 200 => 'OK',
103
+ 201 => 'Created',
104
+ 202 => 'Accepted',
105
+ 203 => 'Non-Authoritative Information',
106
+ 204 => 'No Content',
107
+ 205 => 'Reset Content',
108
+ 206 => 'Partial Content',
109
+ 300 => 'Multiple Choices',
110
+ 301 => 'Moved Permanently',
111
+ 302 => 'Found',
112
+ 303 => 'See Other',
113
+ 304 => 'Not Modified',
114
+ 305 => 'Use Proxy',
115
+ 307 => 'Temporary Redirect',
116
+ 400 => 'Bad Request',
117
+ 401 => 'Unauthorized',
118
+ 402 => 'Payment Required',
119
+ 403 => 'Forbidden',
120
+ 404 => 'Not Found',
121
+ 405 => 'Method Not Allowed',
122
+ 406 => 'Not Acceptable',
123
+ 407 => 'Proxy Authentication Required',
124
+ 408 => 'Request Timeout',
125
+ 409 => 'Conflict',
126
+ 410 => 'Gone',
127
+ 411 => 'Length Required',
128
+ 412 => 'Precondition Failed',
129
+ 413 => 'Request Entity Too Large',
130
+ 414 => 'Request-URI Too Large',
131
+ 415 => 'Unsupported Media Type',
132
+ 416 => 'Request Range Not Satisfiable',
133
+ 417 => 'Expectation Failed',
134
+ 500 => 'Internal Server Error',
135
+ 501 => 'Not Implemented',
136
+ 502 => 'Bad Gateway',
137
+ 503 => 'Service Unavailable',
138
+ 504 => 'Gateway Timeout',
139
+ 505 => 'HTTP Version Not Supported'
140
+ }
141
+ end
142
+
@@ -0,0 +1,10 @@
1
+ AddRoute = Struct.new :route, :method
2
+ AddRoutes = Struct.new :routes
3
+ Request = Struct.new :env
4
+ ConnectionInformation = Struct.new :connection
5
+ Reply = Struct.new :env, :code, :headers, :body
6
+ MessageAndProc = Struct.new :env, :proccess
7
+
8
+ SpawnRequestHandlers = Struct.new :num
9
+ SpawnReceivers = Struct.new :num
10
+ Num = Struct.new :index
@@ -0,0 +1,183 @@
1
+ module Aleph
2
+ class Base
3
+ class << self
4
+ attr_accessor :replier, :supervisor
5
+ attr_accessor :message_receiver_proc, :request_handler_proc, :supervisor_proc, :container_proc
6
+ end
7
+ end
8
+ end
9
+
10
+ # take the route and pattern and keys and this function will match the keyworded params in
11
+ # the url with the pattern. Example:
12
+ #
13
+ # url: /user/2/view/345
14
+ # pattern: /user/:id/view/:comment
15
+ #
16
+ # returns:
17
+ #
18
+ # params = {id: 2, comment: 345}
19
+ #
20
+ def process_route(route, pattern, keys, values = [])
21
+ return unless match = pattern.match(route)
22
+ values += match.captures.map { |v| URI.decode(v) if v }
23
+ params = {}
24
+
25
+ if values.any?
26
+ keys.zip(values) { |k,v| (params[k] ||= '') << v if v }
27
+ end
28
+ params
29
+ end
30
+
31
+ # uuid generator. There's a pretty low chance of collision.
32
+ def new_uuid
33
+ values = [
34
+ rand(0x0010000),
35
+ rand(0x0010000),
36
+ rand(0x0010000),
37
+ rand(0x0010000),
38
+ rand(0x0010000),
39
+ rand(0x1000000),
40
+ rand(0x1000000),
41
+ ]
42
+ "%04x%04x-%04x-%04x-%04x%06x%06x" % values
43
+ end
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+ # this nifty mess helps us just to use the output of the Procs that handle
52
+ # the request instead of making the user manually catch messages and send
53
+ # them out to the replier.
54
+ Aleph::Base.container_proc = Proc.new do
55
+ data = Actor.receive
56
+ env, proccess = data.env, data.proccess
57
+ response = proccess.call(env)
58
+ puts "[container] using response: ", response
59
+ if response.is_a? Array
60
+ #just like Rack: env, code, headers, body. HINT HINT ( can't work because it's all async )
61
+ env[:conn].reply_http(env, response[1], response[2], response[0])
62
+ else
63
+ puts "[container] sending http_reply"
64
+ env[:conn].reply_http(env, response, 200, {"Content-Type", "text/html;charset=utf-8", "Connection", "keep-alive", "Server", "Angstrom", "X-Frame-Options", "sameorigin", "X-XSS_Protection", "1; mode=block"})
65
+ end
66
+ end
67
+
68
+ Aleph::Base.message_receiver_proc = Proc.new do
69
+ @name = "message_receiver"
70
+ puts "started (#{@name})"
71
+
72
+ uuid = new_uuid
73
+ conn = Connection.new uuid
74
+ conn.connect
75
+ puts "replying as mongrel2 service #{uuid}"
76
+
77
+ loop do
78
+ puts "waiting..."
79
+ env = conn.receive
80
+ env[:conn] = conn
81
+ puts "[message_receiver] got request, req = #{!req.nil?}"
82
+ Actor[:supervisor] << Request.new(env) if !req.nil?
83
+ end
84
+ end
85
+
86
+
87
+
88
+ Aleph::Base.request_handler_proc = Proc.new do
89
+ @name = "request_handler"
90
+ @num = 0
91
+ puts "started (#{@name})"
92
+ Actor.trap_exit = true
93
+
94
+ routes = {}
95
+ loop do
96
+ Actor.receive do |f|
97
+ f.when(Num) do |n|
98
+ @num = n.index
99
+ end
100
+
101
+ f.when(AddRoutes) do |r|
102
+ routes = r.routes
103
+ end
104
+
105
+ f.when(Request) do |r|
106
+ failure = true
107
+ verb = r.env[:headers]["METHOD"]
108
+ routes[verb].each do |route|
109
+ if route.route[0].match(r.env[:path])
110
+ puts "[request_handler:#{@num}] route matched! Making container..."
111
+ r.env[:params] = process_route(r.env[:path], route.route[0], route.route[1])
112
+ Actor.spawn(&Aleph::Base.container_proc) << MessageAndProc.new(r.env, route.method)
113
+ failure = false
114
+ break
115
+ end
116
+ end
117
+ env[:conn].reply_http(r.env, "<h1>404</h1>", 404, {'Content-type'=>'text/html'} ) if failure
118
+ end
119
+
120
+ f.when(Actor::DeadActorError) do |exit|
121
+ puts "[request_handler] #{exit.actor} died with reason: [#{exit.reason}]"
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ #if this dies, all hell will break loose
128
+ Aleph::Base.supervisor_proc = Proc.new do
129
+ Actor.register(:supervisor, Actor.current)
130
+ Aleph::Base.supervisor = Actor.current
131
+ Actor.trap_exit = true
132
+ puts "started (supervisor)"
133
+
134
+ handlers = []
135
+ receivers = []
136
+ handler_turn = 0
137
+ receiver_turn = 0
138
+
139
+ loop do
140
+ Actor.receive do |f|
141
+
142
+ f.when(AddRoutes) do |r|
143
+ puts "Adding routes"
144
+ handlers.each { |h| h << r }
145
+ end
146
+
147
+ f.when(SpawnRequestHandlers) do |r|
148
+ r.num.times do
149
+ puts "spawning a request_handler in handlers[#{handlers.size}]"
150
+ handlers << (Actor.spawn_link(&Aleph::Base.request_handler_proc) << Num.new(handlers.size))
151
+ end
152
+ end
153
+
154
+ f.when(Request) do |req|
155
+ puts "[supervisor] routing request"
156
+ handlers[handler_turn] << req
157
+ if(handler_turn == handlers.size)
158
+ handler_turn = 0
159
+ else
160
+ handler_turn += 1
161
+ end
162
+ end
163
+
164
+ f.when(SpawnReceivers) do |r|
165
+ r.num.times do
166
+ puts "spawning a receiver in receivers[#{receivers.size}]"
167
+ receivers << Actor.spawn_link(&Aleph::Base.message_receiver_proc)
168
+ end
169
+ end
170
+
171
+ f.when(Actor::DeadActorError) do |exit|
172
+ "[supervisor] #{exit.actor} died with reason: [#{exit.reason}]"
173
+ # case exit.actor.name
174
+ # when "request_handler"
175
+ # # lets replace that failed request_handler with a new one. NO DOWN TIME
176
+ # handlers[exit.actor.num] = (Actor.spawn_link(&Aleph::Base.request_handler_proc) << Num.new(exit.actor.num))
177
+ # when "message_receiver"
178
+ # repliers[exit.actor.num] = (Actor.spawn_link(&Aleph::Base.message_receiver_proc) << Num.new(exit.actor.num))
179
+ # end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,3 @@
1
+ module Aleph
2
+ VERSION = "0.5.0"
3
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: angstrom
3
+ version: !ruby/object:Gem::Version
4
+ hash: 1694540110752149318
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 5
9
+ - 0
10
+ version: 0.5.0
11
+ platform: ruby
12
+ authors:
13
+ - Artem Titoulenko
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-12-01 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: Angstrom is an Mongrel2 fronted, actor-based web development framework similar in style to sinatra. With natively-threaded interpreters (Rubinius2), Angstrom provides true concurrency and high stability, by design.
83
+ email: artem.titoulenko@gmail.com
84
+ executables:
85
+ - angstrom
86
+ extensions: []
87
+
88
+ extra_rdoc_files: []
89
+
90
+ files:
91
+ - Gemfile
92
+ - README.md
93
+ - Rakefile
94
+ - angstrom.gemspec
95
+ - bin/angstrom
96
+ - bin/armstrong
97
+ - demo/mongrel2.conf
98
+ - lib/angstrom.rb
99
+ - lib/angstrom/connection.rb
100
+ - lib/angstrom/data_structures.rb
101
+ - lib/angstrom/main_actors.rb
102
+ - lib/angstrom/version.rb
103
+ homepage: https://www.github.com/artemtitoulenko/angstrom
104
+ licenses: []
105
+
106
+ post_install_message:
107
+ rdoc_options: []
108
+
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ hash: 2002549777813010636
117
+ segments:
118
+ - 0
119
+ version: "0"
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ hash: 2002549777813010636
126
+ segments:
127
+ - 0
128
+ version: "0"
129
+ requirements: []
130
+
131
+ rubyforge_project:
132
+ rubygems_version: 1.8.11
133
+ signing_key:
134
+ specification_version: 3
135
+ summary: Highly concurrent, sinatra-like framework
136
+ test_files: []
137
+