htttee 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ <%= @framework_env %>:
2
+ redis_host: <%= @redis_host %>
3
+ redis_port: <%= @redis_port %>
@@ -0,0 +1,73 @@
1
+ rack_env = "<%= @node[:environment][:framework_env] %>"
2
+ app_name = "<%= @app %>"
3
+ app_root = "<%= @app_root %>"
4
+ owner = "<%= @node[:owner_name] %>"
5
+ home = "/home/#{owner}"
6
+ instance_id = "<%= @node.engineyard.id %>"
7
+
8
+ <%= @resque_workers_count %>.times do |num|
9
+ inline = "#{home}/.ruby_inline/resque-#{app_name}-#{num}"
10
+
11
+ God.watch do |w|
12
+ w.name = "resque-scheduler-#{app_name}-#{num}"
13
+ w.group = "resque-#{app_name}"
14
+ w.uid = owner
15
+ w.gid = owner
16
+ w.interval = 30.seconds
17
+ w.log = "#{app_root}/log/worker-scheduler.#{num}.log"
18
+ w.dir = app_root
19
+ w.env = {
20
+ "USER" => owner,
21
+ "VERBOSE" => "true",
22
+ "INSTANCE_ID" => instance_id,
23
+ "GOD_WATCH" => w.name,
24
+ "RACK_ENV" => rack_env,
25
+ "HOME" => home,
26
+ "QUEUE" => "*",
27
+ }
28
+
29
+ w.start = "bundle exec rake --trace init resque:scheduler"
30
+
31
+ w.behavior(:clean_pid_file)
32
+
33
+ w.start_grace = 2.minutes
34
+ w.restart_grace = 2.minutes
35
+
36
+ # retart if memory gets too high
37
+ w.transition(:up, :restart) do |on|
38
+ on.condition(:memory_usage) do |c|
39
+ c.above = 350.megabytes
40
+ c.times = 2
41
+ end
42
+ end
43
+
44
+ # determine the state on startup
45
+ w.transition(:init, { true => :up, false => :start }) do |on|
46
+ on.condition(:process_running) do |c|
47
+ c.running = true
48
+ end
49
+ end
50
+
51
+ # determine when process has finished starting
52
+ w.transition([:start, :restart], :up) do |on|
53
+ on.condition(:process_running) do |c|
54
+ c.running = true
55
+ c.interval = 5.seconds
56
+ end
57
+
58
+ # failsafe
59
+ on.condition(:tries) do |c|
60
+ c.times = 5
61
+ c.transition = :start
62
+ c.interval = 5.seconds
63
+ end
64
+ end
65
+
66
+ # start if process is not running
67
+ w.transition(:up, :start) do |on|
68
+ on.condition(:process_running) do |c|
69
+ c.running = false
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,7 @@
1
+ require 'dnapi'
2
+ base_dir = File.dirname(__FILE__)
3
+
4
+ file_store_path base_dir
5
+ file_cache_path base_dir
6
+ cookbook_path "#{base_dir}/cookbooks"
7
+ node_name DNApi.from(File.read("/etc/chef/dna.json")).id
@@ -0,0 +1,30 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "htttee"
3
+ s.version = '0.5.0'
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ["Ben Burkert"]
6
+ s.email = ["ben@benburkert.com"]
7
+ s.homepage = "http://github.com/benburkert/htttee"
8
+ s.summary = %q{Unix's tee as a service}
9
+ s.description = %q{Stream any CLI output as an HTTP chunked response.}
10
+
11
+ s.rubyforge_project = "htttee"
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_dependency 'rack-client', '~> 0.3.1.pre.i'
19
+ s.add_dependency 'trollop'
20
+
21
+ s.add_development_dependency 'sinatra'
22
+ s.add_development_dependency 'thin'
23
+ s.add_development_dependency 'em-redis'
24
+ s.add_development_dependency 'yajl-ruby'
25
+ s.add_development_dependency 'rack-mux'
26
+
27
+ s.add_development_dependency 'rspec'
28
+ s.add_development_dependency 'rake'
29
+ # s.add_development_dependency 'ruby-debug19'
30
+ end
File without changes
@@ -0,0 +1,16 @@
1
+ require 'net/http'
2
+ require 'rack/client'
3
+ #require 'uuidtools'
4
+
5
+ module EY
6
+ module Tea
7
+ module Client
8
+ def self.new(*a)
9
+ Consumer.new(*a)
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ require 'htttee/client/ext/net/http'
16
+ require 'htttee/client/consumer'
@@ -0,0 +1,49 @@
1
+ module EY
2
+ module Tea
3
+ module Client
4
+ class Consumer < Rack::Client::Base
5
+ def initialize(options = {})
6
+ @base_uri = URI.parse(options.fetch(:endpoint, 'http://tea.engineyard.com/'))
7
+ inner_app = options.fetch(:app, Rack::Client::Handler::NetHTTP.new)
8
+
9
+ super(inner_app)
10
+ end
11
+
12
+ def up(io, uuid, content_type = 'text/plain')
13
+ post("/#{uuid}", {'Content-Type' => content_type, 'Transfer-Encoding' => 'chunked'}, io)
14
+ end
15
+
16
+ def down(uuid)
17
+ get("/#{uuid}") do |status, headers, response_body|
18
+ response_body.each do |chunk|
19
+ yield chunk
20
+ end
21
+ end
22
+ end
23
+
24
+ def build_env(request_method, url, headers = {}, body = nil)
25
+ uri = @base_uri.nil? ? URI.parse(url) : @base_uri + url
26
+
27
+ env = super(request_method, uri.to_s, headers, body)
28
+
29
+ env['HTTP_HOST'] ||= http_host_for(uri)
30
+ env['HTTP_USER_AGENT'] ||= http_user_agent
31
+
32
+ env
33
+ end
34
+
35
+ def http_host_for(uri)
36
+ if uri.to_s.include?(":#{uri.port}")
37
+ [uri.host, uri.port].join(':')
38
+ else
39
+ uri.host
40
+ end
41
+ end
42
+
43
+ def http_user_agent
44
+ "htttee (rack-client #{Rack::Client::VERSION} (app: #{@app.class}))"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,27 @@
1
+ module Net
2
+ class HTTP
3
+ class Post
4
+ def send_request_with_body_stream(sock, ver, path, f)
5
+ unless content_length() or chunked?
6
+ raise ArgumentError,
7
+ "Content-Length not given and Transfer-Encoding is not `chunked'"
8
+ end
9
+ supply_default_content_type
10
+ write_header sock, ver, path
11
+ if chunked?
12
+ begin
13
+ while s = f.readpartial(1024)
14
+ sock.write(sprintf("%x\r\n", s.length) << s << "\r\n")
15
+ end
16
+ rescue EOFError
17
+ sock.write "0\r\n\r\n"
18
+ end
19
+ else
20
+ while s = f.read(1024)
21
+ sock.write s
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,74 @@
1
+ require 'thin'
2
+ require 'yajl'
3
+ require 'sinatra/base'
4
+ require 'em-redis'
5
+
6
+ EM.epoll
7
+
8
+ module EY
9
+ module Tea
10
+ module Server
11
+
12
+ def self.app
13
+ mocking? ? mock_app : rack_app
14
+ end
15
+
16
+ def self.api(host = (ENV['REDIS_HOST'] || 'localhost' ), port = (ENV['REDIS_PORT'] || 6379).to_i)
17
+ Api.new(host, port)
18
+ end
19
+
20
+ def self.rack_app
21
+ Rack::Builder.app do |builder|
22
+ builder.use AsyncFixer
23
+ builder.use Dechunker
24
+ builder.run Server.api
25
+ end
26
+ end
27
+
28
+ def self.mock_app
29
+ Rack::Builder.app do |builder|
30
+ builder.use Mock::ThinMuxer
31
+ builder.use Mock::EchoUri
32
+ builder.run Server.rack_app
33
+ end
34
+ end
35
+
36
+ def self.mock!
37
+ require 'htttee/server/mock'
38
+ @mocking = true
39
+
40
+ @mock_uri = Mock.boot_forking_server
41
+ end
42
+
43
+ def self.reset!
44
+ raise "Can't reset in non-mocked mode." unless mocking?
45
+
46
+ EM.run do
47
+ EM::Protocols::Redis.connect.flush_all do
48
+ EM.stop
49
+ end
50
+ end
51
+ end
52
+
53
+ def self.mocking?
54
+ @mocking
55
+ end
56
+
57
+ def self.mock_uri
58
+ raise "Not in mock mode!" unless mocking?
59
+ @mock_uri
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ require 'htttee/server/ext/em-redis'
66
+ require 'htttee/server/ext/thin'
67
+
68
+ require 'htttee/server/pubsub_redis'
69
+
70
+ require 'htttee/server/api'
71
+ require 'htttee/server/chunked_body'
72
+
73
+ require 'htttee/server/middleware/async_fixer'
74
+ require 'htttee/server/middleware/dechunker'
@@ -0,0 +1,201 @@
1
+ module EY
2
+ module Tea
3
+ module Server
4
+ class Api
5
+ STREAMING, FIN = ?0, ?1
6
+ SUBSCRIBE, UNSUBSCRIBE, MESSAGE = 'SUBSCRIBE', 'UNSUBSCRIBE', 'MESSAGE'
7
+
8
+ AsyncResponse = [-1, {}, []].freeze
9
+
10
+ attr_accessor :redis
11
+
12
+ def initialize(host, port)
13
+ @host, @port = host, port
14
+ end
15
+
16
+ def call(env)
17
+ uuid = env['PATH_INFO'].sub(/^\//, '')
18
+
19
+ case env['REQUEST_METHOD']
20
+ when 'POST' then post(env, uuid)
21
+ when 'GET' then get(env, uuid)
22
+ end
23
+
24
+ AsyncResponse
25
+ end
26
+
27
+ def post(env, uuid)
28
+ body = Thin::DeferrableBody.new
29
+
30
+ redis.set(state_key(uuid), STREAMING)
31
+
32
+ set_input_callback(env, uuid, body)
33
+ set_input_errback(env, uuid, body)
34
+ set_input_each(env, uuid, body)
35
+ end
36
+
37
+ def get(env, uuid)
38
+ body = ChunkedBody.new
39
+
40
+ with_state_for(uuid) do |state|
41
+ case state
42
+ when NilClass then four_oh_four_response(env, body)
43
+ when STREAMING then open_stream_response(env, uuid, body)
44
+ when FIN then closed_stream_response(env, uuid, body)
45
+ end
46
+ end
47
+ end
48
+
49
+ def set_input_callback(env, uuid, body)
50
+ rack_input(env).callback do
51
+ async_callback(env).call [204, {}, body]
52
+
53
+ redis.set(state_key(uuid), FIN) do
54
+ finish(channel(uuid))
55
+ body.succeed
56
+ end
57
+ end
58
+ end
59
+
60
+ def set_input_errback(env, uuid, body)
61
+ rack_input(env).errback do |error|
62
+ async_callback(env).call [500, {}, body]
63
+
64
+ body.call [error.inspect]
65
+ body.succeed
66
+ end
67
+ end
68
+
69
+ def set_input_each(env, uuid, body)
70
+ rack_input(env).each do |chunk|
71
+ unless chunk.empty?
72
+ redis.pipeline(['append', data_key(uuid), chunk], ['publish', channel(uuid), Yajl::Encoder.encode([STREAMING, chunk])])
73
+ end
74
+ end
75
+ end
76
+
77
+ def four_oh_four_response(env, body)
78
+ env['async.callback'].call [404, {'Transfer-Encoding' => 'chunked'}, body]
79
+
80
+ body.succeed
81
+ end
82
+
83
+ def open_stream_response(env, uuid, body)
84
+ start_response(env, body)
85
+ stream_data_to(body, data_key(uuid)) do
86
+ with_state_for(uuid) do |state|
87
+ if state == FIN
88
+ body.succeed
89
+ else
90
+ subscribe_and_stream(env, uuid, body)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ def closed_stream_response(env, uuid, body)
97
+ start_response(env, body)
98
+ stream_data_to(body, data_key(uuid)) do
99
+ body.succeed
100
+ end
101
+ end
102
+
103
+ def stream_data_to(body, key, offset = 0, chunk_size = 1024, &block)
104
+ redis.substr(key, offset, offset + chunk_size) do |chunk|
105
+ if chunk.nil? || chunk.empty?
106
+ yield
107
+ else
108
+ body.call [chunk]
109
+ stream_data_to(body, key, offset + chunk.size, chunk_size, &block)
110
+ end
111
+ end
112
+ end
113
+
114
+ def respond_with(env, body, data)
115
+ start_response(env, body, data)
116
+ body.succeed
117
+ end
118
+
119
+ def start_response(env, body, data = nil)
120
+ env['async.callback'].call [200, {'Transfer-Encoding' => 'chunked', 'Content-Type' => 'text/plain'}, body]
121
+
122
+ body.call [data] unless data.nil? || data.empty?
123
+ end
124
+
125
+ def subscribe_and_stream(env, uuid, body)
126
+ subscribe channel(uuid) do |type, message, *extra|
127
+ case type
128
+ #when SUBSCRIBE then start_response(env, body, data)
129
+ when FIN then body.succeed
130
+ when STREAMING then body.call [message]
131
+ end
132
+ end
133
+ end
134
+
135
+ def with_state_for(uuid, &block)
136
+ redis.get(state_key(uuid), &block)
137
+ end
138
+
139
+ def with_state_and_data_for(uuid, &block)
140
+ redis.multi_get(state_key(uuid), data_key(uuid), &block)
141
+ end
142
+
143
+ def data_key(uuid)
144
+ "#{uuid}:data"
145
+ end
146
+
147
+ def state_key(uuid)
148
+ "#{uuid}:state"
149
+ end
150
+
151
+ def channel(uuid)
152
+ uuid
153
+ end
154
+
155
+ def rack_input(rack_env)
156
+ rack_env['rack.input']
157
+ end
158
+
159
+ def async_callback(rack_env)
160
+ rack_env['async.callback']
161
+ end
162
+
163
+ def publish(channel, data)
164
+ redis.publish channel, Yajl::Encoder.encode([STREAMING, data])
165
+ end
166
+
167
+ def finish(channel)
168
+ redis.publish channel, Yajl::Encoder.encode([FIN])
169
+ end
170
+
171
+ def subscribe(channel, &block)
172
+ conn = pubsub
173
+ conn.subscribe channel do |type, chan, data|
174
+ case type.upcase
175
+ when SUBSCRIBE then block.call(SUBSCRIBE, chan)
176
+ when MESSAGE
177
+ state, data = Yajl::Parser.parse(data)
178
+ case state
179
+ when STREAMING then block.call(STREAMING, data)
180
+ when FIN
181
+ conn.unsubscribe channel
182
+ block.call(FIN, data)
183
+ end
184
+ else
185
+ debugger
186
+ ''
187
+ end
188
+ end
189
+ end
190
+
191
+ def pubsub
192
+ EM::Protocols::PubSubRedis.connect(@host, @port)
193
+ end
194
+
195
+ def redis
196
+ @redis ||= EM::Protocols::Redis.connect(@host, @port)
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end