advany-starling 0.9.7.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)
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