timshadel-starling 0.9.8.01.20080924

Sign up to get free protection for your applications and to get access to all the features.
data/lib/starling.rb ADDED
@@ -0,0 +1,131 @@
1
+ require 'memcache'
2
+
3
+ class Starling < MemCache
4
+
5
+ WAIT_TIME = 0.25
6
+ alias_method :_original_get, :get
7
+ alias_method :_original_delete, :delete
8
+
9
+ ##
10
+ # fetch an item from a queue.
11
+
12
+ def get(*args)
13
+ loop do
14
+ response = _original_get(*args)
15
+ return response unless response.nil?
16
+ sleep WAIT_TIME
17
+ end
18
+ end
19
+
20
+ ##
21
+ # will return the next item or nil
22
+
23
+ def fetch(*args)
24
+ _original_get(*args)
25
+ end
26
+
27
+ ##
28
+ # Delete the key (queue) from all Starling servers. This is necessary
29
+ # because the random way a server is chosen in #get_server_for_key
30
+ # implies that the queue could easily be spread across the entire
31
+ # Starling cluster.
32
+
33
+ def delete(key, expiry = 0)
34
+ raise MemCacheError, "Update of readonly cache" if @readonly
35
+ cache_key = make_cache_key key
36
+
37
+ @buckets.each do |server|
38
+ with_socket_management(server) do |socket|
39
+ socket.write "delete #{cache_key} #{expiry}\r\n"
40
+ socket.gets
41
+ end
42
+ end
43
+ end
44
+
45
+ ##
46
+ # insert +value+ into +queue+.
47
+ #
48
+ # +expiry+ is expressed as a UNIX timestamp
49
+ #
50
+ # If +raw+ is true, +value+ will not be Marshalled. If +raw+ = :yaml, +value+
51
+ # will be serialized with YAML, instead.
52
+
53
+ def set(queue, value, expiry = 0, raw = false)
54
+ retries = 0
55
+ begin
56
+ if raw == :yaml
57
+ value = YAML.dump(value)
58
+ raw = true
59
+ end
60
+
61
+ super(queue, value, expiry, raw)
62
+ rescue MemCache::MemCacheError => e
63
+ retries += 1
64
+ sleep WAIT_TIME
65
+ retry unless retries > 3
66
+ raise e
67
+ end
68
+ end
69
+
70
+ ##
71
+ # returns the number of items in +queue+. If +queue+ is +:all+, a hash of all
72
+ # queue sizes will be returned.
73
+
74
+ def sizeof(queue, statistics = nil)
75
+ statistics ||= stats
76
+
77
+ if queue == :all
78
+ queue_sizes = {}
79
+ available_queues(statistics).each do |queue|
80
+ queue_sizes[queue] = sizeof(queue, statistics)
81
+ end
82
+ return queue_sizes
83
+ end
84
+
85
+ statistics.inject(0) { |m,(k,v)| m + v["queue_#{queue}_items"].to_i }
86
+ end
87
+
88
+ ##
89
+ # returns a list of available (currently allocated) queues.
90
+
91
+ def available_queues(statistics = nil)
92
+ statistics ||= stats
93
+
94
+ statistics.map { |k,v|
95
+ v.keys
96
+ }.flatten.uniq.grep(/^queue_(.*)_items/).map { |v|
97
+ v.gsub(/^queue_/, '').gsub(/_items$/, '')
98
+ }.reject { |v|
99
+ v =~ /_total$/ || v =~ /_expired$/
100
+ }
101
+ end
102
+
103
+ ##
104
+ # iterator to flush +queue+. Each element will be passed to the provided
105
+ # +block+
106
+
107
+ def flush(queue)
108
+ sizeof(queue).times do
109
+ v = get(queue)
110
+ yield v if block_given?
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def get_server_for_key(key)
117
+ raise ArgumentError, "illegal character in key #{key.inspect}" if key =~ /\s/
118
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
119
+ raise MemCacheError, "No servers available" if @servers.empty?
120
+
121
+ bukkits = @buckets.dup
122
+ bukkits.nitems.times do |try|
123
+ n = rand(bukkits.nitems)
124
+ server = bukkits[n]
125
+ return server if server.alive?
126
+ bukkits.delete_at(n)
127
+ end
128
+
129
+ raise MemCacheError, "No servers available (all dead)"
130
+ end
131
+ end
@@ -0,0 +1,216 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'rubygems'
4
+ require 'fileutils'
5
+ require 'memcache'
6
+ require 'digest/md5'
7
+ require 'starling'
8
+
9
+ require 'starling/server'
10
+
11
+ class StarlingServer::PersistentQueue
12
+ remove_const :SOFT_LOG_MAX_SIZE
13
+ SOFT_LOG_MAX_SIZE = 16 * 1024 # 16 KB
14
+ end
15
+
16
+ def safely_fork(&block)
17
+ # anti-race juice:
18
+ blocking = true
19
+ Signal.trap("USR1") { blocking = false }
20
+
21
+ pid = Process.fork(&block)
22
+
23
+ while blocking
24
+ sleep 0.1
25
+ end
26
+
27
+ pid
28
+ end
29
+
30
+ describe "StarlingServer" do
31
+ before do
32
+ @tmp_path = File.join(File.dirname(__FILE__), "tmp")
33
+
34
+ begin
35
+ Dir::mkdir(@tmp_path)
36
+ rescue Errno::EEXIST
37
+ end
38
+
39
+ @server_pid = safely_fork do
40
+ server = StarlingServer::Base.new(:host => '127.0.0.1',
41
+ :port => 22133,
42
+ :path => @tmp_path,
43
+ :logger => Logger.new(STDERR),
44
+ :log_level => Logger::FATAL)
45
+ Signal.trap("INT") {
46
+ server.stop
47
+ exit
48
+ }
49
+
50
+ Process.kill("USR1", Process.ppid)
51
+ server.run
52
+ end
53
+
54
+ @client = MemCache.new('127.0.0.1:22133')
55
+
56
+ end
57
+
58
+ it "should test if temp_path exists and is writeable" do
59
+ File.exist?(@tmp_path).should be_true
60
+ File.directory?(@tmp_path).should be_true
61
+ File.writable?(@tmp_path).should be_true
62
+ end
63
+
64
+ it "should set and get" do
65
+ v = rand((2**32)-1)
66
+ @client.get('test_set_and_get_one_entry').should be_nil
67
+ @client.set('test_set_and_get_one_entry', v)
68
+ @client.get('test_set_and_get_one_entry').should eql(v)
69
+ end
70
+
71
+ it "should respond to delete" do
72
+ @client.delete("my_queue").should eql("END\r\n")
73
+ starling_client = Starling.new('127.0.0.1:22133')
74
+ starling_client.set('my_queue', 50)
75
+ starling_client.available_queues.size.should eql(1)
76
+ starling_client.delete("my_queue")
77
+ starling_client.available_queues.size.should eql(0)
78
+ end
79
+
80
+ it "should expire entries" do
81
+ v = rand((2**32)-1)
82
+ @client.get('test_set_with_expiry').should be_nil
83
+ now = Time.now.to_i
84
+ @client.set('test_set_with_expiry', v + 2, now)
85
+ @client.set('test_set_with_expiry', v)
86
+ sleep(1.0)
87
+ @client.get('test_set_with_expiry').should eql(v)
88
+ end
89
+
90
+ it "should have age stat" do
91
+ now = Time.now.to_i
92
+ @client.set('test_age', 'nibbler')
93
+ sleep(1.0)
94
+ @client.get('test_age').should eql('nibbler')
95
+
96
+ stats = @client.stats['127.0.0.1:22133']
97
+ stats.has_key?('queue_test_age_age').should be_true
98
+ (stats['queue_test_age_age'] >= 1000).should be_true
99
+ end
100
+
101
+ it "should rotate log" do
102
+ log_rotation_path = File.join(@tmp_path, 'test_log_rotation')
103
+
104
+ Dir.glob("#{log_rotation_path}*").each do |file|
105
+ File.unlink(file) rescue nil
106
+ end
107
+ @client.get('test_log_rotation').should be_nil
108
+
109
+ v = 'x' * 8192
110
+
111
+ @client.set('test_log_rotation', v)
112
+ File.size(log_rotation_path).should eql(8207)
113
+ @client.get('test_log_rotation')
114
+
115
+ @client.get('test_log_rotation').should be_nil
116
+
117
+ @client.set('test_log_rotation', v)
118
+ @client.get('test_log_rotation').should eql(v)
119
+
120
+ File.size(log_rotation_path).should eql(1)
121
+ # rotated log should be erased after a successful roll.
122
+ Dir.glob("#{log_rotation_path}*").size.should eql(1)
123
+ end
124
+
125
+ it "should output statistics per server" do
126
+ stats = @client.stats
127
+ assert_kind_of Hash, stats
128
+ assert stats.has_key?('127.0.0.1:22133')
129
+
130
+ server_stats = stats['127.0.0.1:22133']
131
+
132
+ basic_stats = %w( bytes pid time limit_maxbytes cmd_get version
133
+ bytes_written cmd_set get_misses total_connections
134
+ curr_connections curr_items uptime get_hits total_items
135
+ rusage_system rusage_user bytes_read )
136
+
137
+ basic_stats.each do |stat|
138
+ server_stats.has_key?(stat).should be_true
139
+ end
140
+ end
141
+
142
+ it "should return valid response with unkown command" do
143
+ response = @client.add('blah', 1)
144
+ response.should eql("CLIENT_ERROR bad command line format\r\n")
145
+ end
146
+
147
+ it "should disconnect and reconnect again" do
148
+ v = rand(2**32-1)
149
+ @client.set('test_that_disconnecting_and_reconnecting_works', v)
150
+ @client.reset
151
+ @client.get('test_that_disconnecting_and_reconnecting_works').should eql(v)
152
+ end
153
+
154
+ it "should use epoll on linux" do
155
+ # this may take a few seconds.
156
+ # the point is to make sure that we're using epoll on Linux, so we can
157
+ # handle more than 1024 connections.
158
+
159
+ unless IO::popen("uname").read.chomp == "Linux"
160
+ pending "skipping epoll test: not on Linux"
161
+ end
162
+
163
+ fd_limit = IO::popen("bash -c 'ulimit -n'").read.chomp.to_i
164
+ unless fd_limit > 1024
165
+ pending "skipping epoll test: 'ulimit -n' = #{fd_limit}, need > 1024"
166
+ end
167
+
168
+ v = rand(2**32 - 1)
169
+ @client.set('test_epoll', v)
170
+
171
+ # we can't open 1024 connections to memcache from within this process,
172
+ # because we will hit ruby's 1024 fd limit ourselves!
173
+ pid1 = safely_fork do
174
+ unused_sockets = []
175
+ 600.times do
176
+ unused_sockets << TCPSocket.new("127.0.0.1", 22133)
177
+ end
178
+ Process.kill("USR1", Process.ppid)
179
+ sleep 90
180
+ end
181
+ pid2 = safely_fork do
182
+ unused_sockets = []
183
+ 600.times do
184
+ unused_sockets << TCPSocket.new("127.0.0.1", 22133)
185
+ end
186
+ Process.kill("USR1", Process.ppid)
187
+ sleep 90
188
+ end
189
+
190
+ begin
191
+ client = MemCache.new('127.0.0.1:22133')
192
+ client.get('test_epoll').should eql(v)
193
+ ensure
194
+ Process.kill("TERM", pid1)
195
+ Process.kill("TERM", pid2)
196
+ end
197
+ end
198
+
199
+ it "should raise error if queue collection is an invalid path" do
200
+ invalid_path = nil
201
+ while invalid_path.nil? || File.exist?(invalid_path)
202
+ invalid_path = File.join('/', Digest::MD5.hexdigest(rand(2**32-1).to_s)[0,8])
203
+ end
204
+
205
+ lambda {
206
+ StarlingServer::QueueCollection.new(invalid_path)
207
+ }.should raise_error(StarlingServer::InaccessibleQueuePath)
208
+ end
209
+
210
+ after do
211
+ Process.kill("INT", @server_pid)
212
+ Process.wait(@server_pid)
213
+ @client.reset
214
+ FileUtils.rm(Dir.glob(File.join(@tmp_path, '*')))
215
+ end
216
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: timshadel-starling
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.8.01.20080924
5
+ platform: ruby
6
+ authors:
7
+ - Blaine Cook
8
+ - Chris Wanstrath
9
+ - Britt Selvitelle
10
+ - Glenn Rempe
11
+ - Abdul-Rahman Advany
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2008-08-13 00:00:00 -07:00
17
+ default_executable:
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: memcache-client
21
+ version_requirement:
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: "0"
27
+ version:
28
+ - !ruby/object:Gem::Dependency
29
+ name: eventmachine
30
+ version_requirement:
31
+ version_requirements: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 0.12.0
36
+ version:
37
+ description: Starling is a lightweight, transactional, distributed queue server
38
+ email:
39
+ - blaine@twitter.com
40
+ - chris@ozmm.org
41
+ - abdulrahman@advany.com
42
+ executables:
43
+ - starling
44
+ - starling_top
45
+ extensions: []
46
+
47
+ extra_rdoc_files:
48
+ - README.rdoc
49
+ - CHANGELOG
50
+ - LICENSE
51
+ files:
52
+ - README.rdoc
53
+ - LICENSE
54
+ - CHANGELOG
55
+ - Rakefile
56
+ - lib/starling/handler.rb
57
+ - lib/starling/persistent_queue.rb
58
+ - lib/starling/queue_collection.rb
59
+ - lib/starling/server_runner.rb
60
+ - lib/starling/server.rb
61
+ - lib/starling.rb
62
+ - etc/starling.redhat
63
+ - etc/starling.ubuntu
64
+ - etc/sample-config.yml
65
+ has_rdoc: true
66
+ homepage: http://github.com/starling/starling/
67
+ post_install_message:
68
+ rdoc_options:
69
+ - --quiet
70
+ - --title
71
+ - starling documentation
72
+ - --opname
73
+ - index.html
74
+ - --line-numbers
75
+ - --main
76
+ - README.rdoc
77
+ - --inline-source
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: "0"
85
+ version:
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ version:
92
+ requirements: []
93
+
94
+ rubyforge_project:
95
+ rubygems_version: 1.2.0
96
+ signing_key:
97
+ specification_version: 2
98
+ summary: Starling is a lightweight, transactional, distributed queue server
99
+ test_files:
100
+ - spec/starling_server_spec.rb