timshadel-starling 0.9.8.01.20080924

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,151 @@
1
+ module StarlingServer
2
+
3
+ ##
4
+ # PersistentQueue is a subclass of Ruby's thread-safe Queue class. It adds a
5
+ # transactional log to the in-memory Queue, which enables quickly rebuilding
6
+ # the Queue in the event of a sever outage.
7
+
8
+ class PersistentQueue < Queue
9
+
10
+ ##
11
+ # When a log reaches the SOFT_LOG_MAX_SIZE, the Queue will wait until
12
+ # it is empty, and will then rotate the log file.
13
+
14
+ SOFT_LOG_MAX_SIZE = 16 * (1024**2) # 16 MB
15
+
16
+ TRX_CMD_PUSH = "\000".freeze
17
+ TRX_CMD_POP = "\001".freeze
18
+
19
+ TRX_PUSH = "\000%s%s".freeze
20
+ TRX_POP = "\001".freeze
21
+
22
+ attr_reader :initial_bytes
23
+ attr_reader :total_items
24
+ attr_reader :logsize
25
+ attr_reader :current_age
26
+
27
+ ##
28
+ # Create a new PersistentQueue at +persistence_path+/+queue_name+.
29
+ # If a queue log exists at that path, the Queue will be loaded from
30
+ # disk before being available for use.
31
+
32
+ def initialize(persistence_path, queue_name, debug = false)
33
+ @persistence_path = persistence_path
34
+ @queue_name = queue_name
35
+ @total_items = 0
36
+ super()
37
+ @initial_bytes = replay_transaction_log(debug)
38
+ @current_age = 0
39
+ end
40
+
41
+ ##
42
+ # Pushes +value+ to the queue. By default, +push+ will write to the
43
+ # transactional log. Set +log_trx=false+ to override this behaviour.
44
+
45
+ def push(value, log_trx = true)
46
+ if log_trx
47
+ raise NoTransactionLog unless @trx
48
+ size = [value.size].pack("I")
49
+ transaction sprintf(TRX_PUSH, size, value)
50
+ end
51
+
52
+ @total_items += 1
53
+ super([now_usec, value])
54
+ end
55
+
56
+ ##
57
+ # Retrieves data from the queue.
58
+
59
+ def pop(log_trx = true)
60
+ raise NoTransactionLog if log_trx && !@trx
61
+
62
+ begin
63
+ rv = super(!log_trx)
64
+ rescue ThreadError
65
+ puts "WARNING: The queue was empty when trying to pop(). Technically this shouldn't ever happen. Probably a bug in the transactional underpinnings. Or maybe shutdown didn't happen cleanly at some point. Ignoring."
66
+ rv = [now_usec, '']
67
+ end
68
+ transaction "\001" if log_trx
69
+ @current_age = (now_usec - rv[0]) / 1000
70
+ rv[1]
71
+ end
72
+
73
+ ##
74
+ # Safely closes the transactional queue.
75
+
76
+ def close
77
+ # Ok, yeah, this is lame, and is *technically* a race condition. HOWEVER,
78
+ # the QueueCollection *should* have stopped processing requests, and I don't
79
+ # want to add yet another Mutex around all the push and pop methods. So we
80
+ # do the next simplest thing, and minimize the time we'll stick around before
81
+ # @trx is nil.
82
+ @not_trx = @trx
83
+ @trx = nil
84
+ @not_trx.close
85
+ end
86
+
87
+ private
88
+
89
+ def log_path #:nodoc:
90
+ File.join(@persistence_path, @queue_name)
91
+ end
92
+
93
+ def reopen_log #:nodoc:
94
+ @trx = File.new(log_path, File::CREAT|File::RDWR)
95
+ @logsize = File.size(log_path)
96
+ end
97
+
98
+ def rotate_log #:nodoc:
99
+ @trx.close
100
+ backup_logfile = "#{log_path}.#{Time.now.to_i}"
101
+ File.rename(log_path, backup_logfile)
102
+ reopen_log
103
+ File.unlink(backup_logfile)
104
+ end
105
+
106
+ def replay_transaction_log(debug) #:nodoc:
107
+ reopen_log
108
+ bytes_read = 0
109
+
110
+ print "Reading back transaction log for #{@queue_name} " if debug
111
+
112
+ while !@trx.eof?
113
+ cmd = @trx.read(1)
114
+ case cmd
115
+ when TRX_CMD_PUSH
116
+ print ">" if debug
117
+ raw_size = @trx.read(4)
118
+ next unless raw_size
119
+ size = raw_size.unpack("I").first
120
+ data = @trx.read(size)
121
+ next unless data
122
+ push(data, false)
123
+ bytes_read += data.size
124
+ when TRX_CMD_POP
125
+ print "<" if debug
126
+ bytes_read -= pop(false).size
127
+ else
128
+ puts "Error reading transaction log: " +
129
+ "I don't understand '#{cmd}' (skipping)." if debug
130
+ end
131
+ end
132
+
133
+ print " done.\n" if debug
134
+
135
+ return bytes_read
136
+ end
137
+
138
+ def transaction(data) #:nodoc:
139
+ raise "no transaction log handle. that totally sucks." unless @trx
140
+
141
+ @trx.write_nonblock data
142
+ @logsize += data.size
143
+ rotate_log if @logsize > SOFT_LOG_MAX_SIZE && self.length == 0
144
+ end
145
+
146
+ def now_usec
147
+ now = Time.now
148
+ now.to_i * 1000000 + now.usec
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,141 @@
1
+ require 'thread'
2
+ require 'starling/persistent_queue'
3
+
4
+ module StarlingServer
5
+ class InaccessibleQueuePath < Exception #:nodoc:
6
+ end
7
+
8
+ ##
9
+ # QueueCollection is a proxy to a collection of PersistentQueue instances.
10
+
11
+ class QueueCollection
12
+
13
+ ##
14
+ # Create a new QueueCollection at +path+
15
+
16
+ def initialize(path)
17
+ unless File.directory?(path) && File.writable?(path)
18
+ raise InaccessibleQueuePath.new("'#{path}' must exist and be read-writable by #{Etc.getpwuid(Process.uid).name}.")
19
+ end
20
+
21
+ @shutdown_mutex = Mutex.new
22
+
23
+ @path = path
24
+ @logger = StarlingServer::Base.logger
25
+
26
+ @queues = {}
27
+ @queue_init_mutexes = {}
28
+
29
+ @stats = Hash.new(0)
30
+ end
31
+
32
+ ##
33
+ # Puts +data+ onto the queue named +key+
34
+
35
+ def put(key, data)
36
+ queue = queues(key)
37
+ return nil unless queue
38
+
39
+ @stats[:current_bytes] += data.size
40
+ @stats[:total_items] += 1
41
+
42
+ queue.push(data)
43
+
44
+ return true
45
+ end
46
+
47
+ ##
48
+ # Retrieves data from the queue named +key+
49
+
50
+ def take(key)
51
+ queue = queues(key)
52
+ if queue.nil? || queue.length == 0
53
+ @stats[:get_misses] += 1
54
+ return nil
55
+ else
56
+ @stats[:get_hits] += 1
57
+ end
58
+ result = queue.pop
59
+ @stats[:current_bytes] -= result.size
60
+ result
61
+ end
62
+
63
+ ##
64
+ # Returns all active queues.
65
+
66
+ def queues(key=nil)
67
+ return nil if @shutdown_mutex.locked?
68
+
69
+ return @queues if key.nil?
70
+
71
+ # First try to return the queue named 'key' if it's available.
72
+ return @queues[key] if @queues[key]
73
+
74
+ # If the queue wasn't available, create or get the mutex that will
75
+ # wrap creation of the Queue.
76
+ @queue_init_mutexes[key] ||= Mutex.new
77
+
78
+ # Otherwise, check to see if another process is already loading
79
+ # the queue named 'key'.
80
+ if @queue_init_mutexes[key].locked?
81
+ # return an empty/false result if we're waiting for the queue
82
+ # to be loaded and we're not the first process to request the queue
83
+ return nil
84
+ else
85
+ begin
86
+ @queue_init_mutexes[key].lock
87
+ # we've locked the mutex, but only go ahead if the queue hasn't
88
+ # been loaded. There's a race condition otherwise, and we could
89
+ # end up loading the queue multiple times.
90
+ if @queues[key].nil?
91
+ @queues[key] = PersistentQueue.new(@path, key)
92
+ @stats[:current_bytes] += @queues[key].initial_bytes
93
+ end
94
+ rescue Object => exc
95
+ puts "ZOMG There was an exception reading back the queue. That totally sucks."
96
+ puts "The exception was: #{exc}. Backtrace: #{exc.backtrace.join("\n")}"
97
+ ensure
98
+ @queue_init_mutexes[key].unlock
99
+ end
100
+ end
101
+
102
+ return @queues[key]
103
+ end
104
+
105
+ ##
106
+ # Returns statistic +stat_name+ for the QueueCollection.
107
+ #
108
+ # Valid statistics are:
109
+ #
110
+ # [:get_misses] Total number of get requests with empty responses
111
+ # [:get_hits] Total number of get requests that returned data
112
+ # [:current_bytes] Current size in bytes of items in the queues
113
+ # [:current_size] Current number of items across all queues
114
+ # [:total_items] Total number of items stored in queues.
115
+
116
+ def stats(stat_name)
117
+ case stat_name
118
+ when nil; @stats
119
+ when :current_size; current_size
120
+ else; @stats[stat_name]
121
+ end
122
+ end
123
+
124
+ ##
125
+ # Safely close all queues.
126
+
127
+ def close
128
+ @shutdown_mutex.lock
129
+ @queues.each_pair do |name,queue|
130
+ queue.close
131
+ @queues.delete(name)
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def current_size #:nodoc:
138
+ @queues.inject(0) { |m, (k,v)| m + v.length }
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,125 @@
1
+ require 'socket'
2
+ require 'logger'
3
+ require 'rubygems'
4
+ require 'eventmachine'
5
+
6
+ here = File.dirname(__FILE__)
7
+
8
+ require File.join(here, 'queue_collection')
9
+ require File.join(here, 'handler')
10
+
11
+ module StarlingServer
12
+
13
+ VERSION = "0.9.8"
14
+
15
+ class Base
16
+ attr_reader :logger
17
+
18
+ DEFAULT_HOST = '127.0.0.1'
19
+ DEFAULT_PORT = 22122
20
+ DEFAULT_PATH = "/tmp/starling/"
21
+ DEFAULT_TIMEOUT = 60
22
+
23
+ ##
24
+ # Initialize a new Starling server and immediately start processing
25
+ # requests.
26
+ #
27
+ # +opts+ is an optional hash, whose valid options are:
28
+ #
29
+ # [:host] Host on which to listen (default is 127.0.0.1).
30
+ # [:port] Port on which to listen (default is 22122).
31
+ # [:path] Path to Starling queue logs. Default is /tmp/starling/
32
+ # [:timeout] Time in seconds to wait before closing connections.
33
+ # [:logger] A Logger object, an IO handle, or a path to the log.
34
+ # [:loglevel] Logger verbosity. Default is Logger::ERROR.
35
+ #
36
+ # Other options are ignored.
37
+
38
+ def self.start(opts = {})
39
+ server = self.new(opts)
40
+ server.run
41
+ end
42
+
43
+ ##
44
+ # Initialize a new Starling server, but do not accept connections or
45
+ # process requests.
46
+ #
47
+ # +opts+ is as for +start+
48
+
49
+ def initialize(opts = {})
50
+ @opts = {
51
+ :host => DEFAULT_HOST,
52
+ :port => DEFAULT_PORT,
53
+ :path => DEFAULT_PATH,
54
+ :timeout => DEFAULT_TIMEOUT,
55
+ :server => self
56
+ }.merge(opts)
57
+
58
+ @stats = Hash.new(0)
59
+
60
+ FileUtils.mkdir_p(@opts[:path])
61
+
62
+ end
63
+
64
+ ##
65
+ # Start listening and processing requests.
66
+
67
+ def run
68
+ @stats[:start_time] = Time.now
69
+
70
+ if @opts[:syslog_channel]
71
+ begin
72
+ require 'syslog_logger'
73
+ @@logger = SyslogLogger.new(@opts[:syslog_channel])
74
+ rescue LoadError
75
+ # SyslogLogger isn't available, so we're just going to use Logger
76
+ end
77
+ end
78
+
79
+ @@logger ||= case @opts[:logger]
80
+ when IO, String; Logger.new(@opts[:logger])
81
+ when Logger; @opts[:logger]
82
+ else; Logger.new(STDERR)
83
+ end
84
+
85
+ begin
86
+ @opts[:queue] = QueueCollection.new(@opts[:path])
87
+ rescue InaccessibleQueuePath => e
88
+ puts "Error: #{e.message}"
89
+ exit 1
90
+ end
91
+ @@logger.level = @opts[:log_level] || Logger::ERROR
92
+
93
+ @@logger.info "Starling STARTUP on #{@opts[:host]}:#{@opts[:port]}"
94
+
95
+ EventMachine.epoll
96
+ EventMachine.set_descriptor_table_size(4096)
97
+ EventMachine.run do
98
+ EventMachine.start_server(@opts[:host], @opts[:port], Handler, @opts)
99
+ end
100
+
101
+ # code here will get executed on shutdown:
102
+ @opts[:queue].close
103
+ end
104
+
105
+ def self.logger
106
+ @@logger
107
+ end
108
+
109
+
110
+ ##
111
+ # Stop accepting new connections and shutdown gracefully.
112
+
113
+ def stop
114
+ EventMachine.stop_event_loop
115
+ end
116
+
117
+ def stats(stat = nil) #:nodoc:
118
+ case stat
119
+ when nil; @stats
120
+ when :connections; 1
121
+ else; @stats[stat]
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,297 @@
1
+ require File.join(File.dirname(__FILE__), 'server')
2
+ require 'optparse'
3
+ require 'yaml'
4
+
5
+ module StarlingServer
6
+ class Runner
7
+
8
+ attr_accessor :options
9
+ private :options, :options=
10
+
11
+ def self.run
12
+ new
13
+ end
14
+
15
+ def self.shutdown
16
+ @@instance.shutdown
17
+ end
18
+
19
+ def initialize
20
+ @@instance = self
21
+ parse_options
22
+
23
+ @process = ProcessHelper.new(options[:logger], options[:pid_file], options[:user], options[:group])
24
+
25
+ pid = @process.running?
26
+ if pid
27
+ STDERR.puts "There is already a Starling process running (pid #{pid}), exiting."
28
+ exit(1)
29
+ elsif pid.nil?
30
+ STDERR.puts "Cleaning up stale pidfile at #{options[:pid_file]}."
31
+ end
32
+
33
+ start
34
+ end
35
+
36
+ def load_config_file(filename)
37
+ YAML.load(File.open(filename))['starling'].each do |key, value|
38
+ # alias some keys
39
+ case key
40
+ when "queue_path" then key = "path"
41
+ when "log_file" then key = "logger"
42
+ end
43
+ options[key.to_sym] = value
44
+
45
+ if options[:log_level].instance_of?(String)
46
+ options[:log_level] = Logger.const_get(options[:log_level])
47
+ end
48
+ end
49
+ end
50
+
51
+ def parse_options
52
+ self.options = { :host => '127.0.0.1',
53
+ :port => 22122,
54
+ :path => File.join('', 'var', 'spool', 'starling'),
55
+ :log_level => Logger::INFO,
56
+ :daemonize => false,
57
+ :timeout => 0,
58
+ :pid_file => File.join('', 'var', 'run', 'starling.pid') }
59
+
60
+ OptionParser.new do |opts|
61
+ opts.summary_width = 25
62
+
63
+ opts.banner = "Starling (#{StarlingServer::VERSION})\n\n",
64
+ "usage: starling [options...]\n",
65
+ " starling --help\n",
66
+ " starling --version\n"
67
+
68
+ opts.separator ""
69
+ opts.separator "Configuration:"
70
+
71
+ opts.on("-f", "--config FILENAME",
72
+ "Config file (yaml) to load") do |filename|
73
+ load_config_file(filename)
74
+ end
75
+
76
+ opts.on("-q", "--queue_path PATH",
77
+ :REQUIRED,
78
+ "Path to store Starling queue logs", "(default: #{options[:path]})") do |queue_path|
79
+ options[:path] = File.expand_path(queue_path)
80
+ end
81
+
82
+ opts.separator ""; opts.separator "Network:"
83
+
84
+ opts.on("-hHOST", "--host HOST", "Interface on which to listen (default: #{options[:host]})") do |host|
85
+ options[:host] = host
86
+ end
87
+
88
+ opts.on("-pHOST", "--port PORT", Integer, "TCP port on which to listen (default: #{options[:port]})") do |port|
89
+ options[:port] = port
90
+ end
91
+
92
+ opts.separator ""; opts.separator "Process:"
93
+
94
+ opts.on("-d", "Run as a daemon.") do
95
+ options[:daemonize] = true
96
+ end
97
+
98
+ opts.on("-PFILE", "--pid FILENAME", "save PID in FILENAME when using -d option.", "(default: #{options[:pid_file]})") do |pid_file|
99
+ options[:pid_file] = File.expand_path(pid_file)
100
+ end
101
+
102
+ opts.on("-u", "--user USER", "User to run as") do |user|
103
+ options[:user] = user.to_i == 0 ? Etc.getpwnam(user).uid : user.to_i
104
+ end
105
+
106
+ opts.on("-gGROUP", "--group GROUP", "Group to run as") do |group|
107
+ options[:group] = group.to_i == 0 ? Etc.getgrnam(group).gid : group.to_i
108
+ end
109
+
110
+ opts.separator ""; opts.separator "Logging:"
111
+
112
+ opts.on("-L", "--log [FILE]", "Path to print debugging information.") do |log_path|
113
+ options[:logger] = File.expand_path(log_path)
114
+ end
115
+
116
+ begin
117
+ require 'syslog_logger'
118
+
119
+ opts.on("-l", "--syslog CHANNEL", "Write logs to the syslog instead of a log file.") do |channel|
120
+ options[:syslog_channel] = channel
121
+ end
122
+ rescue LoadError
123
+ end
124
+
125
+ opts.on("-v", "Increase logging verbosity (may be used multiple times).") do
126
+ options[:log_level] -= 1
127
+ end
128
+
129
+ opts.on("-t", "--timeout [SECONDS]", Integer,
130
+ "Time in seconds before disconnecting inactive clients (0 to disable).",
131
+ "(default: #{options[:timeout]})") do |timeout|
132
+ options[:timeout] = timeout
133
+ end
134
+
135
+ opts.separator ""; opts.separator "Miscellaneous:"
136
+
137
+ opts.on_tail("-?", "--help", "Display this usage information.") do
138
+ puts "#{opts}\n"
139
+ exit
140
+ end
141
+
142
+ opts.on_tail("-V", "--version", "Print version number and exit.") do
143
+ puts "Starling #{StarlingServer::VERSION}\n\n"
144
+ exit
145
+ end
146
+ end.parse!
147
+ end
148
+
149
+ def start
150
+ drop_privileges
151
+
152
+ @process.daemonize if options[:daemonize]
153
+
154
+ setup_signal_traps
155
+ @process.write_pid_file
156
+
157
+ STDOUT.puts "Starting at #{options[:host]}:#{options[:port]}."
158
+ @server = StarlingServer::Base.new(options)
159
+ @server.run
160
+
161
+ @process.remove_pid_file
162
+ end
163
+
164
+ def drop_privileges
165
+ Process.egid = options[:group] if options[:group]
166
+ Process.euid = options[:user] if options[:user]
167
+ end
168
+
169
+ def shutdown
170
+ begin
171
+ STDOUT.puts "Shutting down."
172
+ StarlingServer::Base.logger.info "Shutting down."
173
+ @server.stop
174
+ rescue Object => e
175
+ STDERR.puts "There was an error shutting down: #{e}"
176
+ exit(70)
177
+ end
178
+ end
179
+
180
+ def setup_signal_traps
181
+ Signal.trap("INT") { shutdown }
182
+ Signal.trap("TERM") { shutdown }
183
+ end
184
+ end
185
+
186
+ class ProcessHelper
187
+
188
+ def initialize(log_file = nil, pid_file = nil, user = nil, group = nil)
189
+ @log_file = log_file
190
+ @pid_file = pid_file
191
+ @user = user
192
+ @group = group
193
+ end
194
+
195
+ def safefork
196
+ begin
197
+ if pid = fork
198
+ return pid
199
+ end
200
+ rescue Errno::EWOULDBLOCK
201
+ sleep 5
202
+ retry
203
+ end
204
+ end
205
+
206
+ def daemonize
207
+ sess_id = detach_from_terminal
208
+ exit if pid = safefork
209
+
210
+ Dir.chdir("/")
211
+ File.umask 0000
212
+
213
+ close_io_handles
214
+ redirect_io
215
+
216
+ return sess_id
217
+ end
218
+
219
+ def detach_from_terminal
220
+ srand
221
+ safefork and exit
222
+
223
+ unless sess_id = Process.setsid
224
+ raise "Couldn't detach from controlling terminal."
225
+ end
226
+
227
+ trap 'SIGHUP', 'IGNORE'
228
+
229
+ sess_id
230
+ end
231
+
232
+ def close_io_handles
233
+ ObjectSpace.each_object(IO) do |io|
234
+ unless [STDIN, STDOUT, STDERR].include?(io)
235
+ begin
236
+ io.close unless io.closed?
237
+ rescue Exception
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ def redirect_io
244
+ begin; STDIN.reopen('/dev/null'); rescue Exception; end
245
+
246
+ if @log_file
247
+ begin
248
+ STDOUT.reopen(@log_file, "a")
249
+ STDOUT.sync = true
250
+ rescue Exception
251
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
252
+ end
253
+ else
254
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
255
+ end
256
+
257
+ begin; STDERR.reopen(STDOUT); rescue Exception; end
258
+ STDERR.sync = true
259
+ end
260
+
261
+ def rescue_exception
262
+ begin
263
+ yield
264
+ rescue Exception
265
+ end
266
+ end
267
+
268
+ def write_pid_file
269
+ return unless @pid_file
270
+ FileUtils.mkdir_p(File.dirname(@pid_file))
271
+ File.open(@pid_file, "w") { |f| f.write(Process.pid) }
272
+ File.chmod(0644, @pid_file)
273
+ end
274
+
275
+ def remove_pid_file
276
+ return unless @pid_file
277
+ File.unlink(@pid_file) if File.exists?(@pid_file)
278
+ end
279
+
280
+ def running?
281
+ return false unless @pid_file
282
+
283
+ pid = File.read(@pid_file).chomp.to_i rescue nil
284
+ pid = nil if pid == 0
285
+ return false unless pid
286
+
287
+ begin
288
+ Process.kill(0, pid)
289
+ return pid
290
+ rescue Errno::ESRCH
291
+ return nil
292
+ rescue Errno::EPERM
293
+ return pid
294
+ end
295
+ end
296
+ end
297
+ end