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.
- data/.gitignore +6 -0
- data/Gemfile +2 -0
- data/README.md +105 -0
- data/Rakefile +14 -0
- data/bin/htttee +39 -0
- data/bin/htttee-exec +18 -0
- data/config.ru +6 -0
- data/deploy/after_restart.rb +1 -0
- data/deploy/before_restart.rb +4 -0
- data/deploy/before_symlink.rb +2 -0
- data/deploy/cookbooks/cloudkick-plugins/recipes/default.rb +8 -0
- data/deploy/cookbooks/cloudkick-plugins/recipes/resque.rb +18 -0
- data/deploy/cookbooks/cloudkick-plugins/templates/default/resque.sh.erb +4 -0
- data/deploy/cookbooks/god/recipes/default.rb +50 -0
- data/deploy/cookbooks/god/templates/default/config.erb +3 -0
- data/deploy/cookbooks/god/templates/default/god-inittab.erb +3 -0
- data/deploy/cookbooks/main/attributes/owner_name.rb +3 -0
- data/deploy/cookbooks/main/attributes/recipes.rb +1 -0
- data/deploy/cookbooks/main/libraries/dnapi.rb +7 -0
- data/deploy/cookbooks/main/recipes/default.rb +2 -0
- data/deploy/cookbooks/nginx/files/default/chunkin-nginx-module-v0.22rc1.zip +0 -0
- data/deploy/cookbooks/nginx/files/default/nginx-1.0.0.tar.gz +0 -0
- data/deploy/cookbooks/nginx/recipes/default.rb +2 -0
- data/deploy/cookbooks/nginx/recipes/install.rb +0 -0
- data/deploy/cookbooks/nginx/templates/default/nginx.conf.erb +41 -0
- data/deploy/cookbooks/resque/recipes/default.rb +47 -0
- data/deploy/cookbooks/resque/templates/default/resque.rb.erb +73 -0
- data/deploy/cookbooks/resque/templates/default/resque.yml.erb +3 -0
- data/deploy/cookbooks/resque/templates/default/resque_scheduler.rb.erb +73 -0
- data/deploy/solo.rb +7 -0
- data/htttee.gemspec +30 -0
- data/lib/htttee.rb +0 -0
- data/lib/htttee/client.rb +16 -0
- data/lib/htttee/client/consumer.rb +49 -0
- data/lib/htttee/client/ext/net/http.rb +27 -0
- data/lib/htttee/server.rb +74 -0
- data/lib/htttee/server/api.rb +201 -0
- data/lib/htttee/server/chunked_body.rb +22 -0
- data/lib/htttee/server/ext/em-redis.rb +49 -0
- data/lib/htttee/server/ext/thin.rb +4 -0
- data/lib/htttee/server/ext/thin/connection.rb +8 -0
- data/lib/htttee/server/ext/thin/deferrable_body.rb +24 -0
- data/lib/htttee/server/ext/thin/deferred_request.rb +31 -0
- data/lib/htttee/server/ext/thin/deferred_response.rb +4 -0
- data/lib/htttee/server/middleware/async_fixer.rb +22 -0
- data/lib/htttee/server/middleware/dechunker.rb +63 -0
- data/lib/htttee/server/mock.rb +69 -0
- data/lib/htttee/server/pubsub_redis.rb +78 -0
- data/lib/htttee/version.rb +5 -0
- data/spec/client_spec.rb +126 -0
- data/spec/helpers/rackup.rb +15 -0
- data/spec/spec_helper.rb +21 -0
- metadata +201 -0
@@ -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
|
data/deploy/solo.rb
ADDED
data/htttee.gemspec
ADDED
@@ -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
|
data/lib/htttee.rb
ADDED
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
|