starling 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,156 @@
1
+ if RUBY_VERSION >= "1.8.6"
2
+ require 'thread'
3
+ else
4
+ require 'fastthread'
5
+ end
6
+
7
+ module StarlingServer
8
+
9
+ ##
10
+ # PersistentQueue is a subclass of Ruby's thread-safe Queue class. It adds a
11
+ # transactional log to the in-memory Queue, which enables quickly rebuilding
12
+ # the Queue in the event of a sever outage.
13
+
14
+ class PersistentQueue < Queue
15
+
16
+ ##
17
+ # When a log reaches the SOFT_LOG_MAX_SIZE, the Queue will wait until
18
+ # it is empty, and will then rotate the log file.
19
+
20
+ SOFT_LOG_MAX_SIZE = 16 * (1024**2) # 16 MB
21
+
22
+ TRX_CMD_PUSH = "\000".freeze
23
+ TRX_CMD_POP = "\001".freeze
24
+
25
+ TRX_PUSH = "\000%s%s".freeze
26
+ TRX_POP = "\001".freeze
27
+
28
+ attr_reader :initial_bytes
29
+ attr_reader :total_items
30
+ attr_reader :logsize
31
+
32
+ ##
33
+ # Create a new PersistentQueue at +persistence_path+/+queue_name+.
34
+ # If a queue log exists at that path, the Queue will be loaded from
35
+ # disk before being available for use.
36
+
37
+ def initialize(persistence_path, queue_name, debug = false)
38
+ @persistence_path = persistence_path
39
+ @queue_name = queue_name
40
+ @transaction_mutex = Mutex.new
41
+ @total_items = 0
42
+ super()
43
+ @initial_bytes = replay_transaction_log(debug)
44
+ end
45
+
46
+ ##
47
+ # Pushes +value+ to the queue. By default, +push+ will write to the
48
+ # transactional log. Set +log_trx=false+ to override this behaviour.
49
+
50
+ def push(value, log_trx = true)
51
+ if log_trx
52
+ raise NoTransactionLog unless @trx
53
+ size = [value.size].pack("I")
54
+ transaction sprintf(TRX_PUSH, size, value)
55
+ end
56
+
57
+ @total_items += 1
58
+ super(value)
59
+ end
60
+
61
+ ##
62
+ # Retrieves data from the queue.
63
+
64
+ def pop(log_trx = true)
65
+ raise NoTransactionLog if log_trx && !@trx
66
+
67
+ begin
68
+ rv = super(!log_trx)
69
+ rescue ThreadError
70
+ 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."
71
+ rv = ''
72
+ end
73
+ transaction "\001" if log_trx
74
+ rv
75
+ end
76
+
77
+ ##
78
+ # Safely closes the transactional queue.
79
+
80
+ def close
81
+ @transaction_mutex.lock
82
+
83
+ # Ok, yeah, this is lame, and is *technically* a race condition. HOWEVER,
84
+ # the QueueCollection *should* have stopped processing requests, and I don't
85
+ # want to add yet another Mutex around all the push and pop methods. So we
86
+ # do the next simplest thing, and minimize the time we'll stick around before
87
+ # @trx is nil.
88
+ @not_trx = @trx
89
+ @trx = nil
90
+ @not_trx.close
91
+ end
92
+
93
+ private
94
+
95
+ def log_path #:nodoc:
96
+ File.join(@persistence_path, @queue_name)
97
+ end
98
+
99
+ def reopen_log #:nodoc:
100
+ @trx = File.new(log_path, File::CREAT|File::RDWR)
101
+ @logsize = File.size(log_path)
102
+ end
103
+
104
+ def rotate_log #:nodoc:
105
+ @trx.close
106
+ File.rename(log_path, "#{log_path}.#{Time.now.to_i}")
107
+ reopen_log
108
+ end
109
+
110
+ def replay_transaction_log(debug) #:nodoc:
111
+ reopen_log
112
+ bytes_read = 0
113
+
114
+ print "Reading back transaction log for #{@queue_name} " if debug
115
+
116
+ while !@trx.eof?
117
+ cmd = @trx.read(1)
118
+ case cmd
119
+ when TRX_CMD_PUSH
120
+ print ">" if debug
121
+ raw_size = @trx.read(4)
122
+ next unless raw_size
123
+ size = raw_size.unpack("I").first
124
+ data = @trx.read(size)
125
+ next unless data
126
+ push(data, false)
127
+ bytes_read += data.size
128
+ when TRX_CMD_POP
129
+ print "<" if debug
130
+ bytes_read -= pop(false).size
131
+ else
132
+ puts "Error reading transaction log: " +
133
+ "I don't understand '#{cmd}' (skipping)." if debug
134
+ end
135
+ end
136
+
137
+ print " done.\n" if debug
138
+
139
+ return bytes_read
140
+ end
141
+
142
+ def transaction(data) #:nodoc:
143
+ raise "no transaction log handle. that totally sucks." unless @trx
144
+
145
+ begin
146
+ @transaction_mutex.lock
147
+ @trx.write data
148
+ @trx.fsync
149
+ @logsize += data.size
150
+ rotate_log if @logsize > SOFT_LOG_MAX_SIZE && self.length == 0
151
+ ensure
152
+ @transaction_mutex.unlock
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,140 @@
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)
19
+ end
20
+
21
+ @shutdown_mutex = Mutex.new
22
+
23
+ @path = path
24
+
25
+ @queues = {}
26
+ @queue_init_mutexes = {}
27
+
28
+ @stats = Hash.new(0)
29
+ end
30
+
31
+ ##
32
+ # Puts +data+ onto the queue named +key+
33
+
34
+ def put(key, data)
35
+ queue = queues(key)
36
+ return nil unless queue
37
+
38
+ @stats[:current_bytes] += data.size
39
+ @stats[:total_items] += 1
40
+
41
+ queue.push(data)
42
+
43
+ return true
44
+ end
45
+
46
+ ##
47
+ # Retrieves data from the queue named +key+
48
+
49
+ def take(key)
50
+ queue = queues(key)
51
+ if queue.nil? || queue.length == 0
52
+ @stats[:get_misses] += 1
53
+ return nil
54
+ else
55
+ @stats[:get_hits] += 1
56
+ end
57
+ result = queue.pop
58
+ @stats[:current_bytes] -= result.size
59
+ result
60
+ end
61
+
62
+ ##
63
+ # Returns all active queues.
64
+
65
+ def queues(key=nil)
66
+ return nil if @shutdown_mutex.locked?
67
+
68
+ return @queues if key.nil?
69
+
70
+ # First try to return the queue named 'key' if it's available.
71
+ return @queues[key] if @queues[key]
72
+
73
+ # If the queue wasn't available, create or get the mutex that will
74
+ # wrap creation of the Queue.
75
+ @queue_init_mutexes[key] ||= Mutex.new
76
+
77
+ # Otherwise, check to see if another process is already loading
78
+ # the queue named 'key'.
79
+ if @queue_init_mutexes[key].locked?
80
+ # return an empty/false result if we're waiting for the queue
81
+ # to be loaded and we're not the first process to request the queue
82
+ return nil
83
+ else
84
+ begin
85
+ @queue_init_mutexes[key].lock
86
+ # we've locked the mutex, but only go ahead if the queue hasn't
87
+ # been loaded. There's a race condition otherwise, and we could
88
+ # end up loading the queue multiple times.
89
+ if @queues[key].nil?
90
+ @queues[key] = PersistentQueue.new(@path, key)
91
+ @stats[:current_bytes] += @queues[key].initial_bytes
92
+ end
93
+ rescue Object => exc
94
+ puts "ZOMG There was an exception reading back the queue. That totally sucks."
95
+ puts "The exception was: #{exc}. Backtrace: #{exc.backtrace.join("\n")}"
96
+ ensure
97
+ @queue_init_mutexes[key].unlock
98
+ end
99
+ end
100
+
101
+ return @queues[key]
102
+ end
103
+
104
+ ##
105
+ # Returns statistic +stat_name+ for the QueueCollection.
106
+ #
107
+ # Valid statistics are:
108
+ #
109
+ # [:get_misses] Total number of get requests with empty responses
110
+ # [:get_hits] Total number of get requests that returned data
111
+ # [:current_bytes] Current size in bytes of items in the queues
112
+ # [:current_size] Current number of items across all queues
113
+ # [:total_items] Total number of items stored in queues.
114
+
115
+ def stats(stat_name)
116
+ case stat_name
117
+ when nil; @stats
118
+ when :current_size; current_size
119
+ else; @stats[stat_name]
120
+ end
121
+ end
122
+
123
+ ##
124
+ # Safely close all queues.
125
+
126
+ def close
127
+ @shutdown_mutex.lock
128
+ @queues.each_pair do |name,queue|
129
+ queue.close
130
+ @queues.delete(name)
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def current_size #:nodoc:
137
+ @queues.inject(0) { |m, (k,v)| m + v.length }
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,253 @@
1
+ require File.join(File.dirname(__FILE__), 'server')
2
+ require 'optparse'
3
+
4
+ module StarlingServer
5
+ class Runner
6
+
7
+ attr_accessor :options
8
+ private :options, :options=
9
+
10
+ def self.run
11
+ new
12
+ end
13
+
14
+ def initialize
15
+ parse_options
16
+
17
+ @process = ProcessHelper.new(options[:log_file], options[:pid_file], options[:user], options[:group])
18
+
19
+ pid = @process.running?
20
+ if pid
21
+ STDERR.puts "There is already a Starling process running (pid #{pid}), exiting."
22
+ exit(1)
23
+ elsif pid.nil?
24
+ STDERR.puts "Cleaning up stale pidfile at #{options[:pid_file]}."
25
+ end
26
+
27
+ start
28
+ end
29
+
30
+ def parse_options
31
+ self.options = { :host => '127.0.0.1',
32
+ :port => 22122,
33
+ :path => File.join(%w( / var spool starling )),
34
+ :log_level => 0,
35
+ :daemonize => false,
36
+ :pid_file => File.join(%w( / var run starling.pid )) }
37
+
38
+ OptionParser.new do |opts|
39
+ opts.summary_width = 25
40
+
41
+ opts.banner = "Starling (#{StarlingServer::VERSION})\n\n",
42
+ "usage: starling [-v] [-q path] [-h host] [-p port]\n",
43
+ " [-d [-P pidfile]] [-u user] [-g group] [-l log]\n",
44
+ " starling --help\n",
45
+ " starling --version\n"
46
+
47
+ opts.separator ""
48
+ opts.separator "Configuration:"
49
+ opts.on("-q", "--queue_path PATH",
50
+ :REQUIRED,
51
+ "Path to store Starling queue logs", "(default: #{options[:path]})") do |queue_path|
52
+ options[:path] = queue_path
53
+ end
54
+
55
+ opts.separator ""; opts.separator "Network:"
56
+
57
+ opts.on("-hHOST", "--host HOST", "Interface on which to listen (default: #{options[:host]})") do |host|
58
+ options[:host] = host
59
+ end
60
+
61
+ opts.on("-pHOST", "--port PORT", Integer, "TCP port on which to listen (default: #{options[:port]})") do |port|
62
+ options[:port] = port
63
+ end
64
+
65
+ opts.separator ""; opts.separator "Process:"
66
+
67
+ opts.on("-d", "Run as a daemon.") do
68
+ options[:daemonize] = true
69
+ end
70
+
71
+ opts.on("-PFILE", "--pid FILE", "save PID in FILE when using -d option.", "(default: #{options[:pid_file]})") do |pid_file|
72
+ options[:pid_file] = pid_file
73
+ end
74
+
75
+ opts.on("-u", "--user USER", Integer, "User to run as") do |user|
76
+ options[:user] = user
77
+ end
78
+
79
+ opts.on("-gGROUP", "--group GROUP", "Group to run as") do |group|
80
+ options[:group] = group
81
+ end
82
+
83
+ opts.separator ""; opts.separator "Logging:"
84
+
85
+ opts.on("-l", "--log [FILE]", "Path to print debugging information.") do |log_path|
86
+ options[:log] = log_path
87
+ end
88
+
89
+ opts.on("-v", "Increase logging verbosity.") do
90
+ options[:log_level] += 1
91
+ end
92
+
93
+ opts.separator ""; opts.separator "Miscellaneous:"
94
+
95
+ opts.on_tail("-?", "--help", "Display this usage information.") do
96
+ puts "#{opts}\n"
97
+ exit
98
+ end
99
+
100
+ opts.on_tail("-V", "--version", "Print version number and exit.") do
101
+ puts "Starling #{StarlingServer::VERSION}\n\n"
102
+ exit
103
+ end
104
+ end.parse!
105
+ end
106
+
107
+ def start
108
+ drop_privileges
109
+
110
+ @process.daemonize if options[:daemonize]
111
+
112
+ setup_signal_traps
113
+ @process.write_pid_file
114
+
115
+ @server, @thread = StarlingServer::Base.start(options)
116
+ @thread.join
117
+
118
+ @process.remove_pid_file
119
+ end
120
+
121
+ def drop_privileges
122
+ Process.euid = options[:user] if options[:user]
123
+ Process.egid = options[:group] if options[:group]
124
+ end
125
+
126
+ def shutdown
127
+ begin
128
+ STDOUT.puts "Shutting down."
129
+ @server.logger.info "Shutting down."
130
+ @server.stop
131
+ rescue Object => e
132
+ STDERR.puts "There was an error shutting down: #{e}"
133
+ exit(70)
134
+ end
135
+ end
136
+
137
+ def setup_signal_traps
138
+ Signal.trap("INT") { shutdown }
139
+ Signal.trap("TERM") { shutdown }
140
+ end
141
+ end
142
+
143
+ class ProcessHelper
144
+
145
+ def initialize(log_file = nil, pid_file = nil, user = nil, group = nil)
146
+ @log_file = log_file
147
+ @pid_file = pid_file
148
+ @user = user
149
+ @group = group
150
+ end
151
+
152
+ def safefork
153
+ begin
154
+ if pid = fork
155
+ return pid
156
+ end
157
+ rescue Errno::EWOULDBLOCK
158
+ sleep 5
159
+ retry
160
+ end
161
+ end
162
+
163
+ def daemonize
164
+ sess_id = detach_from_terminal
165
+ exit if pid = safefork
166
+
167
+ Dir.chdir("/")
168
+ File.umask 0000
169
+
170
+ close_io_handles
171
+ redirect_io
172
+
173
+ return sess_id
174
+ end
175
+
176
+ def detach_from_terminal
177
+ srand
178
+ safefork and exit
179
+
180
+ unless sess_id = Process.setsid
181
+ raise "Couldn't detache from controlling terminal."
182
+ end
183
+
184
+ trap 'SIGHUP', 'IGNORE'
185
+
186
+ sess_id
187
+ end
188
+
189
+ def close_io_handles
190
+ ObjectSpace.each_object(IO) do |io|
191
+ unless [STDIN, STDOUT, STDERR].include?(io)
192
+ begin
193
+ io.close unless io.closed?
194
+ rescue Exception
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ def redirect_io
201
+ begin; STDIN.reopen('/dev/null'); rescue Exception; end
202
+
203
+ if @log_file
204
+ begin
205
+ STDOUT.reopen(@log_file, "a")
206
+ STDOUT.sync = true
207
+ rescue Exception
208
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
209
+ end
210
+ else
211
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
212
+ end
213
+
214
+ begin; STDERR.reopen(STDOUT); rescue Exception; end
215
+ STDERR.sync = true
216
+ end
217
+
218
+ def rescue_exception
219
+ begin
220
+ yield
221
+ rescue Exception
222
+ end
223
+ end
224
+
225
+ def write_pid_file
226
+ return unless @pid_file
227
+ File.open(@pid_file, "w") { |f| f.write(Process.pid) }
228
+ File.chmod(0644, @pid_file)
229
+ end
230
+
231
+ def remove_pid_file
232
+ return unless @pid_file
233
+ File.unlink(@pid_file) if File.exists?(@pid_file)
234
+ end
235
+
236
+ def running?
237
+ return false unless @pid_file
238
+
239
+ pid = File.read(@pid_file).chomp.to_i rescue nil
240
+ pid = nil if pid == 0
241
+ return false unless pid
242
+
243
+ begin
244
+ Process.kill(0, pid)
245
+ return pid
246
+ rescue Errno::ESRCH
247
+ return nil
248
+ rescue Errno::EPERM
249
+ return pid
250
+ end
251
+ end
252
+ end
253
+ end