kr-beanstalk-client 1.0.0

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.
data/Manifest.txt ADDED
@@ -0,0 +1,27 @@
1
+ History.txt
2
+ License.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ config/hoe.rb
7
+ config/requirements.rb
8
+ lib/beanstalk-client.rb
9
+ lib/beanstalk-client/connection.rb
10
+ lib/beanstalk-client/errors.rb
11
+ lib/beanstalk-client/job.rb
12
+ lib/beanstalk-client/version.rb
13
+ log/debug.log
14
+ script/destroy
15
+ script/generate
16
+ script/txt2html
17
+ setup.rb
18
+ tasks/deployment.rake
19
+ tasks/environment.rake
20
+ tasks/website.rake
21
+ test/test_beanstalk-client.rb
22
+ test/test_helper.rb
23
+ website/index.html
24
+ website/index.txt
25
+ website/javascripts/rounded_corners_lite.inc.js
26
+ website/stylesheets/screen.css
27
+ website/template.rhtml
data/README.txt ADDED
@@ -0,0 +1 @@
1
+ README
data/Rakefile ADDED
@@ -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 }
data/config/hoe.rb ADDED
@@ -0,0 +1,72 @@
1
+ require 'beanstalk-client/version'
2
+
3
+ AUTHOR = 'Keith Rarick' # can also be an array of Authors
4
+ EMAIL = 'kr@causes.com'
5
+ DESCRIPTION = 'Ruby client library for the Beanstalk protocol'
6
+ GEM_NAME = 'beanstalk-client' # what ppl will type to install your gem
7
+ RUBYFORGE_PROJECT = 'beanstalk' # 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 = Beanstalk::VERSION::STRING + (REV ? ".#{REV}" : "")
35
+ RDOC_OPTS = ['--quiet', '--title', 'beanstalk-client 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
+ PATH = RUBYFORGE_PROJECT
71
+ hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
72
+ hoe.rsync_args = '-av --delete --ignore-errors'
@@ -0,0 +1,17 @@
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]))
16
+
17
+ require 'beanstalk-client'
@@ -0,0 +1,26 @@
1
+ # beanstalk-client.rb - client library for beanstalk
2
+
3
+ # Copyright (C) 2007 Philotic Inc.
4
+
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ $:.unshift(File.dirname(__FILE__))
19
+
20
+ module Beanstalk
21
+ extend self
22
+
23
+ attr_accessor :select
24
+ end
25
+
26
+ require 'beanstalk-client/connection'
@@ -0,0 +1,393 @@
1
+ # beanstalk-client/connection.rb - client library for beanstalk
2
+
3
+ # Copyright (C) 2007 Philotic Inc.
4
+
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'socket'
19
+ require 'fcntl'
20
+ require 'yaml'
21
+ require 'set'
22
+ require 'beanstalk-client/errors'
23
+ require 'beanstalk-client/job'
24
+
25
+ module Beanstalk
26
+ class Connection
27
+ attr_reader :addr
28
+
29
+ def initialize(addr, default_tube=nil)
30
+ @waiting = false
31
+ @addr = addr
32
+ connect
33
+ @last_used = 'default'
34
+ @watch_list = [@last_used]
35
+ self.use(default_tube) if default_tube
36
+ self.watch(default_tube) if default_tube
37
+ end
38
+
39
+ def connect
40
+ host, port = addr.split(':')
41
+ @socket = TCPSocket.new(host, port.to_i)
42
+
43
+ # Don't leak fds when we exec.
44
+ @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
45
+ end
46
+
47
+ def close
48
+ @socket.close
49
+ @socket = nil
50
+ end
51
+
52
+ def put(body, pri=65536, delay=0, ttr=120)
53
+ interact("put #{pri} #{delay} #{ttr} #{body.size}\r\n#{body}\r\n",
54
+ %w(INSERTED BURIED))[0].to_i
55
+ end
56
+
57
+ def yput(obj, pri=65536, delay=0, ttr=120)
58
+ put(YAML.dump(obj), pri, delay, ttr)
59
+ end
60
+
61
+ def peek_job(id)
62
+ interact("peek #{id}\r\n", :job)
63
+ end
64
+
65
+ def peek_ready()
66
+ interact("peek-ready\r\n", :job)
67
+ end
68
+
69
+ def peek_delayed()
70
+ interact("peek-delayed\r\n", :job)
71
+ end
72
+
73
+ def peek_buried()
74
+ interact("peek-buried\r\n", :job)
75
+ end
76
+
77
+ def reserve()
78
+ raise WaitingForJobError if @waiting
79
+ @socket.write("reserve\r\n")
80
+
81
+ begin
82
+ @waiting = true
83
+ # Give the user a chance to select on multiple fds.
84
+ Beanstalk.select.call([@socket]) if Beanstalk.select
85
+ rescue WaitingForJobError
86
+ # just continue
87
+ ensure
88
+ @waiting = false
89
+ end
90
+
91
+ Job.new(self, *read_job('RESERVED'))
92
+ end
93
+
94
+ def delete(id)
95
+ interact("delete #{id}\r\n", %w(DELETED))
96
+ :ok
97
+ end
98
+
99
+ def release(id, pri, delay)
100
+ interact("release #{id} #{pri} #{delay}\r\n", %w(RELEASED))
101
+ :ok
102
+ end
103
+
104
+ def bury(id, pri)
105
+ interact("bury #{id} #{pri}\r\n", %w(BURIED))
106
+ :ok
107
+ end
108
+
109
+ def use(tube)
110
+ return tube if tube == @last_used
111
+ @last_used = interact("use #{tube}\r\n", %w(USING))[0]
112
+ rescue BadFormatError
113
+ raise InvalidTubeName.new(tube)
114
+ end
115
+
116
+ def watch(tube)
117
+ return @watch_list.size if @watch_list.include?(tube)
118
+ r = interact("watch #{tube}\r\n", %w(WATCHING))[0].to_i
119
+ @watch_list += [tube]
120
+ return r
121
+ rescue BadFormatError
122
+ raise InvalidTubeName.new(tube)
123
+ end
124
+
125
+ def ignore(tube)
126
+ return @watch_list.size if !@watch_list.include?(tube)
127
+ r = interact("ignore #{tube}\r\n", %w(WATCHING))[0].to_i
128
+ @watch_list -= [tube]
129
+ return r
130
+ end
131
+
132
+ def stats()
133
+ interact("stats\r\n", :yaml)
134
+ end
135
+
136
+ def job_stats(id)
137
+ interact("stats-job #{id}\r\n", :yaml)
138
+ end
139
+
140
+ def stats_tube(tube)
141
+ interact("stats-tube #{tube}\r\n", :yaml)
142
+ end
143
+
144
+ def list_tubes()
145
+ interact("list-tubes\r\n", :yaml)
146
+ end
147
+
148
+ def list_tube_used()
149
+ interact("list-tube-used\r\n", %w(USING))[0]
150
+ end
151
+
152
+ def list_tubes_watched(cached=false)
153
+ return @watch_list if cached
154
+ @watch_list = interact("list-tubes-watched\r\n", :yaml)
155
+ end
156
+
157
+ private
158
+
159
+ def interact(cmd, rfmt)
160
+ raise WaitingForJobError if @waiting
161
+ @socket.write(cmd)
162
+ return read_yaml('OK') if rfmt == :yaml
163
+ return found_job if rfmt == :job
164
+ check_resp(*rfmt)
165
+ end
166
+
167
+ def get_resp()
168
+ r = @socket.gets("\r\n")
169
+ raise EOFError if r == nil
170
+ r[0...-2]
171
+ end
172
+
173
+ def check_resp(*words)
174
+ r = get_resp()
175
+ rword, *vals = r.split(/\s+/)
176
+ if (words.size > 0) and !words.include?(rword)
177
+ raise UnexpectedResponse.classify(rword, r)
178
+ end
179
+ vals
180
+ end
181
+
182
+ def found_job()
183
+ Job.new(self, *read_job('FOUND'))
184
+ rescue NotFoundError
185
+ nil
186
+ end
187
+
188
+ def read_job(word)
189
+ id, bytes = check_resp(word).map{|s| s.to_i}
190
+ body = read_bytes(bytes)
191
+ raise 'bad trailer' if read_bytes(2) != "\r\n"
192
+ [id, body]
193
+ end
194
+
195
+ def read_yaml(word)
196
+ bytes_s, = check_resp(word)
197
+ yaml = read_bytes(bytes_s.to_i)
198
+ raise 'bad trailer' if read_bytes(2) != "\r\n"
199
+ YAML::load(yaml)
200
+ end
201
+
202
+ def read_bytes(n)
203
+ str = @socket.read(n)
204
+ raise EOFError, 'End of file reached' if str == nil
205
+ raise EOFError, 'End of file reached' if str.size < n
206
+ str
207
+ end
208
+ end
209
+
210
+ class Pool
211
+ attr_accessor :last_conn
212
+
213
+ def initialize(addrs, default_tube=nil)
214
+ @addrs = addrs
215
+ @watch_list = ['default']
216
+ @default_tube=default_tube
217
+ @watch_list = [default_tube] if default_tube
218
+ connect()
219
+ end
220
+
221
+ def connect()
222
+ @connections ||= {}
223
+ @addrs.each do |addr|
224
+ begin
225
+ if !@connections.include?(addr)
226
+ @connections[addr] = Connection.new(addr, @default_tube)
227
+ prev_watched = @connections[addr].list_tubes_watched()
228
+ to_ignore = prev_watched - @watch_list
229
+ @watch_list.each{|tube| @connections[addr].watch(tube)}
230
+ to_ignore.each{|tube| @connections[addr].ignore(tube)}
231
+ end
232
+ rescue Exception => ex
233
+ puts "#{ex.class}: #{ex}"
234
+ #puts begin ex.fixed_backtrace rescue ex.backtrace end
235
+ end
236
+ end
237
+ @connections.size
238
+ end
239
+
240
+ def open_connections()
241
+ @connections.values()
242
+ end
243
+
244
+ def last_server
245
+ @last_conn.addr
246
+ end
247
+
248
+ def put(body, pri=65536, delay=0, ttr=120)
249
+ send_to_rand_conn(:put, body, pri, delay, ttr)
250
+ end
251
+
252
+ def yput(obj, pri=65536, delay=0, ttr=120)
253
+ send_to_rand_conn(:yput, obj, pri, delay, ttr)
254
+ end
255
+
256
+ def reserve()
257
+ send_to_rand_conn(:reserve)
258
+ end
259
+
260
+ def use(tube)
261
+ send_to_all_conns(:use, tube)
262
+ end
263
+
264
+ def watch(tube)
265
+ r = send_to_all_conns(:watch, tube)
266
+ @watch_list = send_to_rand_conn(:list_tubes_watched, true)
267
+ return r
268
+ end
269
+
270
+ def ignore(tube)
271
+ r = send_to_all_conns(:ignore, tube)
272
+ @watch_list = send_to_rand_conn(:list_tubes_watched, true)
273
+ return r
274
+ end
275
+
276
+ def raw_stats()
277
+ send_to_all_conns(:stats)
278
+ end
279
+
280
+ def stats()
281
+ sum_hashes(raw_stats.values)
282
+ end
283
+
284
+ def raw_stats_tube(tube)
285
+ send_to_all_conns(:stats_tube, tube)
286
+ end
287
+
288
+ def stats_tube(tube)
289
+ sum_hashes(raw_stats_tube(tube).values)
290
+ end
291
+
292
+ def list_tubes()
293
+ send_to_all_conns(:list_tubes)
294
+ end
295
+
296
+ def list_tube_used()
297
+ send_to_all_conns(:list_tube_used)
298
+ end
299
+
300
+ def list_tubes_watched(*args)
301
+ send_to_all_conns(:list_tubes_watched, *args)
302
+ end
303
+
304
+ def remove(conn)
305
+ @connections.delete(conn.addr)
306
+ end
307
+
308
+ def close
309
+ while @connections.size > 0
310
+ addr = @connections.keys.last
311
+ conn = @connections[addr]
312
+ @connections.delete(addr)
313
+ conn.close
314
+ end
315
+ end
316
+
317
+ def peek_ready()
318
+ send_to_each_conn_first_res(:peek_ready)
319
+ end
320
+
321
+ def peek_delayed()
322
+ send_to_each_conn_first_res(:peek_delayed)
323
+ end
324
+
325
+ def peek_buried()
326
+ send_to_each_conn_first_res(:peek_buried)
327
+ end
328
+
329
+ def peek_job(id)
330
+ make_hash(send_to_all_conns(:peek_job, id))
331
+ end
332
+
333
+ private
334
+
335
+ def call_wrap(c, *args)
336
+ self.last_conn = c
337
+ c.send(*args)
338
+ rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, UnexpectedResponse => ex
339
+ self.remove(c)
340
+ raise ex
341
+ end
342
+
343
+ def retry_wrap(*args)
344
+ yield
345
+ rescue DrainingError
346
+ # Don't reconnect -- we're not interested in this server
347
+ retry
348
+ rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
349
+ connect()
350
+ retry
351
+ end
352
+
353
+ def send_to_each_conn_first_res(*args)
354
+ connect()
355
+ retry_wrap{open_connections.inject(nil) {|r,c| r or call_wrap(c, *args)}}
356
+ end
357
+
358
+ def send_to_rand_conn(*args)
359
+ connect()
360
+ retry_wrap{call_wrap(pick_connection, *args)}
361
+ end
362
+
363
+ def send_to_all_conns(*args)
364
+ connect()
365
+ retry_wrap{compact_hash(map_hash(@connections){|c| call_wrap(c, *args)})}
366
+ end
367
+
368
+ def pick_connection()
369
+ open_connections[rand(open_connections.size)] or raise NotConnected
370
+ end
371
+
372
+ def make_hash(pairs)
373
+ Hash[*pairs.inject([]){|a,b|a+b}]
374
+ end
375
+
376
+ def map_hash(h)
377
+ make_hash(h.map{|k,v| [k, yield(v)]})
378
+ end
379
+
380
+ def compact_hash(hash)
381
+ hash.reject{|k,v| v == nil}
382
+ end
383
+
384
+ def sum_hashes(hs)
385
+ hs.inject({}){|a,b| a.merge(b) {|k,o,n| combine_stats(k, o, n)}}
386
+ end
387
+
388
+ DONT_ADD = Set['name', 'version', 'pid']
389
+ def combine_stats(k, a, b)
390
+ DONT_ADD.include?(k) ? Set[a] + Set[b] : a + b
391
+ end
392
+ end
393
+ end