timcharper-beanstalk-client 1.0.3

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,7 @@
1
+ = README
2
+
3
+ For more detail on what many of the commands do, see the beanstalkd protocol documentation:
4
+
5
+ http://github.com/kr/beanstalkd/blob/master/doc/protocol.txt?raw=true
6
+
7
+
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,428 @@
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
+ pri = pri.to_i
54
+ delay = delay.to_i
55
+ ttr = ttr.to_i
56
+ body = body.to_s # Make sure that body.size gives a useful number
57
+ interact("put #{pri} #{delay} #{ttr} #{body.size}\r\n#{body}\r\n",
58
+ %w(INSERTED BURIED))[0].to_i
59
+ end
60
+
61
+ def yput(obj, pri=65536, delay=0, ttr=120)
62
+ put(YAML.dump(obj), pri, delay, ttr)
63
+ end
64
+
65
+ def peek_job(id)
66
+ interact("peek #{id}\r\n", :job)
67
+ end
68
+
69
+ def peek_ready()
70
+ interact("peek-ready\r\n", :job)
71
+ end
72
+
73
+ def peek_delayed()
74
+ interact("peek-delayed\r\n", :job)
75
+ end
76
+
77
+ def peek_buried()
78
+ interact("peek-buried\r\n", :job)
79
+ end
80
+
81
+ def reserve(timeout=nil)
82
+ raise WaitingForJobError if @waiting
83
+ if timeout.nil?
84
+ @socket.write("reserve\r\n")
85
+ else
86
+ @socket.write("reserve-with-timeout #{timeout}\r\n")
87
+ end
88
+
89
+ begin
90
+ @waiting = true
91
+ # Give the user a chance to select on multiple fds.
92
+ Beanstalk.select.call([@socket]) if Beanstalk.select
93
+ rescue WaitingForJobError
94
+ # just continue
95
+ ensure
96
+ @waiting = false
97
+ end
98
+
99
+ Job.new(self, *read_job('RESERVED'))
100
+ end
101
+
102
+ def delete(id)
103
+ interact("delete #{id}\r\n", %w(DELETED))
104
+ :ok
105
+ end
106
+
107
+ def release(id, pri, delay)
108
+ id = id.to_i
109
+ pri = pri.to_i
110
+ delay = delay.to_i
111
+ interact("release #{id} #{pri} #{delay}\r\n", %w(RELEASED))
112
+ :ok
113
+ end
114
+
115
+ def bury(id, pri)
116
+ interact("bury #{id} #{pri}\r\n", %w(BURIED))
117
+ :ok
118
+ end
119
+
120
+ def touch(id)
121
+ interact("touch #{id}\r\n", %w(TOUCHED))
122
+ :ok
123
+ end
124
+
125
+ def kick(n)
126
+ interact("kick #{n}\r\n", %w(KICKED))[0].to_i
127
+ end
128
+
129
+ def use(tube)
130
+ return tube if tube == @last_used
131
+ @last_used = interact("use #{tube}\r\n", %w(USING))[0]
132
+ rescue BadFormatError
133
+ raise InvalidTubeName.new(tube)
134
+ end
135
+
136
+ def watch(tube)
137
+ return @watch_list.size if @watch_list.include?(tube)
138
+ r = interact("watch #{tube}\r\n", %w(WATCHING))[0].to_i
139
+ @watch_list += [tube]
140
+ return r
141
+ rescue BadFormatError
142
+ raise InvalidTubeName.new(tube)
143
+ end
144
+
145
+ def ignore(tube)
146
+ return @watch_list.size if !@watch_list.include?(tube)
147
+ r = interact("ignore #{tube}\r\n", %w(WATCHING))[0].to_i
148
+ @watch_list -= [tube]
149
+ return r
150
+ end
151
+
152
+ def stats()
153
+ interact("stats\r\n", :yaml)
154
+ end
155
+
156
+ def job_stats(id)
157
+ interact("stats-job #{id}\r\n", :yaml)
158
+ end
159
+
160
+ def stats_tube(tube)
161
+ interact("stats-tube #{tube}\r\n", :yaml)
162
+ end
163
+
164
+ def list_tubes()
165
+ interact("list-tubes\r\n", :yaml)
166
+ end
167
+
168
+ def list_tube_used()
169
+ interact("list-tube-used\r\n", %w(USING))[0]
170
+ end
171
+
172
+ def list_tubes_watched(cached=false)
173
+ return @watch_list if cached
174
+ @watch_list = interact("list-tubes-watched\r\n", :yaml)
175
+ end
176
+
177
+ private
178
+
179
+ def interact(cmd, rfmt)
180
+ raise WaitingForJobError if @waiting
181
+ @socket.write(cmd)
182
+ return read_yaml('OK') if rfmt == :yaml
183
+ return found_job if rfmt == :job
184
+ check_resp(*rfmt)
185
+ end
186
+
187
+ def get_resp()
188
+ r = @socket.gets("\r\n")
189
+ raise EOFError if r == nil
190
+ r[0...-2]
191
+ end
192
+
193
+ def check_resp(*words)
194
+ r = get_resp()
195
+ rword, *vals = r.split(/\s+/)
196
+ if (words.size > 0) and !words.include?(rword)
197
+ raise UnexpectedResponse.classify(rword, r)
198
+ end
199
+ vals
200
+ end
201
+
202
+ def found_job()
203
+ Job.new(self, *read_job('FOUND'))
204
+ rescue NotFoundError
205
+ nil
206
+ end
207
+
208
+ def read_job(word)
209
+ id, bytes = check_resp(word).map{|s| s.to_i}
210
+ body = read_bytes(bytes)
211
+ raise 'bad trailer' if read_bytes(2) != "\r\n"
212
+ [id, body, word == 'RESERVED']
213
+ end
214
+
215
+ def read_yaml(word)
216
+ bytes_s, = check_resp(word)
217
+ yaml = read_bytes(bytes_s.to_i)
218
+ raise 'bad trailer' if read_bytes(2) != "\r\n"
219
+ YAML::load(yaml)
220
+ end
221
+
222
+ def read_bytes(n)
223
+ str = @socket.read(n)
224
+ raise EOFError, 'End of file reached' if str == nil
225
+ raise EOFError, 'End of file reached' if str.size < n
226
+ str
227
+ end
228
+ end
229
+
230
+ class Pool
231
+ attr_accessor :last_conn
232
+
233
+ def initialize(addrs, default_tube=nil)
234
+ @addrs = addrs
235
+ @watch_list = ['default']
236
+ @default_tube=default_tube
237
+ @watch_list = [default_tube] if default_tube
238
+ connect()
239
+ end
240
+
241
+ def connect()
242
+ @connections ||= {}
243
+ @addrs.each do |addr|
244
+ begin
245
+ if !@connections.include?(addr)
246
+ @connections[addr] = Connection.new(addr, @default_tube)
247
+ prev_watched = @connections[addr].list_tubes_watched()
248
+ to_ignore = prev_watched - @watch_list
249
+ @watch_list.each{|tube| @connections[addr].watch(tube)}
250
+ to_ignore.each{|tube| @connections[addr].ignore(tube)}
251
+ end
252
+ rescue Exception => ex
253
+ puts "#{ex.class}: #{ex}"
254
+ #puts begin ex.fixed_backtrace rescue ex.backtrace end
255
+ end
256
+ end
257
+ @connections.size
258
+ end
259
+
260
+ def open_connections()
261
+ @connections.values()
262
+ end
263
+
264
+ def last_server
265
+ @last_conn.addr
266
+ end
267
+
268
+ # Put a job on the queue.
269
+ #
270
+ # == Parameters:
271
+ #
272
+ # * <tt>body</tt>: the payload of the job (use Beanstalk::Pool#yput / Beanstalk::Job#ybody to automatically serialize your payload with YAML)
273
+ # * <tt>pri</tt>: priority. Default 65536 (higher numbers are higher priority)
274
+ # * <tt>delay</tt>: how long to wait until making the job available for reservation
275
+ # * <tt>ttr</tt>: time in seconds for the job to reappear on the queue (if beanstalk doesn't hear from a consumer within this time, assume the consumer died and make the job available for someone else to process). Default 120 seconds.
276
+ def put(body, pri=65536, delay=0, ttr=120)
277
+ send_to_rand_conn(:put, body, pri, delay, ttr)
278
+ end
279
+
280
+ # Like put, but serialize the object with YAML.
281
+ def yput(obj, pri=65536, delay=0, ttr=120)
282
+ send_to_rand_conn(:yput, obj, pri, delay, ttr)
283
+ end
284
+
285
+ # Reserve a job from the queue.
286
+ #
287
+ # == Parameters
288
+ #
289
+ # * <tt>timeout</tt> - Time (in seconds) to wait for a job to be available. If nil, wait indefinitely.
290
+ def reserve(timeout=nil)
291
+ send_to_rand_conn(:reserve, timeout)
292
+ end
293
+
294
+ def use(tube)
295
+ send_to_all_conns(:use, tube)
296
+ end
297
+
298
+ def watch(tube)
299
+ r = send_to_all_conns(:watch, tube)
300
+ @watch_list = send_to_rand_conn(:list_tubes_watched, true)
301
+ return r
302
+ end
303
+
304
+ def ignore(tube)
305
+ r = send_to_all_conns(:ignore, tube)
306
+ @watch_list = send_to_rand_conn(:list_tubes_watched, true)
307
+ return r
308
+ end
309
+
310
+ def raw_stats()
311
+ send_to_all_conns(:stats)
312
+ end
313
+
314
+ def stats()
315
+ sum_hashes(raw_stats.values)
316
+ end
317
+
318
+ def raw_stats_tube(tube)
319
+ send_to_all_conns(:stats_tube, tube)
320
+ end
321
+
322
+ def stats_tube(tube)
323
+ sum_hashes(raw_stats_tube(tube).values)
324
+ end
325
+
326
+ def list_tubes()
327
+ send_to_all_conns(:list_tubes)
328
+ end
329
+
330
+ def list_tube_used()
331
+ send_to_all_conns(:list_tube_used)
332
+ end
333
+
334
+ def list_tubes_watched(*args)
335
+ send_to_all_conns(:list_tubes_watched, *args)
336
+ end
337
+
338
+ def remove(conn)
339
+ @connections.delete(conn.addr)
340
+ end
341
+
342
+ # Close all open connections for this pool
343
+ def close
344
+ while @connections.size > 0
345
+ addr = @connections.keys.last
346
+ conn = @connections[addr]
347
+ @connections.delete(addr)
348
+ conn.close
349
+ end
350
+ end
351
+
352
+ def peek_ready()
353
+ send_to_each_conn_first_res(:peek_ready)
354
+ end
355
+
356
+ def peek_delayed()
357
+ send_to_each_conn_first_res(:peek_delayed)
358
+ end
359
+
360
+ def peek_buried()
361
+ send_to_each_conn_first_res(:peek_buried)
362
+ end
363
+
364
+ def peek_job(id)
365
+ make_hash(send_to_all_conns(:peek_job, id))
366
+ end
367
+
368
+ private
369
+
370
+ def call_wrap(c, *args)
371
+ self.last_conn = c
372
+ c.send(*args)
373
+ rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, UnexpectedResponse => ex
374
+ self.remove(c)
375
+ raise ex
376
+ end
377
+
378
+ def retry_wrap(*args)
379
+ yield
380
+ rescue DrainingError
381
+ # Don't reconnect -- we're not interested in this server
382
+ retry
383
+ rescue EOFError, Errno::ECONNRESET, Errno::EPIPE
384
+ connect()
385
+ retry
386
+ end
387
+
388
+ def send_to_each_conn_first_res(*args)
389
+ connect()
390
+ retry_wrap{open_connections.inject(nil) {|r,c| r or call_wrap(c, *args)}}
391
+ end
392
+
393
+ def send_to_rand_conn(*args)
394
+ connect()
395
+ retry_wrap{call_wrap(pick_connection, *args)}
396
+ end
397
+
398
+ def send_to_all_conns(*args)
399
+ connect()
400
+ retry_wrap{compact_hash(map_hash(@connections){|c| call_wrap(c, *args)})}
401
+ end
402
+
403
+ def pick_connection()
404
+ open_connections[rand(open_connections.size)] or raise NotConnected
405
+ end
406
+
407
+ def make_hash(pairs)
408
+ Hash[*pairs.inject([]){|a,b|a+b}]
409
+ end
410
+
411
+ def map_hash(h)
412
+ make_hash(h.map{|k,v| [k, yield(v)]})
413
+ end
414
+
415
+ def compact_hash(hash)
416
+ hash.reject{|k,v| v == nil}
417
+ end
418
+
419
+ def sum_hashes(hs)
420
+ hs.inject({}){|a,b| a.merge(b) {|k,o,n| combine_stats(k, o, n)}}
421
+ end
422
+
423
+ DONT_ADD = Set['name', 'version', 'pid']
424
+ def combine_stats(k, a, b)
425
+ DONT_ADD.include?(k) ? Set[a] + Set[b] : a + b
426
+ end
427
+ end
428
+ end