mmmurf-starling 0.9.7.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,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
@@ -0,0 +1,151 @@
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
+ private
88
+
89
+ def log_path #:nodoc:
90
+ File.join(@persistence_path, @queue_name)
91
+ end
92
+
93
+ def reopen_log #:nodoc:
94
+ @trx = File.new(log_path, File::CREAT|File::RDWR)
95
+ @logsize = File.size(log_path)
96
+ end
97
+
98
+ def rotate_log #:nodoc:
99
+ @trx.close
100
+ backup_logfile = "#{log_path}.#{Time.now.to_i}"
101
+ File.rename(log_path, backup_logfile)
102
+ reopen_log
103
+ File.unlink(backup_logfile)
104
+ end
105
+
106
+ def replay_transaction_log(debug) #:nodoc:
107
+ reopen_log
108
+ bytes_read = 0
109
+
110
+ print "Reading back transaction log for #{@queue_name} " if debug
111
+
112
+ while !@trx.eof?
113
+ cmd = @trx.read(1)
114
+ case cmd
115
+ when TRX_CMD_PUSH
116
+ print ">" if debug
117
+ raw_size = @trx.read(4)
118
+ next unless raw_size
119
+ size = raw_size.unpack("I").first
120
+ data = @trx.read(size)
121
+ next unless data
122
+ push(data, false)
123
+ bytes_read += data.size
124
+ when TRX_CMD_POP
125
+ print "<" if debug
126
+ bytes_read -= pop(false).size
127
+ else
128
+ puts "Error reading transaction log: " +
129
+ "I don't understand '#{cmd}' (skipping)." if debug
130
+ end
131
+ end
132
+
133
+ print " done.\n" if debug
134
+
135
+ return bytes_read
136
+ end
137
+
138
+ def transaction(data) #:nodoc:
139
+ raise "no transaction log handle. that totally sucks." unless @trx
140
+
141
+ @trx.write_nonblock data
142
+ @logsize += data.size
143
+ rotate_log if @logsize > SOFT_LOG_MAX_SIZE && self.length == 0
144
+ end
145
+
146
+ def now_usec
147
+ now = Time.now
148
+ now.to_i * 1000000 + now.usec
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,141 @@
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 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
+ ##
64
+ # Returns all active queues.
65
+
66
+ def queues(key=nil)
67
+ return nil if @shutdown_mutex.locked?
68
+
69
+ return @queues if key.nil?
70
+
71
+ # First try to return the queue named 'key' if it's available.
72
+ return @queues[key] if @queues[key]
73
+
74
+ # If the queue wasn't available, create or get the mutex that will
75
+ # wrap creation of the Queue.
76
+ @queue_init_mutexes[key] ||= Mutex.new
77
+
78
+ # Otherwise, check to see if another process is already loading
79
+ # the queue named 'key'.
80
+ if @queue_init_mutexes[key].locked?
81
+ # return an empty/false result if we're waiting for the queue
82
+ # to be loaded and we're not the first process to request the queue
83
+ return nil
84
+ else
85
+ begin
86
+ @queue_init_mutexes[key].lock
87
+ # we've locked the mutex, but only go ahead if the queue hasn't
88
+ # been loaded. There's a race condition otherwise, and we could
89
+ # end up loading the queue multiple times.
90
+ if @queues[key].nil?
91
+ @queues[key] = PersistentQueue.new(@path, key)
92
+ @stats[:current_bytes] += @queues[key].initial_bytes
93
+ end
94
+ rescue Object => exc
95
+ puts "ZOMG There was an exception reading back the queue. That totally sucks."
96
+ puts "The exception was: #{exc}. Backtrace: #{exc.backtrace.join("\n")}"
97
+ ensure
98
+ @queue_init_mutexes[key].unlock
99
+ end
100
+ end
101
+
102
+ return @queues[key]
103
+ end
104
+
105
+ ##
106
+ # Returns statistic +stat_name+ for the QueueCollection.
107
+ #
108
+ # Valid statistics are:
109
+ #
110
+ # [:get_misses] Total number of get requests with empty responses
111
+ # [:get_hits] Total number of get requests that returned data
112
+ # [:current_bytes] Current size in bytes of items in the queues
113
+ # [:current_size] Current number of items across all queues
114
+ # [:total_items] Total number of items stored in queues.
115
+
116
+ def stats(stat_name)
117
+ case stat_name
118
+ when nil; @stats
119
+ when :current_size; current_size
120
+ else; @stats[stat_name]
121
+ end
122
+ end
123
+
124
+ ##
125
+ # Safely close all queues.
126
+
127
+ def close
128
+ @shutdown_mutex.lock
129
+ @queues.each_pair do |name,queue|
130
+ queue.close
131
+ @queues.delete(name)
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ def current_size #:nodoc:
138
+ @queues.inject(0) { |m, (k,v)| m + v.length }
139
+ end
140
+ end
141
+ end