ctcherry-starling 0.9.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,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