ryana-starling 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,233 @@
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
+
60
+ @@next_session_id = 1
61
+
62
+ ##
63
+ # Creates a new handler for the MemCache protocol that communicates with a
64
+ # given client.
65
+
66
+ def initialize(options = {})
67
+ @opts = options
68
+ end
69
+
70
+ ##
71
+ # Process incoming commands from the attached client.
72
+
73
+ def post_init
74
+ @stash = []
75
+ @data = ""
76
+ @data_buf = ""
77
+ @server = @opts[:server]
78
+ @logger = StarlingServer::Base.logger
79
+ @expiry_stats = Hash.new(0)
80
+ @expected_length = nil
81
+ @server.stats[:total_connections] += 1
82
+ set_comm_inactivity_timeout @opts[:timeout]
83
+ @queue_collection = @opts[:queue]
84
+
85
+ @session_id = @@next_session_id
86
+ @@next_session_id += 1
87
+
88
+ peer = Socket.unpack_sockaddr_in(get_peername)
89
+ #@logger.debug "(#{@session_id}) New session from #{peer[1]}:#{peer[0]}"
90
+ end
91
+
92
+ def receive_data(incoming)
93
+ @server.stats[:bytes_read] += incoming.size
94
+ @data << incoming
95
+
96
+ while data = @data.slice!(/.*?\r\n/m)
97
+ response = process(data)
98
+ end
99
+
100
+ send_data response if response
101
+ end
102
+
103
+ def process(data)
104
+ data = @data_buf + data if @data_buf.size > 0
105
+ # our only non-normal state is consuming an object's data
106
+ # when @expected_length is present
107
+ if @expected_length && data.size == @expected_length
108
+ response = set_data(data)
109
+ @data_buf = ""
110
+ return response
111
+ elsif @expected_length
112
+ @data_buf = data
113
+ return
114
+ end
115
+ case data
116
+ when SET_COMMAND
117
+ @server.stats[:set_requests] += 1
118
+ set($1, $2, $3, $4.to_i)
119
+ when GET_COMMAND
120
+ @server.stats[:get_requests] += 1
121
+ get($1)
122
+ when STATS_COMMAND
123
+ stats
124
+ when SHUTDOWN_COMMAND
125
+ # no point in responding, they'll never get it.
126
+ Runner::shutdown
127
+ when DELETE_COMMAND
128
+ delete $1
129
+ else
130
+ logger.warn "Unknown command: #{data}."
131
+ respond ERR_UNKNOWN_COMMAND
132
+ end
133
+ rescue => e
134
+ logger.error "Error handling request: #{e}."
135
+ logger.debug e.backtrace.join("\n")
136
+ respond GET_RESPONSE_EMPTY
137
+ end
138
+
139
+ def unbind
140
+ #@logger.debug "(#{@session_id}) connection ends"
141
+ end
142
+
143
+ private
144
+
145
+ def delete(queue)
146
+ @queue_collection.delete(queue)
147
+ respond DELETE_RESPONSE
148
+ end
149
+
150
+ def respond(str, *args)
151
+ response = sprintf(str, *args)
152
+ @server.stats[:bytes_written] += response.length
153
+ response
154
+ end
155
+
156
+ def set(key, flags, expiry, len)
157
+ @expected_length = len + 2
158
+ @stash = [ key, flags, expiry ]
159
+ nil
160
+ end
161
+
162
+ def set_data(incoming)
163
+ key, flags, expiry = @stash
164
+ data = incoming.slice(0...@expected_length-2)
165
+ @stash = []
166
+ @expected_length = nil
167
+
168
+ internal_data = [expiry.to_i, data].pack(DATA_PACK_FMT)
169
+ if @queue_collection.put(key, internal_data)
170
+ respond SET_RESPONSE_SUCCESS
171
+ else
172
+ respond SET_RESPONSE_FAILURE
173
+ end
174
+ end
175
+
176
+ def get(key)
177
+ now = Time.now.to_i
178
+
179
+ while response = @queue_collection.take(key)
180
+ expiry, data = response.unpack(DATA_PACK_FMT)
181
+
182
+ break if expiry == 0 || expiry >= now
183
+
184
+ @expiry_stats[key] += 1
185
+ expiry, data = nil
186
+ end
187
+
188
+ if data
189
+ respond GET_RESPONSE, key, 0, data.size, data
190
+ else
191
+ respond GET_RESPONSE_EMPTY
192
+ end
193
+ end
194
+
195
+ def stats
196
+ respond STATS_RESPONSE,
197
+ Process.pid, # pid
198
+ Time.now - @server.stats(:start_time), # uptime
199
+ Time.now.to_i, # time
200
+ StarlingServer::VERSION, # version
201
+ Process.times.utime, # rusage_user
202
+ Process.times.stime, # rusage_system
203
+ @queue_collection.stats(:current_size), # curr_items
204
+ @queue_collection.stats(:total_items), # total_items
205
+ @queue_collection.stats(:current_bytes), # bytes
206
+ @server.stats(:connections), # curr_connections
207
+ @server.stats(:total_connections), # total_connections
208
+ @server.stats(:get_requests), # get count
209
+ @server.stats(:set_requests), # set count
210
+ @queue_collection.stats(:get_hits),
211
+ @queue_collection.stats(:get_misses),
212
+ @server.stats(:bytes_read), # total bytes read
213
+ @server.stats(:bytes_written), # total bytes written
214
+ 0, # limit_maxbytes
215
+ queue_stats
216
+ end
217
+
218
+ def queue_stats
219
+ @queue_collection.queues.inject("") do |m,(k,v)|
220
+ m + sprintf(QUEUE_STATS_RESPONSE,
221
+ k, v.length,
222
+ k, v.total_items,
223
+ k, v.logsize,
224
+ k, @expiry_stats[k],
225
+ k, v.current_age)
226
+ end
227
+ end
228
+
229
+ def logger
230
+ @logger
231
+ end
232
+ end
233
+ 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