starling 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ == 1.0.0 2007-11-02
2
+
3
+ * Initial release
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Blaine Cook, Twitter, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ bin/starling
7
+ config/hoe.rb
8
+ config/requirements.rb
9
+ lib/starling.rb
10
+ lib/starling/handler.rb
11
+ lib/starling/persistent_queue.rb
12
+ lib/starling/queue_collection.rb
13
+ lib/starling/runner.rb
14
+ lib/starling/server.rb
15
+ script/destroy
16
+ script/generate
17
+ script/txt2html
18
+ setup.rb
19
+ tasks/deployment.rake
20
+ tasks/environment.rake
21
+ tasks/website.rake
22
+ test/test_helper.rb
23
+ test/test_persistent_queue.rb
24
+ test/test_starling.rb
25
+ website/index.html
26
+ website/index.txt
27
+ website/javascripts/rounded_corners_lite.inc.js
28
+ website/stylesheets/screen.css
29
+ website/template.rhtml
@@ -0,0 +1,42 @@
1
+ = Name
2
+
3
+ Starling - a light weight server for reliable distributed message passing.
4
+
5
+ = Synopsis
6
+
7
+ # Start the Starling server as a daemonized process:
8
+ starling -h 192.168.1.1 -d
9
+
10
+ # Put messages onto a queue:
11
+ require 'memcache'
12
+ starling = MemCache.new('192.168.1.1:22122')
13
+ starling.set('my_queue', 12345)
14
+
15
+ # Get messages from the queue:
16
+ require 'memcache'
17
+ starling = MemCache.new('192.168.1.1:22122')
18
+ loop { puts starling.get('my_queue') }
19
+
20
+ # See the Starling documentation for more information.
21
+
22
+ = Description
23
+
24
+ Starling is a powerful but simple messaging server that enables reliable
25
+ distributed queuing with an absolutely minimal overhead. It speaks the
26
+ MemCache protocol for maximum cross-platform compatibility. Any language
27
+ that speaks MemCache can take advantage of Starling's queue facilities.
28
+
29
+ = Known Issues
30
+
31
+ * Starling is "slow" as far as messaging systems are concerned. In practice,
32
+ it's fast enough. If you'd like to help make it faster please do. Starting
33
+ points would be to use an event-driven interface, and get rid of threading.
34
+
35
+ = Authors
36
+
37
+ Blaine Cook <romeda@gmail.com>
38
+
39
+ = Copyright
40
+
41
+ Starling - a light-weight server for reliable distributed message passing.
42
+ Copyright 2007 Blaine Cook <blaine@twitter.com>, Twitter Inc.
@@ -0,0 +1,4 @@
1
+ require 'config/requirements'
2
+ require 'config/hoe' # setup Hoe + all gem configuration
3
+
4
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'starling/runner'
4
+ StarlingServer::Runner.run
@@ -0,0 +1,71 @@
1
+ require 'starling/server'
2
+
3
+ AUTHOR = 'Blaine Cook' # can also be an array of Authors
4
+ EMAIL = "blaine@twitter.com"
5
+ DESCRIPTION = "Starling is a lightweight, transactional, distributed queue server"
6
+ GEM_NAME = 'starling' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'starling' # The unix name for your project
8
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
9
+ DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
10
+
11
+ @config_file = "~/.rubyforge/user-config.yml"
12
+ @config = nil
13
+ RUBYFORGE_USERNAME = "unknown"
14
+ def rubyforge_username
15
+ unless @config
16
+ begin
17
+ @config = YAML.load(File.read(File.expand_path(@config_file)))
18
+ rescue
19
+ puts <<-EOS
20
+ ERROR: No rubyforge config file found: #{@config_file}
21
+ Run 'rubyforge setup' to prepare your env for access to Rubyforge
22
+ - See http://newgem.rubyforge.org/rubyforge.html for more details
23
+ EOS
24
+ exit
25
+ end
26
+ end
27
+ RUBYFORGE_USERNAME.replace @config["username"]
28
+ end
29
+
30
+
31
+ REV = nil
32
+ # UNCOMMENT IF REQUIRED:
33
+ # REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
34
+ VERS = StarlingServer::VERSION + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'starling documentation',
36
+ "--opname", "index.html",
37
+ "--line-numbers",
38
+ "--main", "README",
39
+ "--inline-source"]
40
+
41
+ class Hoe
42
+ def extra_deps
43
+ @extra_deps.reject! { |x| Array(x).first == 'hoe' }
44
+ @extra_deps
45
+ end
46
+ end
47
+
48
+ # Generate all the Rake tasks
49
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
50
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
51
+ p.author = AUTHOR
52
+ p.description = DESCRIPTION
53
+ p.email = EMAIL
54
+ p.summary = DESCRIPTION
55
+ p.url = HOMEPATH
56
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
57
+ p.test_globs = ["test/**/test_*.rb"]
58
+ p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
59
+
60
+ # == Optional
61
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\\n\\n")
62
+ #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
63
+
64
+ #p.spec_extras = {} # A hash of extra values to set in the gemspec.
65
+
66
+ end
67
+
68
+ CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
69
+ PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
70
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
71
+ hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,15 @@
1
+ require 'fileutils'
2
+ include FileUtils
3
+
4
+ require 'rubygems'
5
+ %w[rake hoe newgem rubigen].each do |req_gem|
6
+ begin
7
+ require req_gem
8
+ rescue LoadError
9
+ puts "This Rakefile requires the '#{req_gem}' RubyGem."
10
+ puts "Installation: gem install #{req_gem} -y"
11
+ exit
12
+ end
13
+ end
14
+
15
+ $:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
@@ -0,0 +1,104 @@
1
+ require 'memcache'
2
+
3
+ class Starling < MemCache
4
+
5
+ WAIT_TIME = 0.25
6
+
7
+ ##
8
+ # fetch an item from a queue.
9
+
10
+ def get(*args)
11
+ loop do
12
+ response = super(*args)
13
+ return response unless response.nil?
14
+ sleep WAIT_TIME
15
+ end
16
+ end
17
+
18
+ ##
19
+ # insert +value+ into +queue+.
20
+ #
21
+ # +expiry+ is expressed as a UNIX timestamp
22
+ #
23
+ # If +raw+ is true, +value+ will not be Marshalled. If +raw+ = :yaml, +value+
24
+ # will be serialized with YAML, instead.
25
+
26
+ def set(queue, value, expiry = 0, raw = false)
27
+ retries = 0
28
+ begin
29
+ if raw == :yaml
30
+ value = YAML.dump(value)
31
+ raw = true
32
+ end
33
+
34
+ super(queue, value, expiry, raw)
35
+ rescue MemCache::MemCacheError => e
36
+ retries += 1
37
+ sleep WAIT_TIME
38
+ retry unless retries > 3
39
+ raise e
40
+ end
41
+ end
42
+
43
+ ##
44
+ # returns the number of items in +queue+. If +queue+ is +:all+, a hash of all
45
+ # queue sizes will be returned.
46
+
47
+ def sizeof(queue, statistics = nil)
48
+ statistics ||= stats
49
+
50
+ if queue == :all
51
+ queue_sizes = {}
52
+ available_queues(statistics).each do |queue|
53
+ queue_sizes[queue] = sizeof(queue, statistics)
54
+ end
55
+ return queue_sizes
56
+ end
57
+
58
+ statistics.inject(0) { |m,(k,v)| m + v["queue_#{queue}_items"].to_i }
59
+ end
60
+
61
+ ##
62
+ # returns a list of available (currently allocated) queues.
63
+
64
+ def available_queues(statistics = nil)
65
+ statistics ||= stats
66
+
67
+ statistics.map { |k,v|
68
+ v.keys
69
+ }.flatten.uniq.grep(/^queue_(.*)_items/).map { |v|
70
+ v.gsub(/^queue_/, '').gsub(/_items$/, '')
71
+ }.reject { |v|
72
+ v =~ /_total$/ || v =~ /_expired$/
73
+ }
74
+ end
75
+
76
+ ##
77
+ # iterator to flush +queue+. Each element will be passed to the provided
78
+ # +block+
79
+
80
+ def flush(queue)
81
+ sizeof(queue).times do
82
+ v = get(queue)
83
+ yield v if block_given?
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def get_server_for_key(key)
90
+ raise ArgumentError, "illegal character in key #{key.inspect}" if key =~ /\s/
91
+ raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
92
+ raise MemCacheError, "No servers available" if @servers.empty?
93
+
94
+ bukkits = @buckets.dup
95
+ bukkits.nitems.times do |try|
96
+ n = rand(bukkits.nitems)
97
+ server = bukkits[n]
98
+ return server if server.alive?
99
+ bukkits.delete_at(n)
100
+ end
101
+
102
+ raise MemCacheError, "No servers available (all dead)"
103
+ end
104
+ end
@@ -0,0 +1,181 @@
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
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 = /^get (.{1,250})\r\n$/
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 = /^set (.{1,250}) ([0-9]+) ([0-9]+) ([0-9]+)\r\n$/
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 = /stats\r\n$/
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\n".freeze
51
+
52
+ ##
53
+ # Creates a new handler for the MemCache protocol that communicates with a
54
+ # given client.
55
+
56
+ def initialize(client, server, queue_collection)
57
+ @client = client
58
+ @server = server
59
+ @queue_collection = queue_collection
60
+ @expiry_stats = Hash.new(0)
61
+ end
62
+
63
+ ##
64
+ # Process incoming commands from the attached client.
65
+
66
+ def run
67
+ while running? do
68
+ process_command(@client.readline)
69
+ Thread.current[:last_activity] = Time.now
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def running?
76
+ !Thread.current[:shutdown]
77
+ end
78
+
79
+ def respond(str, *args)
80
+ response = sprintf(str, *args)
81
+ @server.stats[:bytes_written] += response.length
82
+ @client.write response
83
+ end
84
+
85
+ def process_command(command)
86
+ begin
87
+ @server.stats[:bytes_read] += command.length
88
+ case command
89
+ when SET_COMMAND
90
+ @server.stats[:set_requests] += 1
91
+ set($1, $2, $3, $4.to_i)
92
+ when GET_COMMAND
93
+ @server.stats[:get_requests] += 1
94
+ get($1)
95
+ when STATS_COMMAND
96
+ stats
97
+ else
98
+ logger.warn "Unknown command: #{command[0,4]}.\nFull command was #{command}."
99
+ respond ERR_UNKNOWN_COMMAND
100
+ end
101
+ rescue => e
102
+ logger.error "Error handling request: #{e}."
103
+ logger.debug e.backtrace.join("\n")
104
+ respond GET_RESPONSE_EMPTY
105
+ end
106
+ end
107
+
108
+ def set(key, flags, expiry, len)
109
+ data = @client.read(len)
110
+ data_end = @client.read(2)
111
+ @server.stats[:bytes_read] += len + 2
112
+ if data_end == "\r\n" && data.size == len
113
+ internal_data = [expiry.to_i, data].pack(DATA_PACK_FMT)
114
+ if @queue_collection.put(key, internal_data)
115
+ respond SET_RESPONSE_SUCCESS
116
+ else
117
+ respond SET_RESPONSE_FAILURE
118
+ end
119
+ else
120
+ respond SET_CLIENT_DATA_ERROR
121
+ end
122
+ end
123
+
124
+ def get(key)
125
+ now = Time.now.to_i
126
+
127
+ while response = @queue_collection.take(key)
128
+ expiry, data = response.unpack(DATA_PACK_FMT)
129
+
130
+ break if expiry == 0 || expiry >= now
131
+
132
+ @expiry_stats[key] += 1
133
+ expiry, data = nil
134
+ end
135
+
136
+ if data
137
+ respond GET_RESPONSE, key, 0, data.size, data
138
+ else
139
+ respond GET_RESPONSE_EMPTY
140
+ end
141
+ end
142
+
143
+ def stats
144
+ respond STATS_RESPONSE,
145
+ Process.pid, # pid
146
+ Time.now - @server.stats(:start_time), # uptime
147
+ Time.now.to_i, # time
148
+ StarlingServer::VERSION, # version
149
+ Process.times.utime, # rusage_user
150
+ Process.times.stime, # rusage_system
151
+ @queue_collection.stats(:current_size), # curr_items
152
+ @queue_collection.stats(:total_items), # total_items
153
+ @queue_collection.stats(:current_bytes), # bytes
154
+ @server.stats(:connections), # curr_connections
155
+ @server.stats(:total_connections), # total_connections
156
+ @server.stats(:get_requests), # get count
157
+ @server.stats(:set_requests), # set count
158
+ @queue_collection.stats(:get_hits),
159
+ @queue_collection.stats(:get_misses),
160
+ @server.stats(:bytes_read), # total bytes read
161
+ @server.stats(:bytes_written), # total bytes written
162
+ 0, # limit_maxbytes
163
+ queue_stats
164
+ end
165
+
166
+ def queue_stats
167
+ @queue_collection.queues.inject("") do |m,(k,v)|
168
+ m + sprintf(QUEUE_STATS_RESPONSE,
169
+ k, v.length,
170
+ k, v.total_items,
171
+ k, v.logsize,
172
+ k, @expiry_stats[k])
173
+ end
174
+ end
175
+
176
+ def logger
177
+ @server.logger
178
+ end
179
+
180
+ end
181
+ end