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,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