advany-starling 0.9.7.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,113 @@
1
+ require 'socket'
2
+ require 'logger'
3
+ require 'rubygems'
4
+ require 'eventmachine'
5
+ require 'analyzer_tools/syslog_logger'
6
+
7
+ here = File.dirname(__FILE__)
8
+
9
+ require File.join(here, 'queue_collection')
10
+ require File.join(here, 'handler')
11
+
12
+ module StarlingServer
13
+
14
+ VERSION = "0.9.7.7"
15
+
16
+ class Base
17
+ attr_reader :logger
18
+
19
+ DEFAULT_HOST = '127.0.0.1'
20
+ DEFAULT_PORT = 22122
21
+ DEFAULT_PATH = "/tmp/starling/"
22
+ DEFAULT_TIMEOUT = 60
23
+
24
+ ##
25
+ # Initialize a new Starling server and immediately start processing
26
+ # requests.
27
+ #
28
+ # +opts+ is an optional hash, whose valid options are:
29
+ #
30
+ # [:host] Host on which to listen (default is 127.0.0.1).
31
+ # [:port] Port on which to listen (default is 22122).
32
+ # [:path] Path to Starling queue logs. Default is /tmp/starling/
33
+ # [:timeout] Time in seconds to wait before closing connections.
34
+ # [:logger] A Logger object, an IO handle, or a path to the log.
35
+ # [:loglevel] Logger verbosity. Default is Logger::ERROR.
36
+ #
37
+ # Other options are ignored.
38
+
39
+ def self.start(opts = {})
40
+ server = self.new(opts)
41
+ server.run
42
+ end
43
+
44
+ ##
45
+ # Initialize a new Starling server, but do not accept connections or
46
+ # process requests.
47
+ #
48
+ # +opts+ is as for +start+
49
+
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)
58
+
59
+ @stats = Hash.new(0)
60
+
61
+ FileUtils.mkdir_p(@opts[:path])
62
+
63
+ end
64
+
65
+ ##
66
+ # Start listening and processing requests.
67
+
68
+ def run
69
+ @stats[:start_time] = Time.now
70
+
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
+ @opts[:queue] = QueueCollection.new(@opts[:path])
79
+ @@logger.level = @opts[:log_level] || Logger::ERROR
80
+
81
+ @@logger.info "Starling STARTUP on #{@opts[:host]}:#{@opts[:port]}"
82
+
83
+ EventMachine.epoll
84
+ EventMachine.set_descriptor_table_size(4096)
85
+ EventMachine.run do
86
+ EventMachine.start_server(@opts[:host], @opts[:port], Handler, @opts)
87
+ end
88
+
89
+ # code here will get executed on shutdown:
90
+ @opts[:queue].close
91
+ end
92
+
93
+ def self.logger
94
+ @@logger
95
+ end
96
+
97
+
98
+ ##
99
+ # Stop accepting new connections and shutdown gracefully.
100
+
101
+ def stop
102
+ EventMachine.stop_event_loop
103
+ end
104
+
105
+ def stats(stat = nil) #:nodoc:
106
+ case stat
107
+ when nil; @stats
108
+ when :connections; 1
109
+ else; @stats[stat]
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,292 @@
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(%w( / tmp starling spool )),
55
+ :log_level => Logger::INFO,
56
+ :daemonize => false,
57
+ :timeout => 0,
58
+ :pid_file => File.join(%w( / tmp starling 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] = 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] = pid_file
100
+ end
101
+
102
+ opts.on("-u", "--user USER", Integer, "User to run as") do |user|
103
+ options[:user] = user
104
+ end
105
+
106
+ opts.on("-gGROUP", "--group GROUP", "Group to run as") do |group|
107
+ options[:group] = group
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] = 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
122
+ end
123
+
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
128
+ end
129
+
130
+ opts.separator ""; opts.separator "Miscellaneous:"
131
+
132
+ opts.on_tail("-?", "--help", "Display this usage information.") do
133
+ puts "#{opts}\n"
134
+ exit
135
+ end
136
+
137
+ opts.on_tail("-V", "--version", "Print version number and exit.") do
138
+ puts "Starling #{StarlingServer::VERSION}\n\n"
139
+ exit
140
+ end
141
+ end.parse!
142
+ end
143
+
144
+ def start
145
+ drop_privileges
146
+
147
+ @process.daemonize if options[:daemonize]
148
+
149
+ setup_signal_traps
150
+ @process.write_pid_file
151
+
152
+ STDOUT.puts "Starting at #{options[:host]}:#{options[:port]}."
153
+ @server = StarlingServer::Base.new(options)
154
+ @server.run
155
+
156
+ @process.remove_pid_file
157
+ end
158
+
159
+ def drop_privileges
160
+ Process.euid = options[:user] if options[:user]
161
+ Process.egid = options[:group] if options[:group]
162
+ end
163
+
164
+ def shutdown
165
+ begin
166
+ STDOUT.puts "Shutting down."
167
+ StarlingServer::Base.logger.info "Shutting down."
168
+ @server.stop
169
+ rescue Object => e
170
+ STDERR.puts "There was an error shutting down: #{e}"
171
+ exit(70)
172
+ end
173
+ end
174
+
175
+ def setup_signal_traps
176
+ Signal.trap("INT") { shutdown }
177
+ Signal.trap("TERM") { shutdown }
178
+ end
179
+ end
180
+
181
+ class ProcessHelper
182
+
183
+ def initialize(log_file = nil, pid_file = nil, user = nil, group = nil)
184
+ @log_file = log_file
185
+ @pid_file = pid_file
186
+ @user = user
187
+ @group = group
188
+ end
189
+
190
+ def safefork
191
+ begin
192
+ if pid = fork
193
+ return pid
194
+ end
195
+ rescue Errno::EWOULDBLOCK
196
+ sleep 5
197
+ retry
198
+ end
199
+ end
200
+
201
+ def daemonize
202
+ sess_id = detach_from_terminal
203
+ exit if pid = safefork
204
+
205
+ Dir.chdir("/")
206
+ File.umask 0000
207
+
208
+ close_io_handles
209
+ redirect_io
210
+
211
+ return sess_id
212
+ end
213
+
214
+ def detach_from_terminal
215
+ srand
216
+ safefork and exit
217
+
218
+ unless sess_id = Process.setsid
219
+ raise "Couldn't detache from controlling terminal."
220
+ end
221
+
222
+ trap 'SIGHUP', 'IGNORE'
223
+
224
+ sess_id
225
+ end
226
+
227
+ def close_io_handles
228
+ ObjectSpace.each_object(IO) do |io|
229
+ unless [STDIN, STDOUT, STDERR].include?(io)
230
+ begin
231
+ io.close unless io.closed?
232
+ rescue Exception
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ def redirect_io
239
+ begin; STDIN.reopen('/dev/null'); rescue Exception; end
240
+
241
+ if @log_file
242
+ begin
243
+ STDOUT.reopen(@log_file, "a")
244
+ STDOUT.sync = true
245
+ rescue Exception
246
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
247
+ end
248
+ else
249
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
250
+ end
251
+
252
+ begin; STDERR.reopen(STDOUT); rescue Exception; end
253
+ STDERR.sync = true
254
+ end
255
+
256
+ def rescue_exception
257
+ begin
258
+ yield
259
+ rescue Exception
260
+ end
261
+ end
262
+
263
+ def write_pid_file
264
+ return unless @pid_file
265
+ FileUtils.mkdir_p(File.dirname(@pid_file))
266
+ File.open(@pid_file, "w") { |f| f.write(Process.pid) }
267
+ File.chmod(0644, @pid_file)
268
+ end
269
+
270
+ def remove_pid_file
271
+ return unless @pid_file
272
+ File.unlink(@pid_file) if File.exists?(@pid_file)
273
+ end
274
+
275
+ def running?
276
+ return false unless @pid_file
277
+
278
+ pid = File.read(@pid_file).chomp.to_i rescue nil
279
+ pid = nil if pid == 0
280
+ return false unless pid
281
+
282
+ begin
283
+ Process.kill(0, pid)
284
+ return pid
285
+ rescue Errno::ESRCH
286
+ return nil
287
+ rescue Errno::EPERM
288
+ return pid
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,205 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'rubygems'
4
+ require 'fileutils'
5
+ require 'memcache'
6
+ require 'digest/md5'
7
+
8
+ require 'starling/server'
9
+
10
+ class StarlingServer::PersistentQueue
11
+ remove_const :SOFT_LOG_MAX_SIZE
12
+ SOFT_LOG_MAX_SIZE = 16 * 1024 # 16 KB
13
+ end
14
+
15
+ def safely_fork(&block)
16
+ # anti-race juice:
17
+ blocking = true
18
+ Signal.trap("USR1") { blocking = false }
19
+
20
+ pid = Process.fork(&block)
21
+
22
+ while blocking
23
+ sleep 0.1
24
+ end
25
+
26
+ pid
27
+ end
28
+
29
+ describe "StarlingServer" do
30
+ before do
31
+ @tmp_path = File.join(File.dirname(__FILE__), "tmp")
32
+
33
+ begin
34
+ Dir::mkdir(@tmp_path)
35
+ rescue Errno::EEXIST
36
+ end
37
+
38
+ @server_pid = safely_fork do
39
+ server = StarlingServer::Base.new(:host => '127.0.0.1',
40
+ :port => 22133,
41
+ :path => @tmp_path,
42
+ :logger => Logger.new(STDERR),
43
+ :log_level => Logger::FATAL)
44
+ Signal.trap("INT") { server.stop }
45
+ Process.kill("USR1", Process.ppid)
46
+ server.run
47
+ end
48
+
49
+ @client = MemCache.new('127.0.0.1:22133')
50
+ end
51
+
52
+ it "should test if temp_path exists and is writeable" do
53
+ File.exist?(@tmp_path).should be_true
54
+ File.directory?(@tmp_path).should be_true
55
+ File.writable?(@tmp_path).should be_true
56
+ end
57
+
58
+ it "should set and get" do
59
+ v = rand((2**32)-1)
60
+ @client.get('test_set_and_get_one_entry').should be_nil
61
+ @client.set('test_set_and_get_one_entry', v)
62
+ @client.get('test_set_and_get_one_entry').should eql(v)
63
+ end
64
+
65
+
66
+ it "should expire entries" do
67
+ v = rand((2**32)-1)
68
+ @client.get('test_set_with_expiry').should be_nil
69
+ now = Time.now.to_i
70
+ @client.set('test_set_with_expiry', v + 2, now)
71
+ @client.set('test_set_with_expiry', v)
72
+ sleep(1.0)
73
+ @client.get('test_set_with_expiry').should eql(v)
74
+ end
75
+
76
+ it "should have age stat" do
77
+ now = Time.now.to_i
78
+ @client.set('test_age', 'nibbler')
79
+ sleep(1.0)
80
+ @client.get('test_age').should eql('nibbler')
81
+
82
+ stats = @client.stats['127.0.0.1:22133']
83
+ stats.has_key?('queue_test_age_age').should be_true
84
+ (stats['queue_test_age_age'] >= 1000).should be_true
85
+ end
86
+
87
+ it "should rotate log" do
88
+ log_rotation_path = File.join(@tmp_path, 'test_log_rotation')
89
+
90
+ Dir.glob("#{log_rotation_path}*").each do |file|
91
+ File.unlink(file) rescue nil
92
+ end
93
+ @client.get('test_log_rotation').should be_nil
94
+
95
+ v = 'x' * 8192
96
+
97
+ @client.set('test_log_rotation', v)
98
+ File.size(log_rotation_path).should eql(8207)
99
+ @client.get('test_log_rotation')
100
+
101
+ @client.get('test_log_rotation').should be_nil
102
+
103
+ @client.set('test_log_rotation', v)
104
+ @client.get('test_log_rotation').should eql(v)
105
+
106
+ File.size(log_rotation_path).should eql(1)
107
+ # rotated log should be erased after a successful roll.
108
+ Dir.glob("#{log_rotation_path}*").size.should eql(1)
109
+ end
110
+
111
+ it "should output statistics per server" do
112
+ stats = @client.stats
113
+ assert_kind_of Hash, stats
114
+ assert stats.has_key?('127.0.0.1:22133')
115
+
116
+ server_stats = stats['127.0.0.1:22133']
117
+
118
+ basic_stats = %w( bytes pid time limit_maxbytes cmd_get version
119
+ bytes_written cmd_set get_misses total_connections
120
+ curr_connections curr_items uptime get_hits total_items
121
+ rusage_system rusage_user bytes_read )
122
+
123
+ basic_stats.each do |stat|
124
+ server_stats.has_key?(stat).should be_true
125
+ end
126
+ end
127
+
128
+ it "should return valid response with unkown command" do
129
+ response = @client.add('blah', 1)
130
+ response.should eql("CLIENT_ERROR bad command line format\r\n")
131
+ end
132
+
133
+ it "should disconnect and reconnect again" do
134
+ v = rand(2**32-1)
135
+ @client.set('test_that_disconnecting_and_reconnecting_works', v)
136
+ @client.reset
137
+ @client.get('test_that_disconnecting_and_reconnecting_works').should eql(v)
138
+ end
139
+
140
+ it "should use epoll on linux" do
141
+ # this may take a few seconds.
142
+ # the point is to make sure that we're using epoll on Linux, so we can
143
+ # handle more than 1024 connections.
144
+
145
+ unless IO::popen("uname").read.chomp == "Linux"
146
+ raise "(Skipping epoll test: not on Linux)"
147
+ skip = true
148
+ end
149
+ fd_limit = IO::popen("bash -c 'ulimit -n'").read.chomp.to_i
150
+ unless fd_limit > 1024
151
+ raise "(Skipping epoll test: 'ulimit -n' = #{fd_limit}, need > 1024)"
152
+ skip = true
153
+ end
154
+
155
+ unless skip
156
+ v = rand(2**32 - 1)
157
+ @client.set('test_epoll', v)
158
+
159
+ # we can't open 1024 connections to memcache from within this process,
160
+ # because we will hit ruby's 1024 fd limit ourselves!
161
+ pid1 = safely_fork do
162
+ unused_sockets = []
163
+ 600.times do
164
+ unused_sockets << TCPSocket.new("127.0.0.1", 22133)
165
+ end
166
+ Process.kill("USR1", Process.ppid)
167
+ sleep 90
168
+ end
169
+ pid2 = safely_fork do
170
+ unused_sockets = []
171
+ 600.times do
172
+ unused_sockets << TCPSocket.new("127.0.0.1", 22133)
173
+ end
174
+ Process.kill("USR1", Process.ppid)
175
+ sleep 90
176
+ end
177
+
178
+ begin
179
+ client = MemCache.new('127.0.0.1:22133')
180
+ client.get('test_epoll').should eql(v)
181
+ ensure
182
+ Process.kill("TERM", pid1)
183
+ Process.kill("TERM", pid2)
184
+ end
185
+ end
186
+ end
187
+
188
+ it "should raise error if queue collection is an invalid path" do
189
+ invalid_path = nil
190
+ while invalid_path.nil? || File.exist?(invalid_path)
191
+ invalid_path = File.join('/', Digest::MD5.hexdigest(rand(2**32-1).to_s)[0,8])
192
+ end
193
+
194
+ lambda {
195
+ StarlingServer::QueueCollection.new(invalid_path)
196
+ }.should raise_error(StarlingServer::InaccessibleQueuePath)
197
+ end
198
+
199
+ after do
200
+ Process.kill("INT", @server_pid)
201
+ Process.wait(@server_pid)
202
+ @client.reset
203
+ FileUtils.rm(Dir.glob(File.join(@tmp_path, '*')))
204
+ end
205
+ end