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/CHANGELOG +52 -0
- data/LICENSE +20 -0
- data/README.rdoc +106 -0
- data/Rakefile +22 -0
- data/bin/starling +6 -0
- data/bin/starling_top +57 -0
- data/etc/sample-config.yml +9 -0
- data/etc/starling.redhat +63 -0
- data/etc/starling.ubuntu +71 -0
- data/lib/starling/handler.rb +234 -0
- data/lib/starling/persistent_queue.rb +151 -0
- data/lib/starling/queue_collection.rb +141 -0
- data/lib/starling/server.rb +125 -0
- data/lib/starling/server_runner.rb +297 -0
- data/lib/starling.rb +131 -0
- data/spec/starling_server_spec.rb +216 -0
- metadata +100 -0
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
|