starling 0.9.3 → 0.9.8

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.
@@ -1,9 +1,3 @@
1
- if RUBY_VERSION >= "1.8.6"
2
- require 'thread'
3
- else
4
- require 'fastthread'
5
- end
6
-
7
1
  module StarlingServer
8
2
 
9
3
  ##
@@ -28,19 +22,20 @@ module StarlingServer
28
22
  attr_reader :initial_bytes
29
23
  attr_reader :total_items
30
24
  attr_reader :logsize
25
+ attr_reader :current_age
31
26
 
32
27
  ##
33
28
  # Create a new PersistentQueue at +persistence_path+/+queue_name+.
34
29
  # If a queue log exists at that path, the Queue will be loaded from
35
30
  # disk before being available for use.
36
-
31
+
37
32
  def initialize(persistence_path, queue_name, debug = false)
38
33
  @persistence_path = persistence_path
39
34
  @queue_name = queue_name
40
- @transaction_mutex = Mutex.new
41
35
  @total_items = 0
42
36
  super()
43
37
  @initial_bytes = replay_transaction_log(debug)
38
+ @current_age = 0
44
39
  end
45
40
 
46
41
  ##
@@ -55,31 +50,30 @@ module StarlingServer
55
50
  end
56
51
 
57
52
  @total_items += 1
58
- super(value)
53
+ super([now_usec, value])
59
54
  end
60
55
 
61
56
  ##
62
57
  # Retrieves data from the queue.
63
-
58
+
64
59
  def pop(log_trx = true)
65
60
  raise NoTransactionLog if log_trx && !@trx
66
-
61
+
67
62
  begin
68
63
  rv = super(!log_trx)
69
64
  rescue ThreadError
70
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."
71
- rv = ''
66
+ rv = [now_usec, '']
72
67
  end
73
68
  transaction "\001" if log_trx
74
- rv
69
+ @current_age = (now_usec - rv[0]) / 1000
70
+ rv[1]
75
71
  end
76
72
 
77
73
  ##
78
74
  # Safely closes the transactional queue.
79
-
80
- def close
81
- @transaction_mutex.lock
82
75
 
76
+ def close
83
77
  # Ok, yeah, this is lame, and is *technically* a race condition. HOWEVER,
84
78
  # the QueueCollection *should* have stopped processing requests, and I don't
85
79
  # want to add yet another Mutex around all the push and pop methods. So we
@@ -103,8 +97,10 @@ module StarlingServer
103
97
 
104
98
  def rotate_log #:nodoc:
105
99
  @trx.close
106
- File.rename(log_path, "#{log_path}.#{Time.now.to_i}")
100
+ backup_logfile = "#{log_path}.#{Time.now.to_i}"
101
+ File.rename(log_path, backup_logfile)
107
102
  reopen_log
103
+ File.unlink(backup_logfile)
108
104
  end
109
105
 
110
106
  def replay_transaction_log(debug) #:nodoc:
@@ -142,15 +138,14 @@ module StarlingServer
142
138
  def transaction(data) #:nodoc:
143
139
  raise "no transaction log handle. that totally sucks." unless @trx
144
140
 
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
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
154
149
  end
155
150
  end
156
151
  end
@@ -7,20 +7,21 @@ module StarlingServer
7
7
 
8
8
  ##
9
9
  # QueueCollection is a proxy to a collection of PersistentQueue instances.
10
-
10
+
11
11
  class QueueCollection
12
12
 
13
13
  ##
14
14
  # Create a new QueueCollection at +path+
15
-
15
+
16
16
  def initialize(path)
17
17
  unless File.directory?(path) && File.writable?(path)
18
- raise InaccessibleQueuePath.new(path)
18
+ raise InaccessibleQueuePath.new("'#{path}' must exist and be read-writable by #{Etc.getpwuid(Process.uid).name}.")
19
19
  end
20
20
 
21
21
  @shutdown_mutex = Mutex.new
22
22
 
23
23
  @path = path
24
+ @logger = StarlingServer::Base.logger
24
25
 
25
26
  @queues = {}
26
27
  @queue_init_mutexes = {}
@@ -39,7 +40,7 @@ module StarlingServer
39
40
  @stats[:total_items] += 1
40
41
 
41
42
  queue.push(data)
42
-
43
+
43
44
  return true
44
45
  end
45
46
 
@@ -60,7 +61,7 @@ module StarlingServer
60
61
  end
61
62
 
62
63
  ##
63
- # Returns all active queues.
64
+ # Returns all active queues.
64
65
 
65
66
  def queues(key=nil)
66
67
  return nil if @shutdown_mutex.locked?
@@ -1,5 +1,8 @@
1
1
  require 'socket'
2
2
  require 'logger'
3
+ require 'rubygems'
4
+ require 'eventmachine'
5
+ require 'analyzer_tools/syslog_logger'
3
6
 
4
7
  here = File.dirname(__FILE__)
5
8
 
@@ -8,13 +11,9 @@ require File.join(here, 'handler')
8
11
 
9
12
  module StarlingServer
10
13
 
11
- VERSION = "0.9.3"
12
-
13
- class StopServer < Exception #:nodoc:
14
- end
15
-
14
+ VERSION = "0.9.8"
15
+
16
16
  class Base
17
-
18
17
  attr_reader :logger
19
18
 
20
19
  DEFAULT_HOST = '127.0.0.1'
@@ -36,11 +35,10 @@ module StarlingServer
36
35
  # [:loglevel] Logger verbosity. Default is Logger::ERROR.
37
36
  #
38
37
  # Other options are ignored.
39
-
38
+
40
39
  def self.start(opts = {})
41
40
  server = self.new(opts)
42
- acceptor = server.run
43
- [server, acceptor]
41
+ server.run
44
42
  end
45
43
 
46
44
  ##
@@ -48,32 +46,20 @@ module StarlingServer
48
46
  # process requests.
49
47
  #
50
48
  # +opts+ is as for +start+
51
-
52
- def initialize(opts = {})
53
- @opts = { :host => DEFAULT_HOST,
54
- :port => DEFAULT_PORT,
55
- :path => DEFAULT_PATH,
56
- :timeout => DEFAULT_TIMEOUT }.merge(opts)
57
-
58
- @logger = case @opts[:logger]
59
- when IO, String; Logger.new(opts[:logger])
60
- when Logger; opts[:logger]
61
- else; Logger.new(STDERR)
62
- end
63
-
64
- @logger.level = @opts[:log_level] || Logger::ERROR
65
49
 
66
- @timeout = @opts[:timeout]
67
-
68
- @socket = TCPServer.new(@opts[:host], @opts[:port])
69
- @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
70
- @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
50
+ def initialize(opts = {})
51
+ @opts = {
52
+ :host => DEFAULT_HOST,
53
+ :port => DEFAULT_PORT,
54
+ :path => DEFAULT_PATH,
55
+ :timeout => DEFAULT_TIMEOUT,
56
+ :server => self
57
+ }.merge(opts)
71
58
 
72
- @queue_collection = QueueCollection.new(@opts[:path])
59
+ @stats = Hash.new(0)
73
60
 
74
- @workers = ThreadGroup.new
61
+ FileUtils.mkdir_p(@opts[:path])
75
62
 
76
- @stats = Hash.new(0)
77
63
  end
78
64
 
79
65
  ##
@@ -82,127 +68,51 @@ module StarlingServer
82
68
  def run
83
69
  @stats[:start_time] = Time.now
84
70
 
85
- BasicSocket.do_not_reverse_lookup = true
86
-
87
- @acceptor = Thread.new do
88
- loop do
89
- begin
90
- accept_connection
91
- rescue StopServer
92
- @socket.close rescue Object
93
- break
94
- rescue Errno::EMFILE
95
- cull_workers("Too many open files or sockets")
96
- sleep @timeout / 100
97
- rescue Errno::ECONNABORTED
98
- begin
99
- client.close
100
- rescue Object => e
101
- logger.warn "Got exception closing socket (client aborted connection) #{e}"
102
- end
103
- rescue Object => e
104
- logger.fatal "Unhandled exception: #{e}. TELL BLAINE HE'S A MORON."
105
- logger.debug e.backtrace.join("\n")
106
- end
107
- end
108
-
109
- graceful_shutdown
71
+ @@logger = case @opts[:logger]
72
+ when IO, String; Logger.new(@opts[:logger])
73
+ when Logger; @opts[:logger]
74
+ else; Logger.new(STDERR)
75
+ end
76
+ @@logger = SyslogLogger.new(@opts[:syslog_channel]) if @opts[:syslog_channel]
77
+
78
+ begin
79
+ @opts[:queue] = QueueCollection.new(@opts[:path])
80
+ rescue InaccessibleQueuePath => e
81
+ puts "Error: #{e.message}"
82
+ exit 1
83
+ end
84
+ @@logger.level = @opts[:log_level] || Logger::ERROR
85
+
86
+ @@logger.info "Starling STARTUP on #{@opts[:host]}:#{@opts[:port]}"
87
+
88
+ EventMachine.epoll
89
+ EventMachine.set_descriptor_table_size(4096)
90
+ EventMachine.run do
91
+ EventMachine.start_server(@opts[:host], @opts[:port], Handler, @opts)
110
92
  end
111
93
 
112
- return @acceptor
94
+ # code here will get executed on shutdown:
95
+ @opts[:queue].close
113
96
  end
114
97
 
98
+ def self.logger
99
+ @@logger
100
+ end
101
+
102
+
115
103
  ##
116
104
  # Stop accepting new connections and shutdown gracefully.
117
105
 
118
106
  def stop
119
- stopper = Thread.new { @acceptor.raise(StopServer.new) }
120
- stopper.priority = Thread.current.priority + 10
107
+ EventMachine.stop_event_loop
121
108
  end
122
109
 
123
110
  def stats(stat = nil) #:nodoc:
124
111
  case stat
125
112
  when nil; @stats
126
- when :connections; @workers.list.length
113
+ when :connections; 1
127
114
  else; @stats[stat]
128
115
  end
129
116
  end
130
-
131
- private
132
-
133
- def accept_connection #:nodoc:
134
- client = @socket.accept
135
-
136
- @stats[:total_connections] += 1
137
-
138
- thread = Thread.new(client) { |c| spawn_handler(c) }
139
- thread[:last_activity] = Time.now
140
- @workers.add(thread)
141
- end
142
-
143
- def spawn_handler(client) #:nodoc:
144
- begin
145
- queue_handler = Handler.new(client, self, @queue_collection)
146
- queue_handler.run
147
- rescue EOFError, Errno::EBADF, Errno::ECONNRESET, Errno::EINVAL, Errno::EPIPE
148
- begin
149
- client.close
150
- rescue Object => e
151
- logger.warn "Got exception while closing socket after connection error: #{e}"
152
- logger.debug e.backtrace.join("\n")
153
- end
154
- rescue TimeoutError => reason
155
- begin
156
- logger.info "Shutdown due to timeout: #{reason.message}"
157
- client.close
158
- rescue Object => e
159
- logger.warn "Got exception while closing socket after timeout: #{e}"
160
- logger.debug e.backtrace.join("\n")
161
- end
162
- rescue Errno::EMFILE
163
- cull_workers('Too many open files or sockets')
164
- rescue Object => e
165
- logger.error("Unknown error: #{e}")
166
- logger.debug(e.backtrace.join("\n"))
167
- ensure
168
- begin
169
- client.close
170
- rescue Object => e
171
- logger.info "Got exception while closing socket: #{e}"
172
- logger.debug e.backtrace.join("\n")
173
- end
174
- end
175
- end
176
-
177
- def cull_workers(reason='unknown') #:nodoc:
178
- if @workers.list.length > 0
179
- logger.info "#{Time.now}: Reaping #{@workers.list.length} threads because of #{reason}"
180
- error_msg = "Starling timed out this thread: #{reason}"
181
- mark = Time.now
182
- @workers.list.each do |w|
183
- w[:last_activity] = Time.now if not w[:last_activity]
184
-
185
- if mark - w[:last_activity] > @timeout
186
- logger.warn "Thread #{w.inspect} has been idle for #{mark - w[:last_activity]}s, killing."
187
- w.raise(TimeoutError.new(error_msg))
188
- end
189
- end
190
- end
191
-
192
- return @workers.list.length
193
- end
194
-
195
- def graceful_shutdown #:nodoc:
196
- @workers.list.each do |w|
197
- w[:shutdown] = true
198
- end
199
-
200
- @queue_collection.close
201
-
202
- while cull_workers("shutdown") > 0
203
- logger.info "Waiting for #{@workers.list.length} requests to finish, could take #{@timeout} seconds."
204
- sleep 0.5
205
- end
206
- end
207
117
  end
208
118
  end
@@ -1,5 +1,6 @@
1
1
  require File.join(File.dirname(__FILE__), 'server')
2
2
  require 'optparse'
3
+ require 'yaml'
3
4
 
4
5
  module StarlingServer
5
6
  class Runner
@@ -10,11 +11,16 @@ module StarlingServer
10
11
  def self.run
11
12
  new
12
13
  end
13
-
14
+
15
+ def self.shutdown
16
+ @@instance.shutdown
17
+ end
18
+
14
19
  def initialize
20
+ @@instance = self
15
21
  parse_options
16
22
 
17
- @process = ProcessHelper.new(options[:log_file], options[:pid_file], options[:user], options[:group])
23
+ @process = ProcessHelper.new(options[:logger], options[:pid_file], options[:user], options[:group])
18
24
 
19
25
  pid = @process.running?
20
26
  if pid
@@ -27,29 +33,50 @@ module StarlingServer
27
33
  start
28
34
  end
29
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
+
30
51
  def parse_options
31
52
  self.options = { :host => '127.0.0.1',
32
53
  :port => 22122,
33
- :path => File.join(%w( / var spool starling )),
34
- :log_level => 0,
54
+ :path => File.join('', 'var', 'spool', 'starling'),
55
+ :log_level => Logger::INFO,
35
56
  :daemonize => false,
36
- :pid_file => File.join(%w( / var run starling.pid )) }
57
+ :timeout => 0,
58
+ :pid_file => File.join('', 'var', 'run', 'starling.pid') }
37
59
 
38
60
  OptionParser.new do |opts|
39
61
  opts.summary_width = 25
40
62
 
41
63
  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",
64
+ "usage: starling [options...]\n",
44
65
  " starling --help\n",
45
66
  " starling --version\n"
46
67
 
47
68
  opts.separator ""
48
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
+
49
76
  opts.on("-q", "--queue_path PATH",
50
77
  :REQUIRED,
51
78
  "Path to store Starling queue logs", "(default: #{options[:path]})") do |queue_path|
52
- options[:path] = queue_path
79
+ options[:path] = File.expand_path(queue_path)
53
80
  end
54
81
 
55
82
  opts.separator ""; opts.separator "Network:"
@@ -68,26 +95,36 @@ module StarlingServer
68
95
  options[:daemonize] = true
69
96
  end
70
97
 
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
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)
73
100
  end
74
101
 
75
- opts.on("-u", "--user USER", Integer, "User to run as") do |user|
76
- options[:user] = user
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
77
104
  end
78
105
 
79
106
  opts.on("-gGROUP", "--group GROUP", "Group to run as") do |group|
80
- options[:group] = group
107
+ options[:group] = group.to_i == 0 ? Etc.getgrnam(group).gid : group.to_i
81
108
  end
82
109
 
83
110
  opts.separator ""; opts.separator "Logging:"
84
111
 
85
- opts.on("-l", "--log [FILE]", "Path to print debugging information.") do |log_path|
86
- options[:log] = log_path
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
+ opts.on("-l", "--syslog CHANNEL", "Write logs to the syslog instead of a log file.") do |channel|
117
+ options[:syslog_channel] = channel
118
+ end
119
+
120
+ opts.on("-v", "Increase logging verbosity (may be used multiple times).") do
121
+ options[:log_level] -= 1
87
122
  end
88
123
 
89
- opts.on("-v", "Increase logging verbosity.") do
90
- options[:log_level] += 1
124
+ opts.on("-t", "--timeout [SECONDS]", Integer,
125
+ "Time in seconds before disconnecting inactive clients (0 to disable).",
126
+ "(default: #{options[:timeout]})") do |timeout|
127
+ options[:timeout] = timeout
91
128
  end
92
129
 
93
130
  opts.separator ""; opts.separator "Miscellaneous:"
@@ -112,21 +149,22 @@ module StarlingServer
112
149
  setup_signal_traps
113
150
  @process.write_pid_file
114
151
 
115
- @server, @thread = StarlingServer::Base.start(options)
116
- @thread.join
152
+ STDOUT.puts "Starting at #{options[:host]}:#{options[:port]}."
153
+ @server = StarlingServer::Base.new(options)
154
+ @server.run
117
155
 
118
156
  @process.remove_pid_file
119
157
  end
120
158
 
121
159
  def drop_privileges
122
- Process.euid = options[:user] if options[:user]
123
160
  Process.egid = options[:group] if options[:group]
161
+ Process.euid = options[:user] if options[:user]
124
162
  end
125
163
 
126
164
  def shutdown
127
165
  begin
128
166
  STDOUT.puts "Shutting down."
129
- @server.logger.info "Shutting down."
167
+ StarlingServer::Base.logger.info "Shutting down."
130
168
  @server.stop
131
169
  rescue Object => e
132
170
  STDERR.puts "There was an error shutting down: #{e}"
@@ -178,7 +216,7 @@ module StarlingServer
178
216
  safefork and exit
179
217
 
180
218
  unless sess_id = Process.setsid
181
- raise "Couldn't detache from controlling terminal."
219
+ raise "Couldn't detach from controlling terminal."
182
220
  end
183
221
 
184
222
  trap 'SIGHUP', 'IGNORE'
@@ -196,7 +234,7 @@ module StarlingServer
196
234
  end
197
235
  end
198
236
  end
199
-
237
+
200
238
  def redirect_io
201
239
  begin; STDIN.reopen('/dev/null'); rescue Exception; end
202
240
 
@@ -205,7 +243,7 @@ module StarlingServer
205
243
  STDOUT.reopen(@log_file, "a")
206
244
  STDOUT.sync = true
207
245
  rescue Exception
208
- begin; STDOUT.reopen('/dev/null'); rescue Exception; end
246
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
209
247
  end
210
248
  else
211
249
  begin; STDOUT.reopen('/dev/null'); rescue Exception; end
@@ -224,6 +262,7 @@ module StarlingServer
224
262
 
225
263
  def write_pid_file
226
264
  return unless @pid_file
265
+ FileUtils.mkdir_p(File.dirname(@pid_file))
227
266
  File.open(@pid_file, "w") { |f| f.write(Process.pid) }
228
267
  File.chmod(0644, @pid_file)
229
268
  end