kim-toms-starling 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.
@@ -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
+