fiveruns-starling 0.9.7.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,151 @@
1
+ require 'rubygems'
2
+ require 'starling'
3
+ require 'starling/worker'
4
+ require 'logger'
5
+ require 'eventmachine'
6
+ require 'analyzer_tools/syslog_logger'
7
+
8
+ module StarlingClient
9
+
10
+ VERSION = "0.9.7.5"
11
+
12
+ class Base
13
+ attr_reader :logger
14
+
15
+ DEFAULT_HOST = "localhost"
16
+ DEFAULT_PORT = "22122"
17
+ DEFAULT_TEMPLATES_PATH = File.join("tmp", "starling", "templates")
18
+ DEFAULT_WORKERS_PATH = File.join("tmp", "starling", "workers")
19
+ DEFAULT_TIMEOUT = 60
20
+
21
+ ##
22
+ # Initialize a new Starling client and immediately start processing
23
+ # requests.
24
+ #
25
+ # +opts+ is an optional hash, whose valid options are:
26
+ #
27
+ # [:host] Host on which to listen (default is 127.0.0.1).
28
+ # [:port] Port on which to listen (default is 22122).
29
+ # [:path] Path to Starling queue logs. Default is /tmp/starling/
30
+ # [:timeout] Time in seconds to wait before closing connections.
31
+ # [:logger] A Logger object, an IO handle, or a path to the log.
32
+ # [:loglevel] Logger verbosity. Default is Logger::ERROR.
33
+ #
34
+ # Other options are ignored.
35
+
36
+ def self.start(opts = {})
37
+ server = self.new(opts)
38
+ server.run
39
+ end
40
+
41
+ ##
42
+ # Initialize a new Starling client, but do not start with working
43
+ #
44
+ # +opts+ is as for +start+
45
+
46
+ def initialize(opts = {})
47
+ @opts = {
48
+ :host => DEFAULT_HOST,
49
+ :port => DEFAULT_PORT,
50
+ :templates_path => DEFAULT_TEMPLATES_PATH,
51
+ :workers_path => DEFAULT_WORKERS_PATH,
52
+ :timeout => DEFAULT_TIMEOUT,
53
+ :continues_processing => true
54
+ }.merge(opts)
55
+
56
+ @stats = Hash.new(0)
57
+
58
+ FileUtils.mkdir_p(@opts[:templates_path])
59
+ FileUtils.mkdir_p(@opts[:workers_path])
60
+
61
+ @client = Starling.new("#{@opts[:host]}:#{@opts[:port]}", :multithread => true)
62
+ end
63
+
64
+ ##
65
+ # Start listening and processing requests.
66
+
67
+ def run
68
+ @stats[:start_time] = Time.now
69
+
70
+ @@logger = case @opts[:logger]
71
+ when IO, String; Logger.new(@opts[:logger])
72
+ when Logger; @opts[:logger]
73
+ else; Logger.new(STDERR)
74
+ end
75
+ @@logger = SyslogLogger.new(@opts[:syslog_channel]) if @opts[:syslog_channel]
76
+
77
+ @@logger.level = @opts[:log_level] || Logger::ERROR
78
+
79
+ @@logger.info "Starling Client STARTUP"
80
+
81
+ load_templates
82
+
83
+ @pids = []
84
+
85
+ load_workers.each do |worker|
86
+ @pids << fork {
87
+ worker = StarlingWorker.const_get(worker).new(:host => @opts[:host],
88
+ :port => @opts[:port]).run
89
+ @@logger.info "Starling Client STARTUP"
90
+ }
91
+ end
92
+
93
+ if load_workers.length == 0
94
+ @@logger.error "no workers found"
95
+ @shutdown = true
96
+ end
97
+
98
+ loop {
99
+ sleep 60
100
+ } if @opts[:continues_processing]
101
+ end
102
+
103
+ def stop
104
+ @pids.each do |pid|
105
+ Process.kill(0, pid)
106
+ end
107
+
108
+ exit(0)
109
+ end
110
+
111
+ def load_templates
112
+ templates = []
113
+ Dir.glob("#{@opts[:templates_path]}/*.rb").each do |file|
114
+ unless [".", ".."].include?(file)
115
+ load(file)
116
+ templates << File.basename(file, ".rb").split('_').map{|w| w.capitalize}.join
117
+ end
118
+ end
119
+
120
+ return templates
121
+ end
122
+
123
+ def load_workers
124
+ workers = []
125
+ Dir.glob("#{@opts[:workers_path]}/*.rb").each do |file|
126
+ unless [".", ".."].include?(file)
127
+ load(file)
128
+ workers << File.basename(file, ".rb").split('_').map{|w| w.capitalize}.join
129
+ end
130
+ end
131
+
132
+ return workers
133
+ end
134
+
135
+ def starling
136
+ return @client
137
+ end
138
+
139
+ def self.logger
140
+ @@logger
141
+ end
142
+
143
+ def stats(stat = nil) #:nodoc:
144
+ case stat
145
+ when nil; @stats
146
+ when :connections; 1
147
+ else; @stats[stat]
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,272 @@
1
+ require File.join(File.dirname(__FILE__), 'client')
2
+ require 'optparse'
3
+ require 'yaml'
4
+
5
+ module StarlingClient
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 Client 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 = { :path => File.join(%w( / tmp workers )),
53
+ :log_level => Logger::INFO,
54
+ :daemonize => false,
55
+ :timeout => 0,
56
+ :pid_file => File.join(%w( / tmp starling starling_client.pid )) }
57
+
58
+ OptionParser.new do |opts|
59
+ opts.summary_width = 25
60
+
61
+ opts.banner = "Starling (#{StarlingClient::VERSION})\n\n",
62
+ "usage: starling_client [options...]\n",
63
+ " starling_client --help\n",
64
+ " starling_client --version\n"
65
+
66
+ opts.separator ""
67
+ opts.separator "Configuration:"
68
+
69
+ opts.on("-f", "--config FILENAME",
70
+ "Config file (yaml) to load") do |filename|
71
+ load_config_file(filename)
72
+ end
73
+
74
+ opts.on("-w", "--workers_path PATH",
75
+ :REQUIRED,
76
+ "Path to workers") do |queue_path|
77
+ options[:workers_path] = queue_path
78
+ end
79
+
80
+ opts.on("-t", "--templates_path PATH",
81
+ :REQUIRED,
82
+ "Path to templates") do |queue_path|
83
+ options[:templates_path] = queue_path
84
+ end
85
+
86
+ opts.separator ""; opts.separator "Process:"
87
+
88
+ opts.on("-d", "Run as a daemon.") do
89
+ options[:daemonize] = true
90
+ end
91
+
92
+ opts.on("-PFILE", "--pid FILENAME", "save PID in FILENAME when using -d option.", "(default: #{options[:pid_file]})") do |pid_file|
93
+ options[:pid_file] = pid_file
94
+ end
95
+
96
+ opts.separator ""; opts.separator "Logging:"
97
+
98
+ opts.on("-L", "--log [FILE]", "Path to print debugging information.") do |log_path|
99
+ options[:logger] = log_path
100
+ end
101
+
102
+ opts.on("-l", "--syslog CHANNEL", "Write logs to the syslog instead of a log file.") do |channel|
103
+ options[:syslog_channel] = channel
104
+ end
105
+
106
+ opts.on("-v", "Increase logging verbosity (may be used multiple times).") do
107
+ options[:log_level] -= 1
108
+ end
109
+
110
+ opts.separator ""; opts.separator "Miscellaneous:"
111
+
112
+ opts.on_tail("-?", "--help", "Display this usage information.") do
113
+ puts "#{opts}\n"
114
+ exit
115
+ end
116
+
117
+ opts.on_tail("-V", "--version", "Print version number and exit.") do
118
+ puts "Starling Client #{StarlingClient::VERSION}\n\n"
119
+ exit
120
+ end
121
+ end.parse!
122
+ end
123
+
124
+ def start
125
+ drop_privileges
126
+
127
+ @process.daemonize if options[:daemonize]
128
+
129
+ setup_signal_traps
130
+ @process.write_pid_file
131
+
132
+ STDOUT.puts "Starting."
133
+ @client = StarlingClient::Base.new(options)
134
+ @client.run
135
+
136
+ @process.remove_pid_file
137
+ end
138
+
139
+ def drop_privileges
140
+ Process.euid = options[:user] if options[:user]
141
+ Process.egid = options[:group] if options[:group]
142
+ end
143
+
144
+ def shutdown
145
+ begin
146
+ STDOUT.puts "Shutting down."
147
+ StarlingClient::Base.logger.info "Shutting down."
148
+ @client.stop
149
+ rescue Object => e
150
+ STDERR.puts "There was an error shutting down: #{e}"
151
+ exit(70)
152
+ end
153
+ end
154
+
155
+ def setup_signal_traps
156
+ Signal.trap("INT") { shutdown }
157
+ Signal.trap("TERM") { shutdown }
158
+ end
159
+ end
160
+
161
+ class ProcessHelper
162
+
163
+ def initialize(log_file = nil, pid_file = nil, user = nil, group = nil)
164
+ @log_file = log_file
165
+ @pid_file = pid_file
166
+ @user = user
167
+ @group = group
168
+ end
169
+
170
+ def safefork
171
+ begin
172
+ if pid = fork
173
+ return pid
174
+ end
175
+ rescue Errno::EWOULDBLOCK
176
+ sleep 5
177
+ retry
178
+ end
179
+ end
180
+
181
+ def daemonize
182
+ sess_id = detach_from_terminal
183
+ exit if pid = safefork
184
+
185
+ Dir.chdir("/")
186
+ File.umask 0000
187
+
188
+ close_io_handles
189
+ redirect_io
190
+
191
+ return sess_id
192
+ end
193
+
194
+ def detach_from_terminal
195
+ srand
196
+ safefork and exit
197
+
198
+ unless sess_id = Process.setsid
199
+ raise "Couldn't detache from controlling terminal."
200
+ end
201
+
202
+ trap 'SIGHUP', 'IGNORE'
203
+
204
+ sess_id
205
+ end
206
+
207
+ def close_io_handles
208
+ ObjectSpace.each_object(IO) do |io|
209
+ unless [STDIN, STDOUT, STDERR].include?(io)
210
+ begin
211
+ io.close unless io.closed?
212
+ rescue Exception
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ def redirect_io
219
+ begin; STDIN.reopen('/dev/null'); rescue Exception; end
220
+
221
+ if @log_file
222
+ begin
223
+ STDOUT.reopen(@log_file, "a")
224
+ STDOUT.sync = true
225
+ rescue Exception
226
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
227
+ end
228
+ else
229
+ begin; STDOUT.reopen('/dev/null'); rescue Exception; end
230
+ end
231
+
232
+ begin; STDERR.reopen(STDOUT); rescue Exception; end
233
+ STDERR.sync = true
234
+ end
235
+
236
+ def rescue_exception
237
+ begin
238
+ yield
239
+ rescue Exception
240
+ end
241
+ end
242
+
243
+ def write_pid_file
244
+ return unless @pid_file
245
+ FileUtils.mkdir_p(File.dirname(@pid_file))
246
+ File.open(@pid_file, "w") { |f| f.write(Process.pid) }
247
+ File.chmod(0644, @pid_file)
248
+ end
249
+
250
+ def remove_pid_file
251
+ return unless @pid_file
252
+ File.unlink(@pid_file) if File.exists?(@pid_file)
253
+ end
254
+
255
+ def running?
256
+ return false unless @pid_file
257
+
258
+ pid = File.read(@pid_file).chomp.to_i rescue nil
259
+ pid = nil if pid == 0
260
+ return false unless pid
261
+
262
+ begin
263
+ Process.kill(0, pid)
264
+ return pid
265
+ rescue Errno::ESRCH
266
+ return nil
267
+ rescue Errno::EPERM
268
+ return pid
269
+ end
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,222 @@
1
+ module StarlingServer
2
+
3
+ ##
4
+ # This is an internal class that's used by Starling::Server to handle the
5
+ # MemCache protocol and act as an interface between the Server and the
6
+ # QueueCollection.
7
+
8
+ class Handler < EventMachine::Connection
9
+
10
+ DATA_PACK_FMT = "Ia*".freeze
11
+
12
+ # ERROR responses
13
+ ERR_UNKNOWN_COMMAND = "CLIENT_ERROR bad command line format\r\n".freeze
14
+
15
+ # GET Responses
16
+ GET_COMMAND = /\Aget (.{1,250})\s*\r\n/m
17
+ GET_RESPONSE = "VALUE %s %s %s\r\n%s\r\nEND\r\n".freeze
18
+ GET_RESPONSE_EMPTY = "END\r\n".freeze
19
+
20
+ # SET Responses
21
+ SET_COMMAND = /\Aset (.{1,250}) ([0-9]+) ([0-9]+) ([0-9]+)\r\n/m
22
+ SET_RESPONSE_SUCCESS = "STORED\r\n".freeze
23
+ SET_RESPONSE_FAILURE = "NOT STORED\r\n".freeze
24
+ SET_CLIENT_DATA_ERROR = "CLIENT_ERROR bad data chunk\r\nERROR\r\n".freeze
25
+
26
+ # STAT Response
27
+ STATS_COMMAND = /\Astats\r\n/m
28
+ STATS_RESPONSE = "STAT pid %d
29
+ STAT uptime %d
30
+ STAT time %d
31
+ STAT version %s
32
+ STAT rusage_user %0.6f
33
+ STAT rusage_system %0.6f
34
+ STAT curr_items %d
35
+ STAT total_items %d
36
+ STAT bytes %d
37
+ STAT curr_connections %d
38
+ STAT total_connections %d
39
+ STAT cmd_get %d
40
+ STAT cmd_set %d
41
+ STAT get_hits %d
42
+ STAT get_misses %d
43
+ STAT bytes_read %d
44
+ STAT bytes_written %d
45
+ STAT limit_maxbytes %d
46
+ %sEND\r\n".freeze
47
+ QUEUE_STATS_RESPONSE = "STAT queue_%s_items %d
48
+ STAT queue_%s_total_items %d
49
+ STAT queue_%s_logsize %d
50
+ STAT queue_%s_expired_items %d
51
+ STAT queue_%s_age %d\n".freeze
52
+
53
+ SHUTDOWN_COMMAND = /\Ashutdown\r\n/m
54
+
55
+
56
+ @@next_session_id = 1
57
+
58
+ ##
59
+ # Creates a new handler for the MemCache protocol that communicates with a
60
+ # given client.
61
+
62
+ def initialize(options = {})
63
+ @opts = options
64
+ end
65
+
66
+ ##
67
+ # Process incoming commands from the attached client.
68
+
69
+ def post_init
70
+ @stash = []
71
+ @data = ""
72
+ @data_buf = ""
73
+ @server = @opts[:server]
74
+ @logger = StarlingServer::Base.logger
75
+ @expiry_stats = Hash.new(0)
76
+ @expected_length = nil
77
+ @server.stats[:total_connections] += 1
78
+ set_comm_inactivity_timeout @opts[:timeout]
79
+ @queue_collection = @opts[:queue]
80
+
81
+ @session_id = @@next_session_id
82
+ @@next_session_id += 1
83
+
84
+ peer = Socket.unpack_sockaddr_in(get_peername)
85
+ #@logger.debug "(#{@session_id}) New session from #{peer[1]}:#{peer[0]}"
86
+ end
87
+
88
+ def receive_data(incoming)
89
+ @server.stats[:bytes_read] += incoming.size
90
+ @data << incoming
91
+
92
+ while data = @data.slice!(/.*?\r\n/m)
93
+ response = process(data)
94
+ end
95
+
96
+ send_data response if response
97
+ end
98
+
99
+ def process(data)
100
+ data = @data_buf + data if @data_buf.size > 0
101
+ # our only non-normal state is consuming an object's data
102
+ # when @expected_length is present
103
+ if @expected_length && data.size == @expected_length
104
+ response = set_data(data)
105
+ @data_buf = ""
106
+ return response
107
+ elsif @expected_length
108
+ @data_buf = data
109
+ return
110
+ end
111
+
112
+ case data
113
+ when SET_COMMAND
114
+ @server.stats[:set_requests] += 1
115
+ set($1, $2, $3, $4.to_i)
116
+ when GET_COMMAND
117
+ @server.stats[:get_requests] += 1
118
+ get($1)
119
+ when STATS_COMMAND
120
+ stats
121
+ when SHUTDOWN_COMMAND
122
+ # no point in responding, they'll never get it.
123
+ Runner::shutdown
124
+ else
125
+ logger.warn "Unknown command: #{data}."
126
+ respond ERR_UNKNOWN_COMMAND
127
+ end
128
+ rescue => e
129
+ logger.error "Error handling request: #{e}."
130
+ logger.debug e.backtrace.join("\n")
131
+ respond GET_RESPONSE_EMPTY
132
+ end
133
+
134
+ def unbind
135
+ #@logger.debug "(#{@session_id}) connection ends"
136
+ end
137
+
138
+ private
139
+ def respond(str, *args)
140
+ response = sprintf(str, *args)
141
+ @server.stats[:bytes_written] += response.length
142
+ response
143
+ end
144
+
145
+ def set(key, flags, expiry, len)
146
+ @expected_length = len + 2
147
+ @stash = [ key, flags, expiry ]
148
+ nil
149
+ end
150
+
151
+ def set_data(incoming)
152
+ key, flags, expiry = @stash
153
+ data = incoming.slice(0...@expected_length-2)
154
+ @stash = []
155
+ @expected_length = nil
156
+
157
+ internal_data = [expiry.to_i, data].pack(DATA_PACK_FMT)
158
+ if @queue_collection.put(key, internal_data)
159
+ respond SET_RESPONSE_SUCCESS
160
+ else
161
+ respond SET_RESPONSE_FAILURE
162
+ end
163
+ end
164
+
165
+ def get(key)
166
+ now = Time.now.to_i
167
+
168
+ while response = @queue_collection.take(key)
169
+ expiry, data = response.unpack(DATA_PACK_FMT)
170
+
171
+ break if expiry == 0 || expiry >= now
172
+
173
+ @expiry_stats[key] += 1
174
+ expiry, data = nil
175
+ end
176
+
177
+ if data
178
+ respond GET_RESPONSE, key, 0, data.size, data
179
+ else
180
+ respond GET_RESPONSE_EMPTY
181
+ end
182
+ end
183
+
184
+ def stats
185
+ respond STATS_RESPONSE,
186
+ Process.pid, # pid
187
+ Time.now - @server.stats(:start_time), # uptime
188
+ Time.now.to_i, # time
189
+ StarlingServer::VERSION, # version
190
+ Process.times.utime, # rusage_user
191
+ Process.times.stime, # rusage_system
192
+ @queue_collection.stats(:current_size), # curr_items
193
+ @queue_collection.stats(:total_items), # total_items
194
+ @queue_collection.stats(:current_bytes), # bytes
195
+ @server.stats(:connections), # curr_connections
196
+ @server.stats(:total_connections), # total_connections
197
+ @server.stats(:get_requests), # get count
198
+ @server.stats(:set_requests), # set count
199
+ @queue_collection.stats(:get_hits),
200
+ @queue_collection.stats(:get_misses),
201
+ @server.stats(:bytes_read), # total bytes read
202
+ @server.stats(:bytes_written), # total bytes written
203
+ 0, # limit_maxbytes
204
+ queue_stats
205
+ end
206
+
207
+ def queue_stats
208
+ @queue_collection.queues.inject("") do |m,(k,v)|
209
+ m + sprintf(QUEUE_STATS_RESPONSE,
210
+ k, v.length,
211
+ k, v.total_items,
212
+ k, v.logsize,
213
+ k, @expiry_stats[k],
214
+ k, v.current_age)
215
+ end
216
+ end
217
+
218
+ def logger
219
+ @logger
220
+ end
221
+ end
222
+ end