matthewrudy-rudeq 0.1 → 2.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/README CHANGED
@@ -13,7 +13,6 @@ RudeQ
13
13
  INSTALL
14
14
  ============
15
15
  This plugin requires Rails 2.* currently, and has only been tested on MySQL.
16
- It definitely does not work on SQLite (as it requires :limit and :order options for a SQL UPDATE command)
17
16
 
18
17
  On rails 2.1 you can install straight from github:
19
18
  ruby script/plugin install git://github.com/matthewrudy/rudeq.git
@@ -44,6 +43,9 @@ After you've installed it just run
44
43
 
45
44
  RudeQueue.set(:queue_name, RandomObject)
46
45
  RudeQueue.get(:queue_name)
46
+ RudeQueue.fetch(:queue_name) do |data|
47
+ process(data)
48
+ end
47
49
 
48
50
  And, to keep the queue running fast,
49
51
  set up a cron job to run
data/lib/rude_q.rb CHANGED
@@ -1,8 +1,16 @@
1
1
  # RudeQ
2
2
  require 'digest/sha1'
3
+
4
+ # simply doing;
5
+ # class RudeQueue < ActiveRecord::Base
6
+ # include RudeQ
7
+ # end
8
+ # will include RudeQ::ClassMethods
9
+ # :get
10
+ # :set
3
11
  module RudeQ
4
12
 
5
- def self.included(mod)
13
+ def self.included(mod) # :nodoc:
6
14
  mod.extend(ClassMethods)
7
15
  mod.serialize(:data)
8
16
  end
@@ -16,17 +24,17 @@ module RudeQ
16
24
  self.delete_all(["processed = ? AND updated_at < ?", true, expiry.to_i.ago])
17
25
  end
18
26
 
19
- # Add any serialize-able *data* to the queue *queue_name* (strings and symbols are treated the same)
27
+ # Add any serialize-able +data+ to the queue +queue_name+ (strings and symbols are treated the same)
20
28
  #
21
29
  # RudeQueue.set(:sausage_queue, Sausage.new(:sauce => "yummy"))
22
30
  # RudeQueue.set("sausage_queue", Sausage.new(:other => true))
23
31
  #
24
- # RudeQueue.get("sausage_queue")
25
- # -> *yummy sausage*
26
- # RudeQueue.get(:sausage_queue)
27
- # -> *other_sausage*
28
- # RudeQueue.get(:sausage_queue)
29
- # -> nil
32
+ # >> RudeQueue.get("sausage_queue")
33
+ # -> *yummy sausage*
34
+ # >> RudeQueue.get(:sausage_queue)
35
+ # -> *other_sausage*
36
+ # >> RudeQueue.get(:sausage_queue)
37
+ # -> nil
30
38
  def set(queue_name, data)
31
39
  queue_name = sanitize_queue_name(queue_name)
32
40
  self.create!(:queue_name => queue_name, :data => data)
@@ -34,51 +42,151 @@ module RudeQ
34
42
  end
35
43
 
36
44
  # Grab the first item from the queue *queue_name* (strings and symbols are treated the same)
37
- # - it should always come out the same as it went in
38
- # - they should always come out in the same order they went in
39
- # - it will return a nil if there is no unprocessed entry in the queue
45
+ # - it should always come out the same as it went in
46
+ # - they should always come out in the same order they went in
47
+ # - it will return a nil if there is no unprocessed entry in the queue
40
48
  #
41
- # RudeQueue.get(21)
42
- # -> {:a => "hash"}
43
- # RudeQueue.get(:a_symbol)
44
- # -> 255
45
- # RudeQueue.get("a string")
46
- # -> nil
49
+ # >> RudeQueue.get(21)
50
+ # -> {:a => "hash"}
51
+ # >> RudeQueue.get(:a_symbol)
52
+ # -> 255
53
+ # >> RudeQueue.get("a string")
54
+ # -> nil
47
55
  def get(queue_name)
48
56
  qname = sanitize_queue_name(queue_name)
49
- token = get_unique_token
50
-
51
- self.update_all(["token = ?", token], ["queue_name = ? AND processed = ? AND token IS NULL", qname, false], :limit => 1, :order => "id ASC")
52
- queued = self.find_by_queue_name_and_token_and_processed(qname, token, false)
53
- if queued
54
- queued.update_attribute(:processed, true)
55
- return queued.data
56
- else
57
- return nil # in line with Starling
57
+
58
+ fetch_with_lock(qname) do |record|
59
+ if record
60
+ processed!(record)
61
+ return record.data
62
+ else
63
+ return nil # Starling waits indefinitely for a corresponding queue item
64
+ end
65
+ end
66
+ end
67
+
68
+ # Grab the first item from the queue, and execute the supplied block if there is one
69
+ # - it will return the value of the block
70
+ #
71
+ # >> RudeQueue.fetch(:my_queue) do |data|
72
+ # >> Monster.devour(data)
73
+ # >> end
74
+ # -> nil
75
+ #
76
+ # >> status = RudeQueue.fetch(:my_queue) do |data|
77
+ # >> process(data) # returns the value :update in this case
78
+ # >> end
79
+ # -> :update
80
+ # >> status
81
+ # -> :update
82
+ def fetch(queue_name, &block)
83
+ if data = get(queue_name)
84
+ return block.call(data)
58
85
  end
59
86
  end
60
-
61
- def get_unique_token # :nodoc:
62
87
 
63
- digest = Digest::SHA1.new
64
- digest << Time.now.to_s
65
- digest << Process.pid.to_s
66
- digest << Socket.gethostname
67
- digest << self.token_count!.to_s # multiple requests from the same pid in the same second get different token
88
+ # A snapshot count of unprocessed items for the given +queue_name+
89
+ def backlog(queue_name)
90
+ qname = sanitize_queue_name(queue_name)
91
+ self.count(:conditions => {:queue_name => qname, :processed => false})
92
+ end
68
93
 
69
- return digest.hexdigest
94
+ def fetch_with_lock(qname, &block) # :nodoc:
95
+ lock = case queue_options[:lock]
96
+ when :pessimistic : RudeQ::PessimisticLock
97
+ when :token : RudeQ::TokenLock
98
+ else
99
+ raise(ArgumentError, "bad queue_option for :lock - #{queue_options[:lock].inspect}")
100
+ end
101
+ lock.fetch_with_lock(self, qname, &block)
70
102
  end
71
103
 
72
- protected
104
+ # class method to make it more easily stubbed
105
+ def processed!(record) # :nodoc:
106
+ case queue_options[:processed]
107
+ when :set_flag
108
+ record.update_attribute(:processed, true)
109
+ when :destroy
110
+ record.destroy
111
+ else
112
+ raise(ArgumentError, "bad queue_option for :processed - #{queue_options[:processed].inspect}")
113
+ end
114
+ end
115
+ protected :processed!
73
116
 
74
- def token_count!
75
- @token_count ||= 0
76
- @token_count += 1
77
- return @token_count
117
+ # configure your RudeQ
118
+ # ==== :processed - what do we do after retrieving a queue item?
119
+ # * <tt>:set_flag</tt> - set the +processed+ flag to +true+ (keep data in the db) [*default*]
120
+ # * <tt>:destroy</tt> - destroy the processed item (keep our queue as lean as possible
121
+ #
122
+ # ==== :lock - what locking method should we use?
123
+ # * <tt>:pessimistic</tt> - RudeQ::PessimisticLock [*default*]
124
+ # * <tt>:token</tt> - RudeQ::TokenLock
125
+ def queue_options
126
+ @queue_options ||= {:processed => :set_flag, :lock => :pessimistic}
78
127
  end
128
+
129
+ private
79
130
 
80
- def sanitize_queue_name(queue_name)
131
+ def sanitize_queue_name(queue_name) # :nodoc:
81
132
  queue_name.to_s
82
133
  end
83
134
  end
84
- end
135
+
136
+ # uses standard ActiveRecord :lock => true
137
+ # this will invoke a lock on the particular queue
138
+ # eg. daemon1: RudeQueue.get(:abc)
139
+ # daemon2: RudeQueue.get(:abc) - will have to wait for daemon1 to finish
140
+ # daemon3: RudeQueue.get(:def) - will avoid the lock
141
+ module PessimisticLock
142
+ class << self
143
+
144
+ def fetch_with_lock(klass, qname) # :nodoc:
145
+ klass.transaction do
146
+ record = klass.find(:first,
147
+ :conditions => {:queue_name => qname, :processed => false},
148
+ :lock => true, :order => "id ASC", :limit => 1)
149
+
150
+ return yield(record)
151
+ end
152
+ end
153
+
154
+ end
155
+ end
156
+
157
+ # a crazy hack around database locking
158
+ # that I thought was a good idea
159
+ # turns out we can't make it use transactions properly
160
+ # without creating a whole table lock
161
+ # which misses the point
162
+ #
163
+ # also, it doesn't work on SQLite as it requires "UPDATE ... LIMIT 1 ORDER id ASC"
164
+ module TokenLock
165
+ class << self
166
+
167
+ def fetch_with_lock(klass, qname) # :nodoc:
168
+ token = get_unique_token
169
+ klass.update_all(["token = ?", token], ["queue_name = ? AND processed = ? AND token IS NULL", qname, false], :limit => 1, :order => "id ASC")
170
+ record = klass.find_by_queue_name_and_token_and_processed(qname, token, false)
171
+
172
+ return yield(record)
173
+ end
174
+
175
+ def token_count! # :nodoc:
176
+ @token_count ||= 0
177
+ @token_count += 1
178
+ return @token_count
179
+ end
180
+
181
+ def get_unique_token # :nodoc:
182
+ digest = Digest::SHA1.new
183
+ digest << Time.now.to_s
184
+ digest << Process.pid.to_s
185
+ digest << Socket.gethostname
186
+ digest << self.token_count!.to_s # multiple requests from the same pid in the same second get different token
187
+ return digest.hexdigest
188
+ end
189
+ end
190
+ end
191
+
192
+ end
@@ -1,3 +1,13 @@
1
1
  class ProcessQueue < ActiveRecord::Base
2
2
  include RudeQ
3
+
4
+ class << self
5
+ def processed_with_raise_hack!(*args)
6
+ processed_without_raise_hack!(*args)
7
+ raise RuntimeError if raise_on_processed # want to be able to raise afterwards to check transactions
8
+ end
9
+ alias_method_chain :processed!, :raise_hack
10
+ attr_accessor :raise_on_processed
11
+ end
12
+
3
13
  end
data/spec/rude_q_spec.rb CHANGED
@@ -1,15 +1,15 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper'
2
2
 
3
- describe ProcessQueue do
3
+ describe RudeQ::ClassMethods do # ProcessQueue extends ClassMethods
4
4
  before(:each) do
5
5
  ProcessQueue.delete_all
6
+ ProcessQueue.raise_on_processed = false
6
7
  create_some_noise
7
8
  end
8
9
 
9
10
  def create_some_noise
10
11
  ProcessQueue.create!(:queue_name => "doNT use this in Specs", :data => {:not => "to be messed with"})
11
12
  ProcessQueue.create!(:queue_name => "abcde", :data => {:same_as => "the specs but already processed"}, :processed => true)
12
- ProcessQueue.create!(:queue_name => "abcde", :data => {:same_as => "the specs but with token"}, :token => " unlikely ")
13
13
  end
14
14
 
15
15
  describe "get and set" do
@@ -31,6 +31,17 @@ describe ProcessQueue do
31
31
  ProcessQueue.set('abcde', hash)
32
32
  ProcessQueue.get('abcde').should == hash
33
33
  end
34
+ it "should work with integers" do
35
+ ProcessQueue.set('abcde', 7816327370)
36
+ ProcessQueue.get('abcde').should == 7816327370
37
+ end
38
+ it "unfortunately doesnt resolve booleans correctly" do
39
+ ProcessQueue.set('abcde', true)
40
+ ProcessQueue.get('abcde').should == 1
41
+
42
+ ProcessQueue.set('abcde', false)
43
+ ProcessQueue.get('abcde').should == 0
44
+ end
34
45
 
35
46
  it "should :get in the same order they are :set" do
36
47
  ProcessQueue.set('abcde', :first)
@@ -82,38 +93,125 @@ describe ProcessQueue do
82
93
  end
83
94
  end
84
95
 
85
- describe ".get" do
86
- it "should not return a processed item with the same token" do
87
- @token = "tokEEEannn"
88
- ProcessQueue.should_receive(:get_unique_token).exactly(3).times.and_return(@token)
89
- @existing = ProcessQueue.create!(:queue_name => 'abcde', :data => :old_data, :token => @token, :processed => true)
90
-
91
- ProcessQueue.get('abcde').should be(nil)
92
-
93
- ProcessQueue.set('abcde', :new_data)
94
- ProcessQueue.get('abcde').should == :new_data
95
- ProcessQueue.get('abcde').should be(nil)
96
+ describe ".get" do
97
+ it "should revert a record if something goes wrong before it finishes" do
98
+ ProcessQueue.raise_on_processed = true
99
+ ProcessQueue.set('abcde', :this_will_remain_unprocessed)
100
+
101
+ # confirm the object is in the db
102
+ record = ProcessQueue.find(:first, :order => "id DESC")
103
+ record.queue_name.should == 'abcde'
104
+ record.data.should == :this_will_remain_unprocessed
105
+ record.processed?.should == false
106
+ record.token.should == nil
107
+
108
+ lambda {ProcessQueue.get('abcde')}.should raise_error(RuntimeError)
109
+
110
+ record.reload
111
+ record.queue_name.should == 'abcde'
112
+ record.data.should == :this_will_remain_unprocessed
113
+ record.processed?.should == false
114
+ record.token.should == nil
96
115
  end
97
116
  end
117
+
118
+ describe "fetch" do
119
+ describe "with data" do
120
+
121
+ before(:each) do
122
+ ProcessQueue.set(:fetch_queue, "some data")
123
+ end
98
124
 
99
- describe ".get_unique_token" do
100
- it "should create a unique token" do
101
- lots_of_tokens = Array.new(50) do
102
- ProcessQueue.get_unique_token
125
+ it "should return the value of the block" do
126
+ rtn = ProcessQueue.fetch(:fetch_queue) do |data|
127
+ data.should == "some data"
128
+ :the_return
129
+ end
130
+ rtn.should == :the_return
103
131
  end
104
- lots_of_tokens.uniq.should == lots_of_tokens
132
+
133
+ it "should execute the block with the data" do
134
+ self.should_receive(:something)
135
+ ProcessQueue.fetch(:fetch_queue) do |data|
136
+ self.something
137
+ data.should == "some data"
138
+ end
139
+ end
140
+
105
141
  end
106
-
107
- it "should create a unique token even if time stands still" do
108
- time_now = Time.now
109
- Time.should_receive(:now).at_least(50).times.and_return(time_now)
110
- lots_of_tokens = Array.new(50) do
111
- ProcessQueue.get_unique_token
142
+
143
+ describe "without data" do
144
+
145
+ it "should not execute the block" do
146
+ self.should_not_receive(:something)
147
+ ProcessQueue.fetch(:fetch_queue) do |data|
148
+ raise(Exception, "this should never get here")
149
+ end
150
+ end
151
+
152
+ it "should return nil" do
153
+ rtn = ProcessQueue.fetch(:fetch_queue) do |data|
154
+ raise(Exception, "again this shouldnt happen")
155
+ end
156
+ rtn.should be_nil
157
+ end
158
+
159
+ end
160
+ end
161
+
162
+ describe "queue_options" do
163
+ describe :processed do
164
+ describe "set to :destroy" do
165
+ before(:each) do
166
+ @old_processed = ProcessQueue.queue_options[:processed]
167
+ ProcessQueue.queue_options[:processed] = :destroy
168
+ end
169
+ after(:each) do
170
+ ProcessQueue.queue_options[:processed] = @old_processed
171
+ end
172
+ it "should delete processed items" do
173
+ count = ProcessQueue.count
174
+
175
+ ProcessQueue.set(:abcde, "some value")
176
+ ProcessQueue.count.should == (count + 1)
177
+
178
+ ProcessQueue.get(:abcde).should == "some value"
179
+ ProcessQueue.count.should == count
180
+ end
181
+ end
182
+ describe "set to something crazy" do
183
+ before(:each) do
184
+ @old_processed = ProcessQueue.queue_options[:processed]
185
+ ProcessQueue.queue_options[:processed] = :something_crazy
186
+ end
187
+ after(:each) do
188
+ ProcessQueue.queue_options[:processed] = @old_processed
189
+ end
190
+ it "should raise an exception" do
191
+ ProcessQueue.set(:abcde, "some value")
192
+ lambda {ProcessQueue.get(:abcde)}.should raise_error(ArgumentError)
193
+ end
112
194
  end
113
- lots_of_tokens.uniq.should == lots_of_tokens
114
195
  end
115
196
  end
116
197
 
198
+ describe ".backlog" do
199
+ it "should count the unprocessed items for the provided queue_name" do
200
+ ProcessQueue.backlog(:abcde).should == 0
201
+
202
+ ProcessQueue.set(:abcde, "a value")
203
+ ProcessQueue.backlog(:abcde).should == 1
204
+
205
+ ProcessQueue.set(:something_else, "another value")
206
+ 3.times { ProcessQueue.set(:abcde, :add_three_more)}
207
+
208
+ ProcessQueue.backlog(:abcde).should == 4
209
+
210
+ ProcessQueue.get(:abcde).should == "a value"
211
+ ProcessQueue.backlog(:abcde).should == 3
212
+ end
213
+ end
214
+
117
215
  describe ".cleanup!" do
118
216
  it "should use :delete_all" do
119
217
  ProcessQueue.should_receive(:delete_all) # not :destroy_all
@@ -223,3 +321,40 @@ describe ProcessQueue do
223
321
  end
224
322
  end
225
323
  end
324
+
325
+ describe RudeQ::TokenLock do
326
+
327
+ describe ".get_unique_token" do
328
+ it "should create a unique token" do
329
+ lots_of_tokens = Array.new(50) do
330
+ RudeQ::TokenLock.get_unique_token
331
+ end
332
+ lots_of_tokens.uniq.should == lots_of_tokens
333
+ end
334
+
335
+ it "should create a unique token even if time stands still" do
336
+ time_now = Time.now
337
+ Time.should_receive(:now).at_least(50).times.and_return(time_now)
338
+ lots_of_tokens = Array.new(50) do
339
+ RudeQ::TokenLock.get_unique_token
340
+ end
341
+ lots_of_tokens.uniq.should == lots_of_tokens
342
+ end
343
+ end
344
+
345
+ # it "should not return a processed item with the same token" do
346
+ # @token = "tokEEEannn"
347
+ #
348
+ # RudeQ::TokenLock.should respond_to(:get_unique_token) # ensure our stub is safe
349
+ # RudeQ::TokenLock.should_receive(:get_unique_token).exactly(3).times.and_return(@token)
350
+ #
351
+ # @existing = ProcessQueue.create!(:queue_name => 'abcde', :data => :old_data, :token => @token, :processed => true)
352
+ #
353
+ # ProcessQueue.get('abcde').should be(nil)
354
+ #
355
+ # ProcessQueue.set('abcde', :new_data)
356
+ # ProcessQueue.get('abcde').should == :new_data
357
+ # ProcessQueue.get('abcde').should be(nil)
358
+ # end
359
+
360
+ end
data/spec/spec.opts CHANGED
@@ -1,6 +1,6 @@
1
1
  --colour
2
2
  --format
3
- progress
3
+ specdoc
4
4
  --loadby
5
5
  mtime
6
6
  --reverse
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: matthewrudy-rudeq
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.1"
4
+ version: "2.0"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Rudy Jacobs
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-06-21 00:00:00 -07:00
12
+ date: 2009-02-12 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -21,7 +21,7 @@ dependencies:
21
21
  - !ruby/object:Gem::Version
22
22
  version: "0"
23
23
  version:
24
- description: A simple DB queueing library built on top of ActiveRecord, and designed for use with MySQL.
24
+ description: A simple DB queueing library built on top of ActiveRecord.
25
25
  email: MatthewRudyJacobs@gmail.com
26
26
  executables: []
27
27
 
@@ -62,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
62
  requirements: []
63
63
 
64
64
  rubyforge_project:
65
- rubygems_version: 1.0.1
65
+ rubygems_version: 1.2.0
66
66
  signing_key:
67
67
  specification_version: 2
68
68
  summary: ActiveRecord-based DB-queue