starling 0.9.3 → 0.9.8

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