fiveruns-starling 0.9.7.5

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,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