timshadel-starling 0.9.8.01.20080924

Sign up to get free protection for your applications and to get access to all the features.
@@ -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