mmmurf-starling 0.9.7.10

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,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.9"
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", "User to run as") do |user|
103
+ options[:user] = user.to_i == 0 ? Etc.getpwnam(user).uid : user.to_i
104
+ end
105
+
106
+ opts.on("-gGROUP", "--group GROUP", "Group to run as") do |group|
107
+ options[:group] = group.to_i == 0 ? Etc.getgrnam(group).gid : group.to_i
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.egid = options[:group] if options[:group]
161
+ Process.euid = options[:user] if options[:user]
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