starling 0.9.3

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,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