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.
@@ -1,129 +1 @@
1
- require 'memcache'
2
- require 'yaml'
3
- require 'digest/sha1'
4
-
5
- # Manages a pool of memcache servers. This class should not be called
6
- # outside of the reactor - it does not account for asynchronous access
7
- # to the server list.
8
- module MemcacheCluster
9
- extend self
10
-
11
- HEROKU_NAMESPACE = '0Xfa15837Z' # heroku's internal memcache namespace
12
-
13
- # A MemCache object configured with heroku's internal memcache namespace.
14
- def heroku
15
- cache(HEROKU_NAMESPACE)
16
- end
17
-
18
- def cache_retry(prefix, opts={})
19
- opts[:retries] ||= 5
20
- opts[:delay] ||= 0.5
21
-
22
- retried = 0
23
- begin
24
- c = cache(prefix)
25
- yield c if block_given?
26
- rescue MemCache::MemCacheError => e
27
- Log.error "#{e.class} -> #{e.message}", :exception => e
28
- raise if retried > opts[:retries]
29
- retried += 1
30
- sleep opts[:delay]
31
- @caches = { }
32
- retry
33
- end
34
- end
35
-
36
- def set(prefix, *args)
37
- res = nil
38
- cache_retry(prefix) do |c|
39
- res = c.set(*args)
40
- end
41
- res
42
- end
43
-
44
- def get(prefix, *args)
45
- res = nil
46
- cache_retry(prefix) do |c|
47
- res = c.get(*args)
48
- end
49
- res
50
- end
51
-
52
- # Create listeners for standard memcache cluster related topics.
53
- def attach(droid, file='memcached.yml')
54
- load_from_file(file)
55
-
56
- droid.listen4('memcache.up', :queue => "memcache.up.#{LocalStats.this_instance_name}.#$$") { |msg| add(msg['address'], msg['port']) }
57
- droid.listen4('instance.down', :queue => "instance.down.#{LocalStats.this_instance_name}.#$$") { |msg| remove(msg['local_ip']) if msg['slot'] == 'memcache' }
58
- EM.add_timer(1) { droid.publish('memcache.needed', {}) }
59
- end
60
-
61
- # A MemCache object configured with the given prefix.
62
- def cache(prefix, options={})
63
- caches[prefix] ||=
64
- MemCache.new(servers, options.merge(:namespace => prefix))
65
- end
66
-
67
- alias_method :[], :cache
68
-
69
- def caches
70
- reload_if_stale
71
- @caches ||= {}
72
- end
73
-
74
- def servers
75
- reload_if_stale
76
- @servers ||= []
77
- end
78
-
79
- def add(ip, port)
80
- host = [ip, port].join(':')
81
- return if servers.include?(host)
82
-
83
- log { "#{host} added" }
84
- @servers.push host
85
- @servers.sort!
86
- @caches = {}
87
- write_to_file
88
- @last_read = Time.now
89
- end
90
-
91
- def remove(host)
92
- if servers.reject!{ |s| s =~ /^#{host}/ }
93
- log { "#{host} removed" }
94
- caches.clear
95
- write_to_file
96
- end
97
- end
98
-
99
- def reload_if_stale
100
- if @last_read &&
101
- (Time.now - @last_read) > 5 &&
102
- File.mtime(@file) > @last_read
103
- log { "server list modified. reloading." }
104
- load_from_file(@file)
105
- end
106
- rescue => boom
107
- # ignore errors accessing/reading file.
108
- end
109
-
110
- def load_from_file(file)
111
- @file = file
112
- @last_read = Time.now
113
- @servers = YAML.load(File.read(file)) rescue []
114
- @caches = {}
115
- end
116
-
117
- def write_to_file
118
- log { "writing server list: #{@file}" }
119
- File.open(@file, 'w') do |f|
120
- f.flock(File::LOCK_EX)
121
- f.write YAML.dump(@servers)
122
- f.flock(File::LOCK_UN)
123
- end
124
- end
125
-
126
- def log(type=:debug)
127
- Log.send(type, "memcached: #{yield}")
128
- end
129
- end
1
+ require 'droid/heroku/memcache_cluster'
data/lib/stats.rb CHANGED
@@ -1,30 +1 @@
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
1
+ require 'droid/heroku/stats'
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'AMQP Publish' do
4
+ before do
5
+ @json, @publish_opts = Droid::Utils.format_publish({:x => 1, :y => 2}, {}, {})
6
+ Droid::Utils.stubs(:format_publish).with({:x => 1, :y => 2}, {}, {}).returns([@json, @publish_opts])
7
+ end
8
+
9
+ it "publishes a message to a queue" do
10
+ @q = mock('queue')
11
+ ::MQ.expects(:queue).with('topic').returns(@q)
12
+ @q.expects(:publish).with(@json, @publish_opts)
13
+ Droid.publish_to_q('topic', :x => 1, :y => 2)
14
+ end
15
+
16
+ it "publishes a message to an exchange" do
17
+ @ex = mock('exchange')
18
+ ::MQ.expects(:direct).with('topic').returns(@ex)
19
+ @ex.expects(:publish).with(@json, @publish_opts)
20
+ Droid.publish_to_ex('topic', :x => 1, :y => 2)
21
+ end
22
+
23
+ it "by default publishes to an exchange" do
24
+ Droid.expects(:publish_to_ex).with('topic', {:x => 1, :y => 2}, {}, {})
25
+ Droid.publish('topic', :x => 1, :y => 2)
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Request' do
4
+ before do
5
+ @mq = mock("MQ")
6
+ @exchange = mock("MQ Exchange")
7
+ @queue = mock("MQ Queue")
8
+
9
+ @qobj = mock("Droid::BasicQueue Instance")
10
+ @qobj.stubs(:q).returns(@queue)
11
+ @qobj.stubs(:mq).returns(@mq)
12
+ @qobj.stubs(:ex).returns(@exchange)
13
+
14
+ @header = mock("amqp header", :headers => {})
15
+
16
+ @raw_message = '{"x":123,"y":"abc"}'
17
+
18
+ @res = Droid::Request.new(@qobj, @header, @raw_message)
19
+ end
20
+
21
+ it "allows access to response via symbols or strings in array access" do
22
+ @res['x'].should == 123
23
+ @res[:x].should == 123
24
+ @res['y'].should == 'abc'
25
+ @res[:y].should == 'abc'
26
+ end
27
+
28
+ it "calls ack on the header" do
29
+ @header.expects(:ack)
30
+ @res.ack
31
+ end
32
+
33
+ it "calls q on the qobj" do
34
+ @qobj.expects(:q)
35
+ @res.q
36
+ end
37
+
38
+ it "calls ex on the qobj" do
39
+ @qobj.expects(:ex)
40
+ @res.ex
41
+ end
42
+
43
+ it "calls mq on the qobj" do
44
+ @qobj.expects(:mq)
45
+ @res.mq
46
+ end
47
+ end
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'baconmocha'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+
7
+ require 'droid'
8
+
9
+ Droid.log.level = Logger::FATAL
10
+
11
+ Bacon.summary_on_exit
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Utils' do
4
+ before do
5
+ end
6
+
7
+ it "parses a json message" do
8
+ Droid::Utils.parse_message('{"x":1,"y":2}').should == {'x' => 1, 'y' => 2}
9
+ end
10
+
11
+ it "parses custom headers and force integers on a few values" do
12
+ headers = {
13
+ :x => '1',
14
+ :y => '2',
15
+ :reply_to => 'q.random.reply',
16
+ :published_on => '12345',
17
+ :event_hash => 'x123'
18
+ }
19
+
20
+ h = Droid::Utils.parse_custom_headers(headers)
21
+
22
+ h.size.should == 6
23
+ h[:ttl].should == -1
24
+ h[:reply_to].should == 'q.random.reply'
25
+ h[:published_on].should == 12345
26
+ h[:event_hash].should == 'x123'
27
+ end
28
+
29
+ it "raises an exception if the data to format for publish is not a hash" do
30
+ lambda { Droid::Utils.format_publish('bad payload') }.should.raise Droid::BadPayload
31
+ end
32
+
33
+ it "generates a name for the instance" do
34
+ Socket.stubs(:gethostname).returns('deepblue.123')
35
+ Droid::Utils.generate_name_for_instance('woo').should == 'woo.deepblue.123'
36
+ end
37
+
38
+ it "generates queue name, really more like a generic identifier" do
39
+ Socket.stubs(:gethostname).returns('deepblue.123')
40
+ Droid::Utils.generate_queue('topic').should == "topic.deepblue.123.#{$$}"
41
+ Droid::Utils.generate_queue('topic', 'local').should == "topic.deepblue.123.local"
42
+ end
43
+ end
@@ -1,18 +1,11 @@
1
- require File.dirname(__FILE__) + '/../lib/droid'
2
- require File.dirname(__FILE__) + '/base'
3
-
4
- require 'rubygems'
5
- require 'thread'
6
- require 'bacon'
7
-
8
- Bacon.summary_on_exit
1
+ require 'spec_helper'
9
2
 
10
3
  Thread.new do
11
4
  sleep 2
12
5
  TCPServer.new('localhost', 20_001).accept.close
13
6
  end
14
7
 
15
- describe Droid do
8
+ describe 'Connectivity' do
16
9
  it "waits for the rabbitmq server to come up before opening the amqp connection" do
17
10
  start = Time.now
18
11
  Droid.wait_for_tcp_port('localhost', 20_001)
metadata CHANGED
@@ -3,22 +3,22 @@ name: droid
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
+ - 1
6
7
  - 0
7
- - 9
8
- - 5
9
- version: 0.9.5
8
+ - 0
9
+ version: 1.0.0
10
10
  platform: ruby
11
- authors: []
12
-
11
+ authors:
12
+ - Ricardo Chimal, Jr.
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-09-27 00:00:00 -07:00
18
- default_executable:
17
+ date: 2010-10-21 00:00:00 -07:00
18
+ default_executable: bleedq
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
- name: json_pure
21
+ name: baconmocha
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
24
  requirements:
@@ -27,67 +27,136 @@ dependencies:
27
27
  segments:
28
28
  - 0
29
29
  version: "0"
30
- type: :runtime
30
+ type: :development
31
31
  version_requirements: *id001
32
32
  - !ruby/object:Gem::Dependency
33
- name: amqp
33
+ name: json_pure
34
34
  prerelease: false
35
35
  requirement: &id002 !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  segments:
40
+ - 1
41
+ - 2
40
42
  - 0
41
- version: "0"
43
+ version: 1.2.0
42
44
  type: :runtime
43
45
  version_requirements: *id002
44
46
  - !ruby/object:Gem::Dependency
45
- name: bunny
47
+ name: rest-client
46
48
  prerelease: false
47
49
  requirement: &id003 !ruby/object:Gem::Requirement
48
50
  requirements:
49
51
  - - ">="
50
52
  - !ruby/object:Gem::Version
51
53
  segments:
54
+ - 1
55
+ - 2
52
56
  - 0
53
- version: "0"
57
+ version: 1.2.0
54
58
  type: :runtime
55
59
  version_requirements: *id003
56
60
  - !ruby/object:Gem::Dependency
57
- name: rest-client
61
+ name: amqp
58
62
  prerelease: false
59
63
  requirement: &id004 !ruby/object:Gem::Requirement
60
64
  requirements:
61
- - - ">="
65
+ - - "="
62
66
  - !ruby/object:Gem::Version
63
67
  segments:
64
68
  - 0
65
- version: "0"
69
+ - 6
70
+ - 7
71
+ version: 0.6.7
66
72
  type: :runtime
67
73
  version_requirements: *id004
68
- description:
69
- email:
70
- executables: []
71
-
74
+ - !ruby/object:Gem::Dependency
75
+ name: bunny
76
+ prerelease: false
77
+ requirement: &id005 !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ~>
80
+ - !ruby/object:Gem::Version
81
+ segments:
82
+ - 0
83
+ - 6
84
+ - 0
85
+ version: 0.6.0
86
+ type: :runtime
87
+ version_requirements: *id005
88
+ - !ruby/object:Gem::Dependency
89
+ name: SystemTimer
90
+ prerelease: false
91
+ requirement: &id006 !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ~>
94
+ - !ruby/object:Gem::Version
95
+ segments:
96
+ - 1
97
+ - 2
98
+ - 0
99
+ version: 1.2.0
100
+ type: :runtime
101
+ version_requirements: *id006
102
+ - !ruby/object:Gem::Dependency
103
+ name: eventmachine_httpserver
104
+ prerelease: false
105
+ requirement: &id007 !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "="
108
+ - !ruby/object:Gem::Version
109
+ segments:
110
+ - 0
111
+ - 2
112
+ - 0
113
+ version: 0.2.0
114
+ type: :runtime
115
+ version_requirements: *id007
116
+ description: Easy to use AMQP Library with constructs for typical usage patterns
117
+ email: ricardo@heroku.com
118
+ executables:
119
+ - bleedq
72
120
  extensions: []
73
121
 
74
- extra_rdoc_files:
75
- - README.md
122
+ extra_rdoc_files: []
123
+
76
124
  files:
125
+ - .gitignore
126
+ - Rakefile
127
+ - VERSION
128
+ - bin/bleedq
129
+ - droid.gemspec
130
+ - examples/async_reply.rb
131
+ - examples/heroku_async_reply.rb
132
+ - examples/sync.rb
133
+ - examples/worker.rb
77
134
  - lib/droid.rb
135
+ - lib/droid/em.rb
136
+ - lib/droid/heroku.rb
137
+ - lib/droid/heroku/local_stats.rb
138
+ - lib/droid/heroku/logger_client.rb
139
+ - lib/droid/heroku/memcache_cluster.rb
140
+ - lib/droid/heroku/stats.rb
141
+ - lib/droid/json_server.rb
142
+ - lib/droid/monkey.rb
143
+ - lib/droid/publish.rb
144
+ - lib/droid/queue.rb
145
+ - lib/droid/request.rb
78
146
  - lib/droid/sync.rb
147
+ - lib/droid/utilization.rb
148
+ - lib/droid/utils.rb
79
149
  - lib/heroku_droid.rb
80
150
  - lib/local_stats.rb
81
151
  - lib/memcache_cluster.rb
82
152
  - lib/stats.rb
83
- - lib/utilization.rb
84
- - vendor/logger_client/Rakefile
85
- - vendor/logger_client/init.rb
86
- - vendor/logger_client/lib/logger_client.rb
87
- - vendor/logger_client/test.rb
88
- - README.md
153
+ - spec/publish_spec.rb
154
+ - spec/response_spec.rb
155
+ - spec/spec_helper.rb
156
+ - spec/utils_spec.rb
157
+ - spec/wait_for_port_spec.rb
89
158
  has_rdoc: true
90
- homepage:
159
+ homepage: http://heroku.com
91
160
  licenses: []
92
161
 
93
162
  post_install_message:
@@ -111,13 +180,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
180
  version: "0"
112
181
  requirements: []
113
182
 
114
- rubyforge_project: droid
183
+ rubyforge_project:
115
184
  rubygems_version: 1.3.6
116
185
  signing_key:
117
186
  specification_version: 3
118
- summary: AMQP wrapper
187
+ summary: AMQP Wrapper Library
119
188
  test_files:
120
- - test/base.rb
121
- - test/droid_test.rb
122
- - test/heroku_droid_test.rb
123
- - test/wait_for_port_test.rb
189
+ - spec/publish_spec.rb
190
+ - spec/response_spec.rb
191
+ - spec/spec_helper.rb
192
+ - spec/utils_spec.rb
193
+ - spec/wait_for_port_spec.rb
194
+ - examples/async_reply.rb
195
+ - examples/heroku_async_reply.rb
196
+ - examples/sync.rb
197
+ - examples/worker.rb