ctcherry-starling 0.9.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,237 @@
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
+ # DELETE Responses
27
+ DELETE_COMMAND = /\Adelete (.{1,250}) ([0-9]+)\r\n/m
28
+ DELETE_RESPONSE = "END\r\n".freeze
29
+
30
+ # STAT Response
31
+ STATS_COMMAND = /\Astats\r\n/m
32
+ STATS_RESPONSE = "STAT pid %d\r
33
+ STAT uptime %d\r
34
+ STAT time %d\r
35
+ STAT version %s\r
36
+ STAT rusage_user %0.6f\r
37
+ STAT rusage_system %0.6f\r
38
+ STAT curr_items %d\r
39
+ STAT total_items %d\r
40
+ STAT bytes %d\r
41
+ STAT curr_connections %d\r
42
+ STAT total_connections %d\r
43
+ STAT cmd_get %d\r
44
+ STAT cmd_set %d\r
45
+ STAT get_hits %d\r
46
+ STAT get_misses %d\r
47
+ STAT bytes_read %d\r
48
+ STAT bytes_written %d\r
49
+ STAT limit_maxbytes %d\r
50
+ %sEND\r\n".freeze
51
+ QUEUE_STATS_RESPONSE = "STAT queue_%s_items %d\r
52
+ STAT queue_%s_total_items %d\r
53
+ STAT queue_%s_logsize %d\r
54
+ STAT queue_%s_expired_items %d\r
55
+ STAT queue_%s_age %d\r\n".freeze
56
+
57
+ SHUTDOWN_COMMAND = /\Ashutdown\r\n/m
58
+
59
+ QUIT_COMMAND = /\Aquit\r\n/m
60
+
61
+ @@next_session_id = 1
62
+
63
+ ##
64
+ # Creates a new handler for the MemCache protocol that communicates with a
65
+ # given client.
66
+
67
+ def initialize(options = {})
68
+ @opts = options
69
+ end
70
+
71
+ ##
72
+ # Process incoming commands from the attached client.
73
+
74
+ def post_init
75
+ @stash = []
76
+ @data = ""
77
+ @data_buf = ""
78
+ @server = @opts[:server]
79
+ @logger = StarlingServer::Base.logger
80
+ @expiry_stats = Hash.new(0)
81
+ @expected_length = nil
82
+ @server.stats[:total_connections] += 1
83
+ set_comm_inactivity_timeout @opts[:timeout]
84
+ @queue_collection = @opts[:queue]
85
+
86
+ @session_id = @@next_session_id
87
+ @@next_session_id += 1
88
+
89
+ peer = Socket.unpack_sockaddr_in(get_peername)
90
+ #@logger.debug "(#{@session_id}) New session from #{peer[1]}:#{peer[0]}"
91
+ end
92
+
93
+ def receive_data(incoming)
94
+ @server.stats[:bytes_read] += incoming.size
95
+ @data << incoming
96
+
97
+ while data = @data.slice!(/.*?\r\n/m)
98
+ response = process(data)
99
+ end
100
+
101
+ send_data response if response
102
+ end
103
+
104
+ def process(data)
105
+ data = @data_buf + data if @data_buf.size > 0
106
+ # our only non-normal state is consuming an object's data
107
+ # when @expected_length is present
108
+ if @expected_length && data.size == @expected_length
109
+ response = set_data(data)
110
+ @data_buf = ""
111
+ return response
112
+ elsif @expected_length
113
+ @data_buf = data
114
+ return
115
+ end
116
+ case data
117
+ when SET_COMMAND
118
+ @server.stats[:set_requests] += 1
119
+ set($1, $2, $3, $4.to_i)
120
+ when GET_COMMAND
121
+ @server.stats[:get_requests] += 1
122
+ get($1)
123
+ when STATS_COMMAND
124
+ stats
125
+ when SHUTDOWN_COMMAND
126
+ # no point in responding, they'll never get it.
127
+ Runner::shutdown
128
+ when DELETE_COMMAND
129
+ delete $1
130
+ when QUIT_COMMAND
131
+ # ignore the command, client is closing connection.
132
+ return nil
133
+ else
134
+ logger.warn "Unknown command: #{data}."
135
+ respond ERR_UNKNOWN_COMMAND
136
+ end
137
+ rescue => e
138
+ logger.error "Error handling request: #{e}."
139
+ logger.debug e.backtrace.join("\n")
140
+ respond GET_RESPONSE_EMPTY
141
+ end
142
+
143
+ def unbind
144
+ #@logger.debug "(#{@session_id}) connection ends"
145
+ end
146
+
147
+ private
148
+
149
+ def delete(queue)
150
+ @queue_collection.delete(queue)
151
+ respond DELETE_RESPONSE
152
+ end
153
+
154
+ def respond(str, *args)
155
+ response = sprintf(str, *args)
156
+ @server.stats[:bytes_written] += response.length
157
+ response
158
+ end
159
+
160
+ def set(key, flags, expiry, len)
161
+ @expected_length = len + 2
162
+ @stash = [ key, flags, expiry ]
163
+ nil
164
+ end
165
+
166
+ def set_data(incoming)
167
+ key, flags, expiry = @stash
168
+ data = incoming.slice(0...@expected_length-2)
169
+ @stash = []
170
+ @expected_length = nil
171
+
172
+ internal_data = [expiry.to_i, data].pack(DATA_PACK_FMT)
173
+ if @queue_collection.put(key, internal_data)
174
+ respond SET_RESPONSE_SUCCESS
175
+ else
176
+ respond SET_RESPONSE_FAILURE
177
+ end
178
+ end
179
+
180
+ def get(key)
181
+ now = Time.now.to_i
182
+
183
+ while response = @queue_collection.take(key)
184
+ expiry, data = response.unpack(DATA_PACK_FMT)
185
+
186
+ break if expiry == 0 || expiry >= now
187
+
188
+ @expiry_stats[key] += 1
189
+ expiry, data = nil
190
+ end
191
+
192
+ if data
193
+ respond GET_RESPONSE, key, 0, data.size, data
194
+ else
195
+ respond GET_RESPONSE_EMPTY
196
+ end
197
+ end
198
+
199
+ def stats
200
+ respond STATS_RESPONSE,
201
+ Process.pid, # pid
202
+ Time.now - @server.stats(:start_time), # uptime
203
+ Time.now.to_i, # time
204
+ StarlingServer::VERSION, # version
205
+ Process.times.utime, # rusage_user
206
+ Process.times.stime, # rusage_system
207
+ @queue_collection.stats(:current_size), # curr_items
208
+ @queue_collection.stats(:total_items), # total_items
209
+ @queue_collection.stats(:current_bytes), # bytes
210
+ @server.stats(:connections), # curr_connections
211
+ @server.stats(:total_connections), # total_connections
212
+ @server.stats(:get_requests), # get count
213
+ @server.stats(:set_requests), # set count
214
+ @queue_collection.stats(:get_hits),
215
+ @queue_collection.stats(:get_misses),
216
+ @server.stats(:bytes_read), # total bytes read
217
+ @server.stats(:bytes_written), # total bytes written
218
+ 0, # limit_maxbytes
219
+ queue_stats
220
+ end
221
+
222
+ def queue_stats
223
+ @queue_collection.queues.inject("") do |m,(k,v)|
224
+ m + sprintf(QUEUE_STATS_RESPONSE,
225
+ k, v.length,
226
+ k, v.total_items,
227
+ k, v.logsize,
228
+ k, @expiry_stats[k],
229
+ k, v.current_age)
230
+ end
231
+ end
232
+
233
+ def logger
234
+ @logger
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,156 @@
1
+ module StarlingServer
2
+
3
+ ##
4
+ # PersistentQueue is a subclass of Ruby's thread-safe Queue class. It adds a
5
+ # transactional log to the in-memory Queue, which enables quickly rebuilding
6
+ # the Queue in the event of a sever outage.
7
+
8
+ class PersistentQueue < Queue
9
+
10
+ ##
11
+ # When a log reaches the SOFT_LOG_MAX_SIZE, the Queue will wait until
12
+ # it is empty, and will then rotate the log file.
13
+
14
+ SOFT_LOG_MAX_SIZE = 16 * (1024**2) # 16 MB
15
+
16
+ TRX_CMD_PUSH = "\000".freeze
17
+ TRX_CMD_POP = "\001".freeze
18
+
19
+ TRX_PUSH = "\000%s%s".freeze
20
+ TRX_POP = "\001".freeze
21
+
22
+ attr_reader :initial_bytes
23
+ attr_reader :total_items
24
+ attr_reader :logsize
25
+ attr_reader :current_age
26
+
27
+ ##
28
+ # Create a new PersistentQueue at +persistence_path+/+queue_name+.
29
+ # If a queue log exists at that path, the Queue will be loaded from
30
+ # disk before being available for use.
31
+
32
+ def initialize(persistence_path, queue_name, debug = false)
33
+ @persistence_path = persistence_path
34
+ @queue_name = queue_name
35
+ @total_items = 0
36
+ super()
37
+ @initial_bytes = replay_transaction_log(debug)
38
+ @current_age = 0
39
+ end
40
+
41
+ ##
42
+ # Pushes +value+ to the queue. By default, +push+ will write to the
43
+ # transactional log. Set +log_trx=false+ to override this behaviour.
44
+
45
+ def push(value, log_trx = true)
46
+ if log_trx
47
+ raise NoTransactionLog unless @trx
48
+ size = [value.size].pack("I")
49
+ transaction sprintf(TRX_PUSH, size, value)
50
+ end
51
+
52
+ @total_items += 1
53
+ super([now_usec, value])
54
+ end
55
+
56
+ ##
57
+ # Retrieves data from the queue.
58
+
59
+ def pop(log_trx = true)
60
+ raise NoTransactionLog if log_trx && !@trx
61
+
62
+ begin
63
+ rv = super(!log_trx)
64
+ rescue ThreadError
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."
66
+ rv = [now_usec, '']
67
+ end
68
+ transaction "\001" if log_trx
69
+ @current_age = (now_usec - rv[0]) / 1000
70
+ rv[1]
71
+ end
72
+
73
+ ##
74
+ # Safely closes the transactional queue.
75
+
76
+ def close
77
+ # Ok, yeah, this is lame, and is *technically* a race condition. HOWEVER,
78
+ # the QueueCollection *should* have stopped processing requests, and I don't
79
+ # want to add yet another Mutex around all the push and pop methods. So we
80
+ # do the next simplest thing, and minimize the time we'll stick around before
81
+ # @trx is nil.
82
+ @not_trx = @trx
83
+ @trx = nil
84
+ @not_trx.close
85
+ end
86
+
87
+ def purge
88
+ close
89
+ File.delete(log_path)
90
+ end
91
+
92
+ private
93
+
94
+ def log_path #:nodoc:
95
+ File.join(@persistence_path, @queue_name)
96
+ end
97
+
98
+ def reopen_log #:nodoc:
99
+ @trx = File.new(log_path, File::CREAT|File::RDWR)
100
+ @logsize = File.size(log_path)
101
+ end
102
+
103
+ def rotate_log #:nodoc:
104
+ @trx.close
105
+ backup_logfile = "#{log_path}.#{Time.now.to_i}"
106
+ File.rename(log_path, backup_logfile)
107
+ reopen_log
108
+ File.unlink(backup_logfile)
109
+ end
110
+
111
+ def replay_transaction_log(debug) #:nodoc:
112
+ reopen_log
113
+ bytes_read = 0
114
+
115
+ print "Reading back transaction log for #{@queue_name} " if debug
116
+
117
+ while !@trx.eof?
118
+ cmd = @trx.read(1)
119
+ case cmd
120
+ when TRX_CMD_PUSH
121
+ print ">" if debug
122
+ raw_size = @trx.read(4)
123
+ next unless raw_size
124
+ size = raw_size.unpack("I").first
125
+ data = @trx.read(size)
126
+ next unless data
127
+ push(data, false)
128
+ bytes_read += data.size
129
+ when TRX_CMD_POP
130
+ print "<" if debug
131
+ bytes_read -= pop(false).size
132
+ else
133
+ puts "Error reading transaction log: " +
134
+ "I don't understand '#{cmd}' (skipping)." if debug
135
+ end
136
+ end
137
+
138
+ print " done.\n" if debug
139
+
140
+ return bytes_read
141
+ end
142
+
143
+ def transaction(data) #:nodoc:
144
+ raise "no transaction log handle. that totally sucks." unless @trx
145
+
146
+ @trx.write_nonblock data
147
+ @logsize += data.size
148
+ rotate_log if @logsize > SOFT_LOG_MAX_SIZE && self.length == 0
149
+ end
150
+
151
+ def now_usec
152
+ now = Time.now
153
+ now.to_i * 1000000 + now.usec
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,147 @@
1
+ require 'thread'
2
+ require 'starling/persistent_queue'
3
+
4
+ module StarlingServer
5
+ class InaccessibleQueuePath < Exception #:nodoc:
6
+ end
7
+
8
+ ##
9
+ # QueueCollection is a proxy to a collection of PersistentQueue instances.
10
+
11
+ class QueueCollection
12
+
13
+ ##
14
+ # Create a new QueueCollection at +path+
15
+
16
+ def initialize(path)
17
+ unless File.directory?(path) && File.writable?(path)
18
+ raise InaccessibleQueuePath.new("'#{path}' must exist and be read-writable by #{Etc.getpwuid(Process.uid).name}.")
19
+ end
20
+
21
+ @shutdown_mutex = Mutex.new
22
+
23
+ @path = path
24
+ @logger = StarlingServer::Base.logger
25
+
26
+ @queues = {}
27
+ @queue_init_mutexes = {}
28
+
29
+ @stats = Hash.new(0)
30
+ end
31
+
32
+ ##
33
+ # Puts +data+ onto the queue named +key+
34
+
35
+ def put(key, data)
36
+ queue = queues(key)
37
+ return nil unless queue
38
+
39
+ @stats[:current_bytes] += data.size
40
+ @stats[:total_items] += 1
41
+
42
+ queue.push(data)
43
+
44
+ return true
45
+ end
46
+
47
+ ##
48
+ # Retrieves data from the queue named +key+
49
+
50
+ def take(key)
51
+ queue = queues(key)
52
+ if queue.nil? || queue.length == 0
53
+ @stats[:get_misses] += 1
54
+ return nil
55
+ else
56
+ @stats[:get_hits] += 1
57
+ end
58
+ result = queue.pop
59
+ @stats[:current_bytes] -= result.size
60
+ result
61
+ end
62
+
63
+ def delete(key)
64
+ queue = @queues.delete(key)
65
+ return if queue.nil?
66
+ queue.purge
67
+ end
68
+
69
+ ##
70
+ # Returns all active queues.
71
+
72
+ def queues(key=nil)
73
+ return nil if @shutdown_mutex.locked?
74
+
75
+ return @queues if key.nil?
76
+
77
+ # First try to return the queue named 'key' if it's available.
78
+ return @queues[key] if @queues[key]
79
+
80
+ # If the queue wasn't available, create or get the mutex that will
81
+ # wrap creation of the Queue.
82
+ @queue_init_mutexes[key] ||= Mutex.new
83
+
84
+ # Otherwise, check to see if another process is already loading
85
+ # the queue named 'key'.
86
+ if @queue_init_mutexes[key].locked?
87
+ # return an empty/false result if we're waiting for the queue
88
+ # to be loaded and we're not the first process to request the queue
89
+ return nil
90
+ else
91
+ begin
92
+ @queue_init_mutexes[key].lock
93
+ # we've locked the mutex, but only go ahead if the queue hasn't
94
+ # been loaded. There's a race condition otherwise, and we could
95
+ # end up loading the queue multiple times.
96
+ if @queues[key].nil?
97
+ @queues[key] = PersistentQueue.new(@path, key)
98
+ @stats[:current_bytes] += @queues[key].initial_bytes
99
+ end
100
+ rescue Object => exc
101
+ puts "ZOMG There was an exception reading back the queue. That totally sucks."
102
+ puts "The exception was: #{exc}. Backtrace: #{exc.backtrace.join("\n")}"
103
+ ensure
104
+ @queue_init_mutexes[key].unlock
105
+ end
106
+ end
107
+
108
+ return @queues[key]
109
+ end
110
+
111
+ ##
112
+ # Returns statistic +stat_name+ for the QueueCollection.
113
+ #
114
+ # Valid statistics are:
115
+ #
116
+ # [:get_misses] Total number of get requests with empty responses
117
+ # [:get_hits] Total number of get requests that returned data
118
+ # [:current_bytes] Current size in bytes of items in the queues
119
+ # [:current_size] Current number of items across all queues
120
+ # [:total_items] Total number of items stored in queues.
121
+
122
+ def stats(stat_name)
123
+ case stat_name
124
+ when nil; @stats
125
+ when :current_size; current_size
126
+ else; @stats[stat_name]
127
+ end
128
+ end
129
+
130
+ ##
131
+ # Safely close all queues.
132
+
133
+ def close
134
+ @shutdown_mutex.lock
135
+ @queues.each_pair do |name,queue|
136
+ queue.close
137
+ @queues.delete(name)
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def current_size #:nodoc:
144
+ @queues.inject(0) { |m, (k,v)| m + v.length }
145
+ end
146
+ end
147
+ end