droid 0.9.5 → 1.0.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 +2 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/bin/bleedq +19 -0
- data/droid.gemspec +100 -0
- data/examples/async_reply.rb +25 -0
- data/examples/heroku_async_reply.rb +22 -0
- data/examples/sync.rb +32 -0
- data/examples/worker.rb +58 -0
- data/lib/droid.rb +88 -476
- data/lib/droid/em.rb +55 -0
- data/lib/droid/heroku.rb +102 -0
- data/lib/droid/heroku/local_stats.rb +145 -0
- data/{vendor/logger_client/lib → lib/droid/heroku}/logger_client.rb +9 -5
- data/lib/droid/heroku/memcache_cluster.rb +129 -0
- data/lib/droid/heroku/stats.rb +30 -0
- data/lib/droid/json_server.rb +109 -0
- data/lib/droid/monkey.rb +8 -0
- data/lib/droid/publish.rb +24 -0
- data/lib/droid/queue.rb +196 -0
- data/lib/droid/request.rb +110 -0
- data/lib/droid/sync.rb +88 -0
- data/lib/droid/utilization.rb +113 -0
- data/lib/droid/utils.rb +113 -0
- data/lib/heroku_droid.rb +1 -86
- data/lib/local_stats.rb +1 -143
- data/lib/memcache_cluster.rb +1 -129
- data/lib/stats.rb +1 -30
- data/spec/publish_spec.rb +27 -0
- data/spec/response_spec.rb +47 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/utils_spec.rb +43 -0
- data/{test/wait_for_port_test.rb → spec/wait_for_port_spec.rb} +2 -9
- metadata +109 -35
- data/README.md +0 -34
- data/lib/utilization.rb +0 -90
- data/test/base.rb +0 -43
- data/test/droid_test.rb +0 -53
- data/test/heroku_droid_test.rb +0 -42
- data/vendor/logger_client/Rakefile +0 -53
- data/vendor/logger_client/init.rb +0 -1
- data/vendor/logger_client/test.rb +0 -18
@@ -0,0 +1,30 @@
|
|
1
|
+
module Stats
|
2
|
+
# The MemCache instance used to manipulate stats.
|
3
|
+
def cache
|
4
|
+
MemcacheCluster.cache("heroku:stats")
|
5
|
+
end
|
6
|
+
|
7
|
+
# Increment a stat counter. If the counter does not exist,
|
8
|
+
# yield to the block and use the result as the current counter
|
9
|
+
# value. With no block, the counter will be started at zero.
|
10
|
+
def increment(key, amount=1)
|
11
|
+
if (value = cache.incr(key, amount)).nil?
|
12
|
+
value = yield if block_given?
|
13
|
+
value = (value || 0) + amount
|
14
|
+
cache.add(key, value.to_s, 0, true)
|
15
|
+
end
|
16
|
+
rescue => boom
|
17
|
+
Log.default_error(boom)
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set the stat counter to a specific value.
|
22
|
+
def sample(key, value)
|
23
|
+
cache.set(key, value.to_s, 0, true)
|
24
|
+
rescue => boom
|
25
|
+
Log.default_error(boom)
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
|
29
|
+
extend self
|
30
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'evma_httpserver'
|
3
|
+
|
4
|
+
class Droid
|
5
|
+
class JSONServer < ::EM::Connection
|
6
|
+
include ::EM::HttpServer
|
7
|
+
|
8
|
+
def post_init
|
9
|
+
super
|
10
|
+
no_environment_strings
|
11
|
+
end
|
12
|
+
|
13
|
+
def process_http_request
|
14
|
+
return not_found_response if @http_request_method != "GET"
|
15
|
+
|
16
|
+
if @http_request_uri == "/"
|
17
|
+
default_response
|
18
|
+
else
|
19
|
+
method_name = "get_#{@http_request_uri.split("/")[1]}".gsub(/[^\d\w_]/,'').downcase
|
20
|
+
if public_methods.include?(method_name)
|
21
|
+
generate_response(method_name)
|
22
|
+
else
|
23
|
+
not_found_response
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_response(method_name)
|
29
|
+
status, data, content_type = self.send(method_name)
|
30
|
+
|
31
|
+
response = ::EM::DelegatedHttpResponse.new(self)
|
32
|
+
response.status = status
|
33
|
+
response.content_type content_type
|
34
|
+
response.content = data
|
35
|
+
response.send_response
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_response
|
39
|
+
response = ::EM::DelegatedHttpResponse.new(self)
|
40
|
+
response.status = 200
|
41
|
+
response.content_type 'application/json'
|
42
|
+
response.content = {"status" => "OK"}.to_json
|
43
|
+
response.send_response
|
44
|
+
end
|
45
|
+
|
46
|
+
def not_found_response
|
47
|
+
response = ::EM::DelegatedHttpResponse.new(self)
|
48
|
+
response.status = 404
|
49
|
+
response.content_type 'text/plain'
|
50
|
+
response.content = "Not Found"
|
51
|
+
response.send_response
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_droid
|
55
|
+
report_data = Droid::Utilization.report_data
|
56
|
+
|
57
|
+
metrics = {}
|
58
|
+
report_data.each do |topic, data|
|
59
|
+
metrics[topic] = data['msgs']
|
60
|
+
end
|
61
|
+
metrics['latency'] = Droid::Utilization.latency
|
62
|
+
|
63
|
+
summary = Droid::Utilization.report_summary(report_data)
|
64
|
+
|
65
|
+
metrics['total_msgs'] = summary['msgs']
|
66
|
+
|
67
|
+
status = "AMQP: #{summary['msgs']} msgs processed since #{Droid::Utilization.start.utc}"
|
68
|
+
|
69
|
+
# reset metrics data
|
70
|
+
Droid::Utilization.reinit
|
71
|
+
|
72
|
+
data = {
|
73
|
+
'status' => status,
|
74
|
+
'state' => 'ok',
|
75
|
+
'metrics' => hash_to_metrics(metrics)
|
76
|
+
}
|
77
|
+
[200, data.to_json, "application/json"]
|
78
|
+
end
|
79
|
+
|
80
|
+
def hash_to_metrics(hash); self.class.hash_to_metrics(hash); end
|
81
|
+
|
82
|
+
# utility method to convert a ruby hash to a metrics format
|
83
|
+
# that can be consumed by cloudkick
|
84
|
+
def self.hash_to_metrics(hash)
|
85
|
+
hash.collect do |k,v|
|
86
|
+
name = k.to_s
|
87
|
+
value = v
|
88
|
+
type = if v.kind_of?(Integer)
|
89
|
+
'int'
|
90
|
+
elsif v.kind_of?(Float)
|
91
|
+
'float'
|
92
|
+
else
|
93
|
+
'string'
|
94
|
+
end
|
95
|
+
|
96
|
+
# bool -> int conversion
|
97
|
+
if [TrueClass, FalseClass].include?(v.class)
|
98
|
+
value = v ? 1 : 0
|
99
|
+
type = 'int'
|
100
|
+
end
|
101
|
+
|
102
|
+
# if type is really string then it should respond to .to_s
|
103
|
+
value = value.to_s if type == 'string'
|
104
|
+
|
105
|
+
{ 'name' => name, 'type' => type, 'value' => value }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/droid/monkey.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'droid/utils'
|
2
|
+
|
3
|
+
class Droid
|
4
|
+
# publish to queue directly
|
5
|
+
def self.publish_to_q(queue_name, data, opts={}, popts={})
|
6
|
+
q = ::MQ.queue(queue_name)
|
7
|
+
json, popts = Droid::Utils.format_publish(data, opts, popts)
|
8
|
+
q.publish(json, popts)
|
9
|
+
log.info "amqp_publish queue=#{queue_name} #{Droid::Utils.format_data_summary(data, popts[:headers])}" unless opts[:log] == false
|
10
|
+
end
|
11
|
+
|
12
|
+
# publish to exchange directly
|
13
|
+
def self.publish_to_ex(ex_name, data, opts={}, popts={})
|
14
|
+
ex = ::MQ.direct(ex_name)
|
15
|
+
json, popts = Droid::Utils.format_publish(data, opts, popts)
|
16
|
+
ex.publish(json, popts)
|
17
|
+
log.info "amqp_publish exchange=#{ex_name} #{Droid::Utils.format_data_summary(data, popts[:headers])}" unless opts[:log] == false
|
18
|
+
end
|
19
|
+
|
20
|
+
# default is publish to exchange
|
21
|
+
def self.publish(ex_name, data, opts={}, popts={})
|
22
|
+
publish_to_ex(ex_name, data, opts, popts)
|
23
|
+
end
|
24
|
+
end
|
data/lib/droid/queue.rb
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
class Droid
|
2
|
+
class ExpiredMessage < RuntimeError; end
|
3
|
+
|
4
|
+
class BaseQueue
|
5
|
+
attr_reader :queue_name, :opts
|
6
|
+
attr_reader :q, :ex, :mq
|
7
|
+
|
8
|
+
def initialize(queue_name, opts={})
|
9
|
+
opts[:auto_delete] = true unless opts.has_key?(:auto_delete) and opts[:auto_delete] === false
|
10
|
+
|
11
|
+
@queue_name, @opts = queue_name, opts
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@mq = MQ.new
|
16
|
+
@q = @mq.queue(queue_name, opts)
|
17
|
+
# if we don't specify an exchange name it defaults to the queue_name
|
18
|
+
@ex = @mq.direct(opts[:exchange_name] || queue_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def temp?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def log
|
26
|
+
Droid.log
|
27
|
+
end
|
28
|
+
|
29
|
+
def tag
|
30
|
+
s = "queue=#{q.name}"
|
31
|
+
s += " exchange=#{ex.name}" if ex
|
32
|
+
end
|
33
|
+
|
34
|
+
def subscribe(amqp_opts={}, opts={})
|
35
|
+
setup
|
36
|
+
|
37
|
+
q.bind(ex) if ex
|
38
|
+
q.subscribe(amqp_opts) do |header, message|
|
39
|
+
Droid::Utilization.monitor(q.name, :temp => temp?) do
|
40
|
+
request = Droid::Request.new(self, header, message)
|
41
|
+
log.info "amqp_message #{tag} action=received ttl=#{request.ttl} age=#{request.age} #{request.data_summary}"
|
42
|
+
begin
|
43
|
+
raise Droid::ExpiredMessage if request.expired?
|
44
|
+
yield request if block_given?
|
45
|
+
finished = Time.now.getgm.to_i
|
46
|
+
log.info "amqp_message action=processed #{tag} elapsed=#{finished-request.start} ttl=#{request.ttl} age=#{request.age} #{request.data_summary}"
|
47
|
+
rescue Droid::ExpiredMessage
|
48
|
+
log.info "amqp_message action=timeout #{tag} ttl=#{request.ttl} age=#{request.age} #{request.data_summary}"
|
49
|
+
request.ack if amqp_opts[:ack]
|
50
|
+
rescue => e
|
51
|
+
request.ack if amqp_opts[:ack]
|
52
|
+
Droid.handle_error(e)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
log.info "amqp_subscribe #{tag}"
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def teardown
|
61
|
+
@q.unsubscribe
|
62
|
+
@mq.close
|
63
|
+
log.info "amqp_unsubscribe #{tag}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def destroy
|
67
|
+
teardown
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class WorkerQueue < BaseQueue
|
72
|
+
attr_reader :prefetch
|
73
|
+
|
74
|
+
def initialize(queue_name, opts={})
|
75
|
+
@prefetch = opts.delete(:prefetch) || 1
|
76
|
+
opts[:auto_delete] = false
|
77
|
+
|
78
|
+
super(queue_name, opts)
|
79
|
+
end
|
80
|
+
|
81
|
+
def setup
|
82
|
+
super
|
83
|
+
@mq.prefetch(self.prefetch)
|
84
|
+
end
|
85
|
+
|
86
|
+
def subscribe(amqp_opts={}, opts={})
|
87
|
+
amqp_opts[:ack] = true
|
88
|
+
super(amqp_opts, opts) do |request|
|
89
|
+
begin
|
90
|
+
yield request if block_given?
|
91
|
+
ensure
|
92
|
+
request.ack unless amqp_opts[:auto_ack] == false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class ListenQueue < BaseQueue
|
99
|
+
def initialize(exchange_name, opts={})
|
100
|
+
opts[:exchange_name] = exchange_name
|
101
|
+
queue_name = opts.delete(:queue) || Droid::Utils.generate_queue(exchange_name)
|
102
|
+
super(queue_name, opts)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class ReplyQueue < BaseQueue
|
107
|
+
def initialize(queue_name, opts={})
|
108
|
+
opts[:auto_delete] = true
|
109
|
+
super
|
110
|
+
end
|
111
|
+
|
112
|
+
def setup
|
113
|
+
@mq = MQ.new
|
114
|
+
@q = @mq.queue(queue_name, opts)
|
115
|
+
@ex = nil
|
116
|
+
end
|
117
|
+
|
118
|
+
def temp?
|
119
|
+
true
|
120
|
+
end
|
121
|
+
|
122
|
+
def subscribe(amqp_opts={}, opts={})
|
123
|
+
super(amqp_opts, opts) do |request|
|
124
|
+
yield request if block_given?
|
125
|
+
self.destroy
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def teardown
|
130
|
+
@q.delete
|
131
|
+
super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
module QueueMethods
|
136
|
+
def worker(queue_name, opts={})
|
137
|
+
WorkerQueue.new(queue_name, opts)
|
138
|
+
end
|
139
|
+
|
140
|
+
def listener(exchange_name, opts={})
|
141
|
+
ListenQueue.new(exchange_name, opts)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
class BackwardsCompatibleQueue < BaseQueue
|
146
|
+
def initialize(exchange_name, opts={})
|
147
|
+
opts[:auto_delete] = true unless opts.has_key?(:auto_delete) and opts[:auto_delete] === false
|
148
|
+
opts[:exchange_name] = exchange_name
|
149
|
+
queue_name = opts.delete(:queue) || Droid::Utils.generate_queue(exchange_name)
|
150
|
+
@queue_name, @opts = queue_name, opts
|
151
|
+
end
|
152
|
+
|
153
|
+
def setup
|
154
|
+
@mq = MQ.new
|
155
|
+
@q = @mq.queue(queue_name, opts)
|
156
|
+
@ex = @mq.direct(opts[:exchange_name])
|
157
|
+
|
158
|
+
@mq.prefetch(opts[:prefetch]) if opts[:prefetch]
|
159
|
+
end
|
160
|
+
|
161
|
+
def subscribe(amqp_opts={}, opts={})
|
162
|
+
super(amqp_opts, opts) do |request|
|
163
|
+
if block_given?
|
164
|
+
if opts[:detail]
|
165
|
+
yield request, request.header, request.raw_message if block_given?
|
166
|
+
else
|
167
|
+
yield request if block_given?
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
self
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
module BackwardsCompatibleMethods
|
177
|
+
def listen4(key, orig_opts={}, &block)
|
178
|
+
opts = {}
|
179
|
+
amqp_opts = {}
|
180
|
+
subscribe_opts = {}
|
181
|
+
|
182
|
+
if orig_opts[:prefetch] || orig_opts[:ack]
|
183
|
+
opts[:prefetch] = orig_opts[:prefetch] || 1
|
184
|
+
opts[:ack] = true
|
185
|
+
end
|
186
|
+
if orig_opts[:queue]
|
187
|
+
opts[:queue] = orig_opts[:queue]
|
188
|
+
end
|
189
|
+
if orig_opts[:detail]
|
190
|
+
subscribe_opts[:detail] = true
|
191
|
+
end
|
192
|
+
|
193
|
+
BackwardsCompatibleQueue.new(key, opts).subscribe(amqp_opts, subscribe_opts, &block)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'droid/utils'
|
2
|
+
|
3
|
+
class Droid
|
4
|
+
class UnknownReplyTo < RuntimeError; end
|
5
|
+
|
6
|
+
class Request
|
7
|
+
attr_reader :qobj, :header, :raw_message
|
8
|
+
attr_reader :droid_headers, :msg, :start
|
9
|
+
|
10
|
+
def initialize(qobj, header, raw_message)
|
11
|
+
@qobj = qobj
|
12
|
+
@header = header
|
13
|
+
@raw_message = raw_message
|
14
|
+
|
15
|
+
@droid_headers = Droid::Utils.parse_custom_headers(header.headers)
|
16
|
+
@msg = Droid::Utils.parse_message(raw_message)
|
17
|
+
|
18
|
+
@start = Time.now.getgm.to_i
|
19
|
+
|
20
|
+
@acked = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def q
|
24
|
+
qobj.q
|
25
|
+
end
|
26
|
+
|
27
|
+
def ex
|
28
|
+
qobj.ex
|
29
|
+
end
|
30
|
+
|
31
|
+
def mq
|
32
|
+
qobj.mq
|
33
|
+
end
|
34
|
+
|
35
|
+
def [](field)
|
36
|
+
msg[field.to_s]
|
37
|
+
end
|
38
|
+
|
39
|
+
alias :params :msg
|
40
|
+
|
41
|
+
def age
|
42
|
+
return -1 unless droid_headers[:published_on]
|
43
|
+
start - droid_headers[:published_on]
|
44
|
+
end
|
45
|
+
|
46
|
+
def ttl
|
47
|
+
droid_headers[:ttl] || -1
|
48
|
+
end
|
49
|
+
|
50
|
+
def expired?
|
51
|
+
return false if ttl == -1
|
52
|
+
age > ttl
|
53
|
+
end
|
54
|
+
|
55
|
+
def acked?
|
56
|
+
@acked == true
|
57
|
+
end
|
58
|
+
|
59
|
+
def ack
|
60
|
+
return if acked?
|
61
|
+
header.ack
|
62
|
+
@acked = true
|
63
|
+
end
|
64
|
+
|
65
|
+
def reply(data, opts={}, popts={})
|
66
|
+
opts.merge!(default_publish_opts)
|
67
|
+
reply_to = droid_headers[:reply_to] || self.msg['reply_to']
|
68
|
+
raise UnknownReplyTo unless reply_to
|
69
|
+
Droid.publish_to_q(reply_to, data, opts, popts)
|
70
|
+
end
|
71
|
+
|
72
|
+
def publish(name, data, opts={}, popts={}, &block)
|
73
|
+
opts = default_publish_opts.merge(opts)
|
74
|
+
if block
|
75
|
+
opts[:reply_to] ||= Droid::Utils.generate_reply_to(name)
|
76
|
+
Droid::ReplyQueue.new(opts[:reply_to]).subscribe(&block)
|
77
|
+
end
|
78
|
+
Droid.publish(name, data, opts, popts)
|
79
|
+
end
|
80
|
+
|
81
|
+
def requeue(ropts={})
|
82
|
+
h = droid_headers.dup
|
83
|
+
h[:requeued] = true
|
84
|
+
h.delete(:ttl)
|
85
|
+
popts = { :headers => h }
|
86
|
+
ropts[:ttl] ||= 10
|
87
|
+
Droid.publish_to_q(q.name, msg, ropts, popts)
|
88
|
+
end
|
89
|
+
|
90
|
+
def defer(&blk)
|
91
|
+
EM.defer(lambda do
|
92
|
+
begin
|
93
|
+
blk.call
|
94
|
+
rescue => e
|
95
|
+
Droid.handle_error(e)
|
96
|
+
end
|
97
|
+
end)
|
98
|
+
end
|
99
|
+
|
100
|
+
def default_publish_opts
|
101
|
+
{
|
102
|
+
:event_hash => droid_headers[:event_hash]
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
def data_summary
|
107
|
+
Droid::Utils.format_data_summary(msg, droid_headers)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|