htttee 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +2 -0
  3. data/README.md +105 -0
  4. data/Rakefile +14 -0
  5. data/bin/htttee +39 -0
  6. data/bin/htttee-exec +18 -0
  7. data/config.ru +6 -0
  8. data/deploy/after_restart.rb +1 -0
  9. data/deploy/before_restart.rb +4 -0
  10. data/deploy/before_symlink.rb +2 -0
  11. data/deploy/cookbooks/cloudkick-plugins/recipes/default.rb +8 -0
  12. data/deploy/cookbooks/cloudkick-plugins/recipes/resque.rb +18 -0
  13. data/deploy/cookbooks/cloudkick-plugins/templates/default/resque.sh.erb +4 -0
  14. data/deploy/cookbooks/god/recipes/default.rb +50 -0
  15. data/deploy/cookbooks/god/templates/default/config.erb +3 -0
  16. data/deploy/cookbooks/god/templates/default/god-inittab.erb +3 -0
  17. data/deploy/cookbooks/main/attributes/owner_name.rb +3 -0
  18. data/deploy/cookbooks/main/attributes/recipes.rb +1 -0
  19. data/deploy/cookbooks/main/libraries/dnapi.rb +7 -0
  20. data/deploy/cookbooks/main/recipes/default.rb +2 -0
  21. data/deploy/cookbooks/nginx/files/default/chunkin-nginx-module-v0.22rc1.zip +0 -0
  22. data/deploy/cookbooks/nginx/files/default/nginx-1.0.0.tar.gz +0 -0
  23. data/deploy/cookbooks/nginx/recipes/default.rb +2 -0
  24. data/deploy/cookbooks/nginx/recipes/install.rb +0 -0
  25. data/deploy/cookbooks/nginx/templates/default/nginx.conf.erb +41 -0
  26. data/deploy/cookbooks/resque/recipes/default.rb +47 -0
  27. data/deploy/cookbooks/resque/templates/default/resque.rb.erb +73 -0
  28. data/deploy/cookbooks/resque/templates/default/resque.yml.erb +3 -0
  29. data/deploy/cookbooks/resque/templates/default/resque_scheduler.rb.erb +73 -0
  30. data/deploy/solo.rb +7 -0
  31. data/htttee.gemspec +30 -0
  32. data/lib/htttee.rb +0 -0
  33. data/lib/htttee/client.rb +16 -0
  34. data/lib/htttee/client/consumer.rb +49 -0
  35. data/lib/htttee/client/ext/net/http.rb +27 -0
  36. data/lib/htttee/server.rb +74 -0
  37. data/lib/htttee/server/api.rb +201 -0
  38. data/lib/htttee/server/chunked_body.rb +22 -0
  39. data/lib/htttee/server/ext/em-redis.rb +49 -0
  40. data/lib/htttee/server/ext/thin.rb +4 -0
  41. data/lib/htttee/server/ext/thin/connection.rb +8 -0
  42. data/lib/htttee/server/ext/thin/deferrable_body.rb +24 -0
  43. data/lib/htttee/server/ext/thin/deferred_request.rb +31 -0
  44. data/lib/htttee/server/ext/thin/deferred_response.rb +4 -0
  45. data/lib/htttee/server/middleware/async_fixer.rb +22 -0
  46. data/lib/htttee/server/middleware/dechunker.rb +63 -0
  47. data/lib/htttee/server/mock.rb +69 -0
  48. data/lib/htttee/server/pubsub_redis.rb +78 -0
  49. data/lib/htttee/version.rb +5 -0
  50. data/spec/client_spec.rb +126 -0
  51. data/spec/helpers/rackup.rb +15 -0
  52. data/spec/spec_helper.rb +21 -0
  53. metadata +201 -0
@@ -0,0 +1,22 @@
1
+ module EY
2
+ module Tea
3
+ module Server
4
+ class ChunkedBody < Thin::DeferrableBody
5
+ def call(body)
6
+ body.each do |fragment|
7
+ @body_callback.call(chunk(fragment))
8
+ end
9
+ end
10
+
11
+ def succeed(*a)
12
+ @body_callback.call("0\r\n\r\n")
13
+ super
14
+ end
15
+
16
+ def chunk(fragment)
17
+ "#{fragment.size.to_s(16)}\r\n#{fragment}\r\n"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ module EventMachine
2
+ module Protocols
3
+ module Redis
4
+
5
+ def pipeline(*commands, &blk)
6
+ command = ''
7
+
8
+ commands.each do |argv|
9
+ command << "*#{argv.size}\r\n"
10
+ argv.each do |a|
11
+ a = a.to_s
12
+ command << "$#{get_size(a)}\r\n"
13
+ command << a
14
+ command << "\r\n"
15
+ end
16
+ end
17
+
18
+ maybe_lock do
19
+ commands.map {|c| c.first }.each do |command_name|
20
+ @redis_callbacks << [REPLY_PROCESSOR[command_name], blk]
21
+ end
22
+ send_data command
23
+ end
24
+ end
25
+
26
+ def multi(*command_groups, &blk)
27
+
28
+ command = "*1\r\n$5\r\nMULTI\r\n"
29
+
30
+ command_groups.each do |argv|
31
+ command << "*#{argv.size}\r\n"
32
+ argv.each do |a|
33
+ a = a.to_s
34
+ command << "$#{get_size(a)}\r\n"
35
+ command << a
36
+ command << "\r\n"
37
+ end
38
+ end
39
+
40
+ command << "*1\r\n$4\r\nEXEC\r\n"
41
+
42
+ maybe_lock do
43
+ @redis_callbacks << [REPLY_PROCESSOR['multi'], blk]
44
+ send_data command
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,4 @@
1
+ require 'htttee/server/ext/thin/connection'
2
+ require 'htttee/server/ext/thin/deferrable_body'
3
+ require 'htttee/server/ext/thin/deferred_request'
4
+ require 'htttee/server/ext/thin/deferred_response'
@@ -0,0 +1,8 @@
1
+ module Thin
2
+ class Connection
3
+ def post_init
4
+ @request = DeferredRequest.new
5
+ @response = DeferredResponse.new
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,24 @@
1
+ module Thin
2
+ class DeferrableBody
3
+ include EventMachine::Deferrable
4
+
5
+ def initialize(initial_body = '')
6
+ @initial_body = initial_body.to_s
7
+ end
8
+
9
+ def call(body)
10
+ body.each do |chunk|
11
+ @body_callback.call(chunk)
12
+ end
13
+ end
14
+
15
+ def <<(*chunks)
16
+ call(chunks)
17
+ end
18
+
19
+ def each(&blk)
20
+ blk.call(@initial_body) unless @initial_body.empty?
21
+ @body_callback = blk
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ module Thin
2
+ class DeferredRequest < Request
3
+ def parse(data)
4
+ if @parser.finished? # Header finished, can only be some more body
5
+ body << data
6
+ else # Parse more header using the super parser
7
+ @data << data
8
+ raise InvalidRequest, 'Header longer than allowed' if @data.size > MAX_HEADER
9
+
10
+ @nparsed = @parser.execute(@env, @data, @nparsed)
11
+
12
+ if @parser.finished?
13
+ return super(data) unless @env['HTTP_TRANSFER_ENCODING'] == 'chunked'
14
+
15
+ _, initial_body = @data.split("\r\n\r\n")
16
+ initial_body ||= ''
17
+
18
+ @body = DeferrableBody.new(initial_body)
19
+
20
+ return true # trigger the rack call chain
21
+ end
22
+ end
23
+
24
+ return false # only trigger the rack call chain once, just after the headers are parsed
25
+ end
26
+
27
+ def env
28
+ super.merge(RACK_INPUT => body)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ module Thin
2
+ class DeferredResponse < Response
3
+ end
4
+ end
@@ -0,0 +1,22 @@
1
+
2
+ module EY
3
+ module Tea
4
+ module Server
5
+ class AsyncFixer
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ tuple = @app.call(env)
12
+
13
+ if tuple.first == -1
14
+ Thin::Connection::AsyncResponse
15
+ else
16
+ tuple
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,63 @@
1
+ module EY
2
+ module Tea
3
+ module Server
4
+ class Dechunker
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ env['rack.input'] = ChunkedBody.new(env['rack.input']) if chunked?(env)
11
+
12
+ @app.call(env)
13
+ end
14
+
15
+ def chunked?(env)
16
+ env['HTTP_TRANSFER_ENCODING'] == 'chunked'
17
+ end
18
+
19
+ class ChunkedBody
20
+ extend Forwardable
21
+
22
+ CRLF = "\r\n"
23
+
24
+ attr_reader :input
25
+
26
+ def_delegators :input, :callback, :errback
27
+
28
+ def initialize(input)
29
+ @input, @buffer = input, ''
30
+ end
31
+
32
+ def each(&blk)
33
+ @input.each do |chunk|
34
+ dechunk(chunk, &blk)
35
+ end
36
+ end
37
+
38
+ def dechunk(chunk, &blk)
39
+ @buffer << chunk
40
+
41
+ loop do
42
+ return unless @buffer[CRLF]
43
+
44
+ string_length, remainder = @buffer.split(CRLF, 2)
45
+ length = string_length.to_i(16)
46
+
47
+ if length == 0
48
+ @buffer = ''
49
+ @input.succeed
50
+ return
51
+ elsif remainder.size >= length + 2 # length + CRLF
52
+ data, @buffer = remainder.split(CRLF, 2)
53
+ blk.call(data)
54
+ else
55
+ return
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,69 @@
1
+ require 'rack/mux'
2
+
3
+ module EY
4
+ module Tea
5
+ module Server
6
+ module Mock
7
+
8
+ def self.boot_forking_server
9
+ o,i = IO.pipe
10
+
11
+ if pid = fork
12
+ at_exit { Process.kill(:SIGTERM, pid) }
13
+
14
+ i.close
15
+ URI.parse(o.read)
16
+ else
17
+ o.close
18
+ process_child(i)
19
+ end
20
+ end
21
+
22
+ def self.process_child(i)
23
+ EM.run do
24
+ client = Rack::Client.new { run EY::Tea::Server.mock_app }
25
+
26
+ uri = client.get("/mux-uri").body
27
+ i << uri
28
+ i.close
29
+ end
30
+
31
+ exit
32
+ end
33
+
34
+ class ThinMuxer
35
+ def initialize(app)
36
+ @app = Rack::Mux.new(async_safe(app), thin_options)
37
+ end
38
+
39
+ def call(env)
40
+ @app.call(env)
41
+ end
42
+
43
+ def async_safe(app)
44
+ AsyncFixer.new(app)
45
+ end
46
+
47
+ def thin_options
48
+ { :server => Thin, :environment => 'none' }
49
+ end
50
+ end
51
+
52
+ class EchoUri
53
+
54
+ def initialize(app)
55
+ @app = app
56
+ end
57
+
58
+ def call(env)
59
+ if env['PATH_INFO'] == '/mux-uri'
60
+ [200, {'Content-Type' => 'text/plain'}, [env['X-Mux-Uri']]]
61
+ else
62
+ @app.call(env)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,78 @@
1
+ module EventMachine
2
+ module Protocols
3
+ class PubSubRedis < EventMachine::Connection
4
+ include Redis
5
+
6
+ def subscribe(channel, &block)
7
+ @pubsub_callback = block
8
+
9
+ call_command(['subscribe', channel])
10
+ end
11
+
12
+ def unsubscribe(channel)
13
+ @pubsub_callback = lambda do |*args|
14
+ close_connection
15
+ end
16
+
17
+ call_command(['unsubscribe', channel])
18
+ end
19
+
20
+ def dispatch_response(value)
21
+ if @multibulk_n
22
+ @multibulk_values << value
23
+ @multibulk_n -= 1
24
+
25
+ if @multibulk_n == 0
26
+ value = @multibulk_values
27
+ @multibulk_n,@multibulk_values = @previous_multibulks.pop
28
+ if @multibulk_n
29
+ dispatch_response(value)
30
+ return
31
+ end
32
+ else
33
+ return
34
+ end
35
+ end
36
+
37
+ @pubsub_callback.call(value)
38
+ end
39
+
40
+ def self.connect(*args)
41
+ case args.length
42
+ when 0
43
+ options = {}
44
+ when 1
45
+ arg = args.shift
46
+ case arg
47
+ when Hash then options = arg
48
+ when String then options = {:host => arg}
49
+ else raise ArgumentError, 'first argument must be Hash or String'
50
+ end
51
+ when 2
52
+ options = {:host => args[0], :port => args[1]}
53
+ else
54
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 1)"
55
+ end
56
+ options[:host] ||= '127.0.0.1'
57
+ options[:port] = (options[:port] || 6379).to_i
58
+ EM.connect options[:host], options[:port], self, options
59
+ end
60
+
61
+ def initialize(options = {})
62
+ @host = options[:host]
63
+ @port = options[:port]
64
+ @db = (options[:db] || 0).to_i
65
+ @password = options[:password]
66
+ @logger = options[:logger]
67
+ @error_callback = lambda do |code|
68
+ err = RedisError.new
69
+ err.code = code
70
+ raise err, "Redis server returned error code: #{code}"
71
+ end
72
+
73
+ # These commands should be first
74
+ auth_and_select_db
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,5 @@
1
+ module EY
2
+ module Tea
3
+ VERSION = '0.0.0'
4
+ end
5
+ end
@@ -0,0 +1,126 @@
1
+ require 'spec_helper'
2
+
3
+ describe EY::Tea::Client do
4
+ subject { @client }
5
+
6
+ def new_client
7
+ EY::Tea::Client.new(:endpoint => EY::Tea::Server.mock_uri.to_s)
8
+ end
9
+
10
+ def run(thread)
11
+ thread.join(0.1) if thread.alive?
12
+ end
13
+
14
+ it "can stream an IO" do
15
+ uuid = rand(10_000).to_s
16
+ o,i = IO.pipe
17
+ i << "Hello, World!"
18
+ i.close
19
+
20
+ new_client.up(o, uuid)
21
+
22
+ body = ''
23
+ new_client.down(uuid) do |chunk|
24
+ body << chunk
25
+ end
26
+ body.should == 'Hello, World!'
27
+ end
28
+
29
+ it "can stream out before the incoming stream has finished." do
30
+ scheduler = Thread.current
31
+ uuid = rand(10_000).to_s
32
+ o, i = IO.pipe
33
+ i << 'Hello, '
34
+
35
+ up_thread = Thread.new(new_client, o) do |client, reader|
36
+ run scheduler
37
+ client.up(reader, uuid)
38
+ end
39
+
40
+ down_thread = Thread.new(new_client, i, scheduler) do |client, writer|
41
+ run scheduler
42
+ client.down(uuid) do |chunk|
43
+ if chunk == 'Hello, '
44
+ writer << 'World!'
45
+ writer.close
46
+ run scheduler
47
+ end
48
+ end
49
+ end
50
+
51
+ run up_thread
52
+ run down_thread
53
+ run up_thread
54
+ run down_thread
55
+ end
56
+
57
+ it "streams already recieved data on an open stream to peers." do
58
+ scheduler = Thread.current
59
+ uuid = rand(10_000).to_s
60
+ o, i = IO.pipe
61
+
62
+ i << 'Existing Data'
63
+
64
+ up_thread = Thread.new(subject, o) do |client, reader|
65
+ run scheduler
66
+ client.up(reader, uuid)
67
+ end
68
+
69
+ down_thread = Thread.new(new_client) do |client|
70
+ run scheduler
71
+ Thread.current[:data] = ''
72
+
73
+ client.down(uuid) do |chunk|
74
+ Thread.current[:data] << chunk
75
+ run scheduler
76
+ end
77
+ end
78
+
79
+ run up_thread
80
+ run down_thread
81
+
82
+ down_thread[:data] == 'Existing Data'
83
+ end
84
+
85
+ it "streams the same data to all peers." do
86
+ scheduler = Thread.current
87
+ uuid = rand(10_000).to_s
88
+ o, i = IO.pipe
89
+
90
+ up_thread = Thread.new(new_client, o) do |client, reader|
91
+ client.up(reader, uuid)
92
+ end
93
+
94
+ run up_thread
95
+
96
+ down_threads = Array.new(5, new_client).map do |c|
97
+ Thread.new(c) do |client|
98
+ Thread.current[:chunks] = []
99
+ run scheduler
100
+
101
+ client.down(uuid) do |chunk|
102
+ Thread.current[:chunks] << chunk
103
+ run scheduler
104
+ end
105
+ end
106
+ end
107
+
108
+ i << "First Part"
109
+
110
+ down_threads.each {|t| run t }
111
+ down_threads.each {|t| t[:chunks].should == ['First Part'] }
112
+
113
+ i << "Second Part"
114
+
115
+ down_threads.each {|t| run t }
116
+ down_threads.each {|t| t[:chunks].should == ['First Part', 'Second Part'] }
117
+
118
+ i << "Third Part"
119
+
120
+ down_threads.each {|t| run t }
121
+ down_threads.each {|t| t[:chunks].should == ['First Part', 'Second Part', 'Third Part'] }
122
+
123
+ i.close
124
+ end
125
+
126
+ end