kim-toms-starling 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,317 @@
1
+ require File.join(File.dirname(__FILE__), 'server')
2
+ require 'erb'
3
+ require 'fileutils'
4
+ require 'optparse'
5
+ require 'yaml'
6
+ require 'fileutils'
7
+
8
+ module StarlingServer
9
+ class Runner
10
+
11
+ attr_accessor :options
12
+ private :options, :options=
13
+
14
+ def self.run
15
+ new
16
+ end
17
+
18
+ def self.shutdown
19
+ @@instance.shutdown
20
+ end
21
+
22
+ def initialize
23
+ @@instance = self
24
+ parse_options
25
+
26
+ @process = ProcessHelper.new(options[:logger], options[:pid_file], options[:user], options[:group])
27
+
28
+ pid = @process.running?
29
+ if pid
30
+ STDERR.puts "There is already a Starling process running (pid #{pid}), exiting."
31
+ exit(1)
32
+ elsif pid.nil?
33
+ STDERR.puts "Cleaning up stale pidfile at #{options[:pid_file]}."
34
+ end
35
+
36
+ start
37
+ end
38
+
39
+ def load_config_file(filename)
40
+ config = YAML.load(ERB.new(File.read(filename)).result)
41
+
42
+ unless config.is_a?(Hash)
43
+ STDERR.puts "Config file does not contain a hash: #{filename}, exiting."
44
+ exit(1)
45
+ end
46
+
47
+ if config['starling'].nil?
48
+ STDERR.puts "Missing starling section in config file: #{filename}, exiting."
49
+ exit(1)
50
+ end
51
+
52
+ config['starling'].each do |key, value|
53
+ # alias some keys
54
+ case key
55
+ when "queue_path" then key = "path"
56
+ when "log_file" then key = "logger"
57
+ end
58
+
59
+ if %w(logger path pid_file).include?(key)
60
+ value = File.expand_path(value)
61
+ end
62
+
63
+ options[key.to_sym] = value
64
+
65
+ if options[:log_level].instance_of?(String)
66
+ options[:log_level] = Logger.const_get(options[:log_level])
67
+ end
68
+ end
69
+ end
70
+
71
+ def parse_options
72
+ self.options = { :host => '127.0.0.1',
73
+ :port => 22122,
74
+ :path => File.join('', 'var', 'spool', 'starling'),
75
+ :log_level => Logger::INFO,
76
+ :daemonize => false,
77
+ :timeout => 0,
78
+ :pid_file => File.join('', 'var', 'run', 'starling.pid') }
79
+
80
+ OptionParser.new do |opts|
81
+ opts.summary_width = 25
82
+
83
+ opts.banner = "Starling (#{StarlingServer::VERSION})\n\n",
84
+ "usage: starling [options...]\n",
85
+ " starling --help\n",
86
+ " starling --version\n"
87
+
88
+ opts.separator ""
89
+ opts.separator "Configuration:"
90
+
91
+ opts.on("-f", "--config FILENAME",
92
+ "Config file (yaml) to load") do |filename|
93
+ load_config_file(filename)
94
+ end
95
+
96
+ opts.on("-q", "--queue_path PATH",
97
+ :REQUIRED,
98
+ "Path to store Starling queue logs", "(default: #{options[:path]})") do |queue_path|
99
+ options[:path] = File.expand_path(queue_path)
100
+ end
101
+
102
+ opts.separator ""; opts.separator "Network:"
103
+
104
+ opts.on("-hHOST", "--host HOST", "Interface on which to listen (default: #{options[:host]})") do |host|
105
+ options[:host] = host
106
+ end
107
+
108
+ opts.on("-pHOST", "--port PORT", Integer, "TCP port on which to listen (default: #{options[:port]})") do |port|
109
+ options[:port] = port
110
+ end
111
+
112
+ opts.separator ""; opts.separator "Process:"
113
+
114
+ opts.on("-d", "Run as a daemon.") do
115
+ options[:daemonize] = true
116
+ end
117
+
118
+ opts.on("-PFILE", "--pid FILENAME", "save PID in FILENAME when using -d option.", "(default: #{options[:pid_file]})") do |pid_file|
119
+ options[:pid_file] = File.expand_path(pid_file)
120
+ end
121
+
122
+ opts.on("-u", "--user USER", "User to run as") do |user|
123
+ options[:user] = user.to_i == 0 ? Etc.getpwnam(user).uid : user.to_i
124
+ end
125
+
126
+ opts.on("-gGROUP", "--group GROUP", "Group to run as") do |group|
127
+ options[:group] = group.to_i == 0 ? Etc.getgrnam(group).gid : group.to_i
128
+ end
129
+
130
+ opts.separator ""; opts.separator "Logging:"
131
+
132
+ opts.on("-L", "--log [FILE]", "Path to print debugging information.") do |log_path|
133
+ options[:logger] = File.expand_path(log_path)
134
+ end
135
+
136
+ begin
137
+ require 'syslog_logger'
138
+
139
+ opts.on("-l", "--syslog CHANNEL", "Write logs to the syslog instead of a log file.") do |channel|
140
+ options[:syslog_channel] = channel
141
+ end
142
+ rescue LoadError
143
+ end
144
+
145
+ opts.on("-v", "Increase logging verbosity (may be used multiple times).") do
146
+ options[:log_level] -= 1
147
+ end
148
+
149
+ opts.on("-t", "--timeout [SECONDS]", Integer,
150
+ "Time in seconds before disconnecting inactive clients (0 to disable).",
151
+ "(default: #{options[:timeout]})") do |timeout|
152
+ options[:timeout] = timeout
153
+ end
154
+
155
+ opts.separator ""; opts.separator "Miscellaneous:"
156
+
157
+ opts.on_tail("-?", "--help", "Display this usage information.") do
158
+ puts "#{opts}\n"
159
+ exit
160
+ end
161
+
162
+ opts.on_tail("-V", "--version", "Print version number and exit.") do
163
+ puts "Starling #{StarlingServer::VERSION}\n\n"
164
+ exit
165
+ end
166
+ end.parse!
167
+ end
168
+
169
+ def start
170
+ drop_privileges
171
+
172
+ @process.daemonize if options[:daemonize]
173
+
174
+ setup_signal_traps
175
+ @process.write_pid_file
176
+
177
+ STDOUT.puts "Starting at #{options[:host]}:#{options[:port]}."
178
+ @server = StarlingServer::Base.new(options)
179
+ @server.run
180
+
181
+ @process.remove_pid_file
182
+ end
183
+
184
+ def drop_privileges
185
+ Process.egid = options[:group] if options[:group]
186
+ Process.euid = options[:user] if options[:user]
187
+ end
188
+
189
+ def shutdown
190
+ begin
191
+ STDOUT.puts "Shutting down."
192
+ StarlingServer::Base.logger.info "Shutting down."
193
+ @server.stop
194
+ rescue Object => e
195
+ STDERR.puts "There was an error shutting down: #{e}"
196
+ exit(70)
197
+ end
198
+ end
199
+
200
+ def setup_signal_traps
201
+ Signal.trap("INT") { shutdown }
202
+ Signal.trap("TERM") { shutdown }
203
+ end
204
+ end
205
+
206
+ class ProcessHelper
207
+
208
+ def initialize(log_file = nil, pid_file = nil, user = nil, group = nil)
209
+ @log_file = log_file
210
+ @pid_file = pid_file
211
+ @user = user
212
+ @group = group
213
+ end
214
+
215
+ def safefork
216
+ begin
217
+ if pid = fork
218
+ return pid
219
+ end
220
+ rescue Errno::EWOULDBLOCK
221
+ sleep 5
222
+ retry
223
+ end
224
+ end
225
+
226
+ def daemonize
227
+ sess_id = detach_from_terminal
228
+ exit if pid = safefork
229
+
230
+ Dir.chdir("/")
231
+ File.umask 0000
232
+
233
+ close_io_handles
234
+ redirect_io
235
+
236
+ return sess_id
237
+ end
238
+
239
+ def detach_from_terminal
240
+ srand
241
+ safefork and exit
242
+
243
+ unless sess_id = Process.setsid
244
+ raise "Couldn't detach from controlling terminal."
245
+ end
246
+
247
+ trap 'SIGHUP', 'IGNORE'
248
+
249
+ sess_id
250
+ end
251
+
252
+ def close_io_handles
253
+ ObjectSpace.each_object(IO) do |io|
254
+ unless [STDIN, STDOUT, STDERR].include?(io)
255
+ begin
256
+ io.close unless io.closed?
257
+ rescue Exception
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ def redirect_io
264
+ begin; STDIN.reopen('/dev/null'); rescue Exception; end
265
+
266
+ if @log_file
267
+ begin
268
+ STDOUT.reopen(@log_file, "a")
269
+ STDOUT.sync = true
270
+ rescue Exception
271
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
272
+ end
273
+ else
274
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
275
+ end
276
+
277
+ begin; STDERR.reopen(STDOUT); rescue Exception; end
278
+ STDERR.sync = true
279
+ end
280
+
281
+ def rescue_exception
282
+ begin
283
+ yield
284
+ rescue Exception
285
+ end
286
+ end
287
+
288
+ def write_pid_file
289
+ return unless @pid_file
290
+ FileUtils.mkdir_p(File.dirname(@pid_file))
291
+ File.open(@pid_file, "w") { |f| f.write(Process.pid) }
292
+ File.chmod(0644, @pid_file)
293
+ end
294
+
295
+ def remove_pid_file
296
+ return unless @pid_file
297
+ File.unlink(@pid_file) if File.exists?(@pid_file)
298
+ end
299
+
300
+ def running?
301
+ return false unless @pid_file
302
+
303
+ pid = File.read(@pid_file).chomp.to_i rescue nil
304
+ pid = nil if pid == 0
305
+ return false unless pid
306
+
307
+ begin
308
+ Process.kill(0, pid)
309
+ return pid
310
+ rescue Errno::ESRCH
311
+ return nil
312
+ rescue Errno::EPERM
313
+ return pid
314
+ end
315
+ end
316
+ end
317
+ end
data/lib/starling.rb ADDED
@@ -0,0 +1,139 @@
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
+ with_servers do
35
+ _original_delete(key, expiry)
36
+ end
37
+ end
38
+
39
+ ##
40
+ # Provides a way to work with a specific list of servers by
41
+ # forcing all calls to #get_server_for_key to use a specific
42
+ # server, and changing that server each time that the call
43
+ # yields to the block provided. This helps work around the
44
+ # normally random nature of the #get_server_for_key method.
45
+ #
46
+ def with_servers(my_servers = self.servers.dup)
47
+ return unless block_given?
48
+ my_servers.each do |server|
49
+ yield
50
+ end
51
+ end
52
+
53
+ ##
54
+ # insert +value+ into +queue+.
55
+ #
56
+ # +expiry+ is expressed as a UNIX timestamp
57
+ #
58
+ # If +raw+ is true, +value+ will not be Marshalled. If +raw+ = :yaml, +value+
59
+ # will be serialized with YAML, instead.
60
+
61
+ def set(queue, value, expiry = 0, raw = false)
62
+ retries = 0
63
+ begin
64
+ if raw == :yaml
65
+ value = YAML.dump(value)
66
+ raw = true
67
+ end
68
+
69
+ super(queue, value, expiry, raw)
70
+ rescue MemCache::MemCacheError => e
71
+ retries += 1
72
+ sleep WAIT_TIME
73
+ retry unless retries > 3
74
+ raise e
75
+ end
76
+ end
77
+
78
+ ##
79
+ # returns the number of items in +queue+. If +queue+ is +:all+, a hash of all
80
+ # queue sizes will be returned.
81
+
82
+ def sizeof(queue, statistics = nil)
83
+ statistics ||= stats
84
+
85
+ if queue == :all
86
+ queue_sizes = {}
87
+ available_queues(statistics).each do |queue|
88
+ queue_sizes[queue] = sizeof(queue, statistics)
89
+ end
90
+ return queue_sizes
91
+ end
92
+
93
+ statistics.inject(0) { |m,(k,v)| m + v["queue_#{queue}_items"].to_i }
94
+ end
95
+
96
+ ##
97
+ # returns a list of available (currently allocated) queues.
98
+
99
+ def available_queues(statistics = nil)
100
+ statistics ||= stats
101
+
102
+ res = []
103
+ # Each server returns its own statistics
104
+ statistics.each_pair { |k,v| res = res + v.keys }
105
+ # Here we rely on the name of one of the statistic names to find the queue.
106
+ # The name of the queue is embedded in the statistic name
107
+ res.collect! {|k| /^queue_(.*)_total_items/.match(k)[1] rescue nil }.compact!
108
+ end
109
+
110
+ ##
111
+ # iterator to flush +queue+. Each element will be passed to the provided
112
+ # +block+
113
+
114
+ def flush(queue)
115
+ sizeof(queue).times do
116
+ v = get(queue)
117
+ yield v if block_given?
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def get_server_for_key(key)
124
+ raise ArgumentError, "illegal character in key #{key.inspect}" if key =~ /\s/
125
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
126
+ raise MemCacheError, "No servers available" if servers.empty?
127
+
128
+ # Ignores server weights, oh well
129
+ srvs = servers.dup
130
+ srvs.size.times do |try|
131
+ n = rand(srvs.size)
132
+ server = srvs[n]
133
+ return server if server.alive?
134
+ srvs.delete_at(n)
135
+ end
136
+
137
+ raise MemCacheError, "No servers available (all dead)"
138
+ end
139
+ end
data/starling.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # this file is automatically generated
2
+ Gem::Specification.new do |s|
3
+ s.name = "starling"
4
+ s.version = "1.0.0"
5
+ s.authors = ["Blaine Cook", "Chris Wanstrath", "Britt Selvitelle", "Glenn Rempe", "Abdul-Rahman Advany", "Seth Fitzsimmons", "Haarm Arts", "Chris Gaffney", "Kim Toms"]
6
+ s.email = ["blaine@twitter.com", "chris@ozmm.org", "abdulrahman@advany.com", "starlingmq@groups.google.com", "harmaarts@gmail.com", "gaffneyc@gmail.com", "kim.toms@gmail.com"]
7
+ s.homepage = "http://github.com/starling/starling/"
8
+ s.summary = "Starling is a lightweight, transactional, distributed queue server"
9
+ s.description = s.summary
10
+
11
+ s.files = ["./CHANGELOG", "./LICENSE", "./README.rdoc", "./Rakefile", "./bin/starling", "./bin/starling_top", "./etc/sample-config.yml", "./etc/starling.redhat", "./etc/starling.ubuntu", "./lib/starling.rb", "./lib/starling/handler.rb", "./lib/starling/persistent_queue.rb", "./lib/starling/queue_collection.rb", "./lib/starling/server.rb", "./lib/starling/server_runner.rb", "./starling.gemspec"]
12
+
13
+ s.executables = ["starling", "starling_top"]
14
+ s.require_paths = ["lib"]
15
+
16
+ s.has_rdoc = true
17
+ s.rdoc_options = ["--quiet", "--title", "starling documentation", "--opname", "index.html", "--line-numbers", "--main", "README.rdoc", "--inline-source"]
18
+ s.extra_rdoc_files = ["README.rdoc", "CHANGELOG", "LICENSE"]
19
+
20
+ s.add_dependency 'fiveruns-memcache-client'
21
+ s.add_dependency 'eventmachine', [">= 0.12.0"]
22
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kim-toms-starling
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Blaine Cook
8
+ - Chris Wanstrath
9
+ - Britt Selvitelle
10
+ - Glenn Rempe
11
+ - Abdul-Rahman Advany
12
+ - Seth Fitzsimmons
13
+ - Haarm Arts
14
+ - Chris Gaffney
15
+ - Kim Toms
16
+ autorequire:
17
+ bindir: bin
18
+ cert_chain: []
19
+
20
+ date: 2009-04-08 00:00:00 -07:00
21
+ default_executable:
22
+ dependencies:
23
+ - !ruby/object:Gem::Dependency
24
+ name: fiveruns-memcache-client
25
+ type: :runtime
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: "0"
32
+ version:
33
+ - !ruby/object:Gem::Dependency
34
+ name: eventmachine
35
+ type: :runtime
36
+ version_requirement:
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 0.12.0
42
+ version:
43
+ description: Starling is a lightweight, transactional, distributed queue server
44
+ email:
45
+ - blaine@twitter.com
46
+ - chris@ozmm.org
47
+ - abdulrahman@advany.com
48
+ - starlingmq@groups.google.com
49
+ - harmaarts@gmail.com
50
+ - gaffneyc@gmail.com
51
+ - kim.toms@gmail.com
52
+ executables:
53
+ - starling
54
+ - starling_top
55
+ extensions: []
56
+
57
+ extra_rdoc_files:
58
+ - README.rdoc
59
+ - CHANGELOG
60
+ - LICENSE
61
+ files:
62
+ - ./CHANGELOG
63
+ - ./LICENSE
64
+ - ./README.rdoc
65
+ - ./Rakefile
66
+ - ./bin/starling
67
+ - ./bin/starling_top
68
+ - ./etc/sample-config.yml
69
+ - ./etc/starling.redhat
70
+ - ./etc/starling.ubuntu
71
+ - ./lib/starling.rb
72
+ - ./lib/starling/handler.rb
73
+ - ./lib/starling/persistent_queue.rb
74
+ - ./lib/starling/queue_collection.rb
75
+ - ./lib/starling/server.rb
76
+ - ./lib/starling/server_runner.rb
77
+ - ./starling.gemspec
78
+ - README.rdoc
79
+ - CHANGELOG
80
+ - LICENSE
81
+ has_rdoc: true
82
+ homepage: http://github.com/starling/starling/
83
+ post_install_message:
84
+ rdoc_options:
85
+ - --quiet
86
+ - --title
87
+ - starling documentation
88
+ - --opname
89
+ - index.html
90
+ - --line-numbers
91
+ - --main
92
+ - README.rdoc
93
+ - --inline-source
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: "0"
101
+ version:
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: "0"
107
+ version:
108
+ requirements: []
109
+
110
+ rubyforge_project:
111
+ rubygems_version: 1.2.0
112
+ signing_key:
113
+ specification_version: 2
114
+ summary: Starling is a lightweight, transactional, distributed queue server
115
+ test_files: []
116
+