dharmarth-starling 0.9.9

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,181 @@
1
+ require 'memcache'
2
+
3
+ class Starling < MemCache
4
+
5
+ WAIT_TIME = 0.25
6
+ alias_method :_original_get, :get
7
+ alias_method :_original_delete, :delete
8
+
9
+ def initialize(*args)
10
+ super
11
+
12
+ # @buckets is no longer used in newer version of Memcache-client(1.6.2 onwards)
13
+ unless instance_variable_defined?(:@buckets)
14
+ # Create an array of server buckets for weight selection of servers.
15
+ @buckets = []
16
+ @servers.each do |server|
17
+ server.weight.times { @buckets.push(server) }
18
+ end
19
+ end
20
+ end
21
+
22
+ ##
23
+ # fetch an item from a queue.
24
+
25
+ def get(*args)
26
+ loop do
27
+ response = _original_get(*args)
28
+ return response unless response.nil?
29
+ sleep WAIT_TIME
30
+ end
31
+ end
32
+
33
+ ##
34
+ # will return the next item or nil
35
+
36
+ def fetch(*args)
37
+ _original_get(*args)
38
+ end
39
+
40
+ ##
41
+ # Delete the key (queue) from all Starling servers. This is necessary
42
+ # because the random way a server is chosen in #get_server_for_key
43
+ # implies that the queue could easily be spread across the entire
44
+ # Starling cluster.
45
+
46
+ def delete(key, expiry = 0)
47
+ with_servers do
48
+ _original_delete(key, expiry)
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Provides a way to work with a specific list of servers by
54
+ # forcing all calls to #get_server_for_key to use a specific
55
+ # server, and changing that server each time that the call
56
+ # yields to the block provided. This helps work around the
57
+ # normally random nature of the #get_server_for_key method.
58
+ #
59
+ # Acquires the mutex for the entire duration of the call
60
+ # since unrelated calls to #get_server_for_key might be
61
+ # adversely affected by the non_random result.
62
+ def with_servers(my_servers = @servers.dup)
63
+ return unless block_given?
64
+ with_lock do
65
+ my_servers.each do |server|
66
+ @force_server = server
67
+ yield
68
+ end
69
+ @force_server = nil
70
+ end
71
+ end
72
+
73
+ ##
74
+ # insert +value+ into +queue+.
75
+ #
76
+ # +expiry+ is expressed as a UNIX timestamp
77
+ #
78
+ # If +raw+ is true, +value+ will not be Marshalled. If +raw+ = :yaml, +value+
79
+ # will be serialized with YAML, instead.
80
+
81
+ def set(queue, value, expiry = 0, raw = false)
82
+ retries = 0
83
+ begin
84
+ if raw == :yaml
85
+ value = YAML.dump(value)
86
+ raw = true
87
+ end
88
+
89
+ super(queue, value, expiry, raw)
90
+ rescue MemCache::MemCacheError => e
91
+ retries += 1
92
+ sleep WAIT_TIME
93
+ retry unless retries > 3
94
+ raise e
95
+ end
96
+ end
97
+
98
+ ##
99
+ # returns the number of items in +queue+. If +queue+ is +:all+, a hash of all
100
+ # queue sizes will be returned.
101
+
102
+ def sizeof(queue, statistics = nil)
103
+ statistics ||= stats
104
+
105
+ if queue == :all
106
+ queue_sizes = {}
107
+ available_queues(statistics).each do |queue|
108
+ queue_sizes[queue] = sizeof(queue, statistics)
109
+ end
110
+ return queue_sizes
111
+ end
112
+
113
+ statistics.inject(0) { |m,(k,v)| m + v["queue_#{queue}_items"].to_i }
114
+ end
115
+
116
+ ##
117
+ # returns a list of available (currently allocated) queues.
118
+
119
+ def available_queues(statistics = nil)
120
+ statistics ||= stats
121
+
122
+ statistics.map { |k,v|
123
+ v.keys
124
+ }.flatten.uniq.grep(/^queue_(.*)_items/).map { |v|
125
+ v.gsub(/^queue_/, '').gsub(/_items$/, '')
126
+ }.reject { |v|
127
+ v =~ /_total$/ || v =~ /_expired$/
128
+ }
129
+ end
130
+
131
+ ##
132
+ # iterator to flush +queue+. Each element will be passed to the provided
133
+ # +block+
134
+
135
+ def flush(queue)
136
+ sizeof(queue).times do
137
+ v = get(queue)
138
+ yield v if block_given?
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def get_server_for_key(key)
145
+ raise ArgumentError, "illegal character in key #{key.inspect}" if key =~ /\s/
146
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
147
+ raise MemCacheError, "No servers available" if @servers.empty?
148
+ return @force_server if @force_server
149
+
150
+ bukkits = @buckets.dup
151
+ bukkits.count {|i| !i.nil?}.times do |try|
152
+ n = rand(bukkits.count {|i| !i.nil?})
153
+ server = bukkits[n]
154
+ return server if server.alive?
155
+ bukkits.delete_at(n)
156
+ end
157
+
158
+ raise MemCacheError, "No servers available (all dead)"
159
+ end
160
+ end
161
+
162
+
163
+ class MemCache
164
+
165
+ protected
166
+
167
+ ##
168
+ # Ensure that everything within the given block is executed
169
+ # within the locked mutex if this client is multithreaded.
170
+ # If the client isn't multithreaded, the block is simply executed.
171
+ def with_lock
172
+ return unless block_given?
173
+ begin
174
+ @mutex.lock if @multithread
175
+ yield
176
+ ensure
177
+ @mutex.unlock if @multithread
178
+ end
179
+ end
180
+
181
+ end
@@ -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