htttee 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|